新增报表、客服功能模块

This commit is contained in:
BBIT-Kai
2025-09-18 17:09:40 +08:00
parent 76150031c9
commit 71c12a61d4
12 changed files with 2202 additions and 0 deletions
@@ -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>