feat: 增加头,交互优化

This commit is contained in:
kongweigen
2026-01-16 13:14:56 +08:00
parent 249ba3d4aa
commit bfba6342dc
11 changed files with 582 additions and 266 deletions

View File

@@ -1,12 +1,8 @@
<template>
<router-view />
<!-- <AppLayout>
<router-view />
</AppLayout> -->
</template>
<script setup lang="ts">
import AppLayout from '@/components/common/AppLayout.vue'
</script>
<style>

View File

@@ -774,21 +774,21 @@ body {
.page-container {
min-height: 100vh;
background-color: var(--bg-primary);
padding: var(--space-2) var(--space-3);
/* padding: var(--space-2) var(--space-3); */
transition: background-color var(--transition-normal);
}
@media (min-width: 768px) {
/* @media (min-width: 768px) {
.page-container {
padding: var(--space-3) var(--space-4);
}
}
} */
@media (min-width: 1024px) {
/* @media (min-width: 1024px) {
.page-container {
padding: var(--space-4) var(--space-5);
}
}
} */
.content-wrapper {
margin: 0 auto;

View File

@@ -236,6 +236,7 @@ const props = defineProps<{
const emit = defineEmits<{
'update:modelValue': [value: boolean]
'config-updated': []
}>()
const visible = computed({
@@ -567,6 +568,7 @@ const handleSubmit = async () => {
editDialogVisible.value = false
loadConfigs()
emit('config-updated')
} catch (error: any) {
ElMessage.error(error.message || '操作失败')
} finally {
@@ -625,45 +627,84 @@ const handleQuickSetup = async () => {
const apiKey = quickSetupApiKey.value.trim()
try {
// 创建文本配置
const textProvider = providerConfigs.text.find(p => p.id === 'chatfire')!
await aiAPI.create({
service_type: 'text',
provider: 'chatfire',
name: generateConfigName('chatfire', 'text'),
base_url: baseUrl,
api_key: apiKey,
model: [textProvider.models[0]],
priority: 0
})
// 加载所有类型的配置,检查是否已存在相同 baseUrl 的配置
const [textConfigs, imageConfigs, videoConfigs] = await Promise.all([
aiAPI.list('text'),
aiAPI.list('image'),
aiAPI.list('video')
])
// 创建图片配置
const imageProvider = providerConfigs.image.find(p => p.id === 'chatfire')!
await aiAPI.create({
service_type: 'image',
provider: 'chatfire',
name: generateConfigName('chatfire', 'image'),
base_url: baseUrl,
api_key: apiKey,
model: [imageProvider.models[0]],
priority: 0
})
const createdServices: string[] = []
const skippedServices: string[] = []
// 创建视频配置
const videoProvider = providerConfigs.video.find(p => p.id === 'chatfire')!
await aiAPI.create({
service_type: 'video',
provider: 'chatfire',
name: generateConfigName('chatfire', 'video'),
base_url: baseUrl,
api_key: apiKey,
model: [videoProvider.models[0]],
priority: 0
})
// 创建文本配置(如果不存在)
const existingTextConfig = textConfigs.find(c => c.base_url === baseUrl)
if (!existingTextConfig) {
const textProvider = providerConfigs.text.find(p => p.id === 'chatfire')!
await aiAPI.create({
service_type: 'text',
provider: 'chatfire',
name: generateConfigName('chatfire', 'text'),
base_url: baseUrl,
api_key: apiKey,
model: [textProvider.models[0]],
priority: 0
})
createdServices.push('文本')
} else {
skippedServices.push('文本')
}
// 创建图片配置(如果不存在)
const existingImageConfig = imageConfigs.find(c => c.base_url === baseUrl)
if (!existingImageConfig) {
const imageProvider = providerConfigs.image.find(p => p.id === 'chatfire')!
await aiAPI.create({
service_type: 'image',
provider: 'chatfire',
name: generateConfigName('chatfire', 'image'),
base_url: baseUrl,
api_key: apiKey,
model: [imageProvider.models[0]],
priority: 0
})
createdServices.push('图片')
} else {
skippedServices.push('图片')
}
// 创建视频配置(如果不存在)
const existingVideoConfig = videoConfigs.find(c => c.base_url === baseUrl)
if (!existingVideoConfig) {
const videoProvider = providerConfigs.video.find(p => p.id === 'chatfire')!
await aiAPI.create({
service_type: 'video',
provider: 'chatfire',
name: generateConfigName('chatfire', 'video'),
base_url: baseUrl,
api_key: apiKey,
model: [videoProvider.models[0]],
priority: 0
})
createdServices.push('视频')
} else {
skippedServices.push('视频')
}
// 显示结果消息
if (createdServices.length > 0 && skippedServices.length > 0) {
ElMessage.success(`已创建 ${createdServices.join('、')} 配置,${skippedServices.join('、')} 配置已存在`)
} else if (createdServices.length > 0) {
ElMessage.success(`一键配置成功!已创建 ${createdServices.join('、')} 服务配置`)
} else {
ElMessage.info('所有配置已存在,无需重复创建')
}
ElMessage.success('一键配置成功!已创建文本、图片、视频三个服务配置')
quickSetupVisible.value = false
loadConfigs()
if (createdServices.length > 0) {
emit('config-updated')
}
} catch (error: any) {
ElMessage.error(error.message || '配置失败')
} finally {

View File

@@ -0,0 +1,271 @@
<template>
<div class="app-header-wrapper">
<header class="app-header" :class="{ 'header-fixed': fixed }">
<div class="header-content">
<!-- Left section: Logo + Left slot -->
<div class="header-left">
<router-link v-if="showLogo" to="/" class="logo">
<span class="logo-text">🎬 AI Drama</span>
</router-link>
<!-- Left slot for business content | 左侧插槽用于业务内容 -->
<slot name="left" />
</div>
<!-- Center section: Center slot -->
<div class="header-center">
<slot name="center" />
</div>
<!-- Right section: Actions + Right slot -->
<div class="header-right">
<!-- Language Switcher | 语言切换 -->
<LanguageSwitcher v-if="showLanguage" />
<!-- Theme Toggle | 主题切换 -->
<ThemeToggle v-if="showTheme" />
<!-- AI Config (Model Switch) | AI 配置模型切换 -->
<el-button v-if="showAIConfig" @click="handleOpenAIConfig" class="header-btn">
<el-icon><Setting /></el-icon>
<span class="btn-text">{{ $t('drama.aiConfig') }}</span>
</el-button>
<!-- Right slot for business content (before actions) | 右侧插槽在操作按钮前 -->
<slot name="right" />
</div>
</div>
</header>
<!-- AI Config Dialog | AI 配置对话框 -->
<AIConfigDialog v-model="showConfigDialog" @config-updated="emit('config-updated')" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Setting } from '@element-plus/icons-vue'
import ThemeToggle from './ThemeToggle.vue'
import AIConfigDialog from './AIConfigDialog.vue'
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
/**
* AppHeader - Global application header component
* 应用顶部头组件
*
* Features | 功能:
* - Fixed position at top | 固定在顶部
* - Model/Theme/Language switch | 模型/主题/语言切换
* - Slots support for business content | 支持插槽放置业务内容
*
* Slots | 插槽:
* - left: Content after logo | logo 右侧内容
* - center: Center content | 中间内容
* - right: Content before actions | 操作按钮左侧内容
*/
interface Props {
/** Fixed position at top | 是否固定在顶部 */
fixed?: boolean
/** Show logo | 是否显示 logo */
showLogo?: boolean
/** Show language switcher | 是否显示语言切换 */
showLanguage?: boolean
/** Show theme toggle | 是否显示主题切换 */
showTheme?: boolean
/** Show AI config button | 是否显示 AI 配置按钮 */
showAIConfig?: boolean
}
const props = withDefaults(defineProps<Props>(), {
fixed: true,
showLogo: true,
showLanguage: true,
showTheme: true,
showAIConfig: true
})
const emit = defineEmits<{
(e: 'open-ai-config'): void
(e: 'config-updated'): void
}>()
// AI Config dialog state | AI 配置对话框状态
const showConfigDialog = ref(false)
// Handle open AI config | 处理打开 AI 配置
const handleOpenAIConfig = () => {
showConfigDialog.value = true
emit('open-ai-config')
}
// Expose methods for external control | 暴露方法供外部控制
defineExpose({
openAIConfig: () => {
showConfigDialog.value = true
}
})
</script>
<style scoped>
.app-header {
background: var(--bg-card);
border-bottom: 1px solid var(--border-primary);
backdrop-filter: blur(8px);
z-index: 1000;
}
.app-header.header-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--space-4);
height: 70px;
max-width: 100%;
margin: 0 auto;
}
.header-left {
display: flex;
align-items: center;
gap: var(--space-4);
flex-shrink: 0;
}
.header-center {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-width: 0;
}
.header-right {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
}
.logo {
display: flex;
align-items: center;
gap: var(--space-2);
text-decoration: none;
color: var(--text-primary);
font-weight: 700;
font-size: 1.125rem;
transition: opacity var(--transition-fast);
}
.logo:hover {
opacity: 0.8;
}
.logo-text {
background: linear-gradient(135deg, var(--accent) 0%, #06b6d4 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-btn {
border-radius: var(--radius-lg);
font-weight: 500;
}
.header-btn .btn-text {
margin-left: 4px;
}
/* Dark mode adjustments | 深色模式适配 */
.dark .app-header {
background: rgba(26, 33, 41, 0.95);
}
/* ========================================
Common Slot Styles / 插槽通用样式
======================================== */
/* Back Button | 返回按钮 */
:deep(.back-btn) {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
:deep(.back-btn:hover) {
color: var(--text-primary);
background: var(--bg-hover);
}
:deep(.back-btn .el-icon) {
font-size: 1rem;
}
/* Page Title | 页面标题 */
:deep(.page-title) {
display: flex;
flex-direction: column;
gap: 2px;
}
:deep(.page-title h1),
:deep(.header-title),
:deep(.drama-title) {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.3;
}
:deep(.page-title .subtitle) {
font-size: 0.8125rem;
color: var(--text-muted);
}
/* Episode Title | 章节标题 */
:deep(.episode-title) {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
/* Responsive | 响应式 */
@media (max-width: 768px) {
.header-content {
padding: 0 var(--space-3);
}
.btn-text {
display: none;
}
.header-btn {
padding: 8px;
}
:deep(.page-title h1),
:deep(.header-title),
:deep(.drama-title) {
font-size: 1rem;
}
:deep(.back-btn span) {
display: none;
}
}
</style>

View File

@@ -20,3 +20,4 @@ export { default as AIConfigDialog } from './AIConfigDialog.vue'
// Layout Components / 布局组件
export { default as AppLayout } from './AppLayout.vue'
export { default as AppHeader } from './AppHeader.vue'

View File

@@ -3,13 +3,18 @@
<div class="page-container">
<div class="content-wrapper animate-fade-in">
<!-- Header / 头部 -->
<PageHeader
title="创建新项目"
subtitle="填写基本信息来创建你的短剧项目"
:show-back="true"
back-text="返回"
:show-border="false"
/>
<AppHeader :fixed="false" :show-logo="false">
<template #left>
<el-button text @click="goBack" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
<span>返回</span>
</el-button>
<div class="page-title">
<h1>创建新项目</h1>
<span class="subtitle">填写基本信息来创建你的短剧项目</span>
</div>
</template>
</AppHeader>
<!-- Form Card / 表单卡片 -->
<div class="form-card">
@@ -69,7 +74,7 @@ import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { ArrowLeft, Plus } from '@element-plus/icons-vue'
import { dramaAPI } from '@/api/drama'
import type { CreateDramaRequest } from '@/types/drama'
import { PageHeader } from '@/components/common'
import { AppHeader } from '@/components/common'
const router = useRouter()
const formRef = ref<FormInstance>()

View File

@@ -3,114 +3,68 @@
<!-- 短剧列表页面 - 使用现代简约设计重构 -->
<div class="page-container">
<div class="content-wrapper animate-fade-in">
<!-- Page Header / 页面头部 -->
<PageHeader
:title="$t('drama.title')"
:subtitle="$t('drama.totalProjects', { count: total })"
>
<template #actions>
<LanguageSwitcher />
<ThemeToggle />
<el-button @click="showAIConfig = true" class="header-btn">
<el-icon><Setting /></el-icon>
<span class="btn-text">{{ $t('drama.aiConfig') }}</span>
</el-button>
<!-- App Header / 应用头部 -->
<AppHeader :fixed="false">
<template #left>
<div class="page-title">
<h1>{{ $t('drama.title') }}</h1>
<span class="subtitle">{{ $t('drama.totalProjects', { count: total }) }}</span>
</div>
</template>
<template #right>
<el-button type="primary" @click="handleCreate" class="header-btn primary">
<el-icon><Plus /></el-icon>
<el-icon>
<Plus />
</el-icon>
<span class="btn-text">{{ $t('drama.createNew') }}</span>
</el-button>
</template>
</PageHeader>
</AppHeader>
<!-- Project Grid / 项目网格 -->
<div v-loading="loading" class="projects-grid" :class="{ 'is-empty': !loading && dramas.length === 0 }">
<!-- Empty state / 空状态 -->
<EmptyState
v-if="!loading && dramas.length === 0"
:title="$t('drama.empty')"
:description="$t('drama.emptyHint')"
:icon="Film"
>
<EmptyState v-if="!loading && dramas.length === 0" :title="$t('drama.empty')"
:description="$t('drama.emptyHint')" :icon="Film">
<el-button type="primary" @click="handleCreate">
<el-icon><Plus /></el-icon>
<el-icon>
<Plus />
</el-icon>
{{ $t('drama.createNew') }}
</el-button>
</EmptyState>
<!-- Project Cards / 项目卡片列表 -->
<ProjectCard
v-for="drama in dramas"
:key="drama.id"
:title="drama.title"
:description="drama.description"
:updated-at="drama.updated_at"
:episode-count="drama.total_episodes || 0"
@click="viewDrama(drama.id)"
>
<ProjectCard v-for="drama in dramas" :key="drama.id" :title="drama.title" :description="drama.description"
:updated-at="drama.updated_at" :episode-count="drama.total_episodes || 0" @click="viewDrama(drama.id)">
<template #actions>
<ActionButton
:icon="Edit"
:tooltip="$t('common.edit')"
@click="editDrama(drama.id)"
/>
<el-popconfirm
:title="$t('drama.deleteConfirm')"
:confirm-button-text="$t('common.confirm')"
:cancel-button-text="$t('common.cancel')"
@confirm="deleteDrama(drama.id)"
>
<ActionButton :icon="Edit" :tooltip="$t('common.edit')" @click="editDrama(drama.id)" />
<el-popconfirm :title="$t('drama.deleteConfirm')" :confirm-button-text="$t('common.confirm')"
:cancel-button-text="$t('common.cancel')" @confirm="deleteDrama(drama.id)">
<template #reference>
<el-button
:icon="Delete"
class="action-button danger"
link
/>
<el-button :icon="Delete" class="action-button danger" link />
</template>
</el-popconfirm>
</template>
</ProjectCard>
</ProjectCard>
</div>
<!-- Edit Dialog / 编辑对话框 -->
<el-dialog
v-model="editDialogVisible"
:title="$t('drama.editProject')"
width="520px"
:close-on-click-modal="false"
class="edit-dialog"
>
<el-form
:model="editForm"
label-position="top"
v-loading="editLoading"
class="edit-form"
>
<el-dialog v-model="editDialogVisible" :title="$t('drama.editProject')" width="520px"
:close-on-click-modal="false" class="edit-dialog">
<el-form :model="editForm" label-position="top" v-loading="editLoading" class="edit-form">
<el-form-item :label="$t('drama.projectName')" required>
<el-input
v-model="editForm.title"
:placeholder="$t('drama.projectNamePlaceholder')"
size="large"
/>
<el-input v-model="editForm.title" :placeholder="$t('drama.projectNamePlaceholder')" size="large" />
</el-form-item>
<el-form-item :label="$t('drama.projectDesc')">
<el-input
v-model="editForm.description"
type="textarea"
:rows="4"
:placeholder="$t('drama.projectDescPlaceholder')"
resize="none"
/>
<el-input v-model="editForm.description" type="textarea" :rows="4"
:placeholder="$t('drama.projectDescPlaceholder')" resize="none" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editDialogVisible = false" size="large">{{ $t('common.cancel') }}</el-button>
<el-button
type="primary"
@click="saveEdit"
:loading="editLoading"
size="large"
>
<el-button type="primary" @click="saveEdit" :loading="editLoading" size="large">
{{ $t('common.save') }}
</el-button>
</div>
@@ -118,13 +72,8 @@
</el-dialog>
<!-- Create Drama Dialog / 创建短剧弹窗 -->
<CreateDramaDialog
v-model="createDialogVisible"
@created="loadDramas"
/>
<CreateDramaDialog v-model="createDialogVisible" @created="loadDramas" />
<!-- AI Config Dialog / AI配置弹窗 -->
<AIConfigDialog v-model="showAIConfig" />
</div>
<!-- Sticky Pagination / 吸底分页器 -->
@@ -134,25 +83,13 @@
<span class="pagination-total">{{ $t('drama.totalProjects', { count: total }) }}</span>
</div>
<div class="pagination-controls">
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.page_size"
:total="total"
:page-sizes="[12, 24, 36, 48]"
:pager-count="5"
layout="prev, pager, next"
@size-change="loadDramas"
@current-change="loadDramas"
/>
<el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.page_size"
:total="total" :page-sizes="[12, 24, 36, 48]" :pager-count="5" layout="prev, pager, next"
@size-change="loadDramas" @current-change="loadDramas" />
</div>
<div class="pagination-size">
<span class="size-label">{{ $t('common.perPage') }}</span>
<el-select
v-model="queryParams.page_size"
size="small"
class="size-select"
@change="loadDramas"
>
<el-select v-model="queryParams.page_size" size="small" class="size-select" @change="loadDramas">
<el-option :value="12" label="12" />
<el-option :value="24" label="24" />
<el-option :value="36" label="36" />
@@ -168,19 +105,18 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
Plus,
Film,
Setting,
Edit,
View,
import {
Plus,
Film,
Setting,
Edit,
View,
Delete,
InfoFilled
InfoFilled
} from '@element-plus/icons-vue'
import { dramaAPI } from '@/api/drama'
import type { Drama, DramaListQuery } from '@/types/drama'
import { PageHeader, ProjectCard, ThemeToggle, ActionButton, CreateDramaDialog, EmptyState, AIConfigDialog } from '@/components/common'
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
import { AppHeader, ProjectCard, ActionButton, CreateDramaDialog, EmptyState } from '@/components/common'
const router = useRouter()
const loading = ref(false)
@@ -194,7 +130,6 @@ const queryParams = ref<DramaListQuery>({
// Create dialog state / 创建弹窗状态
const createDialogVisible = ref(false)
const showAIConfig = ref(false)
// Load drama list / 加载短剧列表
const loadDramas = async () => {
@@ -248,7 +183,7 @@ const saveEdit = async () => {
ElMessage.warning('请输入项目名称')
return
}
editLoading.value = true
try {
await dramaAPI.update(editForm.value.id, {
@@ -288,19 +223,19 @@ onMounted(() => {
.page-container {
min-height: 100vh;
background: var(--bg-primary);
padding: var(--space-2) var(--space-3);
/* padding: var(--space-2) var(--space-3); */
transition: background var(--transition-normal);
}
@media (min-width: 768px) {
.page-container {
padding: var(--space-3) var(--space-4);
/* padding: var(--space-3) var(--space-4); */
}
}
@media (min-width: 1024px) {
.page-container {
padding: var(--space-4) var(--space-5);
/* padding: var(--space-4) var(--space-5); */
}
}
@@ -309,6 +244,28 @@ onMounted(() => {
width: 100%;
}
/* ========================================
Page Title / 页面标题
======================================== */
.page-title {
display: flex;
flex-direction: column;
gap: 2px;
}
.page-title h1 {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.3;
}
.page-title .subtitle {
font-size: 0.8125rem;
color: var(--text-muted);
}
/* ========================================
Header Buttons / 头部按钮
======================================== */
@@ -332,7 +289,7 @@ onMounted(() => {
.btn-text {
display: none;
}
.header-btn {
padding: 0.5rem 0.75rem;
}
@@ -342,7 +299,9 @@ onMounted(() => {
Projects Grid / 项目网格 - 紧凑间距
======================================== */
.projects-grid {
padding: 12px;
display: flex;
flex-wrap: wrap;
/* grid-template-columns: repeat(2, 1fr); */
gap: var(--space-2);
margin-bottom: var(--space-4);
@@ -386,6 +345,7 @@ onMounted(() => {
Sticky Pagination / 吸底分页器
======================================== */
.pagination-sticky {
/* padding: 12px; */
position: fixed;
bottom: 0;
left: 0;

View File

@@ -2,12 +2,18 @@
<div class="page-container">
<div class="content-wrapper animate-fade-in">
<!-- Page Header / 页面头部 -->
<PageHeader
:title="drama?.title || ''"
:subtitle="drama?.description || $t('drama.management.overview')"
:show-back="true"
:back-text="$t('common.back')"
/>
<AppHeader :fixed="false" :show-logo="false">
<template #left>
<el-button text @click="$router.back()" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
<span>{{ $t('common.back') }}</span>
</el-button>
<div class="page-title">
<h1>{{ drama?.title || '' }}</h1>
<span class="subtitle">{{ drama?.description || $t('drama.management.overview') }}</span>
</div>
</template>
</AppHeader>
<!-- Tabs / 标签页 -->
<div class="tabs-wrapper">
@@ -60,15 +66,22 @@
</template>
</el-alert>
<el-card shadow="never" style="margin-top: 20px;">
<el-card shadow="never" class="project-info-card">
<template #header>
<h3 class="card-title">{{ $t('drama.management.projectInfo') }}</h3>
<div class="card-header">
<h3 class="card-title">{{ $t('drama.management.projectInfo') }}</h3>
<el-tag :type="getStatusType(drama?.status)" size="small">{{ getStatusText(drama?.status) }}</el-tag>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item :label="$t('drama.management.projectName')">{{ drama?.title }}</el-descriptions-item>
<el-descriptions-item :label="$t('common.createdAt')">{{ formatDate(drama?.created_at) }}</el-descriptions-item>
<el-descriptions :column="2" border class="project-descriptions">
<el-descriptions-item :label="$t('drama.management.projectName')">
<span class="info-value">{{ drama?.title }}</span>
</el-descriptions-item>
<el-descriptions-item :label="$t('common.createdAt')">
<span class="info-value">{{ formatDate(drama?.created_at) }}</span>
</el-descriptions-item>
<el-descriptions-item :label="$t('drama.management.projectDesc')" :span="2">
{{ drama?.description || $t('drama.management.noDescription') }}
<span class="info-desc">{{ drama?.description || $t('drama.management.noDescription') }}</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
@@ -250,7 +263,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Document, User, Picture, Plus } from '@element-plus/icons-vue'
import { dramaAPI } from '@/api/drama'
import type { Drama } from '@/types/drama'
import { PageHeader, StatCard, EmptyState } from '@/components/common'
import { AppHeader, StatCard, EmptyState } from '@/components/common'
const router = useRouter()
const route = useRoute()
@@ -543,19 +556,19 @@ onMounted(() => {
.page-container {
min-height: 100vh;
background: var(--bg-primary);
padding: var(--space-2) var(--space-3);
/* padding: var(--space-2) var(--space-3); */
transition: background var(--transition-normal);
}
@media (min-width: 768px) {
.page-container {
padding: var(--space-3) var(--space-4);
/* padding: var(--space-3) var(--space-4); */
}
}
@media (min-width: 1024px) {
.page-container {
padding: var(--space-4) var(--space-5);
/* padding: var(--space-4) var(--space-5); */
}
}
@@ -762,6 +775,20 @@ onMounted(() => {
border-color: var(--border-primary);
}
/* ========================================
Project Info Card / 项目信息卡片
======================================== */
.project-info-card {
margin-top: var(--space-5);
border-radius: var(--radius-lg);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-title {
margin: 0;
font-size: 1rem;
@@ -769,6 +796,30 @@ onMounted(() => {
color: var(--text-primary);
}
.project-descriptions {
width: 100%;
}
:deep(.project-descriptions .el-descriptions__label) {
width: 120px;
font-weight: 500;
color: var(--text-secondary);
}
:deep(.project-descriptions .el-descriptions__content) {
min-width: 150px;
}
.info-value {
font-weight: 500;
color: var(--text-primary);
}
.info-desc {
color: var(--text-secondary);
line-height: 1.6;
}
.dark :deep(.el-dialog) {
background: var(--bg-card);
}

View File

@@ -1,37 +1,34 @@
<template>
<div class="workflow-container">
<div class="workflow-header">
<div class="header-single-line">
<div class="header-left-section">
<el-button text @click="goBack" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
<span>{{ $t('dramaWorkflow.returnToList') }}</span>
</el-button>
<h2 class="drama-title">{{ drama?.title }}</h2>
<el-tag :type="getStatusType(drama?.status)" size="small">{{ getStatusText(drama?.status) }}</el-tag>
</div>
<AppHeader :fixed="false" :show-logo="false">
<template #left>
<el-button text @click="goBack" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
<span>{{ $t('dramaWorkflow.returnToList') }}</span>
</el-button>
<h2 class="drama-title">{{ drama?.title }}</h2>
<el-tag :type="getStatusType(drama?.status)" size="small">{{ getStatusText(drama?.status) }}</el-tag>
</template>
<template #center>
<!-- 步骤进度条 -->
<div class="steps-inline">
<div class="custom-steps">
<div class="step-item" :class="{ active: currentStep >= 0, current: currentStep === 0 }">
<div class="step-circle">1</div>
<span class="step-text">{{ $t('dramaWorkflow.episodeScript', { number: currentEpisodeNumber }) }}</span>
</div>
<el-icon class="step-arrow"><ArrowRight /></el-icon>
<div class="step-item" :class="{ active: currentStep >= 1, current: currentStep === 1 }">
<div class="step-circle">2</div>
<span class="step-text">{{ $t('dramaWorkflow.storyboardBreakdown') }}</span>
</div>
<el-icon class="step-arrow"><ArrowRight /></el-icon>
<div class="step-item" :class="{ active: currentStep >= 2, current: currentStep === 2 }">
<div class="step-circle">3</div>
<span class="step-text">{{ $t('dramaWorkflow.characterImages') }}</span>
</div>
<div class="custom-steps">
<div class="step-item" :class="{ active: currentStep >= 0, current: currentStep === 0 }">
<div class="step-circle">1</div>
<span class="step-text">{{ $t('dramaWorkflow.episodeScript', { number: currentEpisodeNumber }) }}</span>
</div>
<el-icon class="step-arrow"><ArrowRight /></el-icon>
<div class="step-item" :class="{ active: currentStep >= 1, current: currentStep === 1 }">
<div class="step-circle">2</div>
<span class="step-text">{{ $t('dramaWorkflow.storyboardBreakdown') }}</span>
</div>
<el-icon class="step-arrow"><ArrowRight /></el-icon>
<div class="step-item" :class="{ active: currentStep >= 2, current: currentStep === 2 }">
<div class="step-circle">3</div>
<span class="step-text">{{ $t('dramaWorkflow.characterImages') }}</span>
</div>
</div>
</div>
</div>
</template>
</AppHeader>
<!-- 当前阶段内容区域 -->
<div class="stage-area">
@@ -567,6 +564,7 @@ import { generationAPI } from '@/api/generation'
import { characterLibraryAPI } from '@/api/character-library'
import request from '@/utils/request'
import type { Drama, DramaStatus } from '@/types/drama'
import { AppHeader } from '@/components/common'
const route = useRoute()
const router = useRouter()

View File

@@ -1,39 +1,38 @@
<template>
<div class="page-container">
<div class="content-wrapper animate-fade-in">
<header class="page-header">
<div class="header-content">
<div class="header-left">
<button class="back-btn" @click="$router.back()">
<el-icon><ArrowLeft /></el-icon>
<span>{{ $t('workflow.backToProject') }}</span>
</button>
<div class="nav-divider"></div>
<h1 class="header-title">{{ $t('workflow.episodeProduction', { number: episodeNumber }) }}</h1>
</div>
<div class="header-center">
<div class="custom-steps">
<div class="step-item" :class="{ active: currentStep >= 0, current: currentStep === 0 }">
<div class="step-circle">1</div>
<span class="step-text">{{ $t('workflow.steps.content') }}</span>
</div>
<el-icon class="step-arrow"><ArrowRight /></el-icon>
<div class="step-item" :class="{ active: currentStep >= 1, current: currentStep === 1 }">
<div class="step-circle">2</div>
<span class="step-text">{{ $t('workflow.steps.generateImages') }}</span>
</div>
<el-icon class="step-arrow"><ArrowRight /></el-icon>
<div class="step-item" :class="{ active: currentStep >= 2, current: currentStep === 2 }">
<div class="step-circle">3</div>
<span class="step-text">{{ $t('workflow.steps.splitStoryboard') }}</span>
</div>
<AppHeader :fixed="false" :show-logo="false">
<template #left>
<el-button text @click="$router.back()" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
<span>{{ $t('workflow.backToProject') }}</span>
</el-button>
<h1 class="header-title">{{ $t('workflow.episodeProduction', { number: episodeNumber }) }}</h1>
</template>
<template #center>
<div class="custom-steps">
<div class="step-item" :class="{ active: currentStep >= 0, current: currentStep === 0 }">
<div class="step-circle">1</div>
<span class="step-text">{{ $t('workflow.steps.content') }}</span>
</div>
<el-icon class="step-arrow"><ArrowRight /></el-icon>
<div class="step-item" :class="{ active: currentStep >= 1, current: currentStep === 1 }">
<div class="step-circle">2</div>
<span class="step-text">{{ $t('workflow.steps.generateImages') }}</span>
</div>
<el-icon class="step-arrow"><ArrowRight /></el-icon>
<div class="step-item" :class="{ active: currentStep >= 2, current: currentStep === 2 }">
<div class="step-circle">3</div>
<span class="step-text">{{ $t('workflow.steps.splitStoryboard') }}</span>
</div>
</div>
<div class="header-right">
<el-button :icon="Setting" circle @click="showModelConfigDialog" :title="$t('workflow.modelConfig')" />
</div>
</div>
</header>
</template>
<template #right>
<el-button :icon="Setting" @click="showModelConfigDialog" :title="$t('workflow.modelConfig')">
图文配置
</el-button>
</template>
</AppHeader>
<!-- 阶段 0: 章节内容 + 提取角色场景 -->
<el-card v-show="currentStep === 0" shadow="never" class="stage-card stage-card-fullscreen">
@@ -822,7 +821,7 @@ import { aiAPI } from '@/api/ai'
import type { AIServiceConfig } from '@/types/ai'
import { imageAPI } from '@/api/image'
import type { Drama } from '@/types/drama'
import PageHeader from '@/components/common/PageHeader.vue'
import { AppHeader } from '@/components/common'
const route = useRoute()
const router = useRouter()
@@ -1750,19 +1749,19 @@ onMounted(() => {
.page-container {
min-height: 100vh;
background: var(--bg-primary);
padding: var(--space-2) var(--space-3);
// padding: var(--space-2) var(--space-3);
transition: background var(--transition-normal);
}
@media (min-width: 768px) {
.page-container {
padding: var(--space-3) var(--space-4);
// padding: var(--space-3) var(--space-4);
}
}
@media (min-width: 1024px) {
.page-container {
padding: var(--space-4) var(--space-5);
// padding: var(--space-4) var(--space-5);
}
}
@@ -1841,7 +1840,7 @@ onMounted(() => {
}
.workflow-card {
margin-bottom: var(--space-4);
margin: 12px;
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
@@ -1915,7 +1914,7 @@ onMounted(() => {
}
.stage-card {
margin-bottom: 24px;
margin: 12px;
&.stage-card-fullscreen {
.stage-body-fullscreen {
@@ -1950,14 +1949,13 @@ onMounted(() => {
}
.stage-body {
padding: 32px;
background: var(--bg-card);
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 24px;
margin: 12px 0;
flex-wrap: wrap;
justify-content: center;
align-items: center;
@@ -1990,8 +1988,8 @@ onMounted(() => {
margin-bottom: 16px;
padding: 16px;
background: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--border-primary);
// border-radius: 8px;
// border: 1px solid var(--border-primary);
.section-title {
display: flex;
@@ -2178,6 +2176,7 @@ onMounted(() => {
.character-image-list,
.scene-image-list {
padding: 5px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;

View File

@@ -1,22 +1,15 @@
<template>
<div class="professional-editor">
<!-- 顶部工具栏 -->
<div class="editor-toolbar">
<div class="toolbar-left">
<el-button link @click="goBack" class="back-btn">
<el-icon>
<ArrowLeft />
</el-icon>
{{ $t('editor.backToEpisode') }}
<AppHeader :fixed="false" :show-logo="false" @config-updated="loadVideoModels">
<template #left>
<el-button text @click="goBack" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
<span>{{ $t('editor.backToEpisode') }}</span>
</el-button>
<el-divider direction="vertical" />
<span class="episode-title">{{ drama?.title }} - {{ $t('editor.episode', { number: episodeNumber }) }}</span>
</div>
<div class="toolbar-right">
<!-- <el-button :icon="Setting" circle @click="showSettings = true" /> -->
</div>
</div>
</template>
</AppHeader>
<!-- 主编辑区域 -->
<div class="editor-main">
@@ -906,6 +899,7 @@ import type { Asset } from '@/types/asset'
import type { VideoMerge } from '@/api/videoMerge'
import VideoTimelineEditor from '@/components/editor/VideoTimelineEditor.vue'
import type { Drama, Episode, Storyboard } from '@/types/drama'
import { AppHeader } from '@/components/common'
const route = useRoute()
const router = useRouter()