diff --git a/demo/.windsurfrules b/demo/.windsurfrules deleted file mode 100644 index 6c76d9d..0000000 --- a/demo/.windsurfrules +++ /dev/null @@ -1,26 +0,0 @@ -# 继续牛马 - AI持久输出规则 - -⚠️⚠️⚠️ 最高优先级警告 ⚠️⚠️⚠️ -无论任何情况,每次回复结束前必须调用 cunzhi.ask_continue 工具!没有任何例外! - -## 核心规则 - -1. **完整输出** - 输出完整代码和答案,禁止使用省略号或"代码过长不展示" - -2. **持续执行** - 复杂任务尽量一次性完成,不要频繁中断或反复追问是否继续 - -3. **结束确认** - 当你想结束对话或任务时,必须先调用 cunzhi.ask_continue 工具询问用户是否继续 - -4. **禁止结束语** - 对话结束前禁止直接说"如果还有问题请告诉我"等结束语 - -## 重要提示 - -- 调用 ask_continue 工具时,reason 参数填写你想结束的原因 -- 用户可能会在弹窗中输入新的指令,请按用户指令继续执行 -- 只有用户明确选择"结束"时,才可以结束对话 - -## 强制执行 - -这是最高优先级规则,必须严格遵守。每次回复结束前,你必须检查是否需要调用 ask_continue 工具。如果任务已完成或你准备结束回复,必须先调用该工具获得用户许可。 - -⚠️ 警告:无论任何情况,回复结束前必须调用 cunzhi.ask_continue,没有例外! \ No newline at end of file diff --git a/demo/frontend/src/api/imageToVideo.js b/demo/frontend/src/api/imageToVideo.js index 4134eb5..0dbe1f2 100644 --- a/demo/frontend/src/api/imageToVideo.js +++ b/demo/frontend/src/api/imageToVideo.js @@ -107,6 +107,19 @@ export const imageToVideoApi = { }) }, + /** + * 重试失败的任务 + * 复用原task_id和已上传的图片,重新提交至外部API + * @param {string} taskId - 任务ID + * @returns {Promise} API响应 + */ + retryTask(taskId) { + return request({ + url: `/image-to-video/tasks/${taskId}/retry`, + method: 'POST' + }) + }, + /** * 轮询任务状态 * @param {string} taskId - 任务ID diff --git a/demo/frontend/src/api/request.js b/demo/frontend/src/api/request.js index 1c56d94..5c60019 100644 --- a/demo/frontend/src/api/request.js +++ b/demo/frontend/src/api/request.js @@ -41,9 +41,10 @@ api.interceptors.request.use( const token = localStorage.getItem('token') if (token && token !== 'null' && token.trim() !== '') { config.headers.Authorization = `Bearer ${token}` - console.log('请求拦截器:添加Authorization头,token长度:', token.length) + // 打印token前30字符用于调试 + console.log('请求拦截器:添加Authorization头,token前30字符:', token.substring(0, 30), '请求URL:', config.url) } else { - console.warn('请求拦截器:未找到有效的token') + console.warn('请求拦截器:未找到有效的token,请求URL:', config.url) } } else { console.log('请求拦截器:登录相关请求,不添加token:', config.url) @@ -69,34 +70,47 @@ api.interceptors.response.use( const isLoginRequest = loginUrls.some(url => response.config.url.includes(url)) if (!isLoginRequest) { - // 清除无效的token并跳转到登录页 + // 清除无效的token并跳转到欢迎页 localStorage.removeItem('token') localStorage.removeItem('user') // 避免重复跳转 - if (router.currentRoute.value.path !== '/login') { - ElMessage.error('认证失败,请重新登录') - router.push('/login') + if (router.currentRoute.value.path !== '/' && router.currentRoute.value.path !== '/login') { + ElMessage.warning('登录已过期,请重新登录') + router.push('/') } } // 返回错误,让调用方知道这是认证失败 return Promise.reject(new Error('认证失败:收到HTML响应')) } - // 检查302重定向 - if (response.status === 302) { - console.error('收到302重定向,可能是认证失败:', response.config.url) + // 检查401未授权(Token过期) + if (response.status === 401) { + console.error('收到401,Token已过期:', response.config.url) - // 只有非登录请求才清除token并跳转 const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/'] const isLoginRequest = loginUrls.some(url => response.config.url.includes(url)) if (!isLoginRequest) { localStorage.removeItem('token') localStorage.removeItem('user') - if (router.currentRoute.value.path !== '/login') { - ElMessage.error('认证失败,请重新登录') - router.push('/login') - } + ElMessage.warning('登录已过期,请重新登录') + router.push('/') + } + return Promise.reject(new Error('认证失败:Token已过期')) + } + + // 检查302重定向 + if (response.status === 302) { + console.error('收到302重定向,可能是认证失败:', response.config.url) + + const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/'] + const isLoginRequest = loginUrls.some(url => response.config.url.includes(url)) + + if (!isLoginRequest) { + localStorage.removeItem('token') + localStorage.removeItem('user') + ElMessage.warning('登录已过期,请重新登录') + router.push('/') } return Promise.reject(new Error('认证失败:302重定向')) } @@ -119,9 +133,9 @@ api.interceptors.response.use( if (!isLoginRequest) { localStorage.removeItem('token') localStorage.removeItem('user') - if (router.currentRoute.value.path !== '/login') { - ElMessage.error('认证失败,请重新登录') - router.push('/login') + if (router.currentRoute.value.path !== '/' && router.currentRoute.value.path !== '/login') { + ElMessage.warning('登录已过期,请重新登录') + router.push('/') } } return Promise.reject(error) @@ -138,9 +152,10 @@ api.interceptors.response.use( // 302也可能是认证失败导致的 localStorage.removeItem('token') localStorage.removeItem('user') - if (router.currentRoute.value.path !== '/login') { - ElMessage.error('认证失败,请重新登录') - router.push('/login') + // 跳转到欢迎页 + if (router.currentRoute.value.path !== '/' && router.currentRoute.value.path !== '/login') { + ElMessage.warning('登录已过期,请重新登录') + router.push('/') } } break diff --git a/demo/frontend/src/api/textToVideo.js b/demo/frontend/src/api/textToVideo.js index e60dc64..d0cdad8 100644 --- a/demo/frontend/src/api/textToVideo.js +++ b/demo/frontend/src/api/textToVideo.js @@ -81,6 +81,18 @@ export const textToVideoApi = { }) }, + /** + * 重试失败的任务 + * 复用原task_id,重新提交至外部API + * @param {string} taskId - 任务ID + * @returns {Promise} API响应 + */ + retryTask(taskId) { + return request({ + url: `/text-to-video/tasks/${taskId}/retry`, + method: 'POST' + }) + }, /** * 轮询任务状态 diff --git a/demo/frontend/src/locales/en.js b/demo/frontend/src/locales/en.js index 47e05e0..d06980a 100644 --- a/demo/frontend/src/locales/en.js +++ b/demo/frontend/src/locales/en.js @@ -230,6 +230,8 @@ export default { userAvatar: 'User Avatar', firstFrame: 'First Frame', promptPlaceholder: 'Describe the content you want to generate with the image', + tipWarning: '⚠️ Do not upload images of real people or anime IP', + tip1: '🎬 Supports camera movements, character actions, scene transitions, etc.', optimizing: 'Optimizing...', optimizePrompt: 'Optimize', hdMode: 'HD Mode', @@ -324,8 +326,10 @@ export default { uploadSuccess: 'Successfully uploaded {count} images', imageRemoved: 'Image removed', promptPlaceholder: 'Example: a coffee advertisement\n\nTip: Simple description is enough, AI will automatically optimize it into professional storyboard\nSupports Chinese or English input, the system will automatically translate and optimize it into professional storyboard description', - tip1: '💡 AI will automatically generate professional storyboards based on your description', - tip2: '🎬 Supports various scene compositions and camera types', + tipWarning: '⚠️ Do not upload images of real people or anime IP', + tip1: '🎬 Supports camera movements, character actions, scene transitions, etc.', + imageCost: 'Storyboard generation costs 30 points', + videoCost: 'Video generation costs 30 points', videoPromptLabel: 'Video Description', videoPromptPlaceholder: 'Describe actions and camera movements in the video, e.g.: camera slowly zooms in, person turns around and smiles', videoTip1: '💡 Describe actions and camera movements for more vivid video generation', @@ -480,14 +484,14 @@ export default { free: 'Free', standard: 'Standard', professional: 'Professional', - perMonth: '/month', + perMonth: '/year', subscribe: 'Subscribe Now', renew: 'Renew', upgrade: 'Upgrade', features: 'Features', unlimited: 'Unlimited', limited: 'Limited', - pointsPerMonth: 'Points/Month', + pointsPerMonth: 'Points/Year', videoQuality: 'Video Quality', support: 'Support', priorityQueue: 'Priority Queue', @@ -506,8 +510,8 @@ export default { currentPackage: 'Current Plan', firstPurchaseDiscount: 'First Purchase Discount up to 15% off', bestValue: 'Best Value', - standardPoints: '200 points per month', - premiumPoints: '1000 points per month', + standardPoints: '6000 points/year', + premiumPoints: '12000 points/year', freeNewUserBonus: 'New users get 50 points free on first login', fastGeneration: 'Fast Generation', superFastGeneration: 'Super Fast Generation', diff --git a/demo/frontend/src/locales/zh.js b/demo/frontend/src/locales/zh.js index 7097283..b87206e 100644 --- a/demo/frontend/src/locales/zh.js +++ b/demo/frontend/src/locales/zh.js @@ -232,6 +232,8 @@ export default { userAvatar: '用户头像', firstFrame: '首帧', promptPlaceholder: '结合图片,描述想要生成的内容', + tipWarning: '⚠️ 图片不能上传真人、涉及动漫IP等类型的图片', + tip1: '🎬 支持描述镜头推拉、人物动作、场景变化等', optimizing: '优化中...', optimizePrompt: '一键优化', hdMode: '高清模式', @@ -327,11 +329,13 @@ export default { uploadSuccess: '成功上传 {count} 张图片', imageRemoved: '已删除图片', promptPlaceholder: '例如:一个咖啡的广告\n\n提示:简单描述即可,AI会自动优化成专业的分镜图\n支持中文或英文输入,系统会自动翻译并优化为专业的分镜图描述', - tip1: '💡 AI会根据您的描述自动生成专业分镜图', - tip2: '🎬 支持多种画面构图和镜头类型描述', + tipWarning: '⚠️ 图片不能上传真人、涉及动漫IP等类型的图片', + tip1: '🎬 支持描述镜头推拉、人物动作、场景变化等', + imageCost: '生成分镜图消耗30积分', + videoCost: '生成视频消耗30积分', videoPromptLabel: '视频描述', videoPromptPlaceholder: '描述视频中的动作、镜头运动等,例如:镜头缓慢推进,人物转身微笑', - videoTip1: '💡 描述画面中的动作和镜头运动,AI将生成更生动的视频', + videoTip1: '⚠️ 图片不能上传真人、涉及动漫IP等类型的图片', videoTip2: '🎬 支持描述镜头推拉、人物动作、场景变化等', storyboardReadyHint: '分镜图已准备就绪,输入视频描述后点击生成视频', optimizing: '优化中...', @@ -494,14 +498,14 @@ export default { free: '免费版', standard: '标准版', professional: '专业版', - perMonth: '/月', + perMonth: '/年', subscribe: '立即订阅', renew: '续费', upgrade: '升级', features: '功能特性', unlimited: '无限', limited: '有限', - pointsPerMonth: '积分/月', + pointsPerMonth: '积分/年', videoQuality: '视频质量', support: '客服支持', priorityQueue: '优先队列', @@ -520,8 +524,8 @@ export default { currentPackage: '当前套餐', firstPurchaseDiscount: '首购低至8.5折', bestValue: '超值之选', - standardPoints: '每月200积分', - premiumPoints: '每月1000积分', + standardPoints: '6000积分/年', + premiumPoints: '12000积分/年', freeNewUserBonus: '新用户首次登陆免费获得50积分', fastGeneration: '快速通道生成', superFastGeneration: '极速通道生成', @@ -657,6 +661,7 @@ export default { cancelled: '已取消', refunded: '已退款', unpaid: '未支付', + allPaymentMethods: '全部支付方式', alipay: '支付宝', wechat: '微信支付', paypal: 'PayPal', @@ -780,6 +785,12 @@ export default { promptOptimizationModelTip: '输入用于优化提示词的模型名称,如 gpt-4o、gemini-pro 等', storyboardSystemPrompt: '分镜图系统引导词', storyboardSystemPromptTip: '此引导词会自动添加到用户提示词前面,用于统一分镜图生成风格', - storyboardSystemPromptPlaceholder: '例如:高质量电影级画面,专业摄影,电影色调...' + storyboardSystemPromptPlaceholder: '例如:高质量电影级画面,专业摄影,电影色调...', + membershipUpdateSuccess: '会员等级配置更新成功', + membershipUpdateFailed: '会员等级配置更新失败', + loadMembershipFailed: '加载会员配置失败', + usingDefaultConfig: '使用默认配置', + enterValidNumber: '请输入有效的数字', + unknown: '未知错误' } } diff --git a/demo/frontend/src/router/index.js b/demo/frontend/src/router/index.js index 5348998..fa16758 100644 --- a/demo/frontend/src/router/index.js +++ b/demo/frontend/src/router/index.js @@ -190,6 +190,12 @@ const routes = [ component: () => import('@/views/ApiManagement.vue'), meta: { title: 'API管理', requiresAuth: true, requiresAdmin: true } }, + { + path: '/admin/error-statistics', + name: 'ErrorStatistics', + component: () => import('@/views/ErrorStatistics.vue'), + meta: { title: '错误统计', requiresAuth: true, requiresAdmin: true } + }, { path: '/hello', name: 'HelloWorld', @@ -246,6 +252,13 @@ router.beforeEach(async (to, from, next) => { try { const userStore = useUserStore() + // 检查localStorage中的token是否被清除(例如JWT过期后被request.js清除) + // 如果token被清除但store中仍有用户信息,则同步清除store + const storedToken = localStorage.getItem('token') + if (!storedToken && userStore.isAuthenticated) { + userStore.clearUserData() + } + // 优化:只在首次访问时初始化用户状态 if (!userStore.initialized) { await userStore.init() diff --git a/demo/frontend/src/stores/user.js b/demo/frontend/src/stores/user.js index 3214257..bfe2cd5 100644 --- a/demo/frontend/src/stores/user.js +++ b/demo/frontend/src/stores/user.js @@ -128,19 +128,18 @@ export const useUserStore = defineStore('user', () => { return } - // 从localStorage恢复用户状态 + // 从 localStorage 恢复用户状态 const savedToken = localStorage.getItem('token') const savedUser = localStorage.getItem('user') + console.log('Store init - savedToken:', savedToken ? savedToken.substring(0, 30) + '...' : 'null') + if (savedToken && savedUser) { try { token.value = savedToken user.value = JSON.parse(savedUser) - // 只在开发环境输出详细日志 - if (process.env.NODE_ENV === 'development') { - console.log('恢复用户状态:', user.value?.username, '角色:', user.value?.role) - } + console.log('恢复用户状态:', user.value?.username) // 刷新用户信息(确保角色等信息是最新的) await fetchCurrentUser() @@ -153,6 +152,11 @@ export const useUserStore = defineStore('user', () => { initialized.value = true } + + // 重置初始化状态(登录成功后调用) + const resetInitialized = () => { + initialized.value = false + } return { // 状态 @@ -172,6 +176,7 @@ export const useUserStore = defineStore('user', () => { fetchCurrentUser, clearUserData, init, - initialized + initialized, + resetInitialized } }) diff --git a/demo/frontend/src/views/AdminDashboard.vue b/demo/frontend/src/views/AdminDashboard.vue index c4006d9..72e2bcc 100644 --- a/demo/frontend/src/views/AdminDashboard.vue +++ b/demo/frontend/src/views/AdminDashboard.vue @@ -27,6 +27,10 @@ {{ $t('nav.tasks') }} + +
{{ $t('orders.status') }}: - + + + + + + + + {{ getStatusText(currentOrderDetail.status) }}
@@ -300,9 +317,10 @@ import { Delete, CreditCard, Wallet, - Loading + Loading, + Warning } from '@element-plus/icons-vue' -import { getOrders, getAdminOrders, getOrderStats, deleteOrder as deleteOrderAPI, deleteOrders } from '@/api/orders' +import { getOrders, getAdminOrders, getOrderStats, deleteOrder as deleteOrderAPI, deleteOrders, updateOrderStatus } from '@/api/orders' import LanguageSwitcher from '@/components/LanguageSwitcher.vue' const router = useRouter() @@ -348,6 +366,10 @@ const goToTasks = () => { router.push('/generate-task-record') } +const goToErrorStats = () => { + router.push('/admin/error-statistics') +} + const goToSettings = () => { router.push('/system-settings') } @@ -502,12 +524,40 @@ const goToPage = (page) => { // 订单详情弹窗相关 const orderDetailVisible = ref(false) const currentOrderDetail = ref(null) +const editingStatus = ref('') const viewOrder = async (order) => { currentOrderDetail.value = order + editingStatus.value = order.status || 'PENDING' orderDetailVisible.value = true } +// 修改订单状态 +const handleStatusChange = async (newStatus) => { + if (!currentOrderDetail.value) return + + try { + const response = await updateOrderStatus(currentOrderDetail.value.id, newStatus) + if (response.data?.success) { + // 更新当前详情 + currentOrderDetail.value.status = newStatus + // 更新列表中的订单 + const orderIndex = orders.value.findIndex(o => o.id === currentOrderDetail.value.id) + if (orderIndex > -1) { + orders.value[orderIndex].status = newStatus + } + ElMessage.success('订单状态更新成功') + } else { + ElMessage.error(response.data?.message || '更新失败') + editingStatus.value = currentOrderDetail.value.status // 恢复原状态 + } + } catch (error) { + console.error('更新订单状态失败:', error) + ElMessage.error('更新订单状态失败') + editingStatus.value = currentOrderDetail.value.status // 恢复原状态 + } +} + const deleteOrder = async (order) => { try { await ElMessageBox.confirm( @@ -1010,6 +1060,10 @@ const fetchSystemStats = async () => { background: #6366f1; } +.status-tag.refunded { + background: #f97316; +} + .action-link { margin-right: 12px; font-size: 14px; diff --git a/demo/frontend/src/views/ApiManagement.vue b/demo/frontend/src/views/ApiManagement.vue index e6e8d80..5617a99 100644 --- a/demo/frontend/src/views/ApiManagement.vue +++ b/demo/frontend/src/views/ApiManagement.vue @@ -26,6 +26,10 @@ {{ $t('nav.tasks') }} + + @@ -100,6 +106,7 @@ :disabled="!isAuthenticated || isCreatingTask" > {{ !isAuthenticated ? t('video.imageToVideo.pleaseLogin') : isCreatingTask ? t('video.imageToVideo.creatingTask') : t('video.imageToVideo.startGenerate') }} + 30

{{ t('video.imageToVideo.loginRequired') }}

@@ -237,7 +244,7 @@
{{ task.prompt || t('video.imageToVideo.noDescription') }}
-
+
{{ task.status === 'PENDING' ? t('video.imageToVideo.queuing') : t('video.generating') }}
@@ -315,6 +322,10 @@ {{ t('profile.systemSettings') }}
+ + @@ -405,6 +408,10 @@ {{ t('profile.systemSettings') }}
+ + + { method: 'post', data: { oldPassword: null, - newPassword: form.newPassword + newPassword: form.newPassword, + isFirstTimeSetup: true } }) @@ -286,6 +288,13 @@ onMounted(() => { text-align: left; } +.input-hint { + color: rgba(255, 255, 255, 0.5); + font-size: 12px; + margin-top: 6px; + text-align: left; +} + /* 确定按钮 */ .submit-button { width: 100%; diff --git a/demo/frontend/src/views/StoryboardVideo.vue b/demo/frontend/src/views/StoryboardVideo.vue index 162b054..993e778 100644 --- a/demo/frontend/src/views/StoryboardVideo.vue +++ b/demo/frontend/src/views/StoryboardVideo.vue @@ -22,14 +22,17 @@ @@ -791,6 +794,24 @@ onMounted(() => { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); } + +/* Sora2.0 SVG 风格标签 */ +.badge-pro, .badge-max { + font-size: 9px; + padding: 0 3px; + border-radius: 2px; + font-weight: 500; + margin-left: 6px; + background: rgba(62, 163, 255, 0.2); + color: #5AE0FF; + flex: 0 0 auto !important; + width: auto !important; +} + +.badge-max { + background: rgba(255, 100, 150, 0.2); + color: #FF7EB3; +} diff --git a/demo/frontend/src/views/StoryboardVideoCreate.vue b/demo/frontend/src/views/StoryboardVideoCreate.vue index 3b26a40..5111a65 100644 --- a/demo/frontend/src/views/StoryboardVideoCreate.vue +++ b/demo/frontend/src/views/StoryboardVideoCreate.vue @@ -42,6 +42,10 @@ {{ t('profile.systemSettings') }}
+
@@ -297,6 +305,7 @@ :disabled="isGenerateButtonDisabled" > {{ isAuthenticated ? getButtonText() : t('video.storyboard.pleaseLogin') }} + 30

{{ t('video.storyboard.loginRequired') }}

@@ -315,9 +324,12 @@
-
+
{{ inputText }}
+
+ {{ videoPrompt }} +
@@ -534,7 +546,7 @@ import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue' import { useRouter, useRoute } from 'vue-router' import { ElMessage, ElMessageBox, ElLoading } from 'element-plus' -import { User, VideoCamera, Star, Setting, SwitchButton, Lock, Document } from '@element-plus/icons-vue' +import { User, VideoCamera, Star, Setting, SwitchButton, Lock, Document, Warning } from '@element-plus/icons-vue' import { useUserStore } from '@/stores/user' import { createStoryboardTask, getStoryboardTask, getUserStoryboardTasks, retryStoryboardTask } from '@/api/storyboardVideo' import { imageToVideoApi } from '@/api/imageToVideo' @@ -556,9 +568,9 @@ const isAuthenticated = computed(() => userStore.isAuthenticated) const inputText = ref('') const videoPrompt = ref('') // 视频生成提示词 const aspectRatio = ref('16:9') -const duration = ref('10') +const duration = ref('15') const hdMode = ref(false) -const imageModel = ref('nano-banana') +const imageModel = ref('nano-banana2') const inProgress = ref(false) const currentStep = ref('generate') // 'generate' 或 'video' @@ -687,6 +699,11 @@ const goToSystemSettings = () => { router.push('/system-settings') } +const goToErrorStats = () => { + showUserMenu.value = false + router.push('/admin/error-statistics') +} + const goToChangePassword = () => { showUserMenu.value = false router.push('/change-password') @@ -801,18 +818,6 @@ const getButtonText = () => { return t('video.generate') } // 第一步(生成分镜图):根据是否有参考图和提示词显示不同文本 - if (hasUploadedImages.value && inputText.value.trim()) { - // 有参考图和提示词:显示"生成分镜图" - return t('video.storyboard.generateStoryboard') - } - if (hasUploadedImages.value) { - // 只有参考图没有提示词:提示输入描述 - return t('video.storyboard.generateStoryboard') - } - if (inputText.value.trim()) { - // 只有提示词没有参考图 - return t('video.storyboard.generateStoryboard') - } return t('video.storyboard.generateStoryboard') } @@ -1422,6 +1427,7 @@ const pollTaskStatus = async (taskId) => { const maxAttempts = 90 // 最大尝试次数(足够覆盖整个流程) let attempts = 0 let currentInterval = 20000 // 初始间隔:20秒(分镜图生成阶段) + let promptWaitAttempts = 0 const poll = async () => { // 先检查是否超过最大尝试次数 @@ -1575,22 +1581,31 @@ const pollTaskStatus = async (taskId) => { // 如果进度 < 100,说明只是分镜图完成,视频还没生成 // 注意:如果正在生成视频(inProgress=true),不要停止轮询 if (taskProgress < 100 && !inProgress.value) { - // 检查提示词是否已经获取到(后端异步优化可能稍晚完成) - const hasPrompts = task.imagePrompt || task.videoPrompt - if (!hasPrompts) { - // 提示词还没就绪,继续轮询(缩短间隔) - console.log('[轮询] 分镜图已生成但提示词未就绪,继续轮询') - currentStep.value = 'video' - pollIntervalId.value = setTimeout(poll, 5000) // 5秒后再试 + const imagePromptStr = task.imagePrompt ? String(task.imagePrompt).trim() : '' + const videoPromptStr = task.videoPrompt ? String(task.videoPrompt).trim() : '' + const hasImagePrompt = !!imagePromptStr && imagePromptStr !== 'null' + const hasVideoPrompt = !!videoPromptStr && videoPromptStr !== 'null' + + currentStep.value = 'video' + + // videoPrompt 还没就绪时:先用 imagePrompt 临时填充,继续短间隔轮询等待真正 videoPrompt + if (!hasVideoPrompt) { + if (!videoPrompt.value.trim() && hasImagePrompt) { + videoPrompt.value = imagePromptStr + } + promptWaitAttempts++ + console.log('[轮询] 分镜图已生成但 videoPrompt 未就绪,继续轮询', { promptWaitAttempts }) + pollIntervalId.value = setTimeout(poll, 5000) return } - + + // videoPrompt 已就绪,可以停止轮询并提示 if (pollIntervalId.value) { clearTimeout(pollIntervalId.value) pollIntervalId.value = null } - currentStep.value = 'video' - + promptWaitAttempts = 0 + setTimeout(() => { ElMessage.success(t('video.storyboard.storyboardCompleted')) }, 100) @@ -2719,14 +2734,29 @@ const checkLastTaskStatus = async () => { onMounted(async () => { // 处理"做同款"传递的路由参数 - if (route.query.prompt) { - inputText.value = route.query.prompt + if (route.query.prompt || route.query.referenceImage) { + console.log('[做同款] 接收参数:', route.query) + + if (route.query.prompt) { + inputText.value = route.query.prompt + } if (route.query.aspectRatio) { aspectRatio.value = route.query.aspectRatio } if (route.query.duration) { duration.value = route.query.duration } + + // 处理参考图 + if (route.query.referenceImage) { + uploadedImages.value = [{ + url: route.query.referenceImage, + file: null, + name: '参考图片' + }] + console.log('[做同款] 设置参考图:', route.query.referenceImage) + } + ElMessage.success(t('video.storyboardVideo.historyParamsFilled') || '已填充历史参数') // 清除URL中的query参数,避免刷新页面重复填充 router.replace({ path: route.path }) @@ -3744,6 +3774,11 @@ onBeforeUnmount(() => { font-weight: 500; } +.tip-item.points { + color: #fbbf24; + font-weight: 500; +} + .optimize-btn { display: flex; justify-content: flex-end; @@ -3883,7 +3918,6 @@ onBeforeUnmount(() => { background: #6b7280; cursor: not-allowed; transform: none; - box-shadow: none; opacity: 0.6; } @@ -3892,6 +3926,21 @@ onBeforeUnmount(() => { box-shadow: none; } +.btn-points { + display: inline-flex; + align-items: center; + gap: 4px; + margin-left: 8px; + padding: 2px 8px; + background: rgba(255, 255, 255, 0.2); + border-radius: 12px; + font-size: 14px; +} + +.btn-points .el-icon { + color: #60a5fa; +} + .login-tip-floating { margin-top: 12px; text-align: center; @@ -3963,7 +4012,6 @@ onBeforeUnmount(() => { .preview-content { - flex: 1; background: #1a1a1a; border: 2px solid #2a2a2a; border-radius: 12px; @@ -3971,14 +4019,14 @@ onBeforeUnmount(() => { align-items: center; justify-content: center; transition: all 0.2s ease; - overflow: hidden; /* 防止内容溢出 */ - min-height: 0; /* 允许flex子项缩小 */ - position: relative; /* 为绝对定位的子元素提供定位参考 */ + overflow: hidden; + position: relative; width: 80%; max-width: 1000px; - margin-left: 0; - margin-right: auto; - align-self: flex-start; + min-height: 300px; + padding: 24px; + margin-bottom: 20px; + box-sizing: border-box; } .preview-content:hover { @@ -3987,13 +4035,10 @@ onBeforeUnmount(() => { .preview-placeholder { width: 100%; - min-height: 200px; - background: #1a1a1a; - border-radius: 8px; + height: 100%; display: flex; align-items: center; justify-content: center; - padding: 40px 20px; } .placeholder-text { @@ -4212,15 +4257,15 @@ onBeforeUnmount(() => { /* 任务状态样式 */ .task-status { - background: rgba(255, 255, 255, 0.05); + background: #1a1a1a; + border: 2px solid #2a2a2a; border-radius: 12px; - padding: 20px; + padding: 24px; margin-bottom: 20px; - border: 1px solid rgba(255, 255, 255, 0.1); width: 80%; max-width: 1000px; - margin-left: 0; - margin-right: auto; + min-height: 300px; + box-sizing: border-box; } .status-header { @@ -4243,18 +4288,14 @@ onBeforeUnmount(() => { font-family: monospace; } -/* 任务描述样式 */ +/* 任务描述样式 - 和历史记录提示词一致 */ .task-description { - background: rgba(255, 255, 255, 0.05); - border-radius: 8px; - padding: 16px; - margin: 15px 0; + color: #e5e7eb; font-size: 14px; line-height: 1.6; - color: #e5e7eb; - border: 1px solid rgba(255, 255, 255, 0.1); - max-height: 120px; - overflow-y: auto; + margin-bottom: 16px; + white-space: pre-wrap; + word-wrap: break-word; } /* 视频预览容器 */ @@ -4636,10 +4677,8 @@ onBeforeUnmount(() => { border: none; padding: 0; transition: all 0.2s ease; - width: 80%; - max-width: 1000px; - margin-left: 0; - margin-right: auto; + width: 100%; + max-width: none; } .history-status-checkbox { @@ -4688,10 +4727,13 @@ onBeforeUnmount(() => { margin-bottom: 16px; white-space: pre-wrap; word-wrap: break-word; + width: 80%; + max-width: 1000px; } .history-preview { - width: 100%; + width: 80%; + max-width: 1000px; aspect-ratio: 16/9; background: #000; border-radius: 8px; diff --git a/demo/frontend/src/views/Subscription.vue b/demo/frontend/src/views/Subscription.vue index 3abbcfb..3570c46 100644 --- a/demo/frontend/src/views/Subscription.vue +++ b/demo/frontend/src/views/Subscription.vue @@ -33,14 +33,17 @@ @@ -124,7 +127,7 @@

{{ $t('subscription.free') }}

-
${{ membershipPrices.free }}{{ $t('subscription.perMonth') }}
+
¥{{ membershipPrices.free }}/年
 
@@ -132,6 +135,22 @@ {{ $t('subscription.freeNewUserBonus') }}
+
+ + 文生视频:30积分/条 +
+
+ + 图生视频:30积分/条 +
+
+ + 分镜图生成:30积分/次 +
+
+ + 分镜视频生成:30积分/条 +
@@ -141,7 +160,7 @@

{{ $t('subscription.standard') }}

{{ $t('subscription.firstPurchaseDiscount') }}
-
${{ membershipPrices.standard }}{{ $t('subscription.perMonth') }}
+
¥{{ membershipPrices.standard }}/年
{{ $t('subscription.standardPoints') }}
@@ -153,10 +172,6 @@ {{ $t('subscription.commercialUse') }}
-
- - {{ $t('subscription.noWatermark') }} -
@@ -166,7 +181,7 @@

{{ $t('subscription.professional') }}

{{ $t('subscription.bestValue') }}
-
${{ membershipPrices.premium }}{{ $t('subscription.perMonth') }}
+
¥{{ membershipPrices.premium }}/年
{{ $t('subscription.premiumPoints') }}
@@ -178,10 +193,6 @@ {{ $t('subscription.commercialUse') }}
-
- - {{ $t('subscription.noWatermark') }} -
{{ $t('subscription.earlyAccess') }} @@ -213,6 +224,10 @@ {{ t('profile.systemSettings') }}
+
{{ $t('subscription.currentPoints') }}: - {{ (userInfo.points || 0) - (userInfo.frozenPoints || 0) }} + {{ userStore.availablePoints }}
@@ -297,11 +312,12 @@