feat: 添加任务状态级联触发器,优化支付和做同款功能
主要更新: - 添加 MySQL 触发器实现 task_status 表到其他表的状态级联 - 移除控制器中的多表状态检查代码 - 完善做同款功能,支持参数传递 - 支付宝 USD 转 CNY 汇率转换 - 修复状态枚举映射问题 注意: 触发器仅在 task_status 更新时触发,部分代码仍直接更新业务表
This commit is contained in:
@@ -15,12 +15,47 @@
|
||||
<span class="points-number">{{ userStore.availablePoints }}</span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<div class="user-avatar" @click="toggleUserMenu" ref="userAvatarRef">
|
||||
<img src="/images/backgrounds/avatar-default.svg" :alt="t('video.storyboard.userAvatar')" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 用户菜单下拉 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showUserMenu" class="user-menu-teleport" :style="menuStyle">
|
||||
<!-- 管理员功能 -->
|
||||
<template v-if="userStore.isAdmin">
|
||||
<div class="menu-item" @click.stop="goToDashboard">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>{{ t('profile.dashboard') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToOrders">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('profile.orderManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToMembers">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>{{ t('profile.memberManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToSystemSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>{{ t('profile.systemSettings') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 修改密码(所有登录用户可见) -->
|
||||
<div class="menu-item" @click.stop="goToChangePassword" style="cursor: pointer;">
|
||||
<el-icon><Lock /></el-icon>
|
||||
<span>{{ t('profile.changePassword') }}</span>
|
||||
</div>
|
||||
<!-- 退出登录 -->
|
||||
<div class="menu-item" @click.stop="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
<span>{{ t('common.logout') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 左侧设置面板 -->
|
||||
@@ -175,7 +210,7 @@
|
||||
<button
|
||||
class="generate-btn"
|
||||
@click="handleGenerateClick"
|
||||
:disabled="!isAuthenticated || inProgress || isCreatingTask || (currentStep === 'generate' && !hasUploadedImages && !inputText.trim()) || (currentStep === 'video' && !generatedImageUrl && !hasUploadedImages)"
|
||||
:disabled="!isAuthenticated || isCreatingTask || (currentStep === 'generate' && !hasUploadedImages && !inputText.trim()) || (currentStep === 'video' && !generatedImageUrl && !hasUploadedImages)"
|
||||
>
|
||||
{{ isAuthenticated ? getButtonText() : t('video.storyboard.pleaseLogin') }}
|
||||
</button>
|
||||
@@ -205,10 +240,16 @@
|
||||
<!-- 生成中的状态 -->
|
||||
<div v-if="inProgress" class="generating-container">
|
||||
<div class="generating-placeholder">
|
||||
<div class="generating-text">{{ currentStep === 'generate' ? t('video.storyboard.generatingStoryboardText') : t('video.storyboard.generatingVideoText') }}</div>
|
||||
<div class="progress-bar-large">
|
||||
<div class="progress-fill-large" :style="{ width: (currentStep === 'video' ? videoProgress : 50) + '%' }"></div>
|
||||
<div class="generating-text">{{ taskStatus === 'PENDING' ? t('video.storyboard.queuing') : (currentStep === 'generate' ? t('video.storyboard.generatingStoryboardText') : t('video.storyboard.generatingVideoText')) }}</div>
|
||||
<!-- 排队中:不确定进度条 -->
|
||||
<div v-if="taskStatus === 'PENDING'" class="progress-bar-large indeterminate">
|
||||
<div class="progress-fill-indeterminate"></div>
|
||||
</div>
|
||||
<!-- 生成中:动态进度条 -->
|
||||
<div v-else class="progress-bar-large">
|
||||
<div class="progress-fill-large animated" :style="{ width: (currentStep === 'video' ? videoProgress : 50) + '%' }"></div>
|
||||
</div>
|
||||
<div class="progress-percentage" v-if="taskStatus !== 'PENDING'">{{ currentStep === 'video' ? videoProgress : 50 }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -374,9 +415,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 做同款按钮 -->
|
||||
<!-- 操作按钮 -->
|
||||
<div class="history-actions">
|
||||
<button class="similar-btn" @click="createSimilarFromHistory(task)">{{ t('profile.createSimilar') }}</button>
|
||||
<button
|
||||
v-if="task.status === 'COMPLETED' && task.videoResultUrl"
|
||||
class="download-btn"
|
||||
@click="downloadHistoryVideo(task)"
|
||||
:title="t('video.storyboard.downloadVideo')"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -389,8 +440,9 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
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 { useUserStore } from '@/stores/user'
|
||||
import { createStoryboardTask, getStoryboardTask, getUserStoryboardTasks } from '@/api/storyboardVideo'
|
||||
import { imageToVideoApi } from '@/api/imageToVideo'
|
||||
@@ -402,6 +454,7 @@ import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 计算是否已登录
|
||||
@@ -426,6 +479,23 @@ const pollIntervalId = ref(null) // 保存轮询定时器ID
|
||||
const optimizingPrompt = ref(false) // 优化提示词状态
|
||||
const optimizingVideoPrompt = ref(false) // 优化视频提示词状态
|
||||
|
||||
// 用户菜单相关
|
||||
const showUserMenu = ref(false)
|
||||
const userAvatarRef = ref(null)
|
||||
|
||||
// 计算菜单位置
|
||||
const menuStyle = computed(() => {
|
||||
if (!userAvatarRef.value || !showUserMenu.value) return {}
|
||||
|
||||
const rect = userAvatarRef.value.getBoundingClientRect()
|
||||
return {
|
||||
position: 'fixed',
|
||||
top: `${rect.bottom + 8}px`,
|
||||
right: `${window.innerWidth - rect.right}px`,
|
||||
zIndex: 99999
|
||||
}
|
||||
})
|
||||
|
||||
// 为了兼容性,保留 uploadedImage 作为计算属性(返回第一张图片)
|
||||
const uploadedImage = computed(() => {
|
||||
if (uploadedImages.value.length > 0 && uploadedImages.value[0]?.url) {
|
||||
@@ -472,6 +542,67 @@ const goToImageToVideo = () => {
|
||||
router.push('/image-to-video/create')
|
||||
}
|
||||
|
||||
// 用户菜单相关方法
|
||||
const toggleUserMenu = () => {
|
||||
// 未登录时跳转到登录页面
|
||||
if (!isAuthenticated.value) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
showUserMenu.value = !showUserMenu.value
|
||||
}
|
||||
|
||||
const goToProfile = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
const goToMyWorks = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/works')
|
||||
}
|
||||
|
||||
const goToSubscription = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/subscription')
|
||||
}
|
||||
|
||||
const goToSettings = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/settings')
|
||||
}
|
||||
|
||||
const goToDashboard = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/admin/dashboard')
|
||||
}
|
||||
|
||||
const goToOrders = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/admin/orders')
|
||||
}
|
||||
|
||||
const goToMembers = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/member-management')
|
||||
}
|
||||
|
||||
const goToSystemSettings = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/system-settings')
|
||||
}
|
||||
|
||||
const goToChangePassword = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/change-password')
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
showUserMenu.value = false
|
||||
userStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 切换到生成分镜图步骤
|
||||
const switchToGenerateStep = () => {
|
||||
currentStep.value = 'generate'
|
||||
@@ -851,7 +982,25 @@ const startGenerate = async () => {
|
||||
} else {
|
||||
// 任务创建失败,重置所有状态
|
||||
console.error('创建任务失败,响应:', response.data)
|
||||
ElMessage.error(response.data?.message || t('video.storyboard.createTaskFailed'))
|
||||
const errorMsg = response.data?.message || t('video.storyboard.createTaskFailed')
|
||||
|
||||
// 检测积分不足
|
||||
if (errorMsg.includes('积分不足') || errorMsg.includes('insufficient') || errorMsg.includes('points')) {
|
||||
ElMessageBox.confirm(
|
||||
'您的积分不足,无法创建任务。是否前往充值?',
|
||||
'积分不足',
|
||||
{
|
||||
confirmButtonText: '去充值',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
router.push('/subscription')
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
ElMessage.error(errorMsg)
|
||||
}
|
||||
|
||||
inProgress.value = false
|
||||
isCreatingTask.value = false
|
||||
currentTask.value = null
|
||||
@@ -865,7 +1014,25 @@ const startGenerate = async () => {
|
||||
response: error.response,
|
||||
stack: error.stack
|
||||
})
|
||||
ElMessage.error(t('video.storyboard.generateFailed') + ': ' + (error.response?.data?.message || error.message))
|
||||
const errorMsg = error.response?.data?.message || error.message || ''
|
||||
|
||||
// 检测积分不足
|
||||
if (errorMsg.includes('积分不足') || errorMsg.includes('insufficient') || errorMsg.includes('points')) {
|
||||
ElMessageBox.confirm(
|
||||
'您的积分不足,无法创建任务。是否前往充值?',
|
||||
'积分不足',
|
||||
{
|
||||
confirmButtonText: '去充值',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
router.push('/subscription')
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
ElMessage.error(t('video.storyboard.generateFailed') + ': ' + errorMsg)
|
||||
}
|
||||
|
||||
// 异常情况,重置所有状态
|
||||
inProgress.value = false
|
||||
isCreatingTask.value = false
|
||||
@@ -1137,12 +1304,48 @@ const startVideoGenerate = async () => {
|
||||
pollTaskStatus(taskId.value)
|
||||
} else {
|
||||
console.error('启动视频生成失败,响应:', response.data)
|
||||
ElMessage.error(response.data?.message || t('video.storyboard.videoStartFailed'))
|
||||
const errorMsg = response.data?.message || t('video.storyboard.videoStartFailed')
|
||||
|
||||
// 检测积分不足
|
||||
if (errorMsg.includes('积分不足') || errorMsg.includes('insufficient') || errorMsg.includes('points')) {
|
||||
ElMessageBox.confirm(
|
||||
'您的积分不足,无法创建任务。是否前往充值?',
|
||||
'积分不足',
|
||||
{
|
||||
confirmButtonText: '去充值',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
router.push('/subscription')
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
ElMessage.error(errorMsg)
|
||||
}
|
||||
|
||||
inProgress.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动视频生成失败,完整错误:', error)
|
||||
ElMessage.error(t('video.storyboard.videoStartFailed') + ': ' + (error.response?.data?.message || error.message))
|
||||
const errorMsg = error.response?.data?.message || error.message || ''
|
||||
|
||||
// 检测积分不足
|
||||
if (errorMsg.includes('积分不足') || errorMsg.includes('insufficient') || errorMsg.includes('points')) {
|
||||
ElMessageBox.confirm(
|
||||
'您的积分不足,无法创建任务。是否前往充值?',
|
||||
'积分不足',
|
||||
{
|
||||
confirmButtonText: '去充值',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
router.push('/subscription')
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
ElMessage.error(t('video.storyboard.videoStartFailed') + ': ' + errorMsg)
|
||||
}
|
||||
|
||||
inProgress.value = false
|
||||
}
|
||||
return
|
||||
@@ -1253,7 +1456,25 @@ const startVideoGenerate = async () => {
|
||||
pollTaskStatus(newTaskId)
|
||||
} else {
|
||||
console.error('创建任务失败,响应:', response.data)
|
||||
ElMessage.error(response.data?.message || t('video.storyboard.createTaskFailed'))
|
||||
const errorMsg = response.data?.message || t('video.storyboard.createTaskFailed')
|
||||
|
||||
// 检测积分不足
|
||||
if (errorMsg.includes('积分不足') || errorMsg.includes('insufficient') || errorMsg.includes('points')) {
|
||||
ElMessageBox.confirm(
|
||||
'您的积分不足,无法创建任务。是否前往充值?',
|
||||
'积分不足',
|
||||
{
|
||||
confirmButtonText: '去充值',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
router.push('/subscription')
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
ElMessage.error(errorMsg)
|
||||
}
|
||||
|
||||
inProgress.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1263,7 +1484,25 @@ const startVideoGenerate = async () => {
|
||||
response: error.response,
|
||||
stack: error.stack
|
||||
})
|
||||
ElMessage.error(t('video.storyboard.generateVideoFailed') + ': ' + (error.response?.data?.message || error.message))
|
||||
const errorMsg = error.response?.data?.message || error.message || ''
|
||||
|
||||
// 检测积分不足
|
||||
if (errorMsg.includes('积分不足') || errorMsg.includes('insufficient') || errorMsg.includes('points')) {
|
||||
ElMessageBox.confirm(
|
||||
'您的积分不足,无法创建任务。是否前往充值?',
|
||||
'积分不足',
|
||||
{
|
||||
confirmButtonText: '去充值',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
router.push('/subscription')
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
ElMessage.error(t('video.storyboard.generateVideoFailed') + ': ' + errorMsg)
|
||||
}
|
||||
|
||||
inProgress.value = false
|
||||
}
|
||||
}
|
||||
@@ -1379,14 +1618,15 @@ const loadHistory = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getUserStoryboardTasks(0, 10)
|
||||
// 请求更多条数以确保能筛选出足够的 COMPLETED 任务
|
||||
const response = await getUserStoryboardTasks(0, 50)
|
||||
console.log('分镜视频历史记录API响应:', response)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
// 只显示已完成的任务,排队中和处理中的任务在主创作区显示
|
||||
const tasks = (response.data.data || []).filter(task =>
|
||||
task.status === 'COMPLETED'
|
||||
).slice(0, 10)
|
||||
).slice(0, 5)
|
||||
|
||||
console.log('获取到的任务列表:', tasks)
|
||||
|
||||
@@ -1395,7 +1635,10 @@ const loadHistory = async () => {
|
||||
...task,
|
||||
resultUrl: task.resultUrl && !task.resultUrl.startsWith('data:image')
|
||||
? processHistoryUrl(task.resultUrl)
|
||||
: task.resultUrl
|
||||
: task.resultUrl,
|
||||
imageUrl: task.imageUrl && !task.imageUrl.startsWith('data:image')
|
||||
? processHistoryUrl(task.imageUrl)
|
||||
: task.imageUrl
|
||||
}))
|
||||
|
||||
console.log('历史记录加载成功:', historyTasks.value.length, '条')
|
||||
@@ -1421,20 +1664,25 @@ const createSimilarFromHistory = (task) => {
|
||||
if (task.hdMode !== undefined) {
|
||||
hdMode.value = task.hdMode
|
||||
}
|
||||
// 如果有分镜图URL,加载图片(支持所有URL类型)
|
||||
if (task.resultUrl) {
|
||||
generatedImageUrl.value = task.resultUrl
|
||||
|
||||
// 优先使用用户上传的参考图片(imageUrl),如果没有则使用生成的分镜图(resultUrl)
|
||||
const imageToUse = task.imageUrl || task.resultUrl
|
||||
if (imageToUse) {
|
||||
generatedImageUrl.value = imageToUse
|
||||
uploadedImages.value = [{
|
||||
url: task.resultUrl,
|
||||
url: imageToUse,
|
||||
file: null,
|
||||
name: '分镜图'
|
||||
name: task.imageUrl ? '参考图片' : '分镜图'
|
||||
}]
|
||||
// 设置 taskId,以便可以直接调用生成视频API
|
||||
if (task.taskId) {
|
||||
// 如果是已完成的分镜图任务且有resultUrl,设置taskId以便直接生成视频
|
||||
if (task.resultUrl && task.taskId) {
|
||||
taskId.value = task.taskId
|
||||
// 切换到视频步骤
|
||||
currentStep.value = 'video'
|
||||
} else {
|
||||
// 只有参考图,停留在生成步骤
|
||||
currentStep.value = 'generate'
|
||||
}
|
||||
// 切换到视频步骤
|
||||
currentStep.value = 'video'
|
||||
}
|
||||
// 滚动到顶部
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
@@ -1537,17 +1785,63 @@ const getStatusText = (status) => {
|
||||
}
|
||||
|
||||
// 下载视频
|
||||
const downloadVideo = () => {
|
||||
if (videoResultUrl.value) {
|
||||
const downloadVideo = async () => {
|
||||
if (!videoResultUrl.value) {
|
||||
ElMessage.error(t('video.storyboard.videoUrlNotAvailable'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
ElMessage.info('正在准备下载...')
|
||||
|
||||
// 获取视频文件
|
||||
const response = await fetch(videoResultUrl.value)
|
||||
const blob = await response.blob()
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = videoResultUrl.value
|
||||
link.download = `storyboard_video_${taskId.value}.mp4`
|
||||
link.href = url
|
||||
link.download = `storyboard_video_${taskId.value || Date.now()}.mp4`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success(t('video.storyboard.downloadStarted'))
|
||||
} else {
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
// 备用方案:直接打开链接
|
||||
window.open(videoResultUrl.value, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载历史记录中的视频
|
||||
const downloadHistoryVideo = async (task) => {
|
||||
if (!task || !task.videoResultUrl) {
|
||||
ElMessage.error(t('video.storyboard.videoUrlNotAvailable'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
ElMessage.info('正在准备下载...')
|
||||
|
||||
const response = await fetch(task.videoResultUrl)
|
||||
const blob = await response.blob()
|
||||
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `storyboard_video_${task.taskId || Date.now()}.mp4`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success(t('video.storyboard.downloadStarted'))
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
window.open(task.videoResultUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1848,6 +2142,20 @@ const checkLastTaskStatus = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 处理"做同款"传递的路由参数
|
||||
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
|
||||
}
|
||||
ElMessage.success(t('video.storyboardVideo.historyParamsFilled') || '已填充历史参数')
|
||||
// 清除URL中的query参数,避免刷新页面重复填充
|
||||
router.replace({ path: route.path })
|
||||
}
|
||||
|
||||
loadHistory()
|
||||
// 延迟恢复任务,避免与创建任务冲突
|
||||
setTimeout(async () => {
|
||||
@@ -1886,6 +2194,40 @@ onBeforeUnmount(() => {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* 用户菜单样式 - 深色主题 */
|
||||
.user-menu-teleport {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
min-width: 160px;
|
||||
overflow: hidden;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.menu-item .el-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.menu-item:not(:last-child) {
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.top-header {
|
||||
display: flex;
|
||||
@@ -3105,6 +3447,37 @@ onBeforeUnmount(() => {
|
||||
background: linear-gradient(90deg, #3b82f6, #1d4ed8);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 生成中进度条动态动画 - 使用蓝绿渐变风格 */
|
||||
.progress-fill-large.animated {
|
||||
background: linear-gradient(90deg, #409eff, #67c23a, #409eff);
|
||||
background-size: 200% 100%;
|
||||
animation: progress-move 1.5s ease-in-out infinite, progress-gradient 2s linear infinite;
|
||||
}
|
||||
|
||||
/* 不确定进度条(排队中) */
|
||||
.progress-bar-large.indeterminate {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill-indeterminate {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, #409eff, #67c23a, #409eff, transparent);
|
||||
border-radius: 4px;
|
||||
animation: progress-move 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 进度百分比文字 */
|
||||
.progress-percentage {
|
||||
font-size: 14px;
|
||||
color: #60a5fa;
|
||||
font-weight: 600;
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 完成状态 */
|
||||
@@ -3485,6 +3858,8 @@ onBeforeUnmount(() => {
|
||||
.history-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -3504,6 +3879,41 @@ onBeforeUnmount(() => {
|
||||
background: #3a3a3a;
|
||||
border-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background: #f0f0f0;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 非 scoped 样式用于 @keyframes 动画 -->
|
||||
<style>
|
||||
@keyframes progress-move {
|
||||
0% { transform: translateX(-100%); }
|
||||
50% { transform: translateX(233%); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
@keyframes progress-gradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
100% { background-position: 200% 50%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -3514,5 +3924,3 @@ onBeforeUnmount(() => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user