347 lines
7.6 KiB
Vue
347 lines
7.6 KiB
Vue
|
|
<template>
|
||
|
|
<div class="generate-step">
|
||
|
|
<div class="step-header">
|
||
|
|
<div class="step-indicator">3</div>
|
||
|
|
<h3 class="step-title">生成数字人视频</h3>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 生成摘要 -->
|
||
|
|
<div class="generate-summary">
|
||
|
|
<div class="summary-item">
|
||
|
|
<VideoCameraOutlined class="summary-icon" />
|
||
|
|
<div class="summary-content">
|
||
|
|
<span class="summary-label">视频素材</span>
|
||
|
|
<span class="summary-value">{{ videoName }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="summary-item">
|
||
|
|
<SoundOutlined class="summary-icon" />
|
||
|
|
<div class="summary-content">
|
||
|
|
<span class="summary-label">配音音色</span>
|
||
|
|
<span class="summary-value">{{ store.voice?.name || '未选择' }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="summary-item">
|
||
|
|
<FileTextOutlined class="summary-icon" />
|
||
|
|
<div class="summary-content">
|
||
|
|
<span class="summary-label">文案字数</span>
|
||
|
|
<span class="summary-value">{{ store.text?.length || 0 }} 字</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="summary-item">
|
||
|
|
<ClockCircleOutlined class="summary-icon" />
|
||
|
|
<div class="summary-content">
|
||
|
|
<span class="summary-label">人脸时长</span>
|
||
|
|
<span class="summary-value">{{ formatDurationMs(store.faceDurationMs) }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="summary-item">
|
||
|
|
<AudioOutlined class="summary-icon" />
|
||
|
|
<div class="summary-content">
|
||
|
|
<span class="summary-label">音频时长</span>
|
||
|
|
<span class="summary-value">{{ formatDurationMs(store.audioDurationMs) }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 时间轴最终对比 -->
|
||
|
|
<TimelinePanel
|
||
|
|
v-if="store.timeline"
|
||
|
|
:face-duration-ms="store.timeline.videoDurationMs"
|
||
|
|
:audio-duration-ms="store.timeline.audioDurationMs"
|
||
|
|
:face-start-time="store.timeline.faceStartTime"
|
||
|
|
:face-end-time="store.timeline.faceEndTime"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<!-- 积分预估 -->
|
||
|
|
<div class="points-section">
|
||
|
|
<div class="points-row">
|
||
|
|
<span class="points-label">预计消耗积分</span>
|
||
|
|
<span class="points-value">{{ estimatedPoints }} 积分</span>
|
||
|
|
</div>
|
||
|
|
<div class="points-row">
|
||
|
|
<span class="points-label">当前余额</span>
|
||
|
|
<span class="points-value">{{ userStore.remainingPoints.toLocaleString() }} 积分</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 生成按钮 -->
|
||
|
|
<div class="action-section">
|
||
|
|
<a-button
|
||
|
|
v-if="!store.isDone"
|
||
|
|
type="primary"
|
||
|
|
size="large"
|
||
|
|
:disabled="!store.canGenerate"
|
||
|
|
:loading="store.createStep === 'creating'"
|
||
|
|
block
|
||
|
|
@click="store.createTask"
|
||
|
|
class="action-btn"
|
||
|
|
>
|
||
|
|
<template v-if="store.createStep === 'creating'">
|
||
|
|
正在创建任务...
|
||
|
|
</template>
|
||
|
|
<template v-else>
|
||
|
|
<PlayCircleOutlined /> 生成数字人视频
|
||
|
|
</template>
|
||
|
|
</a-button>
|
||
|
|
|
||
|
|
<!-- 成功状态 -->
|
||
|
|
<div v-if="store.isDone" class="success-result">
|
||
|
|
<CheckCircleFilled class="success-icon" />
|
||
|
|
<h4>任务已提交成功</h4>
|
||
|
|
<p>请在任务中心查看生成进度</p>
|
||
|
|
<a-button type="primary" @click="store.reset">
|
||
|
|
重新生成
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 错误状态 -->
|
||
|
|
<div v-if="store.createStep === 'error'" class="error-result">
|
||
|
|
<ExclamationCircleFilled class="error-icon" />
|
||
|
|
<span>{{ store.error }}</span>
|
||
|
|
<a-button type="link" @click="store.retry">重试</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 导航按钮 -->
|
||
|
|
<div v-if="!store.isDone" class="nav-buttons">
|
||
|
|
<a-button size="large" @click="store.goPrevPhase">
|
||
|
|
<LeftOutlined /> 上一步
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { computed } from 'vue'
|
||
|
|
import {
|
||
|
|
VideoCameraOutlined,
|
||
|
|
SoundOutlined,
|
||
|
|
FileTextOutlined,
|
||
|
|
ClockCircleOutlined,
|
||
|
|
AudioOutlined,
|
||
|
|
PlayCircleOutlined,
|
||
|
|
CheckCircleFilled,
|
||
|
|
ExclamationCircleFilled,
|
||
|
|
LeftOutlined,
|
||
|
|
} from '@ant-design/icons-vue'
|
||
|
|
import TimelinePanel from './TimelinePanel.vue'
|
||
|
|
import { useDigitalHumanStore } from '../stores/useDigitalHumanStore'
|
||
|
|
import { useUserStore } from '@/stores/user'
|
||
|
|
import { usePointsConfigStore } from '@/stores/pointsConfig'
|
||
|
|
import { formatDurationMs } from '../utils/format'
|
||
|
|
|
||
|
|
const store = useDigitalHumanStore()
|
||
|
|
const userStore = useUserStore()
|
||
|
|
const pointsConfigStore = usePointsConfigStore()
|
||
|
|
|
||
|
|
const videoName = computed(() => {
|
||
|
|
return store.selectedVideo?.fileName || store.videoFile?.name || '未选择'
|
||
|
|
})
|
||
|
|
|
||
|
|
const estimatedPoints = computed(() => {
|
||
|
|
const points = pointsConfigStore.getConsumePoints('kling')
|
||
|
|
return points ?? 150
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped lang="less">
|
||
|
|
.generate-step {
|
||
|
|
background: #fff;
|
||
|
|
border: 1px solid #e2e8f0;
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.step-header {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.step-indicator {
|
||
|
|
width: 32px;
|
||
|
|
height: 32px;
|
||
|
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||
|
|
color: #fff;
|
||
|
|
border-radius: 8px 0 12px 0;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 700;
|
||
|
|
}
|
||
|
|
|
||
|
|
.step-title {
|
||
|
|
font-size: 16px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1e293b;
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.generate-summary {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(2, 1fr);
|
||
|
|
gap: 12px;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.summary-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 10px;
|
||
|
|
padding: 12px;
|
||
|
|
background: #f8fafc;
|
||
|
|
border-radius: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.summary-icon {
|
||
|
|
font-size: 18px;
|
||
|
|
color: #3b82f6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.summary-content {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.summary-label {
|
||
|
|
font-size: 11px;
|
||
|
|
color: #94a3b8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.summary-value {
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 500;
|
||
|
|
color: #1e293b;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
white-space: nowrap;
|
||
|
|
max-width: 120px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.points-section {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 16px;
|
||
|
|
background: #f8fafc;
|
||
|
|
border-radius: 8px;
|
||
|
|
margin-top: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.points-row {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.points-label {
|
||
|
|
font-size: 12px;
|
||
|
|
color: #64748b;
|
||
|
|
}
|
||
|
|
|
||
|
|
.points-value {
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1e293b;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-section {
|
||
|
|
margin-top: 20px;
|
||
|
|
padding-top: 16px;
|
||
|
|
border-top: 1px solid #e2e8f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn {
|
||
|
|
height: 44px;
|
||
|
|
font-size: 15px;
|
||
|
|
font-weight: 600;
|
||
|
|
border-radius: 8px;
|
||
|
|
|
||
|
|
&.ant-btn-primary {
|
||
|
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||
|
|
border: none;
|
||
|
|
|
||
|
|
&:hover:not(:disabled) {
|
||
|
|
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.35);
|
||
|
|
}
|
||
|
|
|
||
|
|
&:disabled {
|
||
|
|
background: #d1d5db;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.success-result {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
padding: 24px;
|
||
|
|
background: #f0fdf4;
|
||
|
|
border-radius: 10px;
|
||
|
|
text-align: center;
|
||
|
|
|
||
|
|
.success-icon {
|
||
|
|
font-size: 48px;
|
||
|
|
color: #22c55e;
|
||
|
|
margin-bottom: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
h4 {
|
||
|
|
font-size: 16px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #166534;
|
||
|
|
margin: 0 0 8px 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
p {
|
||
|
|
font-size: 13px;
|
||
|
|
color: #15803d;
|
||
|
|
margin: 0 0 16px 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-result {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 12px;
|
||
|
|
background: #fee2e2;
|
||
|
|
border-radius: 6px;
|
||
|
|
font-size: 13px;
|
||
|
|
color: #dc2626;
|
||
|
|
|
||
|
|
.error-icon {
|
||
|
|
font-size: 16px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-buttons {
|
||
|
|
display: flex;
|
||
|
|
gap: 12px;
|
||
|
|
margin-top: 20px;
|
||
|
|
padding-top: 16px;
|
||
|
|
border-top: 1px solid #e2e8f0;
|
||
|
|
|
||
|
|
.ant-btn {
|
||
|
|
flex: 1;
|
||
|
|
height: 44px;
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 480px) {
|
||
|
|
.generate-summary {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|