feat(ui): replace Ant Design components with shadcn-vue components in content analysis views
- Replace Ant Design buttons and empty state components with shadcn-vue Button and custom empty state using lucide icons - Update ExpandedRowContent.vue to use new component library with consistent styling - Migrate task center components to use new UI framework - Add proper loading states and disabled button handling - Refactor modal dialog structure and styling for better consistency
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { CopyOutlined, SaveOutlined } from '@ant-design/icons-vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import ChatMessageRendererV2 from '@/components/ChatMessageRendererV2.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -26,22 +27,20 @@ function handleCreateContent() {
|
||||
|
||||
<template>
|
||||
<div class="expanded-content">
|
||||
<div v-if="!record.transcriptions && !record.prompt" class="no-analysis-tip">
|
||||
<a-empty description="该视频尚未分析">
|
||||
<template #image>
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="20" y="30" width="80" height="60" rx="4" stroke="currentColor" stroke-width="2" fill="none" opacity="0.3"/>
|
||||
<circle cx="40" cy="50" r="8" fill="currentColor" opacity="0.4"/>
|
||||
<rect x="54" y="47" width="40" height="6" rx="3" fill="currentColor" opacity="0.4"/>
|
||||
<rect x="54" y="60" width="32" height="6" rx="3" fill="currentColor" opacity="0.4"/>
|
||||
</svg>
|
||||
</template>
|
||||
<a-button type="primary" @click="$emit('analyze', record)" :loading="record._analyzing">
|
||||
{{ record._analyzing ? '分析中…' : '开始分析' }}
|
||||
</a-button>
|
||||
</a-empty>
|
||||
<!-- 未分析状态 -->
|
||||
<div v-if="!record.transcriptions && !record.prompt" class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<Icon icon="lucide:video-off" class="size-10 text-muted-foreground/40" />
|
||||
</div>
|
||||
<p class="text-muted-foreground mb-4">该视频尚未分析</p>
|
||||
<Button @click="$emit('analyze', record)" :disabled="record._analyzing">
|
||||
<Icon v-if="record._analyzing" icon="lucide:loader-2" class="size-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:play" class="size-4" />
|
||||
{{ record._analyzing ? '分析中…' : '开始分析' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 已分析内容 -->
|
||||
<div v-else class="two-col">
|
||||
<section class="col left-col">
|
||||
<div class="sub-title">原配音</div>
|
||||
@@ -63,34 +62,33 @@ function handleCreateContent() {
|
||||
</div>
|
||||
|
||||
<div class="right-actions">
|
||||
<a-space>
|
||||
<a-button
|
||||
size="small"
|
||||
type="text"
|
||||
class="copy-btn"
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
title="复制"
|
||||
@click="handleCopy">
|
||||
<template #icon>
|
||||
<CopyOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button
|
||||
@click="handleCopy"
|
||||
>
|
||||
<Icon icon="lucide:copy" class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="record.prompt"
|
||||
size="small"
|
||||
type="text"
|
||||
class="save-server-btn"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
title="保存"
|
||||
@click="handleSaveToServer">
|
||||
<template #icon>
|
||||
<SaveOutlined />
|
||||
</template>
|
||||
@click="handleSaveToServer"
|
||||
>
|
||||
<Icon icon="lucide:save" class="size-4" />
|
||||
保存
|
||||
</a-button>
|
||||
<a-button
|
||||
type="dashed"
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="!record.prompt || record._analyzing"
|
||||
@click="handleCreateContent">基于提示词去创作</a-button>
|
||||
</a-space>
|
||||
@click="handleCreateContent"
|
||||
>
|
||||
基于提示词去创作
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -99,10 +97,29 @@ function handleCreateContent() {
|
||||
|
||||
<style scoped>
|
||||
.expanded-content {
|
||||
padding: var(--space-4);
|
||||
padding: 16px;
|
||||
background: var(--color-gray-50);
|
||||
border-radius: var(--radius-card);
|
||||
margin: var(--space-2) 0;
|
||||
border-radius: 12px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-gray-100);
|
||||
border-radius: 50%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
@@ -112,79 +129,59 @@ function handleCreateContent() {
|
||||
}
|
||||
|
||||
.col {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border: 1px solid var(--color-gray-100);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.left-col .transcript-content {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-card);
|
||||
padding: var(--space-2);
|
||||
color: var(--color-gray-600);
|
||||
background: var(--color-gray-50);
|
||||
border: 1px dashed var(--color-gray-200);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-gray-800);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.no-transcript {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.prompt-display-wrapper {
|
||||
min-height: 200px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
background: var(--color-surface);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-card);
|
||||
padding: var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
background: var(--color-gray-50);
|
||||
border: 1px dashed var(--color-gray-200);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.no-prompt {
|
||||
padding: var(--space-4);
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
margin-top: var(--space-2);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.no-analysis-tip {
|
||||
padding: var(--space-8) var(--space-5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-analysis-tip :deep(.ant-empty) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.no-analysis-tip :deep(.ant-empty-description) {
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--space-4);
|
||||
@media (max-width: 768px) {
|
||||
.two-col {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,781 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'HomeBusiness'
|
||||
})
|
||||
|
||||
const kpis = ref([
|
||||
{ label: '企业客户', value: '1,200+' },
|
||||
{ label: '月度生成内容', value: '3,5M+' },
|
||||
{ label: '平均提效', value: '↑ 67%' },
|
||||
])
|
||||
|
||||
const features = ref([
|
||||
{ title: '内容风格洞察', desc: '结构化拆解头部账号,提炼风格要素与提示词', icon: '📊' },
|
||||
{ title: '热点趋势分析', desc: '全网热度追踪,关联话题、时段与平台建议', icon: '📈' },
|
||||
{ title: 'AI文案与脚本', desc: '场景化模板与知识库支持,一键生成可用稿', icon: '📝' },
|
||||
{ title: '数字人&配音', desc: '声音训练、语音合成、数字人出镜一体化', icon: '🎙️' },
|
||||
])
|
||||
|
||||
// 轮播数据(商务风渐变 + 文案)
|
||||
const banners = ref([
|
||||
{ id: 1, title: '内容风格分析', desc: '结构特征与关键词洞察', color: 'from-indigo-600 via-indigo-500 to-sky-500' },
|
||||
{ id: 2, title: '热点趋势分析', desc: '热度变化与预测建议', color: 'from-fuchsia-600 via-fuchsia-500 to-pink-500' },
|
||||
{ id: 3, title: '数字人与配音', desc: '声音训练与数字人视频', color: 'from-emerald-600 via-emerald-500 to-teal-500' },
|
||||
])
|
||||
const active = ref(0)
|
||||
let timer = null
|
||||
onMounted(() => {
|
||||
timer = setInterval(() => {
|
||||
active.value = (active.value + 1) % banners.value.length
|
||||
}, 4500)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4 home">
|
||||
<!-- Hero 区:大气商务风 -->
|
||||
<section class="hero-section fade-in">
|
||||
<div class="hero-bg"></div>
|
||||
<div class="hero-ornaments">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="grid-lines"></div>
|
||||
</div>
|
||||
<div class="hero-gradient">
|
||||
<div class="container">
|
||||
<div class="hero-grid">
|
||||
<!-- 左侧文案 -->
|
||||
<div class="hero-left">
|
||||
<div class="badge">
|
||||
<span class="badge-dot"></span>
|
||||
企业级内容智能平台
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
让每一次创作,兼具<span class="gradient-text">效率</span>与<span class="gradient-text-2">品质</span>
|
||||
</h1>
|
||||
<p class="hero-desc">
|
||||
金牌内容大师面向品牌与机构,提供从策略到产出的全链路内容能力:对标洞察、趋势分析、AI脚本、数字人及配音,助力营销持续增长。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<button class="btn-primary">
|
||||
立即体验
|
||||
</button>
|
||||
<button class="btn-ghost">
|
||||
安全与合规
|
||||
</button>
|
||||
</div>
|
||||
<!-- 关键信息 KPI -->
|
||||
<div class="kpi-grid">
|
||||
<div v-for="k in kpis" :key="k.label" class="antd-card glass kpi-item">
|
||||
<div class="kpi-value">{{ k.value }}</div>
|
||||
<div class="kpi-label">{{ k.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧示意卡片 -->
|
||||
<div class="hero-right">
|
||||
<!-- 轮播图(大气商务风) -->
|
||||
<div class="carousel antd-shadow">
|
||||
<div v-for="(b, i) in banners" :key="b.id" class="banner"
|
||||
:class="['banner--' + b.id, { 'is-active': i === active }]">
|
||||
<div class="banner-content">
|
||||
<div class="banner-text">
|
||||
<div class="banner-title">{{ b.title }}</div>
|
||||
<div class="banner-desc">{{ b.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 指示点 -->
|
||||
<div class="dots">
|
||||
<button v-for="(b, i) in banners" :key="'dot-' + b.id" class="dot"
|
||||
:class="{ 'dot--active': i === active }" @click="active = i" aria-label="切换轮播"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 核心能力栅格 -->
|
||||
<section class="core-section fade-in">
|
||||
<div class="features-grid">
|
||||
<div v-for="f in features" :key="f.title" class="antd-card glass feature-item">
|
||||
<div class="feature-icon">{{ f.icon }}</div>
|
||||
<div class="feature-title">{{ f.title }}</div>
|
||||
<div class="feature-desc">{{ f.desc }}</div>
|
||||
<div class="feature-link">了解更多</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 品牌背书与合规提示 -->
|
||||
<section class="brand-compliance fade-in">
|
||||
<div class="brand-card antd-card">
|
||||
<div class="brand-title">服务过的行业伙伴</div>
|
||||
<div class="logos-grid">
|
||||
<div v-for="i in 12" :key="i" class="logo-item antd-card">LOGO</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compliance-card antd-shadow">
|
||||
<div class="compliance-title">数据与合规</div>
|
||||
<ul class="compliance-list">
|
||||
<li>· 严格遵循平台与数据合规要求</li>
|
||||
<li>· 支持企业私有化部署与访问控制</li>
|
||||
<li>· 可定制审计留痕与内容风控</li>
|
||||
</ul>
|
||||
<button class="btn-primary compliance-button">查看白皮书</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* antd 阴影与卡片风格 */
|
||||
.antd-shadow {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.antd-shadow:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.antd-card {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.antd-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
|
||||
transform: translateY(-1px);
|
||||
border-color: #e6f4ff;
|
||||
}
|
||||
|
||||
/* 按钮风格(主色 #1890FF,hover #40A9FF) */
|
||||
.btn-primary {
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(90deg, #1890FF, #40A9FF);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.35);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px) scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.45);
|
||||
filter: brightness(1.02);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: #1890FF;
|
||||
font-weight: 600;
|
||||
border: 1px solid #91D5FF;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background-color: #E6F7FF;
|
||||
}
|
||||
|
||||
/* 页面进入动画与可交互动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
}
|
||||
|
||||
/* ======= 首页自定义样式(替代 Tailwind) ======= */
|
||||
.home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.hero-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(900px 300px at 0% 0%, rgba(24, 144, 255, 0.08), transparent);
|
||||
}
|
||||
|
||||
/* 装饰性模糊色块与细网格,增强时尚层次感 */
|
||||
.hero-ornaments {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-ornaments .blob {
|
||||
position: absolute;
|
||||
filter: blur(40px);
|
||||
opacity: 0.5;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.hero-ornaments .blob-1 {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
left: -40px;
|
||||
top: -60px;
|
||||
background: radial-gradient(circle at 30% 30%, rgba(99,102,241,.55), rgba(14,165,233,.25));
|
||||
}
|
||||
|
||||
.hero-ornaments .blob-2 {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
right: -60px;
|
||||
bottom: -80px;
|
||||
background: radial-gradient(circle at 70% 70%, rgba(16,185,129,.5), rgba(20,184,166,.25));
|
||||
}
|
||||
|
||||
.hero-ornaments .grid-lines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: linear-gradient(rgba(24,144,255,0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(24,144,255,0.06) 1px, transparent 1px);
|
||||
background-size: 24px 24px, 24px 24px;
|
||||
-webkit-mask-image: radial-gradient(ellipse at center, black 40%, transparent 70%);
|
||||
mask-image: radial-gradient(ellipse at center, black 40%, transparent 70%);
|
||||
}
|
||||
|
||||
.hero-gradient {
|
||||
background: linear-gradient(180deg, #E6F7FF 0%, #FFFFFF 100%);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 40px 16px;
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding: 48px 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
padding: 64px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
gap: 40px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.hero-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: #1890FF;
|
||||
background: #E6F7FF;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
box-shadow: inset 0 0 0 1px #91D5FF;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #1890FF;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
margin-top: 16px;
|
||||
color: #111827;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
font-size: 24px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.hero-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.hero-title {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #1890FF;
|
||||
}
|
||||
|
||||
.text-accent {
|
||||
color: #FA8C16;
|
||||
}
|
||||
|
||||
/* 渐变文本:更具时尚感 */
|
||||
.gradient-text {
|
||||
background: linear-gradient(90deg, #1677ff, #22d3ee);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.gradient-text-2 {
|
||||
background: linear-gradient(90deg, #f97316, #ef4444);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
margin-top: 16px;
|
||||
font-size: 16px;
|
||||
color: #4B5563;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
margin-top: 32px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.kpi-item {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kpi-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.carousel {
|
||||
position: relative;
|
||||
height: 224px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(145, 213, 255, 0.4);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.carousel {
|
||||
height: 256px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.carousel {
|
||||
height: 288px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.carousel {
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.banner {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.7s ease-in-out;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.banner.is-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #111827;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 玻璃覆层叠加彩色背景,弱化纯色感 */
|
||||
.banner-content::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.18), rgba(255,255,255,0.06));
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.banner-text {
|
||||
padding-left: 40px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.banner-text {
|
||||
padding-left: 48px;
|
||||
padding-right: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.banner-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-desc {
|
||||
margin-top: 8px;
|
||||
font-size: 16px;
|
||||
color: rgba(55, 65, 81, 0.9);
|
||||
}
|
||||
|
||||
.banner--1 .banner-content {
|
||||
background: linear-gradient(90deg, #4F46E5 0%, #6366F1 50%, #0EA5E9 100%);
|
||||
}
|
||||
|
||||
.banner--2 .banner-content {
|
||||
background: linear-gradient(90deg, #A21CAF 0%, #D946EF 50%, #EC4899 100%);
|
||||
}
|
||||
|
||||
.banner--3 .banner-content {
|
||||
background: linear-gradient(90deg, #059669 0%, #10B981 50%, #14B8A6 100%);
|
||||
}
|
||||
|
||||
.dots {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 999px;
|
||||
background: #91D5FF;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dot--active {
|
||||
width: 24px;
|
||||
background: #1890FF;
|
||||
}
|
||||
|
||||
.core-section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(17,24,39,.12);
|
||||
border-color: rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
margin-top: 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
margin-top: 4px;
|
||||
font-size: 16px;
|
||||
color: #4B5563;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: #1890FF;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: none;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.feature-link::after {
|
||||
content: "→";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: transform .2s ease;
|
||||
}
|
||||
|
||||
.feature-link:hover::after {
|
||||
transform: translate(3px, -50%);
|
||||
}
|
||||
|
||||
.brand-compliance {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.brand-compliance {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-card {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 14px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.logos-grid {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.logos-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.logos-grid {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-item {
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #6B7280;
|
||||
filter: grayscale(0.9);
|
||||
transition: filter .2s ease, transform .2s ease, box-shadow .2s ease;
|
||||
}
|
||||
|
||||
.compliance-card {
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #E6F7FF 0%, #FFFFFF 100%);
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(145, 213, 255, 0.6);
|
||||
}
|
||||
|
||||
.logo-item:hover {
|
||||
filter: grayscale(0);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* 玻璃拟态:统一视觉语汇 */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
-webkit-backdrop-filter: saturate(140%) blur(8px);
|
||||
backdrop-filter: saturate(140%) blur(8px);
|
||||
border-color: rgba(145, 213, 255, 0.6);
|
||||
}
|
||||
|
||||
/* 入场动画:特性卡和KPI稍后淡入上移,强化层次 */
|
||||
@keyframes floatIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.kpi-item, .feature-item { opacity: 0; animation: floatIn .45s ease forwards; }
|
||||
.kpi-item:nth-child(1), .feature-item:nth-child(1) { animation-delay: .02s; }
|
||||
.kpi-item:nth-child(2), .feature-item:nth-child(2) { animation-delay: .08s; }
|
||||
.kpi-item:nth-child(3), .feature-item:nth-child(3) { animation-delay: .14s; }
|
||||
.feature-item:nth-child(4) { animation-delay: .2s; }
|
||||
|
||||
.compliance-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.compliance-list {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.compliance-button {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
/* 暗色覆盖(不破坏原有类名,基于变量快速换肤) */
|
||||
.home {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 卡片与玻璃风格在暗色下的表现 */
|
||||
.home .antd-card { background: var(--color-surface); border-color: var(--color-border); }
|
||||
.home .glass { background: rgba(26,26,26,0.66); border-color: var(--color-border); }
|
||||
|
||||
/* 英雄区与渐变、徽标与说明文字 */
|
||||
.home .hero-gradient { background: linear-gradient(180deg, rgba(26,102,224,0.10) 0%, rgba(0,0,0,0) 60%) !important; }
|
||||
.home .hero-title { color: var(--color-text) !important; }
|
||||
.home .hero-desc { color: var(--color-text-secondary) !important; }
|
||||
.home .badge { color: var(--color-blue) !important; background: rgba(26,102,224,0.12) !important; box-shadow: inset 0 0 0 1px rgba(26,102,224,0.35) !important; }
|
||||
.home .badge-dot { background: var(--color-blue) !important; }
|
||||
|
||||
/* KPI 与特性卡片文案色 */
|
||||
.home .kpi-value { color: var(--color-text) !important; }
|
||||
.home .kpi-label { color: var(--color-text-secondary) !important; }
|
||||
.home .feature-title { color: var(--color-text) !important; }
|
||||
.home .feature-desc { color: var(--color-text-secondary) !important; }
|
||||
.home .feature-link { color: var(--color-blue) !important; }
|
||||
|
||||
/* 轮播与横幅 */
|
||||
.home .carousel { border-color: var(--color-border) !important; }
|
||||
.home .banner-content { color: var(--color-text) !important; }
|
||||
.home .banner-content::after { background: linear-gradient(180deg, rgba(0,0,0,0.12), rgba(0,0,0,0.06)) !important; }
|
||||
|
||||
/* 品牌与合规区 */
|
||||
.home .brand-title { color: var(--color-text-secondary) !important; }
|
||||
.home .logo-item { color: var(--color-text-secondary) !important; }
|
||||
.home .compliance-card { background: var(--color-surface) !important; border-color: var(--color-border) !important; }
|
||||
.home .compliance-title { color: var(--color-text) !important; }
|
||||
.home .compliance-list { color: var(--color-text-secondary) !important; }
|
||||
|
||||
/* 主按钮与次要按钮 */
|
||||
.home .btn-primary { background: var(--color-primary) !important; color: #fff !important; box-shadow: var(--glow-primary) !important; }
|
||||
.home .btn-primary:hover { box-shadow: var(--glow-primary) !important; filter: brightness(1.03) !important; }
|
||||
.home .btn-ghost { background: var(--color-surface) !important; color: var(--color-text-secondary) !important; border: 1px solid var(--color-border) !important; }
|
||||
.home .btn-ghost:hover { background: #161616 !important; color: var(--color-text) !important; box-shadow: var(--glow-primary) !important; }
|
||||
|
||||
/* 生成内容区(若有) */
|
||||
.home .generated-content :deep(h1) { color: var(--color-text) !important; }
|
||||
.home .generated-content :deep(h2) { color: var(--color-text) !important; }
|
||||
.home .generated-content :deep(h3) { color: var(--color-text-secondary) !important; }
|
||||
.home .generated-content :deep(p) { color: var(--color-text-secondary) !important; }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,78 +3,75 @@
|
||||
<!-- 顶部操作栏 -->
|
||||
<header class="page-header">
|
||||
<div class="header-left">
|
||||
<a-button type="text" @click="router.push('/material/list')">
|
||||
<template #icon><ArrowLeftOutlined /></template>
|
||||
<Button variant="ghost" size="sm" @click="router.push('/material/list')">
|
||||
<Icon icon="lucide:arrow-left" class="mr-1" />
|
||||
返回
|
||||
</a-button>
|
||||
<a-divider type="vertical" />
|
||||
</Button>
|
||||
<div class="divider-vertical" />
|
||||
<h1 class="page-title">智能混剪</h1>
|
||||
<a-tag color="processing" size="small">{{ sceneCount }} 场景</a-tag>
|
||||
<Badge variant="secondary">{{ sceneCount }} 场景</Badge>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<!-- 核心参数 -->
|
||||
<div class="param-item">
|
||||
<span class="param-label">总时长</span>
|
||||
<a-slider
|
||||
v-model:value="formData.totalDuration"
|
||||
<Slider
|
||||
v-model="formData.totalDuration"
|
||||
:min="15"
|
||||
:max="30"
|
||||
:step="1"
|
||||
:tooltip-visible="false"
|
||||
style="width: 100px;"
|
||||
class="w-24"
|
||||
/>
|
||||
<span class="param-value">{{ formData.totalDuration }}s</span>
|
||||
</div>
|
||||
|
||||
<a-divider type="vertical" />
|
||||
<div class="divider-vertical" />
|
||||
|
||||
<div class="param-item">
|
||||
<span class="param-label">切片</span>
|
||||
<a-slider
|
||||
v-model:value="formData.clipDuration"
|
||||
<Slider
|
||||
v-model="formData.clipDuration"
|
||||
:min="3"
|
||||
:max="5"
|
||||
:step="1"
|
||||
:tooltip-visible="false"
|
||||
style="width: 80px;"
|
||||
class="w-20"
|
||||
/>
|
||||
<span class="param-value">{{ formData.clipDuration }}s</span>
|
||||
</div>
|
||||
|
||||
<a-divider type="vertical" />
|
||||
<div class="divider-vertical" />
|
||||
|
||||
<div class="param-item">
|
||||
<span class="param-label">生成</span>
|
||||
<a-select
|
||||
v-model:value="formData.produceCount"
|
||||
size="small"
|
||||
style="width: 70px;"
|
||||
@change="saveProduceCount"
|
||||
>
|
||||
<a-select-option :value="1">1 个</a-select-option>
|
||||
<a-select-option :value="3">3 个</a-select-option>
|
||||
<a-select-option :value="5">5 个</a-select-option>
|
||||
<a-select-option :value="10">10 个</a-select-option>
|
||||
<a-select-option :value="15">15 个</a-select-option>
|
||||
</a-select>
|
||||
<Select v-model="formData.produceCount" @update:model-value="saveProduceCount">
|
||||
<SelectTrigger class="w-16 h-7">
|
||||
<SelectValue placeholder="选择" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="1">1 个</SelectItem>
|
||||
<SelectItem :value="3">3 个</SelectItem>
|
||||
<SelectItem :value="5">5 个</SelectItem>
|
||||
<SelectItem :value="10">10 个</SelectItem>
|
||||
<SelectItem :value="15">15 个</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<a-button @click="showSettings = true">
|
||||
<template #icon><SettingOutlined /></template>
|
||||
<Button variant="outline" size="sm" @click="showSettings = true">
|
||||
<Icon icon="lucide:settings" class="mr-1" />
|
||||
高级设置
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="uiState.submitting"
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="!canSubmit"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<template #icon><RocketOutlined /></template>
|
||||
<Icon v-if="!uiState.submitting" icon="lucide:rocket" class="mr-1" />
|
||||
<Loader2Icon v-else class="mr-1 h-4 w-4 animate-spin" />
|
||||
开始混剪
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -84,7 +81,7 @@
|
||||
<div class="progress-fill" :style="{ width: progressPercent + '%' }" />
|
||||
</div>
|
||||
<span class="progress-text">
|
||||
<CheckCircleFilled v-if="filledCount === sceneCount" class="icon-complete" />
|
||||
<CheckCircleIcon v-if="filledCount === sceneCount" class="icon-complete" />
|
||||
<span v-else class="icon-pending">{{ filledCount }}/{{ sceneCount }}</span>
|
||||
场景已就绪
|
||||
</span>
|
||||
@@ -95,18 +92,18 @@
|
||||
<section class="scenes-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">
|
||||
<PlaySquareOutlined />
|
||||
<Icon icon="lucide:play-square" />
|
||||
场景编排
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<a-button size="small" @click="clearScenes">
|
||||
<template #icon><ClearOutlined /></template>
|
||||
<Button variant="ghost" size="sm" @click="clearScenes">
|
||||
<Icon icon="lucide:eraser" class="mr-1" />
|
||||
清空
|
||||
</a-button>
|
||||
<a-button size="small" type="primary" ghost @click="autoFillAllScenes">
|
||||
<template #icon><ThunderboltOutlined /></template>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="autoFillAllScenes">
|
||||
<Icon icon="lucide:zap" class="mr-1" />
|
||||
一键填充
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,19 +126,17 @@
|
||||
<!-- 场景头部 -->
|
||||
<div class="scene-header">
|
||||
<div class="scene-drag">
|
||||
<HolderOutlined />
|
||||
<Icon icon="lucide:grip-vertical" />
|
||||
</div>
|
||||
<div class="scene-index">{{ index + 1 }}</div>
|
||||
<div class="scene-duration">{{ formData.clipDuration }}s</div>
|
||||
<div class="scene-status">
|
||||
<a-badge
|
||||
:count="scene.candidates?.length || 0"
|
||||
:number-style="{
|
||||
backgroundColor: scene.candidates?.length
|
||||
? 'var(--color-primary-500)'
|
||||
: 'var(--color-gray-300)'
|
||||
}"
|
||||
/>
|
||||
<Badge
|
||||
:variant="scene.candidates?.length ? 'default' : 'secondary'"
|
||||
class="px-2"
|
||||
>
|
||||
{{ scene.candidates?.length || 0 }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,11 +158,11 @@
|
||||
:src="getFileById(candidate.fileId).imgUrl"
|
||||
/>
|
||||
<div v-else class="thumb-placeholder">
|
||||
<VideoCameraOutlined />
|
||||
<Icon icon="lucide:video" />
|
||||
</div>
|
||||
<div class="thumb-order">{{ cIndex + 1 }}</div>
|
||||
<div class="thumb-remove" @click.stop="removeCandidate(index, cIndex)">
|
||||
<CloseOutlined />
|
||||
<Icon icon="lucide:x" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -177,13 +172,13 @@
|
||||
class="candidate-add"
|
||||
@click.stop="openSceneSelector(index)"
|
||||
>
|
||||
<PlusOutlined />
|
||||
<Icon icon="lucide:plus" />
|
||||
<span>继续添加</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="scene-empty" @click.stop="openSceneSelector(index)">
|
||||
<PlusCircleOutlined />
|
||||
<Icon icon="lucide:plus-circle" />
|
||||
<span>点击添加候选素材</span>
|
||||
<span class="empty-hint">最多 {{ constants.MAX_CANDIDATES_PER_SCENE }} 个</span>
|
||||
</div>
|
||||
@@ -193,12 +188,12 @@
|
||||
<!-- 场景底部:素材来源(只读) -->
|
||||
<div class="scene-footer">
|
||||
<span class="source-label">来源:</span>
|
||||
<a-tag
|
||||
size="small"
|
||||
:color="scene.groupId ? 'blue' : 'default'"
|
||||
<Badge
|
||||
:variant="scene.groupId ? 'default' : 'outline'"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ getSceneEffectiveGroupName(scene) }}
|
||||
</a-tag>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -208,40 +203,72 @@
|
||||
</main>
|
||||
|
||||
<!-- 高级设置抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="showSettings"
|
||||
title="高级设置"
|
||||
placement="right"
|
||||
:width="320"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="裁剪模式">
|
||||
<a-radio-group v-model:value="formData.cropMode" button-style="solid">
|
||||
<a-radio-button value="center">居中裁剪</a-radio-button>
|
||||
<a-radio-button value="fill">填充模式</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<Sheet v-model:open="showSettings" style="padding: 0 16px;">
|
||||
<SheetContent class="w-80">
|
||||
<SheetHeader class="pb-4 border-b border-border">
|
||||
<SheetTitle class="text-lg">高级设置</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-form-item label="统计信息">
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ sceneCount }}</span>
|
||||
<span class="stat-label">场景数</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ filledCount }}</span>
|
||||
<span class="stat-label">已填充</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ actualTotalDuration }}s</span>
|
||||
<span class="stat-label">总时长</span>
|
||||
<div class="py-6 space-y-8">
|
||||
<!-- 裁剪模式 - 胶囊式切换 -->
|
||||
<div class="space-y-3">
|
||||
<Label class="text-sm font-medium text-foreground">裁剪模式</Label>
|
||||
<div class="crop-mode-switcher">
|
||||
<button
|
||||
class="crop-mode-btn"
|
||||
:class="{ 'crop-mode-btn--active': formData.cropMode === 'center' }"
|
||||
@click="formData.cropMode = 'center'"
|
||||
>
|
||||
<Icon icon="lucide:crop" class="size-4" />
|
||||
<span>居中裁剪</span>
|
||||
</button>
|
||||
<button
|
||||
class="crop-mode-btn"
|
||||
:class="{ 'crop-mode-btn--active': formData.cropMode === 'fill' }"
|
||||
@click="formData.cropMode = 'fill'"
|
||||
>
|
||||
<Icon icon="lucide:maximize-2" class="size-4" />
|
||||
<span>填充模式</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
<!-- 统计信息 - 现代卡片 -->
|
||||
<div class="space-y-3">
|
||||
<Label class="text-sm font-medium text-foreground">统计信息</Label>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__icon stat-card__icon--blue">
|
||||
<Icon icon="lucide:layers" class="size-4" />
|
||||
</div>
|
||||
<div class="stat-card__content">
|
||||
<span class="stat-card__value">{{ sceneCount }}</span>
|
||||
<span class="stat-card__label">场景数</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__icon stat-card__icon--green">
|
||||
<Icon icon="lucide:check-circle" class="size-4" />
|
||||
</div>
|
||||
<div class="stat-card__content">
|
||||
<span class="stat-card__value">{{ filledCount }}</span>
|
||||
<span class="stat-card__label">已填充</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card--wide">
|
||||
<div class="stat-card__icon stat-card__icon--purple">
|
||||
<Icon icon="lucide:clock" class="size-4" />
|
||||
</div>
|
||||
<div class="stat-card__content">
|
||||
<span class="stat-card__value">{{ actualTotalDuration }}<small>s</small></span>
|
||||
<span class="stat-card__label">总时长</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<!-- 场景选择弹窗 -->
|
||||
<SceneSelectorModal
|
||||
@@ -269,24 +296,30 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useRouter } from 'vue-router'
|
||||
import draggable from 'vuedraggable'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { Loader2Icon, CheckCircleIcon } from 'lucide-vue-next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
SettingOutlined,
|
||||
RocketOutlined,
|
||||
PlaySquareOutlined,
|
||||
ClearOutlined,
|
||||
ThunderboltOutlined,
|
||||
HolderOutlined,
|
||||
PlusOutlined,
|
||||
PlusCircleOutlined,
|
||||
CheckCircleFilled,
|
||||
VideoCameraOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle
|
||||
} from '@/components/ui/sheet'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
|
||||
import { MaterialService, MaterialGroupService } from '@/api/material'
|
||||
import { MixTaskService } from '@/api/mixTask'
|
||||
@@ -329,8 +362,8 @@ const showSettings = ref(false)
|
||||
const formData = ref({
|
||||
title: '',
|
||||
produceCount: Number(localStorage.getItem('mix-produce-count')) || 3,
|
||||
totalDuration: Number(localStorage.getItem('mix-total-duration')) || 15,
|
||||
clipDuration: Number(localStorage.getItem('mix-clip-duration')) || 5,
|
||||
totalDuration: [Number(localStorage.getItem('mix-total-duration')) || 15],
|
||||
clipDuration: [Number(localStorage.getItem('mix-clip-duration')) || 5],
|
||||
cropMode: localStorage.getItem('mix-crop-mode') || 'center'
|
||||
})
|
||||
|
||||
@@ -355,11 +388,11 @@ const dataState = ref({
|
||||
// 计算属性
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
const sceneCount = computed(() =>
|
||||
Math.floor(formData.value.totalDuration / formData.value.clipDuration)
|
||||
Math.floor(formData.value.totalDuration[0] / formData.value.clipDuration[0])
|
||||
)
|
||||
|
||||
const actualTotalDuration = computed(() =>
|
||||
sceneCount.value * formData.value.clipDuration
|
||||
sceneCount.value * formData.value.clipDuration[0]
|
||||
)
|
||||
|
||||
const filledCount = computed(() =>
|
||||
@@ -416,7 +449,7 @@ watch(sceneCount, (newCount) => {
|
||||
for (let i = current; i < newCount; i++) {
|
||||
dataState.value.scenes.push({
|
||||
index: i,
|
||||
duration: formData.value.clipDuration,
|
||||
duration: formData.value.clipDuration[0],
|
||||
groupId: null,
|
||||
candidates: []
|
||||
})
|
||||
@@ -431,12 +464,12 @@ watch([
|
||||
() => formData.value.clipDuration,
|
||||
() => formData.value.totalDuration
|
||||
], ([clipDuration, totalDuration]) => {
|
||||
localStorage.setItem('mix-clip-duration', clipDuration.toString())
|
||||
localStorage.setItem('mix-total-duration', totalDuration.toString())
|
||||
localStorage.setItem('mix-clip-duration', clipDuration[0].toString())
|
||||
localStorage.setItem('mix-total-duration', totalDuration[0].toString())
|
||||
dataState.value.scenes.forEach(scene => {
|
||||
scene.duration = clipDuration
|
||||
scene.duration = clipDuration[0]
|
||||
})
|
||||
})
|
||||
}, { deep: true })
|
||||
|
||||
const onSceneDragEnd = () => {
|
||||
dataState.value.scenes.forEach((scene, index) => {
|
||||
@@ -447,16 +480,16 @@ const onSceneDragEnd = () => {
|
||||
const clearScenes = () => {
|
||||
dataState.value.scenes = Array(sceneCount.value).fill(null).map((_, index) => ({
|
||||
index,
|
||||
duration: formData.value.clipDuration,
|
||||
duration: formData.value.clipDuration[0],
|
||||
groupId: null,
|
||||
candidates: []
|
||||
}))
|
||||
message.success('已清空所有场景')
|
||||
toast.success('已清空所有场景')
|
||||
}
|
||||
|
||||
const removeCandidate = (sceneIndex, candidateIndex) => {
|
||||
dataState.value.scenes[sceneIndex]?.candidates?.splice(candidateIndex, 1)
|
||||
message.success('已移除候选')
|
||||
toast.success('已移除候选')
|
||||
}
|
||||
|
||||
const openSceneSelector = (index) => {
|
||||
@@ -471,7 +504,7 @@ const openSceneSelector = (index) => {
|
||||
const autoFillAllScenes = async () => {
|
||||
// 检查是否有分组
|
||||
if (dataState.value.groupList.length === 0) {
|
||||
message.warning('暂无素材分组,请先创建分组')
|
||||
toast.warning('暂无素材分组,请先创建分组')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -513,9 +546,9 @@ const doAutoFill = async () => {
|
||||
}
|
||||
|
||||
if (filled > 0) {
|
||||
message.success(`已为 ${filled} 个场景填充候选素材`)
|
||||
toast.success(`已为 ${filled} 个场景填充候选素材`)
|
||||
} else {
|
||||
message.info('所有场景已满')
|
||||
toast.info('所有场景已满')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +563,7 @@ const loadGroups = async () => {
|
||||
dataState.value.groupList = res.data || []
|
||||
}
|
||||
} catch {
|
||||
message.error('加载分组失败')
|
||||
toast.error('加载分组失败')
|
||||
} finally {
|
||||
uiState.value.loadingGroups = false
|
||||
}
|
||||
@@ -552,7 +585,7 @@ const handleLibraryGroupChange = async (groupId) => {
|
||||
dataState.value.allGroupFiles[groupId] = res.data.list || []
|
||||
}
|
||||
} catch {
|
||||
message.error('加载素材失败')
|
||||
toast.error('加载素材失败')
|
||||
} finally {
|
||||
uiState.value.loadingFiles = false
|
||||
}
|
||||
@@ -578,7 +611,7 @@ const loadGroupFiles = async (groupId) => {
|
||||
return files
|
||||
}
|
||||
} catch {
|
||||
message.error('加载素材失败')
|
||||
toast.error('加载素材失败')
|
||||
}
|
||||
return []
|
||||
}
|
||||
@@ -599,7 +632,7 @@ const handleModalConfirm = ({ sceneIndex, fileIds, setGroupId }) => {
|
||||
scene.groupId = setGroupId
|
||||
}
|
||||
|
||||
message.success(`场景 ${sceneIndex + 1} 已设置 ${scene.candidates.length} 个候选`)
|
||||
toast.success(`场景 ${sceneIndex + 1} 已设置 ${scene.candidates.length} 个候选`)
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -627,10 +660,10 @@ const handleSubmit = async () => {
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
message.success('混剪任务创建成功!')
|
||||
toast.success('混剪任务创建成功!')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('提交失败:' + error.message)
|
||||
toast.error('提交失败:' + error.message)
|
||||
} finally {
|
||||
uiState.value.submitting = false
|
||||
}
|
||||
@@ -731,30 +764,6 @@ onMounted(() => {
|
||||
color: @primary;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
:deep(.ant-slider) {
|
||||
margin: 0;
|
||||
|
||||
.ant-slider-track {
|
||||
background: @primary;
|
||||
}
|
||||
|
||||
.ant-slider-handle {
|
||||
border-color: @primary;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-select) {
|
||||
.ant-select-selector {
|
||||
height: 24px !important;
|
||||
line-height: 22px !important;
|
||||
padding: 0 var(--space-2) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
line-height: 22px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,6 +774,12 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.divider-vertical {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: @border;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 进度条 - 状态指示器
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -847,10 +862,6 @@ onMounted(() => {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
color: @text;
|
||||
|
||||
.anticon {
|
||||
color: @primary;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
@@ -1075,11 +1086,6 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-base) ease;
|
||||
|
||||
.anticon {
|
||||
font-size: 28px;
|
||||
color: @text-disabled;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: var(--font-size-xs);
|
||||
color: @text-disabled;
|
||||
@@ -1089,10 +1095,6 @@ onMounted(() => {
|
||||
border-color: @primary;
|
||||
color: @primary;
|
||||
background: @primary-light;
|
||||
|
||||
.anticon {
|
||||
color: @primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1114,32 +1116,175 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 统计信息 - 抽屉内
|
||||
// 高级设置抽屉 - 现代化样式
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
.settings-section {
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: var(--space-3);
|
||||
background: var(--color-gray-100);
|
||||
border-radius: var(--radius-button);
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: var(--font-size-xl);
|
||||
&-title {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: @primary;
|
||||
color: @text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 现代化 Toggle 按钮组
|
||||
.toggle-group {
|
||||
display: flex;
|
||||
background: var(--color-gray-100);
|
||||
border-radius: 10px;
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: 8px;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-600);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover:not(&--active) {
|
||||
color: @text;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: white;
|
||||
color: @primary;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
// 现代化统计卡片网格
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.7) 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&--wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--blue {
|
||||
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
|
||||
color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: var(--font-size-xs);
|
||||
color: @text-muted;
|
||||
margin-top: var(--space-1);
|
||||
&--green {
|
||||
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
|
||||
color: var(--color-success-500);
|
||||
}
|
||||
|
||||
&--purple {
|
||||
background: linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%);
|
||||
color: #a855f7;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: @text;
|
||||
line-height: 1.2;
|
||||
|
||||
small {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-500);
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
&--wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 胶囊式裁剪模式切换器
|
||||
.crop-mode-switcher {
|
||||
display: flex;
|
||||
background: var(--color-gray-100);
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.crop-mode-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-600);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover:not(&--active) {
|
||||
color: @text;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: white;
|
||||
color: var(--color-primary-500);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,52 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="选择素材分组"
|
||||
:width="400"
|
||||
:footer="null"
|
||||
centered
|
||||
@update:open="$emit('update:visible', $event)"
|
||||
>
|
||||
<div class="group-select-content">
|
||||
<p class="group-select-hint">请选择用于填充的素材分组:</p>
|
||||
<div class="group-select-list">
|
||||
<div
|
||||
v-for="group in groupList"
|
||||
:key="group.id"
|
||||
class="group-select-item"
|
||||
:class="{ 'group-select-item--active': tempSelectedId === group.id }"
|
||||
@click="tempSelectedId = group.id"
|
||||
>
|
||||
<FolderOutlined class="group-icon" />
|
||||
<span class="group-name">{{ group.name }}</span>
|
||||
<CheckOutlined v-if="tempSelectedId === group.id" class="group-check" />
|
||||
<Dialog :open="visible" @update:open="$emit('update:visible', $event)">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择素材分组</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="py-4">
|
||||
<p class="text-sm text-muted-foreground mb-4">请选择用于填充的素材分组:</p>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="group in groupList"
|
||||
:key="group.id"
|
||||
class="group-select-item"
|
||||
:class="{ 'group-select-item--active': tempSelectedId === group.id }"
|
||||
@click="tempSelectedId = group.id"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<Icon icon="lucide:folder" class="size-5 text-muted-foreground" />
|
||||
<span class="flex-1 text-left">{{ group.name }}</span>
|
||||
<Icon
|
||||
v-if="tempSelectedId === group.id"
|
||||
icon="lucide:check"
|
||||
class="size-4 text-primary"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-select-footer">
|
||||
<a-button @click="$emit('update:visible', false)">取消</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="$emit('update:visible', false)">取消</Button>
|
||||
<Button
|
||||
:disabled="!tempSelectedId"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确认填充
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { FolderOutlined, CheckOutlined } from '@ant-design/icons-vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
@@ -64,72 +72,24 @@ const handleConfirm = () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.group-select-content {
|
||||
.group-select-hint {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
.group-select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.group-select-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.group-select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
&--active {
|
||||
border-color: #1890ff;
|
||||
background: #e6f7ff;
|
||||
|
||||
.group-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.group-icon {
|
||||
font-size: 18px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.group-check {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.group-select-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
&--active {
|
||||
background: var(--color-primary-50);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,124 +1,112 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
:footer="null"
|
||||
:closable="false"
|
||||
width="800px"
|
||||
centered
|
||||
class="scene-selector-modal"
|
||||
@update:open="$emit('update:visible', $event)"
|
||||
>
|
||||
<!-- 顶部 -->
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<div class="scene-badge">场景 {{ sceneIndex + 1 }}</div>
|
||||
<div class="counter">
|
||||
<span class="counter-current">{{ tempSelectedFiles.length }}</span>
|
||||
<span class="counter-sep">/</span>
|
||||
<span class="counter-max">{{ maxCandidates }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分组切换 -->
|
||||
<div v-if="groupList.length > 1" class="group-tabs">
|
||||
<div
|
||||
v-for="g in groupList"
|
||||
:key="g.id"
|
||||
class="group-tab-item"
|
||||
:class="{ 'group-tab-item--active': selectorGroupId === g.id }"
|
||||
@click="handleSelectorGroupChange(g.id)"
|
||||
>
|
||||
{{ g.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-button type="text" size="small" @click="$emit('update:visible', false)">
|
||||
<CloseOutlined />
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<a-space size="small">
|
||||
<a-button size="small" @click="handleSelectAll">
|
||||
<template #icon><CheckSquareOutlined /></template>
|
||||
全选
|
||||
</a-button>
|
||||
<a-button size="small" @click="handleClearSelection">
|
||||
<template #icon><CloseSquareOutlined /></template>
|
||||
清空
|
||||
</a-button>
|
||||
<a-button size="small" type="primary" ghost @click="handleAutoFill">
|
||||
<template #icon><ThunderboltOutlined /></template>
|
||||
随机
|
||||
</a-button>
|
||||
</a-space>
|
||||
<span class="toolbar-hint">点击选择 · 双击确认 · Enter 提交</span>
|
||||
</div>
|
||||
|
||||
<!-- 素材网格 -->
|
||||
<div class="material-grid">
|
||||
<template v-if="selectorFiles.length > 0">
|
||||
<div
|
||||
v-for="(file, index) in selectorFiles"
|
||||
:key="file.id"
|
||||
class="material-card"
|
||||
:class="{ 'material-card--selected': isSelected(file.id) }"
|
||||
:style="{ animationDelay: `${index * 20}ms` }"
|
||||
@click="toggleSelection(file)"
|
||||
@dblclick="handleQuickConfirm(file)"
|
||||
>
|
||||
<div class="card-cover">
|
||||
<img v-if="file.imgUrl" :src="file.imgUrl" />
|
||||
<div v-else class="cover-placeholder">
|
||||
<VideoCameraOutlined />
|
||||
</div>
|
||||
|
||||
<!-- 选中标记 -->
|
||||
<div v-if="isSelected(file.id)" class="card-check">
|
||||
<CheckOutlined />
|
||||
</div>
|
||||
|
||||
<!-- 序号 -->
|
||||
<div v-if="isSelected(file.id)" class="card-order">
|
||||
{{ getSelectionOrder(file.id) }}
|
||||
</div>
|
||||
<Dialog :open="visible" @update:open="$emit('update:visible', $event)">
|
||||
<DialogContent class="scene-selector-dialog sm:max-w-[800px] p-0 gap-0">
|
||||
<!-- 顶部 -->
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<div class="scene-badge">场景 {{ sceneIndex + 1 }}</div>
|
||||
<div class="counter">
|
||||
<span class="counter-current">{{ tempSelectedFiles.length }}</span>
|
||||
<span class="counter-sep">/</span>
|
||||
<span class="counter-max">{{ maxCandidates }}</span>
|
||||
</div>
|
||||
<div class="card-name">{{ file.displayName || file.fileName }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-empty v-else description="暂无素材" />
|
||||
</div>
|
||||
<!-- 分组切换 -->
|
||||
<div v-if="groupList.length > 1" class="group-tabs">
|
||||
<button
|
||||
v-for="g in groupList"
|
||||
:key="g.id"
|
||||
class="group-tab-item"
|
||||
:class="{ 'group-tab-item--active': selectorGroupId === g.id }"
|
||||
@click="handleSelectorGroupChange(g.id)"
|
||||
>
|
||||
{{ g.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<div class="modal-footer">
|
||||
<a-space>
|
||||
<a-button @click="$emit('update:visible', false)">取消</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="tempSelectedFiles.length === 0"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
<template #icon><CheckOutlined /></template>
|
||||
确认选择
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" variant="outline" @click="handleSelectAll">
|
||||
<Icon icon="lucide:check-square" class="size-4" />
|
||||
全选
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" @click="handleClearSelection">
|
||||
<Icon icon="lucide:x-square" class="size-4" />
|
||||
清空
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" @click="handleAutoFill">
|
||||
<Icon icon="lucide:zap" class="size-4" />
|
||||
随机
|
||||
</Button>
|
||||
</div>
|
||||
<span class="toolbar-hint">点击选择 · 双击确认 · Enter 提交</span>
|
||||
</div>
|
||||
|
||||
<!-- 素材网格 -->
|
||||
<div class="material-grid">
|
||||
<template v-if="selectorFiles.length > 0">
|
||||
<button
|
||||
v-for="(file, index) in selectorFiles"
|
||||
:key="file.id"
|
||||
class="material-card"
|
||||
:class="{ 'material-card--selected': isSelected(file.id) }"
|
||||
:style="{ animationDelay: `${index * 20}ms` }"
|
||||
@click="toggleSelection(file)"
|
||||
@dblclick="handleQuickConfirm(file)"
|
||||
>
|
||||
<div class="card-cover">
|
||||
<img v-if="file.imgUrl" :src="file.imgUrl" />
|
||||
<div v-else class="cover-placeholder">
|
||||
<Icon icon="lucide:video" class="size-6" />
|
||||
</div>
|
||||
|
||||
<!-- 选中标记 -->
|
||||
<div v-if="isSelected(file.id)" class="card-check">
|
||||
<Icon icon="lucide:check" class="size-3" />
|
||||
</div>
|
||||
|
||||
<!-- 序号 -->
|
||||
<div v-if="isSelected(file.id)" class="card-order">
|
||||
{{ getSelectionOrder(file.id) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-name">{{ file.displayName || file.fileName }}</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<Icon icon="lucide:image-off" class="size-12 text-muted-foreground/50" />
|
||||
<p class="text-muted-foreground text-sm">暂无素材</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<div class="modal-footer">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button variant="outline" @click="$emit('update:visible', false)">取消</Button>
|
||||
<Button
|
||||
:disabled="tempSelectedFiles.length === 0"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
<Icon icon="lucide:check" class="size-4" />
|
||||
确认选择
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
VideoCameraOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
CheckSquareOutlined,
|
||||
CloseSquareOutlined,
|
||||
ThunderboltOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
@@ -205,7 +193,7 @@ const toggleSelection = (file) => {
|
||||
if (tempSelectedFiles.value.length < props.maxCandidates) {
|
||||
tempSelectedFiles.value.push(file.id)
|
||||
} else {
|
||||
message.warning(`最多选择 ${props.maxCandidates} 个素材`)
|
||||
toast.warning(`最多选择 ${props.maxCandidates} 个素材`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +217,7 @@ const handleAutoFill = () => {
|
||||
)
|
||||
|
||||
if (availableMaterials.length === 0) {
|
||||
message.warning('没有更多可用素材')
|
||||
toast.warning('没有更多可用素材')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -237,14 +225,14 @@ const handleAutoFill = () => {
|
||||
const targetCount = Math.min(3, maxAddCount)
|
||||
|
||||
if (targetCount <= 0) {
|
||||
message.warning('已达到上限')
|
||||
toast.warning('已达到上限')
|
||||
return
|
||||
}
|
||||
|
||||
const shuffled = [...availableMaterials].sort(() => Math.random() - 0.5)
|
||||
const selected = shuffled.slice(0, targetCount)
|
||||
tempSelectedFiles.value.push(...selected.map(m => m.id))
|
||||
message.success(`已随机选择 ${selected.length} 个素材`)
|
||||
toast.success(`已随机选择 ${selected.length} 个素材`)
|
||||
}
|
||||
|
||||
const handleSelectorGroupChange = (groupId) => {
|
||||
@@ -259,7 +247,7 @@ const handleSelectorGroupChange = (groupId) => {
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (tempSelectedFiles.value.length === 0) {
|
||||
message.warning('请至少选择一个素材')
|
||||
toast.warning('请至少选择一个素材')
|
||||
return
|
||||
}
|
||||
emit('confirm', {
|
||||
@@ -273,40 +261,12 @@ const handleConfirm = () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 弹窗样式 - 使用项目设计系统
|
||||
// 弹窗样式 - 现代化设计
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// 🎨 设计令牌引用
|
||||
@primary: var(--color-primary-500);
|
||||
@primary-light: var(--color-primary-50);
|
||||
@primary-hover: var(--color-primary-400);
|
||||
|
||||
@success: var(--color-success-500);
|
||||
@success-light: var(--color-success-50);
|
||||
|
||||
@warning: var(--color-warning-500);
|
||||
|
||||
@text: var(--color-text);
|
||||
@text-secondary: var(--color-text-secondary);
|
||||
@text-muted: var(--color-text-muted);
|
||||
@text-disabled: var(--color-text-disabled);
|
||||
|
||||
@bg-card: var(--color-bg-card);
|
||||
@bg-elevated: var(--color-gray-100);
|
||||
|
||||
@border: var(--color-border);
|
||||
@border-light: var(--color-gray-100);
|
||||
|
||||
.scene-selector-modal {
|
||||
:deep(.ant-modal-content) {
|
||||
padding: 0;
|
||||
border-radius: var(--radius-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-modal-body) {
|
||||
padding: 0;
|
||||
}
|
||||
.scene-selector-dialog {
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -316,24 +276,24 @@ const handleConfirm = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-4) var(--space-4);
|
||||
background: @bg-elevated;
|
||||
border-bottom: 1px solid @border-light;
|
||||
padding: 16px 20px;
|
||||
background: var(--color-gray-50);
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.scene-badge {
|
||||
padding: var(--space-1) var(--space-3);
|
||||
background: @primary-light;
|
||||
padding: 4px 12px;
|
||||
background: var(--color-primary-50);
|
||||
border: 1px solid var(--color-primary-200);
|
||||
border-radius: var(--radius-tag);
|
||||
font-size: var(--font-size-sm);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: @primary;
|
||||
color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
.counter {
|
||||
@@ -344,49 +304,49 @@ const handleConfirm = () => {
|
||||
.counter-current {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: @primary;
|
||||
color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
.counter-sep {
|
||||
color: @text-disabled;
|
||||
color: var(--color-gray-300);
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.counter-max {
|
||||
font-size: var(--font-size-base);
|
||||
color: @text-muted;
|
||||
font-size: 14px;
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
}
|
||||
|
||||
.group-tabs {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.group-tab-item {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: @text-secondary;
|
||||
color: var(--color-gray-500);
|
||||
background: var(--color-gray-100);
|
||||
border-radius: var(--radius-button);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-base) ease;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-gray-200);
|
||||
color: @text;
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: @primary;
|
||||
color: var(--color-text-inverse);
|
||||
background: var(--color-primary-500);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: @primary-hover;
|
||||
background: var(--color-primary-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,13 +359,13 @@ const handleConfirm = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background: @bg-card;
|
||||
border-bottom: 1px solid @border-light;
|
||||
padding: 12px 20px;
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
|
||||
.toolbar-hint {
|
||||
font-size: var(--font-size-xs);
|
||||
color: @text-muted;
|
||||
font-size: 12px;
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,42 +375,43 @@ const handleConfirm = () => {
|
||||
.material-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
min-height: 360px;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
background: @bg-card;
|
||||
background: white;
|
||||
align-content: start;
|
||||
|
||||
&:deep(.ant-empty) {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 320px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray-300);
|
||||
background: var(--color-gray-200);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 320px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.material-card {
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-button);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: @bg-elevated;
|
||||
background: var(--color-gray-50);
|
||||
border: 2px solid transparent;
|
||||
transition: all var(--duration-base) ease;
|
||||
animation: fadeIn var(--duration-slow) ease backwards;
|
||||
transition: all 0.2s ease;
|
||||
animation: fadeIn 0.3s ease backwards;
|
||||
text-align: left;
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
@@ -464,11 +425,11 @@ const handleConfirm = () => {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: @primary;
|
||||
box-shadow: var(--shadow-blue);
|
||||
border-color: var(--color-primary-300);
|
||||
box-shadow: 0 0 0 3px var(--color-primary-100);
|
||||
}
|
||||
|
||||
// ✅ 选中状态 - 青色主题
|
||||
// ✅ 选中状态
|
||||
&--selected {
|
||||
border-color: var(--color-primary-400);
|
||||
background: var(--color-primary-50);
|
||||
@@ -500,58 +461,56 @@ const handleConfirm = () => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-gray-100);
|
||||
color: @text-disabled;
|
||||
font-size: 24px;
|
||||
color: var(--color-gray-300);
|
||||
}
|
||||
|
||||
// ✓ 选中勾选
|
||||
.card-check {
|
||||
position: absolute;
|
||||
top: var(--space-1);
|
||||
left: var(--space-1);
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: @primary;
|
||||
color: var(--color-text-inverse);
|
||||
background: var(--color-primary-500);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
opacity: 0;
|
||||
transition: opacity var(--duration-base) ease;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
// 🏷️ 选中序号
|
||||
.card-order {
|
||||
position: absolute;
|
||||
top: var(--space-1);
|
||||
right: var(--space-1);
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 var(--space-1);
|
||||
padding: 0 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: @warning;
|
||||
color: var(--color-text-inverse);
|
||||
background: var(--color-warning-500);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border-radius: 10px;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transition: all var(--duration-base) ease;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.card-name {
|
||||
padding: var(--space-2);
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
color: @text-secondary;
|
||||
color: var(--color-gray-500);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: @bg-card;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,35 +521,9 @@ const handleConfirm = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: @bg-elevated;
|
||||
border-top: 1px solid @border-light;
|
||||
|
||||
.selected-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.more-tag {
|
||||
padding: 2px var(--space-2);
|
||||
background: var(--color-gray-100);
|
||||
border-radius: var(--radius-tag);
|
||||
font-size: var(--font-size-xs);
|
||||
color: @text-muted;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.no-selection {
|
||||
font-size: var(--font-size-sm);
|
||||
color: @text-disabled;
|
||||
}
|
||||
}
|
||||
padding: 12px 20px;
|
||||
background: var(--color-gray-50);
|
||||
border-top: 1px solid var(--color-gray-100);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -609,7 +542,7 @@ const handleConfirm = () => {
|
||||
|
||||
.modal-header {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.group-tabs {
|
||||
|
||||
@@ -2,94 +2,209 @@
|
||||
<div class="task-list-container">
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-select
|
||||
v-model:value="filterStatus"
|
||||
placeholder="全部状态"
|
||||
style="width: 150px"
|
||||
allowClear
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-select-option :value="0">待处理</a-select-option>
|
||||
<a-select-option :value="1">处理中</a-select-option>
|
||||
<a-select-option :value="2">成功</a-select-option>
|
||||
<a-select-option :value="3">失败</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="handleRefresh" :loading="loading">刷新</a-button>
|
||||
<Select v-model="filterStatus" @update:model-value="handleFilterChange">
|
||||
<SelectTrigger class="w-[150px]">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">全部状态</SelectItem>
|
||||
<SelectItem :value="0">待处理</SelectItem>
|
||||
<SelectItem :value="1">处理中</SelectItem>
|
||||
<SelectItem :value="2">成功</SelectItem>
|
||||
<SelectItem :value="3">失败</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button @click="handleRefresh" :disabled="loading">
|
||||
<Icon v-if="loading" icon="lucide:loader-2" class="size-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:refresh-cw" class="size-4" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="taskList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'progress'">
|
||||
<a-progress :percent="record.progress" :status="getProgressStatus(record.status)" size="small" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'createTime'">
|
||||
{{ formatTime(record.createTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button v-if="record.status === 2 && record.generatedPrompt" type="link" size="small" @click="handleViewPrompt(record)">查看</a-button>
|
||||
<a-button v-if="record.status === 2 && record.generatedPrompt" type="link" size="small" @click="handleCopyPrompt(record)">复制</a-button>
|
||||
<a-popconfirm v-if="record.status !== 1" title="确定删除?" @confirm="handleDelete(record)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div class="table-wrapper">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="min-w-[200px]">任务名称</TableHead>
|
||||
<TableHead class="w-[100px]">视频数量</TableHead>
|
||||
<TableHead class="w-[100px]">状态</TableHead>
|
||||
<TableHead class="w-[150px]">进度</TableHead>
|
||||
<TableHead class="w-[180px]">创建时间</TableHead>
|
||||
<TableHead class="w-[180px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="record in taskList" :key="record.id">
|
||||
<TableCell class="font-medium">
|
||||
<span class="truncate block max-w-[200px]" :title="record.taskName">
|
||||
{{ record.taskName }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{{ record.videoCount }}</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="getStatusVariant(record.status)" :class="STATUS_MAP.class[record.status]">
|
||||
{{ getStatusText(record.status) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Progress :model-value="record.progress" class="h-2" />
|
||||
</TableCell>
|
||||
<TableCell class="text-muted-foreground">
|
||||
{{ formatTime(record.createTime) }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
v-if="record.status === 2 && record.generatedPrompt"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="h-auto p-0"
|
||||
@click="handleViewPrompt(record)"
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
v-if="record.status === 2 && record.generatedPrompt"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="h-auto p-0"
|
||||
@click="handleCopyPrompt(record)"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<AlertDialog v-if="record.status !== 1">
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button variant="link" size="sm" class="h-auto p-0 text-destructive">
|
||||
删除
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除这个任务吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleDelete(record)">确认删除</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-if="taskList.length === 0 && !loading">
|
||||
<TableCell colspan="6" class="h-32 text-center text-muted-foreground">
|
||||
暂无任务数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="pagination.total > 0" class="pagination-section">
|
||||
<span class="text-sm text-muted-foreground">
|
||||
共 {{ pagination.total }} 条
|
||||
</span>
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="pagination.current === 1"
|
||||
@click="handlePageChange(pagination.current - 1)"
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="pagination.current * pagination.pageSize >= pagination.total"
|
||||
@click="handlePageChange(pagination.current + 1)"
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词弹窗 -->
|
||||
<a-modal v-model:open="promptModalVisible" title="生成的提示词" :footer="null" width="700px">
|
||||
<div class="prompt-content">{{ currentPrompt }}</div>
|
||||
<div class="prompt-actions">
|
||||
<a-button type="primary" @click="handleCopyCurrentPrompt">复制到剪贴板</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
<Dialog v-model:open="promptModalVisible">
|
||||
<DialogContent class="sm:max-w-[700px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>生成的提示词</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="prompt-content">{{ currentPrompt }}</div>
|
||||
<DialogFooter>
|
||||
<Button @click="handleCopyCurrentPrompt">
|
||||
<Icon icon="lucide:copy" class="size-4" />
|
||||
复制到剪贴板
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@/components/ui/table'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
import { BenchmarkTaskApi } from '@/api/benchmarkTask'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
|
||||
const loading = ref(false)
|
||||
const taskList = ref([])
|
||||
const filterStatus = ref(undefined)
|
||||
const filterStatus = ref(null)
|
||||
const promptModalVisible = ref(false)
|
||||
const currentPrompt = ref('')
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '任务名称', dataIndex: 'taskName', key: 'taskName', ellipsis: true },
|
||||
{ title: '视频数量', dataIndex: 'videoCount', key: 'videoCount', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '进度', dataIndex: 'progress', key: 'progress', width: 150 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 180 },
|
||||
]
|
||||
|
||||
const STATUS_MAP = {
|
||||
color: { 0: 'default', 1: 'processing', 2: 'success', 3: 'error' },
|
||||
// 0: 待处理(灰), 1: 处理中(蓝), 2: 成功(绿), 3: 失败(红)
|
||||
variant: { 0: 'secondary', 1: 'default', 2: 'outline', 3: 'destructive' },
|
||||
class: {
|
||||
2: 'border-green-500 text-green-600 bg-green-50'
|
||||
},
|
||||
text: { 0: '待处理', 1: '处理中', 2: '成功', 3: '失败' }
|
||||
}
|
||||
|
||||
@@ -112,30 +227,32 @@ async function loadTaskList() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载失败:', error)
|
||||
message.error('加载任务列表失败')
|
||||
toast.error('加载任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleRefresh() { loadTaskList() }
|
||||
function handleFilterChange() { pagination.current = 1; loadTaskList() }
|
||||
function handleTableChange(page) { pagination.current = page.current; pagination.pageSize = page.pageSize; loadTaskList() }
|
||||
|
||||
function getStatusColor(status) {
|
||||
return STATUS_MAP.color[status] || 'default'
|
||||
function handleFilterChange() {
|
||||
pagination.current = 1
|
||||
loadTaskList()
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
pagination.current = page
|
||||
loadTaskList()
|
||||
}
|
||||
|
||||
function getStatusVariant(status) {
|
||||
return STATUS_MAP.variant[status] || 'secondary'
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
return STATUS_MAP.text[status] || '未知'
|
||||
}
|
||||
|
||||
function getProgressStatus(status) {
|
||||
if (status === 3) return 'exception'
|
||||
if (status === 2) return 'success'
|
||||
return 'active'
|
||||
}
|
||||
|
||||
function handleViewPrompt(record) {
|
||||
currentPrompt.value = record.generatedPrompt
|
||||
promptModalVisible.value = true
|
||||
@@ -143,7 +260,7 @@ function handleViewPrompt(record) {
|
||||
|
||||
async function copyPromptText(text) {
|
||||
const success = await copyToClipboard(text)
|
||||
message[success ? 'success' : 'error'](success ? '已复制' : '复制失败')
|
||||
toast[success ? 'success' : 'error'](success ? '已复制' : '复制失败')
|
||||
}
|
||||
|
||||
function handleCopyPrompt(record) {
|
||||
@@ -157,10 +274,10 @@ function handleCopyCurrentPrompt() {
|
||||
async function handleDelete(record) {
|
||||
try {
|
||||
await BenchmarkTaskApi.deleteTask(record.id)
|
||||
message.success('删除成功')
|
||||
toast.success('删除成功')
|
||||
loadTaskList()
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
toast.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,21 +295,33 @@ onMounted(() => {
|
||||
|
||||
.filter-section {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-gray-100);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pagination-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.prompt-content {
|
||||
padding: 16px;
|
||||
background: var(--color-bg-secondary, #f5f5f5);
|
||||
border-radius: 6px;
|
||||
background: var(--color-gray-50);
|
||||
border-radius: 8px;
|
||||
white-space: pre-wrap;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.prompt-actions {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user