本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

89 lines
5.3 KiB

<template>
<t-card title="🎭 客服角色管理" :bordered="false">
<p class="desc-text">为角色设定人设并授权可检索的知识库分类对话页只能选择这里配置好的角色检索范围由服务端按角色强制限定</p>
<!-- 新增/编辑表单 -->
<div class="card-box form-bar">
<t-row :gutter="12">
<t-col :span="4"><t-input v-model="form.name" placeholder="角色名称 *" size="small" /></t-col>
<t-col :span="4"><t-input v-model="form.description" placeholder="职责描述(可选)" size="small" /></t-col>
<t-col :span="2"><t-checkbox v-model="form.enabled">启用</t-checkbox></t-col>
</t-row>
<t-textarea v-model="form.prompt" :autosize="{minRows:2,maxRows:3}" placeholder="角色人设/风格提示词" style="margin-top:8px;" />
<t-space style="margin-top:8px;">
<t-button theme="success" size="small" @click="submit">{{ editingId?'保存修改':'新增角色' }}</t-button>
<t-button v-if="editingId" variant="outline" size="small" @click="resetForm">取消编辑</t-button>
<t-button variant="outline" size="small" @click="reload">刷新</t-button>
</t-space>
</div>
<!-- 角色列表 -->
<t-empty v-if="!roles.length" description="暂无角色" />
<div v-else>
<div v-for="role in roles" :key="role.id" class="role-card" :style="{opacity:role.enabled?'1':'.6'}">
<div class="role-header">
<div>
<strong class="role-name">{{ role.name }}</strong>
<t-tag v-if="!role.enabled" size="small" theme="danger" variant="light" style="margin-left:6px;">已停用</t-tag>
<div class="role-desc">{{ role.description || '—' }}</div>
</div>
<t-space :size="4">
<t-button size="small" variant="text" @click="startEdit(role)">编辑</t-button>
<t-button size="small" variant="text" theme="danger" @click="remove(role)">删除</t-button>
</t-space>
</div>
<div v-if="role.prompt" class="role-prompt">人设:{{ role.prompt }}</div>
<div class="role-categories">
<span class="cat-label">可检索知识库:</span>
<t-checkbox v-for="c in categoryStore.categories" :key="c.id"
:checked="role.categoryIds.includes(String(c.id))"
@change="toggleCategory(role, String(c.id), ($event as any))">
{{ c.name }}
</t-checkbox>
<span v-if="!categoryStore.categories.length" class="cat-hint">暂无知识库分类</span>
</div>
</div>
</div>
</t-card>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useCategoryStore } from '@/stores/category'
import { getAllRoles, createRole, updateRole, deleteRole, updateRoleCategories } from '@/api/role'
import { toast } from '@/utils/toast'
import { useConfirm } from '@/composables/useConfirm'
const { confirm } = useConfirm()
const categoryStore = useCategoryStore()
const roles = ref<any[]>([])
const editingId = ref('')
const form = reactive({name:'',description:'',prompt:'',enabled:true})
onMounted(()=>{reload();categoryStore.loadCategories()})
async function reload(){try{const r=await getAllRoles();if(r.success)roles.value=(r.data||[]).map((ro:any)=>({...ro,categoryIds:(ro.categoryIds||[]).map(String)}))}catch(e:any){toast('加载失败:'+e.message,'error')}}
function startEdit(role:any){editingId.value=role.id;form.name=role.name;form.description=role.description||'';form.prompt=role.prompt||'';form.enabled=role.enabled}
function resetForm(){editingId.value='';form.name='';form.description='';form.prompt='';form.enabled=true}
async function submit(){
if(!form.name.trim()){toast('请输入角色名称','error');return}
const d:any={name:form.name.trim(),description:form.description.trim(),prompt:form.prompt,enabled:form.enabled}
try{let r;if(editingId.value)r=await updateRole(editingId.value,d);else r=await createRole(d)
if(r.success){toast(editingId.value?'修改成功':'新增成功','success');resetForm();reload()}else toast(r.message||'操作失败','error')}catch(e:any){toast('操作失败:'+e.message,'error')}}
async function remove(role:any){if(!await confirm(`确定删除「${role.name}」?`))return
try{const r=await deleteRole(role.id);if(r.success){toast('删除成功','success');reload()}else toast(r.message||'删除失败','error')}catch(e:any){toast('删除失败:'+e.message,'error')}}
async function toggleCategory(role:any, catId:string, checked:boolean){
const ids=new Set(role.categoryIds);if(checked)ids.add(catId);else ids.delete(catId)
try{const r=await updateRoleCategories(role.id,Array.from(ids));if(r.success){role.categoryIds=Array.from(ids)}else toast(r.message||'授权失败','error')}catch(e:any){toast('授权失败:'+e.message,'error')}}
</script>
<style scoped>
.role-card{padding:12px;border:1px solid #e7e7e7;border-radius:8px;margin-bottom:10px;}
.role-header{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;}.role-name{font-size:14px;}.role-desc{font-size:12px;color:#999;margin-top:2px;}
.role-prompt{font-size:12px;color:#333;margin-top:6px;background:#f9fafb;border-radius:6px;padding:6px 8px;}
.role-categories{margin-top:8px;font-size:12px;display:flex;flex-wrap:wrap;gap:4px 12px;align-items:center;}.cat-label{color:#999;}.cat-hint{color:#999;}
</style>