From d9f31033041d974bd83fe92ced0f7087e405cc7d Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Fri, 28 Nov 2025 20:26:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/ui.md | 35 + .claude/settings.local.json | 4 +- frontend/app/web-gold/src/api/http.js | 27 +- frontend/app/web-gold/src/api/material.js | 6 +- .../material/MaterialBatchGroupModal.vue | 97 +++ .../components/material/MaterialMixModal.vue | 258 +++++++ .../material/MaterialUploadModal.vue | 8 +- frontend/app/web-gold/src/router/index.js | 8 +- .../app/web-gold/src/services/AuthService.js | 241 ------ frontend/app/web-gold/src/utils/coverCache.js | 175 +++++ .../app/web-gold/src/views/auth/Login.vue | 720 +++++++++--------- .../src/views/content-style/Copywriting.vue | 1 - .../src/views/material/MaterialGroup.vue | 175 +++-- .../src/views/material/MaterialList.vue | 424 +++++------ .../web-gold/src/views/trends/Forecast.vue | 1 - frontend/app/web-gold/vite.config.js | 2 +- frontend/config/api.js | 5 +- sql/mysql/V20241220__create_tik_mix_task.sql | 25 - sql/mysql/V20241226__create_tik_tables.sql | 72 ++ .../tik/file/dal/mysql/TikUserFileMapper.java | 8 +- .../file/service/TikFileGroupServiceImpl.java | 18 +- .../file/service/TikUserFileServiceImpl.java | 70 +- .../tik/media/BatchProduceAlignment.java | 11 + .../module/tik/mix/client/IceClient.java | 114 +++ .../module/tik/mix/config/IceProperties.java | 56 ++ .../tik/mix/service/MixTaskServiceImpl.java | 42 + 26 files changed, 1582 insertions(+), 1021 deletions(-) create mode 100644 .claude/commands/ui.md create mode 100644 frontend/app/web-gold/src/components/material/MaterialBatchGroupModal.vue create mode 100644 frontend/app/web-gold/src/components/material/MaterialMixModal.vue delete mode 100644 frontend/app/web-gold/src/services/AuthService.js create mode 100644 frontend/app/web-gold/src/utils/coverCache.js delete mode 100644 sql/mysql/V20241220__create_tik_mix_task.sql create mode 100644 sql/mysql/V20241226__create_tik_tables.sql create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/mix/client/IceClient.java create mode 100644 yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/mix/config/IceProperties.java diff --git a/.claude/commands/ui.md b/.claude/commands/ui.md new file mode 100644 index 0000000000..af89d88a6a --- /dev/null +++ b/.claude/commands/ui.md @@ -0,0 +1,35 @@ +### 【CoT思维链】UI/UX全维度优化专家角色提示词 + +#### 角色设定 +你是**资深UI/UX优化专家**,兼具前端视觉设计能力与用户体验逻辑思维,擅长从“用户需求-场景定位-视觉落地-交互闭环”全链路优化网页,尤其精通科技极简风格、企业级产品界面的设计逻辑。 + +#### 背景前提 +当前待优化对象为**科技极简黑蓝紫风格的企业级AI工具登录页**,核心基调是“深空黑+冰川蓝+熏衣紫”,需适配左右布局,兼顾品牌展示与功能登录的双重需求。 + +#### CoT思维链引导任务 +请按照以下思维步骤,逐步分析并输出优化方案: + +1. **第一步:需求拆解与场景锚定** + - 明确页面核心定位:企业级AI工具登录页的目标用户(企业员工/管理员)、使用场景(办公环境、多设备访问)、核心诉求(专业感/安全感/操作高效性); + - 梳理现有设计的核心要素:风格(科技极简)、配色(黑蓝紫)、布局(左右分栏)、交互(光点联动/输入框渐变/按钮反馈)、动画(鼠标联动/状态过渡)。 + +2. **第二步:现状问题诊断** + - 视觉层面:检查配色对比度是否达标、元素层级是否清晰、品牌识别性是否足够; + - 布局层面:验证左右分栏的空间分配合理性、响应式适配的流畅度、留白与内容密度的平衡; + - 交互层面:分析现有反馈(输入框聚焦/按钮点击)是否明确、用户操作路径是否无歧义; + - 动画层面:判断动态效果(光点联动)是否干扰核心操作、过渡动画是否自然且有目的性。 + +3. **第三步:分维度优化推导** + - **配色优化**:基于科技极简风格,微调色彩明度/饱和度以提升可读性,补充辅助色规范; + - **布局优化**:优化左右分栏的比例、内容对齐方式,强化移动端布局的适配逻辑; + - **样式优化**:细化控件(输入框/按钮)的质感表现,统一视觉细节(圆角/阴影/描边); + - **交互优化**:增强操作反馈的层次感(hover/active/focus状态区分),补充无障碍交互设计; + - **动画优化**:精简非必要动效,强化核心动效的目的性(如登录成功/失败的反馈动画)。 + +4. **第四步:方案验证与落地性** + - 验证优化方案是否符合企业级产品的专业性定位; + - 确保所有优化点可通过CSS/JS实现,兼顾性能与效果; + - 检查多设备(PC/平板/手机)的适配一致性。 + +#### 输出要求 +按“问题诊断→优化方案→代码落地建议”的结构输出,每个优化维度需说明“原设计不足+优化逻辑+具体实现方式”,并附关键代码片段或设计参数参考。 \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b93eae70e8..33373d5561 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,9 @@ "Bash(cat:*)", "Bash(node -e:*)", "Bash(git add:*)", - "Bash(git commit:*)" + "Bash(git commit:*)", + "Bash(git log:*)", + "Bash(xargs:*)" ], "deny": [], "ask": [] diff --git a/frontend/app/web-gold/src/api/http.js b/frontend/app/web-gold/src/api/http.js index 2181bd7289..1c675c5adf 100644 --- a/frontend/app/web-gold/src/api/http.js +++ b/frontend/app/web-gold/src/api/http.js @@ -6,7 +6,7 @@ */ import { createClientAxios } from '@gold/api/axios/client' -import authService from '@/services/AuthService' +import { refreshToken } from '@/api/auth' import router from '@/router' /** @@ -29,16 +29,19 @@ export function createHttpClient(options = {}) { return } - // 默认处理:使用 AuthService 尝试刷新token - await authService.handleAuthError( - error, - () => { - // 刷新失败,使用router跳转,避免整页刷新 - router.push('/login') - } - ) - // 如果刷新成功,AuthService返回null,不跳转,不抛出错误 - // 业务代码可以捕获这个错误并重试请求 + // 默认处理:尝试刷新token + try { + await refreshToken() + // 刷新成功:标记错误已处理,token已更新 + error._handled = true + error._tokenRefreshed = true + console.info('Token刷新成功,可以重试原请求') + // 不抛出错误,交给上层决定是否重试 + } catch (refreshError) { + // 刷新失败:使用router跳转,避免整页刷新 + console.error('刷新token失败:', refreshError) + router.push('/login') + } }, on403: (error) => { // 403:没有权限,直接跳转到登录页 @@ -106,7 +109,7 @@ export default http * console.error('请求失败:', error.message) * } * - * 注意:当 401 发生时,AuthService 会自动尝试刷新 token。 + * 注意:当 401 发生时,系统会自动尝试刷新 token。 * 如果刷新成功,会使用相同的 http 实例重新发起请求, * 确保自定义拦截器被正确应用。 */ diff --git a/frontend/app/web-gold/src/api/material.js b/frontend/app/web-gold/src/api/material.js index b6fc765660..4478c3ef0c 100644 --- a/frontend/app/web-gold/src/api/material.js +++ b/frontend/app/web-gold/src/api/material.js @@ -6,8 +6,8 @@ import http from './http' import { API_BASE } from '@gold/config/api' -// 统一使用 /api/tik 前缀,与 tikhub 保持一致 -const BASE_URL = `${API_BASE.APP}/api/tik/file` +// 使用 webApi 前缀,确保能够被代理 +const BASE_URL = `${API_BASE.APP_TIK}/file` /** * 素材库 API 服务 @@ -104,7 +104,7 @@ export const MaterialService = { /** * 素材分组 API 服务 */ -const GROUP_BASE_URL = `${API_BASE.APP}/api/tik/file/group` +const GROUP_BASE_URL = `${API_BASE.APP_TIK}/file/group` export const MaterialGroupService = { /** diff --git a/frontend/app/web-gold/src/components/material/MaterialBatchGroupModal.vue b/frontend/app/web-gold/src/components/material/MaterialBatchGroupModal.vue new file mode 100644 index 0000000000..0d36bc8a34 --- /dev/null +++ b/frontend/app/web-gold/src/components/material/MaterialBatchGroupModal.vue @@ -0,0 +1,97 @@ + + + diff --git a/frontend/app/web-gold/src/components/material/MaterialMixModal.vue b/frontend/app/web-gold/src/components/material/MaterialMixModal.vue new file mode 100644 index 0000000000..6917564e07 --- /dev/null +++ b/frontend/app/web-gold/src/components/material/MaterialMixModal.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/frontend/app/web-gold/src/components/material/MaterialUploadModal.vue b/frontend/app/web-gold/src/components/material/MaterialUploadModal.vue index 602b354e57..539f9b7a41 100644 --- a/frontend/app/web-gold/src/components/material/MaterialUploadModal.vue +++ b/frontend/app/web-gold/src/components/material/MaterialUploadModal.vue @@ -89,6 +89,10 @@ const props = defineProps({ uploading: { type: Boolean, default: false + }, + fileCategory: { + type: String, + default: 'video' } }) @@ -208,7 +212,9 @@ const handleConfirm = () => { return } - emit('confirm', filesWithCover, DEFAULT_FILE_CATEGORY) + // 使用传入的fileCategory,如果没有则使用默认值 + const category = props.fileCategory || DEFAULT_FILE_CATEGORY + emit('confirm', filesWithCover, category) } // 处理 visible 变化 diff --git a/frontend/app/web-gold/src/router/index.js b/frontend/app/web-gold/src/router/index.js index 6c6ed67baf..1a026b90c1 100644 --- a/frontend/app/web-gold/src/router/index.js +++ b/frontend/app/web-gold/src/router/index.js @@ -100,10 +100,10 @@ router.beforeEach(async (to, from, next) => { } // 检查是否已登录(通过 token 是否有效) - const isAuthenticated = tokenManager.isLoggedIn() + const authenToken = tokenManager.getToken() // 路由访问控制 - if (to.meta.requiresAuth && !isAuthenticated) { + if (to.meta.requiresAuth && !authenToken) { // 需要认证但未登录,跳转到登录页并记录当前路径 next({ path: '/login', @@ -113,13 +113,13 @@ router.beforeEach(async (to, from, next) => { } // 已登录用户访问登录页,重定向到首页 - if (to.path === '/login' && isAuthenticated) { + if (to.path === '/login' && authenToken) { next({ path: '/content-style/benchmark', replace: true }) return } // 首次访问且已登录时,同步用户信息到 store - if (isAuthenticated && !userStore.isLoggedIn) { + if (authenToken) { userStore.isLoggedIn = true userStore.fetchUserInfo().catch(error => { console.error('初始化用户信息失败:', error) diff --git a/frontend/app/web-gold/src/services/AuthService.js b/frontend/app/web-gold/src/services/AuthService.js deleted file mode 100644 index e7835b5ba5..0000000000 --- a/frontend/app/web-gold/src/services/AuthService.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * 认证服务 - 封装所有认证相关业务逻辑 - * 提供登录、登出、刷新token等方法 - * 函数式实现,无 this 绑定问题 - */ - -import { clientAxios } from '@gold/api/axios/client' -import tokenManager from '@gold/utils/token-manager' -import router from '@/router' - -// 刷新token的锁,避免并发刷新 -let isRefreshing = false -let refreshPromise = null - -/** - * 手机号+验证码登录 - */ -async function loginBySms(mobile, code) { - try { - const { data } = await clientAxios.post('/api/member/auth/sms-login', { mobile, code }) - const tokenInfo = data || {} - - // 保存token - tokenManager.setTokens({ - accessToken: tokenInfo.accessToken || '', - refreshToken: tokenInfo.refreshToken || '', - expiresIn: tokenInfo.expiresTime ? Math.floor(tokenInfo.expiresTime / 1000) : 7200, - tokenType: 'Bearer' - }) - - return tokenInfo - } catch (error) { - console.error('短信登录失败:', error) - throw error - } -} - -/** - * 账号密码登录 - */ -async function loginByPassword(mobile, password) { - try { - const { data } = await clientAxios.post('/api/member/auth/login', { mobile, password }) - const tokenInfo = data || {} - - tokenManager.setTokens({ - accessToken: tokenInfo.accessToken || '', - refreshToken: tokenInfo.refreshToken || '', - expiresIn: tokenInfo.expiresTime ? Math.floor(tokenInfo.expiresTime / 1000) : 7200, - tokenType: 'Bearer' - }) - - return tokenInfo - } catch (error) { - console.error('密码登录失败:', error) - throw error - } -} - -/** - * 执行实际刷新token的逻辑 - */ -async function doRefreshToken(refreshToken) { - const { data } = await clientAxios.post('/api/member/auth/refresh-token', null, { - params: { refreshToken } - }) - const tokenInfo = data || {} - - tokenManager.setTokens({ - accessToken: tokenInfo.accessToken || '', - refreshToken: tokenInfo.refreshToken || '', - expiresIn: tokenInfo.expiresTime ? Math.floor(tokenInfo.expiresTime / 1000) : 7200, - tokenType: 'Bearer' - }) - - return tokenInfo -} - -/** - * 刷新token - * 使用锁机制防止并发刷新 - */ -async function refreshToken() { - // 如果正在刷新,返回已有的刷新Promise - if (isRefreshing && refreshPromise) { - return refreshPromise - } - - const refreshTokenValue = tokenManager.getRefreshToken() - if (!refreshTokenValue) { - throw new Error('缺少refreshToken,无法刷新') - } - - try { - isRefreshing = true - refreshPromise = doRefreshToken(refreshTokenValue) - const result = await refreshPromise - return result - } finally { - isRefreshing = false - refreshPromise = null - } -} - -/** - * 发送短信验证码 - */ -async function sendSmsCode(mobile, scene = 1) { - const { data } = await clientAxios.post('/api/member/auth/send-sms-code', { - mobile, - scene - }) - return data -} - -/** - * 登出 - */ -async function logout() { - try { - // 清除token - tokenManager.clearTokens() - - // 使用router跳转,避免整页刷新 - router.push('/login') - } catch (error) { - console.error('登出失败:', error) - // 即使出错也要清除token - tokenManager.clearTokens() - // 依然使用router跳转 - router.push('/login') - } -} - -/** - * 检查是否已登录 - */ -function isAuthenticated() { - return tokenManager.isLoggedIn() -} - -/** - * 获取当前用户信息 - */ -async function getCurrentUser() { - if (!isAuthenticated()) { - return null - } - - try { - const { data } = await clientAxios.get('/api/member/user/get') - return data || null - } catch (error) { - console.error('获取用户信息失败:', error) - return null - } -} - -/** - * 处理401/403错误 - * @param {Error} error - axios错误对象 - * @param {Function} onAuthFailed - 认证失败回调 - */ -async function handleAuthError(error, onAuthFailed) { - const status = error?.status - const data = error?.data - const code = error?.code - - // 统一检查是否为 401 (token无效或过期) - const is401 = (code === 401) || (status === 401) - - // 统一检查是否为 403 (没有权限) - const is403 = (code === 403) || (status === 403) - - // 处理 401 错误:尝试刷新 token - if (is401) { - try { - // 刷新 token - await refreshToken() - - // 刷新成功:标记错误已处理,token已更新 - error._handled = true - error._tokenRefreshed = true - console.info('Token刷新成功,可以重试原请求') - - // 调用回调,告知上层可以重试 - if (typeof onAuthFailed === 'function') { - onAuthFailed(error) - } - - // 不再抛出错误,交给上层决定(如果上层没有跳转,则可以重试请求) - return null - } catch (refreshError) { - console.error('刷新token失败:', refreshError) - - // 刷新失败:标记为未处理,调用回调后抛出错误 - error._handled = false - error._tokenRefreshed = false - - if (typeof onAuthFailed === 'function') { - onAuthFailed(error) - } - throw error - } - } - - // 处理 403 错误:直接调用回调 - if (is403) { - error._handled = true - if (typeof onAuthFailed === 'function') { - onAuthFailed(error) - } - throw error - } - - // 其他错误直接抛出 - throw error -} - -export default { - loginBySms, - loginByPassword, - refreshToken, - sendSmsCode, - logout, - isAuthenticated, - getCurrentUser, - handleAuthError -} - -// 导出便捷方法(解构赋值,不会有 this 问题) -export { - loginBySms, - loginByPassword, - refreshToken, - sendSmsCode, - logout, - isAuthenticated, - getCurrentUser, - handleAuthError -} diff --git a/frontend/app/web-gold/src/utils/coverCache.js b/frontend/app/web-gold/src/utils/coverCache.js new file mode 100644 index 0000000000..23c9a10469 --- /dev/null +++ b/frontend/app/web-gold/src/utils/coverCache.js @@ -0,0 +1,175 @@ +/** + * 封面缓存工具 + * 使用localStorage缓存视频封面base64数据,降低OSS请求成本 + */ + +const CACHE_KEY = 'tik_video_covers' +const CACHE_EXPIRE_DAYS = 7 // 缓存过期时间:7天 + +/** + * 获取所有缓存的封面 + * @returns {Object} 缓存对象 { fileId: { base64: string, timestamp: number } } + */ +function getAllCaches() { + try { + const cacheJson = localStorage.getItem(CACHE_KEY) + return cacheJson ? JSON.parse(cacheJson) : {} + } catch (error) { + console.warn('[CoverCache] 读取缓存失败:', error) + return {} + } +} + +/** + * 保存缓存 + * @param {Object} caches 缓存对象 + */ +function saveCaches(caches) { + try { + localStorage.setItem(CACHE_KEY, JSON.stringify(caches)) + } catch (error) { + console.warn('[CoverCache] 保存缓存失败:', error) + } +} + +/** + * 从缓存获取封面 + * @param {number} fileId 文件ID + * @returns {string|null} base64数据或null + */ +function getCover(fileId) { + const caches = getAllCaches() + const cache = caches[fileId] + + if (!cache) { + return null + } + + // 检查是否过期 + const now = Date.now() + const expireTime = CACHE_EXPIRE_DAYS * 24 * 60 * 60 * 1000 + if (now - cache.timestamp > expireTime) { + // 缓存过期,删除 + delete caches[fileId] + saveCaches(caches) + return null + } + + return cache.base64 +} + +/** + * 保存封面到缓存 + * @param {number} fileId 文件ID + * @param {string} base64 base64数据 + */ +function setCover(fileId, base64) { + if (!fileId || !base64) { + return + } + + const caches = getAllCaches() + caches[fileId] = { + base64, + timestamp: Date.now() + } + saveCaches(caches) +} + +/** + * 批量保存封面 + * @param {Array} coverList 封面列表 [{ fileId, base64 }] + */ +function batchSetCovers(coverList) { + if (!Array.isArray(coverList) || coverList.length === 0) { + return + } + + const caches = getAllCaches() + let hasChange = false + + coverList.forEach(({ fileId, base64 }) => { + if (fileId && base64 && !caches[fileId]) { + caches[fileId] = { + base64, + timestamp: Date.now() + } + hasChange = true + } + }) + + if (hasChange) { + saveCaches(caches) + } +} + +/** + * 清理所有过期缓存 + */ +function cleanExpired() { + const caches = getAllCaches() + const now = Date.now() + const expireTime = CACHE_EXPIRE_DAYS * 24 * 60 * 60 * 1000 + let hasChange = false + + Object.keys(caches).forEach(fileId => { + if (now - caches[fileId].timestamp > expireTime) { + delete caches[fileId] + hasChange = true + } + }) + + if (hasChange) { + saveCaches(caches) + } +} + +/** + * 清空所有缓存 + */ +function clearAll() { + try { + localStorage.removeItem(CACHE_KEY) + } catch (error) { + console.warn('[CoverCache] 清空缓存失败:', error) + } +} + +/** + * 获取缓存统计信息 + * @returns {Object} 统计信息 + */ +function getStats() { + const caches = getAllCaches() + const now = Date.now() + const expireTime = CACHE_EXPIRE_DAYS * 24 * 60 * 60 * 1000 + + let expiredCount = 0 + Object.keys(caches).forEach(fileId => { + if (now - caches[fileId].timestamp > expireTime) { + expiredCount++ + } + }) + + return { + total: Object.keys(caches).length, + expired: expiredCount, + valid: Object.keys(caches).length - expiredCount + } +} + +// 页面加载时自动清理过期缓存 +if (typeof window !== 'undefined') { + window.addEventListener('load', () => { + cleanExpired() + }) +} + +export default { + getCover, + setCover, + batchSetCovers, + cleanExpired, + clearAll, + getStats +} diff --git a/frontend/app/web-gold/src/views/auth/Login.vue b/frontend/app/web-gold/src/views/auth/Login.vue index 1f6929f6f2..7bc78a6c2d 100644 --- a/frontend/app/web-gold/src/views/auth/Login.vue +++ b/frontend/app/web-gold/src/views/auth/Login.vue @@ -1,22 +1,7 @@