<script lang="ts" setup>
|
import { nextTick, onMounted, reactive, ref, toRefs } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { ScrollContainer } from '@jnpf/ui';
|
import { formatToDate } from '@jnpf/utils';
|
|
import { CopyOutlined, LoadingOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons-vue';
|
import { useClipboard } from '@vueuse/core';
|
|
import { deleteHistory, getHistoryInfoList, getHistoryList, saveHistory, sendChat } from '#/api/system/aiChat';
|
|
defineOptions({ name: 'AiChat' });
|
|
interface State {
|
historyList: any[];
|
chatList: any[];
|
activeChatId: string;
|
messageContent: string;
|
activeChat: any;
|
currTitle: any;
|
loading: boolean;
|
listLoading: boolean;
|
isEdit: boolean;
|
}
|
|
const state = reactive<State>({
|
activeChatId: '',
|
messageContent: '',
|
chatList: [],
|
historyList: [],
|
activeChat: '',
|
currTitle: '',
|
loading: false,
|
listLoading: false,
|
isEdit: false,
|
});
|
const { activeChatId, chatList, historyList, messageContent, loading, listLoading, isEdit, currTitle } = toRefs(state);
|
const { createMessage } = useMessage();
|
const { copy } = useClipboard({ legacy: true });
|
const chatContentListRef = ref(null);
|
const titleInputRef = ref(null);
|
const quickQuestions = ['JNPF是什么', 'JNPF的性能特点', 'JNPF支持哪些数据库'];
|
|
function delChatRecord(id) {
|
deleteHistory(id).then((res) => {
|
createMessage.success(res.msg);
|
state.historyList = state.historyList.filter((o) => o.id !== id);
|
if (state.activeChatId === id) {
|
state.activeChatId = '';
|
state.chatList = [];
|
if (state.historyList.length) toggleChat(state.historyList[0]);
|
}
|
});
|
}
|
|
function toggleChat(item) {
|
if (state.activeChatId === item.id) return;
|
state.activeChatId = item.id;
|
state.activeChat = item;
|
state.isEdit = false;
|
state.chatList = [];
|
getChatHistoryInfoList();
|
}
|
function getChatHistoryList() {
|
getHistoryList().then((res) => {
|
state.historyList = res.data;
|
if (!state.historyList.length) return;
|
state.activeChatId = state.historyList[0].id;
|
state.activeChat = state.historyList[0];
|
getChatHistoryInfoList();
|
});
|
}
|
function getChatHistoryInfoList() {
|
state.listLoading = true;
|
getHistoryInfoList(state.activeChatId).then((res) => {
|
state.chatList = res.data;
|
state.listLoading = false;
|
scroll();
|
});
|
}
|
function scroll(num = 0) {
|
setTimeout(() => {
|
nextTick(() => {
|
const ele: any = chatContentListRef.value;
|
if (!ele) return;
|
// 设置滚动条到最底部
|
if (ele.scrollHeight > ele.clientHeight) ele.scrollTop = ele.scrollHeight;
|
});
|
}, num);
|
}
|
function startNewChat() {
|
if ((!state.chatList.length && state.historyList.length) || state.loading) return;
|
const newChat: any = {
|
id: '',
|
fullName: '新对话',
|
data: [],
|
};
|
saveHistory(newChat).then(() => {
|
state.historyList.unshift(newChat);
|
state.chatList = [];
|
getChatHistoryList();
|
});
|
}
|
async function sendMessage(content) {
|
if (state.loading) return;
|
if (!content) return createMessage.warning('请输入问题描述');
|
state.loading = true;
|
const msgItem = { content, type: 1 };
|
state.chatList.push(msgItem);
|
nextTick(() => {
|
scroll();
|
});
|
|
try {
|
const saveQuery = {
|
id: state.activeChatId || '',
|
fullName: state.activeChatId ? '' : content,
|
data: state.chatList,
|
};
|
await saveHistory(saveQuery);
|
|
// 如果是新对话,获取历史列表并更新当前对话
|
if (!state.activeChatId) await getChatHistoryList();
|
// 清空输入框
|
state.messageContent = '';
|
const res = await sendChat({ keyword: content });
|
|
setTimeout(async () => {
|
const aiMsg = {
|
questionText: content,
|
content: res.data,
|
type: 0,
|
};
|
state.chatList.push(aiMsg);
|
const newSaveQuery = {
|
id: state.activeChatId || '',
|
fullName: '',
|
data: state.chatList,
|
};
|
await saveHistory(newSaveQuery);
|
state.loading = false;
|
nextTick(() => {
|
scroll();
|
});
|
}, 1000);
|
} catch {
|
state.loading = false;
|
}
|
}
|
// 开始编辑标题
|
function editTitle() {
|
state.currTitle = state.activeChat.fullName;
|
state.isEdit = true;
|
nextTick(() => {
|
(titleInputRef.value as any)?.focus();
|
});
|
}
|
// 保存标题
|
function saveTitle() {
|
const fullName = state.currTitle.trim();
|
if (fullName) {
|
state.activeChat.fullName = fullName;
|
for (let i = 0; i < state.historyList.length; i++) {
|
if (state.historyList[i].id === state.activeChatId) {
|
state.historyList[i].fullName = fullName;
|
}
|
}
|
}
|
const query: any = {
|
id: state.activeChatId || '',
|
fullName,
|
};
|
saveHistory(query).then(() => {
|
state.isEdit = false;
|
});
|
}
|
function handleCopy(text) {
|
if (!text) return;
|
copy(text);
|
createMessage.success('复制成功');
|
}
|
|
onMounted(() => {
|
getChatHistoryList();
|
});
|
</script>
|
<template>
|
<div class="ai-chat-pane common-pane">
|
<div class="chat-list">
|
<div class="chat-list-header">
|
<div class="add-chat-btn" @click="startNewChat">
|
<PlusOutlined />
|
<span>开启新对话</span>
|
</div>
|
</div>
|
<div class="chat-list-main">
|
<ScrollContainer class="ai-chat-list">
|
<template v-if="historyList.length">
|
<div
|
v-for="(item, i) in historyList"
|
:key="i"
|
class="ai-chat-list-item"
|
:class="{ 'ai-chat-list-item__active': activeChatId === item.id }"
|
@click="toggleChat(item)">
|
<div class="ai-chat-list-item-main">
|
<div class="ai-chat-list-title" :title="item.fullName">{{ item.fullName }}</div>
|
<div class="ai-chat-list-time">{{ formatToDate(item.creatorTime) }}</div>
|
<a-popconfirm title="确定删除此记录?" class="drawing-item-action-item drawing-item-delete" @confirm="delChatRecord(item.id)" :z-index="1000000">
|
<i class="icon-ym icon-ym-btn-clearn" @click.stop></i>
|
</a-popconfirm>
|
</div>
|
</div>
|
</template>
|
<jnpf-empty v-if="!historyList.length" description="暂无历史对话" />
|
</ScrollContainer>
|
</div>
|
</div>
|
<div class="chat-content">
|
<div class="chatBox">
|
<div class="chat-content-header">
|
<div v-if="chatList.length">
|
<a-input
|
v-if="isEdit"
|
v-model:value="currTitle"
|
:maxlength="30"
|
@press-enter="saveTitle"
|
@blur="saveTitle"
|
ref="titleInputRef"
|
class="!text-[16px]" />
|
<div v-else @click="editTitle">
|
{{ state.activeChat?.fullName }}
|
</div>
|
</div>
|
</div>
|
<div class="chat-content-main">
|
<div class="chat-content-list" ref="chatContentListRef">
|
<template v-if="chatList.length">
|
<div class="chat-content-list-item" v-for="(item, index) in chatList" :key="index" :class="{ 'chat-content-list-item--mine': item.type === 1 }">
|
<div class="chat-content-list-text">
|
<div class="chat-content-list-actions">
|
<a-tooltip title="复制" :z-index="1000000" @click="handleCopy(item.content)">
|
<a-button type="text" class="action-btn">
|
<CopyOutlined />
|
</a-button>
|
</a-tooltip>
|
<a-tooltip title="重新生成" :z-index="1000000" @click="sendMessage(item.questionText)">
|
<a-button type="text" class="action-btn">
|
<SyncOutlined />
|
</a-button>
|
</a-tooltip>
|
</div>
|
<p v-html="item.content" class="chat-content-list__msg--text"></p>
|
</div>
|
</div>
|
<div class="chat-content-list-item" v-if="loading">
|
<div class="chat-content-list-text">
|
<LoadingOutlined :style="{ fontSize: '24px', color: '#606266' }" />
|
</div>
|
</div>
|
</template>
|
<div class="welcome-message" v-if="!chatList.length && !listLoading">
|
<div class="welcome-message-main">
|
<div class="welcome-message-title">
|
<img src="@/assets/images/chatImg/robot.png" alt="AI助手头像" class="welcome-message-icon" />
|
我是<span>大迈,JnpfAI智能助手</span>,很高兴能为您服务!
|
</div>
|
<div class="welcome-message-subtitle">我可以为您解答各种问题,请问有什么能帮助您的吗?</div>
|
</div>
|
</div>
|
</div>
|
<div class="writeBox">
|
<div class="toolBox">
|
<div class="toolBox-left">
|
<a v-for="question in quickQuestions" :key="question" class="question-link" @click="sendMessage(question)">
|
{{ question }}
|
</a>
|
</div>
|
</div>
|
<div class="writeBox-main">
|
<jnpf-textarea placeholder="请输入问题描述" v-model:value.trim="messageContent" @keyup.enter="sendMessage(messageContent)" />
|
<a-button :disabled="!messageContent" type="primary" @click="sendMessage(messageContent)" class="send-btn">
|
<i class="icon-ym icon-ym-release"></i>
|
</a-button>
|
</div>
|
</div>
|
<p class="chat-tip">信息由AI生成,仅供参考</p>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|