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

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