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

1023 lines
23 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-03-05 22:58:31 +08:00
<div class="notion-page">
<!-- 主内容区域 -->
2026-03-05 21:01:34 +08:00
<div class="main-content">
<!-- 左侧视频模块 -->
2026-03-05 22:58:31 +08:00
<section class="content-block">
<div class="block-header">
<span class="block-emoji">📹</span>
<h3 class="block-title">视频素材</h3>
</div>
2026-02-12 23:35:39 +08:00
2026-03-05 22:58:31 +08:00
<!-- 视频来源选项 -->
<div class="source-toggle">
<button
class="toggle-btn"
:class="{ active: store.videoSource === 'upload' }"
@click="store.selectUploadMode"
2026-03-05 21:01:34 +08:00
>
2026-03-05 22:58:31 +08:00
<CloudUploadOutlined />
<span>上传视频</span>
</button>
<button
class="toggle-btn"
:class="{ active: store.videoSource === 'select' }"
@click="store.selectLibraryMode"
>
<FolderOutlined />
<span>素材库</span>
</button>
</div>
2025-12-01 22:27:50 +08:00
2026-03-05 22:58:31 +08:00
<!-- 上传区域 -->
<div
v-if="store.videoSource === 'upload'"
class="upload-area"
:class="{ dragover: dragOver, 'has-video': store.videoPreviewUrl }"
@drop.prevent="handleDrop"
@dragover.prevent="dragOver = true"
@dragleave.prevent="dragOver = false"
>
<input
ref="fileInput"
type="file"
accept=".mp4,.mov"
class="hidden-input"
@change="handleFileSelect"
/>
2026-01-17 19:54:57 +08:00
2026-03-05 22:58:31 +08:00
<div v-if="!store.videoPreviewUrl" class="upload-empty" @click="triggerFileSelect">
<div class="upload-icon-wrapper">
<CloudUploadOutlined class="upload-icon" />
2026-03-05 21:01:34 +08:00
</div>
2026-03-05 22:58:31 +08:00
<div class="upload-text">点击上传或拖拽文件到此处</div>
<div class="upload-hint">支持 MP4MOV 格式视频需大于 3 </div>
2026-03-05 21:01:34 +08:00
</div>
2026-02-12 23:35:39 +08:00
2026-03-05 22:58:31 +08:00
<div v-else class="upload-preview">
<video
:src="store.videoPreviewUrl"
controls
playsinline
preload="metadata"
class="preview-video"
></video>
<button class="replace-btn" @click="clearVideo">
<ReloadOutlined />
<span>更换视频</span>
2026-03-05 21:01:34 +08:00
</button>
</div>
2026-03-05 22:58:31 +08:00
</div>
2026-03-05 22:58:31 +08:00
<!-- 已选视频预览素材库 -->
<div v-if="store.selectedVideo && store.videoSource === 'select'" class="library-preview">
<video
v-if="store.videoPreviewUrl"
:src="store.videoPreviewUrl"
controls
playsinline
preload="metadata"
class="preview-video"
></video>
<div class="video-meta">
<div class="video-name" :title="store.selectedVideo.fileName">{{ store.selectedVideo.fileName }}</div>
<div class="video-info">
{{ formatDuration(store.selectedVideo.duration) }} · {{ formatFileSize(store.selectedVideo.fileSize) }}
</div>
</div>
2026-03-05 22:58:31 +08:00
<button class="replace-btn" @click="clearVideo">
<ReloadOutlined />
<span>更换</span>
</button>
</div>
2026-03-05 21:01:34 +08:00
2026-03-05 22:58:31 +08:00
<!-- 识别状态 -->
<div v-if="store.videoStep !== 'idle'" class="process-status" :class="store.videoStep">
<div v-if="store.videoStep === 'uploading'" class="status-row">
<a-spin size="small" />
<span>正在上传视频...</span>
2026-03-05 21:01:34 +08:00
</div>
2026-03-05 22:58:31 +08:00
<div v-else-if="store.videoStep === 'recognizing'" class="status-row">
<a-spin size="small" />
<span>正在识别人脸...</span>
</div>
<div v-else-if="store.videoStep === 'recognized'" class="status-row success">
<CheckCircleOutlined />
<span>识别成功 · 人脸时长 {{ formatDurationMs(store.faceDurationMs) }}</span>
</div>
<div v-else-if="store.videoStep === 'error'" class="status-row error">
<ExclamationCircleOutlined />
<span>{{ store.error }}</span>
<button class="link-btn" @click="store.retry">重试</button>
2026-02-12 23:35:39 +08:00
</div>
2026-03-05 22:58:31 +08:00
</div>
2026-03-05 22:58:31 +08:00
<!-- 生成按钮 - 放在左侧面板底部 -->
<div class="generate-action">
<!-- 成功状态 -->
<div v-if="store.isDone" class="result-inline success">
<CheckCircleFilled />
<span>任务已提交</span>
<button class="inline-btn" @click="store.reset">重新生成</button>
2026-03-05 21:01:34 +08:00
</div>
2026-02-12 23:35:39 +08:00
2026-03-05 22:58:31 +08:00
<!-- 错误状态 -->
<div v-else-if="store.createStep === 'error'" class="result-inline error">
<ExclamationCircleFilled />
<span>{{ store.error }}</span>
<button class="link-btn" @click="store.retry">重试</button>
2026-02-12 23:35:39 +08:00
</div>
2026-02-04 01:46:55 +08:00
2026-03-05 22:58:31 +08:00
<!-- 生成按钮 -->
<button
v-else
class="generate-btn"
:class="{ disabled: !store.canGenerate }"
:disabled="!store.canGenerate"
@click="store.createTask"
>
<div class="btn-glow"></div>
<div class="btn-left">
<template v-if="store.createStep === 'creating'">
<LoadingOutlined class="btn-spin" spin />
<span>正在创建...</span>
2026-03-05 21:01:34 +08:00
</template>
<template v-else>
2026-03-05 22:58:31 +08:00
<PlayCircleOutlined class="btn-icon" />
<span>生成视频</span>
2026-03-05 21:01:34 +08:00
</template>
</div>
2026-03-05 22:58:31 +08:00
<div class="btn-right">
<span class="cost-num">{{ estimatedPoints }}</span>
<span class="cost-label">积分</span>
2026-03-05 21:01:34 +08:00
</div>
2026-03-05 22:58:31 +08:00
</button>
</div>
</section>
2025-12-02 01:55:57 +08:00
2026-03-05 22:58:31 +08:00
<!-- 右侧配音文案模块 -->
<section class="content-block">
<div class="block-header">
<span class="block-emoji">🎙</span>
<h3 class="block-title">配音文案</h3>
</div>
<!-- 文案输入 -->
<div class="input-section">
<div class="label-row">
<label class="input-label">播报文案</label>
<button class="generate-text-btn" @click="openTextGeneratePopup">
<EditOutlined />
<span>AI 生成</span>
</button>
</div>
<a-textarea
v-model:value="store.text"
:placeholder="placeholder"
:rows="6"
:maxlength="4000"
:show-count="true"
class="notion-textarea"
:bordered="false"
2026-03-05 21:01:34 +08:00
/>
2026-03-05 22:58:31 +08:00
<div class="input-footer">
<span>{{ store.text?.length || 0 }} </span>
<span v-if="store.faceDurationMs > 0">· 建议 {{ suggestedChars }} </span>
</div>
2026-02-26 18:52:09 +08:00
</div>
2026-03-05 21:01:34 +08:00
2026-03-05 22:58:31 +08:00
<!-- 文案生成浮窗 -->
<TextGeneratePopup
v-model:visible="textGenerateVisible"
@success="handleTextGenerated"
/>
<!-- 音色选择 -->
<div class="input-section">
<label class="input-label">选择音色</label>
<VoiceSelector
:synth-text="store.text"
:speech-rate="store.speechRate"
@select="store.setVoice"
@audio-generated="handleAudioGenerated"
/>
2026-02-26 18:52:09 +08:00
</div>
2026-03-05 22:58:31 +08:00
<!-- 语速调节 -->
<div class="input-section">
<label class="input-label">语速调节</label>
<div class="rate-control">
<a-slider
v-model:value="store.speechRate"
:min="0.5"
:max="2.0"
:step="0.1"
:marks="rateMarks"
class="rate-slider"
/>
<span class="rate-value">{{ store.speechRate.toFixed(1) }}x</span>
</div>
2026-02-26 18:52:09 +08:00
</div>
2026-03-05 22:58:31 +08:00
<!-- 时间轴对比 -->
<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"
/>
</section>
2026-03-05 21:01:34 +08:00
</div>
2025-12-01 22:27:50 +08:00
</div>
2026-02-12 23:35:39 +08:00
<!-- 视频选择器弹窗 -->
2026-02-26 18:52:09 +08:00
<VideoSelector
v-model:open="store.videoSelectorVisible"
@select="store.selectVideo"
/>
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-26 18:52:09 +08:00
import { ref, computed, onMounted } from 'vue'
2026-03-05 21:01:34 +08:00
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
import VideoSelector from '@/components/VideoSelector.vue'
import VoiceSelector from '@/components/VoiceSelector.vue'
2026-03-05 21:01:34 +08:00
import TimelinePanel from './components/TimelinePanel.vue'
2026-03-05 22:58:31 +08:00
import TextGeneratePopup from './components/TextGeneratePopup.vue'
2026-03-05 21:01:34 +08:00
import {
CloudUploadOutlined,
FolderOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
ReloadOutlined,
PlayCircleOutlined,
CheckCircleFilled,
ExclamationCircleFilled,
2026-03-05 22:58:31 +08:00
LoadingOutlined,
EditOutlined,
2026-03-05 21:01:34 +08:00
} from '@ant-design/icons-vue'
2026-02-26 18:52:09 +08:00
import { useDigitalHumanStore } from './stores/useDigitalHumanStore'
2026-03-05 21:01:34 +08:00
import { useUserStore } from '@/stores/user'
import { usePointsConfigStore } from '@/stores/pointsConfig'
import { useVoiceCopyStore } from '@/stores/voiceCopy'
import { formatDuration, formatDurationMs, formatFileSize } from './utils/format'
2025-12-22 00:15:02 +08:00
2026-02-26 18:52:09 +08:00
const store = useDigitalHumanStore()
2026-02-24 22:11:30 +08:00
const userStore = useUserStore()
2026-02-26 20:04:09 +08:00
const pointsConfigStore = usePointsConfigStore()
2026-03-05 21:01:34 +08:00
const voiceStore = useVoiceCopyStore()
2026-02-26 18:52:09 +08:00
const dragOver = ref(false)
const fileInput = ref<HTMLInputElement | null>(null)
2026-03-05 22:58:31 +08:00
const textGenerateVisible = ref(false)
2025-12-28 13:49:45 +08:00
2026-03-05 21:01:34 +08:00
const estimatedPoints = computed(() => {
const points = pointsConfigStore.getConsumePoints('kling')
return points ?? 150
})
const suggestedChars = computed(() => {
return Math.floor((store.faceDurationMs || 10000) / 1000 * 4)
})
2026-03-05 22:58:31 +08:00
const placeholder = computed(() =>
store.faceDurationMs > 0
? `请输入播报文案,建议不超过 ${suggestedChars.value} 字以确保与视频匹配`
: '请输入你想让角色说话的内容'
)
2026-02-12 23:35:39 +08:00
2026-03-05 21:01:34 +08:00
const rateMarks = {
0.5: '0.5x',
1.0: '1.0x',
1.5: '1.5x',
2.0: '2.0x',
}
2026-02-26 20:04:09 +08:00
2026-02-26 18:52:09 +08:00
function triggerFileSelect() {
2026-02-12 23:35:39 +08:00
fileInput.value?.click()
}
2026-02-26 18:52:09 +08:00
function handleFileSelect(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0]
2026-03-05 21:01:34 +08:00
if (file) {
store.handleFileUpload(file)
}
2026-02-26 18:52:09 +08:00
}
function handleDrop(e: DragEvent) {
dragOver.value = false
const file = e.dataTransfer?.files[0]
2026-03-05 21:01:34 +08:00
if (file) {
store.handleFileUpload(file)
}
2026-02-26 18:52:09 +08:00
}
2026-03-05 21:01:34 +08:00
function clearVideo() {
if (store.videoPreviewUrl?.startsWith('blob:')) {
URL.revokeObjectURL(store.videoPreviewUrl)
}
store.videoFile = null
store.selectedVideo = null
store.videoPreviewUrl = ''
store.videoSource = null
store.resetProcess()
2026-02-26 18:52:09 +08:00
}
2026-03-05 22:58:31 +08:00
function handleAudioGenerated(data: { durationMs: number; audioBase64: string }) {
if (store.timeline && data.durationMs > 0) {
store.timeline.audioDurationMs = data.durationMs
2026-02-26 18:52:09 +08:00
}
2026-03-05 22:58:31 +08:00
if (data.audioBase64) {
store.audioData = {
audioBase64: data.audioBase64,
format: 'mp3',
durationMs: data.durationMs
}
store.audioStep = 'generated'
}
}
function openTextGeneratePopup() {
textGenerateVisible.value = true
}
function handleTextGenerated(text: string) {
store.text = text
2026-02-12 23:35:39 +08:00
}
2025-12-01 22:27:50 +08:00
onMounted(async () => {
2026-02-25 18:21:25 +08:00
await Promise.all([
voiceStore.refresh(),
2026-02-26 20:04:09 +08:00
userStore.fetchUserProfile(),
2026-03-05 21:01:34 +08:00
pointsConfigStore.loadConfig(),
2026-02-25 18:21:25 +08:00
])
2025-12-01 22:27:50 +08:00
})
</script>
2025-12-28 13:49:45 +08:00
<style scoped lang="less">
2026-03-05 22:58:31 +08:00
// ========================================
// Notion + VoiceSelector 蓝紫主题
// ========================================
// Color Palette - 与 VoiceSelector 协调
@bg-page: #fafbfc;
@bg-block: #ffffff;
@text-primary: #1e293b;
@text-secondary: #64748b;
@text-tertiary: #94a3b8;
@border-light: rgba(59, 130, 246, 0.1);
@border-medium: rgba(59, 130, 246, 0.2);
// 蓝紫渐变主题色 (与 VoiceSelector 一致)
@accent-blue: #3b82f6;
@accent-purple: #8b5cf6;
@accent-gradient: linear-gradient(135deg, @accent-blue 0%, @accent-purple 100%);
@accent-green: #10b981;
@accent-red: #ef4444;
@accent-orange: #f59e0b;
// Shadows - 柔和蓝调
@shadow-sm: 0 1px 2px rgba(59, 130, 246, 0.04);
@shadow-md: 0 4px 12px rgba(59, 130, 246, 0.08);
@shadow-lg: 0 8px 24px rgba(59, 130, 246, 0.12);
// Typography
@font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif;
.notion-page {
2026-01-18 00:34:04 +08:00
min-height: 100vh;
2026-03-05 22:58:31 +08:00
background: @bg-page;
padding: 48px 64px;
max-width: 1400px;
margin: 0 auto;
@media (max-width: 1024px) {
padding: 32px 24px;
}
2026-01-18 00:34:04 +08:00
}
2026-03-05 22:58:31 +08:00
// Page Header
2026-03-05 21:01:34 +08:00
.page-header {
2026-03-05 22:58:31 +08:00
margin-bottom: 40px;
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
.header-icon {
font-size: 56px;
margin-bottom: 16px;
line-height: 1;
}
.header-title {
font-family: @font-sans;
font-size: 40px;
2026-03-05 21:01:34 +08:00
font-weight: 700;
2026-03-05 22:58:31 +08:00
color: @text-primary;
2026-03-05 21:01:34 +08:00
margin: 0 0 8px 0;
2026-03-05 22:58:31 +08:00
letter-spacing: -0.5px;
line-height: 1.2;
2026-01-18 00:34:04 +08:00
}
2026-03-05 22:58:31 +08:00
.header-desc {
font-size: 16px;
color: @text-secondary;
2026-03-05 21:01:34 +08:00
margin: 0;
2026-03-05 22:58:31 +08:00
line-height: 1.5;
2026-01-18 00:34:04 +08:00
}
2025-12-01 22:27:50 +08:00
}
2026-03-05 22:58:31 +08:00
// Main Content Layout
2026-03-05 21:01:34 +08:00
.main-content {
2026-03-05 22:58:31 +08:00
display: grid;
grid-template-columns: 1fr 1fr;
2026-03-05 21:01:34 +08:00
gap: 24px;
2026-03-05 22:58:31 +08:00
margin-bottom: 32px;
2026-02-12 23:35:39 +08:00
2026-03-05 21:01:34 +08:00
@media (max-width: 1024px) {
2026-03-05 22:58:31 +08:00
grid-template-columns: 1fr;
2026-02-12 23:35:39 +08:00
}
2025-12-01 22:27:50 +08:00
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
// Content Block
.content-block {
background: @bg-block;
2026-03-05 21:01:34 +08:00
border-radius: 12px;
2026-03-05 22:58:31 +08:00
padding: 24px;
box-shadow: @shadow-md;
border: 1px solid @border-light;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
&:hover {
box-shadow: @shadow-lg;
border-color: rgba(59, 130, 246, 0.2);
}
2025-12-01 22:27:50 +08:00
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
// Block Header
.block-header {
2026-02-12 23:35:39 +08:00
display: flex;
align-items: center;
2026-03-05 21:01:34 +08:00
gap: 10px;
margin-bottom: 20px;
2026-03-05 22:58:31 +08:00
padding-bottom: 14px;
border-bottom: 1px solid rgba(59, 130, 246, 0.08);
2026-02-12 23:35:39 +08:00
2026-03-05 22:58:31 +08:00
.block-emoji {
2026-03-05 21:01:34 +08:00
font-size: 20px;
2026-03-05 22:58:31 +08:00
line-height: 1;
2026-02-12 23:35:39 +08:00
}
2026-03-05 22:58:31 +08:00
.block-title {
font-size: 15px;
2026-02-12 23:35:39 +08:00
font-weight: 600;
2026-03-05 22:58:31 +08:00
color: @text-primary;
2026-03-05 21:01:34 +08:00
margin: 0;
2026-02-12 23:35:39 +08:00
}
}
2026-03-05 22:58:31 +08:00
// Source Toggle
.source-toggle {
display: flex;
gap: 8px;
margin-bottom: 20px;
background: @bg-page;
padding: 4px;
border-radius: 6px;
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
.toggle-btn {
flex: 1;
display: flex;
align-items: center;
2026-03-05 21:01:34 +08:00
justify-content: center;
gap: 8px;
2026-03-05 22:58:31 +08:00
padding: 10px 16px;
border: none;
background: transparent;
2026-03-05 21:01:34 +08:00
border-radius: 8px;
2026-03-05 22:58:31 +08:00
font-size: 13px;
font-weight: 500;
color: @text-secondary;
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-03-05 22:58:31 +08:00
color: @text-primary;
background: rgba(59, 130, 246, 0.05);
2026-01-18 00:34:04 +08:00
}
2026-03-05 22:58:31 +08:00
&.active {
background: @bg-block;
color: @accent-blue;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.12);
2026-01-18 00:34:04 +08:00
}
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
// Upload Area
.upload-area {
2026-03-05 21:01:34 +08:00
min-height: 200px;
2026-03-05 22:58:31 +08:00
border: 1.5px dashed rgba(59, 130, 246, 0.2);
2026-03-05 21:01:34 +08:00
border-radius: 10px;
2026-03-05 22:58:31 +08:00
background: rgba(59, 130, 246, 0.02);
transition: all 0.25s ease;
2026-03-05 21:01:34 +08:00
&.dragover {
2026-03-05 22:58:31 +08:00
border-color: @accent-blue;
background: rgba(59, 130, 246, 0.06);
}
&.has-video {
border-style: solid;
border-color: rgba(59, 130, 246, 0.1);
background: @bg-block;
2026-03-05 21:01:34 +08:00
}
}
2026-03-05 22:58:31 +08:00
.hidden-input {
2026-03-05 21:01:34 +08:00
display: none;
}
2026-02-12 23:35:39 +08:00
2026-03-05 22:58:31 +08:00
.upload-empty {
2026-03-05 21:01:34 +08:00
display: flex;
flex-direction: column;
align-items: center;
2026-03-05 22:58:31 +08:00
justify-content: center;
min-height: 200px;
2026-03-05 21:01:34 +08:00
cursor: pointer;
2026-03-05 22:58:31 +08:00
padding: 24px;
2026-03-05 21:01:34 +08:00
}
2026-02-12 23:35:39 +08:00
2026-03-05 22:58:31 +08:00
.upload-icon-wrapper {
width: 56px;
height: 56px;
border-radius: 50%;
background: rgba(59, 130, 246, 0.08);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
transition: all 0.25s ease;
.upload-icon {
font-size: 24px;
color: @accent-blue;
}
}
.upload-empty:hover .upload-icon-wrapper {
background: rgba(59, 130, 246, 0.12);
transform: scale(1.05);
2026-03-05 21:01:34 +08:00
}
.upload-text {
2026-03-05 22:58:31 +08:00
font-size: 15px;
2026-03-05 21:01:34 +08:00
font-weight: 500;
2026-03-05 22:58:31 +08:00
color: @text-primary;
margin-bottom: 6px;
2026-03-05 21:01:34 +08:00
}
.upload-hint {
2026-03-05 22:58:31 +08:00
font-size: 13px;
color: @text-tertiary;
2026-03-05 21:01:34 +08:00
}
.upload-preview {
2026-03-05 22:58:31 +08:00
padding: 16px;
2026-03-05 21:01:34 +08:00
}
.preview-video {
width: 100%;
2026-03-05 22:58:31 +08:00
max-height: 260px;
border-radius: 4px;
background: #191919;
2026-03-05 21:01:34 +08:00
}
2026-03-05 22:58:31 +08:00
.replace-btn {
display: inline-flex;
2026-03-05 21:01:34 +08:00
align-items: center;
gap: 6px;
margin-top: 12px;
2026-03-05 22:58:31 +08:00
padding: 8px 14px;
2026-03-05 21:01:34 +08:00
font-size: 13px;
2026-03-05 22:58:31 +08:00
font-weight: 500;
color: @text-secondary;
background: transparent;
border: 1px solid rgba(59, 130, 246, 0.15);
border-radius: 8px;
2026-03-05 21:01:34 +08:00
cursor: pointer;
transition: all 0.2s ease;
&:hover {
2026-03-05 22:58:31 +08:00
color: @accent-blue;
border-color: @accent-blue;
background: rgba(59, 130, 246, 0.05);
2026-02-26 18:52:09 +08:00
}
2026-02-12 23:35:39 +08:00
}
2026-03-05 22:58:31 +08:00
// Library Video Preview
.library-preview {
padding: 16px;
background: rgba(59, 130, 246, 0.03);
border-radius: 10px;
border: 1px solid rgba(59, 130, 246, 0.1);
.preview-video {
width: 100%;
max-height: 260px;
border-radius: 4px;
background: #191919;
}
.video-meta {
margin-top: 12px;
}
.replace-btn {
margin-top: 8px;
}
}
2026-02-12 23:35:39 +08:00
2026-03-05 22:58:31 +08:00
.video-thumbnail {
width: 120px;
height: 68px;
border-radius: 4px;
overflow: hidden;
2026-03-05 22:58:31 +08:00
background: #191919;
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
2026-03-05 22:58:31 +08:00
.video-meta {
flex: 1;
min-width: 0;
}
2026-02-12 23:35:39 +08:00
2026-03-05 22:58:31 +08:00
.video-name {
font-size: 14px;
2026-02-12 23:35:39 +08:00
font-weight: 500;
2026-03-05 22:58:31 +08:00
color: @text-primary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
2026-03-05 22:58:31 +08:00
margin-bottom: 4px;
max-width: 180px;
}
2026-02-12 23:35:39 +08:00
2026-03-05 22:58:31 +08:00
.video-info {
font-size: 12px;
color: @text-tertiary;
}
2026-03-05 22:58:31 +08:00
// Process Status
.process-status {
margin-top: 14px;
2026-03-05 21:01:34 +08:00
padding: 12px 16px;
2026-03-05 22:58:31 +08:00
background: rgba(59, 130, 246, 0.04);
2026-03-05 21:01:34 +08:00
border-radius: 8px;
2026-02-12 23:35:39 +08:00
2026-03-05 21:01:34 +08:00
&.recognized {
2026-03-05 22:58:31 +08:00
background: rgba(16, 185, 129, 0.08);
2026-01-18 00:34:04 +08:00
}
2026-03-05 21:01:34 +08:00
&.error {
2026-03-05 22:58:31 +08:00
background: rgba(239, 68, 68, 0.08);
2026-03-05 21:01:34 +08:00
}
2026-02-12 23:35:39 +08:00
}
2026-03-05 22:58:31 +08:00
.status-row {
2026-01-18 00:34:04 +08:00
display: flex;
align-items: center;
2026-03-05 22:58:31 +08:00
gap: 10px;
2026-03-05 21:01:34 +08:00
font-size: 13px;
2026-03-05 22:58:31 +08:00
color: @text-secondary;
2026-03-05 21:01:34 +08:00
&.success {
2026-03-05 22:58:31 +08:00
color: @accent-green;
2026-03-05 21:01:34 +08:00
}
&.error {
2026-03-05 22:58:31 +08:00
color: @accent-red;
2026-03-05 21:01:34 +08:00
}
2026-02-12 23:35:39 +08:00
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
.link-btn {
margin-left: auto;
padding: 0;
2026-02-12 23:35:39 +08:00
font-size: 13px;
font-weight: 500;
2026-03-05 22:58:31 +08:00
color: @accent-blue;
background: none;
border: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}
2026-02-12 23:35:39 +08:00
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
// Input Section
.input-section {
margin-bottom: 24px;
}
.input-label {
display: block;
font-size: 12px;
font-weight: 600;
color: @text-secondary;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.label-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
2026-02-12 23:35:39 +08:00
}
2026-03-05 22:58:31 +08:00
.notion-textarea {
2026-03-05 21:01:34 +08:00
width: 100%;
2026-02-12 23:35:39 +08:00
2026-03-05 21:01:34 +08:00
:deep(.ant-input) {
border: none;
border-radius: 8px;
font-size: 14px;
2026-03-05 22:58:31 +08:00
line-height: 1.6;
padding: 14px 16px;
background: rgba(59, 130, 246, 0.03);
color: @text-primary;
&::placeholder {
color: @text-tertiary;
}
2026-03-05 21:01:34 +08:00
&:focus {
2026-03-05 22:58:31 +08:00
background: @bg-block;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2);
2026-03-05 21:01:34 +08:00
}
2026-01-18 00:34:04 +08:00
}
2026-03-05 22:58:31 +08:00
:deep(.ant-input-data-count) {
color: @text-tertiary;
font-size: 12px;
}
2025-12-28 13:49:45 +08:00
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
.input-footer {
2026-01-18 00:34:04 +08:00
display: flex;
2026-03-05 22:58:31 +08:00
align-items: center;
gap: 4px;
2026-03-05 21:01:34 +08:00
font-size: 12px;
2026-03-05 22:58:31 +08:00
color: @text-tertiary;
2026-03-05 21:01:34 +08:00
margin-top: 8px;
2025-12-28 13:49:45 +08:00
}
2026-03-05 22:58:31 +08:00
.generate-text-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: transparent;
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: @accent-blue;
cursor: pointer;
transition: all 0.2s ease;
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
&:hover {
background: rgba(59, 130, 246, 0.06);
border-color: rgba(59, 130, 246, 0.3);
}
2026-02-26 18:52:09 +08:00
}
2026-03-05 22:58:31 +08:00
// Rate Control
2026-03-05 21:01:34 +08:00
.rate-control {
2026-02-26 18:52:09 +08:00
display: flex;
2026-03-05 21:01:34 +08:00
align-items: center;
gap: 16px;
2026-02-26 18:52:09 +08:00
}
2026-03-05 21:01:34 +08:00
.rate-slider {
flex: 1;
2026-03-05 22:58:31 +08:00
:deep(.ant-slider-rail) {
background: rgba(59, 130, 246, 0.1);
}
:deep(.ant-slider-track) {
background: @accent-gradient;
}
:deep(.ant-slider-handle) {
border-color: @accent-blue;
&:hover, &:focus {
border-color: @accent-blue;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
}
}
2026-03-05 21:01:34 +08:00
:deep(.ant-slider-mark-text) {
2026-03-05 22:58:31 +08:00
font-size: 11px;
color: @text-tertiary;
2026-03-05 21:01:34 +08:00
}
2026-02-26 18:52:09 +08:00
}
2026-03-05 21:01:34 +08:00
.rate-value {
2026-02-26 18:52:09 +08:00
font-size: 14px;
2026-03-05 21:01:34 +08:00
font-weight: 600;
2026-03-05 22:58:31 +08:00
color: @text-primary;
2026-03-05 21:01:34 +08:00
min-width: 40px;
text-align: right;
2026-02-26 18:52:09 +08:00
}
2026-03-05 22:58:31 +08:00
// 生成按钮 - 左右布局,右下角
.generate-action {
margin-top: auto;
padding-top: 16px;
display: flex;
justify-content: flex-end;
2026-02-26 18:52:09 +08:00
}
2026-03-05 22:58:31 +08:00
.result-banner {
2026-02-26 18:52:09 +08:00
display: flex;
align-items: center;
2026-03-05 22:58:31 +08:00
justify-content: flex-end;
gap: 10px;
padding: 10px 14px;
border-radius: 8px;
2026-02-26 18:52:09 +08:00
font-size: 13px;
2026-03-05 22:58:31 +08:00
font-weight: 500;
2026-02-26 18:52:09 +08:00
2026-03-05 22:58:31 +08:00
&.success {
background: rgba(16, 185, 129, 0.08);
color: @accent-green;
2026-02-12 23:35:39 +08:00
}
2026-03-05 22:58:31 +08:00
&.error {
background: rgba(239, 68, 68, 0.08);
color: @accent-red;
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
.banner-icon {
2026-03-05 21:01:34 +08:00
font-size: 16px;
2026-03-05 22:58:31 +08:00
flex-shrink: 0;
2026-03-05 21:01:34 +08:00
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
.banner-text {
flex: 1;
text-align: right;
2026-02-12 23:35:39 +08:00
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
.retry-btn {
padding: 4px 12px;
border-radius: 6px;
border: 1px solid currentColor;
background: transparent;
color: inherit;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
&:hover {
background: rgba(16, 185, 129, 0.1);
}
}
2025-12-28 13:49:45 +08:00
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
.generate-btn {
position: relative;
2026-02-12 23:35:39 +08:00
display: flex;
2026-03-05 21:01:34 +08:00
align-items: center;
2026-03-05 22:58:31 +08:00
justify-content: space-between;
padding: 10px 14px;
border: none;
border-radius: 8px;
background: @accent-gradient;
color: white;
cursor: pointer;
overflow: hidden;
transition: all 0.25s ease;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.25);
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
&:hover:not(.disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.35);
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
&:active:not(.disabled) {
transform: translateY(0);
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
&.disabled {
background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 100%);
cursor: not-allowed;
box-shadow: 0 2px 8px rgba(148, 163, 184, 0.25);
}
2026-03-05 21:01:34 +08:00
2026-03-05 22:58:31 +08:00
.btn-glow {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.15),
transparent
);
animation: glow-slide 3s ease-in-out infinite;
}
2026-03-05 21:01:34 +08:00
2026-03-05 22:58:31 +08:00
.btn-left {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
position: relative;
z-index: 1;
2026-02-12 23:35:39 +08:00
}
2026-03-05 22:58:31 +08:00
.btn-icon {
font-size: 15px;
2026-01-18 00:34:04 +08:00
}
2026-03-05 22:58:31 +08:00
.btn-spin {
font-size: 13px;
animation: spin 1s linear infinite;
}
2026-03-05 21:01:34 +08:00
2026-03-05 22:58:31 +08:00
.btn-right {
display: flex;
align-items: baseline;
gap: 2px;
padding: 2px 8px;
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
font-size: 10px;
position: relative;
z-index: 1;
}
.cost-num {
font-size: 12px;
font-weight: 700;
}
.cost-label {
opacity: 0.9;
2026-01-18 00:34:04 +08:00
}
2026-03-05 21:01:34 +08:00
}
2026-03-05 22:58:31 +08:00
@keyframes glow-slide {
0% {
left: -100%;
}
50%, 100% {
left: 100%;
}
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
2026-01-18 00:34:04 +08:00
}
2026-03-05 21:01:34 +08:00
}
2026-01-18 00:34:04 +08:00
2026-03-05 22:58:31 +08:00
.link-btn {
padding: 0;
font-size: 13px;
font-weight: 500;
color: inherit;
background: none;
border: none;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
&:hover {
opacity: 0.8;
2025-12-28 13:49:45 +08:00
}
}
2025-12-01 22:27:50 +08:00
</style>