新增报表、客服功能模块
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import * as api from '#/api';
|
||||
|
||||
// ---------------- 左侧智能体列表 ----------------
|
||||
interface AgentItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
welcome_words: string;
|
||||
name: string;
|
||||
role: string;
|
||||
service: string;
|
||||
available_report_tables: string[];
|
||||
}
|
||||
const agentList = ref<AgentItem[]>([]);
|
||||
const selectedAgent = ref<AgentItem | null>(null);
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadAgentList();
|
||||
});
|
||||
async function loadAgentList() {
|
||||
// TODO: 调用接口获取智能体列表
|
||||
agentList.value = (await api.getAIReportList?.()) || [];
|
||||
}
|
||||
async function loadAgent(agent: AgentItem) {
|
||||
selectedAgent.value = agent;
|
||||
|
||||
if (!agentDetailFormApi) return;
|
||||
|
||||
// 1️⃣ 异步获取备选表格
|
||||
const tableList = await api.refreshTableList();
|
||||
availableTablesOptions.value = tableList.map(
|
||||
(t: { id: string; name: string }) => ({
|
||||
label: t.name, // 页面显示文字
|
||||
value: t.id, // 对应值
|
||||
}),
|
||||
);
|
||||
|
||||
// 2️⃣ 填充表单,同时 available_report_tables 会对应选中项
|
||||
agentDetailFormApi.setValues?.({
|
||||
name: agent.name,
|
||||
title: agent.title,
|
||||
description: agent.description,
|
||||
welcome_words: agent.welcome_words,
|
||||
role: agent.role,
|
||||
service: agent.service,
|
||||
available_report_tables: agent.available_report_tables || [], // ✅ 默认选中
|
||||
});
|
||||
}
|
||||
|
||||
const availableTablesOptions = ref([]);
|
||||
// ---------------- 新增智能体弹窗 ----------------
|
||||
const [AgentDrawer, agentDrawerApi] = useVbenDrawer({
|
||||
title: '新增智能体',
|
||||
onCancel() {
|
||||
agentFormApi.resetForm?.();
|
||||
agentDrawerApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const values = await agentFormApi.getValues();
|
||||
// TODO: 调用新增智能体接口
|
||||
saveAgent(values);
|
||||
loadAgentList();
|
||||
agentFormApi.resetForm?.();
|
||||
agentDrawerApi.close();
|
||||
},
|
||||
});
|
||||
|
||||
const [AgentForm, agentFormApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'title',
|
||||
label: '智能体标题',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入智能体标题' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'description',
|
||||
label: '描述',
|
||||
componentProps: { placeholder: '请输入描述' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'welcome_words',
|
||||
label: '欢迎词',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入欢迎词', rows: 3 },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '智能体名字',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入智能体名字', rows: 3 },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'role',
|
||||
label: '智能体性格',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入智能体性格特点', rows: 3 },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'service',
|
||||
label: '工作',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入智能体负责的业务板块', rows: 3 },
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
name: 'aaa',
|
||||
options: availableTablesOptions,
|
||||
},
|
||||
fieldName: 'available_report_tables',
|
||||
label: '可用数据源',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// ---------------- 右侧智能体表单 ----------------
|
||||
|
||||
const [AgentDetailForm, agentDetailFormApi] = useVbenForm({
|
||||
layout: 'vertical',
|
||||
submitOnEnter: true,
|
||||
showDefaultActions: true,
|
||||
resetButtonOptions: { show: false },
|
||||
handleSubmit: saveAgent,
|
||||
submitButtonOptions: { content: '保存' },
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'title',
|
||||
label: '智能体标题',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入智能体标题' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'description',
|
||||
label: '描述',
|
||||
componentProps: { placeholder: '请输入描述' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'welcome_words',
|
||||
label: '欢迎词',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入欢迎词', rows: 3 },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '智能体名字',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入智能体名字', rows: 3 },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'role',
|
||||
label: '智能体性格',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入智能体性格特点', rows: 3 },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'service',
|
||||
label: '工作',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入智能体负责的业务板块', rows: 3 },
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
name: 'available_report_tables',
|
||||
options: availableTablesOptions,
|
||||
},
|
||||
fieldName: 'available_report_tables',
|
||||
label: '可用数据源',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
async function saveAgent(values: Record<string, any>) {
|
||||
// 这里做一个类型转换,把 Record<string, any> 转成 saveReportBot 需要的结构
|
||||
const payload: {
|
||||
available_kn_bases: string[];
|
||||
available_module: string;
|
||||
available_report_tables: string[];
|
||||
description: string;
|
||||
id?: string;
|
||||
name: string;
|
||||
role: string;
|
||||
service: string;
|
||||
title: string;
|
||||
welcome_words: string;
|
||||
} = {
|
||||
id: selectedAgent.value?.id,
|
||||
name: values.name,
|
||||
title: values.title,
|
||||
description: values.description,
|
||||
welcome_words: values.welcome_words,
|
||||
role: values.role,
|
||||
service: values.service,
|
||||
available_report_tables: values.available_report_tables || [],
|
||||
available_kn_bases: [],
|
||||
available_module: 'report',
|
||||
};
|
||||
|
||||
// 调用 API 保存
|
||||
await api.saveBot(payload);
|
||||
message.success('保存成功');
|
||||
// 刷新列表
|
||||
loadAgentList();
|
||||
}
|
||||
|
||||
async function agentDrawerOpen() {
|
||||
const tableList = await api.refreshTableList();
|
||||
availableTablesOptions.value = tableList.map(
|
||||
(t: { id: string; name: string }) => ({
|
||||
label: t.name, // 页面显示文字
|
||||
value: t.id, // 对应值
|
||||
}),
|
||||
);
|
||||
agentDrawerApi.open();
|
||||
selectedAgent.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[90dvh] bg-gray-50">
|
||||
<!-- 左侧智能体列表 -->
|
||||
<div class="flex w-64 flex-col border-r bg-white p-2">
|
||||
<Button type="primary" class="mb-2" @click="agentDrawerOpen()">
|
||||
新增智能体
|
||||
</Button>
|
||||
<ul class="flex-1 divide-y divide-gray-200 overflow-y-auto">
|
||||
<li
|
||||
v-for="agent in agentList"
|
||||
:key="agent.id"
|
||||
class="cursor-pointer p-4 transition-colors hover:bg-gray-50"
|
||||
:class="{
|
||||
'bg-gray-100 font-semibold':
|
||||
selectedAgent && selectedAgent.id === agent.id,
|
||||
}"
|
||||
@click="loadAgent(agent)"
|
||||
>
|
||||
<div class="truncate font-bold">{{ agent.title }}</div>
|
||||
<div class="truncate text-sm text-gray-400">
|
||||
{{ agent.description }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 右侧智能体表单 -->
|
||||
<div class="flex flex-1 flex-col p-4">
|
||||
<div v-if="selectedAgent" class="w-2/3">
|
||||
<AgentDetailForm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增智能体弹窗 -->
|
||||
<AgentDrawer>
|
||||
<AgentForm />
|
||||
</AgentDrawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,510 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { HotTable } from '@handsontable/vue3';
|
||||
import { message } from 'ant-design-vue';
|
||||
// 报表相关
|
||||
import { registerAllModules } from 'handsontable/registry';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { markdownItTable } from 'markdown-it-table';
|
||||
|
||||
import * as api from '#/api';
|
||||
|
||||
import 'handsontable/styles/handsontable.min.css';
|
||||
import 'handsontable/styles/ht-theme-main.min.css';
|
||||
import 'handsontable/styles/ht-theme-horizon.min.css';
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
}).use(markdownItTable);
|
||||
|
||||
// 输入框内容
|
||||
const inputMessage = ref('');
|
||||
|
||||
// SQL语句
|
||||
const sqlQuery = ref({
|
||||
reportId: '',
|
||||
sql: '',
|
||||
});
|
||||
|
||||
// 对话列表
|
||||
const reports = ref<{ createAt: string; id: string; title: string }[]>([]);
|
||||
|
||||
// AI 实体
|
||||
interface AgentItem {
|
||||
id: string;
|
||||
title: string;
|
||||
welcome_words: string;
|
||||
}
|
||||
|
||||
// AI列表
|
||||
const aiOptions = ref<AgentItem>();
|
||||
// 租户列表
|
||||
const companyOptions = ref<{ id: string; name: string }[]>([]);
|
||||
|
||||
// 当前选中的AI
|
||||
const selectedAI = ref<AgentItem | null>(null);
|
||||
// 当前选中的租户
|
||||
const selectedCompany = ref<null | {
|
||||
id: string;
|
||||
name: string;
|
||||
}>(null);
|
||||
// 当前选中的对话
|
||||
const currentReport = ref<null | string>(null);
|
||||
|
||||
// 聊天记录
|
||||
const messages = reactive<{ content: string; sender: 'ai' | 'user' }[]>([]);
|
||||
|
||||
// 计算第一次聊天状态
|
||||
const isInitial = computed(() => messages.length === 0);
|
||||
|
||||
function handleEnter(e: KeyboardEvent) {
|
||||
if (e.ctrlKey) {
|
||||
// Ctrl + Enter 换行
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
const start = target.selectionStart;
|
||||
const end = target.selectionEnd;
|
||||
target.value = `${target.value.slice(0, Math.max(0, start))}\n${target.value.slice(Math.max(0, end))}`;
|
||||
target.selectionStart = target.selectionEnd = start + 1;
|
||||
inputMessage.value = target.value;
|
||||
} else {
|
||||
// Enter 发送
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function autoResize(e: Event) {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
if (!target) return;
|
||||
target.style.height = 'auto';
|
||||
target.style.height = `${target.scrollHeight}px`;
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
const container = document.querySelector('#chat-container');
|
||||
if (container)
|
||||
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// 获取对话列表
|
||||
async function getReports() {
|
||||
reports.value = await api.getReports();
|
||||
}
|
||||
|
||||
// 按钮状态
|
||||
const isSaving = ref(false);
|
||||
const saved = ref(false);
|
||||
const buttonText = ref('保存');
|
||||
const canSave = ref(true); // 控制 SQL 改变后才能再保存
|
||||
|
||||
// SQL变化时,允许再次保存
|
||||
watch(
|
||||
() => sqlQuery.value.sql,
|
||||
() => {
|
||||
saved.value = false;
|
||||
canSave.value = true;
|
||||
buttonText.value = '保存';
|
||||
},
|
||||
);
|
||||
|
||||
async function saveReport() {
|
||||
if (isSaving.value || !canSave.value) return;
|
||||
|
||||
isSaving.value = true;
|
||||
canSave.value = false;
|
||||
buttonText.value = '正在保存...';
|
||||
|
||||
try {
|
||||
await api.saveReport({
|
||||
reportId: sqlQuery.value.reportId,
|
||||
});
|
||||
|
||||
// 保存成功
|
||||
saved.value = true;
|
||||
buttonText.value = '保存成功';
|
||||
getReports();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
buttonText.value = '保存失败';
|
||||
canSave.value = true;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 点击对话加载SQL与新对话
|
||||
async function loadConversation(id: string) {
|
||||
createNewConversation();
|
||||
currentReport.value = id;
|
||||
messages.splice(0);
|
||||
const report = await api.getReport(id);
|
||||
sqlQuery.value = {
|
||||
reportId: id,
|
||||
sql: report.sql,
|
||||
};
|
||||
initTableData(report.tableData);
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
const content = inputMessage.value.trim();
|
||||
if (!content) return;
|
||||
|
||||
messages.push({ type: 'human', content });
|
||||
inputMessage.value = '';
|
||||
|
||||
nextTick(() => scrollToBottom());
|
||||
|
||||
// 发送消息
|
||||
const answer = await api.chatWithReport(
|
||||
selectedAI.value?.id || '',
|
||||
selectedCompany.value?.id || '',
|
||||
currentReport.value,
|
||||
content,
|
||||
);
|
||||
initConversation(answer);
|
||||
nextTick(() => scrollToBottom());
|
||||
// 焦点回到输入框
|
||||
const textarea = document.querySelector(
|
||||
'textarea[placeholder="输入消息开始聊天..."], textarea[placeholder="输入消息..."]',
|
||||
) as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 新建聊天
|
||||
function createNewConversation() {
|
||||
currentReport.value = null;
|
||||
messages.splice(0);
|
||||
sqlQuery.value = {
|
||||
reportId: '',
|
||||
sql: '',
|
||||
};
|
||||
tableData.value = [];
|
||||
colHeaders.value = [];
|
||||
columns.value = [];
|
||||
tableKey.value += 1;
|
||||
}
|
||||
|
||||
// 获取AI列表
|
||||
const fetchOptions = async () => {
|
||||
try {
|
||||
aiOptions.value = await api.getAIReportList();
|
||||
if (aiOptions.value.length > 0 && aiOptions.value[0] !== undefined) {
|
||||
selectedAI.value = aiOptions.value[0]!;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下拉选项失败:', error);
|
||||
}
|
||||
};
|
||||
// 页面加载时调用接口
|
||||
onMounted(() => {
|
||||
fetchOptions();
|
||||
getReports();
|
||||
getCompany();
|
||||
});
|
||||
async function getCompany() {
|
||||
companyOptions.value = await api.getCompany();
|
||||
if (companyOptions.value.length > 0) {
|
||||
selectedCompany.value = companyOptions.value[0]!;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册所有模块
|
||||
registerAllModules();
|
||||
|
||||
// 初始化对话
|
||||
function initConversation(answer: any) {
|
||||
currentReport.value = answer.reportId;
|
||||
// 更新对话列表的时间和标题
|
||||
sqlQuery.value = {
|
||||
reportId: answer.reportId,
|
||||
sql: answer.sql,
|
||||
};
|
||||
messages.push({ type: 'ai', content: answer.content });
|
||||
// 恢复按钮状态
|
||||
buttonText.value = '保存';
|
||||
saved.value = false;
|
||||
canSave.value = true;
|
||||
|
||||
initTableData(answer.tableData);
|
||||
}
|
||||
|
||||
function initTableData(res: any) {
|
||||
// 动态生成列头
|
||||
if (res.length > 0) {
|
||||
colHeaders.value = Object.keys(res[0]);
|
||||
columns.value = colHeaders.value.map((key) => {
|
||||
if (key === 'is_active') return { data: key, type: 'checkbox' };
|
||||
if (key === 'join_date')
|
||||
return { data: key, type: 'date', dateFormat: 'YYYY-MM-DD' };
|
||||
if (['age', 'id', 'salary'].includes(key))
|
||||
return { data: key, type: 'numeric' };
|
||||
return { data: key, type: 'text' };
|
||||
});
|
||||
tableData.value = res;
|
||||
tableKey.value += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const tableKey = ref(0);
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([]);
|
||||
const colHeaders = ref([]);
|
||||
const columns = ref([]);
|
||||
|
||||
const displayedChars = ref([]); // 用于显示的字符数组
|
||||
|
||||
let typingTimeout: null | ReturnType<typeof setTimeout> = null;
|
||||
|
||||
// 监听 sqlQuery.sql 变化
|
||||
watch(
|
||||
() => sqlQuery.value.sql,
|
||||
(newSql) => {
|
||||
// 先清空
|
||||
displayedChars.value = [];
|
||||
if (typingTimeout) clearTimeout(typingTimeout);
|
||||
|
||||
let i = 0;
|
||||
function typeNext() {
|
||||
if (i < newSql.length) {
|
||||
displayedChars.value.push(newSql[i]);
|
||||
i++;
|
||||
typingTimeout = setTimeout(typeNext, 5); // 每个字出现的间隔 50ms
|
||||
}
|
||||
}
|
||||
|
||||
typeNext();
|
||||
},
|
||||
);
|
||||
// 复制按钮逻辑
|
||||
function copyText() {
|
||||
const text = displayedChars.value.join('');
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
message.success('复制成功');
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error(`复制失败: ${error}`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[90dvh] bg-gray-50">
|
||||
<!-- 左侧对话列表 -->
|
||||
<div
|
||||
class="flex h-full w-64 flex-shrink-0 flex-col border-r border-gray-200 bg-white"
|
||||
>
|
||||
<div class="flex items-center space-x-2 border-b border-gray-200 p-4">
|
||||
<button
|
||||
@click="createNewConversation"
|
||||
class="flex items-center rounded bg-blue-500 px-3 py-1 text-white transition-colors hover:bg-blue-600"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mr-1 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
新聊天
|
||||
</button>
|
||||
|
||||
<!-- 下拉框 -->
|
||||
<select
|
||||
v-model="selectedAI"
|
||||
class="flex-1 rounded border border-gray-300 p-1"
|
||||
>
|
||||
<option v-for="item in aiOptions" :key="item.id" :value="item">
|
||||
{{ item.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1 border-b border-gray-200 p-4">
|
||||
<select
|
||||
v-model="selectedCompany"
|
||||
class="min-w-0 flex-1 rounded border border-gray-300 p-1"
|
||||
>
|
||||
<option v-for="item in companyOptions" :key="item.id" :value="item">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ul class="flex-1 divide-y divide-gray-200 overflow-y-auto">
|
||||
<li
|
||||
v-for="report in reports"
|
||||
:key="report.id"
|
||||
@click="loadConversation(report.id)"
|
||||
class="cursor-pointer p-4 transition-colors hover:bg-gray-50"
|
||||
:class="{
|
||||
'bg-gray-100 font-semibold': currentReport === report.id,
|
||||
}"
|
||||
>
|
||||
<div class="truncate">{{ report.title }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 中侧聊天区 -->
|
||||
<div class="relative flex flex-1 flex-col">
|
||||
<!-- 报表列表 -->
|
||||
<div
|
||||
id="chat-container"
|
||||
class="flex flex-1 flex-col space-y-4 overflow-y-auto p-4 transition-all duration-300"
|
||||
:class="{
|
||||
'items-center justify-center': isInitial,
|
||||
'justify-start': !isInitial,
|
||||
}"
|
||||
>
|
||||
<template v-if="isInitial">
|
||||
<div class="flex w-full flex-col items-center space-y-4">
|
||||
<!-- 欢迎词 -->
|
||||
<div class="text-center text-lg text-gray-700">
|
||||
{{ selectedAI?.welcome_words }}
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
placeholder="输入消息开始聊天..."
|
||||
rows="1"
|
||||
class="w-1/2 resize-none overflow-hidden rounded-lg border border-gray-300 p-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
@keydown.enter.prevent="handleEnter($event)"
|
||||
@input="autoResize($event)"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex w-full flex-col items-center space-y-4">
|
||||
<div
|
||||
v-for="(msg, idx) in messages"
|
||||
:key="idx"
|
||||
class="animate-fadeIn flex w-full transition-opacity duration-500"
|
||||
:class="msg.type === 'ai' ? 'justify-start' : 'justify-end'"
|
||||
>
|
||||
<div
|
||||
class="prose inline-block max-w-[80%] break-words rounded-lg p-3"
|
||||
:class="
|
||||
msg.type === 'ai'
|
||||
? 'bg-blue-100 text-gray-800'
|
||||
: 'bg-gray-200 text-gray-800'
|
||||
"
|
||||
v-html="md.render(msg.content)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 输入框固定底部 -->
|
||||
<div v-if="!isInitial" class="flex border-t border-gray-200 bg-white p-4">
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
@keyup.enter="sendMessage"
|
||||
rows="1"
|
||||
placeholder="输入消息..."
|
||||
class="flex-1 resize-none overflow-hidden rounded-md border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
@input="
|
||||
$event.target.style.height = 'auto';
|
||||
$event.target.style.height = `${$event.target.scrollHeight}px`;
|
||||
"
|
||||
></textarea>
|
||||
<button
|
||||
@click="sendMessage"
|
||||
class="ml-2 rounded-md bg-blue-500 px-4 py-2 text-white transition-colors hover:bg-blue-600"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧报表区 -->
|
||||
<div
|
||||
class="relative flex w-64 flex-1 flex-col border-l border-gray-200 bg-white"
|
||||
>
|
||||
<div class="flex border-b border-gray-300 bg-gray-50 p-4">
|
||||
<!-- 左侧文本区 -->
|
||||
<pre
|
||||
class="m-0 max-h-40 flex-1 overflow-auto whitespace-pre-wrap break-words text-sm text-gray-700"
|
||||
>
|
||||
<span v-for="(char, index) in displayedChars" :key="index">{{ char }}</span>
|
||||
</pre>
|
||||
|
||||
<!-- 右侧按钮 -->
|
||||
<div class="ml-4 flex flex-col gap-2">
|
||||
<button
|
||||
v-if="displayedChars.length > 0"
|
||||
:disabled="isSaving || !canSave"
|
||||
@click="saveReport"
|
||||
class="rounded bg-blue-500 px-4 py-2 text-white disabled:bg-gray-400"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="displayedChars.length > 0"
|
||||
@click="copyText"
|
||||
class="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Handsontable 区域 -->
|
||||
<HotTable
|
||||
:key="tableKey"
|
||||
:data="tableData"
|
||||
:col-headers="colHeaders"
|
||||
:columns="columns"
|
||||
row-headers
|
||||
stretch-h="all"
|
||||
license-key="non-commercial-and-evaluation"
|
||||
width="100%"
|
||||
height="auto"
|
||||
theme-name="ht-theme-horizon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s forwards;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,246 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import * as api from '#/api';
|
||||
|
||||
// ---------------- 左侧表列表相关 ----------------
|
||||
interface TableItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
interface FieldItem {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
const fieldList = ref<FieldItem[]>([]);
|
||||
|
||||
const tableList = ref<TableItem[]>([]);
|
||||
const selectedTable = ref<null | TableItem>(null);
|
||||
|
||||
async function loadTableList() {
|
||||
tableList.value = await api.refreshTableList();
|
||||
}
|
||||
|
||||
async function loadField(table: TableItem) {
|
||||
selectedTable.value = table;
|
||||
loading.value = true;
|
||||
fieldList.value = await api.refreshFieldList(table.id);
|
||||
loading.value = false;
|
||||
fieldGridApi.reload(); // 刷新表格
|
||||
}
|
||||
|
||||
// ---------------- 新增表弹窗 ----------------
|
||||
const [TableDrawer, tableDrawerApi] = useVbenDrawer({
|
||||
title: '新增表',
|
||||
onCancel() {
|
||||
// 重置表单内容
|
||||
tableFormApi.resetForm?.();
|
||||
tableDrawerApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const values = await tableFormApi.getValues();
|
||||
await api.addTable(values as { description: string; name: string });
|
||||
loadTableList();
|
||||
tableFormApi.resetForm?.();
|
||||
tableDrawerApi.close();
|
||||
},
|
||||
});
|
||||
|
||||
const [TableForm, tableFormApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '表名',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入表名' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'description',
|
||||
label: '描述',
|
||||
componentProps: { placeholder: '请输入描述' },
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// ---------------- 右侧字段表单 ----------------
|
||||
const [FieldForm, fieldFormApi] = useVbenForm({
|
||||
layout: 'horizontal', // 横向排列
|
||||
submitOnEnter: true,
|
||||
showDefaultActions: true,
|
||||
resetButtonOptions: {
|
||||
show: false,
|
||||
},
|
||||
wrapperClass: 'grid-cols-5 gap-2',
|
||||
handleSubmit: saveField,
|
||||
submitButtonOptions: {
|
||||
content: '新增字段',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '字段名称',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入字段名' },
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'type',
|
||||
label: '数据类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
class: 'w-40',
|
||||
placeholder: '数据类型',
|
||||
options: [
|
||||
{ label: 'nvarchar', value: 'nvarchar' }, // 字符串
|
||||
{ label: 'int', value: 'int' }, // 整数
|
||||
{ label: 'bigint', value: 'bigint' }, // 大整数
|
||||
{ label: 'decimal', value: 'decimal' }, // 高精度小数
|
||||
{ label: 'float', value: 'float' }, // 浮点数
|
||||
{ label: 'date', value: 'date' }, // 日期(无时间)
|
||||
{ label: 'datetime', value: 'datetime' }, // 日期时间
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'description',
|
||||
label: '字段描述',
|
||||
componentProps: {
|
||||
placeholder: '字段描述(包含举例)',
|
||||
class: 'w-200',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'is_active',
|
||||
defaultValue: true,
|
||||
label: '是否启用',
|
||||
},
|
||||
],
|
||||
});
|
||||
async function saveField(values: Record<string, any>) {
|
||||
await api.addField({
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
description: values.description,
|
||||
is_active: values.is_active,
|
||||
table_id: selectedTable.value?.id || '',
|
||||
});
|
||||
loadField(selectedTable.value as TableItem);
|
||||
|
||||
// 清空表单
|
||||
resetFieldForm();
|
||||
}
|
||||
function resetFieldForm() {
|
||||
const fields = ['name', 'type', 'description', 'is_active'];
|
||||
const defaultValues: Record<string, any> = {
|
||||
name: '',
|
||||
type: undefined,
|
||||
description: '',
|
||||
is_active: true, // 保持默认值
|
||||
};
|
||||
|
||||
fields.forEach((field) => {
|
||||
fieldFormApi.setFieldValue(field, defaultValues[field]);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------- 字段表格 ----------------
|
||||
const loading = ref(false);
|
||||
const [FieldGrid, fieldGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ field: 'name', title: '字段名' },
|
||||
{ field: 'type', title: '字段类型' },
|
||||
{ field: 'description', title: '字段描述', align: 'left' },
|
||||
{
|
||||
field: 'is_active',
|
||||
title: '状态',
|
||||
formatter: (cellValue: boolean) => (cellValue ? '已启用' : '尚未启用'),
|
||||
},
|
||||
],
|
||||
height: '720px',
|
||||
stripe: true,
|
||||
loading: loading.value,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
return { items: fieldList.value, total: fieldList.value.length };
|
||||
},
|
||||
},
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadTableList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[90dvh] bg-gray-50">
|
||||
<!-- 左侧列表 -->
|
||||
<div class="flex w-64 flex-col border-r bg-white p-2">
|
||||
<Button type="primary" class="mb-2" @click="tableDrawerApi.open()">
|
||||
新增表
|
||||
</Button>
|
||||
<ul class="flex-1 divide-y divide-gray-200 overflow-y-auto">
|
||||
<li
|
||||
v-for="table in tableList"
|
||||
:key="table.id"
|
||||
class="cursor-pointer p-4 transition-colors hover:bg-gray-50"
|
||||
:class="{
|
||||
'bg-gray-100 font-semibold':
|
||||
selectedTable && selectedTable.id === table.id,
|
||||
}"
|
||||
@click="loadField(table)"
|
||||
>
|
||||
<div class="truncate font-bold">{{ table.name }}</div>
|
||||
<div class="truncate text-sm text-gray-400">
|
||||
{{ table.description }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="flex flex-1 flex-col p-4">
|
||||
<!-- 字段表单 -->
|
||||
<div class="mb-2" v-if="selectedTable">
|
||||
<div class="flex w-full items-end space-x-2">
|
||||
<FieldForm />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 字段表格,占满剩余高度 -->
|
||||
<div class="flex-1">
|
||||
<FieldGrid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增表弹窗 -->
|
||||
<TableDrawer>
|
||||
<TableForm />
|
||||
</TableDrawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,211 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import * as api from '#/api';
|
||||
|
||||
// ---------------- 左侧知识库列表相关 ----------------
|
||||
interface KnowledgeBase {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
interface KnowledgeItem {
|
||||
text: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
const knowledgeList = ref<KnowledgeItem[]>([]);
|
||||
const knowledgeBaseList = ref<KnowledgeBase[]>([]);
|
||||
const selectedKnowledgeBase = ref<KnowledgeBase | null>(null);
|
||||
|
||||
async function loadKnowledgeBaseList() {
|
||||
knowledgeBaseList.value = await api.refreshKnowledgeBaseList();
|
||||
}
|
||||
|
||||
async function loadKnowledge(base: KnowledgeBase) {
|
||||
loading.value = true;
|
||||
selectedKnowledgeBase.value = base;
|
||||
loading.value = true;
|
||||
knowledgeList.value = await api.refreshKnowledgeList(base.id);
|
||||
loading.value = false;
|
||||
knowledgeGridApi.reload(); // 刷新表格
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// ---------------- 新增知识库弹窗 ----------------
|
||||
const [KnowledgeBaseDrawer, knowledgeBaseDrawerApi] = useVbenDrawer({
|
||||
title: '新增知识库',
|
||||
onCancel() {
|
||||
knowledgeBaseFormApi.resetForm?.();
|
||||
knowledgeBaseDrawerApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const values = await knowledgeBaseFormApi.getValues();
|
||||
await api.addKnowledgeBase(values as { description: string; name: string });
|
||||
loadKnowledgeBaseList();
|
||||
knowledgeBaseFormApi.resetForm?.();
|
||||
knowledgeBaseDrawerApi.close();
|
||||
},
|
||||
});
|
||||
|
||||
const [KnowledgeBaseForm, knowledgeBaseFormApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '知识库名称',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入知识库名称' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'description',
|
||||
label: '描述',
|
||||
componentProps: { placeholder: '请输入描述' },
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// ---------------- 右侧新增知识表单 ----------------
|
||||
const [KnowledgeForm, knowledgeFormApi] = useVbenForm({
|
||||
layout: 'horizontal',
|
||||
submitOnEnter: true,
|
||||
showDefaultActions: true,
|
||||
resetButtonOptions: {
|
||||
show: false,
|
||||
},
|
||||
wrapperClass: 'grid-cols-5 gap-2',
|
||||
handleSubmit: saveKnowledge,
|
||||
submitButtonOptions: {
|
||||
content: '新增知识',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'Textarea',
|
||||
// 占满三列空间 基线对齐
|
||||
formItemClass: 'col-span-3 items-baseline',
|
||||
fieldName: 'text',
|
||||
label: '知识内容',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入知识内容',
|
||||
rows: 3,
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'is_active',
|
||||
defaultValue: true,
|
||||
label: '是否启用',
|
||||
},
|
||||
],
|
||||
});
|
||||
async function saveKnowledge(values: Record<string, any>) {
|
||||
await api.addKnowledge({
|
||||
text: values.text,
|
||||
is_active: values.is_active,
|
||||
knowledge_base_id: selectedKnowledgeBase.value?.id || '',
|
||||
});
|
||||
loadKnowledge(selectedKnowledgeBase.value as KnowledgeBase);
|
||||
resetKnowledgeForm();
|
||||
}
|
||||
function resetKnowledgeForm() {
|
||||
knowledgeFormApi.setFieldValue('text', '');
|
||||
knowledgeFormApi.setFieldValue('is_active', true);
|
||||
}
|
||||
|
||||
// ---------------- 知识表格 ----------------
|
||||
const loading = ref(false);
|
||||
const [KnowledgeGrid, knowledgeGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ field: 'text', title: '知识内容', align: 'left' },
|
||||
{
|
||||
field: 'is_active',
|
||||
title: '状态',
|
||||
formatter: ({ cellValue }: { cellValue: boolean }) => {
|
||||
return cellValue ? '已启用' : '尚未启用';
|
||||
},
|
||||
},
|
||||
],
|
||||
height: '720px',
|
||||
stripe: true,
|
||||
loading: loading.value,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
return {
|
||||
items: knowledgeList.value,
|
||||
total: knowledgeList.value.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
pagerConfig: { enabled: false },
|
||||
scrollY: { enabled: true, gt: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadKnowledgeBaseList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[90dvh] bg-gray-50">
|
||||
<!-- 左侧知识库列表 -->
|
||||
<div class="flex w-64 flex-col border-r bg-white p-2">
|
||||
<Button
|
||||
type="primary"
|
||||
class="mb-2"
|
||||
@click="knowledgeBaseDrawerApi.open()"
|
||||
>
|
||||
新增知识库
|
||||
</Button>
|
||||
<ul class="flex-1 divide-y divide-gray-200 overflow-y-auto">
|
||||
<li
|
||||
v-for="base in knowledgeBaseList"
|
||||
:key="base.id"
|
||||
class="cursor-pointer p-4 transition-colors hover:bg-gray-50"
|
||||
:class="{
|
||||
'bg-gray-100 font-semibold':
|
||||
selectedKnowledgeBase && selectedKnowledgeBase.id === base.id,
|
||||
}"
|
||||
@click="loadKnowledge(base)"
|
||||
>
|
||||
<div class="truncate font-bold">{{ base.name }}</div>
|
||||
<div class="truncate text-sm text-gray-400">
|
||||
{{ base.description }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="flex flex-1 flex-col p-4">
|
||||
<!-- 新增知识表单 -->
|
||||
<div class="mb-2" v-if="selectedKnowledgeBase">
|
||||
<div class="flex w-full items-end space-x-2">
|
||||
<KnowledgeForm />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 知识表格 -->
|
||||
<div class="flex-1">
|
||||
<KnowledgeGrid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增知识库弹窗 -->
|
||||
<KnowledgeBaseDrawer>
|
||||
<KnowledgeBaseForm />
|
||||
</KnowledgeBaseDrawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,285 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
import { useVbenDrawer } from "@vben/common-ui";
|
||||
|
||||
import { Button, message } from "ant-design-vue";
|
||||
|
||||
import { useVbenForm } from "#/adapter/form";
|
||||
import * as api from "#/api";
|
||||
|
||||
// ---------------- 左侧智能体列表 ----------------
|
||||
interface AgentItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
welcome_words: string;
|
||||
name: string;
|
||||
role: string;
|
||||
service: string;
|
||||
available_kn_bases: string[];
|
||||
}
|
||||
|
||||
const agentList = ref<AgentItem[]>([]);
|
||||
const selectedAgent = ref<AgentItem | null>(null);
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadAgentList();
|
||||
});
|
||||
|
||||
async function loadAgentList() {
|
||||
// 调用接口获取智能体列表
|
||||
agentList.value = await api.getAIBotListForService();
|
||||
}
|
||||
|
||||
async function loadAgent(agent: AgentItem) {
|
||||
selectedAgent.value = agent;
|
||||
|
||||
if (!agentDetailFormApi) return;
|
||||
|
||||
// 1️⃣ 异步获取备选表格
|
||||
const tableList = await api.refreshKnowledgeBaseListForService();
|
||||
availableTablesOptions.value = tableList.map(
|
||||
(t: { id: string; name: string }) => ({
|
||||
label: t.name, // 页面显示文字
|
||||
value: t.id // 对应值
|
||||
})
|
||||
);
|
||||
|
||||
// 2️⃣ 填充表单,同时 available_kn_bases 会对应选中项
|
||||
agentDetailFormApi.setValues?.({
|
||||
name: agent.name,
|
||||
title: agent.title,
|
||||
description: agent.description,
|
||||
welcome_words: agent.welcome_words,
|
||||
role: agent.role,
|
||||
service: agent.service,
|
||||
available_kn_bases: agent.available_kn_bases || [] // ✅ 默认选中
|
||||
});
|
||||
}
|
||||
|
||||
const availableTablesOptions = ref([]);
|
||||
// ---------------- 新增智能体弹窗 ----------------
|
||||
const [AgentDrawer, agentDrawerApi] = useVbenDrawer({
|
||||
title: "新增智能体",
|
||||
onCancel() {
|
||||
agentFormApi.resetForm?.();
|
||||
agentDrawerApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const values = await agentFormApi.getValues();
|
||||
// TODO: 调用新增智能体接口
|
||||
saveAgent(values);
|
||||
loadAgentList();
|
||||
agentFormApi.resetForm?.();
|
||||
agentDrawerApi.close();
|
||||
}
|
||||
});
|
||||
|
||||
const [AgentForm, agentFormApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "title",
|
||||
label: "智能体标题",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入智能体标题" }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "description",
|
||||
label: "描述",
|
||||
componentProps: { placeholder: "请输入描述" }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "welcome_words",
|
||||
label: "欢迎词",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入欢迎词", rows: 3 }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "name",
|
||||
label: "智能体名字",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入智能体名字", rows: 3 }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "role",
|
||||
label: "智能体性格",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入智能体性格特点", rows: 3 }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "service",
|
||||
label: "工作",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入智能体负责的业务板块", rows: 3 }
|
||||
},
|
||||
{
|
||||
component: "CheckboxGroup",
|
||||
componentProps: {
|
||||
name: "aaa",
|
||||
options: availableTablesOptions
|
||||
},
|
||||
fieldName: "available_kn_bases",
|
||||
label: "可用知识库",
|
||||
rules: "selectRequired"
|
||||
}
|
||||
],
|
||||
showDefaultActions: false
|
||||
});
|
||||
|
||||
// ---------------- 右侧智能体表单 ----------------
|
||||
|
||||
const [AgentDetailForm, agentDetailFormApi] = useVbenForm({
|
||||
layout: "vertical",
|
||||
submitOnEnter: true,
|
||||
showDefaultActions: true,
|
||||
resetButtonOptions: { show: false },
|
||||
handleSubmit: saveAgent,
|
||||
submitButtonOptions: { content: "保存" },
|
||||
schema: [
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "title",
|
||||
label: "智能体标题",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入智能体标题" }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "description",
|
||||
label: "描述",
|
||||
componentProps: { placeholder: "请输入描述" }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "welcome_words",
|
||||
label: "欢迎词",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入欢迎词", rows: 3 }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "name",
|
||||
label: "智能体名字",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入智能体名字", rows: 3 }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "role",
|
||||
label: "智能体性格",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入智能体性格特点", rows: 3 }
|
||||
},
|
||||
{
|
||||
component: "Input",
|
||||
fieldName: "service",
|
||||
label: "工作",
|
||||
rules: "required",
|
||||
componentProps: { placeholder: "请输入智能体负责的业务板块", rows: 3 }
|
||||
},
|
||||
{
|
||||
component: "CheckboxGroup",
|
||||
componentProps: {
|
||||
name: "available_kn_bases",
|
||||
options: availableTablesOptions
|
||||
},
|
||||
fieldName: "available_kn_bases",
|
||||
label: "可用知识库",
|
||||
rules: "selectRequired"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
async function saveAgent(values: Record<string, any>) {
|
||||
// 这里做一个类型转换,把 Record<string, any> 转成 saveReportBot 需要的结构
|
||||
const payload: {
|
||||
available_kn_bases: string[];
|
||||
available_module: string;
|
||||
available_report_tables: string[];
|
||||
description: string;
|
||||
id?: string;
|
||||
name: string;
|
||||
role: string;
|
||||
service: string;
|
||||
title: string;
|
||||
welcome_words: string;
|
||||
} = {
|
||||
id: selectedAgent.value?.id,
|
||||
name: values.name,
|
||||
title: values.title,
|
||||
description: values.description,
|
||||
welcome_words: values.welcome_words,
|
||||
role: values.role,
|
||||
service: values.service,
|
||||
available_kn_bases: values.available_kn_bases || [],
|
||||
available_report_tables: [],
|
||||
available_module: "service"
|
||||
};
|
||||
|
||||
// 调用 API 保存
|
||||
await api.saveBot(payload);
|
||||
message.success("保存成功");
|
||||
// 刷新列表
|
||||
loadAgentList();
|
||||
}
|
||||
|
||||
async function agentDrawerOpen() {
|
||||
const tableList = await api.refreshKnowledgeBaseListForService();
|
||||
availableTablesOptions.value = tableList.map(
|
||||
(t: { id: string; name: string }) => ({
|
||||
label: t.name, // 页面显示文字
|
||||
value: t.id // 对应值
|
||||
})
|
||||
);
|
||||
agentDrawerApi.open();
|
||||
selectedAgent.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[90dvh] bg-gray-50">
|
||||
<!-- 左侧智能体列表 -->
|
||||
<div class="flex w-64 flex-col border-r bg-white p-2">
|
||||
<Button type="primary" class="mb-2" @click="agentDrawerOpen()">
|
||||
新增智能体
|
||||
</Button>
|
||||
<ul class="flex-1 divide-y divide-gray-200 overflow-y-auto">
|
||||
<li
|
||||
v-for="agent in agentList"
|
||||
:key="agent.id"
|
||||
class="cursor-pointer p-4 transition-colors hover:bg-gray-50"
|
||||
:class="{
|
||||
'bg-gray-100 font-semibold':
|
||||
selectedAgent && selectedAgent.id === agent.id,
|
||||
}"
|
||||
@click="loadAgent(agent)"
|
||||
>
|
||||
<div class="truncate font-bold">{{ agent.title }}</div>
|
||||
<div class="truncate text-sm text-gray-400">
|
||||
{{ agent.description }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 右侧智能体表单 -->
|
||||
<div class="flex flex-1 flex-col p-4">
|
||||
<div v-if="selectedAgent" class="w-2/3">
|
||||
<AgentDetailForm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增智能体弹窗 -->
|
||||
<AgentDrawer>
|
||||
<AgentForm />
|
||||
</AgentDrawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,309 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { markdownItTable } from 'markdown-it-table';
|
||||
|
||||
import * as api from '#/api';
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
}).use(markdownItTable);
|
||||
// AI 实体
|
||||
interface AgentItem {
|
||||
id: string;
|
||||
title: string;
|
||||
welcome_words: string;
|
||||
}
|
||||
// 输入框内容
|
||||
const inputMessage = ref('');
|
||||
|
||||
// 对话列表
|
||||
const conversations = ref<AgentItem[]>([]);
|
||||
// AI列表
|
||||
const aiOptions = ref<AgentItem[]>([]);
|
||||
|
||||
// 当前选中的AI
|
||||
const selectedAI = ref<AgentItem | null>(null);
|
||||
// 当前选中的对话
|
||||
const currentSession = ref<null | string>(null);
|
||||
|
||||
// 聊天记录
|
||||
const messages = reactive<{ content: string; sender: 'ai' | 'user' }[]>([]);
|
||||
|
||||
// 计算第一次聊天状态
|
||||
const isInitial = computed(() => messages.length === 0);
|
||||
|
||||
function handleEnter(e: KeyboardEvent) {
|
||||
if (e.ctrlKey) {
|
||||
// Ctrl + Enter 换行
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
const start = target.selectionStart;
|
||||
const end = target.selectionEnd;
|
||||
target.value = `${target.value.slice(0, Math.max(0, start))}\n${target.value.slice(Math.max(0, end))}`;
|
||||
target.selectionStart = target.selectionEnd = start + 1;
|
||||
inputMessage.value = target.value;
|
||||
} else {
|
||||
// Enter 发送
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function autoResize(e: Event) {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
if (!target) return;
|
||||
target.style.height = 'auto';
|
||||
target.style.height = `${target.scrollHeight}px`;
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
const container = document.querySelector('#chat-container');
|
||||
if (container)
|
||||
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// 模拟接口获取对话列表
|
||||
async function getSessions() {
|
||||
conversations.value = await api.getSessionsForService();
|
||||
}
|
||||
|
||||
// 模拟接口获取聊天记录
|
||||
async function fetchMessages(conversationId: string) {
|
||||
messages.splice(0);
|
||||
const msg = await api.getHistory(conversationId);
|
||||
messages.push(...msg);
|
||||
}
|
||||
|
||||
// 点击对话加载聊天记录
|
||||
async function loadConversation(id: string) {
|
||||
currentSession.value = id;
|
||||
await fetchMessages(id);
|
||||
nextTick(() => scrollToBottom());
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
const content = inputMessage.value.trim();
|
||||
if (!content) return;
|
||||
|
||||
messages.push({ type: 'human', content });
|
||||
inputMessage.value = '';
|
||||
|
||||
nextTick(() => scrollToBottom());
|
||||
|
||||
// 发送消息
|
||||
const answer = await api.chatWithService(
|
||||
selectedAI.value?.id || '',
|
||||
currentSession.value,
|
||||
content,
|
||||
);
|
||||
currentSession.value = answer.sessionId;
|
||||
if (answer.isNewSession) {
|
||||
conversations.value.unshift({
|
||||
id: answer.sessionId,
|
||||
updatedAt: formatDate(new Date()),
|
||||
title: answer.sessionName,
|
||||
});
|
||||
}
|
||||
messages.push({ type: 'ai', content: answer.content });
|
||||
nextTick(() => scrollToBottom());
|
||||
// 焦点回到输入框
|
||||
const textarea = document.querySelector(
|
||||
'textarea[placeholder="输入消息开始聊天..."], textarea[placeholder="输入消息..."]',
|
||||
) as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 新建聊天
|
||||
function createNewConversation() {
|
||||
currentSession.value = null;
|
||||
messages.splice(0);
|
||||
}
|
||||
|
||||
// 获取AI列表
|
||||
const fetchOptions = async () => {
|
||||
try {
|
||||
aiOptions.value = await api.getAIBotListForService();
|
||||
if (aiOptions.value.length > 0 && aiOptions.value[0] !== undefined) {
|
||||
selectedAI.value = aiOptions.value[0]!;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下拉选项失败:', error);
|
||||
}
|
||||
};
|
||||
function formatDate(date: Date): string {
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
|
||||
return (
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
||||
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
);
|
||||
}
|
||||
// 页面加载时调用接口
|
||||
onMounted(() => {
|
||||
fetchOptions();
|
||||
getSessions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[90dvh] bg-gray-50">
|
||||
<!-- 左侧对话列表 -->
|
||||
<div
|
||||
class="flex h-full w-64 flex-shrink-0 flex-col border-r border-gray-200 bg-white"
|
||||
>
|
||||
<div class="flex items-center space-x-2 border-b border-gray-200 p-4">
|
||||
<button
|
||||
@click="createNewConversation"
|
||||
class="flex items-center rounded bg-blue-500 px-3 py-1 text-white transition-colors hover:bg-blue-600"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mr-1 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
新聊天
|
||||
</button>
|
||||
|
||||
<!-- 下拉框 -->
|
||||
<select
|
||||
v-model="selectedAI"
|
||||
class="flex-1 rounded border border-gray-300 p-1"
|
||||
>
|
||||
<option v-for="item in aiOptions" :key="item.id" :value="item">
|
||||
{{ item.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<ul class="flex-1 divide-y divide-gray-200 overflow-y-auto">
|
||||
<li
|
||||
v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
@click="loadConversation(conv.id)"
|
||||
class="cursor-pointer p-4 transition-colors hover:bg-gray-50"
|
||||
:class="{
|
||||
'bg-gray-100 font-semibold': currentSession === conv.id,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="truncate">{{ conv.title }}</span>
|
||||
<span class="ml-2 text-xs text-gray-400">{{ conv.updatedAt }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 右侧聊天区 -->
|
||||
<div class="relative flex flex-1 flex-col">
|
||||
<!-- 聊天记录 -->
|
||||
<div
|
||||
id="chat-container"
|
||||
class="flex flex-1 flex-col space-y-4 overflow-y-auto p-4 transition-all duration-300"
|
||||
:class="{
|
||||
'items-center justify-center': isInitial,
|
||||
'justify-start': !isInitial,
|
||||
}"
|
||||
>
|
||||
<template v-if="isInitial">
|
||||
<div class="flex w-full flex-col items-center space-y-4">
|
||||
<!-- 欢迎词 -->
|
||||
<div class="text-center text-lg text-gray-700">
|
||||
{{ selectedAI?.welcome_words }}
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
placeholder="输入消息开始聊天..."
|
||||
rows="1"
|
||||
class="w-1/2 resize-none overflow-hidden rounded-lg border border-gray-300 p-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
@keydown.enter.prevent="handleEnter($event)"
|
||||
@input="autoResize($event)"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex w-full flex-col items-center space-y-4">
|
||||
<div
|
||||
v-for="(msg, idx) in messages"
|
||||
:key="idx"
|
||||
class="animate-fadeIn flex w-full transition-opacity duration-500"
|
||||
:class="msg.type === 'ai' ? 'justify-start' : 'justify-end'"
|
||||
>
|
||||
<div
|
||||
class="prose inline-block max-w-[80%] break-words rounded-lg p-3"
|
||||
:class="
|
||||
msg.type === 'ai'
|
||||
? 'bg-blue-100 text-gray-800'
|
||||
: 'bg-gray-200 text-gray-800'
|
||||
"
|
||||
v-html="md.render(msg.content)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 输入框固定底部 -->
|
||||
<div v-if="!isInitial" class="flex border-t border-gray-200 bg-white p-4">
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
@keyup.enter="sendMessage"
|
||||
rows="1"
|
||||
placeholder="输入消息..."
|
||||
class="flex-1 resize-none overflow-hidden rounded-md border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
@input="
|
||||
$event.target.style.height = 'auto';
|
||||
$event.target.style.height = `${$event.target.scrollHeight}px`;
|
||||
"
|
||||
></textarea>
|
||||
<button
|
||||
@click="sendMessage"
|
||||
class="ml-2 rounded-md bg-blue-500 px-4 py-2 text-white transition-colors hover:bg-blue-600"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s forwards;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,211 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import * as api from '#/api';
|
||||
|
||||
// ---------------- 左侧知识库列表相关 ----------------
|
||||
interface KnowledgeBase {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
interface KnowledgeItem {
|
||||
text: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
const knowledgeList = ref<KnowledgeItem[]>([]);
|
||||
const knowledgeBaseList = ref<KnowledgeBase[]>([]);
|
||||
const selectedKnowledgeBase = ref<KnowledgeBase | null>(null);
|
||||
|
||||
async function loadKnowledgeBaseList() {
|
||||
knowledgeBaseList.value = await api.refreshKnowledgeBaseListForService();
|
||||
}
|
||||
|
||||
async function loadKnowledge(base: KnowledgeBase) {
|
||||
loading.value = true;
|
||||
selectedKnowledgeBase.value = base;
|
||||
loading.value = true;
|
||||
knowledgeList.value = await api.refreshKnowledgeList(base.id);
|
||||
loading.value = false;
|
||||
knowledgeGridApi.reload(); // 刷新表格
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// ---------------- 新增知识库弹窗 ----------------
|
||||
const [KnowledgeBaseDrawer, knowledgeBaseDrawerApi] = useVbenDrawer({
|
||||
title: '新增知识库',
|
||||
onCancel() {
|
||||
knowledgeBaseFormApi.resetForm?.();
|
||||
knowledgeBaseDrawerApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const values = await knowledgeBaseFormApi.getValues();
|
||||
await api.addKnowledgeBase(values as { description: string; name: string });
|
||||
loadKnowledgeBaseList();
|
||||
knowledgeBaseFormApi.resetForm?.();
|
||||
knowledgeBaseDrawerApi.close();
|
||||
},
|
||||
});
|
||||
|
||||
const [KnowledgeBaseForm, knowledgeBaseFormApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '知识库名称',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入知识库名称' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'description',
|
||||
label: '描述',
|
||||
componentProps: { placeholder: '请输入描述' },
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// ---------------- 右侧新增知识表单 ----------------
|
||||
const [KnowledgeForm, knowledgeFormApi] = useVbenForm({
|
||||
layout: 'horizontal',
|
||||
submitOnEnter: true,
|
||||
showDefaultActions: true,
|
||||
resetButtonOptions: {
|
||||
show: false,
|
||||
},
|
||||
wrapperClass: 'grid-cols-5 gap-2',
|
||||
handleSubmit: saveKnowledge,
|
||||
submitButtonOptions: {
|
||||
content: '新增知识',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'Textarea',
|
||||
// 占满三列空间 基线对齐
|
||||
formItemClass: 'col-span-3 items-baseline',
|
||||
fieldName: 'text',
|
||||
label: '知识内容',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入知识内容',
|
||||
rows: 3,
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'is_active',
|
||||
defaultValue: true,
|
||||
label: '是否启用',
|
||||
},
|
||||
],
|
||||
});
|
||||
async function saveKnowledge(values: Record<string, any>) {
|
||||
await api.addKnowledge({
|
||||
text: values.text,
|
||||
is_active: values.is_active,
|
||||
knowledge_base_id: selectedKnowledgeBase.value?.id || '',
|
||||
});
|
||||
loadKnowledge(selectedKnowledgeBase.value as KnowledgeBase);
|
||||
resetKnowledgeForm();
|
||||
}
|
||||
function resetKnowledgeForm() {
|
||||
knowledgeFormApi.setFieldValue('text', '');
|
||||
knowledgeFormApi.setFieldValue('is_active', true);
|
||||
}
|
||||
|
||||
// ---------------- 知识表格 ----------------
|
||||
const loading = ref(false);
|
||||
const [KnowledgeGrid, knowledgeGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ field: 'text', title: '知识内容', align: 'left' },
|
||||
{
|
||||
field: 'is_active',
|
||||
title: '状态',
|
||||
formatter: ({ cellValue }: { cellValue: boolean }) => {
|
||||
return cellValue ? '已启用' : '尚未启用';
|
||||
},
|
||||
},
|
||||
],
|
||||
height: '720px',
|
||||
stripe: true,
|
||||
loading: loading.value,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
return {
|
||||
items: knowledgeList.value,
|
||||
total: knowledgeList.value.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
pagerConfig: { enabled: false },
|
||||
scrollY: { enabled: true, gt: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadKnowledgeBaseList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[90dvh] bg-gray-50">
|
||||
<!-- 左侧知识库列表 -->
|
||||
<div class="flex w-64 flex-col border-r bg-white p-2">
|
||||
<Button
|
||||
type="primary"
|
||||
class="mb-2"
|
||||
@click="knowledgeBaseDrawerApi.open()"
|
||||
>
|
||||
新增知识库
|
||||
</Button>
|
||||
<ul class="flex-1 divide-y divide-gray-200 overflow-y-auto">
|
||||
<li
|
||||
v-for="base in knowledgeBaseList"
|
||||
:key="base.id"
|
||||
class="cursor-pointer p-4 transition-colors hover:bg-gray-50"
|
||||
:class="{
|
||||
'bg-gray-100 font-semibold':
|
||||
selectedKnowledgeBase && selectedKnowledgeBase.id === base.id,
|
||||
}"
|
||||
@click="loadKnowledge(base)"
|
||||
>
|
||||
<div class="truncate font-bold">{{ base.name }}</div>
|
||||
<div class="truncate text-sm text-gray-400">
|
||||
{{ base.description }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="flex flex-1 flex-col p-4">
|
||||
<!-- 新增知识表单 -->
|
||||
<div class="mb-2" v-if="selectedKnowledgeBase">
|
||||
<div class="flex w-full items-end space-x-2">
|
||||
<KnowledgeForm />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 知识表格 -->
|
||||
<div class="flex-1">
|
||||
<KnowledgeGrid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增知识库弹窗 -->
|
||||
<KnowledgeBaseDrawer>
|
||||
<KnowledgeBaseForm />
|
||||
</KnowledgeBaseDrawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>正在开发中,敬请期待</h1>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user