Files
admin-ui/src/views/home/index.vue
2026-06-15 16:52:51 +08:00

290 lines
8.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="home-chat-container">
<Sidebar
:active-menu="activeMenu"
:active-history-id="activeHistoryId"
:history-list="historyList"
:tree-nodes="treeNodes"
:tree-loading="treeLoading"
@menu-change="handleMenuChange"
@new-chat="handleCreateHistory"
@select-history="handleSelectHistory"
@delete-history="handleDeleteHistory"
@preview-node="previewNode"
@download-node="downloadNode"
/>
<div class="main-wrapper">
<MainContent :active-menu="activeMenu" />
<InputBar @send="handleSend" />
</div>
<!-- 预览弹窗 -->
<el-dialog v-model="previewDialogVisible" title="预览" width="95%" top="2vh" :close-on-click-modal="false" destroy-on-close>
<div class="preview-container">
<el-image v-if="previewUrl && previewMode === 'image'" :src="previewUrl" fit="contain" style="width: 100%; height: 100%" />
<video
v-else-if="previewUrl && previewMode === 'video'"
:src="previewUrl"
controls
style="width: 100%; height: 100%; background: #000"
></video>
<audio v-else-if="previewUrl && previewMode === 'audio'" :src="previewUrl" controls style="width: 100%"></audio>
<iframe v-else-if="previewUrl" :src="previewUrl" class="preview-iframe" frameborder="0"></iframe>
<el-empty v-else description="无法加载预览内容" />
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import Sidebar from './components/Sidebar.vue';
import MainContent from './components/MainContent.vue';
import InputBar from './components/InputBar.vue';
import type { ExecutionTreeItem } from '/@/api/settings/creation';
import { getExecutionList } from '/@/api/settings/creation';
interface HistoryItem {
id: number;
title: string;
time: string;
}
interface TreeNode {
id: string;
label: string;
nodeType: string;
children?: TreeNode[];
fileUrl?: string;
workflowId?: number | string;
fileType?: string;
sessionId?: string;
}
const activeMenu = ref('chat');
const activeHistoryId = ref(1);
const treeLoading = ref(false);
const treeNodes = ref<TreeNode[]>([]);
const imgAddressPrefix = ref('');
const previewDialogVisible = ref(false);
const previewUrl = ref('');
const previewMode = ref<'iframe' | 'image' | 'video' | 'audio'>('iframe');
const historyList = ref<HistoryItem[]>([
{ id: 1, title: '首页风格优化方案', time: '刚刚' },
{ id: 2, title: '模型配置逻辑检查', time: '今天 11:20' },
{ id: 3, title: '技能管理模块梳理', time: '昨天' },
{ id: 4, title: '快捷回复产品设计', time: '2 天前' },
]);
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
const joinUrl = (b: string, p: string) => `${b.replace(/\/$/, '')}${p.startsWith('/') ? p : `/${p}`}`;
const buildAssetUrl = (p?: string) =>
!p
? ''
: /^https?:\/\//i.test(p)
? p
: /^https?:\/\//i.test(imgAddressPrefix.value || '')
? joinUrl(imgAddressPrefix.value, p)
: imgAddressPrefix.value
? joinUrl(joinUrl(apiBaseUrl, imgAddressPrefix.value), p)
: joinUrl(apiBaseUrl, p);
const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
tree.map((d, di) => ({
id: `date-${di}`,
label: d.createDate,
nodeType: 'date',
children: (d.flows || []).map((f, fi) => ({
id: `flow-${di}-${fi}`,
label: f.flowName || '未命名工作流',
nodeType: 'contentType',
workflowId: f.Id,
sessionId: f.sessionId,
children: (f.items || []).map((item, ii) => ({
id: `item-${di}-${fi}-${ii}`,
label: item.label || `作品${ii + 1}`,
nodeType: 'title',
fileUrl: item.content,
fileType: item.type,
workflowId: f.Id,
sessionId: f.sessionId,
})),
})),
}));
// 模拟mock数据
const mockTreeData: ExecutionTreeItem[] = [
{
createDate: '今天',
flows: [
{
Id: 1,
flowName: '代码审查任务',
sessionId: 'session-1',
items: [
{ label: '审查结果.md', content: 'https://placekitten.com/800/600', type: 'text/markdown', timestamp: '' },
{ label: '流程图.png', content: 'https://placekitten.com/800/600', type: 'image/png', timestamp: '' },
],
},
{
Id: 2,
flowName: '需求分析',
sessionId: 'session-2',
items: [{ label: '需求拆解.txt', content: '# 需求分析结果\n\n- 功能点一\n- 功能点二\n- 需要优化', type: 'text/plain', timestamp: '' }],
},
],
},
{
createDate: '昨天',
flows: [
{
Id: 3,
flowName: '生成演示视频',
sessionId: 'session-3',
items: [{ label: '输出视频.mp4', content: 'https://www.w3schools.com/html/mov_bbb.mp4', type: 'video/mp4', timestamp: '' }],
},
],
},
];
const getList = async () => {
treeLoading.value = true;
try {
const res = await getExecutionList();
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
const data = res.data?.tree || [];
// 如果后端没有数据使用mock数据展示
treeNodes.value = data.length > 0 ? buildTreeNodes(data) : buildTreeNodes(mockTreeData);
} catch {
// 出错时使用mock数据展示
treeNodes.value = buildTreeNodes(mockTreeData);
imgAddressPrefix.value = '';
} finally {
treeLoading.value = false;
}
};
const getPreviewMode = (fileType?: string): 'image' | 'video' | 'audio' | 'iframe' => {
if (!fileType) return 'iframe';
const lower = fileType.toLowerCase();
if (lower.includes('image') || lower.includes('jpg') || lower.includes('png') || lower.includes('gif')) {
return 'image';
}
if (lower.includes('video') || lower.includes('mp4') || lower.includes('webm')) {
return 'video';
}
if (lower.includes('audio') || lower.includes('mp3') || lower.includes('wav')) {
return 'audio';
}
return 'iframe';
};
const previewNode = (data: TreeNode) => {
if (!data.fileUrl) return;
previewUrl.value = buildAssetUrl(data.fileUrl);
previewMode.value = getPreviewMode(data.fileType);
previewDialogVisible.value = true;
};
const downloadNode = (data: TreeNode) => {
if (!data.fileUrl) return;
const url = buildAssetUrl(data.fileUrl);
const a = document.createElement('a');
a.href = url;
a.download = data.label;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const handleMenuChange = (menu: string) => {
activeMenu.value = menu;
};
const handleSend = (_message: string) => {
// 预留发送逻辑
};
const handleSelectHistory = (id: number) => {
activeHistoryId.value = id;
activeMenu.value = 'chat';
ElMessage.success('已切换会话');
};
const handleCreateHistory = () => {
const id = Date.now();
historyList.value.unshift({
id,
title: `新会话 ${historyList.value.length + 1}`,
time: '刚刚',
});
activeHistoryId.value = id;
activeMenu.value = 'chat';
ElMessage.success('已新建会话');
};
const handleDeleteHistory = (id: number) => {
const idx = historyList.value.findIndex((item) => item.id === id);
if (idx < 0) return;
historyList.value.splice(idx, 1);
if (activeHistoryId.value === id && historyList.value.length) {
activeHistoryId.value = historyList.value[0].id;
}
ElMessage.success('已删除会话');
};
onMounted(() => {
getList();
});
</script>
<style scoped lang="scss">
.home-chat-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
overflow: hidden;
background:
radial-gradient(1200px 600px at 0% 0%, rgba(59, 130, 246, 0.18) 0%, rgba(59, 130, 246, 0) 45%),
radial-gradient(1000px 500px at 100% 0%, rgba(139, 92, 246, 0.15) 0%, rgba(139, 92, 246, 0) 50%),
radial-gradient(800px 400px at 0% 100%, rgba(236, 72, 153, 0.12) 0%, rgba(236, 72, 153, 0) 55%),
radial-gradient(600px 300px at 100% 100%, rgba(16, 185, 129, 0.10) 0%, rgba(16, 185, 129, 0) 60%),
linear-gradient(135deg, #f8fbff 0%, #e6f0ff 60%, #f0f4fa 100%);
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
position: relative;
&::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0) 32%),
radial-gradient(900px 360px at 15% 115%, rgba(99, 102, 241, 0.06) 0%, rgba(99, 102, 241, 0) 70%);
}
}
.preview-container {
max-height: 85vh;
overflow: auto;
}
.preview-iframe {
width: 100%;
min-height: 60vh;
}
</style>