完善权限系统

This commit is contained in:
BBIT-Kai
2025-12-08 18:11:48 +08:00
parent c53926afd6
commit dbdc222541
1503 changed files with 132197 additions and 885 deletions
@@ -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>