mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-06 20:42:52 +08:00
feat(download): 添加快手下载器并优化下载配置功能
- 新增快手下载器,支持快手视频下载 - 添加下载配置页面,可设置各平台Cookies - 优化后端接口,增加获取和更新Cookies的功能 - 前端新增Downloader组件和相关表单组件 - 更新路由配置,增加下载配置相关路由
This commit is contained in:
@@ -13,6 +13,8 @@ import StepBar from '@/pages/HomePage/components/StepBar.tsx'
|
||||
import Downloading from '@/components/Lottie/download.tsx'
|
||||
import Prompt from '@/pages/SettingPage/Prompt.tsx'
|
||||
import AboutPage from '@/pages/SettingPage/about.tsx'
|
||||
import Downloader from '@/pages/SettingPage/Downloader.tsx'
|
||||
import DownloaderForm from '@/components/Form/DownloaderForm/Form.tsx'
|
||||
function App() {
|
||||
useTaskPolling(3000) // 每 3 秒轮询一次
|
||||
const steps = [
|
||||
@@ -35,8 +37,11 @@ function App() {
|
||||
{/*<Route index element={<Navigate to="openai" replace />} />*/}
|
||||
<Route path=":id" element={<ProviderForm />} />
|
||||
</Route>
|
||||
{/*<Route path="transcriber" elment={<Transcriber />}></Route>*/}
|
||||
<Route path="prompt" element={<Prompt />}></Route>
|
||||
{/*<Route path="transcriber" element={<Transcriber />}></Route>*/}
|
||||
{/*<Route path="prompt" element={<Prompt />}></Route>*/}
|
||||
<Route path="download" element={<Downloader />}>
|
||||
<Route path=":id" element={<DownloaderForm />} />
|
||||
</Route>
|
||||
<Route path="about" element={<AboutPage />}></Route>
|
||||
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// 下载器 Cookie 设置表单(最简化版)
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import {
|
||||
Form,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { getDownloaderCookie, updateDownloaderCookie } from '@/services/downloader' // 你自定义的请求
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { videoPlatforms } from '@/constant/note.ts'
|
||||
|
||||
const CookieSchema = z.object({
|
||||
cookie: z.string().min(10, '请填写有效 Cookie'),
|
||||
})
|
||||
|
||||
const DownloaderForm = () => {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(CookieSchema),
|
||||
defaultValues: { cookie: '' },
|
||||
})
|
||||
const { id } = useParams()
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const loadCookie = async () => {
|
||||
setLoading(true) // 🔁 切换平台时显示 loading
|
||||
try {
|
||||
const res = await getDownloaderCookie(id)
|
||||
const cookie = res?.data?.data?.cookie || ''
|
||||
form.reset({ cookie }) // ✅ 正确重置表单值
|
||||
} catch (e) {
|
||||
toast.error('加载 Cookie 失败: ' + e)
|
||||
form.reset({ cookie: '' }) // ❗失败时也要清空旧值
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (id) loadCookie()
|
||||
}, [id]) // 🔁 每当 id 变化时触发
|
||||
|
||||
const onSubmit = async values => {
|
||||
try {
|
||||
await updateDownloaderCookie({
|
||||
platform: id,
|
||||
cookie: String(values.cookie),
|
||||
})
|
||||
toast.success('保存成功')
|
||||
} catch (e) {
|
||||
toast.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="p-4">加载中...</div>
|
||||
|
||||
return (
|
||||
<div className="max-w-xl p-4">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<div className="text-lg font-bold">
|
||||
设置{videoPlatforms.find(item => item.value === id)?.label}下载器 Cookie
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cookie"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col gap-2">
|
||||
<FormLabel>Cookie</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="输入 Cookie" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit">保存</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DownloaderForm
|
||||
@@ -0,0 +1,34 @@
|
||||
import ProviderCard from '@/components/Form/DownloaderForm/providerCard.tsx'
|
||||
import { Button } from '@/components/ui/button.tsx'
|
||||
import { useProviderStore } from '@/store/providerStore'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { DouyinLogo, KuaishouLogo } from '@/components/Icons/platform.tsx'
|
||||
import { videoPlatforms } from '@/constant/note.ts'
|
||||
|
||||
const Provider = () => {
|
||||
const navigate = useNavigate()
|
||||
const handleClick = () => {
|
||||
navigate(`/settings/model/new`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm font-light">下载器配置</div>
|
||||
<div>
|
||||
{videoPlatforms &&
|
||||
videoPlatforms.map((provider, index) => {
|
||||
if (provider.value !== 'local')
|
||||
return (
|
||||
<ProviderCard
|
||||
key={index}
|
||||
providerName={provider.label}
|
||||
Icon={provider?.logo}
|
||||
id={provider.value}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Provider
|
||||
@@ -0,0 +1,6 @@
|
||||
.card {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.card:hover {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Switch } from '@/components/ui/switch.tsx'
|
||||
import { FC } from 'react'
|
||||
import styles from './index.module.css'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import AILogo from '@/components/Form/modelForm/Icons'
|
||||
import { useProviderStore } from '@/store/providerStore'
|
||||
export interface IProviderCardProps {
|
||||
id: string
|
||||
providerName: string
|
||||
Icon: any
|
||||
}
|
||||
const ProviderCard: FC<IProviderCardProps> = ({ providerName, Icon, id }: IProviderCardProps) => {
|
||||
const navigate = useNavigate()
|
||||
const updateProvider = useProviderStore(state => state.updateProvider)
|
||||
const handleClick = () => {
|
||||
navigate(`/settings/download/${id}`)
|
||||
}
|
||||
|
||||
const rawId = useParams()
|
||||
console.log('rawId', rawId)
|
||||
// @ts-ignore
|
||||
const { id: currentId } = useParams()
|
||||
const isActive = currentId === id
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
handleClick()
|
||||
}}
|
||||
className={
|
||||
styles.card +
|
||||
' flex h-14 items-center justify-between rounded border border-[#f3f3f3] p-2' +
|
||||
(isActive ? ' bg-[#F0F0F0] font-semibold text-blue-600' : '')
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-lg">
|
||||
<div className="flex h-6 w-6 items-center">{<Icon></Icon>}</div>
|
||||
<div className="font-semibold">{providerName}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ProviderCard
|
||||
168
BillNote_frontend/src/components/Icons/platform.tsx
Normal file
168
BillNote_frontend/src/components/Icons/platform.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
export const KuaishouLogo = () => {
|
||||
return (
|
||||
<svg
|
||||
t="1746695310517"
|
||||
className="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="1680"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M299.27936 624.43008v87.48544c0 14.64832 10.70592 21.24288 23.78752 14.65856l83.49696-42.01984v-32.76288L323.072 609.7664c-13.08672-6.58432-23.79264 0.01536-23.79264 14.66368zM654.42304 436.03456c36.72064 0 66.59584-29.87008 66.59584-66.59072s-29.8752-66.59584-66.59584-66.59584c-36.71552 0-66.5856 29.8752-66.5856 66.59584s29.87008 66.59072 66.5856 66.59072zM443.56096 435.65056c47.73376 0 86.56384-38.8352 86.56384-86.56896s-38.83008-86.56896-86.56384-86.56896-86.56896 38.8352-86.56896 86.56896 38.8352 86.56896 86.56896 86.56896z"
|
||||
fill="#FF4A08"
|
||||
p-id="1681"
|
||||
></path>
|
||||
<path
|
||||
d="M849.92 51.2H174.08c-67.8656 0-122.88 55.0144-122.88 122.88v675.84c0 67.8656 55.0144 122.88 122.88 122.88h675.84c67.8656 0 122.88-55.0144 122.88-122.88V174.08c0-67.8656-55.0144-122.88-122.88-122.88zM443.56096 204.8c54.05184 0 101.22752 29.89056 125.93664 73.99936 22.24128-20.85376 52.11136-33.664 84.93056-33.664 68.54656 0 124.30848 55.76704 124.30848 124.30848s-55.76704 124.30336-124.30848 124.30336c-41.40544 0-78.12608-20.37248-100.73088-51.60448-26.48576 31.29856-66.01728 51.22048-110.13632 51.22048-79.55968 0-144.2816-64.72704-144.2816-144.2816S364.00128 204.8 443.56096 204.8z m336.65536 505.63584c0 59.97568-48.78848 108.76416-108.76416 108.76416H515.328c-47.05792 0-87.22432-30.04416-102.34368-71.96672l-87.81824 42.40384c-9.43616 4.5568-18.97984 6.8608-28.37504 6.8608h-0.00512c-30.70976 0-53.00224-24.3712-53.00224-57.9328v-140.5696c0-33.57696 22.29248-57.94304 53.00736-57.94304 9.3952 0 18.93888 2.30912 28.36992 6.86592l87.59808 42.29632c14.93504-42.26048 55.26528-72.63232 102.56896-72.63232h156.11904c59.97568 0 108.76416 48.7936 108.76416 108.76928v85.08416z"
|
||||
fill="#FF4A08"
|
||||
p-id="1682"
|
||||
></path>
|
||||
<path
|
||||
d="M671.45216 574.28992H515.328c-28.14976 0-51.05664 22.90688-51.05664 51.05664v85.08928c0 28.14976 22.90688 51.05664 51.05664 51.05664h156.11904c28.14976 0 51.05664-22.90688 51.05664-51.05664v-85.08928c0-28.14976-22.90176-51.05664-51.05152-51.05664z"
|
||||
fill="#FF4A08"
|
||||
p-id="1683"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export const DouyinLogo = () => {
|
||||
return (
|
||||
<svg
|
||||
t="1746695428425"
|
||||
className="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="2731"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z"
|
||||
fill="#111111"
|
||||
p-id="2732"
|
||||
></path>
|
||||
<path
|
||||
d="M204.27776 670.59712a246.25152 246.25152 0 0 1 245.97504-245.97504v147.57888a98.49856 98.49856 0 0 0-98.38592 98.38592c0 48.34304 26.14272 100.352 83.54816 100.352 3.81952 0 93.55264-0.88064 93.55264-77.19936V134.35904h157.26592a133.31456 133.31456 0 0 0 133.12 132.99712l-0.13312 147.31264a273.152 273.152 0 0 1-142.62272-38.912l-0.06144 317.98272c0 146.00192-124.24192 224.77824-241.14176 224.77824-131.74784 0.03072-231.1168-106.56768-231.1168-247.92064z"
|
||||
fill="#FF4040"
|
||||
p-id="2733"
|
||||
></path>
|
||||
<path
|
||||
d="M164.92544 631.23456a246.25152 246.25152 0 0 1 245.97504-245.97504v147.57888a98.49856 98.49856 0 0 0-98.38592 98.38592c0 48.34304 26.14272 100.352 83.54816 100.352 3.81952 0 93.55264-0.88064 93.55264-77.19936V94.99648h157.26592a133.31456 133.31456 0 0 0 133.12 132.99712l-0.13312 147.31264a273.152 273.152 0 0 1-142.62272-38.912l-0.06144 317.98272c0 146.00192-124.24192 224.77824-241.14176 224.77824-131.74784 0.03072-231.1168-106.56768-231.1168-247.92064z"
|
||||
fill="#00F5FF"
|
||||
p-id="2734"
|
||||
></path>
|
||||
<path
|
||||
d="M410.91072 427.58144c-158.8224 20.15232-284.44672 222.72-154.112 405.00224 120.40192 98.47808 373.68832 41.20576 380.70272-171.85792l-0.17408-324.1472a280.7296 280.7296 0 0 0 142.88896 38.62528V261.2224a144.98816 144.98816 0 0 1-72.8064-54.82496 135.23968 135.23968 0 0 1-54.70208-72.45824h-123.66848l-0.08192 561.41824c-0.11264 78.46912-130.9696 106.41408-164.18816 30.2592-83.18976-39.77216-64.37888-190.9248 46.31552-192.57344z"
|
||||
fill="#FFFFFF"
|
||||
p-id="2735"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const BiliBiliLogo = () => {
|
||||
return (
|
||||
<svg
|
||||
t="1746696526393"
|
||||
className="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="3757"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z"
|
||||
fill="#EC5D85"
|
||||
p-id="3758"
|
||||
></path>
|
||||
<path
|
||||
d="M512 241.96096h52.224l65.06496-96.31744c49.63328-50.31936 89.64096 0.43008 63.85664 45.71136l-34.31424 51.5072c257.64864 5.02784 257.64864 43.008 257.64864 325.03808 0 325.94944 0 336.46592-404.48 336.46592S107.52 893.8496 107.52 567.90016c0-277.69856 0-318.80192 253.14304-324.95616l-39.43424-58.368c-31.26272-54.90688 37.33504-90.40896 64.68608-42.37312l60.416 99.80928c18.18624-0.0512 41.18528-0.0512 65.66912-0.0512z"
|
||||
fill="#EF85A7"
|
||||
p-id="3759"
|
||||
></path>
|
||||
<path
|
||||
d="M512 338.5856c332.8 0 332.8 0 332.8 240.64s0 248.39168-332.8 248.39168-332.8-7.75168-332.8-248.39168 0-240.64 332.8-240.64z"
|
||||
fill="#EC5D85"
|
||||
p-id="3760"
|
||||
></path>
|
||||
<path
|
||||
d="M281.6 558.08a30.72 30.72 0 0 1-27.47392-16.97792 30.72 30.72 0 0 1 13.73184-41.216l122.88-61.44a30.72 30.72 0 0 1 41.216 13.74208 30.72 30.72 0 0 1-13.74208 41.216l-122.88 61.44a30.59712 30.59712 0 0 1-13.73184 3.23584zM752.64 558.08a30.60736 30.60736 0 0 1-12.8512-2.83648l-133.12-61.44a30.72 30.72 0 0 1-15.04256-40.7552 30.72 30.72 0 0 1 40.76544-15.02208l133.12 61.44A30.72 30.72 0 0 1 752.64 558.08zM454.656 666.88a15.36 15.36 0 0 1-12.288-6.1952 15.36 15.36 0 0 1 3.072-21.49376l68.5056-50.91328 50.35008 52.62336a15.36 15.36 0 0 1-22.20032 21.23776l-31.5904-33.024-46.71488 34.72384a15.28832 15.28832 0 0 1-9.13408 3.04128z"
|
||||
fill="#EF85A7"
|
||||
p-id="3761"
|
||||
></path>
|
||||
<path
|
||||
d="M65.536 369.31584c15.03232 101.90848 32.84992 147.17952 44.544 355.328 14.63296 2.18112 177.70496 10.04544 204.05248-74.62912a16.14848 16.14848 0 0 0 1.64864-10.87488c-30.60736-80.3328-169.216-60.416-169.216-60.416s-10.36288-146.50368-11.49952-238.83776zM362.25024 383.03744l34.816 303.17568h34.64192L405.23776 381.1328zM309.52448 536.28928h45.48608l16.09728 158.6176-31.82592 1.85344zM446.86336 542.98624h45.80352V705.3312h-33.87392zM296.6016 457.97376h21.39136l5.2736 58.99264-18.91328 2.26304zM326.99392 457.97376h21.39136l2.53952 55.808-17.408 1.61792zM470.62016 459.88864h19.456v62.27968h-19.456zM440.23808 459.88864h22.20032v62.27968h-16.62976z"
|
||||
fill="#FFFFFF"
|
||||
p-id="3762"
|
||||
></path>
|
||||
<path
|
||||
d="M243.56864 645.51936a275.456 275.456 0 0 1-28.4672 23.74656 242.688 242.688 0 0 1-29.53216 17.52064 2.70336 2.70336 0 0 1-4.4032-1.95584 258.60096 258.60096 0 0 1-5.12-29.57312c-1.41312-12.1856-1.95584-25.68192-2.16064-36.36224 0-0.3072 0-2.5088 3.01056-1.90464a245.92384 245.92384 0 0 1 34.22208 9.5744 257.024 257.024 0 0 1 32.3584 15.17568c0.52224 0.256 2.51904 1.4848 0.09216 3.77856z"
|
||||
fill="#EB5480"
|
||||
p-id="3763"
|
||||
></path>
|
||||
<path
|
||||
d="M513.29024 369.31584c15.03232 101.90848 32.84992 147.17952 44.544 355.328 14.63296 2.18112 177.70496 10.04544 204.05248-74.62912a16.14848 16.14848 0 0 0 1.64864-10.87488c-30.60736-80.3328-169.216-60.416-169.216-60.416s-10.36288-146.50368-11.49952-238.83776zM810.00448 383.03744l34.816 303.17568h34.64192L852.992 381.1328zM757.27872 536.28928h45.48608l16.09728 158.6176-31.82592 1.85344zM894.6176 542.98624h45.80352V705.3312H906.5472zM744.35584 457.97376h21.39136l5.2736 58.99264-18.91328 2.26304zM774.74816 457.97376h21.39136l2.53952 55.808-17.408 1.61792zM918.3744 459.88864h19.456v62.27968h-19.456zM887.99232 459.88864h22.20032v62.27968h-16.62976z"
|
||||
fill="#FFFFFF"
|
||||
p-id="3764"
|
||||
></path>
|
||||
<path
|
||||
d="M691.32288 645.51936a275.456 275.456 0 0 1-28.4672 23.74656 242.688 242.688 0 0 1-29.53216 17.52064 2.70336 2.70336 0 0 1-4.4032-1.95584 258.60096 258.60096 0 0 1-5.12-29.57312c-1.41312-12.1856-1.95584-25.68192-2.16064-36.36224 0-0.3072 0-2.5088 3.01056-1.90464a245.92384 245.92384 0 0 1 34.22208 9.5744 257.024 257.024 0 0 1 32.3584 15.17568c0.52224 0.256 2.51904 1.4848 0.09216 3.77856z"
|
||||
fill="#EB5480"
|
||||
p-id="3765"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const YoutubeLogo = () => {
|
||||
return (
|
||||
<svg
|
||||
t="1746696577253"
|
||||
className="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="4785"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M426.666667 682.666667V384l256 149.845333L426.666667 682.666667z m587.093333-355.541334s-10.026667-71.04-40.704-102.357333c-38.954667-41.088-82.602667-41.258667-102.613333-43.648C727.168 170.666667 512.213333 170.666667 512.213333 170.666667h-0.426666s-214.954667 0-358.229334 10.453333c-20.053333 2.389333-63.658667 2.56-102.656 43.648-30.677333 31.317333-40.661333 102.4-40.661333 102.4S0 410.538667 0 493.952v78.293333c0 83.456 10.24 166.912 10.24 166.912s9.984 71.04 40.661333 102.357334c38.997333 41.088 90.154667 39.765333 112.938667 44.074666C245.76 893.568 512 896 512 896s215.168-0.341333 358.442667-10.752c20.053333-2.432 63.658667-2.602667 102.613333-43.690667 30.72-31.317333 40.704-102.4 40.704-102.4s10.24-83.413333 10.24-166.869333v-78.250667c0-83.456-10.24-166.912-10.24-166.912z"
|
||||
fill="#FF0000"
|
||||
p-id="4786"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const LocalLogo = () => {
|
||||
return (
|
||||
<svg
|
||||
t="1746696617516"
|
||||
className="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="5795"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M948.736 144.384H461.568l-56.576-83.456c-6.144-7.168-15.36-10.752-24.576-9.728H79.872c-17.152-0.512-34.048 5.632-46.592 17.408-12.544 11.776-19.968 28.16-20.48 45.312v222.464c0-18.944 7.424-37.12 20.992-50.432 13.312-13.312 31.488-20.992 50.432-20.992h855.808c18.944 0 37.12 7.424 50.432 20.992 13.312 13.312 20.992 31.488 20.992 50.432V213.248c1.28-36.096-26.624-66.816-62.72-68.864z m0 0"
|
||||
fill="#FFD569"
|
||||
p-id="5796"
|
||||
></path>
|
||||
<path
|
||||
d="M939.776 265.216H84.224C44.8 265.216 12.8 297.216 12.8 336.64v570.368c0 18.944 7.424 37.12 20.992 50.432 13.312 13.312 31.488 20.992 50.432 20.992h855.808c18.944 0 37.12-7.424 50.432-20.992 13.312-13.312 20.992-31.488 20.992-50.432V336.64c0-18.944-7.424-37.12-20.992-50.432-13.568-13.312-31.744-20.992-50.688-20.992z m-213.76 467.968c0.256 6.4-3.328 12.288-9.216 14.848-1.792 0.256-3.84 0.256-5.632 0-4.096 0-7.936-1.792-10.752-4.864l-54.784-59.136v77.056c0.256 8.704-6.4 15.872-14.848 16.384h-317.44c-7.936-0.512-14.336-6.912-14.848-14.848V495.616c-0.256-8.704 6.4-15.872 14.848-16.384h317.44c8.704 0.512 15.616 7.68 15.36 16.384v76.544l54.784-57.344c3.84-4.864 10.496-6.144 16.128-3.584 5.632 2.816 9.472 8.704 9.216 14.848v207.104z m0 0"
|
||||
fill="#FFC225"
|
||||
p-id="5797"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,35 @@
|
||||
/* -------------------- 常量 -------------------- */
|
||||
import {
|
||||
BiliBiliLogo,
|
||||
DouyinLogo,
|
||||
KuaishouLogo,
|
||||
LocalLogo,
|
||||
YoutubeLogo,
|
||||
} from '@/components/Icons/platform.tsx'
|
||||
|
||||
export const noteFormats = [
|
||||
{ label: '目录', value: 'toc' },
|
||||
{ label: '原片跳转', value: 'link' },
|
||||
{ label: '原片截图', value: 'screenshot' },
|
||||
{ label: 'AI总结', value: 'summary' },
|
||||
{ label: '目录', value: 'toc' },
|
||||
{ label: '原片跳转', value: 'link' },
|
||||
{ label: '原片截图', value: 'screenshot' },
|
||||
{ label: 'AI总结', value: 'summary' },
|
||||
] as const
|
||||
|
||||
export const noteStyles = [
|
||||
{ label: '精简', value: 'minimal' },
|
||||
{ label: '详细', value: 'detailed' },
|
||||
{ label: '教程', value: 'tutorial' },
|
||||
{ label: '学术', value: 'academic' },
|
||||
{ label: '小红书', value: 'xiaohongshu' },
|
||||
{ label: '生活向', value: 'life_journal' },
|
||||
{ label: '任务导向', value: 'task_oriented' },
|
||||
{ label: '商业风格', value: 'business' },
|
||||
{ label: '会议纪要', value: 'meeting_minutes' },
|
||||
] as const
|
||||
{ label: '精简', value: 'minimal' },
|
||||
{ label: '详细', value: 'detailed' },
|
||||
{ label: '教程', value: 'tutorial' },
|
||||
{ label: '学术', value: 'academic' },
|
||||
{ label: '小红书', value: 'xiaohongshu' },
|
||||
{ label: '生活向', value: 'life_journal' },
|
||||
{ label: '任务导向', value: 'task_oriented' },
|
||||
{ label: '商业风格', value: 'business' },
|
||||
{ label: '会议纪要', value: 'meeting_minutes' },
|
||||
] as const
|
||||
|
||||
export const videoPlatforms = [
|
||||
{ label: '哔哩哔哩', value: 'bilibili', logo: BiliBiliLogo },
|
||||
{ label: 'YouTube', value: 'youtube', logo: YoutubeLogo },
|
||||
{ label: '抖音', value: 'douyin', logo: DouyinLogo },
|
||||
{ label: '快手', value: 'kuaishou', logo: KuaishouLogo },
|
||||
{ label: '本地视频', value: 'local', logo: LocalLogo },
|
||||
] as const
|
||||
|
||||
@@ -1,108 +1,128 @@
|
||||
/* NoteForm.tsx ---------------------------------------------------- */
|
||||
import {
|
||||
Form, FormControl, FormField, FormItem, FormLabel, FormMessage,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form.tsx'
|
||||
import { useEffect } from 'react'
|
||||
import { useForm, useWatch } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Info, Loader2 ,Plus} from 'lucide-react'
|
||||
import { Info, Loader2, Plus } from 'lucide-react'
|
||||
import { message, Alert } from 'antd'
|
||||
import { generateNote } from '@/services/note.ts'
|
||||
import { uploadFile } from '@/services/upload.ts'
|
||||
import { useTaskStore } from '@/store/taskStore'
|
||||
import { useModelStore } from '@/store/modelStore'
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip.tsx";
|
||||
import {Checkbox} from "@/components/ui/checkbox.tsx";
|
||||
import {ScrollArea} from "@/components/ui/scroll-area.tsx";
|
||||
import {Button} from "@/components/ui/button.tsx";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select.tsx";
|
||||
import {Input} from "@/components/ui/input.tsx";
|
||||
import {Textarea} from "@/components/ui/textarea.tsx";
|
||||
import {noteStyles,noteFormats} from "@/constant/note.ts";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip.tsx'
|
||||
import { Checkbox } from '@/components/ui/checkbox.tsx'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
|
||||
import { Button } from '@/components/ui/button.tsx'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select.tsx'
|
||||
import { Input } from '@/components/ui/input.tsx'
|
||||
import { Textarea } from '@/components/ui/textarea.tsx'
|
||||
import { noteStyles, noteFormats, videoPlatforms } from '@/constant/note.ts'
|
||||
|
||||
/* -------------------- 校验 Schema -------------------- */
|
||||
const formSchema = z.object({
|
||||
video_url: z.string(),
|
||||
platform: z.string().nonempty('请选择平台'),
|
||||
quality: z.enum(['fast', 'medium', 'slow']),
|
||||
screenshot: z.boolean().optional(),
|
||||
link: z.boolean().optional(),
|
||||
model_name: z.string().nonempty('请选择模型'),
|
||||
format: z.array(z.string()).default([]),
|
||||
style: z.string().nonempty('请选择笔记生成风格'),
|
||||
extras: z.string().optional(),
|
||||
video_understanding: z.boolean().optional(),
|
||||
video_interval: z.coerce.number().min(1).max(30).default(4).optional(),
|
||||
grid_size: z.tuple([
|
||||
z.coerce.number().min(1).max(10),
|
||||
z.coerce.number().min(1).max(10),
|
||||
]).default([3, 3]).optional(),
|
||||
}).superRefine(({ video_url, platform }, ctx) => {
|
||||
if (platform === 'local' || platform === 'douyin') {
|
||||
if (!video_url) {
|
||||
ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] })
|
||||
const formSchema = z
|
||||
.object({
|
||||
video_url: z.string(),
|
||||
platform: z.string().nonempty('请选择平台'),
|
||||
quality: z.enum(['fast', 'medium', 'slow']),
|
||||
screenshot: z.boolean().optional(),
|
||||
link: z.boolean().optional(),
|
||||
model_name: z.string().nonempty('请选择模型'),
|
||||
format: z.array(z.string()).default([]),
|
||||
style: z.string().nonempty('请选择笔记生成风格'),
|
||||
extras: z.string().optional(),
|
||||
video_understanding: z.boolean().optional(),
|
||||
video_interval: z.coerce.number().min(1).max(30).default(4).optional(),
|
||||
grid_size: z
|
||||
.tuple([z.coerce.number().min(1).max(10), z.coerce.number().min(1).max(10)])
|
||||
.default([3, 3])
|
||||
.optional(),
|
||||
})
|
||||
.superRefine(({ video_url, platform }, ctx) => {
|
||||
if (platform === 'local' || platform === 'douyin') {
|
||||
if (!video_url) {
|
||||
ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] })
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(video_url)
|
||||
if (!['http:', 'https:'].includes(url.protocol)) throw new Error()
|
||||
} catch {
|
||||
ctx.addIssue({ code: 'custom', message: '请输入正确的视频链接', path: ['video_url'] })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(video_url)
|
||||
if (!['http:', 'https:'].includes(url.protocol)) throw new Error()
|
||||
} catch {
|
||||
ctx.addIssue({ code: 'custom', message: '请输入正确的视频链接', path: ['video_url'] })
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
type NoteFormValues = z.infer<typeof formSchema>
|
||||
|
||||
/* -------------------- 可复用子组件 -------------------- */
|
||||
const SectionHeader = ({ title, tip }: { title: string; tip?: string }) => (
|
||||
<div className="my-3 flex items-center justify-between">
|
||||
<h2 className="block">{title}</h2>
|
||||
{tip && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-xs">{tip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<div className="my-3 flex items-center justify-between">
|
||||
<h2 className="block">{title}</h2>
|
||||
{tip && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-xs">{tip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const CheckboxGroup = ({
|
||||
value = [], onChange, disabledMap,
|
||||
}: {
|
||||
value = [],
|
||||
onChange,
|
||||
disabledMap,
|
||||
}: {
|
||||
value?: string[]
|
||||
onChange: (v: string[]) => void
|
||||
disabledMap: Record<string, boolean>
|
||||
}) => (
|
||||
<div className="flex flex-wrap space-x-1.5">
|
||||
{noteFormats.map(({ label, value: v }) => (
|
||||
<label key={v} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={value.includes(v)}
|
||||
disabled={disabledMap[v]}
|
||||
onCheckedChange={checked =>
|
||||
onChange(checked ? [...value, v] : value.filter(x => x !== v))}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap space-x-1.5">
|
||||
{noteFormats.map(({ label, value: v }) => (
|
||||
<label key={v} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={value.includes(v)}
|
||||
disabled={disabledMap[v]}
|
||||
onCheckedChange={checked =>
|
||||
onChange(checked ? [...value, v] : value.filter(x => x !== v))
|
||||
}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
/* -------------------- 主组件 -------------------- */
|
||||
const NoteForm = () => {
|
||||
/* ---- 全局状态 ---- */
|
||||
const { addPendingTask, currentTaskId, setCurrentTask,getCurrentTask ,retryTask} = useTaskStore()
|
||||
const { addPendingTask, currentTaskId, setCurrentTask, getCurrentTask, retryTask } =
|
||||
useTaskStore()
|
||||
const { loadEnabledModels, modelList, showFeatureHint, setShowFeatureHint } = useModelStore()
|
||||
|
||||
|
||||
/* ---- 表单 ---- */
|
||||
const form = useForm<NoteFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -116,35 +136,32 @@ const NoteForm = () => {
|
||||
format: [],
|
||||
},
|
||||
})
|
||||
const currentTask = getCurrentTask()
|
||||
const currentTask = getCurrentTask()
|
||||
|
||||
/* ---- 派生状态(只 watch 一次,提高性能) ---- */
|
||||
const platform = useWatch({ control: form.control, name: 'platform' }) as string
|
||||
const videoUnderstandingEnabled = useWatch({ control: form.control, name: 'video_understanding' })
|
||||
const editing = currentTask && currentTask.id
|
||||
const editing = currentTask && currentTask.id
|
||||
|
||||
/* ---- 副作用 ---- */
|
||||
/* ---- 副作用 ---- */
|
||||
useEffect(() => {
|
||||
loadEnabledModels()
|
||||
|
||||
return}, [])
|
||||
useEffect(() => {
|
||||
const currentTask = getCurrentTask()
|
||||
const { formData } = currentTask || {}
|
||||
if (!currentTask) return
|
||||
form.reset(
|
||||
{
|
||||
...formData,
|
||||
extras: formData?.extras || '',
|
||||
}
|
||||
|
||||
)
|
||||
return
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const currentTask = getCurrentTask()
|
||||
const { formData } = currentTask || {}
|
||||
if (!currentTask) return
|
||||
form.reset({
|
||||
...formData,
|
||||
extras: formData?.extras || '',
|
||||
})
|
||||
}, [currentTaskId])
|
||||
|
||||
/* ---- 帮助函数 ---- */
|
||||
const isGenerating = () =>
|
||||
!['SUCCESS', 'FAILED', undefined].includes(getCurrentTask()?.status)
|
||||
const generating = isGenerating()
|
||||
const isGenerating = () => !['SUCCESS', 'FAILED', undefined].includes(getCurrentTask()?.status)
|
||||
const generating = isGenerating()
|
||||
const handleFileUpload = async (file: File, cb: (url: string) => void) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
@@ -158,321 +175,324 @@ const NoteForm = () => {
|
||||
}
|
||||
|
||||
const onSubmit = async (values: NoteFormValues) => {
|
||||
|
||||
const payload:NoteFormValues = {
|
||||
...values,
|
||||
provider_id: modelList.find(m => m.model_name === values.model_name)!.provider_id,
|
||||
task_id: currentTaskId || '',
|
||||
}
|
||||
if (currentTaskId){
|
||||
retryTask(currentTaskId,payload)
|
||||
return
|
||||
const payload: NoteFormValues = {
|
||||
...values,
|
||||
provider_id: modelList.find(m => m.model_name === values.model_name)!.provider_id,
|
||||
task_id: currentTaskId || '',
|
||||
}
|
||||
if (currentTaskId) {
|
||||
retryTask(currentTaskId, payload)
|
||||
return
|
||||
}
|
||||
|
||||
message.success('已提交任务')
|
||||
const { data } = await generateNote(payload)
|
||||
addPendingTask(data.task_id, values.platform, payload)
|
||||
}
|
||||
const handleCreateNew = () => {
|
||||
// 🔁 这里清空当前任务状态
|
||||
// 比如调用 resetCurrentTask() 或者 navigate 到一个新页面
|
||||
setCurrentTask(null)
|
||||
const handleCreateNew = () => {
|
||||
// 🔁 这里清空当前任务状态
|
||||
// 比如调用 resetCurrentTask() 或者 navigate 到一个新页面
|
||||
setCurrentTask(null)
|
||||
}
|
||||
const FormButton = () => {
|
||||
const label = generating ? '正在生成…' : editing ? '重新生成' : '生成笔记'
|
||||
|
||||
}
|
||||
const FormButton = () => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
className={!editing ? 'w-full' : 'w-2/3' + ' bg-primary'}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
|
||||
const label = generating
|
||||
? '正在生成…'
|
||||
: editing
|
||||
? '重新生成'
|
||||
: '生成笔记'
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" className={!editing?'w-full':'w-2/3'+" bg-primary"} disabled={generating}>
|
||||
{generating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
{editing && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-1/3"
|
||||
onClick={handleCreateNew}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新建笔记
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{editing && (
|
||||
<Button type="button" variant="outline" className="w-1/3" onClick={handleCreateNew}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新建笔记
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* -------------------- 渲染 -------------------- */
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* 顶部按钮 */}
|
||||
<FormButton></FormButton>
|
||||
<div className="h-full w-full">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* 顶部按钮 */}
|
||||
<FormButton></FormButton>
|
||||
|
||||
{/* 视频链接 & 平台 */}
|
||||
<SectionHeader title="视频链接" tip="支持 B 站、YouTube 等平台" />
|
||||
<div className="flex gap-2">
|
||||
{/* 平台选择 */}
|
||||
{/* 视频链接 & 平台 */}
|
||||
<SectionHeader title="视频链接" tip="支持 B 站、YouTube 等平台" />
|
||||
<div className="flex gap-2">
|
||||
{/* 平台选择 */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="platform"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Select disabled={!!editing} value={field.value} onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-32"><SelectValue /></SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent >
|
||||
<SelectItem value="bilibili">哔哩哔哩</SelectItem>
|
||||
<SelectItem value="youtube">YouTube</SelectItem>
|
||||
<SelectItem value="douyin">抖音</SelectItem>
|
||||
<SelectItem value="local">本地视频</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 链接输入 / 上传框 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="video_url"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
{platform === 'local' ? (
|
||||
<>
|
||||
<Input disabled={!!editing} placeholder="请输入本地视频路径" {...field} />
|
||||
|
||||
</>
|
||||
) : (
|
||||
<Input disabled={!!editing} placeholder="请输入视频网站链接" {...field} />
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="video_url"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
{platform === 'local' && (
|
||||
<>
|
||||
<div
|
||||
className="hover:border-primary mt-2 flex h-40 cursor-pointer items-center justify-center rounded-md border-2 border-dashed border-gray-300 transition-colors"
|
||||
onDragOver={e => { e.preventDefault(); e.stopPropagation() }}
|
||||
onDrop={e => {
|
||||
e.preventDefault()
|
||||
const file = e.dataTransfer.files?.[0]
|
||||
if (file) handleFileUpload(file, field.onChange)
|
||||
}}
|
||||
onClick={() => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'video/*'
|
||||
input.onchange = e => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file) handleFileUpload(file, field.onChange)
|
||||
}
|
||||
input.click()
|
||||
}}
|
||||
>
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
拖拽文件到这里上传 <br />
|
||||
<span className="text-xs text-gray-400">或点击选择文件</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
|
||||
{/* 模型选择 */}
|
||||
<FormField
|
||||
className="w-full"
|
||||
control={form.control}
|
||||
name="model_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<SectionHeader title="模型选择" tip="不同模型效果不同,建议自行测试" />
|
||||
<Select value={field.value} onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full min-w-0 truncate" ><SelectValue /></SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{modelList.map(m => (
|
||||
<SelectItem key={m.id} value={m.model_name}>
|
||||
{m.model_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 笔记风格 */}
|
||||
<FormField
|
||||
className="w-full"
|
||||
control={form.control}
|
||||
name="style"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<SectionHeader title="笔记风格" tip="选择生成笔记的呈现风格" />
|
||||
<Select value={field.value} onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full min-w-0 truncate" ><SelectValue /></SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{noteStyles.map(({ label, value }) => (
|
||||
<SelectItem key={value} value={value}>{label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* 视频理解 */}
|
||||
<SectionHeader title="视频理解" tip="将视频截图发给多模态模型辅助分析" />
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="video_understanding"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>启用</FormLabel>
|
||||
<Checkbox
|
||||
checked={videoUnderstandingEnabled}
|
||||
onCheckedChange={v => form.setValue('video_understanding', v)}
|
||||
/>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 采样间隔 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="video_interval"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>采样间隔(秒)</FormLabel>
|
||||
<Input
|
||||
disabled={!videoUnderstandingEnabled}
|
||||
type="number"
|
||||
{...field}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 拼图大小 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="grid_size"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>拼图尺寸(列 × 行)</FormLabel>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
disabled={!videoUnderstandingEnabled}
|
||||
type="number"
|
||||
value={field.value?.[0] || 3}
|
||||
onChange={e =>
|
||||
field.onChange([+e.target.value, field.value?.[1] || 3])}
|
||||
className="w-16"
|
||||
/>
|
||||
<span>x</span>
|
||||
<Input
|
||||
disabled={!videoUnderstandingEnabled}
|
||||
type="number"
|
||||
value={field.value?.[1] || 3}
|
||||
onChange={e =>
|
||||
field.onChange([field.value?.[0] || 3, +e.target.value])}
|
||||
className="w-16"
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="platform"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Select
|
||||
disabled={!!editing}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{videoPlatforms?.map(p => (
|
||||
<SelectItem key={p.value} value={p.value}>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-4 w-4">{p.logo()}</div>
|
||||
<span>{p.label}</span>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Alert
|
||||
closable
|
||||
type="error"
|
||||
message={
|
||||
<div>
|
||||
<strong>提示:</strong>
|
||||
<p>视频理解功能必须使用多模态模型。</p>
|
||||
|
||||
</div>
|
||||
}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* 笔记格式 */}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 链接输入 / 上传框 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="video_url"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
{platform === 'local' ? (
|
||||
<>
|
||||
<Input disabled={!!editing} placeholder="请输入本地视频路径" {...field} />
|
||||
</>
|
||||
) : (
|
||||
<Input disabled={!!editing} placeholder="请输入视频网站链接" {...field} />
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="video_url"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
{platform === 'local' && (
|
||||
<>
|
||||
<div
|
||||
className="hover:border-primary mt-2 flex h-40 cursor-pointer items-center justify-center rounded-md border-2 border-dashed border-gray-300 transition-colors"
|
||||
onDragOver={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onDrop={e => {
|
||||
e.preventDefault()
|
||||
const file = e.dataTransfer.files?.[0]
|
||||
if (file) handleFileUpload(file, field.onChange)
|
||||
}}
|
||||
onClick={() => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'video/*'
|
||||
input.onchange = e => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file) handleFileUpload(file, field.onChange)
|
||||
}
|
||||
input.click()
|
||||
}}
|
||||
>
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
拖拽文件到这里上传 <br />
|
||||
<span className="text-xs text-gray-400">或点击选择文件</span>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* 模型选择 */}
|
||||
<FormField
|
||||
className="w-full"
|
||||
control={form.control}
|
||||
name="model_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<SectionHeader title="模型选择" tip="不同模型效果不同,建议自行测试" />
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full min-w-0 truncate">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{modelList.map(m => (
|
||||
<SelectItem key={m.id} value={m.model_name}>
|
||||
{m.model_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 笔记风格 */}
|
||||
<FormField
|
||||
className="w-full"
|
||||
control={form.control}
|
||||
name="style"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<SectionHeader title="笔记风格" tip="选择生成笔记的呈现风格" />
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full min-w-0 truncate">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{noteStyles.map(({ label, value }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* 视频理解 */}
|
||||
<SectionHeader title="视频理解" tip="将视频截图发给多模态模型辅助分析" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="video_understanding"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>启用</FormLabel>
|
||||
<Checkbox
|
||||
checked={videoUnderstandingEnabled}
|
||||
onCheckedChange={v => form.setValue('video_understanding', v)}
|
||||
/>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 采样间隔 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="format"
|
||||
name="video_interval"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<SectionHeader title="笔记格式" tip="选择要包含的笔记元素" />
|
||||
<CheckboxGroup
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
disabledMap={{
|
||||
link: platform === 'local',
|
||||
screenshot: !videoUnderstandingEnabled,
|
||||
}}
|
||||
<FormItem>
|
||||
<FormLabel>采样间隔(秒)</FormLabel>
|
||||
<Input disabled={!videoUnderstandingEnabled} type="number" {...field} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 拼图大小 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="grid_size"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>拼图尺寸(列 × 行)</FormLabel>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
disabled={!videoUnderstandingEnabled}
|
||||
type="number"
|
||||
value={field.value?.[0] || 3}
|
||||
onChange={e => field.onChange([+e.target.value, field.value?.[1] || 3])}
|
||||
className="w-16"
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<span>x</span>
|
||||
<Input
|
||||
disabled={!videoUnderstandingEnabled}
|
||||
type="number"
|
||||
value={field.value?.[1] || 3}
|
||||
onChange={e => field.onChange([field.value?.[0] || 3, +e.target.value])}
|
||||
className="w-16"
|
||||
/>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Alert
|
||||
closable
|
||||
type="error"
|
||||
message={
|
||||
<div>
|
||||
<strong>提示:</strong>
|
||||
<p>视频理解功能必须使用多模态模型。</p>
|
||||
</div>
|
||||
}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 备注 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="extras"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<SectionHeader title="备注" tip="可在 Prompt 结尾附加自定义说明" />
|
||||
<Textarea placeholder="笔记需要罗列出 xxx 关键点…" {...field} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
{/* 笔记格式 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="format"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<SectionHeader title="笔记格式" tip="选择要包含的笔记元素" />
|
||||
<CheckboxGroup
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
disabledMap={{
|
||||
link: platform === 'local',
|
||||
screenshot: !videoUnderstandingEnabled,
|
||||
}}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 备注 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="extras"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<SectionHeader title="备注" tip="可在 Prompt 结尾附加自定义说明" />
|
||||
<Textarea placeholder="笔记需要罗列出 xxx 关键点…" {...field} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
16
BillNote_frontend/src/pages/SettingPage/Downloader.tsx
Normal file
16
BillNote_frontend/src/pages/SettingPage/Downloader.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import Provider from '@/components/Form/modelForm/Provider.tsx'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import Options from '@/components/Form/DownloaderForm/Options.tsx'
|
||||
const Downloader = () => {
|
||||
return (
|
||||
<div className={'flex h-full bg-white'}>
|
||||
<div className={'flex-1/5 border-r border-neutral-200 p-2'}>
|
||||
<Options></Options>
|
||||
</div>
|
||||
<div className={'flex-4/5'}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Downloader
|
||||
@@ -24,12 +24,12 @@ const Menu = () => {
|
||||
// path: '/settings/transcriber',
|
||||
// },
|
||||
// //下载配置
|
||||
// {
|
||||
// id: 'download',
|
||||
// name: '下载配置',
|
||||
// icon: <HardDriveDownload />,
|
||||
// path: '/settings/download',
|
||||
// },
|
||||
{
|
||||
id: 'download',
|
||||
name: '下载配置',
|
||||
icon: <HardDriveDownload />,
|
||||
path: '/settings/download',
|
||||
},
|
||||
// //其他配置
|
||||
// {
|
||||
// id: 'prompt',
|
||||
|
||||
9
BillNote_frontend/src/services/downloader.ts
Normal file
9
BillNote_frontend/src/services/downloader.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request.ts'
|
||||
|
||||
export const getDownloaderCookie = async id => {
|
||||
return await request.get('/get_downloader_cookie/' + id)
|
||||
}
|
||||
|
||||
export const updateDownloaderCookie = async (data: { cookie: string; platform: any }) => {
|
||||
return await request.post('/update_downloader_cookie', data)
|
||||
}
|
||||
Reference in New Issue
Block a user