feat: 功能

This commit is contained in:
2025-12-28 13:49:45 +08:00
parent 36195ea55a
commit bbf8093ca3
25 changed files with 1046 additions and 1970 deletions

View File

@@ -6,67 +6,20 @@ import SvgSprite from '@/components/icons/SvgSprite.vue'
import { useUserStore } from '@/stores/user'
import tokenManager from '@gold/utils/token-manager'
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,
colorInfo: '#2563EB',
colorBgBase: '#F8FAFC',
colorBgContainer: '#FFFFFF',
colorTextBase: '#334155',
colorTextSecondary: '#64748B',
colorBorder: '#E2E8F0',
borderRadius: 8,
}
})
onMounted(async () => {
// 运行时从 :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 }
// 检查登录状态如果有token但store中未标记为登录则恢复登录状态
const userStore = useUserStore()
// 等待store从本地存储恢复完成最多等待500ms
let waitCount = 0
while (!userStore.isHydrated && waitCount < 50) {
await new Promise(resolve => setTimeout(resolve, 10))
waitCount++
}
const token = tokenManager.getAccessToken()
if (token) {
// 如果有token但未登录可能是刷新页面需要恢复登录状态
if (!userStore.isLoggedIn) {
userStore.isLoggedIn = true
// 尝试获取用户信息
try {
await userStore.fetchUserInfo()
} catch (error) {
console.error('初始化用户信息失败:', error)
}
} else if (!userStore.nickname && !userStore.userId) {
// 如果已登录但没有用户信息,尝试获取
try {
await userStore.fetchUserInfo()
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
}
})
onMounted(async () => {})
</script>
<template>

View File

@@ -123,22 +123,7 @@ onUnmounted(() => {
<style scoped>
.prompt-display {
line-height: 1.6;
}
/* 代码块优化 */
.prompt-display :deep(pre) {
max-width: 100%;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
background: #282c34;
color: #abb2bf;
padding: 1em;
border-radius: 8px;
margin: 1em 0;
}
.prompt-display :deep(code) {
font-family: 'Fira Code', Menlo, Monaco, Consolas, 'Courier New', monospace;
color: var(--color-text);
font-size: 14px;
}
</style>

View File

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

View File

@@ -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);
}
/* 响应式设计 */

View File

@@ -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; }
</style>

View File

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

View File

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

View File

@@ -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);
}

View File

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

View File

@@ -1,15 +0,0 @@
<script setup>
</script>
<template>
<div class="space-y-4">
<h2 class="text-xl font-bold">剪映导入</h2>
<div class="bg-white p-4 rounded shadow">选择文案/字幕/音频/数字人视频/工程一键导入</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -544,7 +544,7 @@ defineOptions({ name: 'ContentStyleCopywriting' })
<!-- 空状态 -->
<div v-else-if="!loadingPrompts" class="prompt-empty">
<div style="color: var(--color-text-secondary); font-size: 12px; text-align: center; padding: 20px;">
<div style="color: var(--color-text-secondary); font-size: 14px; text-align: center; padding: 20px;">
您可以在视频分析页面保存风格
</div>
</div>
@@ -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);
}
/* 提示词标签样式 */

View File

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

View File

