feat: 添加用户错误日志功能, 禁用Redis缓存, userId自动生成5位随机字符
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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...'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '例如:高质量电影级画面,专业摄影,电影色调...'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
<div class="nav-item storyboard-item" @click="goToStoryboardVideo">
|
||||
<el-icon><Film /></el-icon>
|
||||
<span>分镜视频</span>
|
||||
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
<div class="nav-item" @click="goToStoryboardVideo">
|
||||
<el-icon><Film /></el-icon>
|
||||
<span>{{ t('works.storyboardVideo') }}</span>
|
||||
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -133,7 +132,7 @@
|
||||
</div>
|
||||
|
||||
<div class="select-row">
|
||||
<el-checkbox v-model="multiSelect" size="small">{{ t('works.selectItems', { count: selectedIds.size || 6 }) }}</el-checkbox>
|
||||
<el-checkbox :model-value="isAllSelected" @change="toggleSelectAll" size="small">{{ t('works.selectAll') }}</el-checkbox>
|
||||
<template v-if="multiSelect && selectedIds.size">
|
||||
<el-tag type="success" size="small">{{ t('works.selectedCount', { count: selectedIds.size }) }}</el-tag>
|
||||
<el-button size="small" type="primary" @click="bulkDownload" plain>{{ t('video.download') }}</el-button>
|
||||
@@ -227,6 +226,7 @@
|
||||
<template #footer>
|
||||
<el-space size="small">
|
||||
<el-button text size="small" @click.stop="download(item)">{{ t('video.download') }}</el-button>
|
||||
<el-button text size="small" type="danger" @click.stop="handleDeleteWork(item)">{{ t('common.delete') || '删除' }}</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</el-card>
|
||||
@@ -489,7 +489,7 @@ const isVerticalVideo = computed(() => {
|
||||
})
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const pageSize = ref(100)
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const items = ref([])
|
||||
@@ -663,12 +663,10 @@ const loadList = async () => {
|
||||
const filteredItems = computed(() => {
|
||||
let filtered = [...items.value]
|
||||
|
||||
// 过滤掉加载失败的作品(URL失效的作品自动隐藏)
|
||||
// 过滤掉加载失败的作品
|
||||
filtered = filtered.filter(item => {
|
||||
// 检查 resultUrl 和 cover 是否在失败集合中
|
||||
const resultUrlFailed = item.resultUrl && failedUrls.value.has(item.resultUrl)
|
||||
const coverFailed = item.cover && failedUrls.value.has(item.cover)
|
||||
// 如果任一URL失败,则不显示该作品
|
||||
return !resultUrlFailed && !coverFailed
|
||||
})
|
||||
|
||||
@@ -1054,7 +1052,7 @@ const download = async (item) => {
|
||||
|
||||
// 备用方案:使用后端代理
|
||||
const downloadUrl = getWorkFileUrl(item.id, true)
|
||||
const token = sessionStorage.getItem('token')
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
console.log('开始下载:', downloadUrl)
|
||||
|
||||
@@ -1151,6 +1149,36 @@ const moreCommand = async (cmd, item) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单个作品
|
||||
const handleDeleteWork = async (item) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('works.deleteWorkConfirm') || `确定要删除作品"${item.title}"吗?`,
|
||||
t('works.deleteConfirmTitle') || '删除确认',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: t('common.delete') || '删除',
|
||||
cancelButtonText: t('common.cancel') || '取消'
|
||||
}
|
||||
)
|
||||
|
||||
// 执行删除
|
||||
const response = await deleteWork(item.id)
|
||||
if (response.data.success) {
|
||||
ElMessage.success(t('works.deleteSuccess') || '删除成功')
|
||||
// 从列表中移除
|
||||
items.value = items.value.filter(i => i.id !== item.id)
|
||||
} else {
|
||||
throw new Error(response.data.message || t('works.deleteFailed') || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除作品失败:', error)
|
||||
ElMessage.error(error.message || t('works.deleteFailed') || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelect = (id) => {
|
||||
const next = new Set(selectedIds.value)
|
||||
if (next.has(id)) next.delete(id)
|
||||
@@ -1158,6 +1186,24 @@ const toggleSelect = (id) => {
|
||||
selectedIds.value = next
|
||||
}
|
||||
|
||||
// 全选/取消全选
|
||||
const isAllSelected = computed(() => {
|
||||
if (filteredItems.value.length === 0) return false
|
||||
return filteredItems.value.every(item => selectedIds.value.has(item.id))
|
||||
})
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (isAllSelected.value) {
|
||||
// 取消全选
|
||||
selectedIds.value = new Set()
|
||||
multiSelect.value = false
|
||||
} else {
|
||||
// 全选
|
||||
multiSelect.value = true
|
||||
selectedIds.value = new Set(filteredItems.value.map(item => item.id))
|
||||
}
|
||||
}
|
||||
|
||||
const bulkDownload = async () => {
|
||||
if (selectedIds.value.size === 0) {
|
||||
ElMessage.warning(t('works.noItemsSelected'))
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
<div class="nav-item">
|
||||
<el-icon><Film /></el-icon>
|
||||
<span @click="goToStoryboardVideo">{{ t('home.storyboardVideo') }}</span>
|
||||
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -106,16 +105,22 @@
|
||||
muted
|
||||
preload="metadata"
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@error="onVideoError($event, video)"
|
||||
></video>
|
||||
<!-- 如果有封面图(thumbnailUrl),使用图片 -->
|
||||
<!-- 如果有封面图,使用图片 -->
|
||||
<img
|
||||
v-else-if="video.cover && video.cover !== video.resultUrl"
|
||||
v-else-if="video.cover"
|
||||
:src="video.cover"
|
||||
:alt="video.title"
|
||||
class="video-cover-img"
|
||||
@error="onImageError"
|
||||
/>
|
||||
<!-- 否则使用占位符 -->
|
||||
<div v-else class="figure"></div>
|
||||
<!-- 否则使用视频图标占位符 -->
|
||||
<div v-else class="video-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="rgba(255,255,255,0.5)">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-action">
|
||||
<el-button v-if="index === 0 && video.status === 'COMPLETED'" type="primary" size="small" @click.stop="createSimilar(video)">{{ t('profile.createSimilar') }}</el-button>
|
||||
@@ -549,8 +554,8 @@ const loadUserInfo = async () => {
|
||||
console.log('设置后的用户信息:', userInfo.value)
|
||||
|
||||
// 检查用户是否需要设置密码(只有数据库中真正没有密码时才弹窗,且只弹一次)
|
||||
const needSetPasswordFlag = sessionStorage.getItem('needSetPassword') === '1'
|
||||
const hasShownPasswordDialog = sessionStorage.getItem('hasShownPasswordDialog') === '1'
|
||||
const needSetPasswordFlag = localStorage.getItem('needSetPassword') === '1'
|
||||
const hasShownPasswordDialog = localStorage.getItem('hasShownPasswordDialog') === '1'
|
||||
|
||||
// 检查后端返回的用户密码状态
|
||||
const hasNoPassword = !user.passwordHash || String(user.passwordHash).trim() === ''
|
||||
@@ -563,12 +568,12 @@ const loadUserInfo = async () => {
|
||||
console.log('检测到用户没有设置密码,弹出修改密码弹窗')
|
||||
openChangePasswordDialog()
|
||||
// 清除标记,确保只弹一次
|
||||
sessionStorage.removeItem('needSetPassword')
|
||||
sessionStorage.setItem('hasShownPasswordDialog', '1')
|
||||
localStorage.removeItem('needSetPassword')
|
||||
localStorage.setItem('hasShownPasswordDialog', '1')
|
||||
} else if (needSetPasswordFlag && !hasNoPassword) {
|
||||
// 如果有标记但用户已经有密码了,清除标记
|
||||
console.log('用户已有密码,清除 needSetPassword 标记')
|
||||
sessionStorage.removeItem('needSetPassword')
|
||||
localStorage.removeItem('needSetPassword')
|
||||
}
|
||||
} else {
|
||||
console.error('获取用户信息失败:', response?.data?.message || '未知错误')
|
||||
@@ -640,6 +645,19 @@ const onVideoLoaded = (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 视频加载失败处理
|
||||
const onVideoError = (event, video) => {
|
||||
console.warn('视频加载失败:', video.resultUrl)
|
||||
// 隐藏video元素,显示占位符
|
||||
event.target.style.display = 'none'
|
||||
}
|
||||
|
||||
// 图片加载失败处理
|
||||
const onImageError = (event) => {
|
||||
console.warn('图片加载失败:', event.target.src)
|
||||
event.target.style.display = 'none'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
loadUserInfo()
|
||||
@@ -1062,6 +1080,15 @@ onUnmounted(() => {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
}
|
||||
|
||||
.figure {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
@@ -139,7 +139,7 @@ const handleSubmit = async () => {
|
||||
ElMessage.success('密码设置成功')
|
||||
|
||||
// 清除首次设置标记
|
||||
sessionStorage.removeItem('needSetPassword')
|
||||
localStorage.removeItem('needSetPassword')
|
||||
|
||||
// 跳转到首页或之前的页面
|
||||
const redirect = route.query.redirect || '/'
|
||||
@@ -159,7 +159,7 @@ const handleSubmit = async () => {
|
||||
// 跳过
|
||||
const handleSkip = () => {
|
||||
// 清除首次设置标记
|
||||
sessionStorage.removeItem('needSetPassword')
|
||||
localStorage.removeItem('needSetPassword')
|
||||
|
||||
// 跳转到首页
|
||||
const redirect = route.query.redirect || '/'
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
<div class="nav-item storyboard-item" @click="goToStoryboardVideoCreate">
|
||||
<el-icon><Film /></el-icon>
|
||||
<span>分镜视频</span>
|
||||
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -41,7 +41,6 @@
|
||||
<div class="nav-item storyboard-item" @click="goToStoryboardVideo">
|
||||
<el-icon><Film /></el-icon>
|
||||
<span>{{ $t('home.storyboardVideo') }}</span>
|
||||
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -409,7 +408,7 @@ const loadUserSubscriptionInfo = async () => {
|
||||
}
|
||||
|
||||
// 检查token是否存在
|
||||
const token = userStore.token || sessionStorage.getItem('token')
|
||||
const token = userStore.token || localStorage.getItem('token')
|
||||
if (!token || token === 'null' || token.trim() === '') {
|
||||
console.warn('未找到有效的token,跳转到登录页')
|
||||
ElMessage.warning(t('subscription.pleaseLogin'))
|
||||
|
||||
@@ -268,16 +268,6 @@
|
||||
</el-input>
|
||||
<div class="model-tip">{{ $t('systemSettings.storyboardSystemPromptTip') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('systemSettings.promptOptimizationSystemPrompt')">
|
||||
<el-input
|
||||
v-model="promptOptimizationSystemPrompt"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
style="width: 500px;"
|
||||
:placeholder="$t('systemSettings.promptOptimizationSystemPromptPlaceholder')">
|
||||
</el-input>
|
||||
<div class="model-tip">{{ $t('systemSettings.promptOptimizationSystemPromptTip') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -510,7 +500,6 @@ const cleanupConfig = reactive({
|
||||
const promptOptimizationModel = ref('gpt-5.1-thinking')
|
||||
const promptOptimizationApiUrl = ref('https://ai.comfly.chat')
|
||||
const storyboardSystemPrompt = ref('')
|
||||
const promptOptimizationSystemPrompt = ref('')
|
||||
const savingAiModel = ref(false)
|
||||
|
||||
const goToDashboard = () => {
|
||||
@@ -664,23 +653,24 @@ const loadMembershipLevels = async () => {
|
||||
|
||||
// 任务清理相关方法
|
||||
const getAuthHeaders = () => {
|
||||
const token = sessionStorage.getItem('token')
|
||||
const token = localStorage.getItem('token')
|
||||
return token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
const refreshStats = async () => {
|
||||
const refreshStats = async (showMessage = true) => {
|
||||
loadingStats.value = true
|
||||
try {
|
||||
const response = await cleanupApi.getCleanupStats()
|
||||
cleanupStats.value = response.data
|
||||
ElMessage.success(t('systemSettings.statsRefreshSuccess'))
|
||||
if (showMessage) {
|
||||
ElMessage.success(t('systemSettings.statsRefreshSuccess'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Get statistics failed:', error)
|
||||
ElMessage.error(t('systemSettings.statsRefreshFailed'))
|
||||
} finally {
|
||||
loadingStats.value = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const performFullCleanup = async () => {
|
||||
@@ -779,7 +769,7 @@ const loadAiModelSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${sessionStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
if (response.ok) {
|
||||
@@ -793,9 +783,6 @@ const loadAiModelSettings = async () => {
|
||||
if (data.storyboardSystemPrompt !== undefined) {
|
||||
storyboardSystemPrompt.value = data.storyboardSystemPrompt
|
||||
}
|
||||
if (data.promptOptimizationSystemPrompt !== undefined) {
|
||||
promptOptimizationSystemPrompt.value = data.promptOptimizationSystemPrompt
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load AI model settings failed:', error)
|
||||
@@ -810,13 +797,12 @@ const saveAiModelSettings = async () => {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${sessionStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
promptOptimizationModel: promptOptimizationModel.value,
|
||||
promptOptimizationApiUrl: promptOptimizationApiUrl.value,
|
||||
storyboardSystemPrompt: storyboardSystemPrompt.value,
|
||||
promptOptimizationSystemPrompt: promptOptimizationSystemPrompt.value
|
||||
storyboardSystemPrompt: storyboardSystemPrompt.value
|
||||
})
|
||||
})
|
||||
if (response.ok) {
|
||||
@@ -834,7 +820,7 @@ const saveAiModelSettings = async () => {
|
||||
|
||||
// 页面加载时获取统计信息和会员等级配置
|
||||
onMounted(() => {
|
||||
refreshStats()
|
||||
refreshStats(false) // 初始加载不显示成功提示
|
||||
loadMembershipLevels()
|
||||
fetchSystemStats()
|
||||
loadAiModelSettings()
|
||||
@@ -845,7 +831,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()
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
<div class="nav-item storyboard-item" @click="goToStoryboardVideo">
|
||||
<el-icon><Film /></el-icon>
|
||||
<span>分镜视频</span>
|
||||
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
@@ -1283,7 +1283,7 @@ onUnmounted(() => {
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 400px 1fr;
|
||||
grid-template-columns: 520px 1fr;
|
||||
gap: 0;
|
||||
height: calc(100vh - 100px);
|
||||
}
|
||||
@@ -1302,33 +1302,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;
|
||||
}
|
||||
|
||||
/* 文本输入区域 */
|
||||
@@ -1607,7 +1612,7 @@ onUnmounted(() => {
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.main-content {
|
||||
grid-template-columns: 350px 1fr;
|
||||
grid-template-columns: 420px 1fr;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
|
||||
Reference in New Issue
Block a user