首页更新

This commit is contained in:
2026-07-07 14:36:59 +08:00
parent 6a588015d0
commit 4679280e45
6 changed files with 426 additions and 208 deletions
+7 -40
View File
@@ -1,7 +1,5 @@
<template>
<div class="chat-list">
<div class="chat-divider">今天 15:14</div>
<div v-for="msg in messages" :key="msg.id" class="message-row" :class="{ 'is-user': msg.isUser }">
<div class="bubble-wrap">
<div class="bubble">{{ msg.content }}</div>
@@ -12,35 +10,18 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
interface Message {
id: number;
interface ChatMessage {
id: string;
content: string;
time: string;
isUser: boolean;
}
const messages = ref<Message[]>([
{
id: 1,
content: '你好!我叫知子,很高兴为您服务。您今天想聊些什么呢?',
time: '09:30',
isUser: false,
},
{
id: 2,
content: '你好!我想了解下这个系统都能做什么。',
time: '09:31',
isUser: true,
},
{
id: 3,
content: '当然可以,这个系统提供了多种功能:日记、文件、快捷指令、快捷回复,以及技能管理和模型管理入口。你想先看哪一块?',
time: '09:31',
isUser: false,
},
]);
interface Props {
messages: ChatMessage[];
}
defineProps<Props>();
</script>
<style scoped lang="scss">
@@ -53,19 +34,6 @@ const messages = ref<Message[]>([
padding: 28px 0 10px;
}
.chat-divider {
align-self: center;
font-size: 12px;
color: #64748b;
line-height: 1;
padding: 4px 16px;
background: rgba(255, 255, 255, 0.7);
border-radius: 999px;
border: 1px solid rgba(226, 233, 244, 0.8);
letter-spacing: 0.2px;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.05);
}
.message-row {
display: flex;
align-items: flex-start;
@@ -95,7 +63,6 @@ const messages = ref<Message[]>([
border: 1px solid rgba(226, 233, 244, 0.95);
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
transition: all 0.2s ease;
.message-row.is-user & {
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #2563eb 100%);
+9 -1
View File
@@ -118,7 +118,13 @@ const toggleWorkflow = (id: string) => {
emit('workflow-select', newId);
};
const clearWorkflow = () => {
const resetAll = () => {
message.value = '';
selectedWorkflowId.value = null;
emit('workflow-select', null);
};
const clearWorkflow = () => {
selectedWorkflowId.value = null;
emit('workflow-select', null);
};
@@ -126,6 +132,8 @@ const clearWorkflow = () => {
onMounted(() => {
fetchWorkflows();
});
defineExpose({ resetAll, clearWorkflow, selectedWorkflowId });
</script>
<style scoped lang="scss">
+127 -50
View File
@@ -130,50 +130,56 @@
</div>
</div>
<!-- 对话区域 -->
<div v-if="activeHistoryId && activeMenu === 'chat'" class="content-body">
<div class="chat-container">
<ChatList />
</div>
</div>
<!-- 默认占位 -->
<div v-if="!workflowDetail && !(activeHistoryId && activeMenu === 'chat')" class="content-body placeholder-body">
<!-- 默认占位 — 无工作流时显示引导 -->
<div v-else class="content-body placeholder-body">
<div class="placeholder-content">
<div class="placeholder-icon">
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="12" width="64" height="48" rx="8" fill="url(#grad1)" fill-opacity="0.18"/>
<rect x="8" y="12" width="64" height="48" rx="8" stroke="url(#grad1)" stroke-width="1.2"/>
<path d="M28 32h24M28 40h16M28 48h20" stroke="url(#grad1)" stroke-width="2" stroke-linecap="round"/>
<path d="M62 72c0-8.837-7.163-16-16-16s-16 7.163-16 16" stroke="url(#grad2)" stroke-width="2" stroke-linecap="round"/>
<circle cx="46" cy="48" r="4" fill="url(#grad2)"/>
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="80" y2="60" gradientUnits="userSpaceOnUse">
<stop stop-color="#3b82f6"/>
<stop offset="1" stop-color="#8b5cf6"/>
</linearGradient>
<linearGradient id="grad2" x1="0" y1="0" x2="80" y2="60" gradientUnits="userSpaceOnUse">
<stop stop-color="#8b5cf6"/>
<stop offset="1" stop-color="#ec4899"/>
</linearGradient>
</defs>
</svg>
<div class="icon-ring ring-outer">
<div class="icon-ring ring-inner">
<div class="icon-ring ring-core">
<svg class="icon-sparkle" width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2l1.09 6.26L18 4.5l-3.41 4.74L20 10l-5.41.76L18 15.5l-4.91-2.76L12 19l-1.09-6.26L6 15.5l3.41-4.74L4 10l5.41-.76L6 4.5l4.91 2.74L12 2z" fill="currentColor"/>
</svg>
</div>
</div>
</div>
</div>
<h2 class="placeholder-title">开始内容创作</h2>
<p class="placeholder-desc">选择下方工作流填写参数后即可快速生成内容</p>
<div class="placeholder-hints">
<div class="hint-item">
<span class="hint-dot"></span>
<span>点击工作流胶囊快速切换</span>
<h2 class="placeholder-title">选择下方工作流进行创作</h2>
<p class="placeholder-desc">点击工作流胶囊快速切换填写参数后一键生成内容</p>
<div class="placeholder-steps">
<div class="step-item">
<div class="step-num">1</div>
<div class="step-text">
<span class="step-label">选择工作流</span>
<span class="step-hint">点击胶囊切换不同创作模式</span>
</div>
</div>
<div class="hint-item">
<span class="hint-dot"></span>
<span>对话记录中查看历史</span>
<div class="step-arrow">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</div>
<div class="hint-item">
<span class="hint-dot"></span>
<span>工作空间中管理作品</span>
<div class="step-item">
<div class="step-num">2</div>
<div class="step-text">
<span class="step-label">填写参数</span>
<span class="step-hint">按需配置内容与样式选项</span>
</div>
</div>
<div class="step-arrow">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</div>
<div class="step-item">
<div class="step-num">3</div>
<div class="step-text">
<span class="step-label">一键生成</span>
<span class="step-hint">AI 自动产出结果同步到工作空间</span>
</div>
</div>
</div>
<div class="placeholder-footer">
<span class="footer-hint">还可从左侧对话记录查看历史或工作空间管理作品</span>
</div>
</div>
</div>
@@ -183,19 +189,18 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { UploadFilled, Document } from '@element-plus/icons-vue';
import ChatList from './ChatList.vue';
import PatchTemplateEditor from '/@/components/patchTemplate/PatchTemplateEditor.vue';
import { uploadFile } from '/@/api/common/upload';
interface Props {
activeMenu: string;
workflowDetail: any;
activeHistoryId?: string | null;
}
const props = withDefaults(defineProps<Props>(), {
activeHistoryId: null,
});
activeHistoryId: null,
});
defineEmits<{
(e: 'close-workflow'): void;
}>();
@@ -204,7 +209,6 @@ const formValues = reactive<Record<string, any>>({});
const fieldFiles = reactive<Record<string, { name: string; url: string }[]>>({});
const uploadingFields = reactive<Record<string, boolean>>({});
const templates = ref<any[]>([]);
const getFieldKey = (node: any, field: any): string => {
if (field?.__isHttpBodyChild) {
return `${node.id}_body_${field.bodyKey}`;
@@ -586,11 +590,84 @@ defineExpose({ formValues, fieldFiles, templates, validateFormFields });
.chat-container { width: min(1060px, 82%); margin: 0 auto; padding: 20px 0 130px; }
.placeholder-body { display: flex; align-items: center; justify-content: center; }
.placeholder-content { text-align: center; padding: 40px 20px; max-width: 400px; }
.placeholder-icon { margin-bottom: 24px; display: flex; justify-content: center; }
.placeholder-title { font-size: 20px; font-weight: 700; color: #1e293b; margin: 0 0 8px; letter-spacing: -0.3px; }
.placeholder-desc { font-size: 14px; color: #94a3b8; margin: 0 0 28px; line-height: 1.6; }
.placeholder-hints { display: flex; flex-direction: column; gap: 10px; align-items: center; }
.hint-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #64748b; }
.hint-dot { width: 6px; height: 6px; border-radius: 50%; background: #cbd5e1; flex-shrink: 0; }
.placeholder-content { text-align: center; padding: 20px; max-width: 480px; }
/* ===== 图标:层叠圆环 + 星形 ===== */
.placeholder-icon { margin-bottom: 28px; display: flex; justify-content: center; }
.icon-ring {
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
}
.ring-outer {
width: 88px; height: 88px;
background: radial-gradient(circle at 35% 30%, rgba(59, 130, 246, 0.10) 0%, rgba(139, 92, 246, 0.06) 100%);
border: 1.5px solid rgba(59, 130, 246, 0.15);
&:hover { transform: scale(1.05); }
}
.ring-inner {
width: 62px; height: 62px;
background: radial-gradient(circle at 35% 30%, rgba(59, 130, 246, 0.14) 0%, rgba(139, 92, 246, 0.08) 100%);
border: 1.5px solid rgba(59, 130, 246, 0.20);
}
.ring-core {
width: 42px; height: 42px;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.35);
}
.icon-sparkle { color: #fff; }
/* ===== 标题与描述 ===== */
.placeholder-title { font-size: 18px; font-weight: 700; color: #0f172a; margin: 0 0 6px; letter-spacing: -0.2px; line-height: 1.4; }
.placeholder-desc { font-size: 14px; color: #94a3b8; margin: 0 0 32px; line-height: 1.5; }
/* ===== 步骤引导 ===== */
.placeholder-steps {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
margin-bottom: 28px;
}
.step-item {
display: flex;
align-items: center;
gap: 10px;
text-align: left;
}
.step-num {
width: 28px; height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #eff6ff 0%, #f0f0ff 100%);
border: 1.5px solid #bfdbfe;
color: #3b82f6;
font-size: 13px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
.step-item:hover & {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
color: #fff;
border-color: transparent;
}
}
.step-text { display: flex; flex-direction: column; gap: 1px; }
.step-label { font-size: 13px; font-weight: 600; color: #334155; line-height: 1.3; white-space: nowrap; }
.step-hint { font-size: 11px; color: #94a3b8; line-height: 1.3; white-space: nowrap; }
.step-arrow {
color: #cbd5e1;
display: flex;
align-items: center;
margin: 0 6px;
flex-shrink: 0;
}
/* ===== 底部辅助提示 ===== */
.placeholder-footer { margin-top: 4px; }
.footer-hint { font-size: 12px; color: #cbd5e1; line-height: 1.5; }
</style>
+40 -10
View File
@@ -38,6 +38,9 @@
<el-icon class="item-icon"><ChatLineRound /></el-icon>
<div class="item-body">
<div class="item-title">{{ item.title }}</div>
<div v-if="item.status === 'executing'" class="status-badge status-running">执行中</div>
<div v-if="item.status === 'completed'" class="status-badge status-done">执行完成</div>
<div v-if="item.status === 'failed'" class="status-badge status-failed">执行失败</div>
<div class="item-time">{{ item.time }}</div>
</div>
<button class="item-del-btn" @click.stop="handleDeleteHistory(item.id)">
@@ -53,7 +56,7 @@
<div v-show="activeTab === 'workspace'" class="panel" v-loading="treeLoading">
<el-empty v-if="!treeLoading && treeNodes.length === 0" :image-size="48" description="暂无内容" />
<div v-else class="panel-list">
<div v-for="dateNode in visibleWorkspace" :key="dateNode.id" class="tree-date-group">
<div v-for="dateNode in treeNodes" :key="dateNode.id" class="tree-date-group">
<div class="tree-date-label">{{ dateNode.label }}</div>
<div v-for="flowNode in dateNode.children" :key="flowNode.id" class="tree-flow-group">
<div class="tree-flow-label">
@@ -73,9 +76,9 @@
</div>
</div>
</div>
<button v-if="workspaceShowCount < treeNodes.length" class="load-more-btn" @click="workspaceShowCount += WORKSPACE_PAGE">
<button v-if="props.hasMoreWorkspace" class="load-more-btn" @click="emit('load-more-workspace')">
<el-icon><ArrowDown /></el-icon>
展开更多 ({{ treeNodes.length - workspaceShowCount }} )
展开更多
</button>
</div>
</div>
@@ -83,7 +86,7 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ref } from 'vue';
import { Delete, ChatDotRound, ChatLineRound, EditPen, ArrowDown, Promotion, Document, VideoPlay, Headset, Picture, FolderOpened } from '@element-plus/icons-vue';
interface TreeNode {
@@ -101,6 +104,7 @@ interface HistoryItem {
id: string;
title: string;
time: string;
status?: 'idle' | 'executing' | 'completed' | 'failed';
}
interface Props {
@@ -109,6 +113,7 @@ interface Props {
historyList: HistoryItem[];
treeNodes: TreeNode[];
treeLoading: boolean;
hasMoreWorkspace: boolean;
}
interface Emits {
@@ -118,19 +123,15 @@ interface Emits {
(e: 'delete-history', id: string): void;
(e: 'preview-node', data: TreeNode): void;
(e: 'download-node', data: TreeNode): void;
(e: 'delete-node', data: TreeNode): void;
(e: 'delete-node', data: TreeNode): void;
(e: 'load-more-workspace'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const WORKSPACE_PAGE = 5;
const activeTab = ref<'history' | 'workspace'>('history');
const workspaceShowCount = ref(WORKSPACE_PAGE);
const visibleHistory = computed(() => props.historyList);
const visibleWorkspace = computed(() => props.treeNodes.slice(0, workspaceShowCount.value));
const getFileIcon = (fileType?: string) => {
if (!fileType) return Document;
@@ -504,4 +505,33 @@ const handleDeleteNode = (data: TreeNode) => emit('delete-node', data);
background: #dbeafe;
}
}
.status-badge {
font-size: 11px;
padding: 1px 8px;
border-radius: 999px;
line-height: 1.5;
flex-shrink: 0;
margin-left: auto;
}
.status-running {
background: #dbeafe;
color: #2563eb;
animation: pulse 1.5s ease-in-out infinite;
}
.status-done {
background: #d1fae5;
color: #059669;
}
.status-failed {
background: #fee2e2;
color: #dc2626;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
</style>