refactor(layout): update BasicLayout and LayoutHeader styling with ant design improvements

- Replace ant-design-vue message with vue-sonner toast notifications
- Remove LayoutHeader component import from BasicLayout
- Update CSS classes for better styling consistency
- Adjust padding and spacing in layout components
- Modify shadow and overflow properties in task management views
- Add hidden class utility for element visibility control
This commit is contained in:
2026-03-16 01:52:38 +08:00
parent 8fdce94c47
commit 16f34ac54e
11 changed files with 977 additions and 692 deletions

View File

@@ -1,8 +1,5 @@
<script setup>
import LayoutHeader from './LayoutHeader.vue'
defineOptions({ name: 'BasicLayout' })
defineProps({
title: { type: String, default: '' },
showBack: { type: Boolean, default: false },
@@ -13,19 +10,9 @@ const emit = defineEmits(['back'])
</script>
<template>
<div class="flex flex-col h-full overflow-hidden rounded-xl border bg-card">
<LayoutHeader
:title="title"
:show-back="showBack"
@back="emit('back')"
>
<template #extra>
<slot name="extra"></slot>
</template>
</LayoutHeader>
<div class="flex flex-col h-full overflow-hidden">
<!-- 页面内容 -->
<div class="flex-1 flex flex-col overflow-x-hidden overflow-y-auto bg-card p-4">
<div class="flex-1 flex flex-col overflow-x-hidden overflow-y-auto p-4">
<slot></slot>
</div>
</div>

View File

@@ -32,12 +32,10 @@ const handleBack = () => {
<template>
<div
class="flex justify-between items-center gap-4 border-b bg-card shrink-0 min-h-14 transition-all"
class="flex justify-between items-center gap-4 shrink-0 min-h-12"
:class="{
'border-border': !ghost,
'border-transparent bg-transparent': ghost,
'px-4 py-3': !padding,
'dark:border-border dark:bg-card dark:text-foreground': !ghost
'bg-transparent': ghost,
'px-4 py-2': !padding
}"
:style="padding ? { padding } : {}"
>
@@ -53,9 +51,9 @@ const handleBack = () => {
</Button>
<div class="flex-1 min-w-0 flex items-center">
<slot name="header">
<h1 v-if="title" class="text-lg font-semibold text-foreground m-0 leading-snug tracking-tight">
<span v-if="title" class="text-base font-medium text-foreground">
{{ title }}
</h1>
</span>
</slot>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { toast } from 'vue-sonner'
import { resolveId } from '@/utils/url'
import { exportBenchmarkDataToExcel } from '@/utils/excel'
import GlobalLoading from '@/components/GlobalLoading.vue'
@@ -75,10 +75,10 @@ async function handleAnalyzeUser() {
maxCursor.value = result.maxCursor
hasMore.value = result.hasMore
await saveTableDataToSession()
message.success('分析完成')
toast.success('分析完成')
} catch (err) {
console.error(err)
message.error('请求失败,请稍后重试')
toast.error('请求失败,请稍后重试')
} finally {
loading.value = false
}
@@ -86,17 +86,17 @@ async function handleAnalyzeUser() {
async function handleExportToExcel() {
if (!data.value?.length) {
message.warning('暂无数据可导出')
toast.warning('暂无数据可导出')
return
}
if (!selectedRowKeys.value.length) {
message.warning('请先选择要导出的行')
toast.warning('请先选择要导出的行')
return
}
if (selectedRowKeys.value.length > 20) {
message.warning('最多只能导出20条数据请重新选择')
toast.warning('最多只能导出20条数据请重新选择')
return
}
@@ -120,7 +120,7 @@ async function handleExportToExcel() {
globalLoadingText.value = '正在导出...'
} catch (error) {
console.error('获取语音转写失败:', error)
message.warning('部分数据语音转写失败,将导出已有数据')
toast.warning('部分数据语音转写失败,将导出已有数据')
}
}
@@ -129,7 +129,11 @@ async function handleExportToExcel() {
platform: form.value.platform,
formatTime
})
message[result.success ? 'success' : 'error'](result.message)
if (result.success) {
toast.success(result.message)
} else {
toast.error(result.message)
}
} finally {
globalLoading.value = false
globalLoadingText.value = ''
@@ -186,10 +190,10 @@ async function handleLoadMore() {
}
await saveTableDataToSession()
message.success(`已加载 ${data.value.length} 条数据`)
toast.success(`已加载 ${data.value.length} 条数据`)
} catch (err) {
console.error(err)
message.error('加载失败,请稍后重试')
toast.error('加载失败,请稍后重试')
} finally {
loadingMore.value = false
}
@@ -206,15 +210,15 @@ function getSelectedVideoUrls() {
// 打开创建风格任务弹窗
function handleCreateAsyncTask() {
if (!selectedRowKeys.value.length) {
message.warning('请先选择要分析的视频')
toast.warning('请先选择要分析的视频')
return
}
if (selectedRowKeys.value.length > 20) {
message.warning('最多只能选择20个视频')
toast.warning('最多只能选择20个视频')
return
}
if (!getSelectedVideoUrls().length) {
message.warning('选中的视频没有有效的URL')
toast.warning('选中的视频没有有效的URL')
return
}
createTaskModalVisible.value = true
@@ -256,19 +260,18 @@ defineOptions({ name: 'ContentStyleBenchmark' })
/>
<section v-if="!data.length" class="card results-card empty-state">
<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"/>
<line x1="32" y1="75" x2="88" y2="75" stroke="currentColor" stroke-width="2" opacity="0.3"/>
<line x1="32" y1="82" x2="88" y2="82" stroke="currentColor" stroke-width="2" opacity="0.3"/>
<line x1="32" y1="89" x2="72" y2="89" stroke="currentColor" stroke-width="2" opacity="0.3"/>
</svg>
</template>
</a-empty>
<div class="empty-content">
<svg class="empty-icon" 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"/>
<line x1="32" y1="75" x2="88" y2="75" stroke="currentColor" stroke-width="2" opacity="0.3"/>
<line x1="32" y1="82" x2="88" y2="82" stroke="currentColor" stroke-width="2" opacity="0.3"/>
<line x1="32" y1="89" x2="72" y2="89" stroke="currentColor" stroke-width="2" opacity="0.3"/>
</svg>
<p class="empty-text">暂无数据请点击开始分析</p>
</div>
</section>
</div>
@@ -322,12 +325,21 @@ defineOptions({ name: 'ContentStyleBenchmark' })
min-height: 420px;
}
.empty-state :deep(.ant-empty) {
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 0;
}
.empty-state :deep(.ant-empty-description) {
.empty-icon {
color: var(--color-gray-400);
margin-bottom: var(--space-4);
}
.empty-text {
color: var(--color-gray-600);
font-size: var(--font-size-base);
margin: 0;
}
</style>

View File

@@ -1,6 +1,10 @@
<script setup>
import { ref, onMounted } from 'vue'
import GradientButton from '@/components/GradientButton.vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Label } from '@/components/ui/label'
import { usePointsConfigStore } from '@/stores/pointsConfig'
const pointsConfigStore = usePointsConfigStore()
@@ -39,23 +43,31 @@ function handleReset() {
<template>
<section class="form-container">
<a-form :model="form" layout="vertical">
<a-form-item label="平台">
<a-radio-group v-model:value="form.platform" button-style="solid">
<a-radio-button value="抖音">抖音</a-radio-button>
</a-radio-group>
</a-form-item>
<div class="form-space-y-6">
<!-- 平台选择 -->
<div class="form-field">
<Label class="form-label">平台</Label>
<RadioGroup v-model="form.platform" class="radio-group">
<div class="radio-item active">
<RadioGroupItem value="抖音" id="douyin" class="hidden" />
<label for="douyin" class="radio-label">抖音</label>
</div>
</RadioGroup>
</div>
<a-form-item label="主页/视频链接">
<a-input
v-model:value="form.url"
<!-- 链接输入 -->
<div class="form-field">
<Label class="form-label">主页/视频链接</Label>
<Input
v-model="form.url"
placeholder="粘贴抖音主页或视频链接"
allow-clear
size="large"
class="form-input h-11"
/>
</a-form-item>
</div>
<a-form-item label="分析数量">
<!-- 分析数量 -->
<div class="form-field">
<Label class="form-label">分析数量</Label>
<div class="slider-row">
<span class="slider-num">{{ form.count }}</span>
<input
@@ -67,9 +79,10 @@ function handleReset() {
/>
</div>
<div class="form-hint">建议 20-30 个内容</div>
</a-form-item>
</div>
<a-space>
<!-- 操作按钮 -->
<div class="button-row">
<GradientButton
text="开始分析"
:loading="loading"
@@ -77,10 +90,10 @@ function handleReset() {
size="middle"
@click="handleAnalyze"
/>
<a-button @click="handleReset">重置</a-button>
</a-space>
<Button variant="outline" @click="handleReset">重置</Button>
</div>
<p class="points-hint">每次分析将消耗积分消耗量与分析数量相关</p>
</a-form>
</div>
</section>
</template>
@@ -92,6 +105,60 @@ function handleReset() {
border: 1px solid var(--color-gray-200);
}
.form-space-y-6 {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.form-field {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.form-label {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-gray-700);
}
.form-input {
background: var(--color-gray-50);
border-color: var(--color-gray-300);
}
.radio-group {
display: flex;
gap: var(--space-2);
}
.radio-item {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 var(--space-4);
height: 36px;
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-gray-600);
background: var(--color-gray-50);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-base);
cursor: pointer;
transition: all var(--duration-fast);
}
.radio-item.active {
color: #fff;
background: var(--color-primary-500);
border-color: var(--color-primary-500);
}
.radio-label {
cursor: pointer;
}
.slider-row {
display: flex;
align-items: center;
@@ -152,6 +219,11 @@ function handleReset() {
color: var(--color-gray-600);
}
.button-row {
display: flex;
gap: var(--space-3);
}
.points-hint {
margin-top: var(--space-3);
font-size: var(--font-size-xs);
@@ -159,4 +231,3 @@ function handleReset() {
text-align: center;
}
</style>

View File

@@ -1,9 +1,20 @@
<script setup>
import { reactive } from 'vue'
import { reactive, computed, ref } from 'vue'
import { formatTime } from '../utils/benchmarkUtils'
import GradientButton from '@/components/GradientButton.vue'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Spinner } from '@/components/ui/spinner'
defineProps({
const props = defineProps({
data: { type: Array, required: true },
selectedRowKeys: { type: Array, required: true },
loading: { type: Boolean, default: false },
@@ -13,29 +24,74 @@ defineProps({
const emit = defineEmits(['update:selectedRowKeys', 'export', 'loadMore', 'createAsyncTask'])
const defaultColumns = [
{ title: '封面', key: 'cover', dataIndex: 'cover', width: 100 },
{ title: '描述', key: 'desc', dataIndex: 'desc', width: 200, ellipsis: true },
{ title: '点赞', key: 'digg_count', dataIndex: 'digg_count', width: 80,
sorter: (a, b) => (a.digg_count || 0) - (b.digg_count || 0), defaultSortOrder: 'descend' },
{ title: '评论', key: 'comment_count', dataIndex: 'comment_count', width: 80,
sorter: (a, b) => (a.comment_count || 0) - (b.comment_count || 0) },
{ title: '分享', key: 'share_count', dataIndex: 'share_count', width: 80,
sorter: (a, b) => (a.share_count || 0) - (b.share_count || 0) },
{ title: '收藏', key: 'collect_count', dataIndex: 'collect_count', width: 80,
sorter: (a, b) => (a.collect_count || 0) - (b.collect_count || 0) },
{ title: '时长', key: 'duration_s', dataIndex: 'duration_s', width: 80,
sorter: (a, b) => (a.duration_s || 0) - (b.duration_s || 0) },
{ title: '创建时间', key: 'create_time', dataIndex: 'create_time', width: 140,
sorter: (a, b) => (a.create_time || 0) - (b.create_time || 0) },
// 列定义
const columns = [
{ key: 'cover', title: '封面', width: '100px' },
{ key: 'desc', title: '描述', width: '200px' },
{ key: 'digg_count', title: '点赞', width: '80px', sortable: true },
{ key: 'comment_count', title: '评论', width: '80px', sortable: true },
{ key: 'share_count', title: '分享', width: '80px', sortable: true },
{ key: 'collect_count', title: '收藏', width: '80px', sortable: true },
{ key: 'duration_s', title: '时长', width: '80px', sortable: true },
{ key: 'create_time', title: '创建时间', width: '140px', sortable: true },
]
const columns = reactive([...defaultColumns])
// 排序状态
const sortKey = ref('digg_count')
const sortOrder = ref('desc')
function onSelectChange(selectedKeys) {
emit('update:selectedRowKeys', selectedKeys)
// 全选状态
const isAllSelected = computed(() => {
return props.data.length > 0 && props.selectedRowKeys.length === props.data.length
})
// 切换排序
function handleSort(key) {
if (sortKey.value === key) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortKey.value = key
sortOrder.value = 'desc'
}
emit('update:selectedRowKeys', [])
}
// 选择切换
function handleSelectAll() {
if (isAllSelected.value) {
emit('update:selectedRowKeys', [])
} else {
emit('update:selectedRowKeys', props.data.map(item => String(item.id)))
}
}
function handleSelect(id) {
const key = String(id)
const newKeys = [...props.selectedRowKeys]
const index = newKeys.indexOf(key)
if (index > -1) {
newKeys.splice(index, 1)
} else {
newKeys.push(key)
}
emit('update:selectedRowKeys', newKeys)
}
// 排序后的数据
const sortedData = computed(() => {
const data = [...props.data]
const key = sortKey.value
if (!key) return data
return data.sort((a, b) => {
const aVal = a[key] || 0
const bVal = b[key] || 0
return sortOrder.value === 'asc' ? aVal - bVal : bVal - aVal
})
})
// 格式化数字
function formatNumber(value) {
return value ? value.toLocaleString('zh-CN') : '0'
}
@@ -62,58 +118,105 @@ function formatNumber(value) {
/>
</div>
</div>
<a-table
:dataSource="data"
:columns="columns"
:pagination="false"
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
:rowKey="(record) => String(record.id)"
:loading="loading"
:scroll="{ y: 'calc(100vh - 400px)' }"
class="benchmark-table">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'cover'">
<div class="cover-wrapper">
<a v-if="record.cover && record.share_url" :href="record.share_url" target="_blank" class="cover-link">
<img :src="record.cover" alt="cover" loading="lazy" class="cover-img" />
<span v-if="record.is_top" class="top-tag">置顶</span>
</a>
<template v-else-if="record.cover">
<img :src="record.cover" alt="cover" loading="lazy" class="cover-img" />
<span v-if="record.is_top" class="top-tag">置顶</span>
</template>
<span v-else class="no-cover">无封面</span>
</div>
</template>
<template v-else-if="column.key === 'desc'">
<span :title="record.desc">{{ record.desc || '-' }}</span>
</template>
<template v-else-if="column.key === 'play_count'">
{{ record.play_count ? (record.play_count / 10000).toFixed(1) + 'w' : '0' }}
</template>
<template v-else-if="['digg_count', 'comment_count', 'share_count', 'collect_count'].includes(column.key)">
{{ formatNumber(record[column.key]) }}
</template>
<template v-else-if="column.key === 'create_time'">
{{ formatTime(record.create_time) }}
</template>
</template>
<template #footer>
<div v-if="hasMore" class="load-more-footer">
<a-button
:loading="loadingMore"
@click="$emit('loadMore')"
size="large"
<!-- 表格 -->
<div class="table-wrapper">
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[40px]">
<Checkbox
:checked="isAllSelected"
@update:checked="handleSelectAll"
class="checkbox-small"
/>
</TableHead>
<TableHead
v-for="col in columns"
:key="col.key"
:class="[col.width && `w-[${col.width}]`, col.sortable && 'cursor-pointer']"
@click="col.sortable && handleSort(col.key)"
>
<div class="flex items-center gap-1">
{{ col.title }}
<span v-if="col.sortable" class="sort-icon">
<svg v-if="sortKey !== col.key" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 15l5 5 5-5M7 9l5-5 5 5"/>
</svg>
<svg v-else-if="sortOrder === 'asc'" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 19V5M5 12l7-7 7 7"/>
</svg>
<svg v-else width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12l7 7 7-7"/>
</svg>
</span>
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="loading">
<TableCell :col-span="columns.length + 1" class="h-32 text-center">
<Spinner class="mx-auto" />
</TableCell>
</TableRow>
<TableRow
v-else
v-for="record in sortedData"
:key="record.id"
class="table-row"
>
加载更多内容
</a-button>
<div class="load-more-hint">已加载 {{ data.length }} </div>
</div>
<div v-else-if="data.length > 0" class="no-more-hint">
已加载全部内容
</div>
</template>
</a-table>
<TableCell class="w-[40px]">
<Checkbox
:checked="selectedRowKeys.includes(String(record.id))"
@update:checked="handleSelect(record.id)"
class="checkbox-small"
/>
</TableCell>
<TableCell>
<div class="cover-wrapper">
<a v-if="record.cover && record.share_url" :href="record.share_url" target="_blank" class="cover-link">
<img :src="record.cover" alt="cover" loading="lazy" class="cover-img" />
<span v-if="record.is_top" class="top-tag">置顶</span>
</a>
<template v-else-if="record.cover">
<img :src="record.cover" alt="cover" loading="lazy" class="cover-img" />
<span v-if="record.is_top" class="top-tag">置顶</span>
</template>
<span v-else class="no-cover">无封面</span>
</div>
</TableCell>
<TableCell>
<span :title="record.desc" class="truncate-text">{{ record.desc || '-' }}</span>
</TableCell>
<TableCell>{{ formatNumber(record.digg_count) }}</TableCell>
<TableCell>{{ formatNumber(record.comment_count) }}</TableCell>
<TableCell>{{ formatNumber(record.share_count) }}</TableCell>
<TableCell>{{ formatNumber(record.collect_count) }}</TableCell>
<TableCell>{{ record.duration_s ? `${record.duration_s}s` : '-' }}</TableCell>
<TableCell>{{ formatTime(record.create_time) }}</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- 加载更多 -->
<div v-if="hasMore" class="load-more-footer">
<Button
:disabled="loadingMore"
size="lg"
variant="outline"
@click="$emit('loadMore')"
class="load-more-btn"
>
<Spinner v-if="loadingMore" class="mr-2 h-4 w-4" />
{{ loadingMore ? '加载中...' : '加载更多内容' }}
</Button>
<div class="load-more-hint">已加载 {{ data.length }} </div>
</div>
<div v-else-if="data.length > 0" class="no-more-hint">
已加载全部内容
</div>
</section>
</template>
@@ -150,13 +253,32 @@ function formatNumber(value) {
margin-bottom: var(--space-3);
}
.batch-btn {
font-weight: 600;
.table-wrapper {
overflow-x: auto;
max-height: calc(100vh - 400px);
overflow-y: auto;
}
.batch-btn:hover {
box-shadow: var(--shadow-blue);
filter: brightness(1.03);
.table-wrapper::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.table-wrapper::-webkit-scrollbar-track {
background: transparent;
}
.table-wrapper::-webkit-scrollbar-thumb {
background: var(--color-gray-300);
border-radius: 3px;
}
.table-row:hover {
background: var(--color-gray-50);
}
.checkbox-small {
transform: scale(0.85);
}
.cover-wrapper {
@@ -199,42 +321,22 @@ function formatNumber(value) {
font-size: var(--font-size-xs);
}
.benchmark-table :deep(.ant-table-expand-icon-th),
.benchmark-table :deep(.ant-table-row-expand-icon-cell) {
width: 48px;
min-width: 48px;
text-align: center;
}
.benchmark-table :deep(.ant-table) {
table-layout: fixed;
}
.benchmark-table :deep(.ant-table-wrapper),
.benchmark-table :deep(.ant-spin-nested-loading),
.benchmark-table :deep(.ant-spin-container),
.benchmark-table :deep(.ant-table-container) {
width: 100%;
.truncate-text {
display: block;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.benchmark-table :deep(.ant-table-body) {
overflow-x: hidden !important;
.sort-icon {
opacity: 0.4;
transition: opacity var(--duration-fast);
}
.benchmark-table :deep(.ant-table-thead > tr > th),
.benchmark-table :deep(.ant-table-tbody > tr > td) {
word-break: break-all;
}
.benchmark-table :deep(.ant-table-row-expand-icon) {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: pointer;
user-select: none;
.table-row:hover .sort-icon,
[onclick]:hover .sort-icon {
opacity: 1;
}
.load-more-footer,
@@ -247,7 +349,7 @@ function formatNumber(value) {
padding: var(--space-4);
}
.load-more-footer .ant-btn {
.load-more-btn {
max-width: 200px;
height: 40px;
font-size: var(--font-size-base);
@@ -268,4 +370,3 @@ function formatNumber(value) {
padding: var(--space-3);
}
</style>

View File

@@ -1,7 +1,20 @@
<script setup>
import { ref, watchEffect } from 'vue'
import { message } from 'ant-design-vue'
import { toast } from 'vue-sonner'
import { BenchmarkTaskApi } from '@/api/benchmarkTask'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Spinner } from '@/components/ui/spinner'
const props = defineProps({
visible: {
@@ -38,17 +51,17 @@ watchEffect(() => {
async function handleSubmit() {
if (!form.value.taskName.trim()) {
message.warning('请输入任务名称')
toast.warning('请输入任务名称')
return
}
if (!props.videoUrls?.length) {
message.warning('没有选择视频')
toast.warning('没有选择视频')
return
}
if (!form.value.promptName.trim()) {
message.warning('请输入提示词名称')
toast.warning('请输入提示词名称')
return
}
@@ -64,7 +77,7 @@ async function handleSubmit() {
const response = await BenchmarkTaskApi.createTask(payload)
if (response?.code === 0 || response?.data) {
message.success('任务创建成功!请到任务中心查看进度')
toast.success('任务创建成功!请到任务中心查看进度')
emit('update:visible', false)
emit('success', response.data)
} else {
@@ -72,7 +85,7 @@ async function handleSubmit() {
}
} catch (error) {
console.error('创建任务失败:', error)
message.error(error?.message || '创建任务失败')
toast.error(error?.message || '创建任务失败')
} finally {
loading.value = false
}
@@ -84,52 +97,61 @@ function handleCancel() {
</script>
<template>
<a-modal
:open="visible"
title="创建风格分析任务"
:width="500"
:maskClosable="false"
:confirmLoading="loading"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form :model="form" layout="vertical">
<a-form-item label="任务名称" required>
<a-input
v-model:value="form.taskName"
placeholder="请输入任务名称"
:maxlength="100"
/>
</a-form-item>
<Dialog :open="visible" @update:open="(v) => emit('update:visible', v)">
<DialogContent class="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>创建风格分析任务</DialogTitle>
<DialogDescription>
将选中的视频创建为异步分析任务
</DialogDescription>
</DialogHeader>
<a-form-item label="提示词名称" required>
<a-input
v-model:value="form.promptName"
placeholder="任务完成后将保存为提示词"
:maxlength="50"
/>
</a-form-item>
<div class="space-y-4 py-4">
<div class="space-y-2">
<Label for="taskName">任务名称 <span class="text-destructive">*</span></Label>
<Input
id="taskName"
v-model="form.taskName"
placeholder="请输入任务名称"
:maxlength="100"
/>
</div>
<a-form-item label="分类/标签">
<a-input
v-model:value="form.category"
placeholder="可选:输入分类或标签"
:maxlength="20"
/>
</a-form-item>
<div class="space-y-2">
<Label for="promptName">提示词名称 <span class="text-destructive">*</span></Label>
<Input
id="promptName"
v-model="form.promptName"
placeholder="任务完成后将保存为提示词"
:maxlength="50"
/>
</div>
<a-form-item label="选中视频">
<a-tag color="blue">{{ videoUrls.length }} 个视频</a-tag>
</a-form-item>
</a-form>
<div class="space-y-2">
<Label for="category">分类/标签</Label>
<Input
id="category"
v-model="form.category"
placeholder="可选:输入分类或标签"
:maxlength="20"
/>
</div>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="loading" @click="handleSubmit">
创建任务
</a-button>
</a-space>
</template>
</a-modal>
<div class="space-y-2">
<Label>选中视频</Label>
<div>
<Badge variant="secondary">{{ videoUrls.length }} 个视频</Badge>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="handleCancel">取消</Button>
<Button :disabled="loading" @click="handleSubmit">
<Spinner v-if="loading" class="mr-2 h-4 w-4" />
{{ loading ? '创建中...' : '创建任务' }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -407,18 +407,16 @@ onMounted(fetchList)
padding: var(--space-4);
background: var(--color-bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.task-page__content {
flex: 1;
overflow: auto;
overflow: hidden;
background: var(--color-bg-card);
border-radius: var(--radius-lg);
padding: var(--space-4);
display: flex;
flex-direction: column;
box-shadow: var(--shadow-sm);
}
.batch-actions {

View File

@@ -8,9 +8,7 @@
v-for="item in NAV_ITEMS"
:key="item.type"
:value="item.type"
class="h-10 px-4 gap-2 rounded-lg transition-all
data-[state=active]:bg-primary data-[state=active]:text-white data-[state=active]:shadow-sm
data-[state=inactive]:text-muted-foreground data-[state=inactive]:hover:text-foreground data-[state=inactive]:hover:bg-muted"
class="h-10 px-4 gap-2 rounded-lg transition-all data-[state=active]:bg-foreground data-[state=active]:!text-white data-[state=active]:shadow-sm data-[state=inactive]:text-muted-foreground data-[state=inactive]:hover:text-foreground data-[state=inactive]:hover:bg-muted"
>
<Icon :icon="item.icon" class="size-4" />
<span>{{ item.label }}</span>
@@ -82,14 +80,12 @@ const currentComponent = computed(() => {
.task-layout__header {
flex-shrink: 0;
padding: 0 var(--space-4);
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-card);
}
.task-layout__content {
flex: 1;
overflow: auto;
background: var(--color-bg-page);
}
.fade-enter-active,

View File

@@ -72,6 +72,7 @@
:disabled="!hasDownloadableSelected"
:loading="batchDownloading"
size="sm"
class="text-white"
@click="handleBatchDownloadSelected"
>
<Icon icon="lucide:download" class="mr-1 size-4" />
@@ -630,7 +631,6 @@ onMounted(fetchList)
padding: var(--space-4);
background: var(--color-bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.task-page__content {
@@ -641,7 +641,6 @@ onMounted(fetchList)
padding: var(--space-4);
display: flex;
flex-direction: column;
box-shadow: var(--shadow-sm);
}
.batch-toolbar {

View File

@@ -1,6 +1,8 @@
<script setup>
import { onMounted, reactive, ref, computed } from 'vue'
import { message } from 'ant-design-vue'
import { toast } from 'vue-sonner'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import TikhubService, { InterfaceType, MethodType, ParamType } from '@/api/tikhub'
import { rewriteStream } from '@/api/forecast'
@@ -76,9 +78,9 @@ function handleSearchKeypress(event) {
async function copyContent() {
const success = await copyToClipboard(generatedContent.value)
if (success) {
message.success('已复制')
toast.success('已复制')
} else {
message.error('复制失败')
toast.error('复制失败')
}
}
@@ -117,7 +119,7 @@ async function analyzeVoice(audioUrl) {
? `${topicDetails.copywriting}\n\n${transcript}`
: transcript
} else {
message.warning('未获取到可用文案')
toast.warning('未获取到可用文案')
}
} catch (error) {
console.error('分析语音失败:', error)
@@ -143,12 +145,12 @@ async function handleCreate(topic) {
// 生成文案(流式)
async function handleGenerate() {
if (!topicDetails.copywriting?.trim()) {
message.warning('请输入文案内容')
toast.warning('请输入文案内容')
return
}
if (!selectedAgent.value?.id) {
message.warning('请先选择文案风格')
toast.warning('请先选择文案风格')
return
}
@@ -197,7 +199,7 @@ async function handleGenerate() {
completed = true
clearTimeout(timeout)
ctrl.abort()
message.error(err?.message || '网络请求失败')
toast.error(err?.message || '网络请求失败')
reject(new Error(err?.message || '网络请求失败'))
}
},
@@ -223,10 +225,10 @@ async function handleGenerate() {
})
generatedContent.value = fullText.trim()
message.success('文案生成成功')
toast.success('文案生成成功')
} catch (error) {
console.error('生成文案失败:', error)
message.error('生成文案失败')
toast.error('生成文案失败')
} finally {
isGenerating.value = false
}
@@ -291,7 +293,7 @@ function processSearchResults(response, startId = 1) {
async function handleSearch() {
const keyword = searchKeyword.value.trim()
if (!keyword) {
message.warning('请输入搜索关键词')
toast.warning('请输入搜索关键词')
return
}
@@ -326,15 +328,15 @@ async function handleSearch() {
const searchResults = processSearchResults(response)
if (searchResults.length === 0) {
message.warning('未找到相关结果')
toast.warning('未找到相关结果')
return
}
hotTopics.value = searchResults
message.success(`找到 ${searchResults.length} 个结果`)
toast.success(`找到 ${searchResults.length} 个结果`)
} catch (error) {
console.error('搜索失败:', error)
message.error(error?.message || '搜索失败')
toast.error(error?.message || '搜索失败')
hotTopics.value = []
} finally {
isLoading.value = false
@@ -385,29 +387,44 @@ onMounted(() => {
<div class="filters-row">
<div class="filter-item">
<span class="filter-label">排序</span>
<a-select v-model:value="searchParams.sort_type" size="small">
<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>
<Select v-model="searchParams.sort_type">
<SelectTrigger class="h-[26px] w-[88px]">
<SelectValue placeholder="综合" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">综合</SelectItem>
<SelectItem value="1">最多点赞</SelectItem>
<SelectItem value="2">最新发布</SelectItem>
</SelectContent>
</Select>
</div>
<div class="filter-item">
<span class="filter-label">时间</span>
<a-select v-model:value="searchParams.publish_time" size="small">
<a-select-option value="0">不限</a-select-option>
<a-select-option value="1">一天</a-select-option>
<a-select-option value="7">一周</a-select-option>
<a-select-option value="180">半年</a-select-option>
</a-select>
<Select v-model="searchParams.publish_time">
<SelectTrigger class="h-[26px] w-[88px]">
<SelectValue placeholder="不限" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">不限</SelectItem>
<SelectItem value="1">一天</SelectItem>
<SelectItem value="7">一周</SelectItem>
<SelectItem value="180">半年</SelectItem>
</SelectContent>
</Select>
</div>
<div class="filter-item">
<span class="filter-label">时长</span>
<a-select v-model:value="searchParams.filter_duration" size="small">
<a-select-option value="0">不限</a-select-option>
<a-select-option value="0-1">1分钟内</a-select-option>
<a-select-option value="1-5">1-5分钟</a-select-option>
<a-select-option value="5-10000">5分钟+</a-select-option>
</a-select>
<Select v-model="searchParams.filter_duration">
<SelectTrigger class="h-[26px] w-[88px]">
<SelectValue placeholder="不限" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">不限</SelectItem>
<SelectItem value="0-1">1分钟内</SelectItem>
<SelectItem value="1-5">1-5分钟</SelectItem>
<SelectItem value="5-10000">5分钟+</SelectItem>
</SelectContent>
</Select>
</div>
</div>
@@ -498,12 +515,12 @@ onMounted(() => {
分析中
</span>
</div>
<a-textarea
v-model:value="topicDetails.copywriting"
<Textarea
v-model="topicDetails.copywriting"
:rows="5"
placeholder="输入文案或点击热点卡片自动提取..."
:disabled="isAnalyzing"
class="content-input"
class="content-input min-h-[120px]"
/>
</div>
@@ -600,24 +617,18 @@ onMounted(() => {
grid-template-columns: 1fr 400px;
gap: var(--space-4);
height: 100%;
background: var(--color-gray-50);
}
// 面板基础
// 面板基础 - 更现代简洁
.search-panel,
.create-panel {
background: var(--color-bg-card);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.panel-header {
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--color-gray-200);
}.panel-header {
padding: var(--space-3) var(--space-4);
}
.panel-title {
@@ -709,7 +720,6 @@ onMounted(() => {
display: flex;
gap: var(--space-3);
padding: 0 var(--space-4) var(--space-3);
border-bottom: 1px solid var(--color-gray-200);
}
.filter-item {
@@ -723,23 +733,6 @@ onMounted(() => {
color: var(--color-gray-400);
}
:deep(.ant-select) {
width: 88px;
.ant-select-selector {
background: var(--color-gray-50) !important;
border-color: var(--color-gray-300) !important;
border-radius: var(--radius-base) !important;
height: 26px !important;
.ant-select-selection-item {
color: var(--color-gray-900) !important;
font-size: var(--font-size-xs) !important;
line-height: 24px !important;
}
}
}
// 结果区域
.results-area {
flex: 1;
@@ -812,19 +805,16 @@ onMounted(() => {
gap: var(--space-3);
padding: var(--space-3);
background: var(--color-bg-card);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--duration-fast);
&:hover {
border-color: var(--color-gray-300);
box-shadow: var(--shadow-md);
background: var(--color-gray-50);
}
&.selected {
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
background: rgba(59, 130, 246, 0.08);
}
}
@@ -1008,42 +998,11 @@ onMounted(() => {
}
.content-input {
:deep(.ant-input) {
background: var(--color-gray-50) !important;
border-color: var(--color-gray-300) !important;
color: var(--color-gray-900) !important;
border-radius: var(--radius-base) !important;
font-size: var(--font-size-sm) !important;
line-height: 1.6 !important;
&::placeholder { color: var(--color-gray-400) !important; }
&:focus {
border-color: var(--color-primary-500) !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1) !important;
}
}
}
// 下拉选择器
:deep(.ant-select) {
.ant-select-selector {
background: var(--color-gray-50) !important;
border-color: var(--color-gray-300) !important;
border-radius: var(--radius-base) !important;
height: 36px !important;
.ant-select-selection-item,
.ant-select-selection-placeholder {
color: var(--color-gray-900) !important;
font-size: var(--font-size-sm) !important;
line-height: 34px !important;
}
}
&.ant-select-focused .ant-select-selector {
border-color: var(--color-primary-500) !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1) !important;
}
background: var(--color-gray-50);
border-color: var(--color-gray-300);
border-radius: var(--radius-base);
font-size: var(--font-size-sm);
line-height: 1.6;
}
.agent-category {