From bac96fcbe6e8f6453137df46b6185ef8c5b744f4 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Mon, 10 Nov 2025 00:59:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 30 + frontend/.cursorules/design.md | 63 + frontend/.cursorules/vue.md | 87 ++ frontend/.gitignore | 28 + frontend/app/web-gold/.editorconfig | 8 + frontend/app/web-gold/.env.development | 6 + frontend/app/web-gold/.env.production | 5 + frontend/app/web-gold/.gitattributes | 1 + frontend/app/web-gold/.gitignore | 30 + frontend/app/web-gold/.prettierrc.json | 6 + frontend/app/web-gold/.vscode/extensions.json | 9 + frontend/app/web-gold/Dockerfile | 37 + frontend/app/web-gold/eslint.config.js | 28 + frontend/app/web-gold/index.html | 13 + frontend/app/web-gold/jsconfig.json | 10 + frontend/app/web-gold/nginx.conf | 23 + frontend/app/web-gold/package.json | 56 + frontend/app/web-gold/postcss.config.js | 6 + frontend/app/web-gold/public/favicon.ico | Bin 0 -> 1940 bytes frontend/app/web-gold/src/App.vue | 88 ++ frontend/app/web-gold/src/api/auth.js | 237 +++ frontend/app/web-gold/src/api/chat.js | 74 + frontend/app/web-gold/src/api/common.js | 67 + frontend/app/web-gold/src/api/http.js | 80 + frontend/app/web-gold/src/api/tikhub.js | 5 + .../app/web-gold/src/api/tikhub/README.md | 180 +++ frontend/app/web-gold/src/api/tikhub/index.js | 7 + .../app/web-gold/src/api/tikhub/tikhub.js | 55 + frontend/app/web-gold/src/api/tikhub/types.js | 87 ++ .../web-gold/src/components/LoginModal.vue | 1005 ++++++++++++ .../web-gold/src/components/SidebarNav.vue | 123 ++ .../web-gold/src/components/TokenInput.vue | 102 ++ .../app/web-gold/src/components/TopNav.vue | 106 ++ .../web-gold/src/components/icons/Icon.vue | 28 + .../src/components/icons/SvgSprite.vue | 67 + frontend/app/web-gold/src/env.d.ts | 29 + frontend/app/web-gold/src/main.js | 19 + frontend/app/web-gold/src/router/index.js | 51 + frontend/app/web-gold/src/stores/prompt.js | 40 + frontend/app/web-gold/src/stores/user.js | 152 ++ frontend/app/web-gold/src/stores/voiceCopy.js | 62 + frontend/app/web-gold/src/style.css | 117 ++ frontend/app/web-gold/src/types/axios.d.ts | 17 + frontend/app/web-gold/src/types/global.d.ts | 29 + frontend/app/web-gold/src/types/vue-shim.d.ts | 6 + frontend/app/web-gold/src/utils/auth.js | 42 + frontend/app/web-gold/src/utils/markdown.js | 20 + frontend/app/web-gold/src/utils/storage.js | 56 + .../app/web-gold/src/utils/token-manager.js | 100 ++ frontend/app/web-gold/src/utils/url.js | 55 + .../src/views/capcut/CapcutImport.vue | 15 + .../src/views/content-style/Benchmark.vue | 1370 +++++++++++++++++ .../src/views/content-style/Copywriting.vue | 905 +++++++++++ frontend/app/web-gold/src/views/dh/Avatar.vue | 25 + .../app/web-gold/src/views/dh/VoiceCopy.vue | 313 ++++ .../web-gold/src/views/dh/VoiceGenerate.vue | 146 ++ frontend/app/web-gold/src/views/home/Home.vue | 781 ++++++++++ .../app/web-gold/src/views/misc/Download.vue | 15 + frontend/app/web-gold/src/views/misc/Help.vue | 15 + .../app/web-gold/src/views/misc/Theme.vue | 15 + .../app/web-gold/src/views/mix/MixEditor.vue | 22 + .../src/views/realtime/RealtimeHot.vue | 15 + .../web-gold/src/views/trends/Copywriting.vue | 16 + .../web-gold/src/views/trends/Forecast.vue | 642 ++++++++ .../app/web-gold/src/views/trends/Heat.vue | 217 +++ frontend/app/web-gold/tailwind.config.js | 11 + frontend/app/web-gold/tsconfig.json | 45 + frontend/app/web-gold/tsconfig.node.json | 11 + frontend/app/web-gold/vite.config.js | 87 ++ frontend/config/axios/config.js | 24 + frontend/doc/readme.md | 146 ++ .../doc/金牌内容大师软件需求文档-合规版.md | 187 +++ frontend/hooks/web/useCache.js | 39 + frontend/hooks/web/useVoiceText.ts | 87 ++ frontend/package.json | 18 + frontend/pnpm-workspace.yaml | 7 + 76 files changed, 8726 insertions(+) create mode 100644 frontend/.cursorules/design.md create mode 100644 frontend/.cursorules/vue.md create mode 100644 frontend/.gitignore create mode 100644 frontend/app/web-gold/.editorconfig create mode 100644 frontend/app/web-gold/.env.development create mode 100644 frontend/app/web-gold/.env.production create mode 100644 frontend/app/web-gold/.gitattributes create mode 100644 frontend/app/web-gold/.gitignore create mode 100644 frontend/app/web-gold/.prettierrc.json create mode 100644 frontend/app/web-gold/.vscode/extensions.json create mode 100644 frontend/app/web-gold/Dockerfile create mode 100644 frontend/app/web-gold/eslint.config.js create mode 100644 frontend/app/web-gold/index.html create mode 100644 frontend/app/web-gold/jsconfig.json create mode 100644 frontend/app/web-gold/nginx.conf create mode 100644 frontend/app/web-gold/package.json create mode 100644 frontend/app/web-gold/postcss.config.js create mode 100644 frontend/app/web-gold/public/favicon.ico create mode 100644 frontend/app/web-gold/src/App.vue create mode 100644 frontend/app/web-gold/src/api/auth.js create mode 100644 frontend/app/web-gold/src/api/chat.js create mode 100644 frontend/app/web-gold/src/api/common.js create mode 100644 frontend/app/web-gold/src/api/http.js create mode 100644 frontend/app/web-gold/src/api/tikhub.js create mode 100644 frontend/app/web-gold/src/api/tikhub/README.md create mode 100644 frontend/app/web-gold/src/api/tikhub/index.js create mode 100644 frontend/app/web-gold/src/api/tikhub/tikhub.js create mode 100644 frontend/app/web-gold/src/api/tikhub/types.js create mode 100644 frontend/app/web-gold/src/components/LoginModal.vue create mode 100644 frontend/app/web-gold/src/components/SidebarNav.vue create mode 100644 frontend/app/web-gold/src/components/TokenInput.vue create mode 100644 frontend/app/web-gold/src/components/TopNav.vue create mode 100644 frontend/app/web-gold/src/components/icons/Icon.vue create mode 100644 frontend/app/web-gold/src/components/icons/SvgSprite.vue create mode 100644 frontend/app/web-gold/src/env.d.ts create mode 100644 frontend/app/web-gold/src/main.js create mode 100644 frontend/app/web-gold/src/router/index.js create mode 100644 frontend/app/web-gold/src/stores/prompt.js create mode 100644 frontend/app/web-gold/src/stores/user.js create mode 100644 frontend/app/web-gold/src/stores/voiceCopy.js create mode 100644 frontend/app/web-gold/src/style.css create mode 100644 frontend/app/web-gold/src/types/axios.d.ts create mode 100644 frontend/app/web-gold/src/types/global.d.ts create mode 100644 frontend/app/web-gold/src/types/vue-shim.d.ts create mode 100644 frontend/app/web-gold/src/utils/auth.js create mode 100644 frontend/app/web-gold/src/utils/markdown.js create mode 100644 frontend/app/web-gold/src/utils/storage.js create mode 100644 frontend/app/web-gold/src/utils/token-manager.js create mode 100644 frontend/app/web-gold/src/utils/url.js create mode 100644 frontend/app/web-gold/src/views/capcut/CapcutImport.vue create mode 100644 frontend/app/web-gold/src/views/content-style/Benchmark.vue create mode 100644 frontend/app/web-gold/src/views/content-style/Copywriting.vue create mode 100644 frontend/app/web-gold/src/views/dh/Avatar.vue create mode 100644 frontend/app/web-gold/src/views/dh/VoiceCopy.vue create mode 100644 frontend/app/web-gold/src/views/dh/VoiceGenerate.vue create mode 100644 frontend/app/web-gold/src/views/home/Home.vue create mode 100644 frontend/app/web-gold/src/views/misc/Download.vue create mode 100644 frontend/app/web-gold/src/views/misc/Help.vue create mode 100644 frontend/app/web-gold/src/views/misc/Theme.vue create mode 100644 frontend/app/web-gold/src/views/mix/MixEditor.vue create mode 100644 frontend/app/web-gold/src/views/realtime/RealtimeHot.vue create mode 100644 frontend/app/web-gold/src/views/trends/Copywriting.vue create mode 100644 frontend/app/web-gold/src/views/trends/Forecast.vue create mode 100644 frontend/app/web-gold/src/views/trends/Heat.vue create mode 100644 frontend/app/web-gold/tailwind.config.js create mode 100644 frontend/app/web-gold/tsconfig.json create mode 100644 frontend/app/web-gold/tsconfig.node.json create mode 100644 frontend/app/web-gold/vite.config.js create mode 100644 frontend/config/axios/config.js create mode 100644 frontend/doc/readme.md create mode 100644 frontend/doc/金牌内容大师软件需求文档-合规版.md create mode 100644 frontend/hooks/web/useCache.js create mode 100644 frontend/hooks/web/useVoiceText.ts create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-workspace.yaml diff --git a/.gitignore b/.gitignore index 49330ee16f..e6837029ce 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ nbdist/ !*/build/*.html !*/build/*.xml + ### JRebel ### rebel.xml @@ -52,3 +53,32 @@ application-my.yaml /yudao-ui-app/unpackage/ **/.DS_Store + + + +node_modules +.vscode-test/ +*.vsix + +.idea + +pnpm-lock.yaml + +.clineignore + + +# Ignore coverage directories and files +coverage +coverage-unit +.nyc_output +# But don't ignore the coverage scripts in .github/scripts/ +!.github/scripts/coverage/ + +*evals.env + + +# E2E Tests +test-results + +## CLI pre-release ## +/cli diff --git a/frontend/.cursorules/design.md b/frontend/.cursorules/design.md new file mode 100644 index 0000000000..4e98c5df48 --- /dev/null +++ b/frontend/.cursorules/design.md @@ -0,0 +1,63 @@ +# 胶卷风格AI工具设计规范(二次创适用版) + + +## **核心风格定位** +「复古胶片暗调+现代代工具极简感」,以黑色基底为核心,叠加入胶片颗粒肌理,整体视觉克制而有质感,突出「剪辑工具的专业感」与「胶卷复古的氛围感」,避免与现有工具同质化。 + + +## **1. 颜色规范** +- **主色**: + - 背景:#0D0D0D(深黑,带1%青灰调,区别纯黑) + - 主功能色:#00B030(低饱和苔藓绿,用于按钮/选中态,与已知品牌色差异明显) +- **辅助色**: + - 交互蓝:#1A66E0(用于预览/保存等次级操作) + - 强调橙:#FF6A30(用于标记点/警告,低明度避免刺眼) +- **中性色**: + - 模块底:#1A1A1A(比背景亮5%,区分层级) + - 文本:#F2F2F2(正文)、#CCCCCC(次要文本) + - 边框:#333333(1px细线条,弱化割裂感) + + +## **2. 质感与阴影** +- **肌理**:全局叠加原创胶片颗粒(3%灰度噪点,随机生成,非真实胶卷扫描图) +- **阴影**: + - 卡片/模块:内阴影(0 2px 4px rgba(0,0,0,0.4)),无外阴影 + - 按钮hover:轻微发光(0 0 6px rgba(0,176,48,0.3),主色低饱和光晕) + + +## **3. 图标选型** +- **风格**:线性几何风,线条粗细1.5px,圆角2px +- **载体**:统一使用 SVG(图标文件与 SVG Sprite/Icon 组件),禁止使用位图作为图标 +- **禁用**:避免使用与知名剪辑工具高度相似的图标(如剪映、Pr的标志性符号) + + +## **4. 卡片规范** +- **形态**:圆角6px(非直角/大圆角),边框1px #333333 +- **内容区**:内边距16px,底部可加「胶片式参数条」(黑底白字小文本,如“1080p | 30fps”,纯装饰) +- **状态**: + - 活跃态:边框改为主色#00B030 + - hover态:背景色加深至#161616 + + +## **5. 布局规范** +- **整体结构**:顶部导航(高52px)+ 左侧功能栏(宽60px图标/200px展开)+ 主内容区(占比70%)+ 右侧参数面板(占比30%) +- **间距**:模块间margin 20px,元素内padding 12-16px,避免拥挤 +- **移动端**:左侧栏转为底部悬浮按钮组(4个核心功能+居中主按钮) + + +## **6. 标题与文本** +- **标题**:字体「Montserrat」(半粗体,20px),字间距0.5px,颜色#F2F2F2 +- **正文**:字体「Inter」(常规,14px),行高1.5,颜色#F2F2F2 +- **辅助文本**:字体「Inter」(常规,12px),颜色#CCCCCC + + +## **7. 设计提示词(供生成式设计/插画参考)** +- **总体风格**:复古胶片暗调、现代极简 UI、低饱和高级质感、专业剪辑工具氛围、控色节制 +- **质感**:微颗粒胶片噪点(3% 灰度随机)、内阴影层次、金属磨砂、磨光边缘 +- **配色**:深黑 #0D0D0D 背景、模块底 #1A1A1A、主色 #00B030、交互蓝 #1A66E0、强调橙 #FF6A30、细边界 #333333 +- **光影**:按钮 Hover 轻微发光(0 0 6px rgba(0,176,48,0.3))、卡片内阴影(inset 0 2px 4px rgba(0,0,0,0.4)) +- **形态**:圆角 6px、1px 细边、紧凑留白(内边距 12–16px,模块间距 20px) +- **图标**:线性几何、1.5px 描边、统一 SVG、避免品牌相似符号 +- **插画/装饰**:暗色渐变+噪点、胶片孔洞/标尺式细节可点缀,勿喧宾夺主 +- **可用性**:高对比可读性、色弱可访问、交互状态清晰(禁用/加载/选中) + diff --git a/frontend/.cursorules/vue.md b/frontend/.cursorules/vue.md new file mode 100644 index 0000000000..767fc359f0 --- /dev/null +++ b/frontend/.cursorules/vue.md @@ -0,0 +1,87 @@ +--- +description: 现代 Web 应用中的 Vue.js 最佳实践与模式 +globs: **/*.vue, **/*.ts, components/**/* +--- + +# Vue.js 最佳实践 + +## 组件结构 +- 优先使用组合式 API 而非选项式 API +- 保持组件小巧且功能专注 +- 采用恰当的 TypeScript 集成方案 +- 实现规范的 props 验证 +- 使用标准的 emit 声明 +- 保持模板逻辑简洁 +- 优先使用template 语法,而不是函数组件 + +## 组合式 API +- 正确使用 ref 与 reactive +- 合理实现生命周期钩子 +- 通过组合式函数封装可复用逻辑 +- 保持 setup 函数整洁 +- 规范使用计算属性 +- 合理实现侦听器 + +## 状态管理 +- 使用 Pinia 进行状态管理 +- 保持仓库模块化 +- 采用合理的状态组织方式 +- 规范实现操作逻辑 +- 正确使用获取器 +- 妥善处理异步状态 + +## 性能优化 +- 实现组件懒加载 +- 配置恰当的缓存策略 +- 高效使用计算属性 +- 避免不必要的侦听器 +- 区分使用 v-show 与 v-if +- 实现科学的 key 管理 + +## 路由管理 +- 规范使用 Vue Router +- 实现完整的导航守卫 +- 合理配置路由元字段 +- 正确处理路由参数 +- 实现路由懒加载 +- 使用标准的导航方法 + +## 表单处理 +- 正确使用 v-model +- 实现完善的验证机制 +- 规范处理表单提交 +- 展示合理的加载状态 +- 配置完整的错误处理 +- 实现表单重置功能 + +## TypeScript 集成 +- 使用规范的组件类型定义 +- 实现完整的 props 类型声明 +- 规范 emit 类型声明 +- 处理类型推断 +- 使用标准的组合函数类型 +- 实现完整的仓库类型定义 + +## 测试策略 +- 编写规范的单元测试 +- 实现完整的组件测试 +- 正确使用 Vue Test Utils +- 全面测试组合式函数 +- 实现科学的模拟机制 +- 测试异步操作流程 + +## 开发规范 +- 遵循 Vue 样式指南 +- 使用统一的命名约定 +- 保持组件结构清晰 +- 实现完整的错误处理 +- 规范事件处理机制 +- 为复杂逻辑添加文档注释 + +## 构建与工具链 +- 使用 Vite 进行开发 +- 配置完整的构建方案 +- 规范使用环境变量 +- 实现代码分割方案 +- 正确处理静态资源 +- 配置完整的优化策略 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000..2b960f3d2e --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,28 @@ +dist +node_modules +.vscode-test/ +*.vsix + +.DS_Store +.idea + +pnpm-lock.yaml + +.clineignore + + +# Ignore coverage directories and files +coverage +coverage-unit +.nyc_output +# But don't ignore the coverage scripts in .github/scripts/ +!.github/scripts/coverage/ + +*evals.env + + +# E2E Tests +test-results + +## CLI pre-release ## +/cli \ No newline at end of file diff --git a/frontend/app/web-gold/.editorconfig b/frontend/app/web-gold/.editorconfig new file mode 100644 index 0000000000..3b510aa687 --- /dev/null +++ b/frontend/app/web-gold/.editorconfig @@ -0,0 +1,8 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 100 diff --git a/frontend/app/web-gold/.env.development b/frontend/app/web-gold/.env.development new file mode 100644 index 0000000000..bf834e9a94 --- /dev/null +++ b/frontend/app/web-gold/.env.development @@ -0,0 +1,6 @@ +VITE_TIKHUB_TIKTOK_TOKEN=lcsEHcw6x1awim+hDZyN9xH2EgXKd5NcvTvzvlqtav/qvGiFH0rSdQzODQ== +VITE_TIKHUB_XHS_TOKEN=pZM4CJ484F+rxlyfLL+XmAzwMCKXVb5l2x7WsUOyCMu1rm61FiDcRQmPCQ== +VITE_DEV_TOKEN=a498c8db4b4e4dbfb9e28ad2606713ec +VITE_BASE_URL=/webApi +# 接口地址 +VITE_API_URL=/admin-api diff --git a/frontend/app/web-gold/.env.production b/frontend/app/web-gold/.env.production new file mode 100644 index 0000000000..66ab5c72b4 --- /dev/null +++ b/frontend/app/web-gold/.env.production @@ -0,0 +1,5 @@ +VITE_TIKHUB_TIKTOK_TOKEN=lcsEHcw6x1awim+hDZyN9xH2EgXKd5NcvTvzvlqtav/qvGiFH0rSdQzODQ== +VITE_TIKHUB_XHS_TOKEN=pZM4CJ484F+rxlyfLL+XmAzwMCKXVb5l2x7WsUOyCMu1rm61FiDcRQmPCQ== +VITE_BASE_URL=/webApi +# 接口地址 +VITE_API_URL=/admin-api diff --git a/frontend/app/web-gold/.gitattributes b/frontend/app/web-gold/.gitattributes new file mode 100644 index 0000000000..6313b56c57 --- /dev/null +++ b/frontend/app/web-gold/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/frontend/app/web-gold/.gitignore b/frontend/app/web-gold/.gitignore new file mode 100644 index 0000000000..8ee54e8d34 --- /dev/null +++ b/frontend/app/web-gold/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/frontend/app/web-gold/.prettierrc.json b/frontend/app/web-gold/.prettierrc.json new file mode 100644 index 0000000000..29a2402ef0 --- /dev/null +++ b/frontend/app/web-gold/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/frontend/app/web-gold/.vscode/extensions.json b/frontend/app/web-gold/.vscode/extensions.json new file mode 100644 index 0000000000..3f841264e6 --- /dev/null +++ b/frontend/app/web-gold/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "Vue.volar", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "oxc.oxc-vscode", + "esbenp.prettier-vscode" + ] +} diff --git a/frontend/app/web-gold/Dockerfile b/frontend/app/web-gold/Dockerfile new file mode 100644 index 0000000000..20f6205d7d --- /dev/null +++ b/frontend/app/web-gold/Dockerfile @@ -0,0 +1,37 @@ +FROM node:20-alpine AS builder + +# 使用 corepack 管理 pnpm,保持与仓库锁一致 +ENV PNPM_HOME=/root/.local/share/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable + +WORKDIR /workspace + + +# docker build -f app/web-gold/Dockerfile -t web-gold-nginx . +# docker run -d --name web-gold -p 8088:8088 web-gold-nginx + +# 复制整个仓库(建议在仓库根目录作为构建上下文执行: +# docker build -f app/web-gold/Dockerfile -t web-gold-nginx . +# ) +COPY . . + +# 安装依赖(工作区模式),严格锁定版本 +RUN pnpm -w install --frozen-lockfile + +# 构建前端应用(工作区内的 app/web-gold) +WORKDIR /workspace/app/web-gold +RUN pnpm build + +FROM nginx:alpine + +# 覆盖默认站点配置(支持 Vite SPA 刷新与可选反代) +COPY app/web-gold/nginx.conf /etc/nginx/conf.d/default.conf + +# 拷贝构建产物到 Nginx 根目录 +COPY --from=builder /workspace/app/web-gold/dist /usr/share/nginx/html + +EXPOSE 8088 +CMD ["nginx", "-g", "daemon off;"] + + diff --git a/frontend/app/web-gold/eslint.config.js b/frontend/app/web-gold/eslint.config.js new file mode 100644 index 0000000000..aaf1136b43 --- /dev/null +++ b/frontend/app/web-gold/eslint.config.js @@ -0,0 +1,28 @@ +import { defineConfig, globalIgnores } from 'eslint/config' +import globals from 'globals' +import js from '@eslint/js' +import pluginVue from 'eslint-plugin-vue' +import pluginOxlint from 'eslint-plugin-oxlint' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default defineConfig([ + { + name: 'app/files-to-lint', + files: ['**/*.{js,mjs,jsx,vue}'], + }, + + globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), + + { + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, + + js.configs.recommended, + ...pluginVue.configs['flat/essential'], + ...pluginOxlint.configs['flat/recommended'], + skipFormatting, +]) diff --git a/frontend/app/web-gold/index.html b/frontend/app/web-gold/index.html new file mode 100644 index 0000000000..b19040a0e6 --- /dev/null +++ b/frontend/app/web-gold/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/frontend/app/web-gold/jsconfig.json b/frontend/app/web-gold/jsconfig.json new file mode 100644 index 0000000000..2b82f87095 --- /dev/null +++ b/frontend/app/web-gold/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/frontend/app/web-gold/nginx.conf b/frontend/app/web-gold/nginx.conf new file mode 100644 index 0000000000..36019457e8 --- /dev/null +++ b/frontend/app/web-gold/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 8088; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Vite SPA:刷新/直达路由回退到 index.html + location / { + try_files $uri $uri/ /index.html; + } + + # 如需在生产环境继续代理后端,可在此追加;示例: + # location /webApi/ { + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_pass http://8.155.172.147:9900/; + # } +} + + diff --git a/frontend/app/web-gold/package.json b/frontend/app/web-gold/package.json new file mode 100644 index 0000000000..ffda06bb55 --- /dev/null +++ b/frontend/app/web-gold/package.json @@ -0,0 +1,56 @@ +{ + "name": "web-gold", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "scripts": { + "dev": "vite --mode development", + "build": "vite build", + "preview": "vite preview", + "lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore", + "lint:eslint": "eslint . --fix", + "lint": "run-s lint:*", + "format": "prettier --write src/" + }, + "dependencies": { + "@ant-design/icons-vue": "^7.0.1", + "@microsoft/fetch-event-source": "^2.0.1", + "@tailwindcss/vite": "^4.1.14", + "ant-design-vue": "^4.2.6", + "axios": "^1.12.2", + "dayjs": "^1.11.18", + "markdown-it": "^14.1.0", + "path-to-regexp": "^6.3.0", + "pinia": "^3.0.3", + "pinia-plugin-persistedstate": "^4.5.0", + "qs": "^6.14.0", + "vue": "^3.5.22", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@prettier/plugin-oxc": "^0.0.4", + "@tailwindcss/postcss": "^4.1.14", + "@vitejs/plugin-vue": "^6.0.1", + "@vitejs/plugin-vue-jsx": "^5.1.1", + "@vue/eslint-config-prettier": "^10.2.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.33.0", + "eslint-plugin-oxlint": "~1.11.0", + "eslint-plugin-vue": "~10.4.0", + "globals": "^16.3.0", + "normalize.css": "^8.0.1", + "npm-run-all2": "^8.0.4", + "oxlint": "~1.11.0", + "postcss": "^8.5.6", + "prettier": "3.6.2", + "tailwindcss": "^4.1.14", + "typescript": "^5.7.3", + "vite": "^7.1.7", + "vite-plugin-vue-devtools": "^8.0.2", + "vue-tsc": "^2.1.28" + } +} diff --git a/frontend/app/web-gold/postcss.config.js b/frontend/app/web-gold/postcss.config.js new file mode 100644 index 0000000000..14502dc1c2 --- /dev/null +++ b/frontend/app/web-gold/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + autoprefixer: {}, + }, +} diff --git a/frontend/app/web-gold/public/favicon.ico b/frontend/app/web-gold/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..70b2389750f2bea64a0dbf542d77cc0ae135030a GIT binary patch literal 1940 zcmV;F2W$9=P)e&^hC@AJM2A94AZz*QsA)^7P!hvOLsa_)oJtyRBrMRxu#fj8Pyh9ds_VaMf> zp$~$|DAPfUP=o>R!YDv;Ab>(|Dvn4-Q8SmZa>Zojzn(me&^=5 zI=7(J^6XZZxAEKMx`12gc%whNO{hXmUamCmGHG9Jz58J2v<65Fu*BX7P&fxKg^(UEU zHRzek#`4USlSM=zA?^g>dvy+fIJgS0?psd(Y&k|EEIcAE^ou$wu^d^8AKk{ZaDpN! zT%>SuK(+f_xZzFi$&~Z-`Y~{kk%)=V5gapqFjs+d0nMw?>eFKku?s{v@LZO_^*>2C zhDOWMo`2O&42l>JECuwOpK>^T1$2pFpW9XfW@RNv!8wbaOBho}+f9qc6ZNl4k1j3r z{oD`&%v|c6;$Pf;?!c`RuJN(-dP8ckxr>AOcd-r(oXcX;yh>>SBJof>Ps9P%2GYp1D#UTW09!|TS{EWl&nvrWSt{c*1wsf*lmX)k_X^fhihC`*IW?4?Hg*cC0 z^-S}}4rIGX;f~!=Bxum2vB=y`GK6rz+6 z@Y`58P7eXgD_!y`0+R64bDwjUsPqlL^!?+%DQoI>fzd4K4J$yq?_-Qxy#lZfPR0P| z``Z>&7r-C=Q^b#*$nnnCQhww_%)3rlQR=hl-$evU?>cHnsIDRfuT;_;xdZ2C)oH^J z1|T`vPi@HJ{>G;PiFYb{?n%KC9+=Syqa7dqdy==~;e?;>d;eHT5PFRru z%TgvFk^0O%mi9bwE!{CKP_rghZOKi#lLZ+N1)Gy5W~`i3B_Lvjh#ID9)i5cfra2k3 z3sE0>;vC9LL_X*#B5>%1msWLrUD46Tj+>udLug&cICKxw^Nmc)r-aln+4~2S?#MZk zd{9m!O%>QV>|c{gm=%b$=o*TXo^#ek{yxO^xn&xaF7Muzcd*3BTEy@cC|hv!}<3*1$7_K(OLP9uN=e-O}PZvNddACDR?q z0kK5ja3et&$9Pc1TfD3r~!zh> zYH)by!74Q0HyS%OeG(6Su^ivJzZA2_3{%zR!_=|Awe7;7q6YOLhDd3GoEU^*03}3~ zc7P4w;soir9A0cH!Gf_4H;pEw@*6W{t$X6Q*MfHpFH!K-SVpFlL6Fc7)2Jv53o!LJ zPJFO1kWLYS+`bx>ldvU8iX;-iD2F7;$tTt#YN=Am)0JxO7%iU#qYM<7w9iGb^iQAd zLfh%{IDf%H=@iNlLStZ*hGi#Emw(jYX(Hly-#FFPQqA2hRp{6=W^=A(oU(gI7wP#m zr5UX%G69umj4I0nYFhOOHFM%8)cBF1N@0*l)iTNfz0l8ws}xRUaRG5?_xxEksI0RRC1|Gi<}nE(I)21!Ig aR09C^1{ZF=YnE;R0000 +import { RouterView } from 'vue-router' +import { ref, onMounted } from 'vue' +import SidebarNav from './components/SidebarNav.vue' +import TopNav from './components/TopNav.vue' +import { theme } from 'ant-design-vue' +import SvgSprite from '@/components/icons/SvgSprite.vue' + +function readCssVar(name) { + return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || undefined +} + +const themeToken = ref({ + algorithm: theme.darkAlgorithm, + token: { + colorPrimary: '#3B82F6', + colorInfo: '#1A66E0', + colorBgBase: '#0D0D0D', + colorBgContainer: '#1A1A1A', + colorTextBase: '#F2F2F2', + colorTextSecondary: '#CCCCCC', + colorBorder: '#333333', + borderRadius: 6, + } +}) + +onMounted(() => { + // 运行时从 :root 读取,若存在则覆盖默认值 + const next = { ...themeToken.value.token } + next.colorPrimary = readCssVar('--color-primary') || next.colorPrimary + next.colorInfo = readCssVar('--color-blue') || next.colorInfo + next.colorBgBase = readCssVar('--color-bg') || next.colorBgBase + next.colorBgContainer = readCssVar('--color-surface') || next.colorBgContainer + next.colorTextBase = readCssVar('--color-text') || next.colorTextBase + next.colorTextSecondary = readCssVar('--color-text-secondary') || next.colorTextSecondary + next.colorBorder = readCssVar('--color-border') || next.colorBorder + themeToken.value = { algorithm: theme.darkAlgorithm, token: next } +}) + + + + + diff --git a/frontend/app/web-gold/src/api/auth.js b/frontend/app/web-gold/src/api/auth.js new file mode 100644 index 0000000000..72f30f880c --- /dev/null +++ b/frontend/app/web-gold/src/api/auth.js @@ -0,0 +1,237 @@ + +import api from '@/api/http' +import { setToken, getRefreshToken } from '@/utils/auth' + +const SERVER_BASE = import.meta.env.VITE_BASE_URL + '/app-api/member' + +/** + * 保存 token 的辅助函数 + * @param {Object} info - 包含 accessToken 和 refreshToken 的对象 + */ +function saveTokens(info) { + if (info?.accessToken || info?.refreshToken) { + setToken({ + accessToken: info.accessToken || '', + refreshToken: info.refreshToken || '', + }) + } +} + +/** + * 响应拦截(可选):统一错误处理 + */ +// api.interceptors.response.use( +// (resp) => resp, +// (err) => { +// // 统一错误日志/提示 +// return Promise.reject(err); +// } +// ); + +/** + * 短信场景枚举(请与后端配置保持一致) + * - MEMBER_LOGIN: 会员短信登录场景 + * - MEMBER_UPDATE_PASSWORD: 已登录用户修改密码(短信校验)场景 + * - MEMBER_RESET_PASSWORD: 未登录用户忘记密码重置(短信校验)场景 + * 如有"注册"独立场景,可在此添加:MEMBER_REGISTER: 13 + */ +export const SMS_SCENE = { + MEMBER_LOGIN: 1, + MEMBER_UPDATE_PASSWORD: 3, + MEMBER_RESET_PASSWORD: 4, +}; + +/** + * 短信模板编码常量 + */ +export const SMS_TEMPLATE_CODE = { + USER_REGISTER: 'muye-user-code', // 用户注册模板编码 +}; + +/** + * 账号密码登录 + * POST /member/auth/login + * + * @param {string} mobile - 手机号(必填) + * @param {string} password - 密码(必填,长度 4-16) + * @returns {Promise} data.data: { accessToken, refreshToken, expiresTime, userInfo } + */ +export async function loginByPassword(mobile, password) { + const { data } = await api.post(`${SERVER_BASE}/auth/login`, { mobile, password }); + const info = data || {}; + saveTokens(info); + return info; +} + +/** + * 发送短信验证码 + * POST /member/auth/send-sms-code + * + * @param {string} mobile - 手机号(登录/忘记密码等需要;修改密码场景可不传) + * @param {number} scene - 短信场景(见 SMS_SCENE) + * @param {string} templateCode - 模板编码(可选,如 'muye-user-code') + * @returns {Promise} 后端通用响应结构 + */ +export async function sendSmsCode(mobile, scene, templateCode) { + const body = { scene }; + if (mobile) body.mobile = mobile; // 修改密码场景后端会根据当前登录用户自动填充手机号 + if (templateCode) body.templateCode = templateCode; // 添加模板编码参数 + const { data } = await api.post(`${SERVER_BASE}/auth/send-sms-code`, body); + return data; +} + +/** + * 校验短信验证码(可选前置校验,一般直接在业务接口 use) + * POST /member/auth/validate-sms-code + * + * @param {string} mobile - 手机号(必填) + * @param {string} code - 验证码(必填,4-6 位数字) + * @param {number} scene - 短信场景(必填,见 SMS_SCENE) + * @returns {Promise} 后端通用响应结构 + */ +export async function validateSmsCode(mobile, code, scene) { + const { data } = await api.post(`${SERVER_BASE}/auth/validate-sms-code`, { mobile, code, scene }); + return data; +} + +/** + * 手机+验证码登录(首次即自动注册) + * POST /member/auth/sms-login + * + * @param {string} mobile - 手机号(必填) + * @param {string} code - 短信验证码(必填) + * @returns {Promise} data.data: { accessToken, refreshToken, expiresTime, userInfo } + */ +export async function loginBySms(mobile, code) { + const { data } = await api.post(`${SERVER_BASE}/auth/sms-login`, { mobile, code }); + const info = data || {}; + saveTokens(info); + return info; +} + +/** + * 刷新令牌 + * POST /member/auth/refresh-token?refreshToken=xxx + * + * @returns {Promise} data.data: { accessToken, refreshToken, expiresTime, userInfo } + */ +export async function refreshToken() { + const rt = getRefreshToken(); + if (!rt) throw new Error('缺少 refresh_token'); + // refreshToken 作为 URL 查询参数传递 + const { data } = await api.post(`${SERVER_BASE}/auth/refresh-token`, null, { params: { refreshToken: rt } }); + const info = data || {}; + saveTokens(info); + return info; +} + +/** + * 登录态下:发送“修改密码”验证码 + * - 场景:SMS_SCENE.MEMBER_UPDATE_PASSWORD + * - 特性:后端会以当前登录用户的手机号为准,无需传 mobile + * POST /member/auth/send-sms-code + * + * @returns {Promise} 后端通用响应结构 + */ +export async function sendUpdatePasswordCode() { + const { data } = await api.post(`${SERVER_BASE}/auth/send-sms-code`, { + scene: SMS_SCENE.MEMBER_UPDATE_PASSWORD, + }); + return data; +} + +/** + * 登录态下:通过短信验证码修改密码 + * PUT /member/user/update-password + * + * @param {string} newPassword - 新密码(必填,4-16 位) + * @param {string} smsCode - 短信验证码(必填,4-6 位数字) + * @returns {Promise} 后端通用响应结构 + */ +export async function updatePasswordBySmsCode(newPassword, smsCode) { + const { data } = await api.put(`${SERVER_BASE}/user/update-password`, { + password: newPassword, + code: smsCode, + }); + return data; +} + +/** + * 未登录:发送“忘记密码”验证码 + * - 场景:SMS_SCENE.MEMBER_RESET_PASSWORD + * POST /member/auth/send-sms-code + * + * @param {string} mobile - 手机号(必填) + * @returns {Promise} 后端通用响应结构 + */ +export async function sendResetPasswordCode(mobile) { + const { data } = await api.post(`${SERVER_BASE}/auth/send-sms-code`, { + mobile, + scene: SMS_SCENE.MEMBER_RESET_PASSWORD, + }); + return data; +} + +/** + * 未登录:通过手机验证码重置密码(忘记密码) + * PUT /member/user/reset-password + * + * @param {string} mobile - 手机号(必填) + * @param {string} newPassword - 新密码(必填,4-16 位) + * @param {string} smsCode - 短信验证码(必填,4-6 位数字) + * @returns {Promise} 后端通用响应结构 + */ +export async function resetPasswordBySms(mobile, newPassword, smsCode) { + const { data } = await api.put(`${SERVER_BASE}/user/reset-password`, { + mobile, + password: newPassword, + code: smsCode, + }); + return data; +} + +/** + * “手机+验证码+密码注册”组合流程(基于短信登录即注册 + 设置密码) + * 说明: + * - 1) 发送登录场景验证码(可选:若已拿到 code 可跳过) + * - 2) 短信登录:首次会自动注册并返回 token + * - 3) 登录态下发送“修改密码”验证码 + * - 4) 用短信验证码设置密码 + * + * @param {string} mobile - 手机号(必填) + * @param {string} loginCode - 第一次登录使用的短信验证码(必填) + * @param {string} newPassword - 注册后设置的新密码(必填) + * @param {string} updatePwdCode - 设置密码用到的短信验证码(必填) + * @returns {Promise} + */ +export async function registerWithMobileCodePassword(mobile, loginCode, newPassword, updatePwdCode) { + // 1) 可选:发送登录场景验证码(若已拿到 loginCode 可跳过) + // await sendSmsCode(mobile, SMS_SCENE.MEMBER_LOGIN); + + // 2) 短信登录(首次即注册) + await loginBySms(mobile, loginCode); + + // 3) 登录态下发送“修改密码”验证码(短信会发到当前账号绑定的手机号) + // await sendUpdatePasswordCode(); + + // 4) 用短信验证码设置密码 + await updatePasswordBySmsCode(newPassword, updatePwdCode); +} + +/** + * 导出一个默认对象,便于统一引入 + */ +export default { + SMS_SCENE, + SMS_TEMPLATE_CODE, + loginByPassword, + sendSmsCode, + validateSmsCode, + loginBySms, + refreshToken, + sendUpdatePasswordCode, + updatePasswordBySmsCode, + sendResetPasswordCode, + resetPasswordBySms, + registerWithMobileCodePassword, +}; diff --git a/frontend/app/web-gold/src/api/chat.js b/frontend/app/web-gold/src/api/chat.js new file mode 100644 index 0000000000..6fe8a16c2c --- /dev/null +++ b/frontend/app/web-gold/src/api/chat.js @@ -0,0 +1,74 @@ +import request from '@/api/http' +import { fetchEventSource } from '@microsoft/fetch-event-source' +import { getAccessToken } from '@/utils/auth' +const SERVER_BASE_AI= import.meta.env.VITE_BASE_URL + '/admin-api/ai' + + + + // AI chat 聊天 +export const ChatMessageApi = { + // 创建【我的】聊天对话 + createChatConversationMy: async (data) => { + return await request.post(`${SERVER_BASE_AI}/chat/conversation/create-my`, data) + }, + + + + // 发送 Stream 消息(对象入参,便于维护) + // 为什么不用 axios 呢?因为它不支持 SSE 调用 + sendChatMessageStream: async (options) => { + const { + conversationId, + content, + ctrl, + enableContext = true, + enableWebSearch = false, + onMessage, + onError, + onClose, + attachmentUrls = [] + } = options || {} + const token = getAccessToken() + + let retryCount = 0 + const maxRetries = 0 // 禁用自动重试 + + return fetchEventSource(`${SERVER_BASE_AI}/chat/message/send-stream`, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + openWhenHidden: true, + body: JSON.stringify({ + conversationId, + content, + useContext: enableContext, + useSearch: enableWebSearch, + attachmentUrls: attachmentUrls || [] + }), + onmessage: onMessage, + onerror: (err) => { + retryCount++ + console.error('SSE错误,重试次数:', retryCount, err) + + // 调用自定义错误处理 + if (typeof onError === 'function') { + onError(err) + } + + // 超过最大重试次数,停止重连 + if (retryCount > maxRetries) { + throw err // 抛出错误,停止自动重连 + } + }, + onclose: () => { + // 调用自定义关闭处理 + if (typeof onClose === 'function') { + onClose() + } + }, + signal: ctrl ? ctrl.signal : undefined + }) + }, +} diff --git a/frontend/app/web-gold/src/api/common.js b/frontend/app/web-gold/src/api/common.js new file mode 100644 index 0000000000..ea7fd9a6d4 --- /dev/null +++ b/frontend/app/web-gold/src/api/common.js @@ -0,0 +1,67 @@ +import http from '@/api/http' +import { fetchEventSource } from '@microsoft/fetch-event-source' +import { getAuthHeader } from '@/utils/token-manager' + +// 使用本地代理前缀 /tikhub,开发环境通过 Vite 代理到 https://api.tikhub.io +const SERVER_BASE = '/webApi/admin-api/ai/tikHup' + +export const CommonService = { + videoToCharacters(data) { + return http.post(`${SERVER_BASE}/videoToCharacters2`, data) + }, + callWorkflow(data) { + return http.post(`${SERVER_BASE}/callWorkflow`, data) + }, + + // 流式调用 workflow + callWorkflowStream: async (options) => { + const { + data, + ctrl, + onMessage, + onError, + onClose + } = options || {} + + const authHeader = getAuthHeader() + + let retryCount = 0 + const maxRetries = 0 // 禁用自动重试 + + return fetchEventSource(`${SERVER_BASE}/callWorkflow`, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + ...(authHeader ? { Authorization: authHeader } : {}) + }, + openWhenHidden: true, + body: JSON.stringify(data), + onmessage: onMessage, + onerror: (err) => { + retryCount++ + console.error('SSE错误,重试次数:', retryCount, err) + + // 调用自定义错误处理 + if (typeof onError === 'function') { + onError(err) + } + + // 超过最大重试次数,停止重连 + if (retryCount > maxRetries) { + throw err // 抛出错误,停止自动重连 + } + }, + onclose: () => { + // 调用自定义关闭处理 + if (typeof onClose === 'function') { + onClose() + } + }, + signal: ctrl ? ctrl.signal : undefined + }) + } +} + +export default CommonService + + diff --git a/frontend/app/web-gold/src/api/http.js b/frontend/app/web-gold/src/api/http.js new file mode 100644 index 0000000000..7632011640 --- /dev/null +++ b/frontend/app/web-gold/src/api/http.js @@ -0,0 +1,80 @@ +import axios from 'axios' +import { getAuthHeader } from '@/utils/token-manager' + +/** + * 不需要 token 的接口白名单 + * 支持完整路径匹配或路径包含匹配 + */ +const WHITE_LIST = [ + '/auth/login', // 密码登录 + '/auth/send-sms-code', // 发送验证码 + '/auth/sms-login', // 短信登录 + '/auth/validate-sms-code', // 验证验证码 + '/auth/register', // 注册(如果有) + '/auth/reset-password', // 重置密码 + '/auth/refresh-token', // 刷新token(可选,根据后端要求) +] + +/** + * 检查 URL 是否在白名单中 + * @param {string} url - 请求 URL + * @returns {boolean} + */ +function isInWhiteList(url) { + if (!url) return false + return WHITE_LIST.some((path) => url.includes(path)) +} + +/** + * 可选:多租户场景可在此处统一注入 tenant-id + * api.interceptors.request.use((config) => { + * config.headers['tenant-id'] = '1'; + * return config; + * }); + */ +// 创建 axios 实例 +const http = axios.create({ + baseURL: '/', + timeout: 180000, // 3分钟 +}) + +// 请求拦截:自动注入 Token +http.interceptors.request.use((config) => { + // 检查是否需要 token(不在白名单中且未显式设置 isToken = false) + const needToken = config.headers?.isToken !== false && !isInWhiteList(config.url || '') + + if (needToken) { + // 使用统一的 token 管理器获取 header + const authHeader = getAuthHeader() + if (authHeader) { + config.headers.Authorization = authHeader + } + } + + // 允许跨域第三方域名,使用绝对地址时保持原样 + return config +}) + +http.interceptors.response.use( + (resp) => { + const data = resp.data + // 检查响应数据中的 code 字段 + if (data && typeof data.code === 'number' && (data.code === 0 || data.code === 200)) { + return data + } else { + // code 不为 0 时,抛出错误 + const error = new Error(data?.message || data?.msg || '请求失败') + error.code = data?.code + error.data = data + return Promise.reject(error) + } + }, + (error) => { + // 统一错误处理:输出关键信息,便于排查 403 等问题 + return Promise.reject(error) + } +) + +export default http + + diff --git a/frontend/app/web-gold/src/api/tikhub.js b/frontend/app/web-gold/src/api/tikhub.js new file mode 100644 index 0000000000..73ac88d3b1 --- /dev/null +++ b/frontend/app/web-gold/src/api/tikhub.js @@ -0,0 +1,5 @@ +// TikHub API 统一导出入口 +export { TikhubService } from './tikhub/tikhub.js' +export { default } from './tikhub/tikhub.js' +export { InterfaceType, MethodType, InterfaceUrlMap, ParamType } from './tikhub/types.js' + diff --git a/frontend/app/web-gold/src/api/tikhub/README.md b/frontend/app/web-gold/src/api/tikhub/README.md new file mode 100644 index 0000000000..b1d3f61347 --- /dev/null +++ b/frontend/app/web-gold/src/api/tikhub/README.md @@ -0,0 +1,180 @@ +# TikHub API 模块 + +本模块提供了统一的 TikHub 接口调用中间层,支持多种平台的 API 接口。 + +## 目录结构 + +``` +tikhub/ +├── types.js # 枚举定义:InterfaceType、MethodType +├── tikhub.js # 核心服务类 TikhubService +├── index.js # 统一导出入口 +├── example.js # 使用示例 +└── README.md # 本文档 +``` + +## 快速开始 + +### 1. 导入模块 + +```javascript +import TikhubService, { InterfaceType, MethodType } from '@/api/tikhub' +``` + +### 2. 调用接口 + +```javascript +// 调用抖音热门搜索接口 +const response = await TikhubService.postTikHup( + InterfaceType.DOUYIN_WEB_HOT_SEARCH, // 接口类型 + MethodType.GET, // HTTP 方法 + { keyword: '测试关键词' } // 实际接口参数 +) +``` + +## API 说明 + +### TikhubService.postTikHup(type, methodType, urlParams) + +统一调用 TikHub 接口的中间层方法。 + +**参数:** +- `type` (String) - 接口类型,使用 `InterfaceType` 枚举值 +- `methodType` (String) - HTTP 方法类型,使用 `MethodType` 枚举值 +- `urlParams` (Object|String) - 实际接口的参数 + +**返回:** +- Promise - axios 响应对象 + +**示例:** + +```javascript +// 获取小红书热门榜单 +await TikhubService.postTikHup( + InterfaceType.XIAOHONGSHU_WEB_HOT_LIST, + MethodType.GET, + { page: 1, page_size: 20 } +) + +// 搜索抖音内容 +await TikhubService.postTikHup( + InterfaceType.DOUYIN_GENERAL_SEARCH_V4, + MethodType.POST, + { + keyword: '热门内容', + sort_type: 0, + publish_time: 0, + } +) +``` + +## 枚举说明 + +### InterfaceType - 接口类型 + +支持的所有接口类型: + +| 枚举值 | 说明 | +|--------|------| +| `XIAOHONGSHU_USER_INFO` | 小红书 - 获取用户信息 | +| `DOUYIN_WEB_USER_POST_VIDEOS` | 抖音 - 网页端获取用户发布的视频 | +| `DOUYIN_APP_USER_POST_VIDEOS` | 抖音 - APP端V3获取用户发布的视频 | +| `DOUYIN_APP_GENERAL_SEARCH` | 抖音 - APP端V3通用搜索结果 | +| `DOUYIN_SEARCH_SUGGEST` | 抖音 - 搜索建议 | +| `DOUYIN_GENERAL_SEARCH_V4` | 抖音 - 通用搜索V4 | +| `XIAOHONGSHU_WEB_HOT_LIST` | 小红书 - 网页端V2热门榜单 | +| `DOUYIN_WEB_HOT_SEARCH` | 抖音 - 网页端热门搜索结果 | +| `KUAISHOU_WEB_HOT_LIST` | 快手 - 网页端热门榜单V2 | +| `BILIBILI_WEB_POPULAR` | B站 - 网页端流行内容 | +| `WEIBO_WEB_HOT_SEARCH` | 微博 - 网页端V2热门搜索指数 | +| `DOUYIN_WEB_GENERAL_SEARCH` | 抖音 - 网页端通用搜索结果 | + +### MethodType - HTTP 方法 + +| 枚举值 | 说明 | +|--------|------| +| `GET` | GET 请求 | +| `POST` | POST 请求 | +| `PUT` | PUT 请求 | +| `DELETE` | DELETE 请求 | +| `PATCH` | PATCH 请求 | + +## 使用示例 + +详细的使用示例请参考 `example.js` 文件。 + +### 示例 1:获取抖音热门搜索 + +```javascript +import TikhubService, { InterfaceType, MethodType } from '@/api/tikhub' + +async function getDouyinHotSearch() { + try { + const response = await TikhubService.postTikHup( + InterfaceType.DOUYIN_WEB_HOT_SEARCH, + MethodType.GET, + { keyword: '测试关键词' } + ) + return response + } catch (error) { + console.error('调用失败:', error) + throw error + } +} +``` + +### 示例 2:获取用户发布的视频 + +```javascript +async function getUserVideos() { + return await TikhubService.postTikHup( + InterfaceType.DOUYIN_WEB_USER_POST_VIDEOS, + MethodType.GET, + { + sec_user_id: 'MS4wLjABAAAxxxxxxxx', + count: 20, + max_cursor: 0, + } + ) +} +``` + +## 错误处理 + +```javascript +try { + const response = await TikhubService.postTikHup( + InterfaceType.DOUYIN_WEB_HOT_SEARCH, + MethodType.GET, + { keyword: '测试' } + ) + console.log('成功:', response) +} catch (error) { + if (error.message.includes('无效的接口类型')) { + console.error('接口类型错误') + } else { + console.error('请求失败:', error.message) + } +} +``` + +## 后端接口格式 + +本模块会调用后端接口 `/webApi/admin-api/ai/tikHup/post_tik_hup`,传递的参数格式为: + +```json +{ + "interface_type": "8", + "method_type": "GET", + "platform_url": "https://api.tikhub.io/api/v1/douyin/web/fetch_hot_search_result", + "url_params": { "keyword": "测试" } +} +``` + +## 注意事项 + +1. 所有接口类型必须使用 `InterfaceType` 枚举,不能使用字符串数字 +2. HTTP 方法必须使用 `MethodType` 枚举 +3. `urlParams` 可以是对象或字符串,取决于实际接口的要求 +4. 后端会根据 `interface_type` 从数据库中获取对应的 `platform_token` + diff --git a/frontend/app/web-gold/src/api/tikhub/index.js b/frontend/app/web-gold/src/api/tikhub/index.js new file mode 100644 index 0000000000..880159eec8 --- /dev/null +++ b/frontend/app/web-gold/src/api/tikhub/index.js @@ -0,0 +1,7 @@ +/** + * TikHub API 统一导出 + */ +export { TikhubService } from './tikhub.js' +export { default } from './tikhub.js' +export { InterfaceType, MethodType, InterfaceUrlMap, ParamType } from './types.js' + diff --git a/frontend/app/web-gold/src/api/tikhub/tikhub.js b/frontend/app/web-gold/src/api/tikhub/tikhub.js new file mode 100644 index 0000000000..15426b49eb --- /dev/null +++ b/frontend/app/web-gold/src/api/tikhub/tikhub.js @@ -0,0 +1,55 @@ +import http from '@/api/http' +import { InterfaceType, MethodType, InterfaceUrlMap, ParamType } from './types' +import qs from 'qs' +// 使用本地代理前缀 /tikhub,开发环境通过 Vite 代理到 https://api.tikhub.io +const SERVER_TIKHUB = '/webApi/admin-api/ai/tikHup' + +/** + * TikHub API 服务类 + */ +export const TikhubService = { + /** + * 统一调用 TikHub 接口的中间层方法 + * @param {String} type - 接口类型,使用 InterfaceType 枚举 + * @param {String} methodType - HTTP 方法类型,使用 MethodType 枚举 + * @param {Object|String} urlParams - 实际接口的参数 + * @returns {Promise} axios 响应 + * + * @example + * // 调用抖音热门搜索接口 + * TikhubService.postTikHup( + * InterfaceType.DOUYIN_WEB_HOT_SEARCH, + * MethodType.GET, + * { keyword: '测试' } + * ) + */ + postTikHup({ + type, + methodType, + urlParams, + paramType = '', + }) { + // 验证接口类型是否存在 + if (!InterfaceUrlMap[type]) { + throw new Error(`无效的接口类型: ${type}`) + } + + + + // 构造请求参数 + const requestData = { + type: type, + methodType: methodType, + paramType, + urlParams: paramType === ParamType.JSON ? JSON.stringify(urlParams) : qs.stringify(urlParams), + } + + return http.post(`${SERVER_TIKHUB}/postTikHup`, qs.stringify(requestData)) + }, +} + +export default TikhubService + +// 导出枚举,方便外部使用 +export { InterfaceType, MethodType, InterfaceUrlMap } + diff --git a/frontend/app/web-gold/src/api/tikhub/types.js b/frontend/app/web-gold/src/api/tikhub/types.js new file mode 100644 index 0000000000..2d3a3f5c93 --- /dev/null +++ b/frontend/app/web-gold/src/api/tikhub/types.js @@ -0,0 +1,87 @@ +/** + * TikHub 接口类型枚举 + * 对应数据库 tik_token 表中的 interface_type 字段 + */ +export const InterfaceType = { + /** 小红书 - 获取用户信息 */ + XIAOHONGSHU_USER_INFO: '1', + + /** 抖音 - 网页端获取用户发布的视频 */ + DOUYIN_WEB_USER_POST_VIDEOS: '2', + + /** 抖音 - APP端V3获取用户发布的视频 */ + DOUYIN_APP_USER_POST_VIDEOS: '3', + + /** 抖音 - APP端V3通用搜索结果 */ + DOUYIN_APP_GENERAL_SEARCH: '4', + + /** 抖音 - 搜索建议 */ + DOUYIN_SEARCH_SUGGEST: '5', + + /** 抖音 - 通用搜索V4 */ + DOUYIN_GENERAL_SEARCH_V4: '6', + + /** 小红书 - 网页端V2热门榜单 */ + XIAOHONGSHU_WEB_HOT_LIST: '7', + + /** 抖音 - 网页端热门搜索结果 */ + DOUYIN_WEB_HOT_SEARCH: '8', + + /** 快手 - 网页端热门榜单V2 */ + KUAISHOU_WEB_HOT_LIST: '9', + + /** B站 - 网页端流行内容 */ + BILIBILI_WEB_POPULAR: '10', + + /** 微博 - 网页端V2热门搜索指数 */ + WEIBO_WEB_HOT_SEARCH: '11', + + /** 抖音 - 网页端通用搜索结果 */ + DOUYIN_WEB_GENERAL_SEARCH: '12', + /** 抖音 - APP通用搜索结果 */ + DOUYIN_SEARCH_GENERAL_SEARCH: '14', +} + +/** + * HTTP 请求方法枚举 + */ +export const MethodType = { + GET: 'GET', + POST: 'POST', + PUT: 'PUT', + DELETE: 'DELETE', + PATCH: 'PATCH', +} +/** + * 请求参数类型枚举 + */ +export const ParamType = { + JSON: 'json', + FORM: 'form' +} +/** + * 接口类型与平台 URL 的映射 + * 根据 tik_token 表中的数据生成 + */ +export const InterfaceUrlMap = { + [InterfaceType.XIAOHONGSHU_USER_INFO]: 'https://api.tikhub.io/api/v1/xiaohongshu/app/get_user_info', + [InterfaceType.DOUYIN_WEB_USER_POST_VIDEOS]: 'https://api.tikhub.io/api/v1/douyin/web/fetch_user_post_videos', + [InterfaceType.DOUYIN_APP_USER_POST_VIDEOS]: 'https://api.tikhub.io/api/v1/douyin/app/v3/fetch_user_post_videos', + [InterfaceType.DOUYIN_APP_GENERAL_SEARCH]: 'https://api.tikhub.io/api/v1/douyin/app/v3/fetch_general_search_result', + [InterfaceType.DOUYIN_SEARCH_GENERAL_SEARCH]: 'https://api.tikhub.io/api/v1/douyin/search/fetch_general_search_v4', + [InterfaceType.DOUYIN_SEARCH_SUGGEST]: 'https://api.tikhub.io/api/v1/douyin/search/fetch_search_suggest', + [InterfaceType.DOUYIN_GENERAL_SEARCH_V4]: 'https://api.tikhub.io/api/v1/douyin/search/fetch_general_search_v4', + [InterfaceType.XIAOHONGSHU_WEB_HOT_LIST]: 'https://api.tikhub.io/api/v1/xiaohongshu/web_v2/fetch_hot_list', + [InterfaceType.DOUYIN_WEB_HOT_SEARCH]: 'https://api.tikhub.io/api/v1/douyin/web/fetch_hot_search_result', + [InterfaceType.KUAISHOU_WEB_HOT_LIST]: 'https://api.tikhub.io/api/v1/kuaishou/web/fetch_kuaishou_hot_list_v2', + [InterfaceType.BILIBILI_WEB_POPULAR]: 'https://api.tikhub.io/api/v1/bilibili/web/fetch_com_popular', + [InterfaceType.WEIBO_WEB_HOT_SEARCH]: 'https://api.tikhub.io/api/v1/weibo/web_v2/fetch_hot_search_index', + [InterfaceType.DOUYIN_WEB_GENERAL_SEARCH]: 'https://api.tikhub.io/api/v1/douyin/web/fetch_general_search_result', +} + +export default { + InterfaceType, + MethodType, + InterfaceUrlMap, +} + diff --git a/frontend/app/web-gold/src/components/LoginModal.vue b/frontend/app/web-gold/src/components/LoginModal.vue new file mode 100644 index 0000000000..b87f679774 --- /dev/null +++ b/frontend/app/web-gold/src/components/LoginModal.vue @@ -0,0 +1,1005 @@ + + + + + diff --git a/frontend/app/web-gold/src/components/SidebarNav.vue b/frontend/app/web-gold/src/components/SidebarNav.vue new file mode 100644 index 0000000000..43adfd9889 --- /dev/null +++ b/frontend/app/web-gold/src/components/SidebarNav.vue @@ -0,0 +1,123 @@ + + + + + + + diff --git a/frontend/app/web-gold/src/components/TokenInput.vue b/frontend/app/web-gold/src/components/TokenInput.vue new file mode 100644 index 0000000000..bed3ce32b0 --- /dev/null +++ b/frontend/app/web-gold/src/components/TokenInput.vue @@ -0,0 +1,102 @@ + + + + + + diff --git a/frontend/app/web-gold/src/components/TopNav.vue b/frontend/app/web-gold/src/components/TopNav.vue new file mode 100644 index 0000000000..58886ba908 --- /dev/null +++ b/frontend/app/web-gold/src/components/TopNav.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/frontend/app/web-gold/src/components/icons/Icon.vue b/frontend/app/web-gold/src/components/icons/Icon.vue new file mode 100644 index 0000000000..04d387b3a7 --- /dev/null +++ b/frontend/app/web-gold/src/components/icons/Icon.vue @@ -0,0 +1,28 @@ + + + + + + + diff --git a/frontend/app/web-gold/src/components/icons/SvgSprite.vue b/frontend/app/web-gold/src/components/icons/SvgSprite.vue new file mode 100644 index 0000000000..09fc4b4ddf --- /dev/null +++ b/frontend/app/web-gold/src/components/icons/SvgSprite.vue @@ -0,0 +1,67 @@ + + + + + + + diff --git a/frontend/app/web-gold/src/env.d.ts b/frontend/app/web-gold/src/env.d.ts new file mode 100644 index 0000000000..82e26651e7 --- /dev/null +++ b/frontend/app/web-gold/src/env.d.ts @@ -0,0 +1,29 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + +interface ImportMetaEnv { + readonly VITE_DEV_TOKEN: string + readonly VITE_TENANT_ID: string + readonly VITE_PROXY_TARGET: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +// 声明 ant-design-vue 的类型 +declare module 'ant-design-vue' { + export const message: { + success: (content: string) => void + error: (content: string) => void + warning: (content: string) => void + info: (content: string) => void + } + export const Modal: any +} + diff --git a/frontend/app/web-gold/src/main.js b/frontend/app/web-gold/src/main.js new file mode 100644 index 0000000000..ce1f643532 --- /dev/null +++ b/frontend/app/web-gold/src/main.js @@ -0,0 +1,19 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import Antd from 'ant-design-vue' +import 'normalize.css' +import 'ant-design-vue/dist/reset.css' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; + +import App from './App.vue' +import router from './router' +import './style.css' + +const app = createApp(App) +const pinia = createPinia() +pinia.use(piniaPluginPersistedstate) +app.use(pinia) +app.use(router) +app.use(Antd) + +app.mount('#app') diff --git a/frontend/app/web-gold/src/router/index.js b/frontend/app/web-gold/src/router/index.js new file mode 100644 index 0000000000..8d63c1e007 --- /dev/null +++ b/frontend/app/web-gold/src/router/index.js @@ -0,0 +1,51 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const routes = [ + { + path: '/', + redirect: '/content-style/benchmark' + }, + // { path: '/home', name: '首页', component: () => import('../views/home/Home.vue') }, + { + path: '/content-style', + name: '内容风格分析', + children: [ + { path: '', redirect: '/content-style/benchmark' }, + { path: 'benchmark', name: '对标分析', component: () => import('../views/content-style/Benchmark.vue') }, + { path: 'copywriting', name: '文案创作', component: () => import('../views/content-style/Copywriting.vue') }, + ] + }, + { + path: '/trends', + name: '热点趋势分析', + children: [ + { path: '', redirect: '/trends/heat' }, + { path: 'heat', name: '热度分析', component: () => import('../views/trends/Heat.vue') }, + { path: 'forecast', name: '热点预测', component: () => import('../views/trends/Forecast.vue') }, + { path: 'copywriting', name: '趋势文案创作', component: () => import('../views/trends/Copywriting.vue') }, + ] + }, + { + path: '/digital-human', + name: '数字人', + children: [ + { path: '', redirect: '/digital-human/voice-copy' }, + { path: 'voice-copy', name: '人声克隆', component: () => import('../views/dh/VoiceCopy.vue') }, + { path: 'voice-generate', name: '生成配音', component: () => import('../views/dh/VoiceGenerate.vue') }, + { path: 'avatar', name: '生成数字人', component: () => import('../views/dh/Avatar.vue') }, + ] + }, + { path: '/realtime-hot', name: '实时热点推送', component: () => import('../views/realtime/RealtimeHot.vue') }, + { path: '/mix-editor', name: '素材混剪', component: () => import('../views/mix/MixEditor.vue') }, + { path: '/capcut-import', name: '剪映导入', component: () => import('../views/capcut/CapcutImport.vue') }, + { path: '/help', name: '帮助', component: () => import('../views/misc/Help.vue') }, + { path: '/download', name: '下载', component: () => import('../views/misc/Download.vue') }, + { path: '/settings/theme', name: '主题设置', component: () => import('../views/misc/Theme.vue') }, +] + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes, +}) + +export default router diff --git a/frontend/app/web-gold/src/stores/prompt.js b/frontend/app/web-gold/src/stores/prompt.js new file mode 100644 index 0000000000..049e793e7f --- /dev/null +++ b/frontend/app/web-gold/src/stores/prompt.js @@ -0,0 +1,40 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import localforage from 'localforage' + +export const usePromptStore = defineStore('prompt', () => { + // 存储当前选中的提示词 + const currentPrompt = ref('') + + // 存储提示词相关的视频信息 + const currentVideoInfo = ref(null) + + // 设置提示词 + function setPrompt(prompt, videoInfo = null) { + currentPrompt.value = prompt + currentVideoInfo.value = videoInfo + } + + // 清空提示词 + function clearPrompt() { + currentPrompt.value = '' + currentVideoInfo.value = null + } + + return { + currentPrompt, + currentVideoInfo, + setPrompt, + clearPrompt + } +}, { + persist: { + key: 'prompt-store', + storage: { + getItem: (key) => localforage.getItem(key), + setItem: (key, value) => localforage.setItem(key, value), + removeItem: (key) => localforage.removeItem(key) + }, + paths: ['currentPrompt', 'currentVideoInfo'] + } +}) diff --git a/frontend/app/web-gold/src/stores/user.js b/frontend/app/web-gold/src/stores/user.js new file mode 100644 index 0000000000..d6d43988e8 --- /dev/null +++ b/frontend/app/web-gold/src/stores/user.js @@ -0,0 +1,152 @@ +import { ref, computed, watch } from 'vue' +import { defineStore } from 'pinia' +import { getJSON, setJSON, remove } from '@/utils/storage' + +// 本地持久化的 key +const STORAGE_KEY = 'user_store_v1' + +export const useUserStore = defineStore('user', () => { + // 基本信息 + const isLoggedIn = ref(false) + const userId = ref('') + const nickname = ref('') + const avatar = ref('') + // 微信相关 + const wechatOpenId = ref('') + const wechatUnionId = ref('') + const wechatNickname = ref('') + const wechatAvatar = ref('') + // 资产与配额 + const balance = ref(0) + const vipLevel = ref(0) + const credits = ref(0) + + const displayName = computed(() => nickname.value || wechatNickname.value || '未命名用户') + const displayAvatar = computed(() => avatar.value || wechatAvatar.value || '') + + // 恢复本地 + async function hydrateFromStorage() { + const saved = await getJSON(STORAGE_KEY) + if (!saved) return + isLoggedIn.value = !!saved.isLoggedIn + userId.value = saved.userId || '' + nickname.value = saved.nickname || '' + avatar.value = saved.avatar || '' + wechatOpenId.value = saved.wechatOpenId || '' + wechatUnionId.value = saved.wechatUnionId || '' + wechatNickname.value = saved.wechatNickname || '' + wechatAvatar.value = saved.wechatAvatar || '' + balance.value = Number(saved.balance || 0) + vipLevel.value = Number(saved.vipLevel || 0) + credits.value = Number(saved.credits || 0) + } + + // 持久化 + async function persist() { + const payload = { + isLoggedIn: isLoggedIn.value, + userId: userId.value, + nickname: nickname.value, + avatar: avatar.value, + wechatOpenId: wechatOpenId.value, + wechatUnionId: wechatUnionId.value, + wechatNickname: wechatNickname.value, + wechatAvatar: wechatAvatar.value, + balance: balance.value, + vipLevel: vipLevel.value, + credits: credits.value, + } + await setJSON(STORAGE_KEY, payload) + } + + // 监听关键字段做持久化 + ;[ + isLoggedIn, + userId, + nickname, + avatar, + wechatOpenId, + wechatUnionId, + wechatNickname, + wechatAvatar, + balance, + vipLevel, + credits, + ].forEach((s) => watch(s, persist)) + + // 登录/登出动作(示例:具体接入后端可在此对接) + async function loginWithPhone(payload) { + // payload: { phone, code, profile? } + isLoggedIn.value = true + userId.value = payload?.profile?.userId || userId.value + nickname.value = payload?.profile?.nickname || nickname.value + avatar.value = payload?.profile?.avatar || avatar.value + balance.value = payload?.profile?.balance ?? balance.value + vipLevel.value = payload?.profile?.vipLevel ?? vipLevel.value + credits.value = payload?.profile?.credits ?? credits.value + await persist() + } + + async function loginWithWeChat(profile) { + // profile: { openId, unionId, nickname, avatar, balance, vipLevel, credits } + isLoggedIn.value = true + wechatOpenId.value = profile?.openId || '' + wechatUnionId.value = profile?.unionId || '' + wechatNickname.value = profile?.nickname || '' + wechatAvatar.value = profile?.avatar || '' + balance.value = profile?.balance ?? balance.value + vipLevel.value = profile?.vipLevel ?? vipLevel.value + credits.value = profile?.credits ?? credits.value + await persist() + } + + async function updateBalance(delta) { + balance.value = Math.max(0, Number(balance.value) + Number(delta || 0)) + await persist() + } + + async function logout() { + isLoggedIn.value = false + userId.value = '' + nickname.value = '' + avatar.value = '' + wechatOpenId.value = '' + wechatUnionId.value = '' + wechatNickname.value = '' + wechatAvatar.value = '' + balance.value = 0 + vipLevel.value = 0 + credits.value = 0 + await remove(STORAGE_KEY) + } + + // 初始化从本地恢复 + hydrateFromStorage() + + return { + // state + isLoggedIn, + userId, + nickname, + avatar, + wechatOpenId, + wechatUnionId, + wechatNickname, + wechatAvatar, + balance, + vipLevel, + credits, + // getters + displayName, + displayAvatar, + // actions + loginWithPhone, + loginWithWeChat, + updateBalance, + logout, + } +}) + +export default useUserStore + + diff --git a/frontend/app/web-gold/src/stores/voiceCopy.js b/frontend/app/web-gold/src/stores/voiceCopy.js new file mode 100644 index 0000000000..1a7144c082 --- /dev/null +++ b/frontend/app/web-gold/src/stores/voiceCopy.js @@ -0,0 +1,62 @@ +import { defineStore } from 'pinia' +import storage from '@/utils/storage' + +const STORAGE_KEY = 'cosy_voice_profiles' + +export const useVoiceCopyStore = defineStore('voiceCopy', { + state: () => ({ + profiles: [], + activeId: '', + loaded: false, + }), + getters: { + activeProfile(state) { + return state.profiles.find(p => p.id === state.activeId) || null + } + }, + actions: { + generateId() { + return `${Date.now()}_${Math.floor(Math.random() * 1e6)}` + }, + async load() { + if (this.loaded) return + const list = await storage.getJSON(STORAGE_KEY, []) + this.profiles = Array.isArray(list) ? list : [] + if (!this.activeId && this.profiles.length) this.activeId = this.profiles[0].id + this.loaded = true + }, + async persist() { + await storage.setJSON(STORAGE_KEY, this.profiles) + }, + async add(profile) { + const id = this.generateId() + const name = profile.name || `克隆语音-${this.profiles.length + 1}` + const payload = { ...profile, id, name } + this.profiles.unshift(payload) + this.activeId = id + await this.persist() + return payload + }, + async update(profile) { + const idx = this.profiles.findIndex(p => p.id === profile.id) + if (idx === -1) return await this.add({ ...profile, id: '' }) + this.profiles[idx] = { ...profile } + await this.persist() + return this.profiles[idx] + }, + async duplicate(profile, name) { + const copy = { ...profile, id: '', name } + return await this.add(copy) + }, + async remove(id) { + this.profiles = this.profiles.filter(p => p.id !== id) + if (this.activeId === id) this.activeId = this.profiles[0]?.id || '' + await this.persist() + }, + select(id) { + this.activeId = id + } + } +}) + + diff --git a/frontend/app/web-gold/src/style.css b/frontend/app/web-gold/src/style.css new file mode 100644 index 0000000000..f257d965df --- /dev/null +++ b/frontend/app/web-gold/src/style.css @@ -0,0 +1,117 @@ +@import "tailwindcss"; + +/* 简单的图标占位类 */ +.i-bell::before { content: "🔔"; display: inline-block; } + +/* 全局滚动条稳定,避免页面切换时左右抖动 */ +body { scrollbar-gutter: stable both-edges; } + +/* 统一阴影层级(与 antd 风格接近) */ +.elev-1 { box-shadow: 0 1px 4px rgba(0,0,0,0.06); } +.elev-2 { box-shadow: 0 2px 8px rgba(0,0,0,0.10); } + +/* 通用卡片表面(可在各页面复用) */ +.card-surface { + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); +} + +/* ========================== + 设计规范:根变量与暗色主题 + 来源:.cursorules/design.md + ========================== */ +:root { + /* 颜色 - 主色与中性色 */ + --color-bg: #0D0D0D; /* 背景:深黑 */ + --color-surface: #1A1A1A; /* 模块底 */ + --color-text: #F2F2F2; /* 正文文本 */ + --color-text-secondary: #CCCCCC; /* 次要文本 */ + --color-border: #333333; /* 边框 */ + --color-primary: #00B030; /* 主功能色 */ + --color-blue: #1A66E0; /* 辅助交互蓝 */ + --color-accent: #FF6A30; /* 强调橙 */ + + /* 字号与行高 */ + --font-title-size: 20px; /* Montserrat 半粗体 */ + --font-body-size: 14px; /* Inter 常规 */ + --font-small-size: 12px; /* 辅助文本 */ + --line-height-base: 1.5; + + /* 圆角与阴影 */ + --radius-card: 6px; /* 卡片圆角 */ + --shadow-inset-card: inset 0 2px 4px rgba(0,0,0,0.4); + --glow-primary: 0 0 6px rgba(0,176,48,0.3); +} + +/* 正确的 :root 变量声明(专业科技蓝方案) */ +:root { + /* 主色系 - 科技蓝 */ + --color-primary: #3B82F6; + --color-primary-light: #60A5FA; + --color-primary-dark: #2563EB; + --color-primary-glow: rgba(59, 130, 246, 0.3); + + /* 辅助色 */ + --color-blue: #1A66E0; + --color-accent: #FF6A30; + + /* 中性色(保持) */ + --color-bg: #0D0D0D; + --color-surface: #1A1A1A; + --color-text: #F2F2F2; + --color-text-secondary: #CCCCCC; + --color-border: #333333; + + /* 尺寸与阴影(保持) */ + --radius-card: 6px; + --shadow-inset-card: inset 0 2px 4px rgba(0,0,0,0.4); + --glow-primary: 0 0 6px var(--color-primary-glow); +} + +/* 全局暗色基础 */ +html, body, #app { + background: var(--color-bg); + color: var(--color-text); + font-size: var(--font-body-size); + line-height: var(--line-height-base); +} + +/* 卡片:遵循新规范(默认暗色表面) */ +.card-surface--dark { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-inset-card); +} + +/* 按钮主色(用于顶部试用按钮等) */ +.btn-primary { + background: var(--color-primary); + color: #fff; +} +.btn-primary:hover { + box-shadow: var(--glow-primary); + filter: brightness(1.03); +} + +/* 次要文本与分割线工具类 */ +.text-secondary { color: var(--color-text-secondary); } +.border-base { border-color: var(--color-border); } + +/* 覆盖 antd 组件的占位符与主按钮色(全局) */ +:root :where(.ant-input, .ant-input-affix-wrapper, .ant-select-selector, textarea)::placeholder { + color: color-mix(in oklab, var(--color-text-secondary) 80%, transparent); +} + +:root :where(.ant-btn-primary) { + background: var(--color-primary); + border-color: var(--color-primary); +} + +:root :where(.ant-btn-primary:hover, .ant-btn-primary:focus) { + background: var(--color-primary); + border-color: var(--color-primary); + box-shadow: var(--glow-primary); +} diff --git a/frontend/app/web-gold/src/types/axios.d.ts b/frontend/app/web-gold/src/types/axios.d.ts new file mode 100644 index 0000000000..ca0149e261 --- /dev/null +++ b/frontend/app/web-gold/src/types/axios.d.ts @@ -0,0 +1,17 @@ +import 'axios' + +declare module 'axios' { + export interface AxiosResponse { + code?: number + message?: string + data: T + [key: string]: any + } + + export interface AxiosError extends Error { + code?: number | string + data?: T + [key: string]: any + } +} + diff --git a/frontend/app/web-gold/src/types/global.d.ts b/frontend/app/web-gold/src/types/global.d.ts new file mode 100644 index 0000000000..7e868191b4 --- /dev/null +++ b/frontend/app/web-gold/src/types/global.d.ts @@ -0,0 +1,29 @@ +// 全局类型声明文件 + +/** + * 音频项接口 + */ +export interface AudioItem { + audio_url: string +} + +/** + * 转录结果接口 + */ +export interface TranscriptionResult { + key: string + audio_url?: string + value: string +} + +/** + * 视频转字符响应接口 + */ +export interface VideoToCharactersResponse { + data: { + results: Array<{ + transcription_url: string + }> + } +} + diff --git a/frontend/app/web-gold/src/types/vue-shim.d.ts b/frontend/app/web-gold/src/types/vue-shim.d.ts new file mode 100644 index 0000000000..36bbaf6e74 --- /dev/null +++ b/frontend/app/web-gold/src/types/vue-shim.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + diff --git a/frontend/app/web-gold/src/utils/auth.js b/frontend/app/web-gold/src/utils/auth.js new file mode 100644 index 0000000000..f0f04b124d --- /dev/null +++ b/frontend/app/web-gold/src/utils/auth.js @@ -0,0 +1,42 @@ +import { useCache } from '@gold/hooks/web/useCache' +import { getToken as getTokenFromManager } from './token-manager' + +const { wsCache } = useCache() + +const AccessTokenKey = 'ACCESS_TOKEN' +const RefreshTokenKey = 'REFRESH_TOKEN' + +// 获取token - 使用统一的 token 管理器 +export const getAccessToken = () => { + return getTokenFromManager() +} + +// 刷新token +export const getRefreshToken = () => { + try { + // 优先从 wsCache 读取 + const refreshToken = wsCache.get(RefreshTokenKey) || wsCache.get('refresh_token') + if (refreshToken) { + return refreshToken + } + } catch (e) { + console.warn('获取 refresh token 失败:', e) + } + return null +} +// 设置token +export const setToken = (token) => { + wsCache.set(RefreshTokenKey, token.refreshToken) + wsCache.set(AccessTokenKey, token.accessToken) +} + +// 删除token +export const removeToken = () => { + wsCache.delete(AccessTokenKey) + wsCache.delete(RefreshTokenKey) +} + +/** 格式化token(jwt格式) */ +export const formatToken = (token) => { + return 'Bearer ' + token +} diff --git a/frontend/app/web-gold/src/utils/markdown.js b/frontend/app/web-gold/src/utils/markdown.js new file mode 100644 index 0000000000..2fa34b59d0 --- /dev/null +++ b/frontend/app/web-gold/src/utils/markdown.js @@ -0,0 +1,20 @@ +import MarkdownIt from 'markdown-it' + +// 创建 markdown 实例 +const md = new MarkdownIt() + +/** + * 渲染 markdown 内容为 HTML + * @param {string} content - markdown 内容 + * @returns {string} HTML 字符串 + */ +export function renderMarkdown(content) { + if (!content) return '' + return md.render(content) +} + +/** + * 导出 md 实例,供需要自定义配置的地方使用 + */ +export { md } + diff --git a/frontend/app/web-gold/src/utils/storage.js b/frontend/app/web-gold/src/utils/storage.js new file mode 100644 index 0000000000..6b755a7eb8 --- /dev/null +++ b/frontend/app/web-gold/src/utils/storage.js @@ -0,0 +1,56 @@ +import localforage from 'localforage' + +// 统一的 IndexedDB 存储封装:API 与 localStorage 类似但为异步 +// 默认使用一个命名的存储实例,便于后续扩展多命名空间 +const store = localforage.createInstance({ + name: 'web-gold', + storeName: 'app_store', + description: 'App persistent storage powered by IndexedDB', +}) + +export async function setJSON(key, value) { + try { + const json = JSON.stringify(value) + await store.setItem(key, json) + return true + } catch (err) { + console.error('[storage.setJSON] failed:', err) + return false + } +} + +export async function getJSON(key, fallback = null) { + try { + const raw = await store.getItem(key) + if (raw == null) return fallback + return JSON.parse(raw) + } catch (err) { + console.error('[storage.getJSON] failed:', err) + return fallback + } +} + +export async function remove(key) { + try { + await store.removeItem(key) + } catch (err) { + console.error('[storage.remove] failed:', err) + } +} + +export async function clearAll() { + try { + await store.clear() + } catch (err) { + console.error('[storage.clearAll] failed:', err) + } +} + +export default { + setJSON, + getJSON, + remove, + clearAll, +} + + diff --git a/frontend/app/web-gold/src/utils/token-manager.js b/frontend/app/web-gold/src/utils/token-manager.js new file mode 100644 index 0000000000..c937041a6b --- /dev/null +++ b/frontend/app/web-gold/src/utils/token-manager.js @@ -0,0 +1,100 @@ +import { useCache } from '@gold/hooks/web/useCache' + +/** + * Token 统一管理模块 + * + * 优先级顺序: + * 1. 手动输入的 dev token (sessionStorage) + * 2. 正式登录的 token (wsCache) + * 3. 环境变量 VITE_DEV_TOKEN + */ + +// sessionStorage 中的手动 token key +const DEV_MANUAL_TOKEN_KEY = 'DEV_MANUAL_TOKEN' + +// 获取缓存实例 +let wsCache = null +function getCache() { + if (!wsCache) { + wsCache = useCache().wsCache + } + return wsCache +} + +/** + * 获取完整的 Authorization Header 值 + * @returns {string} Bearer token 或空字符串 + */ +export function getAuthHeader() { + const token = getToken() + return token ? `Bearer ${token}` : '' +} + +/** + * 获取 token + * @returns {string} token 字符串 + */ +export function getToken() { + // 1. 优先使用手动输入的 dev token + const manualToken = sessionStorage.getItem(DEV_MANUAL_TOKEN_KEY) + if (manualToken) { + return manualToken + } + + // 2. 使用正式登录的 token(从 wsCache 读取) + try { + const cache = getCache() + const accessToken = cache.get('ACCESS_TOKEN') || cache.get('access_token') + + if (accessToken) { + return accessToken + } + } catch (e) { + console.warn('获取 wsCache 失败:', e) + } + + // 3. 兜底:环境变量中的 token + const envToken = import.meta?.env?.VITE_DEV_TOKEN + if (envToken) { + return envToken + } + + return '' +} + +/** + * 设置手动输入的 dev token + * @param {string} token + */ +export function setDevToken(token) { + if (token) { + sessionStorage.setItem(DEV_MANUAL_TOKEN_KEY, token) + } else { + sessionStorage.removeItem(DEV_MANUAL_TOKEN_KEY) + } +} + +/** + * 获取手动输入的 dev token(用于显示) + * @returns {string} + */ +export function getDevToken() { + return sessionStorage.getItem(DEV_MANUAL_TOKEN_KEY) || '' +} + +/** + * 清除所有 token + */ +export function clearAllTokens() { + sessionStorage.removeItem(DEV_MANUAL_TOKEN_KEY) + try { + const cache = getCache() + cache.delete('ACCESS_TOKEN') + cache.delete('access_token') + cache.delete('REFRESH_TOKEN') + cache.delete('refresh_token') + } catch (e) { + console.warn('清除 wsCache 失败:', e) + } +} + diff --git a/frontend/app/web-gold/src/utils/url.js b/frontend/app/web-gold/src/utils/url.js new file mode 100644 index 0000000000..0a693eb306 --- /dev/null +++ b/frontend/app/web-gold/src/utils/url.js @@ -0,0 +1,55 @@ +import { match as pathMatch } from 'path-to-regexp' + + +/** + * 安全解析 URL,失败返回 null + */ +export function safeParseUrl(input) { + const raw = (input || '').trim() + if (!raw) return null + try { + return new URL(raw) + } catch { + return null + } +} + +/** + * 从 URL 对象按顺序读取第一个非空的 query 值 + */ +export function getFirstQuery(urlObj, keys = []) { + if (!urlObj || !urlObj.searchParams) return '' + for (const key of keys) { + const v = urlObj.searchParams.get(key) + if (v) return v + } + return '' +} + +// 组合解析:优先从 queryKeys 取值,其次按 path-to-regexp 的模式从 pathname 匹配(返回首个命名参数),最后可回退 raw +export function resolveId(input, options = {}) { + const raw = (input || '').trim() + if (!raw) return '' + const { queryKeys = [], pathPatterns = [], fallbackRaw = true } = options + const urlObj = safeParseUrl(raw) + const q = getFirstQuery(urlObj, queryKeys) + if (q) return q + if (urlObj) { + for (const pattern of pathPatterns) { + if (typeof pattern === 'string') { + const m = pathMatch(pattern)(urlObj.pathname) + if (m && m.params) { + const firstKey = Object.keys(m.params)[0] + const val = firstKey ? m.params[firstKey] : '' + if (val) return String(val) + } + } else if (pattern instanceof RegExp) { + const m = urlObj.pathname.match(pattern) + if (m && m[1]) return m[1] + } + } + } + return fallbackRaw ? raw : '' +} + + diff --git a/frontend/app/web-gold/src/views/capcut/CapcutImport.vue b/frontend/app/web-gold/src/views/capcut/CapcutImport.vue new file mode 100644 index 0000000000..b5b519a58d --- /dev/null +++ b/frontend/app/web-gold/src/views/capcut/CapcutImport.vue @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/content-style/Benchmark.vue b/frontend/app/web-gold/src/views/content-style/Benchmark.vue new file mode 100644 index 0000000000..e12e73826c --- /dev/null +++ b/frontend/app/web-gold/src/views/content-style/Benchmark.vue @@ -0,0 +1,1370 @@ + + + + + diff --git a/frontend/app/web-gold/src/views/content-style/Copywriting.vue b/frontend/app/web-gold/src/views/content-style/Copywriting.vue new file mode 100644 index 0000000000..17347ede8e --- /dev/null +++ b/frontend/app/web-gold/src/views/content-style/Copywriting.vue @@ -0,0 +1,905 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/dh/Avatar.vue b/frontend/app/web-gold/src/views/dh/Avatar.vue new file mode 100644 index 0000000000..abdfed046b --- /dev/null +++ b/frontend/app/web-gold/src/views/dh/Avatar.vue @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/dh/VoiceCopy.vue b/frontend/app/web-gold/src/views/dh/VoiceCopy.vue new file mode 100644 index 0000000000..253a74b34e --- /dev/null +++ b/frontend/app/web-gold/src/views/dh/VoiceCopy.vue @@ -0,0 +1,313 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/dh/VoiceGenerate.vue b/frontend/app/web-gold/src/views/dh/VoiceGenerate.vue new file mode 100644 index 0000000000..4600525973 --- /dev/null +++ b/frontend/app/web-gold/src/views/dh/VoiceGenerate.vue @@ -0,0 +1,146 @@ + + + + + + + diff --git a/frontend/app/web-gold/src/views/home/Home.vue b/frontend/app/web-gold/src/views/home/Home.vue new file mode 100644 index 0000000000..f25f76ed14 --- /dev/null +++ b/frontend/app/web-gold/src/views/home/Home.vue @@ -0,0 +1,781 @@ + + + + + + diff --git a/frontend/app/web-gold/src/views/misc/Download.vue b/frontend/app/web-gold/src/views/misc/Download.vue new file mode 100644 index 0000000000..032f787b97 --- /dev/null +++ b/frontend/app/web-gold/src/views/misc/Download.vue @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/misc/Help.vue b/frontend/app/web-gold/src/views/misc/Help.vue new file mode 100644 index 0000000000..7222f9e731 --- /dev/null +++ b/frontend/app/web-gold/src/views/misc/Help.vue @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/misc/Theme.vue b/frontend/app/web-gold/src/views/misc/Theme.vue new file mode 100644 index 0000000000..ca2b573011 --- /dev/null +++ b/frontend/app/web-gold/src/views/misc/Theme.vue @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/mix/MixEditor.vue b/frontend/app/web-gold/src/views/mix/MixEditor.vue new file mode 100644 index 0000000000..bd4f7957be --- /dev/null +++ b/frontend/app/web-gold/src/views/mix/MixEditor.vue @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/realtime/RealtimeHot.vue b/frontend/app/web-gold/src/views/realtime/RealtimeHot.vue new file mode 100644 index 0000000000..edea51ecd6 --- /dev/null +++ b/frontend/app/web-gold/src/views/realtime/RealtimeHot.vue @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/trends/Copywriting.vue b/frontend/app/web-gold/src/views/trends/Copywriting.vue new file mode 100644 index 0000000000..a5908b6162 --- /dev/null +++ b/frontend/app/web-gold/src/views/trends/Copywriting.vue @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/frontend/app/web-gold/src/views/trends/Forecast.vue b/frontend/app/web-gold/src/views/trends/Forecast.vue new file mode 100644 index 0000000000..167bf05f95 --- /dev/null +++ b/frontend/app/web-gold/src/views/trends/Forecast.vue @@ -0,0 +1,642 @@ + + + + + diff --git a/frontend/app/web-gold/src/views/trends/Heat.vue b/frontend/app/web-gold/src/views/trends/Heat.vue new file mode 100644 index 0000000000..3a98a4d0bb --- /dev/null +++ b/frontend/app/web-gold/src/views/trends/Heat.vue @@ -0,0 +1,217 @@ + + + + + + + + diff --git a/frontend/app/web-gold/tailwind.config.js b/frontend/app/web-gold/tailwind.config.js new file mode 100644 index 0000000000..bd3eb14e93 --- /dev/null +++ b/frontend/app/web-gold/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/frontend/app/web-gold/tsconfig.json b/frontend/app/web-gold/tsconfig.json new file mode 100644 index 0000000000..b9bf5edf5b --- /dev/null +++ b/frontend/app/web-gold/tsconfig.json @@ -0,0 +1,45 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path mapping */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@gold/*": ["../../*"], + "@/types/*": ["./src/types/*"] + } + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "src/**/*.d.ts", + "../hooks/**/*.ts", + "../hooks/**/*.tsx" + ], + "exclude": [ + "node_modules", + "dist" + ], + "references": [{ "path": "./tsconfig.node.json" }] +} + diff --git a/frontend/app/web-gold/tsconfig.node.json b/frontend/app/web-gold/tsconfig.node.json new file mode 100644 index 0000000000..e8c30d389b --- /dev/null +++ b/frontend/app/web-gold/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.js"] +} + diff --git a/frontend/app/web-gold/vite.config.js b/frontend/app/web-gold/vite.config.js new file mode 100644 index 0000000000..9cecc76467 --- /dev/null +++ b/frontend/app/web-gold/vite.config.js @@ -0,0 +1,87 @@ +/* eslint-env node */ +import path from 'node:path' +import { defineConfig, loadEnv } from 'vite' +import { fileURLToPath, URL } from 'node:url' +import tailwindcss from '@tailwindcss/vite' +import process from 'node:process' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import vueDevTools from 'vite-plugin-vue-devtools' +import UnoCSS from 'unocss/vite' +// import electron from 'vite-plugin-electron/simple' + +/** + * Vite 配置文件 + * 支持 TypeScript 和 JavaScript + * @param {Object} param0 - 配置参数 + * @param {string} param0.mode - 环境模式 + * @returns {import('vite').UserConfig} + */ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const DEV_TOKEN = env.VITE_DEV_TOKEN || '' + const TENANT_ID = env.VITE_TENANT_ID || '1' + const API_TARGET = env.VITE_PROXY_TARGET || 'http://8.155.172.147:9900' + + return { + plugins: [ + vue(), + vueJsx(), + UnoCSS(), + vueDevTools(), + tailwindcss() + // electron({ + // main: { + // // Shortcut of `build.lib.entry`. + // entry: 'electron/main.ts', + // vite: { + // build: { + // rollupOptions: { + // external: ['better-sqlite3'], + // }, + // }, + // }, + // }, + // preload: { + // input: fileURLToPath(new URL('./electron/preload.ts', import.meta.url)), + // }, + // // See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer + // renderer: + // process.env.NODE_ENV === 'test' + // ? // https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808 + // undefined + // : {}, + // }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@gold': fileURLToPath(new URL('../../', import.meta.url)) + }, + }, + server: { + proxy: { + '/webApi': { + target: API_TARGET, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/webApi/, ''), + configure: (proxy, options) => { + proxy.on('proxyReq', (proxyReq, req) => { + // 从客户端请求头中获取 authorization + const authHeader = req.headers?.authorization + if (authHeader) { + proxyReq.setHeader('authorization', authHeader) + } else if (DEV_TOKEN) { + // 兜底:使用环境变量中的 token + proxyReq.setHeader('authorization', `Bearer ${DEV_TOKEN}`) + } + + // 添加 RuoYi 租户 ID + proxyReq.setHeader('tenant-id', TENANT_ID) + }) + }, + }, + }, + }, + } +}) diff --git a/frontend/config/axios/config.js b/frontend/config/axios/config.js new file mode 100644 index 0000000000..6a5ac126fd --- /dev/null +++ b/frontend/config/axios/config.js @@ -0,0 +1,24 @@ +const config = { + /** + * api请求基础路径 + */ + base_url: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL, + /** + * 接口成功返回状态码 + */ + result_code: 200, + + /** + * 接口请求超时时间 + */ + request_timeout: 30000, + + /** + * 默认接口请求类型 + * 可选值:application/x-www-form-urlencoded multipart/form-data + */ + default_headers: 'application/json' + } + + export { config } + \ No newline at end of file diff --git a/frontend/doc/readme.md b/frontend/doc/readme.md new file mode 100644 index 0000000000..62310f0f0f --- /dev/null +++ b/frontend/doc/readme.md @@ -0,0 +1,146 @@ +## 金牌内容大师软件页面结构 + +### 整体布局 +- 顶部导航(深灰):左侧 Logo(紫底白“逗”字 + 文案),右侧通知铃铛(红点数)、用户头像、主题切换 +- 一级模块导航(横向 Tab/菜单): + - 内容风格分析(含 对标分析|文案创作) + - 热点趋势分析(含 热度分析|热点预测|文案创作) + - 数字人(含 声音训练|生成配音|生成数字人) + - 实时热点推送 + - 素材混剪 + - 剪映导入 + - 购买会员(优惠标签)|推广赚钱(HOT 标签)|帮助|下载 +- 主内容区(白色背景):按选中模块展示对应子模块页面 +- 底部信息(可选):版本号、API 状态、版权提示 + + +### 模块与子模块页面结构 + +#### 模块一:内容风格分析 +1) 子模块:对标分析 +- 顶部:标题“对标分析” + 简介 + 帮助入口 +- 操作面板(左侧卡片): + - 链接输入区:平台选择单选(抖音|小红书)+ 主页/视频链接输入框 + - 筛选项:点赞/收藏/评论范围、多选标签、时间范围、数量上限 + - 执行按钮:开始分析(主色)、清空、示例一键填充 +- 结果区(右侧/下方): + - 概览卡片:账号画像、样本量、时间窗口 + - 结构分析卡片:标题结构要素、镜头/段落结构、开场/转场/收束模式 + - 关键词统计:词云 + 频次表格 + - 情绪倾向:柱状/雷达图(正/负/中性 + 情绪强度) + - 文本质量检测:错别字、语法、可读性评分 + - 导出区:生成 Word 报告(含目录/图表/表格)、复制结论摘要 + - 扩展卡片:风格特征提示词(结构模式、表达方式、比喻/设问等) + +2) 子模块:文案创作 +- 顶部:标题“文案创作” + 风格来源选择(对标分析结果/手动配置) +- 左侧:创作参数卡片 + - 主题/选题输入、受众画像、平台体裁(短视频/图文)、字数与段落数 + - 风格特征(从对标分析继承或手动选择):语气、句式密度、金句频率、节奏 + - 约束:禁用词、合规提醒、敏感词检测 + - 生成按钮:生成文案(可多样本) +- 右侧:结果编辑器 + - 多样本标签页切换、评分与重写、段落级润色与扩写 + - 结构优化建议面板(共享逻辑):标题建议、开头抓取点、转场优化、CTA 变体 + - 导出:复制、保存为草稿、导出 Markdown/Word + +#### 模块二:热点趋势分析 +1) 子模块:热度分析 +- 顶部:标题“热度分析” + 数据源指示(官方 API|第三方) +- 左侧:关键词与订阅 + - 关键词输入(支持多个),平台选择,时间窗口,频率(实时/每日) + - 订阅开关:关注话题变化通知 + - 查询按钮 +- 右侧:趋势看板 + - 热度折线/面积图(支持对比多个关键词) + - 相关话题分布(词云/桑基/气泡图) + - 榜单列表(热度值、涨跌幅、来源、时间) + - 个性化推荐(可选):基于历史偏好推荐相近话题 + - 竞品分析(可选):同类内容的表现对比与优化方向 + - 导出:趋势报告 PDF/图片、订阅保存 + +2) 子模块:热点预测 +- 顶部:标题“热点预测” + 更新频率 + 最近同步时间 +- 左侧:数据设置 + - 平台榜单选择、历史维度、预测周期、预测方法(可选) + - 执行按钮:计算预测 +- 右侧:预测结果 + - 趋势外推曲线与置信区间 + - 上升/下降话题榜卡片 + - 风险提示与置信度标注 + - 原创建议(核心扩展):基于趋势给出选题与角度建议,一键跳转“文案创作” + +3) 子模块:文案创作(与模块一共享逻辑) +- 入口参数预填:来自热度分析/热点预测的关键词与角度 +- 结果编辑器同模块一,增加“热点引用标注”与“数据出处注记” + +#### 模块三:数字人 +1) 子模块:声音训练 +- 左侧:样本上传 + - 上传语音样本(数量/时长提示)、采样率与格式要求、合法性合规弹窗 + - 训练参数:噪声抑制、音域范围、情感维度 + - 提交训练按钮 +- 右侧:训练进度与质量 + - 进度条、阶段日志、预计完成时间 + - 质量检测结果:信噪比、清晰度、稳定性评分 + - 优化建议卡片 + +2) 子模块:生成配音 +- 左侧:文本输入与语气控制 + - 文案输入(或从“文案创作”选择)、说话速度、停连、情感标签 + - 说话人(训练好的声线列表)、发音词典(可选) + - 生成按钮 + 批量生成 +- 右侧:音频预览与管理 + - 播放、片段试听、对比 A/B、噪声/呼吸/口型对齐提示 + - 质量检测报告(可选):节奏、情感一致性、清晰度 + - 下载(mp3/wav)与导出到“剪映导入” + +3) 子模块:生成数字人 +- 左侧:配置 + - 数字人形象选择、背景与模板、脚本来源(文案 + 配音) + - 表情与动作控制(关键帧/预设)、唇形同步开关 + - 分辨率、时长、字幕选项 + - 生成按钮 +- 右侧:视频预览与任务队列 + - 渲染进度、日志、错误重试 + - 导出(mp4|mov)与推送至“剪映导入” + +#### 模块四:实时热点推送 +- 榜单看板:平台切换、实时/小时/日视图 +- 订阅管理:新增/编辑/删除、通知频率、通知方式 +- 趋势联动:一键进入“热点预测”分析 + +#### 模块五:素材混剪 +- 左侧:文案拆解 + - 从“文案创作”选择文案,自动分段(镜头/语义) + - 段落卡片:时长建议、情绪标签、镜头类型 +- 右侧:素材匹配与时间线 + - 素材库搜索(标签/颜色/场景/人物),AI 推荐位 + - 画布与时间线:轨道(视频/音频/字幕),吸附与对齐 + - 一键组合:按模版快速生成初版 + - 导出到“剪映导入” + +#### 模块六:剪映导入 +- 导入项选择:文案、字幕文件、配音音频、数字人视频、混剪工程 +- 格式兼容提示:编码、帧率、分辨率、自适配策略 +- 一键导入:生成剪映工程文件/素材包,打开路径/直接启动剪映(可选) + +### 设计要点(模块化增强) +- 色彩:紫色主色、深灰导航、白色内容区;状态色区分任务与错误 +- 图标:模块与子模块使用语义明确图标,功能与结果分区清晰 +- 交互:双栏布局优先(左操作右结果/预览),异步任务显著进度提示 +- 信息层次:概览 > 关键图表/卡片 > 详细表格/日志 +- 模块联动:子模块间保留“送往/引入”操作(如趋势 → 文案、文案 → 配音/数字人、生成项 → 剪映) +- 合规与提示:上传/生成环节均有合规提醒与风险提示,不展示原始受版权保护内容 + +### 技术实现提示词 +- 前端:Vue.js + Vite + Tailwind CSS,组件化(导航、表格、图表、上传、时间线) +- 状态管理:用户信息、通知数量、主题、任务队列、订阅配置 +- 图表:折线/面积/柱状/雷达/词云(ECharts 或 Chart.js) +- 文件:音视频上传与转码进度、任务轮询、失败重试 +- API 接口:模块化设计,参数入参与结构化响应,跨模块数据传递(如选题、风格特征、音视频产物) +- 导出:Word/PDF 报告、媒体文件、剪映工程/素材包 + +### 工作流快捷入口(横向步骤条) +- 对标分析 → 文案创作 → 生成配音/数字人 → 素材混剪 → 剪映导入 +- 每步显示完成状态与产物摘要,支持回溯与替换 \ No newline at end of file diff --git a/frontend/doc/金牌内容大师软件需求文档-合规版.md b/frontend/doc/金牌内容大师软件需求文档-合规版.md new file mode 100644 index 0000000000..3406e32ac6 --- /dev/null +++ b/frontend/doc/金牌内容大师软件需求文档-合规版.md @@ -0,0 +1,187 @@ +# 金牌内容大师软件需求文档(合规版) + +## 1. 项目概述 + +### 1.1 项目背景 +随着社交媒体平台的快速发展,内容创作已成为品牌营销和个人影响力建设的核心环节。为提升内容创作者的效率和质量,降低创作门槛,我们计划开发一款集内容分析、热点追踪、语音生成、数字人视频制作及素材管理于一体的综合性软件工具。 + +### 1.2 项目目标 +- 开发一款可安装在电脑上的金牌内容大师软件 +- 实现内容风格分析、原创内容生成、热点趋势分析等核心功能 +- 集成语音生成、数字人、素材混剪及剪映导入功能 +- 提供高效、稳定、易用的内容创作辅助工具 + +### 1.3 适用范围 +本软件适用于内容创作者、营销人员、自媒体博主等需要进行社交媒体内容创作和分析的用户,支持Windows和MacOS操作系统。 + +### 1.4 法律声明 +**重要提示**:本软件仅用于内容创作辅助和数据分析,用户需确保所提供内容的合法性。软件不承担任何版权责任,用户使用本软件产生的任何法律后果由用户自行承担。 + +## 2. 核心功能需求 + +### 2.1 功能一:内容风格分析 + +#### 2.1.1 核心功能 +- **链接内容解析**:用户提供博主主页或单条视频链接,系统进行内容结构分析 + - 支持抖音、小红书平台链接解析 + - 可选择点赞数、收藏数、评论数等关键变量进行筛选 + - 输出内容结构分析报告,而非原始文案内容 +- **内容特征提取**:分析视频内容的标题结构、关键词分布、情绪倾向 +- **Word报告输出**:包含内容结构分析、关键词统计、情绪分析等表格信息 +- **文本质量检测**:对用户上传的文本内容进行错别字检测和语法优化建议 + +#### 2.1.2 可选择功能 +- **风格特征生成**:基于分析结果生成内容风格特征描述、结构模式、表达方式等提示词 +- **原创内容生成**:基于用户提供的主题和风格特征,生成原创文案内容 +- **内容优化建议**:为用户提供内容结构优化和表达方式改进建议 + +### 2.2 功能二:热点趋势分析 + +#### 2.2.1 核心功能 +- **热点关键词检索**:输入热点选题关键词,检索公开的热点趋势数据 + - 支持从官方API获取热点数据 + - 支持用户订阅特定话题的趋势变化 +- **趋势分析报告**:生成热点趋势分析报告,包含关键词热度变化、相关话题分布等 +- **原创内容生成**:基于热点趋势和用户需求,生成原创内容建议 + +#### 2.2.2 可选择功能 +- **个性化推荐**:根据用户历史偏好推荐相关热点话题 +- **竞品分析**:分析同类内容的表现趋势和优化方向 + +### 2.3 功能三:实时热点推送 +- **热点数据更新**:通过官方API或第三方数据服务获取热点榜单数据 +- **热点展示**:以直观的方式展示各平台热点内容及相关数据 +- **趋势预测**:基于历史数据预测热点发展趋势 + +### 2.4 功能四:语音生成 +- **声线模拟**:用户上传语音样本,系统学习声线特征并生成相似声线的语音 +- **丰富语气**:生成的语音支持丰富的语气表达,符合语境要求 +- **语音优化**:提供语音质量检测和优化建议 + +### 2.5 功能五:数字人 +- **数字人调用**:可调用训练好的数字人模型 +- **数字人视频生成**:基于选定的数字人和内容,生成对应的数字人视频 +- **表情控制**:支持数字人表情和动作的精细控制 + +### 2.6 功能六:素材混剪 +- **文案拆解与素材匹配**:拆解文案内容,为对应的文案部分选择合适的素材 +- **素材组合**:组合所有选定的素材,生成完整的视频内容 +- **智能推荐**:基于内容特征智能推荐合适的素材 + +### 2.7 功能七:剪映导入 +- **无缝导入**:将上述功能生成的内容(文案、语音、数字人视频、混剪素材等)导入到剪映软件中进行进一步编辑 +- **格式兼容**:确保导入的内容与剪映软件格式兼容,减少用户手动调整 + +### 2.8 功能八:用户与账号 + +#### 2.8.1 核心功能 +- **微信扫码登录**:支持通过微信二维码进行登录授权 + - 扫码状态轮询与超时处理 + - 登录成功后建立用户会话(Token/Cookie) +- **头像入口**:登录后在页面右上角显示用户微信头像,点击进入个人中心 +- **个人中心**:展示并管理用户信息与偏好配置 + - 账号信息:昵称、头像、绑定手机号(可选) + - + +#### 2.8.2 可选择功能 +- **多端同步**:支持同账号多设备登录的会话管理与踢下线机制 +- **通知中心**:登录与安全异常提醒、热点订阅提醒 +- **企业微信/扫码登录扩展**:支持企业微信、飞书等扫码登录扩展 + +#### 2.8.3 合规与安全 +- 严格遵循微信开放平台与微信扫码登录接口规范,仅获取最小必要的用户信息 +- 明确授权用途并提供解除绑定入口,不保留敏感信息明文 +- 登录与个人数据传输全程 HTTPS,服务端存储敏感信息脱敏/加密 + +## 3. 系统架构 + +### 3.1 整体架构 +采用模块化设计,各功能模块相对独立又相互协作,主要包括: +- 数据解析模块:负责解析用户提供的链接和内容 +- 数据分析模块:对解析的数据进行分析,提取关键信息和特征 +- 内容生成模块:基于分析结果生成原创文案、语音、数字人视频等内容 +- 素材管理模块:管理用户上传和系统生成的各类素材 +- 用户界面模块:提供友好的用户交互界面 +- 外部接口模块:与剪映等外部软件进行数据交互 + +### 3.2 技术选型 +- **前端技术**:Vue3、Vite、Tailwind CSS、Ant Design Vue(桌面端采用 Electron 封装),确保跨平台兼容性与良好用户体验 +- **后端技术** java/ruoyi-vue-pro处理数据采集、分析和内容生成逻辑 +- **数据库**:MySQL,存储用户数据、配置信息和临时文件 +- **AI模型**:NLP模型(用于文案分析和生成)、语音合成模型(用于语音生成)、计算机视觉模型(用于数字人视频生成) +- **第三方API**:各社交平台开放API、语音合成API、剪映接口等 + +## 4. 技术要求 + + + +### 4.2 安全要求 +- **数据安全**:用户数据加密存储,防止数据泄露和滥用 +- **隐私保护**:严格遵守用户隐私政策,未经授权不得收集和使用用户个人信息 +- **权限管理**:合理设置软件权限,避免越权操作 +- **内容合规**:不存储原始内容,只保留分析结果和生成内容 + +### 4.3 兼容性要求 +- **操作系统**:支持Windows 10/11及MacOS 12+操作系统 +- **软件兼容**:与主流浏览器(Chrome、Firefox、Safari)和剪映等常用视频编辑软件兼容 +- **分辨率**:支持1080p及以上分辨率显示器 + +## 5. 用户界面要求 +- **简洁直观**:界面设计简洁明了,操作流程直观易懂 +- **功能分区**:各功能模块清晰分区,便于用户快速找到所需功能 +- **交互友好**:提供丰富的交互提示和帮助信息,减少用户学习成本 +- **自定义配置**:支持用户根据个人习惯自定义界面布局和功能设置 +- **多语言支持**:支持中文简体和英文界面切换 + +## 6. 法律合规要求 + +### 6.1 用户协议 +- **内容合法性**:用户需确保所提供内容的合法性,不得上传侵权内容 +- **使用目的**:软件仅用于内容创作辅助和数据分析,不得用于商业侵权 +- **免责声明**:软件不承担任何版权责任,用户使用产生的法律后果由用户自行承担 + +### 6.2 数据处理规范 +- **数据最小化**:只收集和处理必要的数据 +- **数据安全**:采用加密技术保护用户数据 +- **数据删除**:用户可随时删除个人数据 +- **数据共享**:不向第三方分享用户数据 + +### 6.3 内容生成规范 +- **原创性保证**:生成的内容基于算法和模型,不直接复制他人作品 +- **风格学习**:通过分析内容特征学习风格,而非复制具体表达 +- **用户责任**:用户对生成内容的使用承担全部责任 + +## 7. 培训与支持 +- **用户手册**:提供详细的用户操作手册,包含功能介绍、操作步骤和常见问题解答 +- **技术支持**:提供7*12小时技术支持服务,及时解决用户遇到的问题 +- **定期升级**:定期发布软件更新,修复bug并优化功能体验 + +## 8. 实施计划 +- **需求确认**:1周,明确用户需求和功能细节 +- **设计阶段**:2周,完成系统架构设计和界面原型设计 +- **开发实现**:8周,完成软件核心功能开发和模块集成 +- **测试验收**:2周,进行全面的功能测试和性能测试 +- **上线部署**:1周,发布正式版本并提供用户下载安装 + +## 9. 附录 + +### 9.1 术语定义 +- **金牌内容大师**:本软件的正式名称,指一款集内容分析、生成、管理于一体的综合性内容创作辅助工具 +- **内容风格分析**:通过分析内容特征提取风格特征,而非复制具体内容 +- **热点趋势分析**:基于公开数据分析热点话题的发展趋势 +- **数字人**:通过计算机技术生成的具有人类外观和行为特征的虚拟形象 +- **素材混剪**:将多个视频、音频、图片等素材按照一定的逻辑和节奏进行组合编辑 + +### 9.2 参考文档 +- 各社交平台开放平台开发者文档 +- 自然语言处理技术白皮书 +- 语音合成技术规范 +- 计算机视觉与数字人技术指南 +- 剪映软件接口文档 +- 数据保护法规和版权法相关文档 + +### 9.3 风险控制措施 +- **技术措施**:采用内容特征提取而非内容复制 +- **法律措施**:完善的用户协议和免责声明 +- **管理措施**:严格的数据处理和内容生成规范 +- **监控措施**:实时监控用户行为,防止违规使用 diff --git a/frontend/hooks/web/useCache.js b/frontend/hooks/web/useCache.js new file mode 100644 index 0000000000..ca2c307e0e --- /dev/null +++ b/frontend/hooks/web/useCache.js @@ -0,0 +1,39 @@ +/** + * 配置浏览器本地存储的方式,可直接存储对象数组。 + */ + +import WebStorageCache from 'web-storage-cache' + +export const CACHE_KEY = { + // 用户相关 + ROLE_ROUTERS: 'roleRouters', + USER: 'user', + VisitTenantId: 'visitTenantId', + // 系统设置 + IS_DARK: 'isDark', + LANG: 'lang', + THEME: 'theme', + LAYOUT: 'layout', + DICT_CACHE: 'dictCache', + // 登录表单 + LoginForm: 'loginForm', + TenantId: 'tenantId' +} + +export const useCache = (type ='localStorage') => { + const wsCache = new WebStorageCache({ + storage: type + }) + + return { + wsCache + } +} + +export const deleteUserCache = () => { + const { wsCache } = useCache() + wsCache.delete(CACHE_KEY.USER) + wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + wsCache.delete(CACHE_KEY.VisitTenantId) + // 注意,不要清理 LoginForm 登录表单 +} diff --git a/frontend/hooks/web/useVoiceText.ts b/frontend/hooks/web/useVoiceText.ts new file mode 100644 index 0000000000..31a8a4832c --- /dev/null +++ b/frontend/hooks/web/useVoiceText.ts @@ -0,0 +1,87 @@ +import CommonService from '@/api/common' +import type { AudioItem, TranscriptionResult } from '@/src/types/global' + +/** + * 转录数据接口 + */ +interface TranscriptionData { + file_url?: string + transcripts?: Array<{ text: string }> +} + +/** + * 响应结果接口 + */ +interface TranscriptionResponse { + results: Array<{ + transcription_url: string + }> +} + +/** + * 将音频列表转换为文本转录 + * @param list - 音频项列表 + * @returns 转录结果数组 + * @throws 当转录过程出错时抛出错误 + * + * @example + * const audioList = [{ audio_url: 'https://example.com/audio.mp3' }] + * const transcriptions = await getVoiceText(audioList) + * console.log(transcriptions) // [{ key: 'url', value: 'transcribed text' }] + */ +export async function getVoiceText(list: AudioItem[]): Promise { + // 调用API将视频转换为文本 + const ret = await CommonService.videoToCharacters({ + fileLinkList: list.map(item => item.audio_url), + }) + + // 解析响应数据 + const data: string = ret.data + const rst: TranscriptionResponse = JSON.parse(data) + const transcription_url: string[] = rst.results.map(item => item.transcription_url) + + // 并行获取所有转录内容 + const transcriptions: TranscriptionResult[] = await Promise.all( + (transcription_url || []).filter(Boolean).map(async (url: string): Promise => { + try { + const resp: Response = await fetch(url) + const contentType: string = resp.headers.get('content-type') || '' + const value: string = contentType.includes('application/json') + ? JSON.stringify(await resp.json()) + : await resp.text() + const parsed: TranscriptionData = JSON.parse(value) + return { + key: url, + audio_url: parsed.file_url, + value: parsed.transcripts?.[0]?.text || '' + } + } catch (e: unknown) { + console.warn('获取转写内容失败:', url, e) + return { key: url, value: '' } + } + }) + ) + return transcriptions +} + +/** + * Hook 返回值接口 + */ +interface UseVoiceTextReturn { + getVoiceText: (list: AudioItem[]) => Promise +} + +/** + * 语音文本转换 Hook + * @returns 包含 getVoiceText 方法的对象 + * + * @example + * const { getVoiceText } = useVoiceText() + * const result = await getVoiceText(audioList) + */ +export default function useVoiceText(): UseVoiceTextReturn { + return { getVoiceText } +} + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000..1091ec9b32 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "gold-master", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "gold": "pnpm --filter ./app/web-gold run dev", + "build:gold:prod": "pnpm --filter ./app/web-gold run build" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "localforage": "^1.10.0", + "unocss": "^66.5.4", + "web-storage-cache": "^1.1.1" + } +} diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000000..07e98d7abc --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +packages: + - build + - utils + # - packages/* + - app/* +catalog: + \ No newline at end of file