Merge pull request #172 from Karasukaigan/master

fix: 修复多个前后端错误与警告
This commit is contained in:
Jianwu Huang
2025-07-02 15:13:35 +08:00
committed by GitHub
8 changed files with 68 additions and 20 deletions
+3 -3
View File
@@ -2,7 +2,7 @@ import './App.css'
import { HomePage } from './pages/HomePage/Home.tsx' import { HomePage } from './pages/HomePage/Home.tsx'
import { useTaskPolling } from '@/hooks/useTaskPolling.ts' import { useTaskPolling } from '@/hooks/useTaskPolling.ts'
import SettingPage from './pages/SettingPage/index.tsx' import SettingPage from './pages/SettingPage/index.tsx'
import { BrowserRouter, HashRouter, Navigate, Routes } from 'react-router-dom' import { BrowserRouter, Navigate, Routes } from 'react-router-dom'
import { Route } from 'react-router-dom' import { Route } from 'react-router-dom'
import Index from '@/pages/Index.tsx' import Index from '@/pages/Index.tsx'
import NotFoundPage from '@/pages/NotFoundPage' import NotFoundPage from '@/pages/NotFoundPage'
@@ -43,7 +43,7 @@ function App() {
// 后端已初始化,渲染主应用 // 后端已初始化,渲染主应用
return ( return (
<> <>
<HashRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/" element={<Index />}> <Route path="/" element={<Index />}>
<Route index element={<HomePage />} /> <Route index element={<HomePage />} />
@@ -62,7 +62,7 @@ function App() {
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Route> </Route>
</Routes> </Routes>
</HashRouter> </BrowserRouter>
</> </>
) )
} }
@@ -1,10 +1,26 @@
import * as React from 'react' import * as React from 'react'
import { useState, useEffect } from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox' import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { CheckIcon } from 'lucide-react' import { CheckIcon } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) { function Checkbox({ className, checked, onChange, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
const [isChecked, setIsChecked] = useState(checked || false);
useEffect(() => {
if (checked !== undefined) {
setIsChecked(checked);
}
}, [checked]);
const handleCheckChange = (newChecked: boolean) => {
setIsChecked(newChecked);
if (onChange) {
onChange({} as React.FormEvent<HTMLButtonElement>);
}
};
return ( return (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
data-slot="checkbox" data-slot="checkbox"
@@ -12,6 +28,8 @@ function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxP
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', 'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
checked={isChecked}
onCheckedChange={handleCheckChange}
{...props} {...props}
> >
<CheckboxPrimitive.Indicator <CheckboxPrimitive.Indicator
@@ -2,7 +2,7 @@ import * as React from 'react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
function Input({ className, type, ...props }: React.ComponentProps<'input'>) { function Input({ className, type, value, onChange, ...props }: React.ComponentProps<'input'>) {
return ( return (
<input <input
type={type} type={type}
@@ -13,6 +13,8 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className className
)} )}
value={value ?? ''}
onChange={onChange}
{...props} {...props}
/> />
) )
+2 -2
View File
@@ -24,7 +24,7 @@ const HomeLayout: FC<IProps> = ({ NoteForm, Preview, History }) => {
<div className="flex h-screen flex-col overflow-hidden"> <div className="flex h-screen flex-col overflow-hidden">
<ResizablePanelGroup direction="horizontal" className="h-full w-full"> <ResizablePanelGroup direction="horizontal" className="h-full w-full">
{/* 左边表单 */} {/* 左边表单 */}
<ResizablePanel defaultSize={18} minSize={10} maxSize={35}> <ResizablePanel defaultSize={23} minSize={10} maxSize={35}>
<aside className="flex h-full flex-col overflow-hidden border-r border-neutral-200 bg-white"> <aside className="flex h-full flex-col overflow-hidden border-r border-neutral-200 bg-white">
<header className="flex h-16 items-center justify-between px-6"> <header className="flex h-16 items-center justify-between px-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -68,7 +68,7 @@ const HomeLayout: FC<IProps> = ({ NoteForm, Preview, History }) => {
<ResizableHandle /> <ResizableHandle />
{/* 右边预览 */} {/* 右边预览 */}
<ResizablePanel defaultSize={55} minSize={30}> <ResizablePanel defaultSize={61} minSize={30}>
<main className="flex h-full flex-col overflow-hidden bg-white p-6">{Preview}</main> <main className="flex h-full flex-col overflow-hidden bg-white p-6">{Preview}</main>
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
@@ -43,7 +43,7 @@ import { useNavigate } from 'react-router-dom'
/* -------------------- 校验 Schema -------------------- */ /* -------------------- 校验 Schema -------------------- */
const formSchema = z const formSchema = z
.object({ .object({
video_url: z.string(), video_url: z.string().optional(),
platform: z.string().nonempty('请选择平台'), platform: z.string().nonempty('请选择平台'),
quality: z.enum(['fast', 'medium', 'slow']), quality: z.enum(['fast', 'medium', 'slow']),
screenshot: z.boolean().optional(), screenshot: z.boolean().optional(),
@@ -60,10 +60,10 @@ const formSchema = z
.optional(), .optional(),
}) })
.superRefine(({ video_url, platform }, ctx) => { .superRefine(({ video_url, platform }, ctx) => {
if (platform === 'local' || platform === 'douyin') { if (platform === 'local' && !video_url) {
if (!video_url) { ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] })
ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] }) } else if (!video_url) {
} ctx.addIssue({ code: 'custom', message: '视频链接不能为空', path: ['video_url'] })
} else { } else {
try { try {
const url = new URL(video_url) const url = new URL(video_url)
@@ -202,7 +202,7 @@ const NoteForm = () => {
setUploadSuccess(true) setUploadSuccess(true)
} catch (err) { } catch (err) {
console.error('上传失败:', err) console.error('上传失败:', err)
message.error('上传失败,请重试') // message.error('上传失败,请重试')
} finally { } finally {
setIsUploading(false) setIsUploading(false)
} }
@@ -220,13 +220,13 @@ const NoteForm = () => {
return return
} }
message.success('已提交任务') // message.success('已提交任务')
const data = await generateNote(payload) const data = await generateNote(payload)
addPendingTask(data.task_id, values.platform, payload) addPendingTask(data.task_id, values.platform, payload)
} }
const onInvalid = (errors: FieldErrors<NoteFormValues>) => { const onInvalid = (errors: FieldErrors<NoteFormValues>) => {
console.warn('表单校验失败:', errors) console.warn('表单校验失败:', errors)
message.error('请完善所有必填项后再提交') // message.error('请完善所有必填项后再提交')
} }
const handleCreateNew = () => { const handleCreateNew = () => {
// 🔁 这里清空当前任务状态 // 🔁 这里清空当前任务状态
@@ -297,7 +297,7 @@ const NoteForm = () => {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<FormMessage /> <FormMessage style={{ display: 'none' }} />
</FormItem> </FormItem>
)} )}
/> />
@@ -314,7 +314,7 @@ const NoteForm = () => {
) : ( ) : (
<Input disabled={!!editing} placeholder="请输入视频网站链接" {...field} /> <Input disabled={!!editing} placeholder="请输入视频网站链接" {...field} />
)} )}
<FormMessage /> <FormMessage style={{ display: 'none' }} />
</FormItem> </FormItem>
)} )}
/> />
@@ -77,16 +77,15 @@ const NoteHistory: FC<NoteHistoryProps> = ({ onSelect, selectedId }) => {
<div className="flex flex-col gap-2 overflow-hidden"> <div className="flex flex-col gap-2 overflow-hidden">
{filteredTasks.map(task => ( {filteredTasks.map(task => (
<div <div
onClick={() => onSelect(task.id)} key={task.id}
onClick={() => onSelect(task.id)}
className={cn( className={cn(
'flex cursor-pointer flex-col rounded-md border border-neutral-200 p-3', 'flex cursor-pointer flex-col rounded-md border border-neutral-200 p-3',
selectedId === task.id && 'border-primary bg-primary-light' selectedId === task.id && 'border-primary bg-primary-light'
)} )}
> >
<div <div
key={task.id}
className={cn('flex items-center gap-4')} className={cn('flex items-center gap-4')}
> >
{/* 封面图 */} {/* 封面图 */}
{task.platform === 'local' ? ( {task.platform === 'local' ? (
+22
View File
@@ -1,5 +1,6 @@
import re import re
from typing import Optional from typing import Optional
import requests
def extract_video_id(url: str, platform: str) -> Optional[str]: def extract_video_id(url: str, platform: str) -> Optional[str]:
@@ -11,6 +12,12 @@ def extract_video_id(url: str, platform: str) -> Optional[str]:
:return: 提取到的视频 ID 或 None :return: 提取到的视频 ID 或 None
""" """
if platform == "bilibili": if platform == "bilibili":
# 如果是短链接,则解析真实链接
if "b23.tv" in url:
resolved_url = resolve_bilibili_short_url(url)
if resolved_url:
url = resolved_url
# 匹配 BV号(如 BV1vc411b7Wa # 匹配 BV号(如 BV1vc411b7Wa
match = re.search(r"BV([0-9A-Za-z]+)", url) match = re.search(r"BV([0-9A-Za-z]+)", url)
return f"BV{match.group(1)}" if match else None return f"BV{match.group(1)}" if match else None
@@ -26,3 +33,18 @@ def extract_video_id(url: str, platform: str) -> Optional[str]:
return match.group(1) if match else None return match.group(1) if match else None
return None return None
def resolve_bilibili_short_url(short_url: str) -> Optional[str]:
"""
解析哔哩哔哩短链接以获取真实视频链接
:param short_url: Bilibili短链接(如"https://b23.tv/xxxxxx"
:return: 真实的视频链接或None
"""
try:
response = requests.head(short_url, allow_redirects=True)
return response.url
except requests.RequestException as e:
print(f"Error resolving short URL: {e}")
return None
@@ -1,5 +1,6 @@
from pydantic import AnyUrl, validator, BaseModel, field_validator from pydantic import AnyUrl, validator, BaseModel, field_validator
import re import re
from urllib.parse import urlparse
SUPPORTED_PLATFORMS = { SUPPORTED_PLATFORMS = {
"bilibili": r"(https?://)?(www\.)?bilibili\.com/video/[a-zA-Z0-9]+", "bilibili": r"(https?://)?(www\.)?bilibili\.com/video/[a-zA-Z0-9]+",
@@ -10,6 +11,12 @@ SUPPORTED_PLATFORMS = {
def is_supported_video_url(url: str) -> bool: def is_supported_video_url(url: str) -> bool:
parsed = urlparse(url)
# 检查是否为Bilibili的短链接
if parsed.netloc == "b23.tv":
return True
for name, pattern in SUPPORTED_PLATFORMS.items(): for name, pattern in SUPPORTED_PLATFORMS.items():
if pattern in ["douyin", "kuaishou"]: if pattern in ["douyin", "kuaishou"]:
if pattern in url: if pattern in url: