mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-07 08:12:47 +08:00
@@ -2,7 +2,7 @@ import './App.css'
|
||||
import { HomePage } from './pages/HomePage/Home.tsx'
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling.ts'
|
||||
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 Index from '@/pages/Index.tsx'
|
||||
import NotFoundPage from '@/pages/NotFoundPage'
|
||||
@@ -43,7 +43,7 @@ function App() {
|
||||
// 后端已初始化,渲染主应用
|
||||
return (
|
||||
<>
|
||||
<HashRouter>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />}>
|
||||
<Route index element={<HomePage />} />
|
||||
@@ -62,7 +62,7 @@ function App() {
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</BrowserRouter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import * as React from 'react'
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { CheckIcon } from 'lucide-react'
|
||||
|
||||
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 (
|
||||
<CheckboxPrimitive.Root
|
||||
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',
|
||||
className
|
||||
)}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleCheckChange}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
function Input({ className, type, value, onChange, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
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',
|
||||
className
|
||||
)}
|
||||
value={value ?? ''}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ const HomeLayout: FC<IProps> = ({ NoteForm, Preview, History }) => {
|
||||
<div className="flex h-screen flex-col overflow-hidden">
|
||||
<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">
|
||||
<header className="flex h-16 items-center justify-between px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -68,7 +68,7 @@ const HomeLayout: FC<IProps> = ({ NoteForm, Preview, History }) => {
|
||||
<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>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
@@ -43,7 +43,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
/* -------------------- 校验 Schema -------------------- */
|
||||
const formSchema = z
|
||||
.object({
|
||||
video_url: z.string(),
|
||||
video_url: z.string().optional(),
|
||||
platform: z.string().nonempty('请选择平台'),
|
||||
quality: z.enum(['fast', 'medium', 'slow']),
|
||||
screenshot: z.boolean().optional(),
|
||||
@@ -60,10 +60,10 @@ const formSchema = z
|
||||
.optional(),
|
||||
})
|
||||
.superRefine(({ video_url, platform }, ctx) => {
|
||||
if (platform === 'local' || platform === 'douyin') {
|
||||
if (!video_url) {
|
||||
ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] })
|
||||
}
|
||||
if (platform === 'local' && !video_url) {
|
||||
ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] })
|
||||
} else if (!video_url) {
|
||||
ctx.addIssue({ code: 'custom', message: '视频链接不能为空', path: ['video_url'] })
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(video_url)
|
||||
@@ -202,7 +202,7 @@ const NoteForm = () => {
|
||||
setUploadSuccess(true)
|
||||
} catch (err) {
|
||||
console.error('上传失败:', err)
|
||||
message.error('上传失败,请重试')
|
||||
// message.error('上传失败,请重试')
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
@@ -220,13 +220,13 @@ const NoteForm = () => {
|
||||
return
|
||||
}
|
||||
|
||||
message.success('已提交任务')
|
||||
// message.success('已提交任务')
|
||||
const data = await generateNote(payload)
|
||||
addPendingTask(data.task_id, values.platform, payload)
|
||||
}
|
||||
const onInvalid = (errors: FieldErrors<NoteFormValues>) => {
|
||||
console.warn('表单校验失败:', errors)
|
||||
message.error('请完善所有必填项后再提交')
|
||||
// message.error('请完善所有必填项后再提交')
|
||||
}
|
||||
const handleCreateNew = () => {
|
||||
// 🔁 这里清空当前任务状态
|
||||
@@ -297,7 +297,7 @@ const NoteForm = () => {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
<FormMessage style={{ display: 'none' }} />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -314,7 +314,7 @@ const NoteForm = () => {
|
||||
) : (
|
||||
<Input disabled={!!editing} placeholder="请输入视频网站链接" {...field} />
|
||||
)}
|
||||
<FormMessage />
|
||||
<FormMessage style={{ display: 'none' }} />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -77,16 +77,15 @@ const NoteHistory: FC<NoteHistoryProps> = ({ onSelect, selectedId }) => {
|
||||
<div className="flex flex-col gap-2 overflow-hidden">
|
||||
{filteredTasks.map(task => (
|
||||
<div
|
||||
onClick={() => onSelect(task.id)}
|
||||
key={task.id}
|
||||
onClick={() => onSelect(task.id)}
|
||||
className={cn(
|
||||
'flex cursor-pointer flex-col rounded-md border border-neutral-200 p-3',
|
||||
selectedId === task.id && 'border-primary bg-primary-light'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
key={task.id}
|
||||
className={cn('flex items-center gap-4')}
|
||||
|
||||
>
|
||||
{/* 封面图 */}
|
||||
{task.platform === 'local' ? (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
if platform == "bilibili":
|
||||
# 如果是短链接,则解析真实链接
|
||||
if "b23.tv" in url:
|
||||
resolved_url = resolve_bilibili_short_url(url)
|
||||
if resolved_url:
|
||||
url = resolved_url
|
||||
|
||||
# 匹配 BV号(如 BV1vc411b7Wa)
|
||||
match = re.search(r"BV([0-9A-Za-z]+)", url)
|
||||
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 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
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
SUPPORTED_PLATFORMS = {
|
||||
"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:
|
||||
parsed = urlparse(url)
|
||||
|
||||
# 检查是否为Bilibili的短链接
|
||||
if parsed.netloc == "b23.tv":
|
||||
return True
|
||||
|
||||
for name, pattern in SUPPORTED_PLATFORMS.items():
|
||||
if pattern in ["douyin", "kuaishou"]:
|
||||
if pattern in url:
|
||||
|
||||
Reference in New Issue
Block a user