From b80de78d7c6f48c0015d4986f61f4e1a0a968292 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Sun, 21 Dec 2025 22:24:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 13 +- frontend/api/axios/client.js | 93 +- frontend/app/web-gold/src/api/auth.js | 29 + frontend/app/web-gold/src/api/digitalHuman.js | 10 + frontend/app/web-gold/src/api/http.js | 1 + .../web-gold/src/components/SidebarNav.vue | 47 +- .../web-gold/src/components/UserDropdown.vue | 15 +- frontend/app/web-gold/src/router/index.js | 10 + frontend/app/web-gold/src/stores/user.js | 12 +- .../src/views/material/MaterialGroup.vue | 2 +- .../app/web-gold/src/views/material/Mix.vue | 24 +- .../task-management/components/.gitkeep | 0 .../components/TaskActionButtons.vue | 169 +++ .../components/TaskFilterBar.vue | 143 +++ .../components/TaskStatusTag.vue | 96 ++ .../task-management/composables/.gitkeep | 0 .../composables/useTaskList.js | 147 +++ .../composables/useTaskOperations.js | 203 ++++ .../composables/useTaskPolling.js | 171 +++ .../digital-human-task/.gitkeep | 0 .../digital-human-task/index.vue | 579 ++++++++++ .../system/task-management/layout/.gitkeep | 0 .../task-management/layout/TaskLayout.vue | 335 ++++++ .../system/task-management/mix-task/.gitkeep | 0 .../system/task-management/mix-task/index.vue | 482 ++++++++ frontend/hooks/web/useUserInfo.js | 83 +- .../add-auto-refresh-token/proposal.md | 40 + .../add-auto-refresh-token/specs/auth/spec.md | 74 ++ .../changes/add-auto-refresh-token/tasks.md | 33 + .../refactor-task-management/proposal.md | 127 ++ .../specs/digital-human-task/spec.md | 344 ++++++ .../specs/mix-task/spec.md | 115 ++ .../specs/task-management/spec.md | 179 +++ .../changes/refactor-task-management/tasks.md | 189 +++ openspec/mix-logic-spec.md | 1026 ----------------- openspec/professional-simple-mix-spec.md | 135 --- 36 files changed, 3721 insertions(+), 1205 deletions(-) create mode 100644 frontend/app/web-gold/src/views/system/task-management/components/.gitkeep create mode 100644 frontend/app/web-gold/src/views/system/task-management/components/TaskActionButtons.vue create mode 100644 frontend/app/web-gold/src/views/system/task-management/components/TaskFilterBar.vue create mode 100644 frontend/app/web-gold/src/views/system/task-management/components/TaskStatusTag.vue create mode 100644 frontend/app/web-gold/src/views/system/task-management/composables/.gitkeep create mode 100644 frontend/app/web-gold/src/views/system/task-management/composables/useTaskList.js create mode 100644 frontend/app/web-gold/src/views/system/task-management/composables/useTaskOperations.js create mode 100644 frontend/app/web-gold/src/views/system/task-management/composables/useTaskPolling.js create mode 100644 frontend/app/web-gold/src/views/system/task-management/digital-human-task/.gitkeep create mode 100644 frontend/app/web-gold/src/views/system/task-management/digital-human-task/index.vue create mode 100644 frontend/app/web-gold/src/views/system/task-management/layout/.gitkeep create mode 100644 frontend/app/web-gold/src/views/system/task-management/layout/TaskLayout.vue create mode 100644 frontend/app/web-gold/src/views/system/task-management/mix-task/.gitkeep create mode 100644 frontend/app/web-gold/src/views/system/task-management/mix-task/index.vue create mode 100644 openspec/changes/add-auto-refresh-token/proposal.md create mode 100644 openspec/changes/add-auto-refresh-token/specs/auth/spec.md create mode 100644 openspec/changes/add-auto-refresh-token/tasks.md create mode 100644 openspec/changes/refactor-task-management/proposal.md create mode 100644 openspec/changes/refactor-task-management/specs/digital-human-task/spec.md create mode 100644 openspec/changes/refactor-task-management/specs/mix-task/spec.md create mode 100644 openspec/changes/refactor-task-management/specs/task-management/spec.md create mode 100644 openspec/changes/refactor-task-management/tasks.md delete mode 100644 openspec/mix-logic-spec.md delete mode 100644 openspec/professional-simple-mix-spec.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3e8a6bec5f..aec80adaf0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -18,7 +18,18 @@ "Bash(mysql:*)", "Bash(npm run lint:*)", "Bash(npx vue-tsc:*)", - "Bash(pnpm add:*)" + "Bash(pnpm add:*)", + "Bash(./mvnw compile:*)", + "Bash(openspec list:*)", + "Bash(openspec validate:*)", + "Bash(../mvnw:*)", + "Bash(openspec change show:*)", + "Bash(openspec proposal:*)", + "Bash(openspec --help)", + "Bash(openspec:*)", + "Bash(node -c /d/projects/sionrui/frontend/app/web-gold/src/views/system/task-management/digital-human-task/index.vue)", + "Bash(echo \"=== Token 自动刷新功能验证 ===\n\n✅ 已实现的功能:\n1. 请求前检查 token 是否即将过期(5分钟缓冲)\n2. 如果即将过期,自动触发 refreshToken 刷新\n3. 并发请求时,只有一个请求触发刷新,其他请求等待\n4. 刷新完成后,所有等待的请求使用新 token\n5. 白名单接口(login、refresh-token等)跳过检查\n6. 401 错误:尝试刷新,失败则跳转登录页\n7. 403 错误:直接跳转登录页\n\n✅ 核心文件修改:\n- frontend/api/axios/client.js - 添加了预检查和刷新逻辑\n- frontend/app/web-gold/src/api/http.js - 保持原有的 401/403 处理\n\n✅ 兼容性:\n- 向后兼容:不影响现有认证流程\n- API 兼容:不改变后端接口契约\n- 用户透明:完全无感知的自动刷新\n\n=== 验证完成 ===\")", + "Bash(node:*)" ], "deny": [], "ask": [] diff --git a/frontend/api/axios/client.js b/frontend/api/axios/client.js index d58711c527..cf80c76bb1 100644 --- a/frontend/api/axios/client.js +++ b/frontend/api/axios/client.js @@ -28,6 +28,32 @@ function isInWhiteList(url) { return WHITE_LIST.some((path) => url.includes(path)) } +/** + * 自动刷新 token 的锁机制和队列 + */ +let isRefreshing = false +let refreshSubscribers = [] + +/** + * 订阅 token 刷新完成 + * @param {Function} callback - 回调函数 + */ +function subscribeTokenRefresh(callback) { + if (isRefreshing) { + refreshSubscribers.push(callback) + } else { + callback() + } +} + +/** + * 执行所有订阅回调 + */ +function onRefreshed() { + refreshSubscribers.forEach(callback => callback()) + refreshSubscribers = [] +} + /** * 处理 401 未授权错误 * 注意:只做清理工作,不处理重定向(重定向由上层回调处理) @@ -59,6 +85,7 @@ function handle401Error(error) { * @param {number} options.timeout - 超时时间(毫秒) * @param {Function} options.on401 - 401 错误处理函数 * @param {Function} options.on403 - 403 错误处理函数 + * @param {Function} options.refreshTokenFn - Token 刷新函数(可选) * @returns {AxiosInstance} Axios 实例 */ export function createClientAxios(options = {}) { @@ -67,6 +94,7 @@ export function createClientAxios(options = {}) { timeout = 180000, on401 = handle401Error, on403 = null, + refreshTokenFn = null, } = options const client = axios.create({ @@ -77,21 +105,74 @@ export function createClientAxios(options = {}) { // 请求拦截器 client.interceptors.request.use((config) => { // 添加 tenant-id - const tenantId = + const tenantId = (typeof import.meta !== 'undefined' && import.meta.env?.VITE_TENANT_ID) || (typeof process !== 'undefined' && process.env?.VITE_TENANT_ID) || '1' - + if (tenantId) { config.headers['tenant-id'] = tenantId } - // 添加 Authorization header + // 检查是否需要认证 const needToken = config.headers?.isToken !== false && !isInWhiteList(config.url || '') + if (needToken) { - const authHeader = tokenManager.getAuthHeader() - if (authHeader) { - config.headers.Authorization = authHeader + // 检查 token 是否即将过期(30秒缓冲) + const BUFFER_TIME = 30 * 1000 + const currentToken = tokenManager.getAccessToken() + + if (!currentToken) { + console.warn('[Token] 没有可用的 accessToken') + return config + } + + const isTokenExpired = tokenManager.isExpired(BUFFER_TIME) + + if (isTokenExpired) { + console.info('[Token] Token刷新') + + // 如果不在刷新过程中,启动刷新 + if (!isRefreshing) { + isRefreshing = true + + // 执行刷新(使用上层传入的刷新函数) + if (refreshTokenFn && typeof refreshTokenFn === 'function') { + refreshTokenFn() + .then(() => { + console.info('[Token] 刷新成功') + isRefreshing = false + onRefreshed() + }) + .catch((error) => { + isRefreshing = false + onRefreshed() + console.error('[Token] 刷新失败:', error.message) + }) + } else { + console.warn('[Token] 未提供刷新函数,跳过刷新') + isRefreshing = false + onRefreshed() + } + } + + // 等待刷新完成 + return new Promise((resolve) => { + subscribeTokenRefresh(() => { + // 刷新完成后,重新获取 token 并添加到请求头 + const authHeader = tokenManager.getAuthHeader() + if (authHeader) { + config.headers.Authorization = authHeader + } + resolve(config) + }) + }) + } else { + // Token 未过期,直接添加 Authorization 头 + const authHeader = tokenManager.getAuthHeader() + if (authHeader) { + config.headers.Authorization = authHeader + } } } diff --git a/frontend/app/web-gold/src/api/auth.js b/frontend/app/web-gold/src/api/auth.js index 59b1410667..0a66883956 100644 --- a/frontend/app/web-gold/src/api/auth.js +++ b/frontend/app/web-gold/src/api/auth.js @@ -16,6 +16,8 @@ function saveTokens(info) { tokenManager.setTokens({ accessToken: info.accessToken || '', refreshToken: info.refreshToken || '', + expiresIn: info.expiresTime || 7200, // expiresTime 是秒数 + tokenType: info.tokenType || 'Bearer' }) } } @@ -63,6 +65,15 @@ export async function loginByPassword(mobile, password) { const { data } = await api.post(`${SERVER_BASE}/auth/login`, { mobile, password }); const info = data || {}; saveTokens(info); + + // 清除用户信息缓存,确保登录后获取最新信息 + try { + const { clearUserInfoCache } = await import('@gold/hooks/web/useUserInfo') + clearUserInfoCache() + } catch (e) { + console.error('清除用户信息缓存失败:', e) + } + return info; } @@ -109,6 +120,15 @@ export async function loginBySms(mobile, code) { const { data } = await api.post(`${SERVER_BASE}/auth/sms-login`, { mobile, code }); const info = data || {}; saveTokens(info); + + // 清除用户信息缓存,确保登录后获取最新信息 + try { + const { clearUserInfoCache } = await import('@gold/hooks/web/useUserInfo') + clearUserInfoCache() + } catch (e) { + console.error('清除用户信息缓存失败:', e) + } + return info; } @@ -125,6 +145,15 @@ export async function refreshToken() { const { data } = await api.post(`${SERVER_BASE}/auth/refresh-token`, null, { params: { refreshToken: rt } }); const info = data || {}; saveTokens(info); + + // 清除用户信息缓存,因为 token 刷新后用户信息可能已更新 + try { + const { clearUserInfoCache } = await import('@gold/hooks/web/useUserInfo') + clearUserInfoCache() + } catch (e) { + console.error('清除用户信息缓存失败:', e) + } + return info; } diff --git a/frontend/app/web-gold/src/api/digitalHuman.js b/frontend/app/web-gold/src/api/digitalHuman.js index 95805e2772..eacf4b0ee7 100644 --- a/frontend/app/web-gold/src/api/digitalHuman.js +++ b/frontend/app/web-gold/src/api/digitalHuman.js @@ -75,3 +75,13 @@ export function deleteTask(taskId) { method: 'delete' }) } + +/** + * 获取任务输出文件的签名URL + */ +export function getSignedUrls(taskId) { + return request({ + url: `/webApi/api/tik/digital-human/task/${taskId}/signed-url`, + method: 'get' + }) +} diff --git a/frontend/app/web-gold/src/api/http.js b/frontend/app/web-gold/src/api/http.js index 1c675c5adf..8960e5d6d2 100644 --- a/frontend/app/web-gold/src/api/http.js +++ b/frontend/app/web-gold/src/api/http.js @@ -22,6 +22,7 @@ export function createHttpClient(options = {}) { const httpClient = createClientAxios({ baseURL: '/', timeout: 180000, + refreshTokenFn: refreshToken, // 传递刷新函数给拦截器 on401: async (error) => { // 401:优先使用上层自定义处理 if (on401) { diff --git a/frontend/app/web-gold/src/components/SidebarNav.vue b/frontend/app/web-gold/src/components/SidebarNav.vue index 3ea24f84f9..e9bf5b8db6 100644 --- a/frontend/app/web-gold/src/components/SidebarNav.vue +++ b/frontend/app/web-gold/src/components/SidebarNav.vue @@ -26,41 +26,40 @@ const items = computed(() => { { title: '功能', children: [ - // { path: '/home', label: '首页', icon: 'home' }, - { path: '/content-style/benchmark', label: '对标分析', icon: 'grid' }, - { path: '/content-style/copywriting', label: '文案创作', icon: 'text' }, - { path: '/trends/forecast', label: '热点趋势', icon: 'text' }, + // { name: '首页', label: '首页', icon: 'home' }, + { name: '对标分析', label: '对标分析', icon: 'grid' }, + { name: '文案创作', label: '文案创作', icon: 'text' }, + { name: '热点预测', label: '热点趋势', icon: 'text' }, ] }, { title: '数字人', children: [ - { path: '/digital-human/voice-copy', label: '人声克隆', icon: 'mic' }, - { path: "/digital-human/kling", label: "可灵数字人", icon: "user" }, - // { path: '/digital-human/video', label: '数字人视频', icon: 'video' }, + { name: '人声克隆', label: '人声克隆', icon: 'mic' }, + { name: '可灵数字人', label: "可灵数字人", icon: "user" }, + // { name: '数字人视频', label: '数字人视频', icon: 'video' }, ] }, { title: '素材库', children: [ - { path: '/material/list', label: '素材列表', icon: 'grid' }, - { path: '/material/mix', label: '智能混剪', icon: 'scissors' }, - { path: '/material/mix-task', label: '混剪任务', icon: 'video' }, - { path: '/material/group', label: '素材分组', icon: 'folder' }, + { name: '素材列表', label: '素材列表', icon: 'grid' }, + { name: '智能混剪', label: '智能混剪', icon: 'scissors' }, + { name: '素材分组', label: '素材分组', icon: 'folder' }, + ] + }, + { + title: '任务管理', + children: [ + { name: '任务中心', label: '任务中心', icon: 'video', params: { type: 'mix-task' } }, ] }, { title: '系统', children: [ - { path: '/system/style-settings', label: '风格设置', icon: 'text' }, + { name: '风格设置', label: '风格设置', icon: 'text' }, ] - }, - // { - // title: '视频', - // children: [ - // { path: '/digital-human/avatar', label: '生成数字人', icon: 'user' }, - // ] - // }, + } ] // 如果未登录,过滤掉"系统"菜单组 @@ -71,8 +70,12 @@ const items = computed(() => { return allItems }) -function go(p) { - router.push(p) +function go(item) { + if (item.params) { + router.push({ name: item.name, params: item.params }) + } else { + router.push({ name: item.name }) + } } @@ -81,7 +84,7 @@ function go(p) {