2 Commits

9 changed files with 1718 additions and 637 deletions

View File

@@ -0,0 +1,141 @@
<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 v-if="!msg.isUser" class="avatar-wrap">
<div class="ai-avatar">AI</div>
</div>
<div class="bubble-wrap">
<div class="bubble">{{ msg.content }}</div>
<div class="time">{{ msg.time }}</div>
</div>
<div v-if="msg.isUser" class="avatar-wrap">
<el-avatar :size="28" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
interface Message {
id: number;
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,
},
]);
</script>
<style scoped lang="scss">
.chat-list {
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 16px;
padding: 28px 0 10px;
}
.chat-divider {
align-self: center;
font-size: 12px;
color: #8b95a7;
line-height: 1;
padding: 0 12px;
letter-spacing: 0.2px;
}
.message-row {
display: flex;
align-items: flex-end;
gap: 10px;
max-width: 100%;
&.is-user {
justify-content: flex-end;
}
}
.avatar-wrap {
width: 30px;
height: 30px;
flex-shrink: 0;
}
.ai-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: #53607a;
background: linear-gradient(145deg, #f7f9fc 0%, #e9eef6 100%);
border: 1px solid #dfe6f0;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
}
.bubble-wrap {
display: flex;
flex-direction: column;
gap: 5px;
max-width: min(84%, 1120px);
}
.bubble {
font-size: 14px;
line-height: 1.75;
padding: 12px 14px;
border-radius: 12px;
word-break: break-word;
color: #1f2937;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(226, 233, 244, 0.95);
backdrop-filter: blur(8px);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
.message-row.is-user & {
background: linear-gradient(135deg, #58a6ff 0%, #3b82f6 52%, #2563eb 100%);
border-color: rgba(59, 130, 246, 0.32);
color: #fff;
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.33);
}
}
.time {
font-size: 11px;
color: #8f9aae;
padding: 0 4px;
.message-row.is-user & {
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,164 @@
<template>
<div class="command-list">
<div v-for="command in commandList" :key="command.id" class="command-card" @click="handleCommandClick(command)">
<div class="command-icon">
<el-icon :size="32" color="#3b82f6">
<Lightning />
</el-icon>
</div>
<div class="command-content">
<h3 class="command-name">{{ command.name }}</h3>
<p class="command-desc">{{ command.description }}</p>
<div class="command-trigger">
<el-tag size="small" type="info">{{ command.trigger }}</el-tag>
</div>
</div>
<div class="command-actions">
<el-button text size="small" @click.stop="handleEdit(command)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button text size="small" type="danger" @click.stop="handleDelete(command)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Lightning, Edit, Delete } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
interface Command {
id: number;
name: string;
description: string;
trigger: string;
action: string;
}
const commandList = ref<Command[]>([
{
id: 1,
name: '快速创建组件',
description: '快速创建一个 Vue 组件模板,包含基础结构和样式',
trigger: '/component',
action: 'create_component',
},
{
id: 2,
name: '生成 API 接口',
description: '根据接口文档快速生成 API 请求函数',
trigger: '/api',
action: 'generate_api',
},
{
id: 3,
name: '代码格式化',
description: '格式化当前文件的代码,统一代码风格',
trigger: '/format',
action: 'format_code',
},
{
id: 4,
name: '添加类型定义',
description: '为 JavaScript 代码添加 TypeScript 类型定义',
trigger: '/types',
action: 'add_types',
},
{
id: 5,
name: '生成测试用例',
description: '为当前组件或函数生成单元测试代码',
trigger: '/test',
action: 'generate_test',
},
{
id: 6,
name: '优化性能',
description: '分析代码并提供性能优化建议',
trigger: '/optimize',
action: 'optimize_code',
},
]);
const handleCommandClick = (command: Command) => {
ElMessage.success(`执行指令: ${command.name}`);
};
const handleEdit = (command: Command) => {
ElMessage.info(`编辑指令: ${command.name}`);
};
const handleDelete = (command: Command) => {
ElMessage.warning(`删除指令: ${command.name}`);
};
</script>
<style scoped lang="scss">
.command-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 16px;
}
.command-card {
background: #ffffff;
border-radius: 12px;
padding: 20px;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
gap: 16px;
align-items: flex-start;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
border-color: #3b82f6;
background: linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%);
}
}
.command-icon {
flex-shrink: 0;
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border-radius: 12px;
}
.command-content {
flex: 1;
min-width: 0;
}
.command-name {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px 0;
}
.command-desc {
font-size: 13px;
color: #6b7280;
line-height: 1.5;
margin: 0 0 12px 0;
}
.command-trigger {
display: inline-block;
}
.command-actions {
display: flex;
flex-direction: column;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,171 @@
<template>
<div class="diary-list">
<div v-for="diary in diaryList" :key="diary.id" class="diary-card" @click="handleDiaryClick(diary)">
<div class="diary-header">
<div class="diary-date">
<el-icon><Calendar /></el-icon>
{{ diary.date }}
</div>
<el-tag :type="diary.mood === 'happy' ? 'success' : diary.mood === 'sad' ? 'danger' : 'warning'" size="small">
{{ getMoodText(diary.mood) }}
</el-tag>
</div>
<h3 class="diary-title">{{ diary.title }}</h3>
<p class="diary-content">{{ diary.content }}</p>
<div class="diary-footer">
<div class="diary-tags">
<el-tag v-for="tag in diary.tags" :key="tag" size="small" effect="plain">{{ tag }}</el-tag>
</div>
<div class="diary-actions">
<el-button text size="small" @click.stop="handleEdit(diary)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button text size="small" type="danger" @click.stop="handleDelete(diary)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Calendar, Edit, Delete } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
interface Diary {
id: number;
title: string;
content: string;
date: string;
mood: 'happy' | 'sad' | 'normal';
tags: string[];
}
const diaryList = ref<Diary[]>([
{
id: 1,
title: '今天学习了 Vue 3 新特性',
content: '今天深入学习了 Vue 3 的 Composition API感觉比 Options API 更加灵活和强大。特别是 setup 函数和响应式系统的改进,让代码组织更加清晰...',
date: '2026-05-26',
mood: 'happy',
tags: ['学习', 'Vue3', '前端'],
},
{
id: 2,
title: '项目进度顺利',
content: '今天完成了首页重构的基础布局,整体效果不错。明天继续完善功能模块和样式细节...',
date: '2026-05-25',
mood: 'happy',
tags: ['工作', '项目'],
},
{
id: 3,
title: '遇到了一些技术难题',
content: '在处理复杂组件通信时遇到了一些问题,花了不少时间调试。最后通过重新设计数据流解决了...',
date: '2026-05-24',
mood: 'normal',
tags: ['技术', '调试'],
},
]);
const getMoodText = (mood: string) => {
const moodMap: Record<string, string> = {
happy: '😊 开心',
sad: '😢 难过',
normal: '😐 平静',
};
return moodMap[mood] || '平静';
};
const handleDiaryClick = (diary: Diary) => {
ElMessage.info(`查看日记: ${diary.title}`);
};
const handleEdit = (diary: Diary) => {
ElMessage.info(`编辑日记: ${diary.title}`);
};
const handleDelete = (diary: Diary) => {
ElMessage.warning(`删除日记: ${diary.title}`);
};
</script>
<style scoped lang="scss">
.diary-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.diary-card {
background: #ffffff;
border-radius: 12px;
padding: 20px;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
border-color: #3b82f6;
}
}
.diary-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.diary-date {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #6b7280;
}
.diary-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin: 0 0 12px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.diary-content {
font-size: 14px;
color: #6b7280;
line-height: 1.6;
margin: 0 0 16px 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.diary-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f3f4f6;
}
.diary-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.diary-actions {
display: flex;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div class="file-list">
<div v-for="file in fileList" :key="file.id" class="file-card" @click="handleFileClick(file)">
<div class="file-icon">
<el-icon :size="40" :color="getFileColor(file.type)">
<component :is="getFileIcon(file.type)" />
</el-icon>
</div>
<div class="file-info">
<h3 class="file-name">{{ file.name }}</h3>
<div class="file-meta">
<span class="file-size">{{ file.size }}</span>
<span class="file-date">{{ file.date }}</span>
</div>
<div class="file-tags">
<el-tag v-for="tag in file.tags" :key="tag" size="small" effect="plain">{{ tag }}</el-tag>
</div>
</div>
<div class="file-actions">
<el-button text size="small" @click.stop="handleDownload(file)">
<el-icon><Download /></el-icon>
</el-button>
<el-button text size="small" type="danger" @click.stop="handleDelete(file)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Document, Picture, VideoPlay, Folder, Download, Delete } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
interface FileItem {
id: number;
name: string;
type: 'document' | 'image' | 'video' | 'folder';
size: string;
date: string;
tags: string[];
}
const fileList = ref<FileItem[]>([
{
id: 1,
name: 'Vue3 学习笔记.pdf',
type: 'document',
size: '2.5 MB',
date: '2026-05-26',
tags: ['学习', '文档'],
},
{
id: 2,
name: '项目截图.png',
type: 'image',
size: '856 KB',
date: '2026-05-25',
tags: ['截图', '项目'],
},
{
id: 3,
name: '演示视频.mp4',
type: 'video',
size: '15.3 MB',
date: '2026-05-24',
tags: ['视频', '演示'],
},
{
id: 4,
name: '项目文档',
type: 'folder',
size: '12 个文件',
date: '2026-05-23',
tags: ['文件夹'],
},
{
id: 5,
name: 'API 接口文档.docx',
type: 'document',
size: '1.2 MB',
date: '2026-05-22',
tags: ['文档', 'API'],
},
{
id: 6,
name: 'UI 设计稿.sketch',
type: 'image',
size: '4.8 MB',
date: '2026-05-21',
tags: ['设计', 'UI'],
},
]);
const getFileIcon = (type: string) => {
const iconMap: Record<string, any> = {
document: Document,
image: Picture,
video: VideoPlay,
folder: Folder,
};
return iconMap[type] || Document;
};
const getFileColor = (type: string) => {
const colorMap: Record<string, string> = {
document: '#3b82f6',
image: '#10b981',
video: '#f59e0b',
folder: '#8b5cf6',
};
return colorMap[type] || '#6b7280';
};
const handleFileClick = (file: FileItem) => {
ElMessage.info(`打开文件: ${file.name}`);
};
const handleDownload = (file: FileItem) => {
ElMessage.success(`下载文件: ${file.name}`);
};
const handleDelete = (file: FileItem) => {
ElMessage.warning(`删除文件: ${file.name}`);
};
</script>
<style scoped lang="scss">
.file-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
.file-card {
background: #ffffff;
border-radius: 12px;
padding: 20px;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
gap: 16px;
align-items: flex-start;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
border-color: #3b82f6;
}
}
.file-icon {
flex-shrink: 0;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
border-radius: 8px;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-size: 15px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-meta {
display: flex;
gap: 12px;
font-size: 13px;
color: #6b7280;
margin-bottom: 8px;
}
.file-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.file-actions {
display: flex;
flex-direction: column;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,216 @@
<template>
<div class="input-shell">
<div class="input-card">
<el-input
v-model="message"
type="textarea"
:rows="1"
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder="多说说你的偏好和要求,我会越用越懂你"
class="message-input"
@keydown.enter.exact="handleSend"
/>
<div class="input-toolbar">
<div class="toolbar-left">
<el-button circle class="tool-btn" @click="handleAttachment">
<el-icon><Plus /></el-icon>
</el-button>
<el-dropdown trigger="click" @command="handleCommandSelect">
<el-button class="pill-btn">快捷指令</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="rewrite">帮我润色这段文案</el-dropdown-item>
<el-dropdown-item command="summary">总结当前对话重点</el-dropdown-item>
<el-dropdown-item command="todo">整理下一步 TODO</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click" @command="handleReplySelect">
<el-button class="pill-btn">快捷回复</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="ok">收到我马上处理</el-dropdown-item>
<el-dropdown-item command="confirm">这个方向可以继续</el-dropdown-item>
<el-dropdown-item command="adjust">这个比例还要再调一下</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="toolbar-right">
<el-button class="mode-btn">启动</el-button>
<el-button circle class="send-btn" :disabled="!message.trim()" @click="handleSend">
<el-icon><Top /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Top, Plus } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
interface Emits {
(e: 'send', message: string): void;
}
const emit = defineEmits<Emits>();
const message = ref('');
const quickCommands: Record<string, string> = {
rewrite: '请帮我润色下面这段内容,保持原意并提升表达质感:',
summary: '请总结我们当前对话的重点,并给出 3 条执行建议。',
todo: '请把当前需求拆解成可执行的 TODO 列表。',
};
const quickReplies: Record<string, string> = {
ok: '收到,我马上处理。',
confirm: '这个方向可以,继续。',
adjust: '这个比例还要再调一下。',
};
const handleSend = (event?: KeyboardEvent) => {
if (event) event.preventDefault();
const msg = message.value.trim();
if (!msg) return;
emit('send', msg);
message.value = '';
};
const handleAttachment = () => {
ElMessage.info('附件上传功能开发中...');
};
const handleCommandSelect = (key: string | number | object) => {
const k = String(key);
message.value = quickCommands[k] || message.value;
};
const handleReplySelect = (key: string | number | object) => {
const k = String(key);
message.value = quickReplies[k] || message.value;
};
</script>
<style scoped lang="scss">
.input-shell {
position: relative;
z-index: 12;
margin-top: -20px;
padding: 0 0 50px;
background: transparent;
}
.input-card {
width: min(1060px, 82%);
margin: 0 auto;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.97) 0%, rgba(255, 255, 255, 0.92) 100%),
radial-gradient(120% 180% at 0% 0%, rgba(59, 130, 246, 0.12) 0%, rgba(59, 130, 246, 0) 38%);
border: 1px solid rgba(191, 219, 254, 0.62);
border-radius: 18px;
box-shadow:
0 20px 44px rgba(15, 23, 42, 0.14),
0 0 0 1px rgba(59, 130, 246, 0.09) inset,
0 10px 24px rgba(37, 99, 235, 0.16);
padding: 14px 16px 12px;
backdrop-filter: blur(14px);
}
.message-input {
:deep(.el-textarea__inner) {
border: none;
box-shadow: none;
resize: none;
padding: 8px 6px 14px;
font-size: 15px;
line-height: 1.65;
color: #0f172a;
background: transparent;
min-height: 50px;
font-weight: 500;
&::placeholder {
color: #90a1b7;
font-weight: 400;
}
}
}
.input-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding-top: 2px;
}
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 10px;
}
.tool-btn {
width: 30px;
height: 30px;
border: 1px solid #d9e4f5;
color: #5b6b84;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.08);
&:hover {
color: #2b4d8f;
background: #f3f8ff;
}
}
.pill-btn {
height: 30px;
padding: 0 12px;
border-radius: 999px;
border: 1px solid #dbe7f7;
color: #30435f;
background: linear-gradient(180deg, #ffffff 0%, #f6f9ff 100%);
font-size: 12px;
font-weight: 600;
}
.mode-btn {
height: 30px;
padding: 0 13px;
border-radius: 999px;
border: 1px solid #dbe7f7;
color: #30435f;
background: linear-gradient(180deg, #ffffff 0%, #f6f9ff 100%);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.2px;
}
.send-btn {
width: 30px;
height: 30px;
border: none;
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 55%, #2563eb 100%);
color: #fff;
box-shadow: 0 6px 14px rgba(59, 130, 246, 0.4);
&:hover:not(:disabled) {
filter: brightness(1.06);
transform: translateY(-1px);
}
&:disabled {
opacity: 0.55;
box-shadow: none;
background: #cbd5e1;
}
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<div class="main-content">
<div v-if="activeMenu !== 'chat'" class="content-header">
<h2 class="content-title">{{ currentTitle }}</h2>
<div class="content-actions">
<el-button v-if="showAddButton" type="primary" size="small" @click="handleAdd">
<el-icon><Plus /></el-icon>
新建
</el-button>
</div>
</div>
<div class="content-body">
<ChatList v-if="activeMenu === 'chat'" />
<DiaryList v-else-if="activeMenu === 'diary'" />
<FileList v-else-if="activeMenu === 'files'" />
<CommandList v-else-if="activeMenu === 'commands'" />
<ReplyList v-else-if="activeMenu === 'replies'" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Plus } from '@element-plus/icons-vue';
import ChatList from './ChatList.vue';
import DiaryList from './DiaryList.vue';
import FileList from './FileList.vue';
import CommandList from './CommandList.vue';
import ReplyList from './ReplyList.vue';
interface Props {
activeMenu: string;
}
const props = defineProps<Props>();
const menuTitles: Record<string, string> = {
chat: '对话',
diary: '我的日记',
files: '文件管理',
commands: '快捷指令',
replies: '快捷回复',
};
const currentTitle = computed(() => menuTitles[props.activeMenu] || '首页');
const showAddButton = computed(() => ['diary', 'files', 'commands', 'replies'].includes(props.activeMenu));
const handleAdd = () => {
console.log('新建:', props.activeMenu);
};
</script>
<style scoped lang="scss">
.main-content {
flex: 1;
display: flex;
flex-direction: column;
background: #ffffff;
overflow: hidden;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
background: #ffffff;
}
.content-title {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.content-actions {
display: flex;
gap: 12px;
}
.content-body {
flex: 1;
overflow-y: auto;
background:
radial-gradient(1200px 420px at 58% -120px, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0) 65%),
radial-gradient(900px 320px at 20% 120%, rgba(99, 102, 241, 0.08) 0%, rgba(99, 102, 241, 0) 68%),
linear-gradient(180deg, #f8fbff 0%, #f5f7fb 100%);
/* 隐藏滚动条但保持滚动功能 */
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
// 聊天界面的特殊样式
&:has(.chat-list) {
padding: 0 8% 130px;
display: block;
}
// 其他列表的样式
&:not(:has(.chat-list)) {
padding: 20px 24px;
background: #f9fafb;
}
}
</style>

View File

@@ -0,0 +1,207 @@
<template>
<div class="reply-list">
<div v-for="reply in replyList" :key="reply.id" class="reply-card" @click="handleReplyClick(reply)">
<div class="reply-header">
<div class="reply-category">
<el-tag :type="getCategoryType(reply.category)" size="small">{{ reply.category }}</el-tag>
</div>
<div class="reply-usage">
<el-icon><ChatDotRound /></el-icon>
<span>{{ reply.usageCount }} 次使用</span>
</div>
</div>
<h3 class="reply-title">{{ reply.title }}</h3>
<p class="reply-content">{{ reply.content }}</p>
<div class="reply-footer">
<div class="reply-tags">
<el-tag v-for="tag in reply.tags" :key="tag" size="small" effect="plain">{{ tag }}</el-tag>
</div>
<div class="reply-actions">
<el-button text size="small" @click.stop="handleCopy(reply)">
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button text size="small" @click.stop="handleEdit(reply)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button text size="small" type="danger" @click.stop="handleDelete(reply)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ChatDotRound, CopyDocument, Edit, Delete } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
interface Reply {
id: number;
title: string;
content: string;
category: string;
tags: string[];
usageCount: number;
}
const replyList = ref<Reply[]>([
{
id: 1,
title: '欢迎语',
content: '您好!很高兴为您服务。请问有什么可以帮助您的吗?',
category: '问候',
tags: ['欢迎', '问候'],
usageCount: 156,
},
{
id: 2,
title: '感谢回复',
content: '非常感谢您的反馈!我们会继续努力为您提供更好的服务。',
category: '感谢',
tags: ['感谢', '反馈'],
usageCount: 89,
},
{
id: 3,
title: '技术支持',
content: '我已经收到您的问题,正在为您查询相关信息。请稍等片刻,我会尽快回复您。',
category: '支持',
tags: ['技术', '支持'],
usageCount: 234,
},
{
id: 4,
title: '道歉回复',
content: '非常抱歉给您带来不便。我们会立即处理这个问题,并尽快给您一个满意的答复。',
category: '道歉',
tags: ['道歉', '问题'],
usageCount: 45,
},
{
id: 5,
title: '结束语',
content: '如果您还有其他问题,随时欢迎联系我们。祝您生活愉快!',
category: '结束',
tags: ['结束', '祝福'],
usageCount: 178,
},
{
id: 6,
title: '产品介绍',
content: '我们的产品采用最新技术,为您提供高效、稳定、安全的服务体验。',
category: '介绍',
tags: ['产品', '介绍'],
usageCount: 67,
},
]);
const getCategoryType = (category: string) => {
const typeMap: Record<string, any> = {
问候: 'success',
感谢: 'primary',
支持: 'warning',
道歉: 'danger',
结束: 'info',
介绍: '',
};
return typeMap[category] || '';
};
const handleReplyClick = (reply: Reply) => {
ElMessage.success(`使用快捷回复: ${reply.title}`);
};
const handleCopy = (reply: Reply) => {
navigator.clipboard.writeText(reply.content);
ElMessage.success('已复制到剪贴板');
};
const handleEdit = (reply: Reply) => {
ElMessage.info(`编辑回复: ${reply.title}`);
};
const handleDelete = (reply: Reply) => {
ElMessage.warning(`删除回复: ${reply.title}`);
};
</script>
<style scoped lang="scss">
.reply-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.reply-card {
background: #ffffff;
border-radius: 12px;
padding: 20px;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
border-color: #3b82f6;
}
}
.reply-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.reply-category {
flex-shrink: 0;
}
.reply-usage {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #6b7280;
}
.reply-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin: 0 0 12px 0;
}
.reply-content {
font-size: 14px;
color: #6b7280;
line-height: 1.6;
margin: 0 0 16px 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.reply-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f3f4f6;
}
.reply-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.reply-actions {
display: flex;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<div class="sidebar">
<div class="sidebar-header">
<div class="brand-dot"></div>
<div class="user-info">
<el-avatar :size="42" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
<div class="user-details">
<div class="user-name">助手</div>
<div class="user-status">在线</div>
</div>
</div>
<el-button class="new-chat-btn" @click="handleNewChat">
<el-icon><Plus /></el-icon>
新增对话
</el-button>
</div>
<div class="sidebar-menu">
<div v-for="item in menuItems" :key="item.key" class="menu-item" :class="{ active: activeMenu === item.key }" @click="handleMenuClick(item)">
<el-icon :size="18" class="menu-icon">
<component :is="item.icon" />
</el-icon>
<span class="menu-label">{{ item.label }}</span>
<el-badge v-if="item.badge" :value="item.badge" class="menu-badge" />
</div>
</div>
<div class="sidebar-footer">
<el-button text class="footer-btn" @click="handleSettings">
<el-icon><Setting /></el-icon>
设置
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { Document, Folder, ChatDotRound, MagicStick, Cpu, VideoPlay, Setting, Plus } from '@element-plus/icons-vue';
import { useRouter } from 'vue-router';
interface MenuItem {
key: string;
label: string;
icon: any;
badge?: number;
route?: string;
}
interface Props {
activeMenu: string;
}
interface Emits {
(e: 'menu-change', key: string): void;
(e: 'new-chat'): void;
}
defineProps<Props>();
const emit = defineEmits<Emits>();
const router = useRouter();
const menuItems: MenuItem[] = [
{ key: 'chat', label: '对话', icon: ChatDotRound },
{ key: 'diary', label: '日记', icon: Document, badge: 3 },
{ key: 'files', label: '文件', icon: Folder },
{ key: 'skills', label: '技能管理', icon: MagicStick, route: '/settings/skill' },
{ key: 'models', label: '模型管理', icon: Cpu, route: '/settings/modelConfig/modelModule' },
{ key: 'creation', label: '内容创作', icon: VideoPlay, route: '/settings/creation' },
];
const handleMenuClick = (item: MenuItem) => {
if (item.route) {
router.push(item.route);
} else {
emit('menu-change', item.key);
}
};
const handleNewChat = () => {
emit('new-chat');
};
const handleSettings = () => {
router.push('/personal');
};
</script>
<style scoped lang="scss">
.sidebar {
width: 252px;
background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%);
border-right: 1px solid #e7edf7;
display: flex;
flex-direction: column;
height: 100%;
box-shadow: 6px 0 20px rgba(15, 23, 42, 0.04);
}
.sidebar-header {
padding: 18px 16px 14px;
border-bottom: 1px solid #e9eef7;
position: relative;
}
.new-chat-btn {
margin-top: 10px;
width: 100%;
height: 34px;
border-radius: 10px;
border: 1px solid #dbe7f7;
background: linear-gradient(135deg, #f8fbff 0%, #eff6ff 100%);
color: #1f4db8;
font-weight: 600;
letter-spacing: 0.2px;
&:hover {
color: #1e40af;
border-color: #bfdbfe;
background: linear-gradient(135deg, #eff6ff 0%, #e0eeff 100%);
}
}
.brand-dot {
position: absolute;
right: 16px;
top: 18px;
width: 8px;
height: 8px;
border-radius: 999px;
background: linear-gradient(135deg, #60a5fa 0%, #2563eb 100%);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.14);
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 8px 6px;
border-radius: 12px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.86) 0%, rgba(248, 251, 255, 0.92) 100%);
border: 1px solid #e7edf7;
}
.user-details {
flex: 1;
}
.user-name {
font-size: 15px;
font-weight: 700;
color: #111827;
margin-bottom: 4px;
letter-spacing: 0.2px;
}
.user-status {
font-size: 12px;
color: #10b981;
font-weight: 500;
}
.sidebar-menu {
flex: 1;
padding: 12px 10px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
.menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 12px;
margin-bottom: 6px;
border-radius: 11px;
cursor: pointer;
transition: all 0.22s ease;
color: #64748b;
position: relative;
border: 1px solid transparent;
&:hover {
background: #f3f7ff;
color: #1f2937;
border-color: #e3ecfb;
}
&.active {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.16) 0%, rgba(37, 99, 235, 0.12) 100%);
color: #1f4db8;
font-weight: 600;
border-color: rgba(59, 130, 246, 0.2);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
&::before {
content: '';
position: absolute;
left: -2px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 18px;
background: linear-gradient(180deg, #60a5fa 0%, #2563eb 100%);
border-radius: 0 4px 4px 0;
}
}
}
.menu-icon {
opacity: 0.92;
}
.menu-label {
flex: 1;
font-size: 13px;
}
.menu-badge {
:deep(.el-badge__content) {
background: #ef4444;
border: none;
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.25);
}
}
.sidebar-footer {
padding: 10px;
border-top: 1px solid #e9eef7;
}
.footer-btn {
width: 100%;
justify-content: flex-start;
gap: 8px;
color: #64748b;
font-size: 13px;
border-radius: 10px;
padding: 10px 12px;
&:hover {
background: #f4f7fd;
color: #1f2937;
}
}
</style>

File diff suppressed because one or more lines are too long