Tcode平台改造升级-AI助手菜单显示功能修改

This commit is contained in:
fmk1023
2025-12-03 09:53:16 +08:00
parent e01a050f80
commit 86d26f161a
3 changed files with 923 additions and 1 deletions

View File

@@ -0,0 +1,324 @@
<template>
<Card class="ViewHistoryAside">
<!-- 1. 新对话按钮 -->
<div class="new-conv-wrap">
<d-button
variant="solid"
class="new-conv-btn"
@click="handleNewConversation"
>
<template #icon>
<d-icon name="icon-plus" />
</template>
新对话
<span class="shortcut">Ctrl+N</span>
</d-button>
</div>
<!-- 2. 功能入口区域 -->
<div class="function-list">
<div class="function-item" @click="handleFunction('write')">
<d-icon name="icon-pencil" />
<span>帮我写</span>
</div>
<div class="function-item" @click="handleFunction('create')">
<d-icon name="icon-magic" />
<span>AI 创作</span>
</div>
<div class="function-item" @click="handleFunction('app')">
<d-icon name="icon-app" />
<span>应用生成</span>
</div>
<div class="function-item" @click="handleFunction('cloud')">
<d-icon name="icon-cloud" />
<span>云盘</span>
</div>
<div class="function-item more-item" @click="toggleMoreFunc">
<span>更多</span>
<d-icon name="icon-arrow-right" />
</div>
</div>
<!-- 3. 历史对话标题 -->
<div class="history-title">历史对话</div>
<!-- 4. 历史对话列表 -->
<div class="history-conv-list">
<!-- 列表项循环渲染 -->
<div
v-if="dhlsList.length>0"
v-for="(item, index) in dhlsList"
:key="item.id"
class="conv-item"
@click="selectConversation(item)"
@mouseenter="hoverIndex = index"
@mouseleave="hoverIndex = null"
>
<!-- 对话标题 + 星标收藏标识 -->
<div class="conv-title">
<span>{{ item.question }}</span>
<d-icon
v-if="item.isStarred"
name="icon-star"
class="star-icon"
@click.stop="toggleStar(item)"
/>
</div>
<!-- 未读小红点 -->
<d-badge
v-if="item.unreadCount > 0"
:content="item.unreadCount"
class="unread-badge"
/>
<!-- 更多操作按钮hover显示 -->
<d-icon
v-show="hoverIndex === index"
name="icon-more-operate"
class="more-icon"
@click.stop="handleMore(item)"
/>
</div>
<!-- 无数据占位 -->
<NoData v-else :small="false"></NoData>
</div>
</Card>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { Message } from 'vue-devui/message';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import axios from 'axios';
// ========== 状态与数据 ==========
const hoverIndex = ref(null); // 鼠标悬浮的列表项索引
const { formatTime } = useTimeFormat();
const VITE_AI_API_HOST = import.meta.env.VITE_AI_API_HOST;
// 历史对话列表(新增 isStarred星标收藏unreadCount未读小红点
const dhlsList = ref([
{
question: "解决菜单功能异常",
id: 1,
isStarred: true,
unreadCount: 0
},
{
question: "项目界面与前端实现",
id: 2,
isStarred: true,
unreadCount: 0
},
{
question: "添加拓扑折叠置顶",
id: 3,
isStarred: false,
unreadCount: 0
},
{
question: "Vue3 代码片段展示组件",
id: 4,
isStarred: false,
unreadCount: 0
},
{
question: "Spring Boot 项目文件结构解释",
id: 5,
isStarred: false,
unreadCount: 0
},
{
question: "调用接口获取文件信息",
id: 6,
isStarred: false,
unreadCount: 1
},
{
question: "Nuxt 项目创建及报错处理",
id: 7,
isStarred: false,
unreadCount: 0
},
{
question: "手机版对话",
id: 8,
isStarred: false,
unreadCount: 0
}
]);
// ========== 方法 ==========
// 新对话按钮点击
const handleNewConversation = () => {
console.log("创建新对话");
// 可添加:清空当前对话、初始化新会话等逻辑
};
// 功能入口点击
const handleFunction = (type) => {
console.log("点击功能:", type);
// 可添加:跳转到对应功能页的逻辑
};
// 展开/收起“更多”功能
const toggleMoreFunc = () => {
console.log("切换更多功能展开状态");
// 可添加:下拉菜单展开逻辑
};
// 选择对话
const selectConversation = (item) => {
console.log("选中对话:", item);
// 可添加:加载对应对话内容的逻辑
};
// 切换星标收藏
const toggleStar = (item) => {
item.isStarred = !item.isStarred;
Message.success(item.isStarred ? "已收藏" : "已取消收藏");
};
// 更多操作
const handleMore = (item) => {
console.log("更多操作:", item);
// 可添加:编辑/删除/分享等逻辑
};
// 接口获取历史对话(替换模拟数据)
const getQuestionHistory = async () => {
try {
const { data } = await axios.get(`${VITE_AI_API_HOST}/api/conversationList?userId=?`);
if (data.questions) {
// 给接口返回的数据补充 isStarred/unreadCount 字段
dhlsList.value = data.questions.map(item => ({
...item,
isStarred: item.isCollected || false, // 假设接口返回 isCollected 表示收藏
unreadCount: item.unreadNum || 0 // 假设接口返回 unreadNum 表示未读
}));
}
} catch (err) {
Message.warning('获取历史对话失败!');
}
};
// 组件挂载时加载数据
onMounted(() => {
getQuestionHistory();
});
</script>
<style lang="scss" scoped>
.ViewHistoryAside {
width: 18vw;
position: fixed;
top: 64px;
left: 20px;
height: 85vh;
padding: 12px 16px;
// 新对话按钮样式
.new-conv-wrap {
margin-bottom: 16px;
.new-conv-btn {
width: 100%;
background-color: #e6f7ff;
color: #1890ff;
justify-content: flex-start;
.shortcut {
margin-left: auto;
font-size: 12px;
color: #888;
}
}
}
// 功能入口列表样式
.function-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
.function-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
.more-item {
justify-content: space-between;
}
}
}
// 历史对话标题样式
.history-title {
font-size: 14px;
color: #888;
margin-bottom: 12px;
padding-left: 4px;
}
// 历史对话列表样式
.history-conv-list {
height: calc(100% - 180px); // 自适应剩余高度
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #ddd transparent;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: #ddd;
border-radius: 2px;
}
// 列表项样式
.conv-item {
display: flex;
align-items: center;
padding: 8px 4px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 4px;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
// 对话标题+星标
.conv-title {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.star-icon {
color: #faad14;
}
}
// 未读小红点
.unread-badge {
margin-right: 8px;
}
// 更多操作按钮
.more-icon {
opacity: 0.6;
&:hover {
opacity: 1;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,596 @@
<template>
<div class="chat-sidebar">
<div class="top-section">
<div class="new-chat-btn" @click="handleNewChat">
<div class="btn-content">
<d-icon name="icon-edit" size="16px"></d-icon>
<span class="btn-text">新对话</span>
</div>
</div>
</div>
<div class="scroll-container">
<div class="menu-section">
<div
v-for="menu in menuList"
:key="menu.id"
class="menu-item"
@click="handleMenuClick(menu)"
>
<d-icon :name="menu.icon" size="16px" class="menu-icon"></d-icon>
<span class="menu-text">{{ menu.title }}</span>
</div>
</div>
<div class="divider"></div>
<div class="search-section">
<d-search icon-position="left" style="width: 100%" placeholder="搜索历史对话" v-model="searchText" @change="loadData" @search="loadData"></d-search>
</div>
<div class="history-section">
<div class="section-title">历史对话</div>
<div v-if="dhlsList.length > 0">
<div
v-for="(item, index) in dhlsList"
:key="item.conversationId"
:class="['history-item', { 'active': props.CONV_ID === item.conversationId }]"
@click="handleSelect(item)"
@mouseenter="hoverIndex = index"
@mouseleave="hoverIndex = null"
>
<template v-if="item.isEditing">
<d-input
v-model="item.tempTitle"
autofocus
class="edit-input"
@keypress.enter="confirmEdit(item)"
@blur="cancelEdit(item)"
/>
<div class="edit-actions">
<d-icon name="icon-right" class="action-icon success" @click.stop="confirmEdit(item)"/>
<d-icon name="icon-error" class="action-icon danger" @click.stop="cancelEdit(item)"/>
</div>
</template>
<template v-else>
<d-icon name="icon-message" size="16px" class="item-icon"></d-icon>
<div class="item-content" @dblclick="startEdit(item)">
<span class="item-title text-ellipsis">{{ item.title || '新对话' }}</span>
</div>
<div class="item-actions">
<d-icon v-if="item.is_collected" name="icon-star-o" size="14px" class="pin-icon" style="color: #f7ba2a;"></d-icon>
<d-icon v-if="item.isPinned" name="icon-pin-fill" size="14px" class="pin-icon"></d-icon>
<div class="hover-actions" v-show="hoverIndex === index">
<d-dropdown trigger="click" :close-on-click="true" align="start">
<d-icon name="icon-more-operate" size="16px" class="more-btn"></d-icon>
<template #menu>
<ul class="dropdown-menu">
<li @click.stop="handlePin(item)">
{{ item.isPinned ? '取消置顶' : '置顶' }}
</li>
<li @click.stop="toggleCollect(item)">
{{ item.is_collected ? '取消收藏' : '收藏' }}
</li>
<li @click.stop="startEdit(item)">重命名</li>
<li @click.stop="handleDelete([item.conversationId])" class="danger">删除</li>
</ul>
</template>
</d-dropdown>
</div>
</div>
</template>
</div>
</div>
<div v-else class="empty-state">
暂无历史对话
</div>
</div>
</div>
<div class="bottom-section">
<div class="bottom-controls">
<div class="limit-select">
<span>保存条数</span>
<d-select
v-model="historyLimit"
size="sm"
class="mini-select"
@value-change="handleHistoryLimitChange"
>
<d-option value="10">10</d-option>
<d-option value="20">20</d-option>
<d-option value="30">30</d-option>
<d-option value="0">不限</d-option>
</d-select>
</div>
<div class="batch-btn" @click="openBatchManageModal">
<d-icon name="icon-property-setting"></d-icon>
<span>批量管理</span>
</div>
</div>
<BatchManageModal :username="props.username" @loadData="loadData" ref="BatchManageModalRef"/>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, defineEmits, defineExpose, defineProps } from 'vue';
import BatchManageModal from './BatchManageModal.vue';
import { Message } from 'vue-devui/message';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import {
fetchChangeConversationTitle,
fetchConversationList,
fetchDeleteConversation,
fetchToggleConversationCollect
} from '@/api/jyh';
// --- Props & Emits ---
const props = defineProps(['username', 'CONV_ID']);
const emit = defineEmits(['select-conv','onNewConvo']);
// --- State ---
const searchText = ref('');
const historyLimit = ref('30');
const hoverIndex = ref(null);
const BatchManageModalRef = ref(null);
const dhlsList = ref([]); // 历史对话列表
const page = ref({
pageNum: 1,
size: 30 // 默认 30与 historyLimit 对应
});
// 静态菜单数据
const menuList = [
{ id: 'write', title: 'SBOM分析', icon: 'icon-accelerations' },
{ id: 'create', title: '许可证合规检测', icon: 'icon-add-example' },
{ id: 'app', title: '供应链安全检测', icon: 'icon-branch-merge-o' },
{ id: 'cloud', title: '代码安全审计', icon: 'icon-add-token' },
{ id: 'more', title: '漏洞智能修复', icon: 'icon-assembly-new' },
];
const { formatTime } = useTimeFormat();
// --- Methods ---
// 1. 加载数据
const loadData = async () => {
await getQuestionHistory();
};
const getQuestionHistory = async () => {
if (props.username) {
const { data } = await fetchConversationList({
"userId": props.username,
"list_type": "history", // 或者根据需求调整,例如同时获取收藏
"is_manage": false,
"page": page.value.pageNum,
"size": page.value.size,
"search": searchText.value
});
if (data.code === 200) {
// 处理数据,补充前端状态字段
dhlsList.value = data.data.map(item => ({
...item,
isEditing: false,
tempTitle: '',
isPinned: item.isPinned || false // 如果后端有置顶字段则使用,否则默认 false
}));
} else {
// Message.error(data.msg || '接口异常!');
}
}
};
// 2. 新建对话
const handleNewChat = () => {
emit('select-conv', null);
};
// 3. 选中对话
const handleSelect = (item) => {
if (item.isEditing) return; // 编辑中不触发选中
emit('select-conv', item.conversationId);
};
// 4. 菜单点击
const handleMenuClick = (menu) => {
console.log('点击菜单:', menu.title);
// 路由跳转或功能触发逻辑
};
// 5. 编辑/重命名逻辑
const startEdit = (item) => {
item.isEditing = true;
item.tempTitle = item.title;
};
const confirmEdit = async (item) => {
if (!item.tempTitle.trim()) {
Message.error('标题不能为空');
return;
}
try {
const res = await fetchChangeConversationTitle({
conversationId: item.conversationId,
newTitle: item.tempTitle,
userId: props.username,
});
if (res.data.code === 200 || res.data.code === 20001 || res.data.code === 20000) {
item.title = item.tempTitle;
item.isEditing = false;
Message.success('标题修改成功');
} else {
Message.error(res.data.msg || '修改失败');
}
} catch (error) {
console.error('编辑失败:', error);
item.tempTitle = item.title;
item.isEditing = false;
}
};
const cancelEdit = (item) => {
item.isEditing = false;
item.tempTitle = item.title;
};
// 6. 收藏逻辑
const toggleCollect = async (item) => {
if (!props.username) {
return Message.error('请先登录');
}
try {
const res = await fetchToggleConversationCollect({
userId: props.username,
conversationIdList: [item.conversationId]
});
if (res.data.code === 200 || res.data.code === 20000) {
await loadData(); // 重新加载以更新状态
Message.success(res.data.msg || '操作成功');
} else {
Message.error(res.data.msg);
}
} catch (error) {
console.error('收藏操作失败:', error);
}
};
// 7. 删除逻辑
const handleDelete = async (conversationIds) => {
if (!props.username) {
return Message.error('请先登录');
}
try {
const res = await fetchDeleteConversation({
userId: props.username,
conversationIdList: conversationIds
}
);
if (res.data.code === 200 || res.data.code === 20000) {
Message.success(res.data.message || '删除成功');
loadData();
} else {
Message.error(res.data.msg || '删除对话失败!');
}
} catch (error) {
console.error('删除对话失败:', error);
}
};
// 8. 置顶逻辑 (前端模拟)
const handlePin = (item) => {
item.isPinned = !item.isPinned;
// 本地排序:置顶的排前面
dhlsList.value.sort((a, b) => (b.isPinned ? 1 : 0) - (a.isPinned ? 1 : 0));
// 如果有后端置顶接口,请在这里调用
};
// 9. 批量管理 & 分页条数
const openBatchManageModal = () => {
if (BatchManageModalRef.value) {
BatchManageModalRef.value.openModal();
}
};
const handleHistoryLimitChange = (val) => {
page.value.size = Number(val) || 10000; // 0 为不限
page.value.pageNum = 1;
loadData();
};
// --- Lifecycle ---
onMounted(() => {
loadData();
});
defineExpose({
getQuestionHistory,
loadData
});
</script>
<style lang="scss" scoped>
/* 容器样式:固定宽度,高度适配 */
.chat-sidebar {
width: 260px;
height: 85vh; /* 或 100% */
background-color: #f7f8fa; /* 浅灰背景 */
border-radius: 12px;
display: flex;
flex-direction: column;
padding: 12px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: #333;
position: fixed; /* 保持原来的固定定位 */
top: 64px;
left: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
/* 1. 顶部:新对话按钮 */
.top-section {
flex-shrink: 0;
margin-bottom: 12px;
}
.new-chat-btn {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #eef4ff; /* 浅蓝背景 */
color: #2563eb; /* 蓝色文字 */
border-radius: 8px;
padding: 10px 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: #dbeafe;
}
.btn-content {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
font-size: 14px;
}
}
/* 2. 中间滚动区域 */
.scroll-container {
flex: 1;
overflow-y: auto;
/* 隐藏滚动条但保持功能 */
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: #ddd;
border-radius: 2px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
/* 功能菜单 */
.menu-section {
margin-bottom: 8px;
}
.menu-item {
display: flex;
align-items: center;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
color: #4b5563;
font-size: 14px;
transition: background-color 0.2s;
&:hover {
background-color: #e5e7eb;
}
.menu-icon {
margin-right: 10px;
color: #6b7280;
}
.menu-text {
flex: 1;
}
}
/* 分割线 */
.divider {
height: 1px;
background-color: #e5e7eb;
margin: 8px 4px;
}
/* 搜索框 */
.search-section {
padding: 0 4px 8px 4px;
}
/* 历史对话列表 */
.history-section {
.section-title {
font-size: 12px;
color: #9ca3af;
padding: 8px 4px;
}
}
.history-item {
display: flex;
align-items: center;
padding: 10px 8px;
border-radius: 8px;
cursor: pointer;
position: relative;
transition: background-color 0.2s;
height: 40px; /* 固定高度 */
&:hover {
background-color: #e5e7eb;
.item-actions .pin-icon {
display: none; /* 悬停时隐藏置顶/收藏图标,显示更多操作 */
}
}
&.active {
background-color: #e5e7eb; /* 选中态背景 */
font-weight: 500;
}
.item-icon {
margin-right: 10px;
color: #9ca3af;
flex-shrink: 0;
}
.item-content {
flex: 1;
overflow: hidden;
margin-right: 4px;
}
.item-title {
font-size: 14px;
color: #374151;
display: block;
}
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-actions {
display: flex;
align-items: center;
min-width: 20px;
justify-content: flex-end;
}
.pin-icon {
color: #9ca3af;
}
.hover-actions {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background-color: #e5e7eb;
padding-left: 4px;
.more-btn {
color: #6b7280;
&:hover { color: #374151; }
}
}
/* 编辑框样式 */
.edit-input {
height: 30px;
font-size: 13px;
width: 100%;
}
.edit-actions {
display: flex;
align-items: center;
gap: 4px;
margin-left: 4px;
.action-icon {
font-size: 14px;
cursor: pointer;
&.success { color: #50d4ab; }
&.danger { color: #f66f6a; }
}
}
}
.empty-state {
text-align: center;
color: #9ca3af;
font-size: 13px;
padding: 20px 0;
}
/* 3. 底部区域:批量管理等 */
.bottom-section {
flex-shrink: 0;
padding-top: 8px;
border-top: 1px solid #e5e7eb;
}
.bottom-controls {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #666;
padding: 0 4px;
.limit-select {
display: flex;
align-items: center;
gap: 4px;
:deep(.devui-select) {
width: 60px;
}
}
.batch-btn {
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
&:hover { color: #2563eb; }
}
}
/* 下拉菜单样式 */
.dropdown-menu {
list-style: none;
padding: 4px 0;
margin: 0;
min-width: 100px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
li {
padding: 8px 12px;
font-size: 13px;
cursor: pointer;
color: #374151;
&:hover {
background-color: #f3f4f6;
}
&.danger {
color: #ef4444;
}
}
}
</style>

View File

@@ -139,6 +139,7 @@
</div>
<ViewHistoryAside ref="viewHistoryRef" :CONV_ID="CONV_ID" :username="username" v-show="isHistoryVisible" @select-conv="handleSelectConv"/>
<!-- <ViewHistoryAsideNew ref="viewHistoryRef" :CONV_ID="CONV_ID" :username="username" v-show="isHistoryVisible" @onNewConvo="onNewConvo" @select-conv="handleSelectConv"/>-->
</div>
</template>
@@ -153,6 +154,7 @@ import OssDetail from './components/OssDetail.vue';
import Other from './components/Other.vue';
import AIList from './components/AIList.vue';
import ViewHistoryAside from './components/ViewHistoryAside/ViewHistoryAside.vue';
import ViewHistoryAsideNew from './components/ViewHistoryAside/ViewHistoryAsideNew.vue';
import axios from 'axios';
import { useDiscussGetUserInfo } from '@/api/discussion/hook';
import { Message } from 'vue-devui/message';
@@ -196,7 +198,7 @@ const messages = ref([]);
const followupQuestions = ref([]);
// 控制 ViewHistoryAside 组件的显示状态
const isHistoryVisible = ref(false);
const isHistoryVisible = ref(true);
// 切换 ViewHistoryAside 组件的显示与隐藏