27aff59152
新增地区-知识库绑定管理功能,支持增删改查操作 - 添加 V3 API 路由层:area-datasets 相关接口 - 添加 API 客户端:area-datasets.ts - 添加自定义 Hook:use-area-dataset-config.ts - 添加管理组件:area-dataset-config.tsx - 修复路由冲突问题,删除重复的 .ts 路由文件 - 更新 dataset-manager 页面,添加 Tabs 导航 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
36 KiB
36 KiB
Dify 模块前端对接文档
版本:1.0 更新时间:2025-12-06 适用前端:Vue 3 + TypeScript
目录
概述
模块结构
AI法务助手 /chat-with-llm
├── 大模型对话 /chat-with-llm/chat
│ ├── 使用AI对话 (dify:chat:use)
│ ├── 查看对话历史 (dify:conversation:read)
│ ├── 删除对话 (dify:conversation:delete)
│ └── 消息反馈 (dify:message:feedback)
│
└── 知识库管理 /chat-with-llm/dataset-manager
├── 查看知识库 (dify:dataset:read)
├── 编辑知识库内容 (dify:dataset:write)
├── 管理知识库绑定 (dify:dataset:manage)
├── 下载文件 (dify:file:read)
└── 上传文件 (dify:file:upload)
角色权限矩阵
| 功能 | common (普通员工) | admin (市级管理员) | provincial_admin (省级管理员) |
|---|---|---|---|
| 大模型对话 | ✅ | ✅ | ✅ |
| 查看对话历史 | ✅ 仅自己 | ✅ 仅自己 | ✅ 仅自己 |
| 删除对话 | ✅ 仅自己 | ✅ 仅自己 | ✅ 仅自己 |
| 查看知识库 | ✅ 本地区+公共 | ✅ 本地区+公共 | ✅ 全部 |
| 编辑知识库内容 | ❌ | ✅ 仅本地区 | ✅ 全部 |
| 管理知识库绑定 | ❌ | ❌ | ✅ |
| 文件上传/下载 | ✅ | ✅ | ✅ |
权限体系
权限标识 (permission_key)
| 权限标识 | 显示名称 | HTTP方法 | API路径 |
|---|---|---|---|
dify:chat:use |
使用AI对话 | POST | /api/dify/chat/chat-messages |
dify:conversation:read |
查看对话历史 | GET | /api/dify/chat/conversations |
dify:conversation:delete |
删除对话 | DELETE | /api/dify/chat/conversations/{conversation_id} |
dify:message:feedback |
消息反馈 | POST | /api/dify/chat/messages/{message_id}/feedbacks |
dify:dataset:read |
查看知识库 | GET | /api/v3/dify/area-datasets/my |
dify:dataset:write |
编辑知识库内容 | POST | /api/dify/dataset/documents |
dify:dataset:manage |
管理知识库绑定 | POST | /api/v3/dify/area-datasets |
dify:file:read |
下载文件 | GET | /api/dify/file/{file_id}/file-preview |
dify:file:upload |
上传文件 | POST | /api/dify/chat/files/upload |
前端权限检查
// stores/permission.ts
import { defineStore } from 'pinia'
interface RoutePermission {
route_path: string
permissions: string[]
}
export const usePermissionStore = defineStore('permission', {
state: () => ({
routes: [] as RoutePermission[],
routesFlat: [] as RoutePermission[]
}),
actions: {
// 检查是否有某个权限
hasPermission(permissionKey: string): boolean {
return this.routesFlat.some(route =>
route.permissions.includes(permissionKey)
)
},
// 检查是否有某个路由下的权限
hasRoutePermission(routePath: string, permissionKey: string): boolean {
const route = this.routesFlat.find(r => r.route_path === routePath)
return route?.permissions.includes(permissionKey) ?? false
}
}
})
// 使用示例
const permissionStore = usePermissionStore()
// 检查是否可以编辑知识库
if (permissionStore.hasPermission('dify:dataset:write')) {
// 显示编辑按钮
}
// 检查是否可以管理知识库绑定
if (permissionStore.hasPermission('dify:dataset:manage')) {
// 显示管理按钮
}
API 接口清单
基础配置
// api/config.ts
const API_BASE = '/api'
export const DIFY_API = {
// 对话相关
CHAT_MESSAGES: `${API_BASE}/dify/chat/chat-messages`,
CONVERSATIONS: `${API_BASE}/dify/chat/conversations`,
CONVERSATION_MESSAGES: (id: string) => `${API_BASE}/dify/chat/conversations/${id}/messages`,
CONVERSATION_DELETE: (id: string) => `${API_BASE}/dify/chat/conversations/${id}`,
CONVERSATION_RENAME: (id: string) => `${API_BASE}/dify/chat/conversations/${id}/name`,
MESSAGE_FEEDBACK: (id: string) => `${API_BASE}/dify/chat/messages/${id}/feedbacks`,
SUGGESTED_QUESTIONS: (id: string) => `${API_BASE}/dify/chat/messages/${id}/suggested`,
// 文件相关
FILE_UPLOAD: `${API_BASE}/dify/chat/files/upload`,
FILE_PREVIEW: (id: string) => `${API_BASE}/dify/file/${id}/file-preview`,
// 知识库相关 (新增)
AREA_DATASETS_MY: `${API_BASE}/v3/dify/area-datasets/my`,
AREA_DATASETS: `${API_BASE}/v3/dify/area-datasets`,
AREA_DATASETS_DETAIL: (id: number) => `${API_BASE}/v3/dify/area-datasets/${id}`,
AREA_DATASETS_AREAS: `${API_BASE}/v3/dify/area-datasets/areas`,
AREA_DATASETS_CHECK: (datasetId: string) => `${API_BASE}/v3/dify/area-datasets/check/${datasetId}`,
}
大模型对话模块
1. 发送对话消息
权限要求: dify:chat:use
// api/dify/chat.ts
interface ChatMessageRequest {
query: string // 用户输入的问题
conversation_id?: string // 对话ID(首次对话不传)
response_mode?: 'streaming' | 'blocking' // 响应模式,默认 streaming
files?: Array<{
type: 'image' | 'document'
transfer_method: 'local_file'
upload_file_id: string
}>
}
interface ChatMessageResponse {
event: string
message_id: string
conversation_id: string
answer: string
created_at: number
}
// 流式对话(推荐)
export async function sendChatMessageStream(
data: ChatMessageRequest,
onMessage: (chunk: string) => void,
onDone: (response: ChatMessageResponse) => void,
onError: (error: Error) => void
) {
const response = await fetch(DIFY_API.CHAT_MESSAGES, {
method: 'POST',
headers: {
'Authorization': `Bearer ${getToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
...data,
response_mode: 'streaming'
})
})
if (!response.ok) {
if (response.status === 403) {
onError(new Error('权限不足:您没有使用AI对话的权限'))
return
}
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader!.read()
if (done) break
const chunk = decoder.decode(value)
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
if (data.event === 'message') {
onMessage(data.answer)
} else if (data.event === 'message_end') {
onDone(data)
} else if (data.event === 'error') {
onError(new Error(data.message))
}
}
}
}
}
// 阻塞式对话
export async function sendChatMessage(data: ChatMessageRequest): Promise<ChatMessageResponse> {
const response = await request.post(DIFY_API.CHAT_MESSAGES, {
...data,
response_mode: 'blocking'
})
return response.data
}
使用示例:
<template>
<div class="chat-container">
<div class="messages">
<div v-for="msg in messages" :key="msg.id" :class="msg.role">
{{ msg.content }}
</div>
</div>
<div class="input-area">
<input v-model="inputText" @keyup.enter="sendMessage" />
<button @click="sendMessage" :disabled="!canChat">发送</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { usePermissionStore } from '@/stores/permission'
import { sendChatMessageStream } from '@/api/dify/chat'
const permissionStore = usePermissionStore()
const canChat = computed(() => permissionStore.hasPermission('dify:chat:use'))
const inputText = ref('')
const messages = ref<Array<{ id: string, role: string, content: string }>>([])
const conversationId = ref<string>()
const currentAnswer = ref('')
async function sendMessage() {
if (!inputText.value.trim() || !canChat.value) return
const userMessage = inputText.value
inputText.value = ''
// 添加用户消息
messages.value.push({
id: Date.now().toString(),
role: 'user',
content: userMessage
})
// 添加AI消息占位
const aiMessageId = Date.now().toString() + '_ai'
messages.value.push({
id: aiMessageId,
role: 'assistant',
content: ''
})
currentAnswer.value = ''
await sendChatMessageStream(
{
query: userMessage,
conversation_id: conversationId.value
},
// onMessage - 流式接收
(chunk) => {
currentAnswer.value += chunk
const aiMsg = messages.value.find(m => m.id === aiMessageId)
if (aiMsg) aiMsg.content = currentAnswer.value
},
// onDone - 完成
(response) => {
conversationId.value = response.conversation_id
},
// onError - 错误
(error) => {
console.error('对话失败:', error)
ElMessage.error(error.message)
}
)
}
</script>
2. 获取对话历史列表
权限要求: dify:conversation:read
interface Conversation {
id: string
name: string
created_at: number
updated_at: number
}
interface ConversationListResponse {
data: Conversation[]
has_more: boolean
limit: number
}
export async function getConversations(
limit: number = 20,
last_id?: string
): Promise<ConversationListResponse> {
const params = new URLSearchParams({ limit: limit.toString() })
if (last_id) params.append('last_id', last_id)
const response = await request.get(`${DIFY_API.CONVERSATIONS}?${params}`)
return response.data
}
3. 获取对话消息详情
权限要求: dify:conversation:read
interface Message {
id: string
conversation_id: string
query: string
answer: string
created_at: number
feedback?: {
rating: 'like' | 'dislike'
}
}
interface MessageListResponse {
data: Message[]
has_more: boolean
limit: number
}
export async function getConversationMessages(
conversationId: string,
limit: number = 20,
first_id?: string
): Promise<MessageListResponse> {
const params = new URLSearchParams({ limit: limit.toString() })
if (first_id) params.append('first_id', first_id)
const response = await request.get(
`${DIFY_API.CONVERSATION_MESSAGES(conversationId)}?${params}`
)
return response.data
}
4. 删除对话
权限要求: dify:conversation:delete
export async function deleteConversation(conversationId: string): Promise<void> {
await request.delete(DIFY_API.CONVERSATION_DELETE(conversationId))
}
使用示例:
<template>
<div class="conversation-item">
<span>{{ conversation.name }}</span>
<button
v-if="canDelete"
@click="handleDelete(conversation.id)"
>
删除
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { usePermissionStore } from '@/stores/permission'
import { deleteConversation } from '@/api/dify/chat'
const permissionStore = usePermissionStore()
const canDelete = computed(() =>
permissionStore.hasPermission('dify:conversation:delete')
)
async function handleDelete(id: string) {
try {
await ElMessageBox.confirm('确定要删除这个对话吗?', '确认删除')
await deleteConversation(id)
ElMessage.success('删除成功')
emit('refresh')
} catch (error: any) {
if (error.response?.status === 403) {
ElMessage.error('您没有删除对话的权限')
} else if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
</script>
5. 消息反馈(点赞/点踩)
权限要求: dify:message:feedback
interface FeedbackRequest {
rating: 'like' | 'dislike' | null // null 表示取消反馈
content?: string // 反馈内容(可选)
}
export async function submitMessageFeedback(
messageId: string,
feedback: FeedbackRequest
): Promise<void> {
await request.post(DIFY_API.MESSAGE_FEEDBACK(messageId), feedback)
}
6. 上传文件
权限要求: dify:file:upload
interface FileUploadResponse {
id: string
name: string
size: number
extension: string
mime_type: string
created_at: number
}
export async function uploadFile(file: File): Promise<FileUploadResponse> {
const formData = new FormData()
formData.append('file', file)
const response = await request.post(DIFY_API.FILE_UPLOAD, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
return response.data
}
知识库管理模块
1. 获取当前用户可访问的知识库列表
权限要求: dify:dataset:read
接口说明: 根据用户角色自动返回对应数据范围
common/admin: 本地区 + 公共知识库provincial_admin: 全部知识库
interface AreaDataset {
id: number
area: string // 地区:梅州、云浮、揭阳、潮州、省级
dataset_id: string // Dify 知识库 ID
dataset_name: string // 知识库名称
dataset_description?: string // 知识库描述
is_default: boolean // 是否为该地区默认知识库
is_public: boolean // 是否公开(省级公共知识库)
sort_order: number // 排序顺序
status: number // 状态:1=启用, 0=禁用
created_at: string
updated_at: string
}
interface MyDatasetsResponse {
data: AreaDataset[]
total: number
user_area: string // 当前用户所属地区
user_role: string // 当前用户角色
}
export async function getMyDatasets(): Promise<MyDatasetsResponse> {
const response = await request.get(DIFY_API.AREA_DATASETS_MY)
return response.data
}
使用示例:
<template>
<div class="dataset-list">
<div class="header">
<h3>我的知识库</h3>
<span class="info">
地区: {{ userArea }} | 角色: {{ userRoleLabel }}
</span>
</div>
<div class="datasets">
<div
v-for="dataset in datasets"
:key="dataset.id"
class="dataset-card"
:class="{ 'is-public': dataset.is_public }"
>
<div class="name">
{{ dataset.dataset_name }}
<el-tag v-if="dataset.is_public" type="success" size="small">
公共
</el-tag>
<el-tag v-if="dataset.is_default" type="primary" size="small">
默认
</el-tag>
</div>
<div class="area">{{ dataset.area }}</div>
<div class="description">{{ dataset.dataset_description }}</div>
<div class="actions">
<!-- 编辑按钮:需要 dify:dataset:write 权限 -->
<el-button
v-if="canEdit"
size="small"
@click="handleEdit(dataset)"
>
编辑
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { usePermissionStore } from '@/stores/permission'
import { getMyDatasets } from '@/api/dify/dataset'
const permissionStore = usePermissionStore()
const canEdit = computed(() =>
permissionStore.hasPermission('dify:dataset:write')
)
const datasets = ref<AreaDataset[]>([])
const userArea = ref('')
const userRole = ref('')
const userRoleLabel = computed(() => {
const labels: Record<string, string> = {
'common': '普通员工',
'admin': '市级管理员',
'provincial_admin': '省级管理员'
}
return labels[userRole.value] || userRole.value
})
onMounted(async () => {
try {
const res = await getMyDatasets()
datasets.value = res.data
userArea.value = res.user_area
userRole.value = res.user_role
} catch (error: any) {
if (error.response?.status === 403) {
ElMessage.error('您没有查看知识库的权限')
}
}
})
</script>
2. 获取所有知识库绑定列表(管理员)
权限要求: dify:dataset:manage (仅省级管理员)
interface DatasetListResponse {
data: AreaDataset[]
total: number
page: number
page_size: number
has_more: boolean
}
interface DatasetListParams {
area?: string // 筛选地区
only_enabled?: boolean // 是否只返回启用的,默认 true
page?: number // 页码,默认 1
page_size?: number // 每页数量,默认 20
}
export async function getAllDatasets(
params: DatasetListParams = {}
): Promise<DatasetListResponse> {
const response = await request.get(DIFY_API.AREA_DATASETS, { params })
return response.data
}
3. 获取可用地区列表
权限要求: dify:dataset:manage
export async function getAvailableAreas(): Promise<string[]> {
const response = await request.get(DIFY_API.AREA_DATASETS_AREAS)
return response.data.data
}
4. 创建知识库绑定
权限要求: dify:dataset:manage (仅省级管理员)
interface CreateDatasetRequest {
area: string // 地区名称
dataset_id: string // Dify 知识库 ID
dataset_name: string // 知识库名称
dataset_description?: string // 描述
is_default?: boolean // 是否默认,默认 false
is_public?: boolean // 是否公开,默认 false
sort_order?: number // 排序,默认 0
}
export async function createDatasetBinding(
data: CreateDatasetRequest
): Promise<AreaDataset> {
const response = await request.post(DIFY_API.AREA_DATASETS, data)
return response.data.data
}
5. 更新知识库绑定
权限要求: dify:dataset:manage (仅省级管理员)
interface UpdateDatasetRequest {
dataset_name?: string
dataset_description?: string
is_default?: boolean
is_public?: boolean
sort_order?: number
status?: number // 1=启用, 0=禁用
}
export async function updateDatasetBinding(
id: number,
data: UpdateDatasetRequest
): Promise<AreaDataset> {
const response = await request.put(DIFY_API.AREA_DATASETS_DETAIL(id), data)
return response.data.data
}
6. 删除知识库绑定
权限要求: dify:dataset:manage (仅省级管理员)
export async function deleteDatasetBinding(id: number): Promise<void> {
await request.delete(DIFY_API.AREA_DATASETS_DETAIL(id))
}
7. 检查知识库访问权限
权限要求: dify:dataset:read
interface CheckAccessResponse {
has_access: boolean
user_area: string
dataset_id: string
}
export async function checkDatasetAccess(
datasetId: string
): Promise<CheckAccessResponse> {
const response = await request.get(DIFY_API.AREA_DATASETS_CHECK(datasetId))
return response.data
}
使用示例:
// 在访问 Dify 知识库详情之前检查权限
async function viewDatasetDetail(datasetId: string) {
try {
const { has_access } = await checkDatasetAccess(datasetId)
if (!has_access) {
ElMessage.warning('您没有访问该知识库的权限')
return
}
// 继续访问知识库详情...
router.push(`/dataset/${datasetId}`)
} catch (error) {
ElMessage.error('权限检查失败')
}
}
权限控制实现
1. 获取用户权限
登录后调用 /user/routes 获取用户权限:
// api/auth.ts
interface UserRoutesResponse {
user_id: number
username: string
routes: RouteInfo[]
routes_flat: RouteInfo[]
}
export async function getUserRoutes(): Promise<UserRoutesResponse> {
const response = await request.get('/user/routes')
return response.data
}
// 登录后初始化权限
async function initPermissions() {
const data = await getUserRoutes()
const permissionStore = usePermissionStore()
permissionStore.setRoutes(data.routes, data.routes_flat)
}
2. 路由守卫
// router/guards.ts
import { usePermissionStore } from '@/stores/permission'
router.beforeEach(async (to, from, next) => {
const permissionStore = usePermissionStore()
// 检查是否有该路由的访问权限
const hasAccess = permissionStore.routesFlat.some(
route => route.route_path === to.path
)
if (!hasAccess && to.path !== '/403') {
next('/403')
return
}
next()
})
3. 按钮级权限控制
// directives/permission.ts
import { usePermissionStore } from '@/stores/permission'
export const vPermission = {
mounted(el: HTMLElement, binding: { value: string }) {
const permissionStore = usePermissionStore()
const permissionKey = binding.value
if (!permissionStore.hasPermission(permissionKey)) {
el.parentNode?.removeChild(el)
}
}
}
// main.ts
app.directive('permission', vPermission)
使用示例:
<template>
<!-- 只有拥有 dify:dataset:write 权限的用户才能看到编辑按钮 -->
<el-button v-permission="'dify:dataset:write'">编辑</el-button>
<!-- 只有省级管理员才能看到管理按钮 -->
<el-button v-permission="'dify:dataset:manage'">管理绑定</el-button>
</template>
4. 组合式权限 Hook
// composables/usePermission.ts
import { computed } from 'vue'
import { usePermissionStore } from '@/stores/permission'
export function usePermission() {
const store = usePermissionStore()
// Dify 对话权限
const canChat = computed(() => store.hasPermission('dify:chat:use'))
const canViewHistory = computed(() => store.hasPermission('dify:conversation:read'))
const canDeleteConversation = computed(() => store.hasPermission('dify:conversation:delete'))
const canFeedback = computed(() => store.hasPermission('dify:message:feedback'))
// Dify 知识库权限
const canViewDataset = computed(() => store.hasPermission('dify:dataset:read'))
const canEditDataset = computed(() => store.hasPermission('dify:dataset:write'))
const canManageDataset = computed(() => store.hasPermission('dify:dataset:manage'))
// Dify 文件权限
const canDownloadFile = computed(() => store.hasPermission('dify:file:read'))
const canUploadFile = computed(() => store.hasPermission('dify:file:upload'))
return {
// 检查任意权限
hasPermission: (key: string) => store.hasPermission(key),
// 对话权限
canChat,
canViewHistory,
canDeleteConversation,
canFeedback,
// 知识库权限
canViewDataset,
canEditDataset,
canManageDataset,
// 文件权限
canDownloadFile,
canUploadFile
}
}
使用示例:
<template>
<div class="chat-page">
<!-- 对话输入框 -->
<div v-if="canChat" class="chat-input">
<input v-model="message" />
<el-button @click="send">发送</el-button>
</div>
<div v-else class="no-permission">
您没有使用AI对话的权限
</div>
<!-- 对话历史 -->
<div v-if="canViewHistory" class="history">
<div v-for="conv in conversations" :key="conv.id">
{{ conv.name }}
<el-button
v-if="canDeleteConversation"
@click="deleteConv(conv.id)"
>
删除
</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { usePermission } from '@/composables/usePermission'
const {
canChat,
canViewHistory,
canDeleteConversation
} = usePermission()
</script>
错误处理
HTTP 状态码
| 状态码 | 说明 | 处理方式 |
|---|---|---|
| 200 | 成功 | 正常处理 |
| 400 | 请求参数错误 | 显示错误信息 |
| 401 | 未认证 | 跳转登录页 |
| 403 | 权限不足 | 显示权限提示 |
| 404 | 资源不存在 | 显示不存在提示 |
| 500 | 服务器错误 | 显示系统错误提示 |
统一错误处理
// utils/request.ts
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 30000
})
// 请求拦截器
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
request.interceptors.response.use(
response => response,
error => {
const { response } = error
if (response) {
switch (response.status) {
case 401:
ElMessage.error('登录已过期,请重新登录')
localStorage.removeItem('token')
router.push('/login')
break
case 403:
// 权限不足
const errorData = response.data
ElMessage.error(errorData?.detail || '权限不足,无法执行此操作')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器错误,请稍后重试')
break
default:
ElMessage.error(response.data?.message || '请求失败')
}
} else {
ElMessage.error('网络错误,请检查网络连接')
}
return Promise.reject(error)
}
)
export default request
权限错误特殊处理
// 403 错误的详细信息格式
interface PermissionError {
error: string // 如 "权限不足: dify:dataset:manage"
detail: string // 如 "您没有执行此操作的权限,请联系管理员"
}
// 解析权限错误
function parsePermissionError(error: any): string {
if (error.response?.status === 403) {
const data = error.response.data as PermissionError
// 提取权限标识
const match = data.error?.match(/权限不足: (.+)/)
if (match) {
const permissionKey = match[1]
const permissionLabels: Record<string, string> = {
'dify:chat:use': '使用AI对话',
'dify:conversation:read': '查看对话历史',
'dify:conversation:delete': '删除对话',
'dify:dataset:read': '查看知识库',
'dify:dataset:write': '编辑知识库',
'dify:dataset:manage': '管理知识库绑定',
'dify:file:read': '下载文件',
'dify:file:upload': '上传文件'
}
return `您没有"${permissionLabels[permissionKey] || permissionKey}"的权限`
}
return data.detail || '权限不足'
}
return '操作失败'
}
完整代码示例
知识库管理页面完整实现
<!-- views/dataset-manager/Index.vue -->
<template>
<div class="dataset-manager">
<el-card>
<template #header>
<div class="card-header">
<span>知识库管理</span>
<div class="header-info">
<el-tag>地区: {{ userArea }}</el-tag>
<el-tag type="info">{{ userRoleLabel }}</el-tag>
</div>
<!-- 只有省级管理员可以新增绑定 -->
<el-button
v-if="canManageDataset"
type="primary"
@click="handleCreate"
>
新增绑定
</el-button>
</div>
</template>
<!-- 筛选区域(仅省级管理员可见) -->
<div v-if="canManageDataset" class="filter-area">
<el-select v-model="filterArea" placeholder="选择地区" clearable>
<el-option
v-for="area in areas"
:key="area"
:label="area"
:value="area"
/>
</el-select>
<el-button @click="loadDatasets">查询</el-button>
</div>
<!-- 知识库列表 -->
<el-table :data="datasets" v-loading="loading">
<el-table-column prop="area" label="地区" width="100" />
<el-table-column prop="dataset_name" label="知识库名称" />
<el-table-column prop="dataset_description" label="描述" />
<el-table-column label="标签" width="150">
<template #default="{ row }">
<el-tag v-if="row.is_public" type="success" size="small">
公共
</el-tag>
<el-tag v-if="row.is_default" type="primary" size="small">
默认
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" v-if="canEditDataset">
<template #default="{ row }">
<el-button
v-if="canEditDataset"
size="small"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-if="canManageDataset"
size="small"
type="danger"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页(仅管理员视图) -->
<el-pagination
v-if="canManageDataset"
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
@current-change="loadDatasets"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="editingId ? '编辑知识库绑定' : '新增知识库绑定'"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<el-form-item label="地区" prop="area">
<el-select v-model="form.area" :disabled="!!editingId">
<el-option
v-for="area in areas"
:key="area"
:label="area"
:value="area"
/>
</el-select>
</el-form-item>
<el-form-item label="Dify知识库ID" prop="dataset_id">
<el-input v-model="form.dataset_id" :disabled="!!editingId" />
</el-form-item>
<el-form-item label="知识库名称" prop="dataset_name">
<el-input v-model="form.dataset_name" />
</el-form-item>
<el-form-item label="描述" prop="dataset_description">
<el-input v-model="form.dataset_description" type="textarea" />
</el-form-item>
<el-form-item label="是否公开">
<el-switch v-model="form.is_public" />
<span class="form-tip">公开后所有地区用户可见</span>
</el-form-item>
<el-form-item label="是否默认">
<el-switch v-model="form.is_default" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort_order" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { usePermission } from '@/composables/usePermission'
import {
getMyDatasets,
getAllDatasets,
getAvailableAreas,
createDatasetBinding,
updateDatasetBinding,
deleteDatasetBinding,
type AreaDataset
} from '@/api/dify/dataset'
// 权限
const { canViewDataset, canEditDataset, canManageDataset } = usePermission()
// 数据
const loading = ref(false)
const datasets = ref<AreaDataset[]>([])
const areas = ref<string[]>([])
const userArea = ref('')
const userRole = ref('')
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const filterArea = ref('')
// 表单
const dialogVisible = ref(false)
const editingId = ref<number | null>(null)
const submitting = ref(false)
const formRef = ref()
const form = ref({
area: '',
dataset_id: '',
dataset_name: '',
dataset_description: '',
is_public: false,
is_default: false,
sort_order: 0
})
const rules = {
area: [{ required: true, message: '请选择地区', trigger: 'change' }],
dataset_id: [{ required: true, message: '请输入Dify知识库ID', trigger: 'blur' }],
dataset_name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }]
}
const userRoleLabel = computed(() => {
const labels: Record<string, string> = {
'common': '普通员工',
'admin': '市级管理员',
'provincial_admin': '省级管理员'
}
return labels[userRole.value] || userRole.value
})
// 加载数据
async function loadDatasets() {
loading.value = true
try {
if (canManageDataset.value) {
// 省级管理员:获取全部
const res = await getAllDatasets({
area: filterArea.value || undefined,
page: page.value,
page_size: pageSize.value
})
datasets.value = res.data
total.value = res.total
} else {
// 其他角色:获取自己可访问的
const res = await getMyDatasets()
datasets.value = res.data
userArea.value = res.user_area
userRole.value = res.user_role
total.value = res.total
}
} catch (error) {
console.error('加载知识库失败:', error)
} finally {
loading.value = false
}
}
// 加载地区列表
async function loadAreas() {
if (canManageDataset.value) {
try {
areas.value = await getAvailableAreas()
} catch (error) {
console.error('加载地区失败:', error)
}
}
}
// 新增
function handleCreate() {
editingId.value = null
form.value = {
area: '',
dataset_id: '',
dataset_name: '',
dataset_description: '',
is_public: false,
is_default: false,
sort_order: 0
}
dialogVisible.value = true
}
// 编辑
function handleEdit(row: AreaDataset) {
editingId.value = row.id
form.value = {
area: row.area,
dataset_id: row.dataset_id,
dataset_name: row.dataset_name,
dataset_description: row.dataset_description || '',
is_public: row.is_public,
is_default: row.is_default,
sort_order: row.sort_order
}
dialogVisible.value = true
}
// 删除
async function handleDelete(row: AreaDataset) {
try {
await ElMessageBox.confirm(
`确定要删除知识库绑定「${row.dataset_name}」吗?`,
'确认删除'
)
await deleteDatasetBinding(row.id)
ElMessage.success('删除成功')
loadDatasets()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
async function handleSubmit() {
try {
await formRef.value?.validate()
submitting.value = true
if (editingId.value) {
await updateDatasetBinding(editingId.value, {
dataset_name: form.value.dataset_name,
dataset_description: form.value.dataset_description,
is_public: form.value.is_public,
is_default: form.value.is_default,
sort_order: form.value.sort_order
})
ElMessage.success('更新成功')
} else {
await createDatasetBinding(form.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false
loadDatasets()
} catch (error: any) {
if (error.response?.status === 403) {
ElMessage.error('权限不足')
} else if (error.response?.status === 400) {
ElMessage.error(error.response.data?.message || '操作失败')
}
} finally {
submitting.value = false
}
}
// 初始化
onMounted(() => {
loadDatasets()
loadAreas()
})
</script>
<style scoped lang="scss">
.dataset-manager {
padding: 20px;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.header-info {
display: flex;
gap: 10px;
}
}
.filter-area {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.form-tip {
margin-left: 10px;
color: #909399;
font-size: 12px;
}
}
</style>
更新日志
| 版本 | 日期 | 说明 |
|---|---|---|
| 1.0 | 2025-12-06 | 初始版本,包含完整的前端对接说明 |