feat(admin): knowledge iteration + ragflow ingestion views
新增 KnowledgeIteration/RagflowIngestion 视图; api/troubleshooting/admin store 适配; 锁定 pnpm-lock.yaml。
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
*.log
|
||||||
@@ -16,7 +16,7 @@ WORKDIR /app
|
|||||||
COPY package.json pnpm-lock.yaml* ./
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
# 复制源码(后续通过 volume mount 更新)
|
# 复制源码(后续通过 volume mount 更新)
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "企微智能IT支持服务台 - 管理后台前端",
|
"description": "企微智能IT支持服务台 - 管理后台前端",
|
||||||
"engines": { "node": ">=20.0.0 <21.0.0", "pnpm": ">=9.0.0" },
|
"engines": {
|
||||||
|
"node": ">=20.0.0 <21.0.0",
|
||||||
|
"pnpm": ">=9.0.0"
|
||||||
|
},
|
||||||
"packageManager": "pnpm@9.15.0",
|
"packageManager": "pnpm@9.15.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -20,6 +23,7 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.0",
|
"@element-plus/icons-vue": "^2.3.0",
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.7.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
"element-plus": "^2.7.0",
|
"element-plus": "^2.7.0",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
|
|||||||
Generated
+2160
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,7 @@ apiClient.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 业务成功:Scheme A — 直接返回 inner data(三端统一契约)
|
// 业务成功:Scheme A — 直接返回 inner data(三端统一契约)
|
||||||
return res.data
|
return res.data as any
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// 网络错误或服务器错误(HTTP 状态码非 2xx)— 统一 reject {code, message}(CTRT-03)
|
// 网络错误或服务器错误(HTTP 状态码非 2xx)— 统一 reject {code, message}(CTRT-03)
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export async function updateTemplate(
|
|||||||
id: string,
|
id: string,
|
||||||
data: TroubleshootingTemplate
|
data: TroubleshootingTemplate
|
||||||
): Promise<TroubleshootingTemplate> {
|
): Promise<TroubleshootingTemplate> {
|
||||||
return await apiClient.put<TroubleshootingTemplate>(`/troubleshooting-templates/${id}`, data)
|
return await apiClient.put(`/troubleshooting-templates/${id}`, data) as any
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DELETE /api/troubleshooting-templates/{id} — 删除模板 */
|
/** DELETE /api/troubleshooting-templates/{id} — 删除模板 */
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const useAdminStore = defineStore('admin', () => {
|
|||||||
logging.value = true
|
logging.value = true
|
||||||
try {
|
try {
|
||||||
// 注意:后端 AgentLogin Schema 要求必填字段 name
|
// 注意:后端 AgentLogin Schema 要求必填字段 name
|
||||||
const response = await apiClient.post('/agents/login', {
|
const response: any = await apiClient.post('/agents/login', {
|
||||||
user_id: inputUserId,
|
user_id: inputUserId,
|
||||||
name: inputUserId, // 后端必填字段,使用 user_id 作为默认值
|
name: inputUserId, // 后端必填字段,使用 user_id 作为默认值
|
||||||
password: password,
|
password: password,
|
||||||
@@ -144,8 +144,8 @@ export const useAdminStore = defineStore('admin', () => {
|
|||||||
async function refreshAdminInfo(): Promise<void> {
|
async function refreshAdminInfo(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!token.value) return
|
if (!token.value) return
|
||||||
const response = await apiClient.get('/agents/me')
|
const response: any = await apiClient.get('/agents/me')
|
||||||
adminInfo.value = response
|
adminInfo.value = response as any
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取管理员信息失败:', error)
|
console.error('获取管理员信息失败:', error)
|
||||||
// 如果是 401 未授权,说明 token 过期,需要重新登录
|
// 如果是 401 未授权,说明 token 过期,需要重新登录
|
||||||
|
|||||||
@@ -177,14 +177,14 @@ onMounted(async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await getDashboardOverview()
|
const response = await getDashboardOverview()
|
||||||
const data = response.data.data
|
// 拦截器已解包到 inner data,response 直接就是 {online_agents, today_conversations, ...}
|
||||||
Object.assign(overview, data)
|
Object.assign(overview, response)
|
||||||
|
|
||||||
// 构建待处理事项
|
// 构建待处理事项
|
||||||
buildPendingItems(data.system_alerts || [])
|
buildPendingItems(response.system_alerts || [])
|
||||||
|
|
||||||
// 构建系统健康
|
// 构建系统健康
|
||||||
buildHealthItems(data.integrations_health || [])
|
buildHealthItems(response.integrations_health || [])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载仪表盘数据失败:', error)
|
console.error('加载仪表盘数据失败:', error)
|
||||||
// 使用默认 demo 数据
|
// 使用默认 demo 数据
|
||||||
|
|||||||
@@ -0,0 +1,981 @@
|
|||||||
|
<!-- =============================================================================
|
||||||
|
企微IT智能服务台 — 管理后台 知识迭代提案管理页(Tier1 / P0-6 / P2-1)
|
||||||
|
=============================================================================
|
||||||
|
说明:管理后台的知识迭代提案管理页面,支持:
|
||||||
|
- 提案列表(含筛选/分页)
|
||||||
|
- 审阅提案详情
|
||||||
|
- 图字段编辑(issue/action/relation/parent)
|
||||||
|
- 训练师手动录入入口(通道 B)
|
||||||
|
|
||||||
|
D7 硬约束:
|
||||||
|
- 内联审批 + 独立队列
|
||||||
|
- 提案默认 pending
|
||||||
|
============================================================================= -->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="knowledge-iteration-page">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="knowledge-iteration-page__header">
|
||||||
|
<h2>📚 知识迭代管理</h2>
|
||||||
|
<div class="knowledge-iteration-page__header-actions">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button :type="viewMode === 'list' ? 'primary' : ''" @click="viewMode = 'list'">
|
||||||
|
列表
|
||||||
|
</el-button>
|
||||||
|
<el-button :type="viewMode === 'graph' ? 'primary' : ''" @click="switchToGraph">
|
||||||
|
图谱
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
<el-button type="primary" @click="showCreateDialog = true">
|
||||||
|
+ 手动录入(通道 B)
|
||||||
|
</el-button>
|
||||||
|
<el-button :loading="analyzing" @click="triggerAnalyze">
|
||||||
|
触发分析
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计概要 -->
|
||||||
|
<div class="knowledge-iteration-page__stats-bar">
|
||||||
|
<div
|
||||||
|
v-for="stat in statsItems"
|
||||||
|
:key="stat.key"
|
||||||
|
class="knowledge-iteration-page__stat-item"
|
||||||
|
>
|
||||||
|
<span class="knowledge-iteration-page__stat-value">{{ stat.value }}</span>
|
||||||
|
<span class="knowledge-iteration-page__stat-label">{{ stat.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== 列表视图 ===== -->
|
||||||
|
<template v-if="viewMode === 'list'">
|
||||||
|
|
||||||
|
<!-- 筛选栏 -->
|
||||||
|
<div class="knowledge-iteration-page__filters">
|
||||||
|
<el-select
|
||||||
|
v-model="filterStatus"
|
||||||
|
placeholder="状态"
|
||||||
|
clearable
|
||||||
|
size="default"
|
||||||
|
style="width: 140px"
|
||||||
|
@change="fetchSuggestions"
|
||||||
|
>
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="待审核" value="pending" />
|
||||||
|
<el-option label="队列中" value="queued" />
|
||||||
|
<el-option label="已通过" value="approved" />
|
||||||
|
<el-option label="已驳回" value="rejected" />
|
||||||
|
<el-option label="已应用" value="applied" />
|
||||||
|
<el-option label="已同步" value="graph_synced" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-select
|
||||||
|
v-model="filterAudience"
|
||||||
|
placeholder="受众"
|
||||||
|
clearable
|
||||||
|
size="default"
|
||||||
|
style="width: 160px"
|
||||||
|
@change="fetchSuggestions"
|
||||||
|
>
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="员工快捷回复" value="employee_quick_reply" />
|
||||||
|
<el-option label="工程师作业指导" value="engineer_workguide" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-input
|
||||||
|
v-model="filterConfMin"
|
||||||
|
placeholder="置信度下限"
|
||||||
|
size="default"
|
||||||
|
type="number"
|
||||||
|
style="width: 120px"
|
||||||
|
@change="fetchSuggestions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-input
|
||||||
|
v-model="filterConfMax"
|
||||||
|
placeholder="置信度上限"
|
||||||
|
size="default"
|
||||||
|
type="number"
|
||||||
|
style="width: 120px"
|
||||||
|
@change="fetchSuggestions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-button :icon="'Refresh'" @click="fetchSuggestions">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提案表格 -->
|
||||||
|
<div class="knowledge-iteration-page__table" v-loading="loading">
|
||||||
|
<el-table :data="suggestions" stripe size="default">
|
||||||
|
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="suggestion_type" label="类型" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.suggestion_type === 'new_faq' ? 'success' : 'warning'" size="small">
|
||||||
|
{{ row.suggestion_type === 'new_faq' ? '新FAQ' : '更新' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusTagType(row.status)" size="small">
|
||||||
|
{{ statusLabel(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="confidence" label="置信度" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :style="{ color: confidenceColor(row.confidence) }">
|
||||||
|
{{ confidencePercent(row.confidence) }}%
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="audience" label="受众" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ audienceLabel(row.audience) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="source_type" label="来源" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ sourceTypeLabel(row.source_type) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatTime(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="220" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
v-if="isApprovable(row)"
|
||||||
|
size="small"
|
||||||
|
type="success"
|
||||||
|
@click="approveEdit(row)"
|
||||||
|
>
|
||||||
|
采纳
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="isApprovable(row)"
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="rejectSuggestion(row)"
|
||||||
|
>
|
||||||
|
驳回
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="viewDetail(row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div v-if="totalPages > 1" class="knowledge-iteration-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page"
|
||||||
|
:total="total"
|
||||||
|
:page-size="pageSize"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
@current-change="fetchSuggestions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== 图谱视图(任务2:P2 ECharts力导向图) ===== -->
|
||||||
|
<template v-if="viewMode === 'graph'">
|
||||||
|
<div class="knowledge-iteration-page__graph-filters">
|
||||||
|
<el-input
|
||||||
|
v-model="graphIssueFilter"
|
||||||
|
placeholder="按 Issue 名称筛选子图(留空查全图)"
|
||||||
|
size="default"
|
||||||
|
style="width: 280px"
|
||||||
|
clearable
|
||||||
|
@change="fetchGraphData"
|
||||||
|
/>
|
||||||
|
<el-button :loading="graphLoading" @click="fetchGraphData">
|
||||||
|
刷新图谱
|
||||||
|
</el-button>
|
||||||
|
<span class="knowledge-iteration-page__graph-info">
|
||||||
|
节点: {{ graphNodeCount }} | 关系: {{ graphLinkCount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="graphContainer"
|
||||||
|
class="knowledge-iteration-page__graph-container"
|
||||||
|
v-loading="graphLoading"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 详情/编辑弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showDetailDialog"
|
||||||
|
:title="editingSuggestion ? '提案详情与编辑' : '提案详情'"
|
||||||
|
width="650px"
|
||||||
|
>
|
||||||
|
<template v-if="editingSuggestion">
|
||||||
|
<el-form label-width="80px" size="default">
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input v-model="editForm.title" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="内容">
|
||||||
|
<el-input v-model="editForm.content" type="textarea" :rows="4" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类">
|
||||||
|
<el-select v-model="editForm.category">
|
||||||
|
<el-option label="硬件" value="硬件" />
|
||||||
|
<el-option label="软件" value="软件" />
|
||||||
|
<el-option label="网络" value="网络" />
|
||||||
|
<el-option label="安全" value="安全" />
|
||||||
|
<el-option label="账号" value="账号" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="受众">
|
||||||
|
<el-select v-model="editForm.audience">
|
||||||
|
<el-option label="员工快捷回复" value="employee_quick_reply" />
|
||||||
|
<el-option label="工程师作业指导" value="engineer_workguide" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 图字段编辑(D1) -->
|
||||||
|
<el-divider>知识图谱字段</el-divider>
|
||||||
|
<el-form-item label="Issue 节点">
|
||||||
|
<el-input v-model="editForm.issue" placeholder="问题名称,如'VPN问题'" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Action 节点">
|
||||||
|
<el-input v-model="editForm.action" placeholder="动作名称,如'个人VPN开通'" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="关系类型">
|
||||||
|
<el-select v-model="editForm.relation_type">
|
||||||
|
<el-option label="LEADS_TO" value="LEADS_TO" />
|
||||||
|
<el-option label="RELATES_TO" value="RELATES_TO" />
|
||||||
|
<el-option label="CAN_JUMP_TO" value="CAN_JUMP_TO" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="父 Issue">
|
||||||
|
<el-input v-model="editForm.parent_issue" placeholder="(可选)父级问题名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showDetailDialog = false">关闭</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="editingSuggestion && isApprovable(editingSuggestion)"
|
||||||
|
type="warning"
|
||||||
|
:loading="checkingDup"
|
||||||
|
@click="checkDuplicates(editingSuggestion)"
|
||||||
|
>
|
||||||
|
检查重复
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="editingSuggestion && isApprovable(editingSuggestion)"
|
||||||
|
type="primary"
|
||||||
|
@click="submitRewrite"
|
||||||
|
>
|
||||||
|
提交改写
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 重复检查结果弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showDupDialog"
|
||||||
|
title="⚠ 重复检测结果"
|
||||||
|
width="550px"
|
||||||
|
>
|
||||||
|
<div v-if="dupResults.length === 0" style="color: #67c23a; text-align: center; padding: 20px;">
|
||||||
|
✅ 未发现重复,可安全采纳
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-alert
|
||||||
|
title="发现可能重复的知识条目,建议合并而非新建"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 12px;"
|
||||||
|
/>
|
||||||
|
<el-table :data="dupResults" size="small" max-height="300">
|
||||||
|
<el-table-column prop="name" label="名称" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="type" label="匹配类型" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.type === 'same_issue' ? 'danger' : 'warning'" size="small">
|
||||||
|
{{ dupTypeLabel(row.type) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="similarity" label="相似度" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ Math.round((row.similarity ?? 0) * 100) }}%
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
v-if="row.suggestion_id"
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="handleMerge(row.suggestion_id)"
|
||||||
|
>
|
||||||
|
合并
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 手动录入弹窗(通道 B) -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showCreateDialog"
|
||||||
|
title="手动录入知识建议(通道 B)"
|
||||||
|
width="650px"
|
||||||
|
>
|
||||||
|
<el-form ref="createFormRef" :model="createForm" label-width="80px" size="default">
|
||||||
|
<el-form-item label="标题" required>
|
||||||
|
<el-input v-model="createForm.title" placeholder="知识条目标题" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="内容" required>
|
||||||
|
<el-input v-model="createForm.content" type="textarea" :rows="4" placeholder="答案内容" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类">
|
||||||
|
<el-select v-model="createForm.category">
|
||||||
|
<el-option label="硬件" value="硬件" />
|
||||||
|
<el-option label="软件" value="软件" />
|
||||||
|
<el-option label="网络" value="网络" />
|
||||||
|
<el-option label="安全" value="安全" />
|
||||||
|
<el-option label="账号" value="账号" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="受众">
|
||||||
|
<el-select v-model="createForm.audience">
|
||||||
|
<el-option label="员工快捷回复" value="employee_quick_reply" />
|
||||||
|
<el-option label="工程师作业指导" value="engineer_workguide" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-divider>知识图谱字段(可选)</el-divider>
|
||||||
|
<el-form-item label="Issue">
|
||||||
|
<el-input v-model="createForm.issue" placeholder="问题名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Action">
|
||||||
|
<el-input v-model="createForm.action" placeholder="动作名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="关系类型">
|
||||||
|
<el-select v-model="createForm.relation_type">
|
||||||
|
<el-option label="LEADS_TO" value="LEADS_TO" />
|
||||||
|
<el-option label="RELATES_TO" value="RELATES_TO" />
|
||||||
|
<el-option label="CAN_JUMP_TO" value="CAN_JUMP_TO" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showCreateDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="creating" @click="createSuggestion">
|
||||||
|
提交(待审)
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, onUnmounted, computed, nextTick, watch } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 类型
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
interface SuggestionItem {
|
||||||
|
id: string
|
||||||
|
suggestion_type: string
|
||||||
|
status: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
category: string
|
||||||
|
tags: string[]
|
||||||
|
source_type: string
|
||||||
|
source_data?: string[]
|
||||||
|
confidence?: number | null
|
||||||
|
audience?: string | null
|
||||||
|
issue?: string | null
|
||||||
|
action?: string | null
|
||||||
|
relation_type?: string | null
|
||||||
|
parent_issue?: string | null
|
||||||
|
graph_meta?: any
|
||||||
|
graph_sync_status?: string | null
|
||||||
|
source_failed?: boolean
|
||||||
|
reviewer_id?: string | null
|
||||||
|
reviewed_at?: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 状态
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
const suggestions = ref<SuggestionItem[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const analyzing = ref(false)
|
||||||
|
const creating = ref(false)
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const filterStatus = ref('')
|
||||||
|
const filterAudience = ref('')
|
||||||
|
const filterConfMin = ref<number | undefined>()
|
||||||
|
const filterConfMax = ref<number | undefined>()
|
||||||
|
|
||||||
|
const showDetailDialog = ref(false)
|
||||||
|
const showCreateDialog = ref(false)
|
||||||
|
const editingSuggestion = ref<SuggestionItem | null>(null)
|
||||||
|
|
||||||
|
// ── 图谱视图状态(任务2:P2) ──
|
||||||
|
const viewMode = ref<'list' | 'graph'>('list')
|
||||||
|
const graphLoading = ref(false)
|
||||||
|
const graphIssueFilter = ref('')
|
||||||
|
const graphContainer = ref<HTMLDivElement | null>(null)
|
||||||
|
let graphChart: echarts.ECharts | null = null
|
||||||
|
const graphNodeCount = ref(0)
|
||||||
|
const graphLinkCount = ref(0)
|
||||||
|
|
||||||
|
// ── 重复检查状态(任务3:P2) ──
|
||||||
|
const checkingDup = ref(false)
|
||||||
|
const showDupDialog = ref(false)
|
||||||
|
const dupResults = ref<any[]>([])
|
||||||
|
|
||||||
|
const editForm = reactive({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
category: '',
|
||||||
|
audience: '',
|
||||||
|
issue: '',
|
||||||
|
action: '',
|
||||||
|
relation_type: '',
|
||||||
|
parent_issue: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const createForm = reactive({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
category: '其他',
|
||||||
|
audience: 'engineer_workguide' as string,
|
||||||
|
issue: '',
|
||||||
|
action: '',
|
||||||
|
relation_type: 'LEADS_TO' as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
const statsItems = ref([
|
||||||
|
{ key: 'pending', label: '待审核', value: 0 },
|
||||||
|
{ key: 'queued', label: '队列中', value: 0 },
|
||||||
|
{ key: 'approved', label: '已通过', value: 0 },
|
||||||
|
{ key: 'applied', label: '已应用', value: 0 },
|
||||||
|
{ key: 'graph_synced', label: '已同步', value: 0 },
|
||||||
|
])
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 方法
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
async function fetchSuggestions() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (filterStatus.value) params.set('status', filterStatus.value)
|
||||||
|
if (filterAudience.value) params.set('audience', filterAudience.value)
|
||||||
|
if (filterConfMin.value !== undefined && filterConfMin.value !== null) {
|
||||||
|
params.set('confidence_min', String(filterConfMin.value / 100))
|
||||||
|
}
|
||||||
|
if (filterConfMax.value !== undefined && filterConfMax.value !== null) {
|
||||||
|
params.set('confidence_max', String(filterConfMax.value / 100))
|
||||||
|
}
|
||||||
|
params.set('page', String(page.value))
|
||||||
|
params.set('page_size', String(pageSize.value))
|
||||||
|
|
||||||
|
const res = await fetch(`/api/admin/knowledge-iteration/suggestions?${params}`)
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0 && json.data) {
|
||||||
|
suggestions.value = json.data.items || []
|
||||||
|
total.value = json.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取建议列表失败:', err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStats() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/knowledge-iteration/stats')
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0 && json.data) {
|
||||||
|
for (const stat of statsItems.value) {
|
||||||
|
stat.value = json.data[stat.key] || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取统计失败:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerAnalyze() {
|
||||||
|
analyzing.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/knowledge-iteration/analyze', { method: 'POST' })
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0) {
|
||||||
|
// 分析完成,刷新列表
|
||||||
|
await Promise.all([fetchSuggestions(), fetchStats()])
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('触发分析失败:', err)
|
||||||
|
} finally {
|
||||||
|
analyzing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isApprovable(row: SuggestionItem): boolean {
|
||||||
|
return row.status === 'pending' || row.status === 'queued'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function approveEdit(row: SuggestionItem) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/admin/knowledge-iteration/suggestions/${row.id}/approve`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0) {
|
||||||
|
await Promise.all([fetchSuggestions(), fetchStats()])
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('审批失败:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rejectSuggestion(row: SuggestionItem) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/admin/knowledge-iteration/suggestions/${row.id}/reject`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ reject_reason: '审核不通过' }),
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0) {
|
||||||
|
await Promise.all([fetchSuggestions(), fetchStats()])
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('驳回失败:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewDetail(row: SuggestionItem) {
|
||||||
|
editingSuggestion.value = row
|
||||||
|
editForm.title = row.title
|
||||||
|
editForm.content = row.content
|
||||||
|
editForm.category = row.category
|
||||||
|
editForm.audience = row.audience || ''
|
||||||
|
editForm.issue = row.issue || ''
|
||||||
|
editForm.action = row.action || ''
|
||||||
|
editForm.relation_type = row.relation_type || ''
|
||||||
|
editForm.parent_issue = row.parent_issue || ''
|
||||||
|
showDetailDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRewrite() {
|
||||||
|
if (!editingSuggestion.value) return
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/admin/knowledge-iteration/suggestions/${editingSuggestion.value.id}/rewrite`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: editForm.title || undefined,
|
||||||
|
content: editForm.content || undefined,
|
||||||
|
category: editForm.category || undefined,
|
||||||
|
audience: editForm.audience || undefined,
|
||||||
|
issue: editForm.issue || undefined,
|
||||||
|
action: editForm.action || undefined,
|
||||||
|
relation_type: editForm.relation_type || undefined,
|
||||||
|
parent_issue: editForm.parent_issue || undefined,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0) {
|
||||||
|
showDetailDialog.value = false
|
||||||
|
await Promise.all([fetchSuggestions(), fetchStats()])
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('改写失败:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSuggestion() {
|
||||||
|
creating.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/knowledge-iteration/suggestions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
suggestion_type: 'new_faq',
|
||||||
|
title: createForm.title,
|
||||||
|
content: createForm.content,
|
||||||
|
category: createForm.category,
|
||||||
|
source_type: 'manual',
|
||||||
|
audience: createForm.audience,
|
||||||
|
confidence: 1.0,
|
||||||
|
issue: createForm.issue || undefined,
|
||||||
|
action: createForm.action || undefined,
|
||||||
|
relation_type: createForm.relation_type || undefined,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0) {
|
||||||
|
showCreateDialog.value = false
|
||||||
|
// 重置表单
|
||||||
|
createForm.title = ''
|
||||||
|
createForm.content = ''
|
||||||
|
await Promise.all([fetchSuggestions(), fetchStats()])
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('创建建议失败:', err)
|
||||||
|
} finally {
|
||||||
|
creating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 工具函数
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
function statusTagType(status: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
pending: 'warning',
|
||||||
|
queued: 'info',
|
||||||
|
approved: 'success',
|
||||||
|
rejected: 'danger',
|
||||||
|
applied: 'success',
|
||||||
|
graph_synced: '',
|
||||||
|
expired: 'info',
|
||||||
|
}
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
pending: '待审核',
|
||||||
|
queued: '队列中',
|
||||||
|
approved: '已通过',
|
||||||
|
rejected: '已驳回',
|
||||||
|
applied: '已应用',
|
||||||
|
graph_synced: '已同步',
|
||||||
|
expired: '已过期',
|
||||||
|
}
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
function confidencePercent(c: number | null | undefined): number {
|
||||||
|
return Math.round((c ?? 0) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
function confidenceColor(c: number | null | undefined): string {
|
||||||
|
const v = c ?? 0
|
||||||
|
if (v >= 0.7) return '#67c23a'
|
||||||
|
if (v >= 0.5) return '#e6a23c'
|
||||||
|
return '#f56c6c'
|
||||||
|
}
|
||||||
|
|
||||||
|
function audienceLabel(a: string | null | undefined): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
employee_quick_reply: '员工快捷回复',
|
||||||
|
engineer_workguide: '工程师指导',
|
||||||
|
}
|
||||||
|
return map[a || ''] || a || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceTypeLabel(type: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
annotation: '标注分析',
|
||||||
|
conversation: '会话分析',
|
||||||
|
ai_uncertain: 'AI不确定',
|
||||||
|
manual: '手动录入',
|
||||||
|
document_ragflow: 'RAGFlow',
|
||||||
|
}
|
||||||
|
return map[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(iso: string): string {
|
||||||
|
if (!iso) return '—'
|
||||||
|
return new Date(iso).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 图谱视图方法(任务2:P2 ECharts力导向图)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
async function switchToGraph() {
|
||||||
|
viewMode.value = 'graph'
|
||||||
|
await nextTick()
|
||||||
|
await fetchGraphData()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGraphData() {
|
||||||
|
graphLoading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', '200')
|
||||||
|
if (graphIssueFilter.value) {
|
||||||
|
params.set('issue_name', graphIssueFilter.value)
|
||||||
|
}
|
||||||
|
const res = await fetch(`/api/admin/knowledge-iteration/graph?${params}`)
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0 && json.data) {
|
||||||
|
graphNodeCount.value = json.data.nodes?.length || 0
|
||||||
|
graphLinkCount.value = json.data.links?.length || 0
|
||||||
|
await nextTick()
|
||||||
|
renderGraph(json.data)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取图谱数据失败:', err)
|
||||||
|
} finally {
|
||||||
|
graphLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGraph(data: { nodes: any[], links: any[] }) {
|
||||||
|
if (!graphContainer.value) return
|
||||||
|
|
||||||
|
if (graphChart) {
|
||||||
|
graphChart.dispose()
|
||||||
|
graphChart = null
|
||||||
|
}
|
||||||
|
|
||||||
|
graphChart = echarts.init(graphContainer.value)
|
||||||
|
|
||||||
|
// 节点分类映射颜色
|
||||||
|
const categories = [
|
||||||
|
{ name: 'issue', itemStyle: { color: '#67c23a' } },
|
||||||
|
{ name: 'action', itemStyle: { color: '#e6a23c' } },
|
||||||
|
]
|
||||||
|
|
||||||
|
const nodes = (data.nodes || []).map((n: any) => ({
|
||||||
|
id: n.id,
|
||||||
|
name: n.name || n.label || n.id,
|
||||||
|
category: n.type || 'issue',
|
||||||
|
symbolSize: n.type === 'issue' ? 28 : 22,
|
||||||
|
label: { show: true, fontSize: 11 },
|
||||||
|
draggable: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const links = (data.links || []).map((l: any) => ({
|
||||||
|
source: l.source,
|
||||||
|
target: l.target,
|
||||||
|
label: { show: true, formatter: l.type || '', fontSize: 9 },
|
||||||
|
lineStyle: { color: '#c0c4cc', width: Math.max(0.5, (l.weight || 1) * 1.5) },
|
||||||
|
}))
|
||||||
|
|
||||||
|
graphChart.setOption({
|
||||||
|
tooltip: {
|
||||||
|
formatter: (params: any) => {
|
||||||
|
if (params.dataType === 'node') return `<b>${params.name}</b><br/>类型: ${params.data.category}`
|
||||||
|
return `${params.data.label?.formatter || ''}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: [{ data: categories.map((c) => c.name), orient: 'vertical', right: 10, top: 10 }],
|
||||||
|
series: [{
|
||||||
|
type: 'graph',
|
||||||
|
layout: 'force',
|
||||||
|
data: nodes,
|
||||||
|
links: links,
|
||||||
|
categories: categories,
|
||||||
|
roam: true,
|
||||||
|
force: {
|
||||||
|
repulsion: 300,
|
||||||
|
gravity: 0.1,
|
||||||
|
edgeLength: [100, 250],
|
||||||
|
layoutAnimation: true,
|
||||||
|
},
|
||||||
|
emphasis: { focus: 'adjacency', lineStyle: { width: 4 } },
|
||||||
|
lineStyle: { color: '#c0c4cc', curveness: 0.3 },
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应式 resize
|
||||||
|
const observer = new ResizeObserver(() => graphChart?.resize())
|
||||||
|
observer.observe(graphContainer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 重复检查与合并方法(任务3:P2)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
async function checkDuplicates(row: SuggestionItem) {
|
||||||
|
checkingDup.value = true
|
||||||
|
showDupDialog.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/admin/knowledge-iteration/suggestions/${row.id}/duplicates`)
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0 && json.data) {
|
||||||
|
dupResults.value = json.data.duplicates || []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('重复检查失败:', err)
|
||||||
|
dupResults.value = []
|
||||||
|
} finally {
|
||||||
|
checkingDup.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMerge(duplicateId: string) {
|
||||||
|
if (!editingSuggestion.value) return
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/admin/knowledge-iteration/suggestions/${editingSuggestion.value.id}/merge`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ duplicate_id: duplicateId }),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0) {
|
||||||
|
showDupDialog.value = false
|
||||||
|
await Promise.all([fetchSuggestions(), fetchStats()])
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('合并失败:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dupTypeLabel(type: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
same_issue: '同名Issue',
|
||||||
|
similar_issue: '相似Issue',
|
||||||
|
title_similar: '标题相似',
|
||||||
|
}
|
||||||
|
return map[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 生命周期
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSuggestions()
|
||||||
|
fetchStats()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (graphChart) {
|
||||||
|
graphChart.dispose()
|
||||||
|
graphChart = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.knowledge-iteration-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-iteration-page__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-iteration-page__header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-iteration-page__header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计栏 */
|
||||||
|
.knowledge-iteration-page__stats-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-iteration-page__stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-iteration-page__stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-iteration-page__stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选栏 */
|
||||||
|
.knowledge-iteration-page__filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格 */
|
||||||
|
.knowledge-iteration-page__table {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
|
.knowledge-iteration-page__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 图谱视图(任务2:P2) ── */
|
||||||
|
.knowledge-iteration-page__graph-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-iteration-page__graph-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-iteration-page__graph-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 550px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -222,10 +222,7 @@ async function loadMatrix() {
|
|||||||
try {
|
try {
|
||||||
const resp = await apiClient.get('/admin/roles/permissions/matrix')
|
const resp = await apiClient.get('/admin/roles/permissions/matrix')
|
||||||
// 拦截器已统一返回 inner data(Scheme A),失败会 reject,故成功即有效数据
|
// 拦截器已统一返回 inner data(Scheme A),失败会 reject,故成功即有效数据
|
||||||
matrixData.value = resp
|
matrixData.value = resp as any
|
||||||
} else {
|
|
||||||
ElMessage.error('拉取权限矩阵失败')
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('loadMatrix 失败:', e)
|
console.error('loadMatrix 失败:', e)
|
||||||
ElMessage.error('加载失败: ' + (e?.message || '未知错误'))
|
ElMessage.error('加载失败: ' + (e?.message || '未知错误'))
|
||||||
|
|||||||
@@ -0,0 +1,417 @@
|
|||||||
|
<!-- =============================================================================
|
||||||
|
企微IT智能服务台 — 管理后台 RAGFlow 文档上传页(Tier1 / P1-5 / 通道 C)
|
||||||
|
=============================================================================
|
||||||
|
说明:训练师上传非标准格式文档到 RAGFlow 进行 ETL 处理,
|
||||||
|
拖拽上传 + 处理进度 + 生成提案预览。
|
||||||
|
|
||||||
|
P1-5 硬约束:
|
||||||
|
- 训练师手动上传触发(非定时扫描)
|
||||||
|
- 支持格式:.docx/.pdf/.txt/.png/.jpg
|
||||||
|
- source_type=document_ragflow
|
||||||
|
- 产出走 D7 审批流
|
||||||
|
============================================================================= -->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ragflow-ingestion-page">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="ragflow-ingestion-page__header">
|
||||||
|
<h2>📄 文档知识导入(RAGFlow 通道 C)</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 说明 -->
|
||||||
|
<el-alert
|
||||||
|
title="通过 RAGFlow 将非标准格式文档整理为结构化知识片段,生成待审提案进入审批队列。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
class="ragflow-ingestion-page__alert"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 上传区域 -->
|
||||||
|
<div
|
||||||
|
class="ragflow-ingestion-page__upload"
|
||||||
|
:class="{ 'ragflow-ingestion-page__upload--dragover': isDragover }"
|
||||||
|
@dragenter.prevent="isDragover = true"
|
||||||
|
@dragover.prevent="isDragover = true"
|
||||||
|
@dragleave.prevent="isDragover = false"
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
>
|
||||||
|
<div class="ragflow-ingestion-page__upload-content">
|
||||||
|
<el-icon :size="48" color="#909399">
|
||||||
|
<UploadFilled />
|
||||||
|
</el-icon>
|
||||||
|
<p class="ragflow-ingestion-page__upload-text">
|
||||||
|
拖拽文件到此处,或点击下方按钮选择
|
||||||
|
</p>
|
||||||
|
<p class="ragflow-ingestion-page__upload-hint">
|
||||||
|
支持 .docx / .pdf / .txt / .png / .jpg,最大 20MB
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 分类提示选择 -->
|
||||||
|
<div class="ragflow-ingestion-page__category-select">
|
||||||
|
<span>分类提示:</span>
|
||||||
|
<el-select v-model="categoryHint" size="default" style="width: 140px">
|
||||||
|
<el-option label="硬件" value="硬件" />
|
||||||
|
<el-option label="软件" value="软件" />
|
||||||
|
<el-option label="网络" value="网络" />
|
||||||
|
<el-option label="安全" value="安全" />
|
||||||
|
<el-option label="账号" value="账号" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 选择文件按钮 -->
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="'Upload'"
|
||||||
|
size="default"
|
||||||
|
:loading="uploading"
|
||||||
|
class="ragflow-ingestion-page__upload-btn"
|
||||||
|
@click="openFilePicker"
|
||||||
|
>
|
||||||
|
选择文件并上传
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件选择器 -->
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
:accept="allowedExtensions.join(',')"
|
||||||
|
class="ragflow-ingestion-page__file-input"
|
||||||
|
@change="onFileSelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传进度与结果 -->
|
||||||
|
<div v-if="uploadResult" class="ragflow-ingestion-page__result">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<span>处理结果</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="ragflow-ingestion-page__result-info">
|
||||||
|
<el-tag :type="uploadResult.status === 'completed' ? 'success' : 'warning'" size="default">
|
||||||
|
{{ statusLabel(uploadResult.status) }}
|
||||||
|
</el-tag>
|
||||||
|
<span>文件: {{ uploadResult.file_name }}</span>
|
||||||
|
<span>生成提案: {{ uploadResult.suggestions_count }} 条</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提案预览 -->
|
||||||
|
<div v-if="uploadResult.suggestions && uploadResult.suggestions.length > 0" class="ragflow-ingestion-page__suggestions">
|
||||||
|
<div
|
||||||
|
v-for="(sug, idx) in uploadResult.suggestions"
|
||||||
|
:key="idx"
|
||||||
|
class="ragflow-ingestion-page__suggestion-item"
|
||||||
|
>
|
||||||
|
<div class="ragflow-ingestion-page__suggestion-header">
|
||||||
|
<span>{{ sug.title }}</span>
|
||||||
|
<el-tag size="small" type="info">置信度 {{ Math.round(sug.confidence * 100) }}%</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="ragflow-ingestion-page__suggestion-category">
|
||||||
|
分类: {{ sug.category }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ragflow-ingestion-page__result-note">
|
||||||
|
✅ 生成的提案已进入审批队列,请前往
|
||||||
|
<router-link to="/knowledge-iteration">知识迭代管理</router-link>
|
||||||
|
审批。
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 处理日志 -->
|
||||||
|
<div v-if="processingLogs.length > 0" class="ragflow-ingestion-page__logs">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<span>处理日志</span>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-for="(log, idx) in processingLogs"
|
||||||
|
:key="idx"
|
||||||
|
class="ragflow-ingestion-page__log-item"
|
||||||
|
:class="`ragflow-ingestion-page__log-item--${log.level}`"
|
||||||
|
>
|
||||||
|
<span class="ragflow-ingestion-page__log-time">{{ log.time }}</span>
|
||||||
|
<span>{{ log.message }}</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 常量
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
const allowedExtensions = ['.docx', '.pdf', '.txt', '.png', '.jpg', '.jpeg']
|
||||||
|
const allowedMimeTypes = [
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/pdf',
|
||||||
|
'text/plain',
|
||||||
|
'image/png',
|
||||||
|
'image/jpeg',
|
||||||
|
]
|
||||||
|
const maxFileSize = 20 * 1024 * 1024
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 状态
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
const fileInput = ref<HTMLInputElement>()
|
||||||
|
const isDragover = ref(false)
|
||||||
|
const uploading = ref(false)
|
||||||
|
const categoryHint = ref('其他')
|
||||||
|
|
||||||
|
const uploadResult = ref<{
|
||||||
|
task_id: string
|
||||||
|
status: string
|
||||||
|
file_name: string
|
||||||
|
suggestions_count: number
|
||||||
|
suggestions: Array<{ title: string; category: string; confidence: number }>
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
const processingLogs = ref<Array<{ level: string; time: string; message: string }>>([])
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 方法
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
function openFilePicker() {
|
||||||
|
fileInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLog(level: string, message: string) {
|
||||||
|
processingLogs.value.push({
|
||||||
|
level,
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFileSelected(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
uploadFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event: DragEvent) {
|
||||||
|
isDragover.value = false
|
||||||
|
const file = event.dataTransfer?.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
uploadFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile(file: File) {
|
||||||
|
// 校验扩展名
|
||||||
|
const ext = '.' + file.name.split('.').pop()?.toLowerCase()
|
||||||
|
if (!allowedExtensions.includes(ext)) {
|
||||||
|
addLog('error', `不支持的文件格式: ${ext}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验大小
|
||||||
|
if (file.size > maxFileSize) {
|
||||||
|
addLog('error', `文件过大: ${(file.size / 1024 / 1024).toFixed(1)}MB(最大 20MB)`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploading.value = true
|
||||||
|
addLog('info', `开始上传: ${file.name}(${(file.size / 1024).toFixed(1)}KB)`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('category_hint', categoryHint.value)
|
||||||
|
|
||||||
|
addLog('info', '上传至 RAGFlow,等待 ETL 处理...')
|
||||||
|
|
||||||
|
const res = await fetch('/api/ragflow/ingest', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
|
||||||
|
if (json.code === 0 && json.data) {
|
||||||
|
uploadResult.value = json.data
|
||||||
|
addLog('success', `处理完成!生成 ${json.data.suggestions_count} 条待审提案`)
|
||||||
|
} else {
|
||||||
|
addLog('error', `处理失败: ${json.message || '未知错误'}`)
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
addLog('error', `上传异常: ${err.message}`)
|
||||||
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
|
if (fileInput.value) {
|
||||||
|
fileInput.value.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
completed: '处理完成',
|
||||||
|
failed: '处理失败',
|
||||||
|
pending: '处理中',
|
||||||
|
disabled: '服务未启用',
|
||||||
|
timeout: '处理超时',
|
||||||
|
}
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ragflow-ingestion-page {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__header h2 {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传区域 */
|
||||||
|
.ragflow-ingestion-page__upload {
|
||||||
|
border: 2px dashed #dcdfe6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fafafa;
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__upload--dragover {
|
||||||
|
border-color: #409eff;
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__upload-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__upload-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__upload-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__category-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__upload-btn {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 处理结果 */
|
||||||
|
.ragflow-ingestion-page__result {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__result-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__suggestions {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__suggestion-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__suggestion-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__suggestion-category {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__result-note {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0f9eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__result-note a {
|
||||||
|
color: #409eff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 处理日志 */
|
||||||
|
.ragflow-ingestion-page__logs {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__log-item {
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__log-time {
|
||||||
|
color: #c0c4cc;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__log-item--info {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__log-item--success {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ragflow-ingestion-page__log-item--error {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -415,10 +415,13 @@ onMounted(async () => {
|
|||||||
getRoleMappingRules(),
|
getRoleMappingRules(),
|
||||||
getUserRoleAssignments(),
|
getUserRoleAssignments(),
|
||||||
])
|
])
|
||||||
roles.value = rolesRes.data.data
|
// apiClient 响应拦截器已解包:返回的是后端 data 字段(数组本身),
|
||||||
mappingRules.value = rulesRes.data.data
|
// 不再是 { code, data, message } 包裹结构。直接取数组,避免 .data.data 报错。
|
||||||
userRoles.value = userRolesRes.data.data || []
|
roles.value = unwrapList<Role>(rolesRes)
|
||||||
} catch {
|
mappingRules.value = unwrapList<RoleMappingRule>(rulesRes)
|
||||||
|
userRoles.value = unwrapList<UserRole>(userRolesRes)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Roles] 加载角色管理数据失败:', e)
|
||||||
roles.value = getDefaultRoles()
|
roles.value = getDefaultRoles()
|
||||||
mappingRules.value = getDefaultMappingRules()
|
mappingRules.value = getDefaultMappingRules()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -532,6 +535,22 @@ function isDefaultRole(roleName: string): boolean {
|
|||||||
return roles.value.some(r => r.name === roleName && r.is_default)
|
return roles.value.some(r => r.name === roleName && r.is_default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全解包列表型接口返回。
|
||||||
|
* apiClient 响应拦截器已解包,成功时返回的是后端 data 字段(数组本身);
|
||||||
|
* 这里做防御性处理,兼容「数组 / {data: 数组} / {data:{data: 数组}}」三种形态,
|
||||||
|
* 避免历史包裹结构导致的 .data.data 取数报错(曾导致用户角色分配表空白)。
|
||||||
|
*/
|
||||||
|
function unwrapList<T>(resp: unknown): T[] {
|
||||||
|
if (Array.isArray(resp)) return resp as T[]
|
||||||
|
const r = resp as { data?: unknown }
|
||||||
|
if (r && Array.isArray(r.data)) return r.data as T[]
|
||||||
|
if (r && r.data && typeof r.data === 'object' && Array.isArray((r.data as { data?: unknown }).data)) {
|
||||||
|
return (r.data as { data: T[] }).data
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// 操作处理
|
// 操作处理
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -664,11 +683,12 @@ async function loadData(): Promise<void> {
|
|||||||
getRoleMappingRules(),
|
getRoleMappingRules(),
|
||||||
getUserRoleAssignments(),
|
getUserRoleAssignments(),
|
||||||
])
|
])
|
||||||
roles.value = rolesRes.data.data
|
roles.value = unwrapList<Role>(rolesRes)
|
||||||
mappingRules.value = rulesRes.data.data
|
mappingRules.value = unwrapList<RoleMappingRule>(rulesRes)
|
||||||
userRoles.value = userRolesRes.data.data || []
|
userRoles.value = unwrapList<UserRole>(userRolesRes)
|
||||||
} catch {
|
} catch (e) {
|
||||||
// 静默失败,保留现有数据
|
console.error('[Roles] 刷新角色管理数据失败:', e)
|
||||||
|
// 保留现有数据
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user