feat: 系统功能更新 - 添加错误统计、数据初始化、订单调度等功能
This commit is contained in:
@@ -42,6 +42,10 @@
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>{{ t('profile.systemSettings') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToErrorStats">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>错误统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 修改密码(所有登录用户可见) -->
|
||||
<div class="menu-item" @click.stop="goToChangePassword" style="cursor: pointer;">
|
||||
@@ -166,8 +170,9 @@
|
||||
rows="6"
|
||||
></textarea>
|
||||
<div class="input-tips">
|
||||
<div class="tip-item warning">{{ t('video.storyboard.tipWarning') }}</div>
|
||||
<div class="tip-item">{{ t('video.storyboard.tip1') }}</div>
|
||||
<div class="tip-item">{{ t('video.storyboard.tip2') }}</div>
|
||||
<div class="tip-item points">💎 {{ t('video.storyboard.imageCost') }}</div>
|
||||
</div>
|
||||
<div class="optimize-btn">
|
||||
<button type="button" class="optimize-button" @click="handleStep1ButtonClick" :disabled="!inputText.trim() || inProgress">
|
||||
@@ -249,6 +254,7 @@
|
||||
<div class="input-tips">
|
||||
<div class="tip-item">{{ t('video.storyboard.videoTip1') }}</div>
|
||||
<div class="tip-item">{{ t('video.storyboard.videoTip2') }}</div>
|
||||
<div class="tip-item points">💎 {{ t('video.storyboard.videoCost') }}</div>
|
||||
</div>
|
||||
<div class="optimize-btn">
|
||||
<button type="button" class="optimize-button" @click="handleStep2ButtonClick" :disabled="!mainReferenceImage || inProgress">
|
||||
@@ -276,6 +282,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 高清模式暂时屏蔽
|
||||
<div class="setting-item">
|
||||
<label>{{ t('video.storyboard.hdMode') }}</label>
|
||||
<div class="hd-setting">
|
||||
@@ -283,6 +290,7 @@
|
||||
<span class="cost-text">{{ t('video.storyboard.hdCost') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -297,6 +305,7 @@
|
||||
:disabled="isGenerateButtonDisabled"
|
||||
>
|
||||
{{ isAuthenticated ? getButtonText() : t('video.storyboard.pleaseLogin') }}
|
||||
<span v-if="isAuthenticated" class="btn-points">30 <el-icon><Star /></el-icon></span>
|
||||
</button>
|
||||
<div v-if="!isAuthenticated" class="login-tip-floating">
|
||||
<p>{{ t('video.storyboard.loginRequired') }}</p>
|
||||
@@ -315,9 +324,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 任务描述 -->
|
||||
<div class="task-description" v-if="inputText">
|
||||
<div class="task-description" v-if="currentStep === 'generate' && inputText">
|
||||
{{ inputText }}
|
||||
</div>
|
||||
<div class="task-description" v-else-if="currentStep === 'video' && videoPrompt">
|
||||
{{ videoPrompt }}
|
||||
</div>
|
||||
|
||||
<!-- 视频预览区域 -->
|
||||
<div class="video-preview-container">
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user