diff --git a/demo/database_migration_user_error_log.sql b/demo/database_migration_user_error_log.sql new file mode 100644 index 0000000..2541ba6 --- /dev/null +++ b/demo/database_migration_user_error_log.sql @@ -0,0 +1,55 @@ +-- ===================================================== +-- 用户错误日志表 +-- 用于记录和统计用户操作过程中产生的错误 +-- ===================================================== + +CREATE TABLE IF NOT EXISTS user_error_log ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) COMMENT '用户名(可为空,未登录用户)', + error_type VARCHAR(50) NOT NULL COMMENT '错误类型枚举', + error_code VARCHAR(50) COMMENT '错误代码', + error_message TEXT COMMENT '错误消息', + error_source VARCHAR(100) NOT NULL COMMENT '错误来源(服务类名或接口路径)', + task_id VARCHAR(100) COMMENT '关联的任务ID', + task_type VARCHAR(50) COMMENT '任务类型', + request_path VARCHAR(500) COMMENT '请求路径', + request_method VARCHAR(10) COMMENT '请求方法 GET/POST等', + request_params TEXT COMMENT '请求参数(JSON格式)', + stack_trace TEXT COMMENT '堆栈跟踪', + ip_address VARCHAR(50) COMMENT 'IP地址', + user_agent VARCHAR(500) COMMENT '用户代理', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + + INDEX idx_username (username), + INDEX idx_error_type (error_type), + INDEX idx_created_at (created_at), + INDEX idx_error_source (error_source), + INDEX idx_task_id (task_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户错误日志表'; + +-- ===================================================== +-- 错误类型说明: +-- TASK_SUBMIT_ERROR - 任务提交失败 +-- TASK_PROCESSING_ERROR - 任务处理失败 +-- TASK_TIMEOUT - 任务超时 +-- TASK_CANCELLED - 任务取消 +-- API_CALL_ERROR - API调用失败 +-- API_RESPONSE_ERROR - API响应异常 +-- API_TIMEOUT - API超时 +-- PAYMENT_ERROR - 支付失败 +-- PAYMENT_CALLBACK_ERROR- 支付回调异常 +-- REFUND_ERROR - 退款失败 +-- AUTH_ERROR - 认证失败 +-- TOKEN_EXPIRED - Token过期 +-- PERMISSION_DENIED - 权限不足 +-- DATA_VALIDATION_ERROR - 数据验证失败 +-- DATA_NOT_FOUND - 数据未找到 +-- DATA_CONFLICT - 数据冲突 +-- FILE_UPLOAD_ERROR - 文件上传失败 +-- FILE_DOWNLOAD_ERROR - 文件下载失败 +-- FILE_PROCESS_ERROR - 文件处理失败 +-- SYSTEM_ERROR - 系统错误 +-- DATABASE_ERROR - 数据库错误 +-- NETWORK_ERROR - 网络错误 +-- UNKNOWN - 未知错误 +-- ===================================================== diff --git a/demo/docs/task_status_cascade_trigger.sql b/demo/docs/task_status_cascade_trigger.sql index 355fc7e..ce27815 100644 --- a/demo/docs/task_status_cascade_trigger.sql +++ b/demo/docs/task_status_cascade_trigger.sql @@ -60,6 +60,8 @@ BEGIN UPDATE text_to_video_tasks SET status = CASE WHEN NEW.status = 'TIMEOUT' THEN 'FAILED' ELSE NEW.status END, updated_at = NOW(), + progress = CASE WHEN NEW.status = 'COMPLETED' THEN 100 ELSE progress END, + completed_at = CASE WHEN NEW.status IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT') THEN NOW() ELSE completed_at END, error_message = CASE WHEN NEW.status IN ('FAILED', 'TIMEOUT') THEN COALESCE(NEW.error_message, '任务超时') ELSE error_message END, result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END WHERE task_id = NEW.task_id; @@ -70,6 +72,8 @@ BEGIN UPDATE image_to_video_tasks SET status = CASE WHEN NEW.status = 'TIMEOUT' THEN 'FAILED' ELSE NEW.status END, updated_at = NOW(), + progress = CASE WHEN NEW.status = 'COMPLETED' THEN 100 ELSE progress END, + completed_at = CASE WHEN NEW.status IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT') THEN NOW() ELSE completed_at END, error_message = CASE WHEN NEW.status IN ('FAILED', 'TIMEOUT') THEN COALESCE(NEW.error_message, '任务超时') ELSE error_message END, result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END WHERE task_id = NEW.task_id; @@ -80,6 +84,8 @@ BEGIN UPDATE storyboard_video_tasks SET status = CASE WHEN NEW.status = 'TIMEOUT' THEN 'FAILED' ELSE NEW.status END, updated_at = NOW(), + progress = CASE WHEN NEW.status = 'COMPLETED' THEN 100 ELSE progress END, + completed_at = CASE WHEN NEW.status IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT') THEN NOW() ELSE completed_at END, error_message = CASE WHEN NEW.status IN ('FAILED', 'TIMEOUT') THEN COALESCE(NEW.error_message, '任务超时') ELSE error_message END, result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END WHERE task_id = NEW.task_id; diff --git a/demo/frontend/src/api/request.js b/demo/frontend/src/api/request.js index 0104b8e..1c56d94 100644 --- a/demo/frontend/src/api/request.js +++ b/demo/frontend/src/api/request.js @@ -38,7 +38,7 @@ api.interceptors.request.use( if (!isLoginRequest) { // 非登录请求才添加Authorization头 - const token = sessionStorage.getItem('token') + const token = localStorage.getItem('token') if (token && token !== 'null' && token.trim() !== '') { config.headers.Authorization = `Bearer ${token}` console.log('请求拦截器:添加Authorization头,token长度:', token.length) @@ -70,8 +70,8 @@ api.interceptors.response.use( if (!isLoginRequest) { // 清除无效的token并跳转到登录页 - sessionStorage.removeItem('token') - sessionStorage.removeItem('user') + localStorage.removeItem('token') + localStorage.removeItem('user') // 避免重复跳转 if (router.currentRoute.value.path !== '/login') { ElMessage.error('认证失败,请重新登录') @@ -91,8 +91,8 @@ api.interceptors.response.use( const isLoginRequest = loginUrls.some(url => response.config.url.includes(url)) if (!isLoginRequest) { - sessionStorage.removeItem('token') - sessionStorage.removeItem('user') + localStorage.removeItem('token') + localStorage.removeItem('user') if (router.currentRoute.value.path !== '/login') { ElMessage.error('认证失败,请重新登录') router.push('/login') @@ -117,8 +117,8 @@ api.interceptors.response.use( const isLoginRequest = loginUrls.some(url => error.config.url.includes(url)) if (!isLoginRequest) { - sessionStorage.removeItem('token') - sessionStorage.removeItem('user') + localStorage.removeItem('token') + localStorage.removeItem('user') if (router.currentRoute.value.path !== '/login') { ElMessage.error('认证失败,请重新登录') router.push('/login') @@ -136,8 +136,8 @@ api.interceptors.response.use( if (!isLoginRequest) { // 302也可能是认证失败导致的 - sessionStorage.removeItem('token') - sessionStorage.removeItem('user') + localStorage.removeItem('token') + localStorage.removeItem('user') if (router.currentRoute.value.path !== '/login') { ElMessage.error('认证失败,请重新登录') router.push('/login') diff --git a/demo/frontend/src/api/storyboardVideo.js b/demo/frontend/src/api/storyboardVideo.js index 2763061..0a97012 100644 --- a/demo/frontend/src/api/storyboardVideo.js +++ b/demo/frontend/src/api/storyboardVideo.js @@ -37,4 +37,12 @@ export const startVideoGeneration = async (taskId, params = {}) => { */ export const mergeImagesToGrid = async (images, cols = 3) => { return api.post('/image-grid/merge', { images, cols }) +} + +/** + * 重试失败的分镜视频任务 + * @param {string} taskId - 任务ID + */ +export const retryStoryboardTask = async (taskId) => { + return api.post(`/storyboard-video/task/${taskId}/retry`) } \ No newline at end of file diff --git a/demo/frontend/src/locales/en.js b/demo/frontend/src/locales/en.js index 0597341..47e05e0 100644 --- a/demo/frontend/src/locales/en.js +++ b/demo/frontend/src/locales/en.js @@ -310,13 +310,15 @@ export default { generateStoryboard: 'Generate Storyboard', generateVideo: 'Generate Video', uploadStoryboard: 'Upload Storyboard (can generate video directly)', - uploadHint: 'Upload 1-9 storyboard images, can generate video directly without text description', + uploadImage: 'Upload Image', + imageLabel: 'Image ', + uploadHint: 'Upload reference images, AI will generate 6-grid storyboard based on images', addMore: 'Add More', uploadedCount: 'Uploaded {count}/9', uploadLimit: 'Limit reached', uploadedImage: 'Uploaded image {index}', - maxImages: 'Maximum 9 images allowed', - maxImagesWarning: 'Maximum 9 images allowed, you have uploaded {current}, you can upload {remaining} more', + maxImages: 'Maximum 3 images allowed', + maxImagesWarning: 'Maximum 3 images allowed', fileSizeLimit: 'Image file size cannot exceed 100MB', invalidFileType: 'Please select valid image files', uploadSuccess: 'Successfully uploaded {count} images', @@ -365,6 +367,7 @@ export default { generateVideoWithUpload: 'Generate Video with Uploaded Image', startGenerateStoryboard: 'Start Generate Storyboard', startGenerate: 'Start Generate', + generating: 'Generating...', enterDescription: 'Please enter description', enterDescriptionForImage: 'Please enter description, AI will generate storyboard based on reference image and description', startingGenerate: 'Starting to generate storyboard...', @@ -375,6 +378,7 @@ export default { storyboardCompleted: 'Storyboard generation completed! Please click "Start Generate" button to generate video', videoCompleted: 'Video generation completed!', taskFailed: 'Task failed', + checkInputOrRetry: 'Please check input or retry', unknownError: 'Unknown error', startingVideoGenerate: 'Starting to generate video...', videoTaskStarted: 'Video generation task started, please wait...', @@ -390,7 +394,17 @@ export default { taskCompleted: 'Task completed!', resumingVideoTask: 'Detected unfinished video generation task, resuming...', resumingStoryboardTask: 'Detected unfinished storyboard generation task, resuming...', - resumingTask: 'Detected unfinished task, resuming...' + resumingTask: 'Detected unfinished task, resuming...', + storyboardReady: 'Storyboard generated, click button below to generate video', + regenerate: 'Regenerate', + regenerateConfirm: 'Regenerating will consume points and create a new task. Continue?', + regenerateTitle: 'Regenerate Storyboard', + statusPending: 'Pending', + statusProcessing: 'Processing', + statusCompleted: 'Completed', + statusFailed: 'Failed', + statusCancelled: 'Cancelled', + statusUnknown: 'Unknown Status' } }, @@ -426,6 +440,7 @@ export default { popular: 'Popular', searchPlaceholder: 'Name/Prompt/ID', selectItems: 'Select {count} items', + selectAll: 'Select All', selectedCount: '{count} selected', favorite: 'Favorite', downloadWithWatermark: 'Download with Watermark', @@ -788,9 +803,6 @@ export default { promptOptimizationModelTip: 'Enter the model name for prompt optimization, e.g., gpt-4o, gemini-pro', storyboardSystemPrompt: 'Storyboard System Prompt', storyboardSystemPromptTip: 'This prompt will be prepended to user prompts for consistent storyboard generation style', - storyboardSystemPromptPlaceholder: 'E.g., high quality cinematic shot, professional photography, film tone...', - promptOptimizationSystemPrompt: 'Prompt Optimization System Instruction', - promptOptimizationSystemPromptTip: 'Custom system instruction for AI prompt optimization. Leave empty to use default. This instruction determines how AI understands and optimizes user prompts', - promptOptimizationSystemPromptPlaceholder: 'E.g., You are a professional AI prompt optimization expert, transform user descriptions into detailed, professional English prompts...' + storyboardSystemPromptPlaceholder: 'E.g., high quality cinematic shot, professional photography, film tone...' } } diff --git a/demo/frontend/src/locales/zh.js b/demo/frontend/src/locales/zh.js index ab8e293..7097283 100644 --- a/demo/frontend/src/locales/zh.js +++ b/demo/frontend/src/locales/zh.js @@ -313,13 +313,15 @@ export default { generateStoryboard: '生成分镜图', generateVideo: '生成视频', uploadStoryboard: '上传参考图片', - uploadHint: '上传一张参考图片,AI将根据图片生成6宫格分镜图', + uploadImage: '上传图片', + imageLabel: '图', + uploadHint: '上传参考图片,AI将根据图片生成6宫格分镜图', addMore: '重新上传', uploadedCount: '已上传图片', uploadLimit: '已上传', uploadedImage: '参考图片', - maxImages: '只能上传1张参考图片', - maxImagesWarning: '只能上传1张参考图片', + maxImages: '最多只能上传3张参考图片', + maxImagesWarning: '最多只能上传3张参考图片', fileSizeLimit: '图片文件大小不能超过100MB', invalidFileType: '请选择有效的图片文件', uploadSuccess: '成功上传 {count} 张图片', @@ -369,6 +371,7 @@ export default { generateStoryboardWithImage: '使用图片生成分镜图', startGenerateStoryboard: '开始生成分镜图', startGenerate: '开始生成', + generating: '生成中...', enterDescription: '请输入描述文字', enterDescriptionForImage: '请输入描述文字,AI将根据参考图和描述生成分镜图', startingGenerate: '开始生成分镜图...', @@ -379,6 +382,7 @@ export default { storyboardCompleted: '分镜图生成完成!请点击"开始生成"按钮生成视频', videoCompleted: '视频生成完成!', taskFailed: '任务失败', + checkInputOrRetry: '请检查输入或重新尝试', unknownError: '未知错误', startingVideoGenerate: '开始生成视频...', videoTaskStarted: '视频生成任务已启动,请稍候...', @@ -395,7 +399,16 @@ export default { resumingVideoTask: '检测到未完成的视频生成任务,继续处理中...', resumingStoryboardTask: '检测到未完成的分镜图生成任务,继续处理中...', resumingTask: '检测到未完成的任务,继续处理中...', - storyboardReady: '分镜图已生成,点击下方按钮生成视频' + storyboardReady: '分镜图已生成,点击下方按钮生成视频', + regenerate: '重新生成', + regenerateConfirm: '重新生成将消耗积分并创建新任务,确定要继续吗?', + regenerateTitle: '重新生成分镜图', + statusPending: '排队中', + statusProcessing: '生成中', + statusCompleted: '已完成', + statusFailed: '生成失败', + statusCancelled: '已取消', + statusUnknown: '未知状态' } }, @@ -439,6 +452,7 @@ export default { popular: '热门', searchPlaceholder: '名字/提示词/ID', selectItems: '选择{count}个项目', + selectAll: '全选', selectedCount: '已选 {count} 个项目', favorite: '收藏', downloadWithWatermark: '带水印下载', @@ -766,9 +780,6 @@ export default { promptOptimizationModelTip: '输入用于优化提示词的模型名称,如 gpt-4o、gemini-pro 等', storyboardSystemPrompt: '分镜图系统引导词', storyboardSystemPromptTip: '此引导词会自动添加到用户提示词前面,用于统一分镜图生成风格', - storyboardSystemPromptPlaceholder: '例如:高质量电影级画面,专业摄影,电影色调...', - promptOptimizationSystemPrompt: '优化提示词系统指令', - promptOptimizationSystemPromptTip: '自定义AI优化提示词的系统指令,留空则使用默认指令。该指令决定了AI如何理解和优化用户输入的提示词', - promptOptimizationSystemPromptPlaceholder: '例如:你是一个专业的AI提示词优化专家,将用户描述优化为详细、专业的英文提示词...' + storyboardSystemPromptPlaceholder: '例如:高质量电影级画面,专业摄影,电影色调...' } } diff --git a/demo/frontend/src/stores/user.js b/demo/frontend/src/stores/user.js index d16f9b4..3214257 100644 --- a/demo/frontend/src/stores/user.js +++ b/demo/frontend/src/stores/user.js @@ -3,21 +3,21 @@ import { ref, computed } from 'vue' import { login, register, logout, getCurrentUser } from '@/api/auth' export const useUserStore = defineStore('user', () => { - // 状态 - 从 sessionStorage 尝试恢复用户信息 + // 状态 - 从 localStorage 尝试恢复用户信息 const user = ref(null) const token = ref(null) const loading = ref(false) const initialized = ref(false) try { - const cachedUser = sessionStorage.getItem('user') - const cachedToken = sessionStorage.getItem('token') + const cachedUser = localStorage.getItem('user') + const cachedToken = localStorage.getItem('token') if (cachedUser && cachedToken) { user.value = JSON.parse(cachedUser) token.value = cachedToken } } catch (_) { - // ignore sessionStorage parse errors + // ignore localStorage parse errors } // 计算属性 @@ -45,9 +45,9 @@ export const useUserStore = defineStore('user', () => { user.value = response.data.user token.value = response.data.token - // 保存到sessionStorage,关闭页面时自动清除 - sessionStorage.setItem('token', response.data.token) - sessionStorage.setItem('user', JSON.stringify(user.value)) + // 保存到localStorage,关闭浏览器后仍保持登录 + localStorage.setItem('token', response.data.token) + localStorage.setItem('user', JSON.stringify(user.value)) return { success: true } } else { return { success: false, message: response.message } @@ -82,11 +82,11 @@ export const useUserStore = defineStore('user', () => { // 登出 const logoutUser = async () => { try { - // JWT无状态,直接清除sessionStorage即可 + // JWT无状态,直接清除localStorage即可 token.value = null user.value = null - sessionStorage.removeItem('token') - sessionStorage.removeItem('user') + localStorage.removeItem('token') + localStorage.removeItem('user') } catch (error) { console.error('Logout error:', error) } @@ -101,7 +101,7 @@ export const useUserStore = defineStore('user', () => { if (data.success) { user.value = data.data - sessionStorage.setItem('user', JSON.stringify(user.value)) + localStorage.setItem('user', JSON.stringify(user.value)) } else { console.warn('获取用户信息失败:', data.message) // 不要立即清除用户数据,保持当前登录状态 @@ -117,9 +117,9 @@ export const useUserStore = defineStore('user', () => { const clearUserData = () => { token.value = null user.value = null - // 清除 sessionStorage 中的用户数据 - sessionStorage.removeItem('token') - sessionStorage.removeItem('user') + // 清除 localStorage 中的用户数据 + localStorage.removeItem('token') + localStorage.removeItem('user') } // 初始化 @@ -128,9 +128,9 @@ export const useUserStore = defineStore('user', () => { return } - // 从sessionStorage恢复用户状态 - const savedToken = sessionStorage.getItem('token') - const savedUser = sessionStorage.getItem('user') + // 从localStorage恢复用户状态 + const savedToken = localStorage.getItem('token') + const savedUser = localStorage.getItem('user') if (savedToken && savedUser) { try { diff --git a/demo/frontend/src/views/AdminDashboard.vue b/demo/frontend/src/views/AdminDashboard.vue index 09e7563..c4006d9 100644 --- a/demo/frontend/src/views/AdminDashboard.vue +++ b/demo/frontend/src/views/AdminDashboard.vue @@ -493,7 +493,7 @@ const fetchSystemStats = async () => { try { const response = await fetch('/api/admin/online-stats', { headers: { - 'Authorization': `Bearer ${sessionStorage.getItem('token')}` + 'Authorization': `Bearer ${localStorage.getItem('token')}` } }) const data = await response.json() diff --git a/demo/frontend/src/views/AdminOrders.vue b/demo/frontend/src/views/AdminOrders.vue index f428e73..1f7a5ed 100644 --- a/demo/frontend/src/views/AdminOrders.vue +++ b/demo/frontend/src/views/AdminOrders.vue @@ -654,7 +654,7 @@ const fetchSystemStats = async () => { try { const response = await fetch('/api/admin/online-stats', { headers: { - 'Authorization': `Bearer ${sessionStorage.getItem('token')}` + 'Authorization': `Bearer ${localStorage.getItem('token')}` } }) const data = await response.json() diff --git a/demo/frontend/src/views/ApiManagement.vue b/demo/frontend/src/views/ApiManagement.vue index 401f71f..e6e8d80 100644 --- a/demo/frontend/src/views/ApiManagement.vue +++ b/demo/frontend/src/views/ApiManagement.vue @@ -280,7 +280,7 @@ const fetchSystemStats = async () => { try { const response = await fetch('/api/admin/online-stats', { headers: { - 'Authorization': `Bearer ${sessionStorage.getItem('token')}` + 'Authorization': `Bearer ${localStorage.getItem('token')}` } }) const data = await response.json() diff --git a/demo/frontend/src/views/ChangePassword.vue b/demo/frontend/src/views/ChangePassword.vue index c7b4edb..01a7bc8 100644 --- a/demo/frontend/src/views/ChangePassword.vue +++ b/demo/frontend/src/views/ChangePassword.vue @@ -87,7 +87,7 @@ const loading = ref(false) // 判断是否首次设置密码 const isFirstTimeSetup = computed(() => { - return sessionStorage.getItem('needSetPassword') === '1' + return localStorage.getItem('needSetPassword') === '1' }) const form = reactive({ @@ -162,7 +162,7 @@ const handleSubmit = async () => { ElMessage.success('密码修改成功') // 清除首次设置标记 - sessionStorage.removeItem('needSetPassword') + localStorage.removeItem('needSetPassword') // 跳转到首页或之前的页面 const redirect = route.query.redirect || '/profile' diff --git a/demo/frontend/src/views/GenerateTaskRecord.vue b/demo/frontend/src/views/GenerateTaskRecord.vue index cf47296..295756a 100644 --- a/demo/frontend/src/views/GenerateTaskRecord.vue +++ b/demo/frontend/src/views/GenerateTaskRecord.vue @@ -522,7 +522,7 @@ const handleDelete = async (task) => { ) // 调用后端API删除 - const token = sessionStorage.getItem('token') + const token = localStorage.getItem('token') if (!token) { ElMessage.error('请先登录') return @@ -574,7 +574,7 @@ const handleBatchDelete = async () => { ) // 调用后端API批量删除 - const token = sessionStorage.getItem('token') + const token = localStorage.getItem('token') if (!token) { ElMessage.error('请先登录') return @@ -759,7 +759,7 @@ const fetchSystemStats = async () => { try { const response = await fetch('/api/admin/online-stats', { headers: { - 'Authorization': `Bearer ${sessionStorage.getItem('token')}` + 'Authorization': `Bearer ${localStorage.getItem('token')}` } }) const data = await response.json() diff --git a/demo/frontend/src/views/ImageToVideo.vue b/demo/frontend/src/views/ImageToVideo.vue index 6c60c62..590a299 100644 --- a/demo/frontend/src/views/ImageToVideo.vue +++ b/demo/frontend/src/views/ImageToVideo.vue @@ -30,7 +30,6 @@
diff --git a/demo/frontend/src/views/ImageToVideoCreate.vue b/demo/frontend/src/views/ImageToVideoCreate.vue index 993e7bf..05400a4 100644 --- a/demo/frontend/src/views/ImageToVideoCreate.vue +++ b/demo/frontend/src/views/ImageToVideoCreate.vue @@ -1457,7 +1457,7 @@ onUnmounted(() => { .main-content { flex: 1; display: grid; - grid-template-columns: 400px 1fr; + grid-template-columns: 520px 1fr; gap: 0; height: calc(100vh - 100px); } @@ -1505,33 +1505,38 @@ onUnmounted(() => { /* 创作模式标签 */ .creation-tabs { display: flex; - gap: 4px; - background: #0a0a0a; - padding: 4px; - border-radius: 12px; + gap: 8px; + padding: 0; } .tab { - flex: 1; - padding: 12px 16px; - border-radius: 8px; + width: 155px; + height: 44px; + padding: 0; + border-radius: 12px; cursor: pointer; transition: all 0.2s ease; color: #9ca3af; font-size: 14px; font-weight: 500; text-align: center; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.1); } .tab.active { - background: #3b82f6; + background: #313338; color: #fff; - box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); + border-color: transparent; } .tab:hover:not(.active) { - background: #2a2a2a; + background: rgba(49, 51, 56, 0.5); color: #fff; + border-color: transparent; } /* 图片输入区域 */ @@ -2598,7 +2603,7 @@ onUnmounted(() => { /* 响应式设计 */ @media (max-width: 1200px) { .main-content { - grid-template-columns: 350px 1fr; + grid-template-columns: 420px 1fr; } .left-panel { diff --git a/demo/frontend/src/views/Login.vue b/demo/frontend/src/views/Login.vue index c340c7e..1456889 100644 --- a/demo/frontend/src/views/Login.vue +++ b/demo/frontend/src/views/Login.vue @@ -308,17 +308,17 @@ const handleLogin = async () => { const loginToken = response.data.data.token const needsPasswordChange = response.data.data.needsPasswordChange // 后端直接返回是否需要修改密码 - sessionStorage.setItem('token', loginToken) - sessionStorage.setItem('user', JSON.stringify(loginUser)) + localStorage.setItem('token', loginToken) + localStorage.setItem('user', JSON.stringify(loginUser)) userStore.user = loginUser userStore.token = loginToken // 根据后端返回的标记设置是否需要修改密码 if (needsPasswordChange) { - sessionStorage.setItem('needSetPassword', '1') + localStorage.setItem('needSetPassword', '1') console.log('新用户首次登录,需要设置密码') } else { - sessionStorage.removeItem('needSetPassword') + localStorage.removeItem('needSetPassword') } console.log('登录成功,用户信息:', userStore.user, '需要设置密码:', needsPasswordChange) @@ -328,7 +328,7 @@ const handleLogin = async () => { await new Promise(resolve => setTimeout(resolve, 200)) // 如果需要设置密码,跳转到设置密码页面 - const needSetPassword = sessionStorage.getItem('needSetPassword') === '1' + const needSetPassword = localStorage.getItem('needSetPassword') === '1' const redirectPath = needSetPassword ? '/set-password' : (route.query.redirect || '/profile') console.log('准备跳转到:', redirectPath, '需要设置密码:', needSetPassword) diff --git a/demo/frontend/src/views/MemberManagement.vue b/demo/frontend/src/views/MemberManagement.vue index 6563f4e..f00d65f 100644 --- a/demo/frontend/src/views/MemberManagement.vue +++ b/demo/frontend/src/views/MemberManagement.vue @@ -276,7 +276,7 @@ const currentPage = ref(1) const pageSize = ref(10) const totalMembers = ref(50) -// 当前用户角色(从 sessionStorage 或 API 获取) +// 当前用户角色(从 localStorage 或 API 获取) const currentUserRole = ref('') const isSuperAdmin = computed(() => currentUserRole.value === 'ROLE_SUPER_ADMIN') @@ -702,7 +702,7 @@ const toggleRole = async (member) => { const fetchCurrentUserRole = () => { try { // 登录时保存的是 'user' 而不是 'userInfo' - const userStr = sessionStorage.getItem('user') + const userStr = localStorage.getItem('user') if (userStr) { const user = JSON.parse(userStr) currentUserRole.value = user.role || 'ROLE_USER' @@ -727,7 +727,7 @@ const fetchSystemStats = async () => { try { const response = await fetch('/api/admin/online-stats', { headers: { - 'Authorization': `Bearer ${sessionStorage.getItem('token')}` + 'Authorization': `Bearer ${localStorage.getItem('token')}` } }) const data = await response.json() diff --git a/demo/frontend/src/views/MyWorks.vue b/demo/frontend/src/views/MyWorks.vue index eec2773..36c5d1c 100644 --- a/demo/frontend/src/views/MyWorks.vue +++ b/demo/frontend/src/views/MyWorks.vue @@ -41,7 +41,6 @@ @@ -133,7 +132,7 @@