Files
sionrui/frontend/app/web-gold/src/views/kling/components/GenerateStep.vue
2026-03-05 21:01:34 +08:00

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>