Files
sionrui/frontend/app/web-gold/src/views/kling/IdentifyFace.vue

825 lines
17 KiB
Vue
Raw Normal View History

2025-12-01 22:27:50 +08:00
<template>
2026-01-17 19:54:57 +08:00
<FullWidthLayout :show-padding="false">
2026-02-12 23:35:39 +08:00
<div class="digital-human-page">
<!-- 左侧配置区 -->
<section class="config-panel">
<!-- 步骤1输入文案 -->
<div class="config-card">
<div class="step-indicator">1</div>
<h3 class="card-title">输入播文案</h3>
<a-textarea
v-model:value="ttsText"
:placeholder="textareaPlaceholder"
:rows="4"
:maxlength="maxTextLength"
:show-count="true"
class="text-input"
:bordered="false"
/>
2025-12-01 22:27:50 +08:00
2026-02-12 23:35:39 +08:00
<div class="input-meta">
<span>当前字数{{ ttsText?.length || 0 }}</span>
2026-01-17 19:54:57 +08:00
</div>
2026-02-12 23:35:39 +08:00
</div>
2025-12-01 22:27:50 +08:00
2026-02-12 23:35:39 +08:00
<!-- 步骤2语音设置 -->
<div class="config-card">
<div class="step-indicator">2</div>
<h3 class="card-title">语音设置</h3>
<div class="voice-settings">
<div class="setting-group">
<label class="setting-label">选择音色</label>
<VoiceSelector
:synth-text="ttsText"
:speech-rate="speechRate"
@select="handleVoiceSelect"
/>
2025-12-01 22:27:50 +08:00
</div>
2026-02-12 23:35:39 +08:00
<div class="setting-group">
<label class="setting-label">合成模型等级</label>
<div class="model-options">
<button
class="model-btn"
:class="{ 'model-btn--active': speechRate <= 1 }"
>
标准版 (1x积分)
</button>
<button
class="model-btn model-btn--pro"
:class="{ 'model-btn--active': speechRate > 1 }"
>
Pro 旗舰版 (3x积分)
<CrownFilled class="pro-icon" />
</button>
2026-01-17 19:54:57 +08:00
</div>
</div>
2026-02-12 23:35:39 +08:00
</div>
</div>
2026-01-17 19:54:57 +08:00
2026-02-12 23:35:39 +08:00
<!-- 步骤3上传视频 -->
<div class="config-card">
<div class="step-indicator">3</div>
<h3 class="card-title">上传/选择视频素材</h3>
<div class="video-options">
<!-- 上传新视频 -->
<div
class="video-option-card"
:class="{ 'video-option-card--selected': videoState.videoSource === 'upload' }"
@click="handleSelectUpload"
>
<div class="video-option-icon">
<CloudUploadOutlined />
</div>
2026-02-12 23:35:39 +08:00
<div class="video-option-content">
<h4>点击上传新视频</h4>
<p>支持 MP4MOV ( >3)</p>
</div>
</div>
2026-02-12 23:35:39 +08:00
<!-- 从素材库选择 -->
<div
2026-02-12 23:35:39 +08:00
class="video-option-card"
:class="{ 'video-option-card--selected': videoState.videoSource === 'select' }"
@click="handleSelectFromLibrary"
>
2026-02-12 23:35:39 +08:00
<div class="video-option-icon">
<PictureOutlined />
</div>
2026-02-12 23:35:39 +08:00
<div class="video-option-content">
<h4>从素材库选择</h4>
<p>选择已上传的视频</p>
</div>
</div>
</div>
2026-02-12 23:35:39 +08:00
<!-- 已选择视频预览 -->
<div v-if="videoState.selectedVideo" class="selected-video">
<div class="video-preview-thumb">
<img
:src="getVideoPreviewUrl(videoState.selectedVideo)"
:alt="videoState.selectedVideo.fileName"
/>
</div>
<div class="video-preview-info">
<div class="video-name">{{ videoState.selectedVideo.fileName }}</div>
<div class="video-meta">{{ formatDuration(videoState.selectedVideo.duration) }}</div>
</div>
<button class="change-video-btn" @click="replaceVideo">更换</button>
</div>
2026-02-12 23:35:39 +08:00
<!-- 上传区域 -->
<div
v-if="videoState.videoSource === 'upload'"
class="upload-zone"
:class="{ 'upload-zone--dragover': dragOver }"
@drop.prevent="handleDrop"
@dragover.prevent="dragOver = true"
@dragleave.prevent="dragOver = false"
>
<input ref="fileInput" type="file" accept=".mp4,.mov" class="file-input" @change="handleFileSelectWrapper" />
<div v-if="!videoState.uploadedVideo" class="upload-placeholder">
<CloudUploadOutlined class="upload-icon" />
<span class="upload-text">点击上传新视频</span>
<span class="upload-hint">支持 MP4MOV ( >3)</span>
<button class="select-file-btn" @click="triggerFileSelect">选择文件</button>
</div>
2026-02-04 01:46:55 +08:00
2026-02-12 23:35:39 +08:00
<div v-else class="upload-preview">
<video :src="videoState.uploadedVideo" controls class="preview-video-player"></video>
<p class="upload-filename">{{ videoState.videoFile?.name }}</p>
<button class="change-video-btn" @click="replaceVideo">更换</button>
</div>
</div>
2026-02-12 23:35:39 +08:00
</div>
2025-12-02 01:55:57 +08:00
2026-02-12 23:35:39 +08:00
<!-- Pipeline 进度 -->
<PipelineProgress
v-if="isPipelineBusy || isPipelineReady || isPipelineFailed || isPipelineCompleted"
:state="pipelineState"
:progress="pipelineProgress"
:is-busy="isPipelineBusy"
:is-ready="isPipelineReady"
:is-failed="isPipelineFailed"
:is-completed="isPipelineCompleted"
:error="pipelineError"
@retry="retryPipeline"
@reset="resetPipeline"
/>
<!-- 操作按钮 -->
<div class="action-section">
<a-button
2026-02-24 21:47:21 +08:00
v-if="!isPipelineCompleted"
2026-02-12 23:35:39 +08:00
type="primary"
size="large"
:disabled="!canGenerate"
:loading="isPipelineBusy"
block
@click="generateAudio"
class="action-btn"
>
2026-02-24 21:47:21 +08:00
{{ isPipelineBusy ? pipelineStateLabel + '...' : '生成数字人视频' }}
2026-02-12 23:35:39 +08:00
</a-button>
2026-02-24 21:47:21 +08:00
<div v-else class="completed-tip">
<span>任务已提交成功</span>
<a-button @click="resetPipeline" class="reset-btn">重新生成</a-button>
</div>
2026-02-12 23:35:39 +08:00
</div>
2026-02-04 01:18:16 +08:00
2026-02-12 23:35:39 +08:00
</section>
<!-- 右侧预览区 -->
<aside class="preview-panel">
<div class="preview-card">
<h3 class="preview-title">合成预览</h3>
2025-12-02 01:55:57 +08:00
2026-02-12 23:35:39 +08:00
<div class="preview-screen">
<ResultPanel @videoLoaded="handleVideoLoaded" />
2025-12-02 01:55:57 +08:00
</div>
2026-02-12 23:35:39 +08:00
<div class="preview-meta">
<div class="meta-row">
<span class="meta-label">预计消耗积分</span>
<span class="meta-value">150 积分</span>
</div>
<div class="meta-row">
<span class="meta-label">当前余额</span>
2026-02-24 22:11:30 +08:00
<span class="meta-value">{{ userStore.remainingPoints.toLocaleString() }} 积分</span>
2026-02-12 23:35:39 +08:00
</div>
</div>
</div>
</aside>
2025-12-01 22:27:50 +08:00
</div>
2026-02-12 23:35:39 +08:00
<!-- 视频选择器弹窗 -->
<VideoSelector v-model:open="videoState.selectorVisible" @select="handleVideoSelect" />
2026-01-17 19:54:57 +08:00
</FullWidthLayout>
2025-12-01 22:27:50 +08:00
</template>
2025-12-28 13:49:45 +08:00
<script setup lang="ts">
2026-02-12 23:35:39 +08:00
import { ref } from 'vue'
import { CloudUploadOutlined, CrownFilled } from '@ant-design/icons-vue'
2025-12-01 22:27:50 +08:00
import { useVoiceCopyStore } from '@/stores/voiceCopy'
2026-02-24 22:11:30 +08:00
import { useUserStore } from '@/stores/user'
import VideoSelector from '@/components/VideoSelector.vue'
import VoiceSelector from '@/components/VoiceSelector.vue'
import ResultPanel from '@/components/ResultPanel.vue'
2026-01-17 19:54:57 +08:00
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
2026-02-04 01:18:16 +08:00
import PipelineProgress from '@/components/PipelineProgress.vue'
2025-12-01 22:27:50 +08:00
2025-12-28 13:49:45 +08:00
// Controller Hook
import { useIdentifyFaceController } from './hooks/useIdentifyFaceController'
2025-12-22 00:15:02 +08:00
2025-12-28 13:49:45 +08:00
const voiceStore = useVoiceCopyStore()
2026-02-24 22:11:30 +08:00
const userStore = useUserStore()
const dragOver = ref(false)
2025-12-28 13:49:45 +08:00
// ==================== 初始化 Controller ====================
// Controller 内部直接创建和管理两个子 Hook
const controller = useIdentifyFaceController()
// 解构 controller 以简化模板调用
const {
// 语音生成相关
ttsText,
speechRate,
generateAudio,
// 数字人生成相关
videoState,
getVideoPreviewUrl,
// 计算属性
canGenerate,
maxTextLength,
textareaPlaceholder,
2026-02-04 01:18:16 +08:00
audioDurationSec,
2026-02-04 01:46:55 +08:00
// Pipeline 状态(单一状态源)
2026-02-04 01:18:16 +08:00
pipelineState,
2026-02-24 21:47:21 +08:00
pipelineStateLabel,
2026-02-04 01:18:16 +08:00
isPipelineBusy,
isPipelineReady,
isPipelineFailed,
isPipelineCompleted,
pipelineProgress,
pipelineError,
retryPipeline,
resetPipeline,
2025-12-28 13:49:45 +08:00
// 事件处理方法
handleVoiceSelect,
handleDrop,
handleSelectUpload,
handleSelectFromLibrary,
handleVideoSelect,
handleVideoLoaded,
replaceVideo,
// UI 辅助方法
formatDuration,
} = controller
// ==================== 生命周期 ====================
2025-12-01 22:27:50 +08:00
2026-02-12 23:35:39 +08:00
// 引用 fileInput 用于手动触发点击
const fileInput = ref<HTMLInputElement | null>(null)
// 触发文件选择
const triggerFileSelect = () => {
fileInput.value?.click()
}
// 覆盖 controller 中的方法,使用 ref
const handleFileSelectWrapper = (e: Event) => {
controller.handleFileSelect(e)
}
// ==================== 生命周期 ====================
import { onMounted } from 'vue'
2025-12-01 22:27:50 +08:00
onMounted(async () => {
await voiceStore.refresh()
})
</script>
2025-12-28 13:49:45 +08:00
<style scoped lang="less">
2026-02-12 23:35:39 +08:00
.digital-human-page {
2026-01-18 00:34:04 +08:00
min-height: 100vh;
2026-02-12 23:35:39 +08:00
background: #F8FAFC;
display: flex;
flex-direction: row;
gap: 24px;
padding: 24px;
2026-01-18 00:34:04 +08:00
}
2026-02-12 23:35:39 +08:00
// 布局容器
.config-panel {
flex: 1;
2026-02-13 00:21:21 +08:00
padding:0 20px;
2026-02-12 23:35:39 +08:00
max-width: 100%;
2025-12-28 13:49:45 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.preview-panel {
width: 400px;
background: #fff;
border-left: 1px solid #E2E8F0;
padding: 20px;
display: flex;
flex-direction: column;
flex-shrink: 0;
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
@media (max-width: 1200px) {
width: 100%;
border-left: none;
border-top: 1px solid #E2E8F0;
order: -1;
2026-01-18 00:34:04 +08:00
}
2025-12-28 13:49:45 +08:00
}
2025-12-01 22:27:50 +08:00
2026-02-12 23:35:39 +08:00
// 配置卡片
.config-card {
position: relative;
background: #fff;
border: 1px solid #E2E8F0;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
overflow: hidden;
2025-12-28 13:49:45 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.step-indicator {
position: absolute;
left: 0;
top: 0;
width: 32px;
height: 32px;
background: linear-gradient(135deg, #3B82F6 0%, #2563EB 100%);
color: #fff;
border-radius: 8px 0 12px 0;
display: flex;
align-items: center;
justify-content: center;
2026-01-18 00:34:04 +08:00
font-size: 14px;
2026-02-12 23:35:39 +08:00
font-weight: 700;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
2026-01-18 00:34:04 +08:00
}
2026-02-12 23:35:39 +08:00
.card-title {
font-size: 16px;
font-weight: 600;
color: #1E293B;
margin: 0 0 12px 12px;
padding-left: 44px;
2026-01-18 00:34:04 +08:00
}
2025-12-01 22:27:50 +08:00
2026-02-12 23:35:39 +08:00
// 文案输入
.text-input {
width: 100%;
2026-01-18 00:34:04 +08:00
:deep(.ant-input) {
border: none;
2026-02-12 23:35:39 +08:00
border-radius: 8px;
font-size: 14px;
padding: 16px 12px;
background: #F8FAFC;
&:focus {
background: #fff;
box-shadow: 0 0 0 1px #E2E8F0;
}
:deep(.ant-input-textarea-show-count) {
bottom: 8px;
}
2026-01-18 00:34:04 +08:00
}
2025-12-01 22:27:50 +08:00
}
2026-02-12 23:35:39 +08:00
.input-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #94A3B8;
margin-top: 12px;
2026-01-18 00:34:04 +08:00
}
2026-02-12 23:35:39 +08:00
.meta-value {
2025-12-01 22:27:50 +08:00
font-weight: 600;
2026-02-12 23:35:39 +08:00
color: #1E293B;
2025-12-01 22:27:50 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
// 语音设置
.voice-settings {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
@media (max-width: 640px) {
grid-template-columns: 1fr;
}
2025-12-01 22:27:50 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.setting-group {
2025-12-01 22:27:50 +08:00
display: flex;
2026-02-12 23:35:39 +08:00
flex-direction: column;
gap: 8px;
2025-12-01 22:27:50 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.setting-label {
font-size: 13px;
font-weight: 500;
color: #64748B;
}
.model-options {
display: flex;
background: #F1F5F9;
border-radius: 6px;
padding: 2px;
gap: 2px;
2025-12-01 22:27:50 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.model-btn {
flex: 1;
padding: 8px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: #64748B;
border: none;
background: transparent;
2025-12-01 22:27:50 +08:00
cursor: pointer;
2026-02-12 23:35:39 +08:00
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&:hover {
background: rgba(255, 255, 255, 0.5);
}
&--active {
background: #fff;
color: #22C55E;
font-weight: 600;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
}
.pro-icon {
2025-12-01 22:27:50 +08:00
font-size: 12px;
2026-02-12 23:35:39 +08:00
color: #EAB308;
2025-12-01 22:27:50 +08:00
}
2026-02-12 23:35:39 +08:00
// 视频选项
.video-options {
display: grid;
grid-template-columns: 1fr 1fr;
2026-01-18 00:34:04 +08:00
gap: 12px;
margin-bottom: 16px;
2026-02-12 23:35:39 +08:00
@media (max-width: 480px) {
grid-template-columns: 1fr;
}
}
2026-01-18 00:34:04 +08:00
.video-option-card {
display: flex;
align-items: center;
2026-01-18 00:34:04 +08:00
gap: 12px;
2026-02-12 23:35:39 +08:00
padding: 14px 16px;
border: 2px dashed #E2E8F0;
border-radius: 10px;
background: #F8FAFC;
cursor: pointer;
2026-02-12 23:35:39 +08:00
transition: all 0.2s ease;
2026-01-18 00:34:04 +08:00
&:hover {
2026-02-13 00:21:21 +08:00
border-color: #3B82F6;
background: #EFF6FF;
2026-01-18 00:34:04 +08:00
}
2026-02-12 23:35:39 +08:00
&--selected {
border-style: solid;
2026-02-13 00:21:21 +08:00
border-color: #2563EB;
background: #EFF6FF;
2026-01-18 00:34:04 +08:00
}
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.video-option-icon {
font-size: 20px;
color: #94A3B8;
display: flex;
align-items: center;
justify-content: center;
2026-02-12 23:35:39 +08:00
width: 36px;
height: 36px;
background: #fff;
border-radius: 6px;
2026-01-18 00:34:04 +08:00
flex-shrink: 0;
}
2026-02-12 23:35:39 +08:00
.video-option-content {
flex: 1;
}
.video-option-content h4 {
font-size: 13px;
font-weight: 500;
color: #1E293B;
margin: 0 0 4px 0;
}
.video-option-content p {
font-size: 11px;
color: #94A3B8;
margin: 0;
}
// 已选择视频
.selected-video {
display: flex;
align-items: center;
2026-02-12 23:35:39 +08:00
gap: 10px;
padding: 10px;
background: #F1F5F9;
border-radius: 8px;
}
2026-02-12 23:35:39 +08:00
.video-preview-thumb {
width: 70px;
height: 40px;
border-radius: 6px;
overflow: hidden;
2026-02-12 23:35:39 +08:00
background: #1E293B;
flex-shrink: 0;
2026-02-12 23:35:39 +08:00
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
2026-02-12 23:35:39 +08:00
.video-preview-info {
flex: 1;
min-width: 0;
}
2026-02-12 23:35:39 +08:00
.video-name {
font-size: 13px;
font-weight: 500;
color: #1E293B;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
2026-02-12 23:35:39 +08:00
.video-meta {
2026-02-12 23:35:39 +08:00
font-size: 11px;
color: #94A3B8;
}
.change-video-btn {
padding: 4px 10px;
font-size: 11px;
color: #64748B;
border: none;
background: transparent;
cursor: pointer;
text-decoration: underline;
flex-shrink: 0;
&:hover {
2026-02-13 00:21:21 +08:00
color: #3B82F6;
2026-02-12 23:35:39 +08:00
}
}
2026-02-12 23:35:39 +08:00
// 上传区域
2025-12-28 13:49:45 +08:00
.upload-zone {
2026-02-12 23:35:39 +08:00
min-height: 160px;
2025-12-28 13:49:45 +08:00
display: flex;
align-items: center;
justify-content: center;
2026-02-12 23:35:39 +08:00
border: 2px dashed #E2E8F0;
border-radius: 10px;
background: #F8FAFC;
transition: all 0.2s ease;
&--dragover {
2026-02-13 00:21:21 +08:00
border-color: #3B82F6;
background: #EFF6FF;
2026-01-18 00:34:04 +08:00
}
2025-12-28 13:49:45 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.file-input {
display: none;
}
2025-12-28 13:49:45 +08:00
.upload-placeholder {
2026-01-18 00:34:04 +08:00
display: flex;
flex-direction: column;
align-items: center;
2026-02-12 23:35:39 +08:00
gap: 8px;
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.upload-icon {
font-size: 28px;
color: #94A3B8;
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.upload-text {
font-size: 13px;
font-weight: 500;
color: #1E293B;
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.upload-hint {
font-size: 11px;
color: #94A3B8;
}
.select-file-btn {
margin-top: 12px;
padding: 6px 16px;
font-size: 12px;
font-weight: 500;
2026-02-13 00:21:21 +08:00
color: #3B82F6;
2026-02-12 23:35:39 +08:00
border: none;
background: transparent;
cursor: pointer;
2026-02-13 00:21:21 +08:00
border: 1px solid #3B82F6;
2026-02-12 23:35:39 +08:00
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
2026-02-13 00:21:21 +08:00
background: rgba(59, 130, 246, 0.1);
2026-01-18 00:34:04 +08:00
}
2025-12-28 13:49:45 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.upload-preview {
2026-01-18 00:34:04 +08:00
display: flex;
flex-direction: column;
align-items: center;
2026-02-12 23:35:39 +08:00
gap: 10px;
padding: 12px;
2025-12-28 13:49:45 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.preview-video-player {
2025-12-28 13:49:45 +08:00
width: 100%;
2026-02-12 23:35:39 +08:00
max-height: 140px;
2025-12-28 13:49:45 +08:00
border-radius: 8px;
}
2026-02-12 23:35:39 +08:00
.upload-filename {
font-size: 12px;
color: #64748B;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 6px 10px;
background: #F1F5F9;
border-radius: 6px;
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
// 操作按钮区
.action-section {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #E2E8F0;
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.action-btn {
height: 44px;
font-size: 15px;
font-weight: 600;
2026-02-12 23:35:39 +08:00
border-radius: 8px;
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
&.ant-btn-primary {
2026-02-13 00:21:21 +08:00
background: linear-gradient(135deg, #3B82F6 0%, #2563EB 100%);
2026-02-12 23:35:39 +08:00
border: none;
&:hover:not(:disabled) {
2026-02-13 00:21:21 +08:00
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.35);
2026-02-12 23:35:39 +08:00
}
&:disabled {
background: #D1D5DB;
}
}
}
2026-02-24 21:47:21 +08:00
.completed-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 12px 16px;
background: #F0FDF4;
border: 1px solid #86EFAC;
border-radius: 8px;
color: #166534;
font-size: 14px;
font-weight: 500;
.reset-btn {
padding: 4px 12px;
font-size: 13px;
color: #3B82F6;
border: 1px solid #3B82F6;
border-radius: 4px;
background: transparent;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(59, 130, 246, 0.1);
}
}
}
2026-02-12 23:35:39 +08:00
// 预览面板
.preview-card {
position: sticky;
top: 24px;
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.preview-title {
font-size: 16px;
font-weight: 600;
color: #1E293B;
margin: 0 0 16px 0;
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.preview-screen {
aspect-ratio: 9/16;
background: #1E293B;
border-radius: 12px;
margin-bottom: 20px;
overflow: hidden;
position: relative;
2026-01-18 00:34:04 +08:00
display: flex;
2026-02-12 23:35:39 +08:00
align-items: center;
justify-content: center;
2026-02-12 23:35:39 +08:00
&::after {
content: '预览画面不可用';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 13px;
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
:deep(.result-panel) {
height: 100%;
}
2026-01-18 00:34:04 +08:00
}
2026-02-12 23:35:39 +08:00
.preview-meta {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px;
background: #F8FAFC;
2025-12-28 13:49:45 +08:00
border-radius: 8px;
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
.meta-row {
display: flex;
justify-content: space-between;
font-size: 12px;
2026-01-18 00:34:04 +08:00
}
2026-02-12 23:35:39 +08:00
.meta-label {
color: #64748B;
2026-01-18 00:34:04 +08:00
}
2026-02-12 23:35:39 +08:00
.meta-row .meta-value {
2026-01-18 00:34:04 +08:00
font-weight: 600;
2026-02-12 23:35:39 +08:00
color: #1E293B;
2025-12-28 13:49:45 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-12 23:35:39 +08:00
// 响应式
2025-12-28 13:49:45 +08:00
@media (max-width: 1024px) {
2026-02-12 23:35:39 +08:00
.digital-human-page {
flex-direction: column;
}
.config-panel {
max-width: 100%;
2026-01-18 00:34:04 +08:00
padding: 16px;
}
2026-02-12 23:35:39 +08:00
.preview-panel {
width: 100%;
border-left: none;
border-top: 1px solid #E2E8F0;
order: -1;
padding: 16px;
2026-01-18 00:34:04 +08:00
}
2026-02-12 23:35:39 +08:00
.preview-card {
position: static;
2026-01-18 00:34:04 +08:00
}
2026-02-12 23:35:39 +08:00
.preview-screen {
min-height: 200px;
2025-12-28 13:49:45 +08:00
}
}
2025-12-01 22:27:50 +08:00
</style>