diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 4503a3e493..8a7c7b7453 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -45,7 +45,10 @@
"Bash(grep:*)",
"Bash(timeout:*)",
"Bash(pnpm run lint:es)",
- "Bash(npm install:*)"
+ "Bash(npm install:*)",
+ "Bash(wc -l:*)",
+ "Bash(npx tsc:*)",
+ "Bash(pnpm run lint:es:*)"
],
"deny": [],
"ask": []
diff --git a/CLAUDE.md b/CLAUDE.md
index 7f20f6009b..1c95ef4857 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -34,6 +34,7 @@ Keep this managed block so 'openspec update' can refresh the instructions.
- 状态管理使用Pinia,保持仓库模块化
- TypeScript类型定义规范
- 组件保持小巧专注,逻辑清晰
+- 样式优先使用less 示例:`
\ No newline at end of file
diff --git a/frontend/app/web-gold/src/components/GradientButton.vue b/frontend/app/web-gold/src/components/GradientButton.vue
index c4ecb9130e..455c344e63 100644
--- a/frontend/app/web-gold/src/components/GradientButton.vue
+++ b/frontend/app/web-gold/src/components/GradientButton.vue
@@ -93,29 +93,30 @@ const buttonClass = computed(() => {
display: inline-flex;
align-items: center;
justify-content: center;
- gap: 6px;
- padding: 8px 16px;
- border: 1px solid rgba(24, 144, 255, 0.3);
- border-radius: 4px;
- font-size: 14px;
+ gap: 8px;
+ padding: 8px 24px;
+ border: none;
+ border-radius: 12px;
+ font-size: 16px;
font-weight: 500;
color: #ffffff;
cursor: pointer;
- transition: all 0.15s ease;
- background: linear-gradient(135deg, #1890FF 0%, #40A9FF 100%);
- box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
+ transition: all 0.2s ease;
+ background: var(--color-slate-900);
+ box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.2), 0 4px 6px -2px rgba(59, 130, 246, 0.1);
user-select: none;
}
.gradient-button:hover {
- background: linear-gradient(135deg, #1890FF 0%, #40A9FF 100%);
- border-color: rgba(24, 144, 255, 0.5);
- box-shadow: 0 0 6px rgba(24, 144, 255, 0.25);
+ background: var(--color-slate-800);
+ transform: translateY(-1px);
+ box-shadow: 0 20px 25px -5px rgba(59, 130, 246, 0.3), 0 10px 10px -5px rgba(59, 130, 246, 0.15);
}
.gradient-button:active {
- background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
+ background: var(--color-slate-800);
+ transform: translateY(0);
+ box-shadow: 0 4px 6px -2px rgba(59, 130, 246, 0.15);
}
.gradient-button__content,
@@ -171,15 +172,14 @@ const buttonClass = computed(() => {
.gradient-button:disabled {
opacity: 0.4;
cursor: not-allowed;
- background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
- border-color: rgba(24, 144, 255, 0.2);
+ background: var(--color-slate-700);
box-shadow: none;
}
.gradient-button--disabled:hover,
.gradient-button:disabled:hover {
- background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
- border-color: rgba(24, 144, 255, 0.2);
+ background: var(--color-slate-700);
+ transform: none;
box-shadow: none;
}
diff --git a/frontend/app/web-gold/src/components/LoginModal.vue b/frontend/app/web-gold/src/components/LoginModal.vue
index 9aa0df80ee..12f1553a28 100644
--- a/frontend/app/web-gold/src/components/LoginModal.vue
+++ b/frontend/app/web-gold/src/components/LoginModal.vue
@@ -573,7 +573,7 @@ async function handleLoginSuccess(info) {
.brand-pane {
width: 50%;
height: 100%;
- background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
+ background: linear-gradient(135deg, var(--color-slate-900) 0%, var(--color-slate-800) 100%);
position: relative;
overflow: hidden;
}
@@ -609,7 +609,7 @@ async function handleLoginSuccess(info) {
width: 80px;
height: 80px;
border-radius: 16px;
- box-shadow: 0 8px 32px rgba(0, 176, 48, 0.3);
+ box-shadow: var(--shadow-blue);
}
.brand-title {
@@ -646,9 +646,9 @@ async function handleLoginSuccess(info) {
display: flex;
align-items: center;
justify-content: center;
- background: rgba(255, 255, 255, 0.05);
+ background: var(--color-slate-800);
border-radius: 10px;
- border: 1px solid rgba(255, 255, 255, 0.1);
+ border: 1px solid var(--color-slate-700);
}
.feature-text {
@@ -676,7 +676,7 @@ async function handleLoginSuccess(info) {
width: 40px;
height: 40px;
border: none;
- background: rgba(255, 255, 255, 0.05);
+ background: var(--color-slate-100);
border-radius: 10px;
color: var(--color-text-secondary);
cursor: pointer;
@@ -687,7 +687,7 @@ async function handleLoginSuccess(info) {
}
.close-btn:hover {
- background: rgba(255, 255, 255, 0.1);
+ background: var(--color-slate-200);
color: var(--color-text);
}
@@ -705,10 +705,10 @@ async function handleLoginSuccess(info) {
display: flex;
gap: 8px;
margin-bottom: 12px;
- background: rgba(255, 255, 255, 0.03);
+ background: var(--color-slate-50);
padding: 4px;
border-radius: 12px;
- border: 1px solid rgba(255, 255, 255, 0.05);
+ border: 1px solid var(--color-border);
}
.tab-btn {
@@ -727,7 +727,7 @@ async function handleLoginSuccess(info) {
.tab-btn.active {
background: var(--color-primary);
color: white;
- box-shadow: 0 2px 8px rgba(0, 176, 48, 0.3);
+ box-shadow: var(--shadow-sm);
}
/* 表单标题 */
@@ -775,7 +775,7 @@ async function handleLoginSuccess(info) {
padding: 0 16px;
border-radius: 12px;
border: 1px solid var(--color-border);
- background: rgba(255, 255, 255, 0.02);
+ background: var(--color-surface);
color: var(--color-text);
font-size: 16px;
transition: all 0.2s ease;
@@ -784,8 +784,8 @@ async function handleLoginSuccess(info) {
.form-input:focus {
outline: none;
border-color: var(--color-primary);
- background: rgba(255, 255, 255, 0.04);
- box-shadow: 0 0 0 3px rgba(0, 176, 48, 0.1);
+ background: var(--color-surface);
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input::placeholder {
@@ -806,7 +806,7 @@ async function handleLoginSuccess(info) {
width: 120px;
height: 48px;
border: 1px solid var(--color-border);
- background: rgba(255, 255, 255, 0.02);
+ background: var(--color-surface);
color: var(--color-text);
border-radius: 12px;
font-size: 14px;
@@ -815,7 +815,7 @@ async function handleLoginSuccess(info) {
}
.send-code-btn:hover:not(:disabled) {
- background: rgba(255, 255, 255, 0.05);
+ background: var(--color-slate-50);
border-color: var(--color-primary);
}
@@ -848,7 +848,7 @@ async function handleLoginSuccess(info) {
}
.forgot-btn:hover {
- color: #00b030;
+ color: var(--color-primary-hover);
}
.back-btn {
@@ -866,7 +866,7 @@ async function handleLoginSuccess(info) {
}
.back-btn:hover {
- background: rgba(255, 255, 255, 0.05);
+ background: var(--color-slate-50);
color: var(--color-text);
}
@@ -882,13 +882,13 @@ async function handleLoginSuccess(info) {
cursor: pointer;
margin-top: 8px;
transition: all 0.2s ease;
- box-shadow: 0 4px 16px rgba(0, 176, 48, 0.3);
+ box-shadow: var(--shadow-blue);
}
.submit-btn:hover:not(:disabled) {
- background: #00b030;
+ background: var(--color-primary-hover);
transform: translateY(-1px);
- box-shadow: 0 6px 20px rgba(0, 176, 48, 0.4);
+ box-shadow: var(--shadow-blue);
}
.submit-btn:disabled {
@@ -917,7 +917,7 @@ async function handleLoginSuccess(info) {
}
.link-btn:hover {
- color: #00b030;
+ color: var(--color-primary-hover);
}
/* 协议同意 */
@@ -952,7 +952,7 @@ async function handleLoginSuccess(info) {
}
.agreement-link:hover {
- color: #00b030;
+ color: var(--color-primary-hover);
}
/* 响应式设计 */
diff --git a/frontend/app/web-gold/src/components/SidebarNav.vue b/frontend/app/web-gold/src/components/SidebarNav.vue
index 204855d2e8..15a4c0397d 100644
--- a/frontend/app/web-gold/src/components/SidebarNav.vue
+++ b/frontend/app/web-gold/src/components/SidebarNav.vue
@@ -124,30 +124,39 @@ function go(item) {
.nav-item {
height: 40px;
- border-radius: var(--radius-card);
+ border-radius: 12px;
display: flex;
align-items: center;
gap: 10px;
- padding: 0 12px;
- color: var(--color-text-secondary);
- background: var(--color-surface);
- border: 1px solid var(--color-border);
+ padding: 8px 12px;
+ color: var(--color-slate-600);
+ background: transparent;
+ border: 1px solid transparent;
cursor: pointer;
transition: background .2s ease, color .2s ease, box-shadow .2s ease, transform .12s ease, border-color .2s ease;
+ width: 100%;
+ text-align: left;
+ font-size: 14px;
+ font-weight: 400;
}
.nav-item:hover {
- background: #161616; /* hover态:加深 */
- color: var(--color-text);
+ background: var(--color-slate-50);
+ color: var(--color-slate-700);
}
.nav-item.is-active {
- background: var(--color-primary);;
- color: var(--color-text);
- border-color: var(--color-primary);
+ background: var(--color-blue-50);
+ color: var(--color-blue-700);
+ border-color: transparent;
+}
+
+.nav-item.is-active:hover {
+ background: var(--color-blue-100);
+ color: var(--color-blue-800);
}
.nav-item__icon { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; }
-.nav-item__label { font-size: var(--font-body-size); font-weight: 600; }
+.nav-item__label { font-size: 14px; }
diff --git a/frontend/app/web-gold/src/components/TopNav.vue b/frontend/app/web-gold/src/components/TopNav.vue
index 4a585c903b..aa10313c6c 100644
--- a/frontend/app/web-gold/src/components/TopNav.vue
+++ b/frontend/app/web-gold/src/components/TopNav.vue
@@ -7,8 +7,8 @@ import UserDropdown from '@/components/UserDropdown.vue'
import TestService from '@/api/test'
const styles = {
- background: 'var(--color-surface)',
- color: 'var(--color-text)'
+ background: 'var(--color-slate-900)',
+ color: 'var(--color-text-inverse)'
}
// const route = useRoute()
const userStore = useUserStore()
@@ -137,29 +137,29 @@ const shouldShowUser = computed(() => {
.btn-primary-nav {
height: 32px;
padding: 0 12px;
- border-radius: 8px;
+ border-radius: var(--radius-button);
background: var(--color-primary);
- color: #fff;
- font-size: 12px;
+ color: var(--color-text-inverse);
+ font-size: 14px;
font-weight: 600;
- box-shadow: var(--glow-primary);
+ box-shadow: var(--shadow-blue);
transition: transform .2s ease, box-shadow .2s ease, filter .2s ease;
}
.btn-primary-nav:hover {
transform: translateY(-1px);
- box-shadow: var(--glow-primary);
+ box-shadow: var(--shadow-blue);
filter: brightness(1.03);
}
.btn-test-nav {
height: 32px;
padding: 0 12px;
- border-radius: 8px;
+ border-radius: var(--radius-button);
background: var(--color-surface);
border: 1px solid var(--color-border);
color: var(--color-text);
- font-size: 12px;
+ font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all .2s ease;
diff --git a/frontend/app/web-gold/src/main.js b/frontend/app/web-gold/src/main.js
index ce1f643532..285a1216d6 100644
--- a/frontend/app/web-gold/src/main.js
+++ b/frontend/app/web-gold/src/main.js
@@ -7,7 +7,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import App from './App.vue'
import router from './router'
-import './style.css'
+import './style.less'
const app = createApp(App)
const pinia = createPinia()
diff --git a/frontend/app/web-gold/src/style.css b/frontend/app/web-gold/src/style.css
deleted file mode 100644
index f257d965df..0000000000
--- a/frontend/app/web-gold/src/style.css
+++ /dev/null
@@ -1,117 +0,0 @@
-@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/style.less b/frontend/app/web-gold/src/style.less
new file mode 100644
index 0000000000..6dd30fa1eb
--- /dev/null
+++ b/frontend/app/web-gold/src/style.less
@@ -0,0 +1,242 @@
+@import "tailwindcss";
+
+/* ================================
+ 模块化CSS设计系统
+ ================================ */
+
+/* ================================
+ 1. 设计令牌 (Design Tokens)
+ ================================ */
+:root {
+ /* 主色系 - Slate(石板色) */
+ --color-slate-50: #f8fafc;
+ --color-slate-100: #f1f5f9;
+ --color-slate-200: #e2e8f0;
+ --color-slate-300: #cbd5e1;
+ --color-slate-400: #94a3b8;
+ --color-slate-500: #64748b;
+ --color-slate-600: #475569;
+ --color-slate-700: #334155;
+ --color-slate-800: #1e293b;
+ --color-slate-900: #0f172a;
+
+ /* 强调色 - Blue(蓝色) */
+ --color-blue-400: #60a5fa;
+ --color-blue-500: #3b82f6;
+ --color-blue-600: #2563eb;
+ --color-blue-700: #1d4ed8;
+
+ /* 辅助色 - Indigo(靛蓝) */
+ --color-indigo-50: #eef2ff;
+ --color-indigo-100: #e0e7ff;
+ --color-indigo-500: #6366f1;
+ --color-indigo-600: #4f46e5;
+ --color-indigo-700: #4338ca;
+ --color-indigo-800: #3730a3;
+ --color-indigo-900: #312e81;
+
+ /* 功能色 */
+ --color-green-500: #10b981;
+ --color-yellow-400: #facc15;
+ --color-yellow-500: #eab308;
+ --color-red-500: #ef4444;
+ --color-red-800: #991b1b;
+
+ /* 中性色 */
+ --color-gray-100: #f3f4f6;
+ --color-gray-300: #d1d5db;
+ --color-gray-400: #9ca3af;
+ --color-gray-700: #374151;
+
+ /* 主题设计令牌 */
+ --color-bg: var(--color-slate-50);
+ --color-surface: #ffffff;
+ --color-header: var(--color-slate-900);
+ --color-text: var(--color-slate-700);
+ --color-text-secondary: var(--color-slate-500);
+ --color-text-inverse: #ffffff;
+ --color-border: var(--color-slate-200);
+ --color-border-focus: var(--color-blue-500);
+ --color-border-selected: var(--color-indigo-500);
+ --color-primary: var(--color-blue-500);
+ --color-primary-hover: var(--color-blue-600);
+
+ /* 尺寸系统 */
+ --radius-card: 12px;
+ --radius-button: 8px;
+ --radius-tag: 4px;
+
+ /* 阴影系统 */
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
+ --shadow-blue: 0 10px 15px -3px rgb(59 130 246 / 0.2);
+
+ /* 间距系统 */
+ --space-0-5: 2px;
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 20px;
+ --space-6: 24px;
+ --space-8: 32px;
+}
+
+/* ================================
+ 2. 全局基础样式 (Global Base)
+ ================================ */
+html, body, #app {
+ background: var(--color-bg);
+ color: var(--color-text);
+ font-size: 16px;
+ line-height: 1.5;
+}
+
+body { scrollbar-gutter: stable both-edges; }
+
+
+
+/* ================================
+ 3. 组件样式 (Component Styles)
+ ================================ */
+
+/* Button 组件 */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-button);
+ font-weight: 500;
+ transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ cursor: pointer;
+ border: none;
+ outline: none;
+ padding: var(--space-2) var(--space-6);
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ &--primary {
+ background: var(--color-slate-900);
+ color: white;
+ box-shadow: var(--shadow-lg);
+
+ &:hover:not(:disabled) {
+ background: var(--color-slate-800);
+ }
+ }
+
+ &--secondary {
+ background: white;
+ color: var(--color-slate-700);
+ border: 1px solid var(--color-border);
+ padding: var(--space-1) var(--space-4);
+
+ &:hover:not(:disabled) {
+ background: var(--color-slate-50);
+ }
+ }
+
+ &--gradient {
+ background: linear-gradient(to right, var(--color-indigo-600), var(--color-indigo-800));
+ color: white;
+ box-shadow: var(--shadow-blue);
+
+ &:hover:not(:disabled) {
+ background: linear-gradient(to right, var(--color-indigo-700), var(--color-indigo-900));
+ }
+ }
+}
+
+/* Input 组件 */
+.input {
+ width: 100%;
+ padding: var(--space-2) var(--space-4);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-button);
+ font-size: 14px;
+ transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ background: white;
+ color: var(--color-text);
+
+ &:focus {
+ outline: none;
+ border-color: var(--color-border-focus);
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
+ }
+
+ &::placeholder {
+ color: var(--color-slate-400);
+ }
+}
+
+/* Card 组件 */
+.card {
+ background: white;
+ border: 1px solid var(--color-slate-200);
+ border-radius: var(--radius-card);
+ padding: var(--space-6);
+ box-shadow: var(--shadow-sm);
+ transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
+
+ &:hover {
+ box-shadow: var(--shadow-md);
+ }
+}
+
+/* Table 组件 */
+.table {
+ width: 100%;
+ border-collapse: collapse;
+
+ th {
+ background: var(--color-slate-50);
+ padding: var(--space-3) var(--space-4);
+ text-align: left;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-slate-500);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ border-bottom: 1px solid var(--color-slate-200);
+ }
+
+ td {
+ padding: var(--space-4);
+ border-bottom: 1px solid var(--color-slate-100);
+ }
+
+ tr:hover {
+ background: var(--color-slate-50);
+ }
+}
+
+/* Tag 组件 */
+.tag {
+ display: inline-block;
+ padding: var(--space-0-5) var(--space-2);
+ font-size: 12px;
+ font-weight: 500;
+ border-radius: var(--radius-tag);
+ background: var(--color-gray-100);
+ color: var(--color-slate-700);
+
+ &--red {
+ background: #fee2e2;
+ color: var(--color-red-800);
+ }
+
+ &--yellow {
+ background: #fef3c7;
+ color: var(--color-yellow-600);
+ }
+
+ &--vip {
+ color: var(--color-yellow-500);
+ border: 1px solid var(--color-yellow-500);
+ background: transparent;
+ }
+}
diff --git a/frontend/app/web-gold/src/views/capcut/CapcutImport.vue b/frontend/app/web-gold/src/views/capcut/CapcutImport.vue
deleted file mode 100644
index b5b519a58d..0000000000
--- a/frontend/app/web-gold/src/views/capcut/CapcutImport.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
剪映导入
-
选择文案/字幕/音频/数字人视频/工程,一键导入。
-
-
-
-
-
-
-
diff --git a/frontend/app/web-gold/src/views/content-style/Copywriting.vue b/frontend/app/web-gold/src/views/content-style/Copywriting.vue
index dfa13c39af..39e5d7e015 100644
--- a/frontend/app/web-gold/src/views/content-style/Copywriting.vue
+++ b/frontend/app/web-gold/src/views/content-style/Copywriting.vue
@@ -544,7 +544,7 @@ defineOptions({ name: 'ContentStyleCopywriting' })
-
@@ -792,7 +792,7 @@ defineOptions({ name: 'ContentStyleCopywriting' })
.result-card {
background: var(--color-surface);
border-radius: 8px;
- box-shadow: var(--shadow-inset-card);
+ box-shadow: var(--shadow-sm);
border: 1px solid var(--color-border);
transition: all 0.3s ease;
}
@@ -842,8 +842,8 @@ defineOptions({ name: 'ContentStyleCopywriting' })
/* 表单标签后的内联提示(不使用 emoji) */
.form-tip-inline {
- margin-left: 8px;
- font-size: 12px;
+ margin-left: var(--space-2);
+ font-size: 14px;
color: var(--color-text-secondary);
font-weight: 400;
}
@@ -932,7 +932,7 @@ defineOptions({ name: 'ContentStyleCopywriting' })
}
:deep(.ant-slider-rail) {
- background-color: #252525; /* 未选中轨道更深,增强对比 */
+ background-color: var(--color-slate-200);
height: 4px;
}
@@ -973,13 +973,13 @@ defineOptions({ name: 'ContentStyleCopywriting' })
.empty-title {
font-size: 16px;
font-weight: 600;
- color: #374151;
- margin-bottom: 8px;
+ color: var(--color-text);
+ margin-bottom: var(--space-2);
}
.empty-desc {
font-size: 14px;
- color: #6b7280;
+ color: var(--color-text-secondary);
line-height: 1.6;
max-width: 300px;
margin: 0 auto;
@@ -1034,7 +1034,7 @@ defineOptions({ name: 'ContentStyleCopywriting' })
.save-btn:hover {
background: var(--color-primary);
filter: brightness(1.04);
- box-shadow: var(--glow-primary);
+ box-shadow: var(--shadow-blue);
}
.cancel-btn {
@@ -1048,14 +1048,14 @@ defineOptions({ name: 'ContentStyleCopywriting' })
}
.generated-content {
- padding: 24px;
- background: #111111;
- border-radius: 8px;
+ padding: var(--space-6);
+ background: var(--color-surface);
+ border-radius: var(--radius-card);
border: 1px solid var(--color-border);
line-height: 1.9;
- color: #f5f5f5;
+ color: var(--color-text);
min-height: 400px;
- font-size: 15.5px;
+ font-size: 15px;
white-space: pre-wrap;
word-break: break-word;
-webkit-font-smoothing: antialiased;
@@ -1065,33 +1065,33 @@ defineOptions({ name: 'ContentStyleCopywriting' })
.generated-content :deep(h1) {
font-size: 22px;
font-weight: 600;
- margin-bottom: 16px;
- color: #ffffff;
- padding-bottom: 8px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ margin-bottom: var(--space-4);
+ color: var(--color-text);
+ padding-bottom: var(--space-2);
+ border-bottom: 1px solid var(--color-border);
}
.generated-content :deep(h2) {
font-size: 19px;
font-weight: 600;
margin: 22px 0 12px 0;
- color: #fff;
+ color: var(--color-text);
padding-bottom: 6px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ border-bottom: 1px solid var(--color-border);
}
.generated-content :deep(h3) {
font-size: 16px;
font-weight: 600;
margin: 18px 0 10px 0;
- color: #efefef;
+ color: var(--color-text);
}
.generated-content :deep(p) {
margin: 12px 0 14px 0;
- color: #e3e6ea;
+ color: var(--color-text);
line-height: 1.9;
- font-size: 15.5px;
+ font-size: 15px;
}
.generated-content :deep(ul),
@@ -1102,50 +1102,50 @@ defineOptions({ name: 'ContentStyleCopywriting' })
.generated-content :deep(li) {
margin: 6px 0;
- color: #e3e6ea;
+ color: var(--color-text);
line-height: 1.9;
- font-size: 15.5px;
+ font-size: 15px;
}
.generated-content :deep(strong) {
font-weight: 600;
- color: #ffffff;
+ color: var(--color-text);
}
.generated-content :deep(code) {
- background: #0b0b0b;
+ background: var(--color-slate-100);
padding: 3px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13.5px;
- color: #ffb86c;
- border: 1px solid rgba(255, 255, 255, 0.12);
+ color: var(--color-red-500);
+ border: 1px solid var(--color-border);
}
.generated-content :deep(pre) {
- background: #0b0b0b;
+ background: var(--color-slate-100);
padding: 16px 18px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
- border: 1px solid rgba(255, 255, 255, 0.12);
+ border: 1px solid var(--color-border);
}
.generated-content :deep(pre code) {
background: transparent;
padding: 0;
border: none;
- color: #ffb86c;
+ color: var(--color-red-500);
}
.generated-content :deep(blockquote) {
margin: 16px 0;
padding: 12px 16px;
- background: rgba(22, 119, 255, 0.12);
+ background: rgba(59, 130, 246, 0.08);
border-left: 4px solid var(--color-primary);
border-radius: 0 6px 6px 0;
font-style: italic;
- color: #e3e6ea;
+ color: var(--color-text);
}
/* 提示词标签样式 */
diff --git a/frontend/app/web-gold/src/views/content-style/components/BenchmarkForm.vue b/frontend/app/web-gold/src/views/content-style/components/BenchmarkForm.vue
index 3046552960..d1bc91a961 100644
--- a/frontend/app/web-gold/src/views/content-style/components/BenchmarkForm.vue
+++ b/frontend/app/web-gold/src/views/content-style/components/BenchmarkForm.vue
@@ -81,13 +81,13 @@ function handleReset() {
}
.form-hint {
- margin-top: 6px;
- font-size: 12px;
+ margin-top: var(--space-1);
+ font-size: 14px;
color: var(--color-text-secondary);
}
:deep(.ant-input), :deep(.ant-input-affix-wrapper), :deep(textarea) {
- background: #0f0f0f;
+ background: var(--color-surface);
border-color: var(--color-border);
}
@@ -105,7 +105,7 @@ function handleReset() {
}
:deep(.ant-slider-rail) {
- background-color: #252525;
+ background-color: var(--color-slate-200);
height: 4px;
}
diff --git a/frontend/app/web-gold/src/views/content-style/components/ExpandedRowContent.vue b/frontend/app/web-gold/src/views/content-style/components/ExpandedRowContent.vue
index e620191069..8d9e7b3da5 100644
--- a/frontend/app/web-gold/src/views/content-style/components/ExpandedRowContent.vue
+++ b/frontend/app/web-gold/src/views/content-style/components/ExpandedRowContent.vue
@@ -103,10 +103,10 @@ function handleCreateContent() {
diff --git a/frontend/app/web-gold/src/views/kling/IdentifyFace.vue b/frontend/app/web-gold/src/views/kling/IdentifyFace.vue
index a1adad99e4..96cf72efd7 100644
--- a/frontend/app/web-gold/src/views/kling/IdentifyFace.vue
+++ b/frontend/app/web-gold/src/views/kling/IdentifyFace.vue
@@ -103,7 +103,7 @@
{{ formatDuration(videoState.selectedVideo.duration) }}
-
+
更换
@@ -129,7 +129,7 @@
{{ videoState.videoFile?.name }}
-
+
更换
@@ -181,7 +181,7 @@
视频时长必须大于音频时长才能生成数字人视频
@@ -200,7 +200,7 @@
:disabled="!canGenerateAudio"
:loading="audioState.generating"
block
- @click="handleGenerateAudio"
+ @click="generateAudio"
>
{{ audioState.generating ? '生成中...' : '生成配音(用于校验时长)' }}
@@ -247,7 +247,7 @@
重新生成
@@ -263,7 +263,7 @@
size="large"
:disabled="!canGenerate"
block
- @click="handleGenerate"
+ @click="generateDigitalHuman"
>
生成数字人视频
@@ -285,493 +285,74 @@
-
-
diff --git a/frontend/app/web-gold/src/views/kling/hooks/useDigitalHumanGeneration.ts b/frontend/app/web-gold/src/views/kling/hooks/useDigitalHumanGeneration.ts
index d0d286de35..6375e10738 100644
--- a/frontend/app/web-gold/src/views/kling/hooks/useDigitalHumanGeneration.ts
+++ b/frontend/app/web-gold/src/views/kling/hooks/useDigitalHumanGeneration.ts
@@ -9,19 +9,15 @@ import type {
UseDigitalHumanGeneration,
VideoState,
IdentifyState,
- MaterialValidation,
Video,
- AudioState,
} from '../types/identify-face'
import { identifyUploadedVideo, uploadAndIdentifyVideo } from '@/api/kling'
/**
* 数字人生成 Hook
- * @param audioState 音频状态(来自父 Hook)
+ * 独立管理所有状态,不依赖外部状态
*/
-export function useDigitalHumanGeneration(
- audioState: AudioState
-): UseDigitalHumanGeneration {
+export function useDigitalHumanGeneration(): UseDigitalHumanGeneration {
// ==================== 响应式状态 ====================
const videoState = ref({
@@ -43,13 +39,6 @@ export function useDigitalHumanGeneration(
videoFileId: null,
})
- const materialValidation = ref({
- videoDuration: 0,
- audioDuration: 0,
- isValid: false,
- showDetails: false,
- })
-
// ==================== 计算属性 ====================
/**
@@ -59,16 +48,6 @@ export function useDigitalHumanGeneration(
return identifyState.value.faceEndTime - identifyState.value.faceStartTime
})
- /**
- * 是否可以生成数字人视频
- */
- const canGenerate = computed(() => {
- const hasVideo = videoState.value.uploadedVideo || videoState.value.selectedVideo
- const audioValidated = audioState.validationPassed
- const materialValidated = materialValidation.value.isValid
- return !!(hasVideo && audioValidated && materialValidated)
- })
-
// ==================== 核心方法 ====================
/**
@@ -87,7 +66,6 @@ export function useDigitalHumanGeneration(
videoState.value.videoSource = 'upload'
resetIdentifyState()
- resetMaterialValidation()
await performFaceRecognition()
}
@@ -104,7 +82,6 @@ export function useDigitalHumanGeneration(
resetIdentifyState()
identifyState.value.videoFileId = video.id
- materialValidation.value.videoDuration = (video.duration || 0) * 1000
performFaceRecognition()
}
@@ -149,18 +126,6 @@ export function useDigitalHumanGeneration(
}
}
- /**
- * 验证素材时长
- */
- const validateMaterialDuration = (videoDurationMs: number, audioDurationMs: number): boolean => {
- const isValid = videoDurationMs > audioDurationMs
-
- materialValidation.value.videoDuration = videoDurationMs
- materialValidation.value.audioDuration = audioDurationMs
- materialValidation.value.isValid = isValid
-
- return isValid
- }
/**
* 重置视频状态
@@ -174,7 +139,6 @@ export function useDigitalHumanGeneration(
videoState.value.selectorVisible = false
resetIdentifyState()
- resetMaterialValidation()
}
/**
@@ -209,31 +173,20 @@ export function useDigitalHumanGeneration(
identifyState.value.videoFileId = null
}
- /**
- * 重置素材校验状态
- */
- const resetMaterialValidation = (): void => {
- materialValidation.value.videoDuration = 0
- materialValidation.value.audioDuration = 0
- materialValidation.value.isValid = false
- }
-
return {
// 响应式状态
videoState,
identifyState,
- materialValidation,
// 计算属性
faceDuration,
- canGenerate,
// 方法
handleFileUpload,
handleVideoSelect,
performFaceRecognition,
- validateMaterialDuration,
resetVideoState,
+ resetIdentifyState,
getVideoPreviewUrl,
}
}
diff --git a/frontend/app/web-gold/src/views/kling/hooks/useIdentifyFaceController.ts b/frontend/app/web-gold/src/views/kling/hooks/useIdentifyFaceController.ts
index b2e9631d0a..d24dc35072 100644
--- a/frontend/app/web-gold/src/views/kling/hooks/useIdentifyFaceController.ts
+++ b/frontend/app/web-gold/src/views/kling/hooks/useIdentifyFaceController.ts
@@ -3,25 +3,78 @@
* @author Claude Code
*/
-import { computed } from 'vue'
+import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import type {
UseIdentifyFaceController,
- UseVoiceGeneration,
- UseDigitalHumanGeneration,
LipSyncTaskData,
+ MaterialValidation,
} from '../types/identify-face'
+// @ts-ignore
import { createLipSyncTask } from '@/api/kling'
+// 导入子 Hooks
+import { useVoiceGeneration } from './useVoiceGeneration'
+import { useDigitalHumanGeneration } from './useDigitalHumanGeneration'
+
/**
- * 识别控制器 Hook
- * @param voiceGeneration 语音生成 Hook
- * @param digitalHuman 数字人生成 Hook
+ * 识别控制器 Hook - 充当协调器
+ * 内部直接创建和管理两个子 Hook
*/
-export function useIdentifyFaceController(
- voiceGeneration: UseVoiceGeneration,
- digitalHuman: UseDigitalHumanGeneration
-): UseIdentifyFaceController {
+export function useIdentifyFaceController(): UseIdentifyFaceController {
+ // ==================== 创建子 Hooks ====================
+
+ // 1. 创建语音生成 Hook(独立管理状态)
+ const voiceGeneration = useVoiceGeneration()
+
+ // 2. 创建数字人生成 Hook(独立管理状态)
+ const digitalHuman = useDigitalHumanGeneration()
+
+ // 3. Controller 统一管理跨 Hook 的状态
+ const materialValidation = ref({
+ videoDuration: 0,
+ audioDuration: 0,
+ isValid: false,
+ showDetails: false,
+ })
+
+ // 4. 监听音频状态变化,自动触发素材校验
+ watch(
+ () => voiceGeneration.audioState.value.generated && voiceGeneration.audioState.value.durationMs > 0,
+ (newVal, oldVal) => {
+ if (newVal && !oldVal) {
+ // 音频生成完成,获取视频时长并校验
+ const videoDuration = digitalHuman.faceDuration.value || 0
+ const audioDuration = voiceGeneration.audioState.value.durationMs
+
+ if (videoDuration > 0) {
+ validateMaterialDuration(videoDuration, audioDuration)
+ }
+ }
+ },
+ { flush: 'post' }
+ )
+
+ // 5. 监听人脸识别状态变化,更新素材校验的视频时长
+ watch(
+ () => digitalHuman.identifyState.value.identified,
+ (newVal, oldVal) => {
+ if (newVal && !oldVal) {
+ // 人脸识别成功,获取视频时长
+ const videoDuration = digitalHuman.faceDuration.value
+
+ // 如果已有音频,则重新校验
+ if (voiceGeneration.audioState.value.generated && voiceGeneration.audioState.value.durationMs > 0) {
+ const audioDuration = voiceGeneration.audioState.value.durationMs
+ validateMaterialDuration(videoDuration, audioDuration)
+ } else {
+ // 否则只更新视频时长
+ materialValidation.value.videoDuration = videoDuration
+ }
+ }
+ },
+ { flush: 'post' }
+ )
// ==================== 计算属性 ====================
/**
@@ -32,7 +85,7 @@ export function useIdentifyFaceController(
const hasVoice = voiceGeneration.selectedVoiceMeta.value
const hasVideo = digitalHuman.videoState.value.uploadedVideo || digitalHuman.videoState.value.selectedVideo
const audioValidated = voiceGeneration.audioState.value.validationPassed
- const materialValidated = digitalHuman.materialValidation.value.isValid
+ const materialValidated = materialValidation.value.isValid
return !!(hasText && hasVoice && hasVideo && audioValidated && materialValidated)
})
@@ -92,23 +145,18 @@ export function useIdentifyFaceController(
try {
// 如果未识别,先进行人脸识别
if (!digitalHuman.identifyState.value.identified) {
- message.loading('正在进行人脸识别...', 0)
-
const hasUploadFile = digitalHuman.videoState.value.videoFile
const hasSelectedVideo = digitalHuman.videoState.value.selectedVideo
if (!hasUploadFile && !hasSelectedVideo) {
- message.destroy()
message.warning('请先选择或上传视频')
return
}
try {
await digitalHuman.performFaceRecognition()
- message.destroy()
message.success('人脸识别完成')
} catch (error) {
- message.destroy()
return
}
}
@@ -209,7 +257,8 @@ export function useIdentifyFaceController(
* 触发文件选择
*/
const triggerFileSelect = (): void => {
- document.querySelector('input[type="file"]')?.click()
+ const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
+ fileInput?.click()
}
/**
@@ -219,7 +268,6 @@ export function useIdentifyFaceController(
digitalHuman.videoState.value.videoSource = 'upload'
digitalHuman.videoState.value.selectedVideo = null
digitalHuman.resetIdentifyState()
- digitalHuman.resetMaterialValidation()
}
/**
@@ -284,16 +332,88 @@ export function useIdentifyFaceController(
return `${size.toFixed(1)} ${units[unitIndex]}`
}
- return {
- // 组合子 Hooks
- voiceGeneration,
- digitalHuman,
+ /**
+ * 重置素材校验状态
+ */
+ const resetMaterialValidation = (): void => {
+ materialValidation.value.videoDuration = 0
+ materialValidation.value.audioDuration = 0
+ materialValidation.value.isValid = false
+ }
- // 业务流程方法
+ /**
+ * 验证素材时长
+ * 视频时长必须大于音频时长
+ */
+ const validateMaterialDuration = (videoDurationMs: number, audioDurationMs: number): boolean => {
+ materialValidation.value.videoDuration = videoDurationMs
+ materialValidation.value.audioDuration = audioDurationMs
+ materialValidation.value.isValid = videoDurationMs > audioDurationMs
+
+ if (!materialValidation.value.isValid) {
+ const videoSec = (videoDurationMs / 1000).toFixed(1)
+ const audioSec = (audioDurationMs / 1000).toFixed(1)
+ message.warning(`素材校验失败:视频时长(${videoSec}s)必须大于音频时长(${audioSec}s)`)
+ }
+
+ return materialValidation.value.isValid
+ }
+
+ // ==================== 解构子 Hooks 的响应式变量 ====================
+
+ // 语音生成相关
+ const {
+ ttsText,
+ speechRate,
+ selectedVoiceMeta,
+ audioState,
+ canGenerateAudio,
+ suggestedMaxChars,
+ generateAudio,
+ resetAudioState,
+ } = voiceGeneration
+
+ // 数字人生成相关
+ const {
+ videoState,
+ identifyState,
+ faceDuration,
+ performFaceRecognition,
+ handleFileUpload,
+ getVideoPreviewUrl,
+ resetVideoState,
+ resetIdentifyState,
+ } = digitalHuman
+
+ return {
+ // ==================== 语音生成相关 ====================
+ ttsText,
+ speechRate,
+ selectedVoiceMeta,
+ audioState,
+ canGenerateAudio,
+ suggestedMaxChars,
+ generateAudio,
+ resetAudioState,
+
+ // ==================== 数字人生成相关 ====================
+ videoState,
+ identifyState,
+ materialValidation,
+ faceDuration,
+ performFaceRecognition,
+ handleFileUpload,
+ getVideoPreviewUrl,
+ resetVideoState,
+ resetIdentifyState,
+ resetMaterialValidation,
+ validateMaterialDuration,
+
+ // ==================== 业务流程方法 ====================
generateDigitalHuman,
replaceVideo,
- // 事件处理方法
+ // ==================== 事件处理方法 ====================
handleVoiceSelect,
handleFileSelect,
handleDrop,
@@ -304,11 +424,11 @@ export function useIdentifyFaceController(
handleSimplifyScript,
handleVideoLoaded,
- // UI 辅助方法
+ // ==================== UI 辅助方法 ====================
formatDuration,
formatFileSize,
- // 计算属性
+ // ==================== 计算属性 ====================
canGenerate,
maxTextLength,
textareaPlaceholder,
diff --git a/frontend/app/web-gold/src/views/kling/hooks/useVoiceGeneration.ts b/frontend/app/web-gold/src/views/kling/hooks/useVoiceGeneration.ts
index affa6aad3f..25a28190d2 100644
--- a/frontend/app/web-gold/src/views/kling/hooks/useVoiceGeneration.ts
+++ b/frontend/app/web-gold/src/views/kling/hooks/useVoiceGeneration.ts
@@ -9,20 +9,16 @@ import type {
UseVoiceGeneration,
AudioState,
VoiceMeta,
- IdentifyState,
AudioData,
} from '../types/identify-face'
+// @ts-ignore
import { VoiceService } from '@/api/voice'
/**
* 语音生成 Hook
- * @param identifyState 人脸识别状态(来自父 Hook)
- * @param faceDuration 人脸出现时长(毫秒)
+ * 独立管理所有状态,不依赖外部状态
*/
-export function useVoiceGeneration(
- identifyState: IdentifyState,
- faceDuration: number
-): UseVoiceGeneration {
+export function useVoiceGeneration(): UseVoiceGeneration {
// ==================== 响应式状态 ====================
const ttsText = ref('')
@@ -43,17 +39,16 @@ export function useVoiceGeneration(
const canGenerateAudio = computed(() => {
const hasText = ttsText.value.trim()
const hasVoice = selectedVoiceMeta.value
- const hasVideo = identifyState.identified
+ const hasVideo = true // 语音生成不依赖视频状态
return !!(hasText && hasVoice && hasVideo && !audioState.value.generating)
})
/**
- * 建议的最大字符数
+ * 建议的最大字符数(需要从外部传入)
*/
const suggestedMaxChars = computed(() => {
- const durationSec = faceDuration / 1000
- const adjustedRate = speechRate.value || 1.0
- return Math.floor(durationSec * 3.5 * adjustedRate)
+ // 默认为 4000,需要从外部设置
+ return 4000
})
// ==================== 核心方法 ====================
@@ -156,31 +151,33 @@ export function useVoiceGeneration(
}
/**
- * 验证音频与人脸区间的重合时长
+ * 验证音频与人脸区间的重合时长(外部调用时传入校验参数)
*/
- const validateAudioDuration = (): boolean => {
- if (!identifyState.identified || faceDuration <= 0) {
+ const validateAudioDuration = (
+ faceStartTime: number = 0,
+ faceEndTime: number = 0,
+ minOverlapMs: number = 2000
+ ): boolean => {
+ if (faceStartTime <= 0 || faceEndTime <= 0) {
audioState.value.validationPassed = false
return false
}
- const faceStart = identifyState.faceStartTime
- const faceEnd = identifyState.faceEndTime
- const faceDurationMs = faceEnd - faceStart
+ const faceDurationMs = faceEndTime - faceStartTime
const audioDuration = audioState.value.durationMs
- const overlapStart = faceStart
- const overlapEnd = Math.min(faceEnd, faceStart + audioDuration)
+ const overlapStart = faceStartTime
+ const overlapEnd = Math.min(faceEndTime, faceStartTime + audioDuration)
const overlapDuration = Math.max(0, overlapEnd - overlapStart)
- const isValid = overlapDuration >= 2000
+ const isValid = overlapDuration >= minOverlapMs
audioState.value.validationPassed = isValid
if (!isValid) {
const overlapSec = (overlapDuration / 1000).toFixed(1)
message.warning(
- `音频时长(${(audioDuration/1000).toFixed(1)}秒)与人脸区间(${(faceDurationMs/1000).toFixed(1)}秒)不匹配,重合部分仅${overlapSec}秒,至少需要2秒`
+ `音频时长(${(audioDuration/1000).toFixed(1)}秒)与人脸区间(${(faceDurationMs/1000).toFixed(1)}秒)不匹配,重合部分仅${overlapSec}秒,至少需要${(minOverlapMs/1000)}秒`
)
} else {
message.success('时长校验通过!')
diff --git a/frontend/app/web-gold/src/views/kling/types/identify-face.ts b/frontend/app/web-gold/src/views/kling/types/identify-face.ts
index 3e47983105..e1f24015f3 100644
--- a/frontend/app/web-gold/src/views/kling/types/identify-face.ts
+++ b/frontend/app/web-gold/src/views/kling/types/identify-face.ts
@@ -109,34 +109,70 @@ export interface UseDigitalHumanGeneration {
// 响应式状态
videoState: import('vue').Ref
identifyState: import('vue').Ref
- materialValidation: import('vue').Ref
// 计算属性
faceDuration: import('vue').ComputedRef
- canGenerate: import('vue').ComputedRef
// 方法
handleFileUpload: (file: File) => Promise
handleVideoSelect: (video: Video) => void
performFaceRecognition: () => Promise
- validateMaterialDuration: (videoMs: number, audioMs: number) => boolean
resetVideoState: () => void
+ resetIdentifyState: () => void
getVideoPreviewUrl: (video: Video) => string
}
/**
* useIdentifyFaceController Hook 返回接口
+ * 扁平化结构,直接暴露所有响应式变量和方法
*/
export interface UseIdentifyFaceController {
- // 组合子 Hooks
- voiceGeneration: UseVoiceGeneration
- digitalHuman: UseDigitalHumanGeneration
+ // ==================== 语音生成相关 ====================
+ ttsText: import('vue').Ref
+ speechRate: import('vue').Ref
+ selectedVoiceMeta: import('vue').Ref
+ audioState: import('vue').Ref
+ canGenerateAudio: import('vue').ComputedRef
+ suggestedMaxChars: import('vue').ComputedRef
+ generateAudio: () => Promise
+ resetAudioState: () => void
- // 业务流程方法
+ // ==================== 数字人生成相关 ====================
+ videoState: import('vue').Ref
+ identifyState: import('vue').Ref
+ materialValidation: import('vue').Ref
+ faceDuration: import('vue').ComputedRef
+ performFaceRecognition: () => Promise
+ handleFileUpload: (file: File) => Promise
+ getVideoPreviewUrl: (video: Video) => string
+ resetVideoState: () => void
+ resetIdentifyState: () => void
+ resetMaterialValidation: () => void
+ validateMaterialDuration: (videoDurationMs: number, audioDurationMs: number) => boolean
+
+ // ==================== 业务流程方法 ====================
generateDigitalHuman: () => Promise
replaceVideo: () => void
- // UI 辅助方法
+ // ==================== 事件处理方法 ====================
+ handleVoiceSelect: (voice: VoiceMeta) => void
+ handleFileSelect: (event: Event) => void
+ handleDrop: (event: DragEvent) => void
+ triggerFileSelect: () => void
+ handleSelectUpload: () => void
+ handleSelectFromLibrary: () => void
+ handleVideoSelect: (video: Video) => void
+ handleSimplifyScript: () => void
+ handleVideoLoaded: (videoUrl: string) => void
+
+ // ==================== 计算属性 ====================
+ canGenerate: import('vue').ComputedRef
+ maxTextLength: import('vue').ComputedRef
+ textareaPlaceholder: import('vue').ComputedRef
+ speechRateMarks: Record
+ speechRateDisplay: import('vue').ComputedRef
+
+ // ==================== UI 辅助方法 ====================
formatDuration: (seconds: number) => string
formatFileSize: (bytes: number) => string
}
diff --git a/frontend/app/web-gold/src/views/system/StyleSettings.vue b/frontend/app/web-gold/src/views/system/StyleSettings.vue
index 14b9555c43..a3479f87a6 100644
--- a/frontend/app/web-gold/src/views/system/StyleSettings.vue
+++ b/frontend/app/web-gold/src/views/system/StyleSettings.vue
@@ -75,7 +75,7 @@ const columns = [
customRender: ({ text }) => {
return h('span', {
style: {
- color: text === 1 ? '#52c41a' : '#ff4d4f',
+ color: text === 1 ? 'var(--color-green-500)' : 'var(--color-red-500)',
},
}, text === 1 ? '启用' : '禁用')
},
@@ -498,14 +498,14 @@ onMounted(() => {
:deep(.action-btn-edit:hover),
:deep(.action-btn-edit:hover .anticon) {
- background: rgba(24, 144, 255, 0.1) !important;
- color: #1890FF !important;
+ background: rgba(59, 130, 246, 0.1) !important;
+ color: var(--color-primary) !important;
}
:deep(.action-btn-delete:hover),
:deep(.action-btn-delete:hover .anticon) {
- background: rgba(24, 144, 255, 0.1) !important;
- color: #1890FF !important;
+ background: rgba(59, 130, 246, 0.1) !important;
+ color: var(--color-primary) !important;
}
:deep(.action-btn:hover) {
diff --git a/frontend/app/web-gold/src/views/trends/Forecast.vue b/frontend/app/web-gold/src/views/trends/Forecast.vue
index 0bc80b767e..9709958c9b 100644
--- a/frontend/app/web-gold/src/views/trends/Forecast.vue
+++ b/frontend/app/web-gold/src/views/trends/Forecast.vue
@@ -651,12 +651,12 @@ onMounted(async () => {
-
更多 ({{ allPrompts.length }})
@@ -679,7 +679,7 @@ onMounted(async () => {
-
@@ -826,15 +826,15 @@ onMounted(async () => {
.param-label {
display: block;
- font-size: 12px;
+ font-size: 14px;
color: var(--color-text-secondary);
- margin-bottom: 4px;
+ margin-bottom: var(--space-1);
}
.param-select {
width: 100%;
padding: 6px 8px;
- font-size: 13px;
+ font-size: 14px;
color: var(--color-text);
background: var(--color-bg);
border: 1px solid var(--color-border);
@@ -1020,13 +1020,13 @@ onMounted(async () => {
.topic-title--clickable {
cursor: pointer;
- color: #1890ff;
+ color: var(--color-primary);
transition: all 0.2s;
}
.topic-title--clickable:hover {
text-decoration: underline;
- color: #40a9ff;
+ color: var(--color-primary-hover);
opacity: 0.8;
}
@@ -1159,7 +1159,7 @@ onMounted(async () => {
border-radius: 16px;
cursor: pointer;
transition: all 0.2s ease;
- font-size: 13px;
+ font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
@@ -1277,18 +1277,18 @@ onMounted(async () => {
padding: 12px 24px;
font-size: 14px;
font-weight: 600;
- color: #1890ff;
+ color: var(--color-primary);
background: transparent;
- border: 1px solid #1890ff;
+ border: 1px solid var(--color-primary);
border-radius: 0;
cursor: pointer;
overflow: hidden;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
- box-shadow:
- 0 0 10px rgba(24, 144, 255, 0.3),
- inset 0 0 10px rgba(24, 144, 255, 0.1);
+ box-shadow:
+ 0 0 10px rgba(59, 130, 246, 0.3),
+ inset 0 0 10px rgba(59, 130, 246, 0.1);
}
.cyber-button::before {
@@ -1298,7 +1298,7 @@ onMounted(async () => {
left: -100%;
width: 100%;
height: 100%;
- background: linear-gradient(90deg, transparent, rgba(24, 144, 255, 0.2), transparent);
+ background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.2), transparent);
transition: left 0.5s ease;
}
@@ -1307,19 +1307,19 @@ onMounted(async () => {
}
.cyber-button:hover:not(.cyber-button--disabled) {
- background: rgba(24, 144, 255, 0.1);
- box-shadow:
- 0 0 20px rgba(24, 144, 255, 0.5),
- 0 0 40px rgba(24, 144, 255, 0.3),
- inset 0 0 20px rgba(24, 144, 255, 0.2);
+ background: rgba(59, 130, 246, 0.1);
+ box-shadow:
+ 0 0 20px rgba(59, 130, 246, 0.5),
+ 0 0 40px rgba(59, 130, 246, 0.3),
+ inset 0 0 20px rgba(59, 130, 246, 0.2);
transform: translateY(-1px);
}
.cyber-button:active:not(.cyber-button--disabled) {
transform: translateY(0);
- box-shadow:
- 0 0 15px rgba(24, 144, 255, 0.4),
- inset 0 0 15px rgba(24, 144, 255, 0.15);
+ box-shadow:
+ 0 0 15px rgba(59, 130, 246, 0.4),
+ inset 0 0 15px rgba(59, 130, 246, 0.15);
}
.cyber-button__content {
@@ -1338,7 +1338,7 @@ onMounted(async () => {
width: 0;
height: 0;
border-radius: 50%;
- background: rgba(24, 144, 255, 0.4);
+ background: rgba(59, 130, 246, 0.4);
transform: translate(-50%, -50%);
transition: width 0.6s ease, height 0.6s ease;
pointer-events: none;
@@ -1352,7 +1352,7 @@ onMounted(async () => {
.cyber-button__text {
position: relative;
z-index: 2;
- text-shadow: 0 0 10px rgba(24, 144, 255, 0.8);
+ text-shadow: 0 0 10px rgba(59, 130, 246, 0.8);
}
.cyber-button__arrow {
@@ -1378,8 +1378,8 @@ onMounted(async () => {
.cyber-button__spinner {
width: 14px;
height: 14px;
- border: 2px solid rgba(24, 144, 255, 0.3);
- border-top-color: #1890ff;
+ border: 2px solid rgba(59, 130, 246, 0.3);
+ border-top-color: var(--color-primary);
border-radius: 50%;
animation: cyber-spin 0.8s linear infinite;
}
@@ -1394,9 +1394,9 @@ onMounted(async () => {
.cyber-button:disabled {
opacity: 0.4;
cursor: not-allowed;
- border-color: rgba(24, 144, 255, 0.3);
+ border-color: rgba(59, 130, 246, 0.3);
box-shadow: none;
- color: rgba(24, 144, 255, 0.5);
+ color: rgba(59, 130, 246, 0.5);
}
.cyber-button--disabled:hover,
@@ -1419,7 +1419,7 @@ onMounted(async () => {
display: flex;
align-items: center;
gap: 6px;
- font-size: 12px;
+ font-size: 14px;
color: var(--color-primary);
}
diff --git a/frontend/app/web-gold/tsconfig.json b/frontend/app/web-gold/tsconfig.json
index b9bf5edf5b..91098929a5 100644
--- a/frontend/app/web-gold/tsconfig.json
+++ b/frontend/app/web-gold/tsconfig.json
@@ -13,7 +13,7 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
-
+ "allowJs": true, // 必须为 tru
/* Linting */
"strict": true,
"noUnusedLocals": true,
diff --git a/frontend/integration-test.js b/frontend/integration-test.js
deleted file mode 100644
index 3940a4dbc5..0000000000
--- a/frontend/integration-test.js
+++ /dev/null
@@ -1,277 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * SMS 登录集成测试脚本
- * 模拟真实的使用场景,验证整个流程
- */
-
-const fs = require('fs')
-const path = require('path')
-
-// 模拟 localStorage
-global.localStorage = {
- data: {},
- getItem(key) {
- return this.data[key] || null
- },
- setItem(key, value) {
- this.data[key] = value
- },
- removeItem(key) {
- delete this.data[key]
- }
-}
-
-// 验证 token-manager.js 文件存在
-const tokenManagerPath = path.join(__dirname, 'utils', 'token-manager.js')
-if (!fs.existsSync(tokenManagerPath)) {
- console.error('❌ token-manager.js 不存在')
- process.exit(1)
-}
-console.log('✅ token-manager.js 文件存在,开始集成测试...\n')
-
-// 手动创建 TokenManager 实例(从 token-manager.js 复制核心逻辑)
-class TokenManager {
- constructor() {
- this.subscribers = []
- }
-
- parseLocalDateTime(dateTimeStr) {
- if (!dateTimeStr) return 0
-
- const normalizedStr = dateTimeStr.includes(' ')
- ? dateTimeStr.replace(' ', 'T')
- : dateTimeStr
-
- const dayjs = require('./app/web-gold/node_modules/dayjs')
- const parsedTime = dayjs(normalizedStr)
-
- if (!parsedTime.isValid()) {
- console.warn('[TokenManager] 无法解析过期时间:', dateTimeStr)
- return 0
- }
-
- return parsedTime.valueOf()
- }
-
- getAccessToken() {
- return localStorage.getItem('access_token')
- }
-
- getExpiresTime() {
- const expiresTimeStr = localStorage.getItem('expires_time')
- return expiresTimeStr ? parseInt(expiresTimeStr, 10) : 0
- }
-
- setTokens(tokenInfo) {
- const {
- accessToken,
- refreshToken,
- expiresIn,
- expiresTime,
- tokenType = 'Bearer'
- } = tokenInfo
-
- if (!accessToken) {
- console.error('[TokenManager] 设置令牌失败:缺少 accessToken')
- return
- }
-
- localStorage.setItem('access_token', accessToken)
-
- if (refreshToken) {
- localStorage.setItem('refresh_token', refreshToken)
- }
-
- let expiresTimeMs = 0
- if (expiresTime) {
- if (typeof expiresTime === 'string' && expiresTime.includes('T')) {
- expiresTimeMs = this.parseLocalDateTime(expiresTime)
- } else if (typeof expiresTime === 'number') {
- expiresTimeMs = expiresTime > 10000000000 ? expiresTime : expiresTime * 1000
- } else if (expiresIn) {
- expiresTimeMs = Date.now() + (expiresIn * 1000)
- }
-
- if (expiresTimeMs) {
- localStorage.setItem('expires_time', String(expiresTimeMs))
- }
- }
-
- localStorage.setItem('token_type', tokenType)
- }
-
- isExpired(bufferTime = 5 * 60 * 1000) {
- const expiresTime = this.getExpiresTime()
- const now = Date.now()
- return !expiresTime || now >= (expiresTime - bufferTime)
- }
-
- isLoggedIn() {
- const token = this.getAccessToken()
- return Boolean(token) && !this.isExpired()
- }
-}
-
-const tokenManager = new TokenManager()
-
-console.log('🧪 SMS 登录集成测试\n')
-console.log('='.repeat(60))
-
-// 测试场景 1: SMS 登录返回 LocalDateTime 格式
-console.log('\n📱 场景 1: SMS 登录返回 LocalDateTime 格式')
-console.log('-'.repeat(60))
-
-const smsLoginResponse = {
- code: 0,
- data: {
- accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
- refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ',
- expiresTime: '2025-12-27T10:27:42', // LocalDateTime 格式
- tokenType: 'Bearer'
- }
-}
-
-console.log('登录响应:', JSON.stringify(smsLoginResponse.data, null, 2))
-
-// 保存令牌
-tokenManager.setTokens(smsLoginResponse.data)
-
-// 验证存储
-const storedToken = tokenManager.getAccessToken()
-const storedExpiresTime = tokenManager.getExpiresTime()
-
-console.log('\n✅ 验证结果:')
-console.log(` accessToken: ${storedToken ? '✓ 已存储' : '✗ 未存储'}`)
-console.log(` expiresTime: ${storedExpiresTime ? storedExpiresTime : '✗ 未存储'}`)
-
-if (storedToken === smsLoginResponse.data.accessToken) {
- console.log(' ✅ 令牌存储正确')
-} else {
- console.log(' ❌ 令牌存储错误')
- process.exit(1)
-}
-
-// 测试场景 2: 带空格的 LocalDateTime 格式
-console.log('\n📅 场景 2: 带空格的 LocalDateTime 格式')
-console.log('-'.repeat(60))
-
-const responseWithSpace = {
- accessToken: 'token_2',
- refreshToken: 'refresh_2',
- expiresTime: '2025-12-27 10:27:42', // 带空格格式
- tokenType: 'Bearer'
-}
-
-console.log('expiresTime 格式:', responseWithSpace.expiresTime)
-
-tokenManager.setTokens(responseWithSpace)
-const expiresTime2 = tokenManager.getExpiresTime()
-
-console.log('\n✅ 验证结果:')
-console.log(` expiresTime: ${expiresTime2}`)
-console.log(` ✅ 格式解析正确`)
-
-// 测试场景 3: 数字格式(毫秒)
-console.log('\n🔢 场景 3: 数字格式(毫秒)')
-console.log('-'.repeat(60))
-
-const responseWithMs = {
- accessToken: 'token_3',
- expiresTime: 1766841662689 // 毫秒格式
-}
-
-console.log('expiresTime:', responseWithMs.expiresTime)
-
-tokenManager.setTokens(responseWithMs)
-const expiresTime3 = tokenManager.getExpiresTime()
-
-console.log('\n✅ 验证结果:')
-console.log(` expiresTime: ${expiresTime3}`)
-console.log(` ${expiresTime3 === 1766841662689 ? '✅ 格式正确' : '❌ 格式错误'}`)
-
-// 测试场景 4: 数字格式(秒)
-console.log('\n⏱️ 场景 4: 数字格式(秒)')
-console.log('-'.repeat(60))
-
-const responseWithSec = {
- accessToken: 'token_4',
- expiresTime: 1766841662 // 秒格式
-}
-
-console.log('expiresTime:', responseWithSec.expiresTime)
-
-tokenManager.setTokens(responseWithSec)
-const expiresTime4 = tokenManager.getExpiresTime()
-
-console.log('\n✅ 验证结果:')
-console.log(` expiresTime: ${expiresTime4}`)
-console.log(` 期望值: ${1766841662 * 1000}`)
-console.log(` ${expiresTime4 === 1766841662 * 1000 ? '✅ 自动转换为毫秒' : '❌ 转换失败'}`)
-
-// 测试场景 5: 过期时间检查
-console.log('\n⏳ 场景 5: 过期时间检查')
-console.log('-'.repeat(60))
-
-// 设置一个已过期的令牌
-tokenManager.setTokens({
- accessToken: 'expired_token',
- expiresTime: Date.now() - 10000 // 10秒前过期
-})
-
-const isLoggedIn = tokenManager.isLoggedIn()
-const isExpired = tokenManager.isExpired()
-
-console.log('\n✅ 验证结果:')
-console.log(` isLoggedIn(): ${isLoggedIn ? '✓ 已登录' : '✗ 未登录'}`)
-console.log(` isExpired(): ${isExpired ? '✓ 已过期' : '✗ 未过期'}`)
-console.log(` ✅ 过期检查正确`)
-
-// 测试场景 6: 即将过期的令牌
-console.log('\n⚠️ 场景 6: 即将过期的令牌(30秒缓冲)')
-console.log('-'.repeat(60))
-
-tokenManager.setTokens({
- accessToken: 'expiring_token',
- expiresTime: Date.now() + 20000 // 20秒后过期(少于30秒缓冲)
-})
-
-const isExpiring = tokenManager.isExpired(30 * 1000) // 30秒缓冲
-
-console.log('\n✅ 验证结果:')
-console.log(` 当前时间: ${Date.now()}`)
-console.log(` 过期时间: ${tokenManager.getExpiresTime()}`)
-console.log(` 剩余时间: ${(tokenManager.getExpiresTime() - Date.now()) / 1000} 秒`)
-console.log(` isExpired(30s): ${isExpiring ? '✓ 即将过期' : '✗ 未过期'}`)
-console.log(` ✅ 预检查逻辑正确`)
-
-// 测试场景 7: 有效令牌
-console.log('\n✅ 场景 7: 有效令牌')
-console.log('-'.repeat(60))
-
-tokenManager.setTokens({
- accessToken: 'valid_token',
- expiresTime: Date.now() + 3600000 // 1小时后过期
-})
-
-const isValid = tokenManager.isLoggedIn()
-const isNotExpired = tokenManager.isExpired(30 * 1000)
-
-console.log('\n✅ 验证结果:')
-console.log(` isLoggedIn(): ${isValid ? '✓ 已登录' : '✗ 未登录'}`)
-console.log(` isExpired(30s): ${isNotExpired ? '✓ 已过期' : '✗ 未过期'}`)
-console.log(` ✅ 有效令牌识别正确`)
-
-// 总结
-console.log('\n' + '='.repeat(60))
-console.log('🎉 所有集成测试通过!')
-console.log('='.repeat(60))
-console.log('\n📊 测试统计:')
-console.log(' ✅ LocalDateTime 格式解析')
-console.log(' ✅ 带空格格式解析')
-console.log(' ✅ 毫秒格式处理')
-console.log(' ✅ 秒格式自动转换')
-console.log(' ✅ 过期时间检查')
-console.log(' ✅ 预检查逻辑')
-console.log(' ✅ 有效令牌识别')
-console.log('\n💡 系统已准备好处理 SMS 登录的各种 expiresTime 格式!')