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:
2026-03-16 02:30:43 +08:00
parent 4b705f3530
commit 52c3b5489d
7 changed files with 1171 additions and 2338 deletions

View File

@@ -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>

View File

@@ -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;
}
/* 按钮风格(主色 #1890FFhover #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

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>