Browse Source

feat(role): 客服角色管理页布局优化并支持 MCP 工具全选

TDesign-Vue-Next-1.20.6
wanghanlin 1 week ago
parent
commit
7222893120
  1. 196
      frontend/src/views/RoleManager.vue

196
frontend/src/views/RoleManager.vue

@ -1,5 +1,5 @@
<template>
<t-card title="🎭 客服角色管理" :bordered="false">
<t-card title="🎭 客服角色管理" :bordered="false" class="role-page-card">
<p class="desc-text">为角色设定人设并授权可检索的知识库分类对话页只能选择这里配置好的角色检索范围由服务端按角色强制限定</p>
<div class="role-shell">
@ -77,20 +77,20 @@
<t-form label-align="top" class="detail-form">
<t-row :gutter="16">
<t-col :span="8">
<t-col :span="6" :xs="12">
<t-form-item label="角色名称 *">
<t-input v-model="form.name" :disabled="saving" placeholder="请输入角色名称" />
</t-form-item>
</t-col>
<t-col :span="4">
<t-form-item label="状态">
<t-switch v-model="form.enabled" :disabled="saving" :label="['启用', '停用']" />
<t-col :span="6" :xs="12">
<t-form-item label="职责描述">
<t-input v-model="form.description" :disabled="saving" placeholder="职责描述(可选)" />
</t-form-item>
</t-col>
</t-row>
<t-form-item label="职责描述">
<t-input v-model="form.description" :disabled="saving" placeholder="职责描述(可选)" />
<t-form-item label="状态">
<t-switch v-model="form.enabled" :disabled="saving" :label="['启用', '停用']" />
</t-form-item>
<t-form-item label="人设 / 风格提示词">
@ -102,6 +102,8 @@
/>
</t-form-item>
<div class="form-section-title">授权配置</div>
<t-form-item label="可检索知识库授权">
<t-checkbox-group v-if="categoryStore.categories.length" v-model="form.categoryIds">
<t-checkbox
@ -125,24 +127,43 @@
全部授权允许调用所有已接入的 MCP 工具
</t-checkbox>
<t-loading size="small" :loading="mcpLoading">
<t-checkbox-group
v-if="!allToolsSelected && mcpServers.length"
v-model="form.allowedMcpTools"
class="mcp-tools-list"
>
<div v-for="s in mcpServers" :key="s.configId" class="mcp-server-group">
<div class="mcp-server-name">{{ s.serverName }}</div>
<template v-if="!allToolsSelected && mcpServers.length">
<div class="mcp-tools-toolbar">
<t-checkbox
v-for="t in s.tools"
:key="t.name"
:value="t.name"
:checked="allToolsChecked"
:indeterminate="allToolsIndeterminate"
:disabled="saving"
@change="toggleAllTools"
>
<span class="mcp-tool-label">{{ t.rawName }}</span>
<span v-if="t.description" class="mcp-tool-desc"> {{ t.description }}</span>
全选工具
</t-checkbox>
</div>
</t-checkbox-group>
<div class="mcp-tools-list">
<div v-for="s in mcpServers" :key="s.configId" class="mcp-server-group">
<div class="mcp-server-header">
<span class="mcp-server-name">{{ s.serverName }}</span>
<t-checkbox
:checked="isGroupAllChecked(s)"
:indeterminate="isGroupIndeterminate(s)"
:disabled="saving"
@change="(val) => toggleGroupAll(s, val)"
>
全选
</t-checkbox>
</div>
<t-checkbox
v-for="t in s.tools"
:key="t.name"
:checked="form.allowedMcpTools.includes(t.name)"
:disabled="saving"
@change="(val) => toggleTool(t.name, val)"
>
<span class="mcp-tool-label">{{ t.rawName }}</span>
<span v-if="t.description" class="mcp-tool-desc"> {{ t.description }}</span>
</t-checkbox>
</div>
</div>
</template>
<span v-else-if="!allToolsSelected && !mcpServers.length && !mcpLoading" class="desc-text">
暂无已接入的 MCP 工具请先在MCP 服务管理中配置并启用 Server
</span>
@ -230,12 +251,88 @@ const isDirty = computed(() => takeSnapshot() !== snapshot.value)
// ---- ----
const selectedRole = computed(() => roles.value.find(r => r.id === selectedId.value) ?? null)
// ["*"]
//
let lastToolsBeforeAll: string[] = []
// ["*"]
const allToolsSelected = computed<boolean>({
get: () => form.allowedMcpTools.includes('*'),
set: (val) => { form.allowedMcpTools = val ? ['*'] : [] },
set: (val) => {
if (val) {
lastToolsBeforeAll = form.allowedMcpTools.filter(t => t !== '*')
form.allowedMcpTools = ['*']
} else {
form.allowedMcpTools = [...lastToolsBeforeAll]
}
},
})
// ---- MCP ----
//
const allToolNames = computed<string[]>(() => {
const names = new Set<string>()
for (const s of mcpServers.value) {
for (const t of (s.tools || [])) {
names.add(t.name)
}
}
return [...names]
})
//
const allToolsChecked = computed<boolean>(() => {
const names = allToolNames.value
return names.length > 0 && names.every(n => form.allowedMcpTools.includes(n))
})
//
const allToolsIndeterminate = computed<boolean>(() => {
const names = allToolNames.value
const selected = names.filter(n => form.allowedMcpTools.includes(n)).length
return selected > 0 && selected < names.length
})
function toggleAllTools(val: boolean): void {
if (val) {
form.allowedMcpTools = [...new Set([...form.allowedMcpTools, ...allToolNames.value])]
} else {
const nameSet = new Set(allToolNames.value)
form.allowedMcpTools = form.allowedMcpTools.filter(n => !nameSet.has(n))
}
}
// Server
function isGroupAllChecked(group: any): boolean {
const tools = group.tools || []
return tools.length > 0 && tools.every((t: any) => form.allowedMcpTools.includes(t.name))
}
// Server
function isGroupIndeterminate(group: any): boolean {
const tools = group.tools || []
const selected = tools.filter((t: any) => form.allowedMcpTools.includes(t.name)).length
return selected > 0 && selected < tools.length
}
function toggleGroupAll(group: any, val: boolean): void {
const names = (group.tools || []).map((t: any) => t.name)
if (val) {
form.allowedMcpTools = [...new Set([...form.allowedMcpTools, ...names])]
} else {
const nameSet = new Set(names)
form.allowedMcpTools = form.allowedMcpTools.filter(n => !nameSet.has(n))
}
}
// /
function toggleTool(name: string, val: boolean): void {
if (val) {
form.allowedMcpTools = [...new Set([...form.allowedMcpTools, name])]
} else {
form.allowedMcpTools = form.allowedMcpTools.filter(n => n !== name)
}
}
// ---- ----
onMounted(() => {
reload()
@ -450,10 +547,16 @@ async function handleDelete(): Promise<void> {
</script>
<style scoped>
/* ===== 页级留白 ===== */
.role-page-card {
margin: 16px;
}
/* ===== 整体分栏 ===== */
.role-shell {
display: flex;
gap: 16px;
height: calc(100vh - 220px);
min-height: 520px;
}
@ -527,8 +630,13 @@ async function handleDelete(): Promise<void> {
}
.role-item-name {
flex: 1;
min-width: 0;
font-size: 13px;
color: var(--td-text-color-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.role-item-desc {
@ -548,13 +656,14 @@ async function handleDelete(): Promise<void> {
border: 1px solid var(--td-border-level-1-color);
border-radius: 10px;
padding: 20px 24px;
overflow-y: auto;
overflow: hidden;
}
.detail-header {
display: flex;
align-items: flex-start;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid var(--td-border-level-1-color);
@ -575,22 +684,43 @@ async function handleDelete(): Promise<void> {
.detail-form {
flex: 1;
overflow-y: auto;
}
.detail-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
flex-shrink: 0;
margin-top: 24px;
padding-top: 14px;
border-top: 1px solid var(--td-border-level-1-color);
}
/* ===== 表单分区标题 ===== */
.form-section-title {
margin: 8px 0 12px;
padding-left: 8px;
font-size: 13px;
font-weight: 600;
color: var(--td-text-color-primary);
border-left: 3px solid var(--td-brand-color);
line-height: 1.4;
}
/* ===== MCP 工具授权 ===== */
.mcp-tools-box {
width: 100%;
}
.mcp-tools-toolbar {
display: flex;
align-items: center;
margin-top: 8px;
padding-bottom: 8px;
border-bottom: 1px dashed var(--td-border-level-1-color);
}
.mcp-tools-list {
width: 100%;
display: flex;
@ -608,11 +738,17 @@ async function handleDelete(): Promise<void> {
border-radius: 8px;
}
.mcp-server-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2px;
}
.mcp-server-name {
font-size: 12px;
font-weight: 600;
color: var(--td-text-color-secondary);
margin-bottom: 2px;
}
.mcp-tool-label {
@ -635,12 +771,22 @@ async function handleDelete(): Promise<void> {
}
@media (max-width: 768px) {
.role-page-card {
margin: 12px;
}
.role-shell {
flex-direction: column;
height: auto;
}
.role-sidebar {
width: auto;
max-height: 260px;
}
.role-detail {
overflow: visible;
}
.detail-form {
overflow: visible;
}
}
</style>
Loading…
Cancel
Save