From f06de558b00ee05f5c1bf254b3a08c20454d73e4 Mon Sep 17 00:00:00 2001 From: techotaku39 Date: Wed, 27 May 2026 12:07:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E4=BC=98=E5=8C=96=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E8=AF=A6=E6=83=85=E5=B8=83=E5=B1=80=E4=B8=8E=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=85=83=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/HomePage/components/History.tsx | 10 +- .../HomePage/components/MarkdownHeader.tsx | 215 ++++++++------ .../HomePage/components/MarkdownViewer.tsx | 149 ++++++---- .../pages/HomePage/components/NoteForm.tsx | 183 ++++++------ .../pages/HomePage/components/VideoBanner.tsx | 276 +++++++++++++++--- BillNote_frontend/src/services/auth.ts | 40 +++ BillNote_frontend/src/services/note.ts | 29 +- .../src/store/taskStore/index.ts | 263 +++++++++++++---- .../src/styles/markdown-responsive.css | 57 ++++ .../app/downloaders/bilibili_downloader.py | 66 ++++- backend/app/downloaders/douyin_downloader.py | 30 +- .../app/downloaders/kuaishou_downloader.py | 34 ++- backend/app/downloaders/youtube_downloader.py | 2 +- backend/app/routers/note.py | 59 +++- backend/app/services/note_storage.py | 242 +++++++++++++++ backend/app/services/video_metadata.py | 170 +++++++++++ doc/images/frontend-note-layout-metadata.png | Bin 0 -> 422508 bytes 17 files changed, 1448 insertions(+), 377 deletions(-) create mode 100644 BillNote_frontend/src/services/auth.ts create mode 100644 BillNote_frontend/src/styles/markdown-responsive.css create mode 100644 backend/app/services/note_storage.py create mode 100644 backend/app/services/video_metadata.py create mode 100644 doc/images/frontend-note-layout-metadata.png diff --git a/BillNote_frontend/src/pages/HomePage/components/History.tsx b/BillNote_frontend/src/pages/HomePage/components/History.tsx index 3f34ae86..3afd3f8b 100644 --- a/BillNote_frontend/src/pages/HomePage/components/History.tsx +++ b/BillNote_frontend/src/pages/HomePage/components/History.tsx @@ -1,19 +1,13 @@ import NoteHistory from '@/pages/HomePage/components/NoteHistory.tsx' import { useTaskStore } from '@/store/taskStore' -import { Info, Clock, Loader2 } from 'lucide-react' import { ScrollArea } from '@/components/ui/scroll-area.tsx' const History = () => { const currentTaskId = useTaskStore(state => state.currentTaskId) const setCurrentTask = useTaskStore(state => state.setCurrentTask) return ( <> -
- {/*生成历史 */} -
- -

生成历史

-
- +
+ {/*
*/} {/*
*/} diff --git a/BillNote_frontend/src/pages/HomePage/components/MarkdownHeader.tsx b/BillNote_frontend/src/pages/HomePage/components/MarkdownHeader.tsx index 89934b53..9a07b4ac 100644 --- a/BillNote_frontend/src/pages/HomePage/components/MarkdownHeader.tsx +++ b/BillNote_frontend/src/pages/HomePage/components/MarkdownHeader.tsx @@ -1,11 +1,12 @@ 'use client' import { useEffect, useState } from 'react' -import { Copy, Download, BrainCircuit, MessageSquare } from 'lucide-react' +import { Copy, Download, BrainCircuit, MessageSquare, Captions } from 'lucide-react' import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Badge } from '@/components/ui/badge' +import { videoPlatforms } from '@/constant/note' interface VersionNote { ver_id: string @@ -17,7 +18,8 @@ interface VersionNote { interface NoteHeaderProps { currentTask?: { markdown: VersionNote[] | string - } + } | null + platform?: string isMultiVersion: boolean currentVerId: string setCurrentVerId: (id: string) => void @@ -27,13 +29,17 @@ interface NoteHeaderProps { onCopy: () => void onDownload: () => void createAt?: string | Date + showTranscribe: boolean setShowTranscribe: (show: boolean) => void showChat?: false | 'half' | 'full' setShowChat?: (mode: false | 'half' | 'full') => void + viewMode: 'map' | 'preview' + setViewMode: (mode: 'map' | 'preview') => void } export function MarkdownHeader({ currentTask, + platform, isMultiVersion, currentVerId, setCurrentVerId, @@ -66,10 +72,10 @@ export function MarkdownHeader({ } const styleName = noteStyles.find(v => v.value === style)?.label || style - - const reversedMarkdown: VersionNote[] = Array.isArray(currentTask?.markdown) - ? [...currentTask!.markdown].reverse() - : [] + const versions: VersionNote[] = Array.isArray(currentTask?.markdown) ? currentTask.markdown : [] + const platformMeta = videoPlatforms.find(item => item.value === platform) + const platformName = platformMeta?.label || platform || '' + const PlatformLogo = platformMeta?.logo const formatDate = (date: string | Date | undefined) => { if (!date) return '' @@ -87,22 +93,21 @@ export function MarkdownHeader({ } return ( -
- {/* 左侧区域:版本 + 标签 + 创建时间 */} -
+
+
{isMultiVersion && ( )} - - {modelName} - - - {styleName} - + {platformName && ( + + {PlatformLogo && ( + + + + )} + {platformName} + + )} - {createAt && ( -
创建时间: {formatDate(createAt)}
+ {modelName && ( + + {modelName} + + )} + {styleName && ( + + {styleName} + )}
- {/* 右侧操作按钮 */} -
- - - - - - 思维导图 - - - - - - - - 复制内容 - - - - - - - - - 下载为 Markdown 文件 - - - - - - - - 原文参照 - - - {setShowChat && ( +
+
+ {createAt ? `创建时间: ${formatDate(createAt)}` : ''} +
+ +
+ + 切换视图 + + + + + + - 基于笔记内容的 AI 问答 + 原文参照 - )} + + {setShowChat && ( + + + + + + 基于笔记内容的 AI 问答 + + + )} + + + + + + + 复制内容 + + + + + + + + + 下载为 Markdown 文件 + + +
) diff --git a/BillNote_frontend/src/pages/HomePage/components/MarkdownViewer.tsx b/BillNote_frontend/src/pages/HomePage/components/MarkdownViewer.tsx index c34a0e54..357ce078 100644 --- a/BillNote_frontend/src/pages/HomePage/components/MarkdownViewer.tsx +++ b/BillNote_frontend/src/pages/HomePage/components/MarkdownViewer.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useMemo, memo, FC } from 'react' import ReactMarkdown from 'react-markdown' import { Button } from '@/components/ui/button.tsx' -import { Copy, Download, ArrowRight, Play, ExternalLink } from 'lucide-react' +import { Copy, Download, ArrowRight, Play, ExternalLink, X } from 'lucide-react' import { toast } from 'react-hot-toast' import Error from '@/components/Lottie/error.tsx' import Loading from '@/components/Lottie/Loading.tsx' @@ -17,6 +17,7 @@ import rehypeKatex from 'rehype-katex' import rehypeSlug from 'rehype-slug' import 'katex/dist/katex.min.css' import 'github-markdown-css/github-markdown-light.css' +import '@/styles/markdown-responsive.css' import { ScrollArea } from '@/components/ui/scroll-area.tsx' import { useTaskStore } from '@/store/taskStore' import { noteStyles } from '@/constant/note.ts' @@ -25,6 +26,7 @@ import TranscriptViewer from '@/pages/HomePage/components/transcriptViewer.tsx' import MarkmapEditor from '@/pages/HomePage/components/MarkmapComponent.tsx' import ChatPanel from '@/pages/HomePage/components/ChatPanel.tsx' import VideoBanner from '@/pages/HomePage/components/VideoBanner.tsx' +import { withAuthTokenQuery } from '@/services/auth' interface VersionNote { ver_id: string @@ -58,7 +60,7 @@ function createMarkdownComponents(baseURL: string) { return { h1: ({ children, ...props }: any) => (

{children} @@ -66,7 +68,7 @@ function createMarkdownComponents(baseURL: string) { ), h2: ({ children, ...props }: any) => (

{children} @@ -74,7 +76,7 @@ function createMarkdownComponents(baseURL: string) { ), h3: ({ children, ...props }: any) => (

{children} @@ -82,21 +84,23 @@ function createMarkdownComponents(baseURL: string) { ), h4: ({ children, ...props }: any) => (

{children}

), p: ({ children, ...props }: any) => ( -

+

{children}

), a: ({ href, children, ...props }: any) => { const isOriginLink = - typeof children[0] === 'string' && - (children[0] as string).startsWith('原片 @') + typeof children[0] === 'string' && (children[0] as string).startsWith('原片 @') if (isOriginLink) { const timeMatch = (children[0] as string).match(/原片 @ (\d{2}:\d{2})/) @@ -168,13 +172,11 @@ function createMarkdownComponents(baseURL: string) { href={href} target="_blank" rel="noopener noreferrer" - className="text-primary hover:text-primary/80 inline-flex items-center gap-0.5 font-medium underline underline-offset-4" + className="text-primary hover:text-primary/80 font-medium [overflow-wrap:anywhere] break-words underline underline-offset-4" {...props} > {children} - {href?.startsWith('http') && ( - - )} + {href?.startsWith('http') && } ) }, @@ -183,7 +185,7 @@ function createMarkdownComponents(baseURL: string) { if (src.startsWith('/')) { src = baseURL + src } - props.src = src + props.src = withAuthTokenQuery(src) return (
@@ -207,13 +209,11 @@ function createMarkdownComponents(baseURL: string) { const isFakeHeading = /^(\*\*.+\*\*)$/.test(rawText.trim()) if (isFakeHeading) { - return ( -
{children}
- ) + return
{children}
} return ( -
  • +
  • {children}
  • ) @@ -242,7 +242,7 @@ function createMarkdownComponents(baseURL: string) { if (!inline && match) { return ( -
    +
    {match[1].toUpperCase()}
    +
    +
    + +
    +
    + )} + {/* 侧边问答模式:markdown + ChatPanel 各占一半 */} + {showChat === 'half' && currentTask && ( +
    + +
    + )} + )} ) : ( diff --git a/BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx b/BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx index 9e9cbfa5..24645e07 100644 --- a/BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx +++ b/BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx @@ -7,8 +7,8 @@ import { FormLabel, FormMessage, } from '@/components/ui/form.tsx' -import { useEffect,useState } from 'react' -import { useForm, useWatch } from 'react-hook-form' +import { useEffect, useState } from 'react' +import { type FieldErrors, useForm, useWatch } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' @@ -25,7 +25,6 @@ import { 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, @@ -37,7 +36,6 @@ import { import { Input } from '@/components/ui/input.tsx' import { Textarea } from '@/components/ui/textarea.tsx' import { noteStyles, noteFormats, videoPlatforms } from '@/constant/note.ts' -import { fetchModels } from '@/services/model.ts' import { useNavigate } from 'react-router-dom' import toast from 'react-hot-toast' @@ -65,18 +63,14 @@ const formSchema = z if (!video_url) { ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] }) } - } - else { + } else { if (!video_url) { ctx.addIssue({ code: 'custom', message: '视频链接不能为空', path: ['video_url'] }) - } - else { + } else { try { const url = new URL(video_url) - if (!['http:', 'https:'].includes(url.protocol)) - throw new Error() - } - catch { + if (!['http:', 'https:'].includes(url.protocol)) throw new Error() + } catch { ctx.addIssue({ code: 'custom', message: '请输入正确的视频链接', path: ['video_url'] }) } } @@ -111,7 +105,7 @@ const CheckboxGroup = ({ onChange: (v: string[]) => void disabledMap: Record }) => ( -
    +
    {noteFormats.map(({ label, value: v }) => (