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 @@