@@ -103,10 +103,10 @@ function handleCreateContent() {
<style scoped>
.expanded-content {
padding: 16px;
background: #161616;
border-radius: 6px;
margin: 8px 0;
padding: var(--space-4);
background: var(--color-slate-50);
border-radius: var(--radius-card);
margin: var(--space-2) 0;
}
.two-col {
@@ -126,21 +126,21 @@ function handleCreateContent() {
white-space: pre-wrap;
line-height: 1.6;
color: var(--color-text-secondary);
background: #0d0d0d;
background: var(--color-surface);
border: 1px dashed var(--color-border);
border-radius: 6px;
padding: 10px;
border-radius: var(--radius-card);
padding: var(--space-2);
}
.sub-title {
font-size: 14px;
font-size: 16px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 8px;
margin-bottom: var(--space-2);
}
.no-transcript {
font-size: 12px;
font-size: 14px;
color: var(--color-text-secondary);
}
@@ -148,27 +148,27 @@ function handleCreateContent() {
min-height: 200px;
max-height: 500px;
overflow-y: auto;
background: #0d0d0d;
background: var(--color-surface);
border: 1px dashed var(--color-border);
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
border-radius: var(--radius-card);
padding: var(--space-3);
margin-bottom: var(--space-2);
}
.no-prompt {
padding: 16px;
padding: var(--space-4);
text-align: center;
color: var(--color-text-secondary);
}
.right-actions {
margin-top: 8px;
margin-top: var(--space-2);
}
.copy-btn {
display: inline-flex;
align-items: center;
gap: 4px;
gap: var(--space-1);
color: var(--color-primary);
cursor: pointer;
}
@@ -179,7 +179,7 @@ function handleCreateContent() {
.no-analysis-tip {
padding: 40px 20px;
padding: var(--space-8) var(--space-5);
text-align: center;
}
@@ -189,7 +189,7 @@ function handleCreateContent() {
.no-analysis-tip :deep(.ant-empty-description) {
color: var(--color-text-secondary);
margin-bottom: 16px;
margin-bottom: var(--space-4);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -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<VideoState>({
@@ -43,13 +39,6 @@ export function useDigitalHumanGeneration(
videoFileId: null,
})
const materialValidation = ref<MaterialValidation>({
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,
}
}

View File

@@ -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<MaterialValidation>({
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,

View File

@@ -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<string>('')
@@ -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('时长校验通过!')

View File

@@ -109,34 +109,70 @@ export interface UseDigitalHumanGeneration {
// 响应式状态
videoState: import('vue').Ref<VideoState>
identifyState: import('vue').Ref<IdentifyState>
materialValidation: import('vue').Ref<MaterialValidation>
// 计算属性
faceDuration: import('vue').ComputedRef<number>
canGenerate: import('vue').ComputedRef<boolean>
// 方法
handleFileUpload: (file: File) => Promise<void>
handleVideoSelect: (video: Video) => void
performFaceRecognition: () => Promise<void>
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<string>
speechRate: import('vue').Ref<number>
selectedVoiceMeta: import('vue').Ref<VoiceMeta | null>
audioState: import('vue').Ref<AudioState>
canGenerateAudio: import('vue').ComputedRef<boolean>
suggestedMaxChars: import('vue').ComputedRef<number>
generateAudio: () => Promise<void>
resetAudioState: () => void
// 业务流程方法
// ==================== 数字人生成相关 ====================
videoState: import('vue').Ref<VideoState>
identifyState: import('vue').Ref<IdentifyState>
materialValidation: import('vue').Ref<MaterialValidation>
faceDuration: import('vue').ComputedRef<number>
performFaceRecognition: () => Promise<void>
handleFileUpload: (file: File) => Promise<void>
getVideoPreviewUrl: (video: Video) => string
resetVideoState: () => void
resetIdentifyState: () => void
resetMaterialValidation: () => void
validateMaterialDuration: (videoDurationMs: number, audioDurationMs: number) => boolean
// ==================== 业务流程方法 ====================
generateDigitalHuman: () => Promise<void>
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<boolean>
maxTextLength: import('vue').ComputedRef<number>
textareaPlaceholder: import('vue').ComputedRef<string>
speechRateMarks: Record<number, string>
speechRateDisplay: import('vue').ComputedRef<string>
// ==================== UI 辅助方法 ====================
formatDuration: (seconds: number) => string
formatFileSize: (bytes: number) => string
}

View File

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

View File

@@ -651,12 +651,12 @@ onMounted(async () => {
<div>
<div class="form-label-wrapper">
<label class="form-label">风格提示词</label>
<a-button
<a-button
v-if="allPrompts.length > DISPLAY_COUNT"
size="small"
type="link"
size="small"
type="link"
@click="showAllPromptsModal = true"
style="padding: 0; height: auto; font-size: 12px;"
style="padding: 0; height: auto; font-size: 14px;"
>
更多 ({{ allPrompts.length }})
</a-button>
@@ -679,7 +679,7 @@ onMounted(async () => {
<!-- 空状态 -->
<div v-else-if="!loadingPrompts" class="prompt-empty">
<div style="color: var(--color-text-secondary); font-size: 12px; text-align: center; padding: 20px;">
<div style="color: var(--color-text-secondary); font-size: 14px; text-align: center; padding: 20px;">
您可以在视频分析页面保存风格
</div>
</div>
@@ -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);
}

View File

@@ -13,7 +13,7 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"allowJs": true, // 必须为 tru
/* Linting */
"strict": true,
"noUnusedLocals": true,