Files
wecom_it_smart_desk/frontend-admin/src/views/KnowledgeSuggestions.vue
T

510 lines
14 KiB
Vue
Raw Normal View History

<!--
=============================================================================
企微IT智能服务台 知识库优化建议审核页 (P2-13)
=============================================================================
说明知识库自动迭代建议的审核管理
- 查看待审核建议列表
- 审核通过/拒绝建议
- 统计分析
-->
<template>
<div class="knowledge-suggestions-page">
<!-- 页面标题 -->
<div class="page-title">知识库优化建议</div>
<div class="page-desc">AI分析标注数据和会话数据生成的优化建议审核通过后自动更新知识库</div>
<!-- 统计卡片 -->
<div class="stats-cards">
<el-card class="stat-card">
<div class="stat-card-content">
<div class="stat-value">{{ stats.total }}</div>
<div class="stat-label">总建议数</div>
</div>
</el-card>
<el-card class="stat-card stat-card-warning">
<div class="stat-card-content">
<div class="stat-value">{{ stats.pending }}</div>
<div class="stat-label">待审核</div>
</div>
</el-card>
<el-card class="stat-card stat-card-success">
<div class="stat-card-content">
<div class="stat-value">{{ stats.approved }}</div>
<div class="stat-label">已通过</div>
</div>
</el-card>
<el-card class="stat-card stat-card-danger">
<div class="stat-card-content">
<div class="stat-value">{{ stats.rejected }}</div>
<div class="stat-label">已拒绝</div>
</div>
</el-card>
</div>
<!-- 顶部操作栏 -->
<div class="toolbar">
<!-- 状态筛选 -->
<el-radio-group v-model="filters.status" @change="loadSuggestions">
<el-radio-button value="">全部</el-radio-button>
<el-radio-button value="pending">待审核</el-radio-button>
<el-radio-button value="approved">已通过</el-radio-button>
<el-radio-button value="rejected">已拒绝</el-radio-button>
<el-radio-button value="applied">已应用</el-radio-button>
</el-radio-group>
<!-- 触发分析按钮 -->
<el-button type="primary" @click="triggerAnalysis">
<el-icon><Refresh /></el-icon>
触发AI分析
</el-button>
</div>
<!-- 建议列表 -->
<el-table :data="suggestions" v-loading="loading" style="width: 100%">
<el-table-column prop="suggestion_type" label="类型" width="100">
<template #default="{ row }">
<el-tag :type="getTypeTagType(row.suggestion_type)">
{{ getTypeLabel(row.suggestion_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="category" label="分类" width="100">
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.category }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">
{{ getStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="source_type" label="来源" width="120">
<template #default="{ row }">
<span>{{ getSourceLabel(row.source_type) }}</span>
</template>
</el-table-column>
<el-table-column prop="reason" label="生成理由" min-width="200" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" width="160">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)">查看</el-button>
<template v-if="row.status === 'pending'">
<el-button size="small" type="success" @click="approveSuggestion(row)">
通过
</el-button>
<el-button size="small" type="danger" @click="openRejectDialog(row)">
拒绝
</el-button>
</template>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="loadSuggestions"
@current-change="loadSuggestions"
/>
</div>
<!-- 详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="建议详情" width="600px">
<el-descriptions :column="1" border v-if="currentSuggestion">
<el-descriptions-item label="类型">
<el-tag :type="getTypeTagType(currentSuggestion.suggestion_type)">
{{ getTypeLabel(currentSuggestion.suggestion_type) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="标题">{{ currentSuggestion.title }}</el-descriptions-item>
<el-descriptions-item label="分类">{{ currentSuggestion.category }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(currentSuggestion.status)">
{{ getStatusLabel(currentSuggestion.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="来源">{{ getSourceLabel(currentSuggestion.source_type) }}</el-descriptions-item>
<el-descriptions-item label="生成理由">{{ currentSuggestion.reason }}</el-descriptions-item>
<el-descriptions-item label="答案内容">
<div class="content-box">{{ currentSuggestion.content }}</div>
</el-descriptions-item>
<el-descriptions-item v-if="currentSuggestion.tags?.length" label="标签">
<el-tag v-for="tag in currentSuggestion.tags" :key="tag" size="small" class="mr-2">
{{ tag }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item v-if="currentSuggestion.reject_reason" label="拒绝理由">
<span style="color: var(--el-color-danger)">{{ currentSuggestion.reject_reason }}</span>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
<template v-if="currentSuggestion?.status === 'pending'">
<el-button type="success" @click="approveSuggestion(currentSuggestion)">通过</el-button>
<el-button type="danger" @click="openRejectDialog(currentSuggestion)">拒绝</el-button>
</template>
</template>
</el-dialog>
<!-- 拒绝对话框 -->
<el-dialog v-model="rejectDialogVisible" title="拒绝建议" width="400px">
<el-form @submit.prevent="rejectSuggestion">
<el-form-item label="拒绝理由" required>
<el-input
v-model="rejectForm.reject_reason"
type="textarea"
:rows="3"
placeholder="请输入拒绝理由"
maxlength="500"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rejectDialogVisible = false">取消</el-button>
<el-button type="danger" @click="rejectSuggestion" :disabled="!rejectForm.reject_reason">
确认拒绝
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { adminApi } from '@/api/admin'
// 状态筛选
const filters = reactive({
status: '',
})
// 列表数据
const loading = ref(false)
const suggestions = ref([])
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0,
})
// 统计数据
const stats = reactive({
total: 0,
pending: 0,
approved: 0,
rejected: 0,
applied: 0,
})
// 详情对话框
const detailDialogVisible = ref(false)
const currentSuggestion = ref(null)
// 拒绝对话框
const rejectDialogVisible = ref(false)
const rejectForm = reactive({
suggestion_id: '',
reject_reason: '',
})
// 加载建议列表
async function loadSuggestions() {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.pageSize,
}
if (filters.status) {
params.status = filters.status
}
const res = await adminApi.getKnowledgeSuggestions(params)
if (res.code === 0) {
suggestions.value = res.data.items
pagination.total = res.data.total
}
} catch (error) {
console.error('加载建议列表失败:', error)
ElMessage.error('加载建议列表失败')
} finally {
loading.value = false
}
}
// 加载统计数据
async function loadStats() {
try {
const res = await adminApi.getKnowledgeSuggestionStats()
if (res.code === 0) {
Object.assign(stats, res.data)
}
} catch (error) {
console.error('加载统计失败:', error)
}
}
// 触发AI分析
async function triggerAnalysis() {
try {
await ElMessageBox.confirm('将分析过去7天的标注数据和会话数据,生成优化建议。是否继续?', '触发分析', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'info',
})
const res = await adminApi.triggerKnowledgeIterationAnalysis({ days: 7 })
if (res.code === 0) {
ElMessage.success('分析完成')
loadSuggestions()
loadStats()
} else {
ElMessage.error(res.message || '分析失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('触发分析失败:', error)
ElMessage.error('触发分析失败')
}
}
}
// 查看详情
function viewDetail(row) {
currentSuggestion.value = row
detailDialogVisible.value = true
}
// 通过建议
async function approveSuggestion(row) {
try {
await ElMessageBox.confirm('审核通过后,该建议将应用到知识库。是否继续?', '审核通过', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'success',
})
const res = await adminApi.approveKnowledgeSuggestion(row.id)
if (res.code === 0) {
ElMessage.success('审核通过')
loadSuggestions()
loadStats()
detailDialogVisible.value = false
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('审核通过失败:', error)
ElMessage.error('操作失败')
}
}
}
// 打开拒绝对话框
function openRejectDialog(row) {
rejectForm.suggestion_id = row.id
rejectForm.reject_reason = ''
rejectDialogVisible.value = true
}
// 拒绝建议
async function rejectSuggestion() {
if (!rejectForm.reject_reason) {
ElMessage.warning('请输入拒绝理由')
return
}
try {
const res = await adminApi.rejectKnowledgeSuggestion(rejectForm.suggestion_id, {
reject_reason: rejectForm.reject_reason,
})
if (res.code === 0) {
ElMessage.success('已拒绝')
loadSuggestions()
loadStats()
rejectDialogVisible.value = false
detailDialogVisible.value = false
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
console.error('拒绝建议失败:', error)
ElMessage.error('操作失败')
}
}
// 工具函数
function getTypeLabel(type) {
const map = {
new_faq: '新增FAQ',
update: '更新',
outdated: '标记过时',
}
return map[type] || type
}
function getTypeTagType(type) {
const map = {
new_faq: 'success',
update: 'warning',
outdated: 'info',
}
return map[type] || ''
}
function getStatusLabel(status) {
const map = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
applied: '已应用',
}
return map[status] || status
}
function getStatusTagType(status) {
const map = {
pending: 'warning',
approved: 'success',
rejected: 'danger',
applied: 'info',
}
return map[status] || ''
}
function getSourceLabel(source) {
const map = {
annotation: '标注数据',
conversation: '会话数据',
ai_uncertain: 'AI不确定',
}
return map[source] || source
}
function formatDate(dateStr) {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
onMounted(() => {
loadSuggestions()
loadStats()
})
</script>
<style scoped>
.knowledge-suggestions-page {
padding: 20px;
}
.page-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.page-desc {
color: var(--text-secondary, #909399);
margin-bottom: 20px;
}
/* 统计卡片 */
.stats-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-card-content {
padding: 10px 0;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: var(--el-color-primary);
}
.stat-card-warning .stat-value {
color: var(--el-color-warning);
}
.stat-card-success .stat-value {
color: var(--el-color-success);
}
.stat-card-danger .stat-value {
color: var(--el-color-danger);
}
.stat-label {
font-size: 14px;
color: var(--text-secondary, #909399);
margin-top: 4px;
}
/* 工具栏 */
.toolbar {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
/* 分页 */
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
/* 内容框 */
.content-box {
max-height: 200px;
overflow-y: auto;
padding: 8px;
background: var(--el-fill-color-light);
border-radius: 4px;
white-space: pre-wrap;
}
.mr-2 {
margin-right: 8px;
}
</style>