优化
This commit is contained in:
@@ -15,16 +15,13 @@ const shouldShowUser = computed(() => {
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="fixed top-0 left-0 right-0
|
||||
flex items-center px-[30px]
|
||||
bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 shadow-sm
|
||||
text-slate-900 dark:text-slate-50"
|
||||
:style="{ height: 'var(--header-height)', zIndex: 'var(--z-header)' }"
|
||||
class="fixed top-0 left-0 right-0 flex items-center px-6 h-[70px] bg-background/95 backdrop-blur-sm border-b border-border z-50"
|
||||
>
|
||||
<div class="flex items-center gap-md flex-1">
|
||||
<BrandLogo :size="40" />
|
||||
<div class="flex items-center gap-4 flex-1">
|
||||
<BrandLogo :size="36" />
|
||||
</div>
|
||||
<div class="flex items-center gap-md pr-1">
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<UserDropdown v-if="shouldShowUser" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Dialog v-model:open="visible" @update:open="$emit('update:open', $event)">
|
||||
<DialogContent class="sm:max-w-[900px] p-0">
|
||||
<DialogContent class="sm:max-w-[900px] p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ modalTitle }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -324,13 +324,14 @@ watch(() => props.open, (isOpen) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 搜索栏样式 */
|
||||
.search-bar {
|
||||
padding: 16px;
|
||||
background: var(--color-gray-50);
|
||||
border-radius: 8px;
|
||||
background: var(--muted);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
@@ -338,7 +339,7 @@ watch(() => props.open, (isOpen) => {
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-gray-400);
|
||||
color: var(--muted-foreground);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -361,20 +362,20 @@ watch(() => props.open, (isOpen) => {
|
||||
}
|
||||
|
||||
.video-grid::-webkit-scrollbar-track {
|
||||
background: var(--color-gray-100);
|
||||
border-radius: 3px;
|
||||
background: var(--muted);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.video-grid::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray-300);
|
||||
border-radius: 3px;
|
||||
background: var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* 视频卡片样式 */
|
||||
.video-card {
|
||||
background: var(--card);
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-fast);
|
||||
@@ -399,7 +400,7 @@ watch(() => props.open, (isOpen) => {
|
||||
width: 100%;
|
||||
height: 112px;
|
||||
overflow: hidden;
|
||||
background: var(--color-gray-100);
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.video-thumbnail img {
|
||||
@@ -493,7 +494,7 @@ watch(() => props.open, (isOpen) => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--color-gray-500);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.empty-illustration {
|
||||
@@ -504,7 +505,7 @@ watch(() => props.open, (isOpen) => {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 16px;
|
||||
background: var(--color-gray-100);
|
||||
background: var(--muted);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -518,13 +519,13 @@ watch(() => props.open, (isOpen) => {
|
||||
.empty-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-900);
|
||||
color: var(--foreground);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 14px;
|
||||
color: var(--color-gray-500);
|
||||
color: var(--muted-foreground);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -553,14 +554,14 @@ watch(() => props.open, (isOpen) => {
|
||||
.video-card-skeleton {
|
||||
background: white;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeleton-thumbnail {
|
||||
width: 100%;
|
||||
height: 112px;
|
||||
background: linear-gradient(90deg, var(--color-gray-100) 25%, var(--color-gray-200) 50%, var(--color-gray-100) 75%);
|
||||
background: linear-gradient(90deg, var(--muted) 25%, var(--border) 50%, var(--muted) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
@@ -572,20 +573,20 @@ watch(() => props.open, (isOpen) => {
|
||||
.skeleton-title {
|
||||
height: 16px;
|
||||
width: 70%;
|
||||
background: linear-gradient(90deg, var(--color-gray-100) 25%, var(--color-gray-200) 50%, var(--color-gray-100) 75%);
|
||||
background: linear-gradient(90deg, var(--muted) 25%, var(--border) 50%, var(--muted) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.skeleton-meta {
|
||||
height: 12px;
|
||||
width: 40%;
|
||||
background: linear-gradient(90deg, var(--color-gray-100) 25%, var(--color-gray-200) 50%, var(--color-gray-100) 75%);
|
||||
background: linear-gradient(90deg, var(--muted) 25%, var(--border) 50%, var(--muted) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
|
||||
@@ -385,8 +385,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.synthesize-btn {
|
||||
height: 34px;
|
||||
padding: 0 var(--space-3-5);
|
||||
height: 36px;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: var(--radius);
|
||||
border: none;
|
||||
background: var(--primary);
|
||||
@@ -413,7 +413,7 @@ onBeforeUnmount(() => {
|
||||
.voice-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
|
||||
&.has-many {
|
||||
max-height: 280px;
|
||||
@@ -588,7 +588,7 @@ onBeforeUnmount(() => {
|
||||
.download-btn {
|
||||
color: var(--muted-foreground);
|
||||
font-size: var(--font-size-xs);
|
||||
padding: var(--space-1) var(--space-2-5);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
height: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--duration-fast);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col items-center justify-center p-8 text-center">
|
||||
<div class="relative w-24 h-24 mb-6">
|
||||
<div class="absolute inset-0 rounded-full border-2 border-primary/20 animate-pulse"></div>
|
||||
<div class="absolute inset-1 rounded-full border-2 border-primary/15 animate-pulse" style="animation-delay: 0.5s"></div>
|
||||
<div class="absolute inset-2 rounded-full border-2 border-primary/10 animate-pulse" style="animation-delay: 1s"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-gradient-to-br from-primary to-violet-500 flex items-center justify-center shadow-lg shadow-primary/30">
|
||||
<Icon icon="lucide:bot" class="text-white text-xl" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-foreground mb-2">准备好为你生成内容</h3>
|
||||
<p class="text-sm text-muted-foreground mb-6 max-w-[280px]">
|
||||
在下方输入框描述你的需求,AI 将立即开始创作
|
||||
</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Icon icon="lucide:zap" class="text-primary" />
|
||||
<span>深度模式支持复杂任务</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Icon icon="lucide:pencil" class="text-primary" />
|
||||
<span>可随时重新生成调整</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import PointsTag from '@/components/PointsTag.vue'
|
||||
|
||||
const modelMode = defineModel('modelMode', { default: 'pro' })
|
||||
const inputText = defineModel('inputText', { default: '' })
|
||||
|
||||
defineProps({
|
||||
isGenerating: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['generate'])
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
emit('generate')
|
||||
}
|
||||
}
|
||||
|
||||
const handleGenerate = () => emit('generate')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="shrink-0 px-5 py-4 border-t border-border bg-background/95 backdrop-blur-sm">
|
||||
<!-- Model Selector -->
|
||||
<Tabs v-model="modelMode" class="mb-3">
|
||||
<TabsList class="w-full">
|
||||
<TabsTrigger value="standard" class="flex-1 gap-1.5">
|
||||
标准
|
||||
<PointsTag :points="10" size="small" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pro" class="flex-1 gap-1.5">
|
||||
<Icon icon="lucide:zap" class="size-4" />
|
||||
深度
|
||||
<PointsTag :points="50" size="small" />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<!-- Input Box -->
|
||||
<div class="relative">
|
||||
<Textarea
|
||||
v-model="inputText"
|
||||
placeholder="输入你的需求..."
|
||||
class="pr-12 min-h-[52px] max-h-[120px] resize-none"
|
||||
@keydown="handleKeyDown"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
class="absolute right-2 bottom-2 size-9"
|
||||
:disabled="!inputText.trim() || isGenerating"
|
||||
@click="handleGenerate"
|
||||
>
|
||||
<Icon v-if="isGenerating" icon="lucide:loader-2" class="size-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:send" class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-muted-foreground mt-2 mb-0">
|
||||
按 Enter 发送,Shift + Enter 换行
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
defineProps({
|
||||
agent: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['history'])
|
||||
|
||||
const openHistory = () => emit('history')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-border shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-primary to-violet-500 flex items-center justify-center overflow-hidden">
|
||||
<img
|
||||
v-if="agent?.avatar"
|
||||
:src="agent.avatar"
|
||||
:alt="agent.name"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<Icon v-else icon="lucide:bot" class="text-white text-xl" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-foreground">{{ agent?.name || 'AI 助手' }}</div>
|
||||
<div class="text-sm text-muted-foreground">{{ agent?.categoryName || '通用' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 w-9 hover:bg-accent hover:text-accent-foreground"
|
||||
@click="openHistory"
|
||||
title="历史记录"
|
||||
>
|
||||
<Icon icon="lucide:history" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
|
||||
defineProps({
|
||||
currentInput: { type: String, default: '' },
|
||||
generatedContent: { type: String, default: '' },
|
||||
isGenerating: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['reset', 'copy', 'regenerate'])
|
||||
|
||||
const handleReset = () => emit('reset')
|
||||
const handleCopy = () => emit('copy', props.generatedContent)
|
||||
const handleRegenerate = () => emit('regenerate')
|
||||
|
||||
const scrollAreaRef = defineExpose({ scrollAreaRef })
|
||||
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (scrollAreaRef.value) {
|
||||
const viewport = scrollAreaRef.value.$el?.querySelector('[data-reka-scroll-area-viewport]')
|
||||
if (viewport) {
|
||||
viewport.scrollTop = viewport.scrollHeight
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ scrollToBottom })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col min-h-0 p-5 gap-4">
|
||||
<!-- User Prompt Display -->
|
||||
<div class="flex items-start gap-2 p-3 bg-muted rounded-lg">
|
||||
<p class="flex-1 text-sm text-foreground leading-relaxed m-0">{{ currentInput }}</p>
|
||||
<Button
|
||||
v-if="generatedContent && !isGenerating"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="size-7 shrink-0"
|
||||
@click="handleReset"
|
||||
>
|
||||
<Icon icon="lucide:pencil" class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Result Content -->
|
||||
<ScrollArea ref="scrollAreaRef" class="flex-1 min-h-0">
|
||||
<div class="p-4 bg-card border border-border rounded-lg">
|
||||
<!-- Generating Skeleton -->
|
||||
<div v-if="isGenerating && !generatedContent" class="space-y-3">
|
||||
<div class="h-4 bg-muted rounded animate-pulse"></div>
|
||||
<div class="h-4 bg-muted rounded animate-pulse w-3/4"></div>
|
||||
<div class="h-4 bg-muted rounded animate-pulse w-1/2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Actual Content -->
|
||||
<div
|
||||
v-else
|
||||
class="text-sm text-foreground leading-relaxed whitespace-pre-wrap"
|
||||
:class="{ 'opacity-90': isGenerating }"
|
||||
>
|
||||
{{ generatedContent }}
|
||||
<span v-if="isGenerating" class="inline-block w-0.5 h-4 bg-primary ml-0.5 animate-pulse"></span>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<!-- Action Bar -->
|
||||
<div v-if="generatedContent && !isGenerating" class="flex gap-3">
|
||||
<Button class="flex-1" @click="handleCopy">
|
||||
<Icon icon="lucide:copy" class="mr-2" />
|
||||
复制
|
||||
</Button>
|
||||
<Button variant="outline" class="flex-1" @click="handleRegenerate">
|
||||
<Icon icon="lucide:refresh-cw" class="mr-2" />
|
||||
重新生成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user