From 3a801ba016468d7c90d3dbd182d425d4b3372de6 Mon Sep 17 00:00:00 2001 From: HoshinoSuzumi Date: Sun, 8 Feb 2026 21:16:25 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8chore:=20=E4=BD=BF=E7=94=A8=20oxlin?= =?UTF-8?q?t,=20oxfmt&=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy.yml | 6 +- .github/copilot-instructions.md | 38 +- .oxfmtrc.json | 16 + .oxlintrc.json | 40 ++ .prettierrc | 2 +- .vscode/extensions.json | 3 + .vscode/settings.json | 8 + README.md | 2 +- components/BubbleTitle.vue | 14 +- components/DatePicker.vue | 34 +- components/DigitalHumanTrainCreator.vue | 149 +++++-- components/GradientDivider.vue | 4 +- components/Icon/MessageResponding.vue | 257 +++++++++--- components/ImagePlaceholder.vue | 17 +- components/LoginNeededContent.vue | 33 +- components/Markdown.vue | 31 +- components/ModalDigitalHumanSelect.vue | 4 +- components/SlideCreateCourse.vue | 20 +- components/SlideCreateCourseGreen.vue | 8 +- components/aigc/NavItem.vue | 17 +- components/aigc/RatioSelector.vue | 51 ++- components/aigc/ReferenceFigureSelector.vue | 145 +++++-- components/aigc/chat/ChatItem.vue | 32 +- components/aigc/chat/Message.vue | 53 ++- components/aigc/chat/NewSessionScreen.vue | 179 ++++++--- components/aigc/drawing/OptionBlock.vue | 42 +- components/aigc/drawing/ResultBlock.vue | 256 ++++++++---- components/aigc/drawing/index.d.ts | 2 +- components/aigc/generation/GBTaskCard.vue | 321 +++++++++++---- components/aigc/generation/SRTEditor.vue | 5 +- components/uni/Button/index.d.ts | 2 +- components/uni/Button/index.vue | 57 ++- components/uni/Copyable/index.vue | 141 +++++-- components/uni/FileDnD/index.vue | 63 +-- components/uni/Icon/CircleError.vue | 23 +- components/uni/Icon/CircleInfo.vue | 16 +- components/uni/Icon/CircleSuccess.vue | 23 +- components/uni/Icon/CircleWarning.vue | 23 +- components/uni/Icon/Spinner.vue | 31 +- components/uni/Input/index.vue | 79 ++-- components/uni/Message/Provider.vue | 53 ++- components/uni/Message/index.d.ts | 2 +- components/uni/Message/index.vue | 61 ++- components/uni/Select/index.d.ts | 2 +- components/uni/Select/index.vue | 162 +++++--- components/uni/TextArea/index.vue | 76 ++-- components/uni/Toggle/index.vue | 66 +-- composables/fetchCourseSubtitleUrl.ts | 6 +- composables/useBlobUrlFromB64.ts | 6 +- composables/useDefer.ts | 38 +- composables/useDownload.ts | 9 +- composables/useFetchWrapped.ts | 36 +- composables/useFormPayload.ts | 14 +- composables/useHistory.ts | 38 +- composables/useLLM.ts | 65 +-- composables/useLoginState.ts | 105 ++--- composables/useTourState.ts | 63 +-- composables/useVideoBackgroundCompositing.ts | 7 +- composables/useVideoSubtitleEmbedding.ts | 60 +-- package.json | 6 +- pages/aigc/chat/index.vue | 365 ++++++++++------- pages/aigc/draw/index.vue | 341 +++++++++++----- pages/aigc/navigation.vue | 4 +- pages/generation.vue | 10 +- .../generation/admin/digital-human-train.vue | 56 ++- pages/generation/admin/index.vue | 31 +- pages/generation/admin/materials.vue | 376 ++++++++++++++---- pages/generation/admin/users.vue | 33 +- pages/generation/course.vue | 123 +++--- pages/generation/green-screen.vue | 74 ++-- pages/generation/materials.vue | 60 +-- pages/index.vue | 12 +- pages/user/authenticate.vue | 2 +- pnpm-lock.yaml | 179 +++++++++ tailwind.config.ts | 10 +- tsconfig.json | 2 +- tsconfig.node.json | 2 +- typings/llm.ts | 33 +- 78 files changed, 3367 insertions(+), 1468 deletions(-) create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index d86eabc..c9cead4 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,7 +1,7 @@ name: 'CD' on: - push: + push: branches: - 'release/**' tags: @@ -25,10 +25,10 @@ jobs: - name: ⚙ Install dependencies run: pnpm i - + - name: 🔨 Genereate project run: pnpm generate - + - name: 📂 Sync deployment uses: SamKirkland/FTP-Deploy-Action@v4.3.5 with: diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0459a2f..0d936f2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -32,6 +32,7 @@ Pinia Stores(持久化状态) ``` **关键模式**: + - **Pinia stores 必须是单一实例**,通过 `storeToRefs()` 获取响应式引用 - **API 请求必须通过 `useFetchWrapped`** 来自动处理认证头(token/user_id) - **FFmpeg 采用单例模式**(`useFFmpeg()` 返回全局加载的实例),避免重复初始化 @@ -53,6 +54,7 @@ useFetchWrapped>( ``` **约定**: + - **每个请求必须包含** `token` 和 `user_id`(来自 `useLoginState`) - **响应结构统一**: `BaseResponse` 包含 `ret: number` 状态码和 `data: T` 数据 - **API_BASE 在 `nuxt.config.ts` 中定义**,所有请求都相对于此 URL @@ -60,11 +62,13 @@ useFetchWrapped>( ### 3. 媒体处理架构 #### FFmpeg 初始化流程 + - **单例加载**: 首次调用 `useFFmpeg()` 时初始化,后续复用缓存的实例 - **WASM 资源加载**: 从 CDN(`cdn.jsdelivr.net`)加载 FFmpeg core、wasm、worker - **错误恢复**: 调用 `cleanupFFmpeg()` 清理资源并重置单例 #### 视频合成流程(核心用例) + ``` 输入: 透明通道视频 (WebM) + 背景图 (PNG/File) ↓ @@ -82,7 +86,9 @@ useFetchWrapped>( ### 4. UI 组件架构 #### 内置组件库(`components/uni/`) + 自定义包装组件,提供统一 API: + - `UniButton`: 按钮 + loading 状态 - `UniInput`/`UniTextArea`: 表单输入 - `UniSelect`: 下拉选择 @@ -90,8 +96,9 @@ useFetchWrapped>( - `UniCopyable`: 可复制文本 **消息通知用法**: + ```typescript -const toast = useToast() // Radix Vue 的 Toast(顶部通知) +const toast = useToast() // Radix Vue 的 Toast(顶部通知) // 或从 provide 注入 const messageApi = inject('uni-message') messageApi.success('操作成功') @@ -99,6 +106,7 @@ messageApi.error('操作失败', 5000) ``` #### Radix Vue + Nuxt UI 集成 + - 使用 Radix Vue for 基础组件(button, dialog, select) - Nuxt UI 用于高级组件 + 主题管理 - **颜色方案**: primary='indigo', gray='neutral',详见 `app.config.ts` @@ -106,6 +114,7 @@ messageApi.error('操作失败', 5000) ### 5. 路由与页面结构 **目录映射**: + ``` pages/ ├── generation.vue (导航枢纽) @@ -122,6 +131,7 @@ pages/ ``` **导航约定**: + - `/generation` → 功能导航页面 - `/aigc/chat` → 聊天/文本生成 - `/generation/course` → 视频生成工作流 @@ -130,6 +140,7 @@ pages/ ## 开发工作流 ### 启动项目 + ```bash ni # 安装依赖 nr dev # 启动 http://localhost:3000 @@ -139,11 +150,13 @@ nr generate # 生产构建 (生成静态文件) ### 常见任务 **添加新的 API 端点**: + 1. 定义 Request 和 Response 类型(参考 `typings/llm.ts`) 2. 在 composable 中使用 `useFetchWrapped` 调用 3. 自动包含 token/user_id(来自 `useLoginState`) **添加新的视频处理功能**: + 1. 使用 `useFFmpeg()` 获取实例(自动初始化) 2. 写入文件到 vFS: `ffmpeg.writeFile()` 3. 执行命令:`ffmpeg.exec([...filterArgs])` @@ -151,6 +164,7 @@ nr generate # 生产构建 (生成静态文件) 5. 使用 progress callback 通报处理进度 **添加新的 UI 组件**: + 1. 创建在 `components/` 下(自动注册) 2. 优先使用 Radix Vue + Nuxt UI(已集成) 3. 使用 Tailwind CSS utility classes + `@apply` 指令 @@ -159,22 +173,26 @@ nr generate # 生产构建 (生成静态文件) ## 项目特定的约定 ### 类型定义位置 + - **LLM 相关**: `typings/llm.ts`(ChatMessage, ChatSession, ModelTag, LLMModal) - **全局类型**: `typings/types.d.ts`(BaseResponse, AuthedRequest, UserSchema) - **组件接口**: 组件目录下的 `index.d.ts`(例 `components/aigc/drawing/index.d.ts`) ### 命名规范 + - **Composables**: `use` 前缀(`useLoginState`, `useLLM`) - **Stores**: `use` + 功能名(`useHistory`, `useTourState`) - **组件**: PascalCase(`ChatItem.vue`, `ModalAuthentication.vue`) - **工具函数**: camelCase,放在 `composables/` 或各功能目录 ### 响应式数据模式 + - **Pinia store 返回值**: 必须通过 `storeToRefs()` 才能保持响应式 - **模板中的 ref**: 直接访问(Vue 自动展开) - **跨组件数据**: 优先使用 Pinia store(带持久化) ### 进度反馈与错误处理 + - **长时间操作** (视频处理): 通过 callback 函数报告 progress(0-100) - **错误处理**: 返回 Promise reject,上层 catch 处理;可选通过 toast/message 提示 - **FFmpeg 错误**: 捕获 exitCode 非零,记录详细的 FFmpeg 输出 @@ -182,6 +200,7 @@ nr generate # 生产构建 (生成静态文件) ## 依赖与性能优化 ### 关键依赖 + - **@ffmpeg/ffmpeg@0.12.15**: WASM 视频处理(从 CDN 加载) - **@webav/av-cliper**: 客户端视频剪辑库 - **markdown-it + highlight.js**: 内容渲染(支持代码高亮) @@ -189,13 +208,16 @@ nr generate # 生产构建 (生成静态文件) - **idb-keyval**: IndexedDB 简化操作(缓存大文件) ### Vite 优化设置 + ```typescript // nuxt.config.ts 中排除以下包进行优化,避免 bundling WASM optimizeDeps.exclude: ['@ffmpeg/ffmpeg', 'idb-keyval', '@webav/av-cliper', 'gsap', 'markdown-it'] ``` ### 构建排除项 + Worker 格式设置为 ES Module,避免 Vite 默认处理: + ```typescript vite.worker.format = 'es' ``` @@ -209,13 +231,13 @@ vite.worker.format = 'es' ## 常见陷阱与解决方案 -| 问题 | 原因 | 解决方案 | -|------|------|--------| -| API 请求 401 | 缺少 token 或已过期 | 检查 `useLoginState().token`,通过 ModalAuthentication 重新登录 | -| FFmpeg 加载超时 | CDN 资源加载慢 | 检查网络,可切换到本地 `/public/assets/ffmpeg` | -| 视频输出无声音 | 滤镜链未映射音频 | 确保 FFmpeg 命令包含 `-map '1:a?'` 映射音频轨道 | -| 组件未注册 | 文件位置错误 | 确保在 `components/` 目录下,子目录自动扁平化注册 | -| Pinia 状态未持久化 | 未配置 persist 选项 | 在 store 返回语句后添加 persist 配置(参考 `useLoginState`) | +| 问题 | 原因 | 解决方案 | +| ------------------ | ------------------- | --------------------------------------------------------------- | +| API 请求 401 | 缺少 token 或已过期 | 检查 `useLoginState().token`,通过 ModalAuthentication 重新登录 | +| FFmpeg 加载超时 | CDN 资源加载慢 | 检查网络,可切换到本地 `/public/assets/ffmpeg` | +| 视频输出无声音 | 滤镜链未映射音频 | 确保 FFmpeg 命令包含 `-map '1:a?'` 映射音频轨道 | +| 组件未注册 | 文件位置错误 | 确保在 `components/` 目录下,子目录自动扁平化注册 | +| Pinia 状态未持久化 | 未配置 persist 选项 | 在 store 返回语句后添加 persist 配置(参考 `useLoginState`) | ## 资源链接 diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..f99bc53 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,16 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "singleQuote": true, + "jsxSingleQuote": true, + "htmlWhitespaceSensitivity": "ignore", + "printWidth": 80, + "tabWidth": 2, + "bracketSpacing": true, + "semi": false, + "trailingComma": "es5", + "vueIndentScriptAndStyle": false, + "bracketSameLine": false, + "singleAttributePerLine": true, + "experimentalSortPackageJson": false, + "ignorePatterns": [] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..b99210f --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,40 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": null, + "categories": {}, + "rules": {}, + "settings": { + "jsx-a11y": { + "polymorphicPropName": null, + "components": {}, + "attributes": {} + }, + "next": { + "rootDir": [] + }, + "react": { + "formComponents": [], + "linkComponents": [], + "version": null, + "componentWrapperFunctions": [] + }, + "jsdoc": { + "ignorePrivate": false, + "ignoreInternal": false, + "ignoreReplacesDocs": true, + "overrideReplacesDocs": true, + "augmentsExtendsReplacesDocs": false, + "implementsReplacesDocs": false, + "exemptDestructuredRootsFromChecks": false, + "tagNamePreference": {} + }, + "vitest": { + "typecheck": false + } + }, + "env": { + "builtin": true + }, + "globals": {}, + "ignorePatterns": [] +} diff --git a/.prettierrc b/.prettierrc index cdb284f..3cb9919 100644 --- a/.prettierrc +++ b/.prettierrc @@ -10,4 +10,4 @@ "vueIndentScriptAndStyle": false, "bracketSameLine": false, "singleAttributePerLine": true -} \ No newline at end of file +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..99e2f7d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["oxc.oxc-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d599423 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "oxc.fmt.configPath": ".oxfmtrc.json", + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "always" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 3609b56..c3a8cce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # XSH 数字人微课平台 Next -*🚧 文档施工中* +_🚧 文档施工中_ ## Setup diff --git a/components/BubbleTitle.vue b/components/BubbleTitle.vue index f9050b6..648c983 100644 --- a/components/BubbleTitle.vue +++ b/components/BubbleTitle.vue @@ -25,15 +25,19 @@ const props = defineProps({

{{ subtitle }}

+ > + {{ subtitle }} + -

+

{{ title }}

- +
- \ No newline at end of file + diff --git a/components/DatePicker.vue b/components/DatePicker.vue index 16913a5..6675b50 100644 --- a/components/DatePicker.vue +++ b/components/DatePicker.vue @@ -1,18 +1,23 @@ @@ -299,7 +307,11 @@ const showAuthModal = ref(false) - + - + - + - \ No newline at end of file + diff --git a/components/GradientDivider.vue b/components/GradientDivider.vue index fcb1c9c..4604951 100644 --- a/components/GradientDivider.vue +++ b/components/GradientDivider.vue @@ -27,6 +27,4 @@ const props = defineProps({ >
- \ No newline at end of file + diff --git a/components/Icon/MessageResponding.vue b/components/Icon/MessageResponding.vue index 798a680..ca90376 100644 --- a/components/Icon/MessageResponding.vue +++ b/components/Icon/MessageResponding.vue @@ -1,52 +1,217 @@ \ No newline at end of file + diff --git a/components/ImagePlaceholder.vue b/components/ImagePlaceholder.vue index 85b1647..9936517 100644 --- a/components/ImagePlaceholder.vue +++ b/components/ImagePlaceholder.vue @@ -2,26 +2,29 @@ const props = defineProps({ gradient: { type: String, - default: '90deg, #FFC0CB 0%, #FFC0CB 100%' + default: '90deg, #FFC0CB 0%, #FFC0CB 100%', }, aspect: { type: String, - default: '16/9' - } + default: '16/9', + }, }) const elem = ref() const size = computed(() => { return { width: elem.value?.getBoundingClientRect().width.toFixed(0), - height: elem.value?.getBoundingClientRect().height.toFixed(0) + height: elem.value?.getBoundingClientRect().height.toFixed(0), } }) diff --git a/components/uni/Input/index.vue b/components/uni/Input/index.vue index 6aa79d1..9f80a93 100644 --- a/components/uni/Input/index.vue +++ b/components/uni/Input/index.vue @@ -1,31 +1,33 @@ diff --git a/components/uni/Message/Provider.vue b/components/uni/Message/Provider.vue index 2de2d03..cc5fd69 100644 --- a/components/uni/Message/Provider.vue +++ b/components/uni/Message/Provider.vue @@ -1,24 +1,32 @@