WIP-CHECKPOINT[auth-refactor]: 固化工程师崩溃前部分成果 + 同树其他未提交WIP(仅源码,不含密钥/二进制)-- 待重激活工程师续作
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// 企微IT智能服务台 — 管理后台 API 调用函数
|
||||
// =============================================================================
|
||||
// 说明:封装所有管理后台 API 端点调用,统一返回类型
|
||||
// 所有函数返回 axios response,调用方从 response.data.data 获取业务数据
|
||||
// 所有函数返回 axios response,调用方从 response 获取业务数据
|
||||
|
||||
import apiClient from './index'
|
||||
import type {
|
||||
@@ -114,6 +114,14 @@ export function deleteAgent(
|
||||
return apiClient.delete(`/admin/agents/${id}`)
|
||||
}
|
||||
|
||||
/** 重置坐席密码 */
|
||||
export function resetAgentPassword(
|
||||
id: string,
|
||||
newPassword: string
|
||||
): Promise<{ data: { code: number; data: { message: string }; message: string } }> {
|
||||
return apiClient.post(`/admin/agents/${id}/reset-password`, { new_password: newPassword })
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 外部系统集成配置
|
||||
// ==========================================================================
|
||||
@@ -250,6 +258,71 @@ export function reviewQuickReply(
|
||||
return apiClient.put(`/admin/quick-replies/${id}/review`, body)
|
||||
}
|
||||
|
||||
/** 创建快速回复模板 */
|
||||
export function createQuickReplyTemplate(
|
||||
data: QuickReplyTemplate
|
||||
): Promise<{ data: { code: number; data: QuickReplyTemplate; message: string } }> {
|
||||
return apiClient.post('/quick-replies', data)
|
||||
}
|
||||
|
||||
/** 更新快速回复模板 */
|
||||
export function updateQuickReplyTemplate(
|
||||
id: string,
|
||||
data: Partial<QuickReplyTemplate>
|
||||
): Promise<{ data: { code: number; data: QuickReplyTemplate; message: string } }> {
|
||||
return apiClient.put(`/quick-replies/${id}`, data)
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 知识库管理
|
||||
// ==========================================================================
|
||||
|
||||
/** 知识库条目类型 */
|
||||
export interface KnowledgeBaseItem {
|
||||
id: string
|
||||
category: string
|
||||
title: string
|
||||
content: string
|
||||
tags: string[]
|
||||
view_count: number
|
||||
use_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** 获取知识库列表 */
|
||||
export function getKnowledgeBase(
|
||||
category?: string,
|
||||
keyword?: string
|
||||
): Promise<{ data: { code: number; data: { items: KnowledgeBaseItem[] }; message: string } }> {
|
||||
const params: Record<string, string> = {}
|
||||
if (category) params.category = category
|
||||
if (keyword) params.keyword = keyword
|
||||
return apiClient.get('/knowledge', { params })
|
||||
}
|
||||
|
||||
/** 创建知识库条目 */
|
||||
export function createKnowledgeBase(
|
||||
data: Omit<KnowledgeBaseItem, 'id' | 'view_count' | 'use_count' | 'created_at' | 'updated_at'>
|
||||
): Promise<{ data: { code: number; data: KnowledgeBaseItem; message: string } }> {
|
||||
return apiClient.post('/knowledge', data)
|
||||
}
|
||||
|
||||
/** 更新知识库条目 */
|
||||
export function updateKnowledgeBase(
|
||||
id: string,
|
||||
data: Partial<KnowledgeBaseItem>
|
||||
): Promise<{ data: { code: number; data: KnowledgeBaseItem; message: string } }> {
|
||||
return apiClient.put(`/knowledge/${id}`, data)
|
||||
}
|
||||
|
||||
/** 删除知识库条目 */
|
||||
export function deleteKnowledgeBase(
|
||||
id: string
|
||||
): Promise<{ data: { code: number; data: null; message: string } }> {
|
||||
return apiClient.delete(`/knowledge/${id}`)
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 消息分配模式
|
||||
// ==========================================================================
|
||||
@@ -383,3 +456,176 @@ export function getSystemLogs(params?: {
|
||||
}): Promise<{ data: { code: number; data: { items: any[]; total: number; page: number; page_size: number }; message: string } }> {
|
||||
return apiClient.get('/admin/system-logs', { params })
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// P2: 满意度评价统计
|
||||
// ==========================================================================
|
||||
|
||||
/** 满意度评价统计数据项 */
|
||||
export interface EvaluationStatsItem {
|
||||
label: string
|
||||
count: number
|
||||
percentage: number
|
||||
}
|
||||
|
||||
/** 满意度评价记录 */
|
||||
export interface EvaluationRecord {
|
||||
id: string
|
||||
conversation_id: string
|
||||
employee_id: string
|
||||
employee_name: string
|
||||
star_rating: number
|
||||
emoji: string
|
||||
feedback_text: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** 满意度评价统计响应 */
|
||||
export interface EvaluationStatsResponse {
|
||||
total_count: number
|
||||
avg_star_rating: number
|
||||
star_distribution: EvaluationStatsItem[]
|
||||
emoji_distribution: EvaluationStatsItem[]
|
||||
recent_evaluations: EvaluationRecord[]
|
||||
}
|
||||
|
||||
/** 获取满意度评价统计数据 */
|
||||
export function getEvaluationStats(params?: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
}): Promise<{ data: { code: number; data: EvaluationStatsResponse; message: string } }> {
|
||||
return apiClient.get('/evaluations/stats', { params })
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 知识库自动迭代 (P2-13)
|
||||
// ==========================================================================
|
||||
|
||||
/** 知识库优化建议项 */
|
||||
export interface KnowledgeSuggestionItem {
|
||||
id: string
|
||||
suggestion_type: 'new_faq' | 'update' | 'outdated'
|
||||
status: 'pending' | 'approved' | 'rejected' | 'applied'
|
||||
title: string
|
||||
content: string
|
||||
category: string
|
||||
tags: string[]
|
||||
source_type: 'annotation' | 'conversation' | 'ai_uncertain'
|
||||
source_data: string[] | null
|
||||
reason: string | null
|
||||
reject_reason: string | null
|
||||
reviewer_id: string | null
|
||||
reviewed_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** 知识库优化建议统计 */
|
||||
export interface KnowledgeSuggestionStats {
|
||||
total: number
|
||||
pending: number
|
||||
approved: number
|
||||
rejected: number
|
||||
applied: number
|
||||
new_faq_count: number
|
||||
update_count: number
|
||||
outdated_count: number
|
||||
}
|
||||
|
||||
/** 获取知识库优化建议列表 */
|
||||
export function getKnowledgeSuggestions(params?: {
|
||||
status?: string
|
||||
suggestion_type?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}): Promise<{ data: { code: number; data: { total: number; items: KnowledgeSuggestionItem[] }; message: string } }> {
|
||||
return apiClient.get('/admin/knowledge-iteration/suggestions', { params })
|
||||
}
|
||||
|
||||
/** 获取知识库优化建议详情 */
|
||||
export function getKnowledgeSuggestion(
|
||||
id: string
|
||||
): Promise<{ data: { code: number; data: KnowledgeSuggestionItem; message: string } }> {
|
||||
return apiClient.get(`/admin/knowledge-iteration/suggestions/${id}`)
|
||||
}
|
||||
|
||||
/** 触发知识库迭代分析 */
|
||||
export function triggerKnowledgeIterationAnalysis(params: {
|
||||
days?: number
|
||||
}): Promise<{ data: { code: number; data: any; message: string } }> {
|
||||
return apiClient.post('/admin/knowledge-iteration/analyze', null, { params })
|
||||
}
|
||||
|
||||
/** 获取知识库建议统计 */
|
||||
export function getKnowledgeSuggestionStats(): Promise<{ data: { code: number; data: KnowledgeSuggestionStats; message: string } }> {
|
||||
return apiClient.get('/admin/knowledge-iteration/stats')
|
||||
}
|
||||
|
||||
/** 审核通过知识库建议 */
|
||||
export function approveKnowledgeSuggestion(
|
||||
id: string
|
||||
): Promise<{ data: { code: number; data: KnowledgeSuggestionItem; message: string } }> {
|
||||
return apiClient.post(`/admin/knowledge-iteration/suggestions/${id}/approve`)
|
||||
}
|
||||
|
||||
/** 拒绝知识库建议 */
|
||||
export function rejectKnowledgeSuggestion(
|
||||
id: string,
|
||||
body: { reject_reason: string }
|
||||
): Promise<{ data: { code: number; data: KnowledgeSuggestionItem; message: string } }> {
|
||||
return apiClient.post(`/admin/knowledge-iteration/suggestions/${id}/reject`, body)
|
||||
}
|
||||
|
||||
/** 管理员 API 对象 (供 KnowledgeSuggestions 等页面使用) */
|
||||
export const adminApi = {
|
||||
getKnowledgeSuggestions,
|
||||
getKnowledgeSuggestionStats,
|
||||
triggerKnowledgeIterationAnalysis,
|
||||
approveKnowledgeSuggestion,
|
||||
rejectKnowledgeSuggestion,
|
||||
getDashboardOverview,
|
||||
getConfigGroups,
|
||||
updateConfig,
|
||||
getConfigHistory,
|
||||
getAgents,
|
||||
createAgent,
|
||||
updateAgent,
|
||||
unbindOtp,
|
||||
deleteAgent,
|
||||
getIntegrations,
|
||||
updateIntegration,
|
||||
testHuorongConnection,
|
||||
getHuorongTerminals,
|
||||
getHuorongTerminalDetail,
|
||||
getHuorongLeaks,
|
||||
getHuorongVirusEvents,
|
||||
testLianruanConnection,
|
||||
queryLianruanTerminals,
|
||||
getLianruanTerminalDetail,
|
||||
testRagflowConnection,
|
||||
getRagflowDatasets,
|
||||
ragflowRetrieval,
|
||||
getPendingQuickReplies,
|
||||
reviewQuickReply,
|
||||
createQuickReplyTemplate,
|
||||
updateQuickReplyTemplate,
|
||||
getKnowledgeBase,
|
||||
createKnowledgeBase,
|
||||
updateKnowledgeBase,
|
||||
deleteKnowledgeBase,
|
||||
getAssignmentMode,
|
||||
updateAssignmentMode,
|
||||
getMonitorSessions,
|
||||
globalSearch,
|
||||
getRoles,
|
||||
assignRole,
|
||||
revokeRole,
|
||||
getRoleMappingRules,
|
||||
createRoleMappingRule,
|
||||
deleteRoleMappingRule,
|
||||
getAuditConversations,
|
||||
getAuditConversationDetail,
|
||||
getAgentPerformance,
|
||||
getSystemLogs,
|
||||
getEvaluationStats,
|
||||
}
|
||||
|
||||
@@ -71,19 +71,21 @@ apiClient.interceptors.response.use(
|
||||
})
|
||||
}
|
||||
|
||||
// 返回 rejected Promise,让调用方的 catch 能捕获
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
// 返回 rejected Promise,让调用方的 catch 能捕获(Scheme A: {code, message})
|
||||
return Promise.reject({ code: res.code, message: res.message || '请求失败' })
|
||||
}
|
||||
|
||||
// 业务成功:返回完整响应(调用方从 response.data.data 获取业务数据)
|
||||
return response
|
||||
// 业务成功:Scheme A — 直接返回 inner data(三端统一契约)
|
||||
return res.data
|
||||
},
|
||||
(error) => {
|
||||
// 网络错误或服务器错误(HTTP 状态码非 2xx)
|
||||
// 网络错误或服务器错误(HTTP 状态码非 2xx)— 统一 reject {code, message}(CTRT-03)
|
||||
let message = '网络异常,请稍后重试'
|
||||
let code = -1
|
||||
|
||||
if (error.response) {
|
||||
// 服务器返回了错误状态码
|
||||
code = error.response.status
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
message = '未授权,请重新登录'
|
||||
@@ -110,7 +112,7 @@ apiClient.interceptors.response.use(
|
||||
// 显示错误提示
|
||||
ElMessage.error(message)
|
||||
|
||||
return Promise.reject(error)
|
||||
return Promise.reject({ code, message })
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function listMfaUsers(
|
||||
params: MfaUserListParams = {}
|
||||
): Promise<MfaUserListData> {
|
||||
const response: AxiosResponse = await apiClient.get('/admin/mfa/users', { params })
|
||||
return response.data.data
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,5 +107,5 @@ export async function resetMfa(employeeId: string): Promise<MfaAdminResetData> {
|
||||
const response: AxiosResponse = await apiClient.post(
|
||||
`/admin/mfa/reset/${encodeURIComponent(employeeId)}`
|
||||
)
|
||||
return response.data.data
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export async function listTemplates(): Promise<TroubleshootingTemplate[]> {
|
||||
const res = await http.get<ApiResponse<TroubleshootingTemplate[]>>(
|
||||
'/troubleshooting-templates'
|
||||
)
|
||||
return res.data.data?.items || []
|
||||
return res.data.data || []
|
||||
}
|
||||
|
||||
/** GET /api/troubleshooting-templates/{id} — 获取模板详情 */
|
||||
|
||||
@@ -94,11 +94,14 @@
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text type="primary" @click="$emit('edit', row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" text type="warning" @click="$emit('reset-password', row)">
|
||||
重置密码
|
||||
</el-button>
|
||||
<el-button size="small" text type="danger" @click="$emit('delete', row)">
|
||||
移除
|
||||
</el-button>
|
||||
@@ -127,6 +130,8 @@ defineProps<{
|
||||
defineEmits<{
|
||||
/** 编辑坐席 */
|
||||
edit: [agent: Agent]
|
||||
/** 重置密码 */
|
||||
'reset-password': [agent: Agent]
|
||||
/** 移除坐席 */
|
||||
delete: [agent: Agent]
|
||||
}>()
|
||||
|
||||
@@ -95,6 +95,14 @@
|
||||
<el-icon><Notebook /></el-icon>
|
||||
<span>系统日志</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/evaluation-stats">
|
||||
<el-icon><Star /></el-icon>
|
||||
<span>满意度评价</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/knowledge-suggestions">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>知识库建议</span>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 🔒 开发中(P2占位) -->
|
||||
<div class="menu-section-title">🔒 开发中</div>
|
||||
@@ -123,7 +131,7 @@
|
||||
// ==========================================================================
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Headset, PieChart, Switch, UserFilled, Connection, Warning, ChatLineSquare, Sort, Share, Monitor, Brush, DataAnalysis, Reading, Lock, Key, Document, TrendCharts, Notebook } from '@element-plus/icons-vue'
|
||||
import { Headset, PieChart, Switch, UserFilled, Connection, Warning, ChatLineSquare, Sort, Share, Monitor, Brush, DataAnalysis, Reading, Lock, Key, Document, TrendCharts, Notebook, Star } from '@element-plus/icons-vue'
|
||||
|
||||
// ==========================================================================
|
||||
// 当前激活菜单
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="(v) => $emit('update:modelValue', v)"
|
||||
@update:model-value="(v: boolean) => $emit('update:modelValue', v)"
|
||||
:title="dialogTitle"
|
||||
width="80%"
|
||||
top="5vh"
|
||||
|
||||
@@ -76,6 +76,12 @@ const routes = [
|
||||
component: () => import('@/views/QuickReplies.vue'),
|
||||
meta: { title: '快速回复', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'knowledge',
|
||||
name: 'Knowledge',
|
||||
component: () => import('@/views/Knowledge.vue'),
|
||||
meta: { title: '知识库', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'assignment-mode',
|
||||
name: 'AssignmentMode',
|
||||
@@ -140,6 +146,20 @@ const routes = [
|
||||
component: () => import('@/views/MfaManage.vue'),
|
||||
meta: { title: 'MFA 管理', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
// 满意度评价统计
|
||||
path: 'evaluation-stats',
|
||||
name: 'EvaluationStats',
|
||||
component: () => import('@/views/EvaluationStats.vue'),
|
||||
meta: { title: '满意度评价', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
// 知识库优化建议 (P2-13)
|
||||
path: 'knowledge-suggestions',
|
||||
name: 'KnowledgeSuggestions',
|
||||
component: () => import('@/views/KnowledgeSuggestions.vue'),
|
||||
meta: { title: '知识库建议', requiresAuth: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -68,13 +68,15 @@ export const useAdminStore = defineStore('admin', () => {
|
||||
async function login(inputUserId: string, password: string, otpCode?: string): Promise<void> {
|
||||
logging.value = true
|
||||
try {
|
||||
// 注意:后端 AgentLogin Schema 要求必填字段 name
|
||||
const response = await apiClient.post('/agents/login', {
|
||||
user_id: inputUserId,
|
||||
name: inputUserId, // 后端必填字段,使用 user_id 作为默认值
|
||||
password: password,
|
||||
otp_code: otpCode || undefined,
|
||||
})
|
||||
|
||||
const data = response.data.data
|
||||
const data = response
|
||||
const agentInfoData = data.agent_info || data
|
||||
|
||||
// 检查是否需要 OTP 验证
|
||||
@@ -143,7 +145,7 @@ export const useAdminStore = defineStore('admin', () => {
|
||||
try {
|
||||
if (!token.value) return
|
||||
const response = await apiClient.get('/agents/me')
|
||||
adminInfo.value = response.data.data
|
||||
adminInfo.value = response
|
||||
} catch (error) {
|
||||
console.error('获取管理员信息失败:', error)
|
||||
// 如果是 401 未授权,说明 token 过期,需要重新登录
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// =============================================================================
|
||||
// 企微IT智能服务台 — 知识库状态管理(Pinia Store)
|
||||
// =============================================================================
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { KnowledgeBaseItem } from '@/api/admin'
|
||||
import { getKnowledgeBase, createKnowledgeBase, updateKnowledgeBase, deleteKnowledgeBase } from '@/api/admin'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Store 定义
|
||||
// --------------------------------------------------------------------------
|
||||
export const useKnowledgeStore = defineStore('knowledge', () => {
|
||||
// ==========================================================================
|
||||
// 响应式状态
|
||||
// ==========================================================================
|
||||
|
||||
/** 知识库列表 */
|
||||
const items = ref<KnowledgeBaseItem[]>([])
|
||||
|
||||
/** 当前选中的分类筛选 */
|
||||
const activeCategory = ref<string>('全部')
|
||||
|
||||
/** 搜索关键词 */
|
||||
const keyword = ref<string>('')
|
||||
|
||||
/** 是否正在加载 */
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
// ==========================================================================
|
||||
// 计算属性
|
||||
// ==========================================================================
|
||||
|
||||
/** 按分类筛选后的列表 */
|
||||
const filteredItems = computed(() => {
|
||||
if (activeCategory.value === '全部') {
|
||||
return items.value
|
||||
}
|
||||
return items.value.filter((item) => item.category === activeCategory.value)
|
||||
})
|
||||
|
||||
/** 各分类计数 */
|
||||
const categoryCounts = computed(() => {
|
||||
const counts: Record<string, number> = { '全部': items.value.length }
|
||||
for (const item of items.value) {
|
||||
counts[item.category] = (counts[item.category] || 0) + 1
|
||||
}
|
||||
return counts
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 方法
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 加载知识库列表
|
||||
*/
|
||||
async function loadKnowledge(category?: string, searchKeyword?: string): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getKnowledgeBase(category, searchKeyword)
|
||||
items.value = response.data.data.items
|
||||
} catch (error) {
|
||||
console.error('加载知识库列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分类筛选
|
||||
*/
|
||||
function setCategory(category: string): void {
|
||||
activeCategory.value = category
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置搜索关键词
|
||||
*/
|
||||
function setKeyword(kw: string): void {
|
||||
keyword.value = kw
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建知识库条目
|
||||
*/
|
||||
async function create(
|
||||
data: Omit<KnowledgeBaseItem, 'id' | 'view_count' | 'use_count' | 'created_at' | 'updated_at'>
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await createKnowledgeBase(data)
|
||||
await loadKnowledge()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('创建知识库条目失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识库条目
|
||||
*/
|
||||
async function update(id: string, data: Partial<KnowledgeBaseItem>): Promise<boolean> {
|
||||
try {
|
||||
await updateKnowledgeBase(id, data)
|
||||
await loadKnowledge()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('更新知识库条目失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识库条目
|
||||
*/
|
||||
async function remove(id: string): Promise<boolean> {
|
||||
try {
|
||||
await deleteKnowledgeBase(id)
|
||||
await loadKnowledge()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除知识库条目失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 返回
|
||||
// ==========================================================================
|
||||
return {
|
||||
// 状态
|
||||
items,
|
||||
activeCategory,
|
||||
keyword,
|
||||
loading,
|
||||
|
||||
// 计算属性
|
||||
filteredItems,
|
||||
categoryCounts,
|
||||
|
||||
// 方法
|
||||
loadKnowledge,
|
||||
setCategory,
|
||||
setKeyword,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
}
|
||||
})
|
||||
@@ -42,6 +42,7 @@
|
||||
:agents="agentStore.filteredAgents"
|
||||
:loading="agentStore.loading"
|
||||
@edit="openEditDialog"
|
||||
@reset-password="openResetPasswordDialog"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
|
||||
@@ -165,6 +166,45 @@
|
||||
<el-button type="primary" @click="handleAddSave">确认添加</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- 重置密码对话框 -->
|
||||
<!-- ================================================================ -->
|
||||
<el-dialog
|
||||
v-model="resetPasswordDialogVisible"
|
||||
title="重置密码"
|
||||
width="400px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form v-if="resetPasswordAgent" label-position="top">
|
||||
<el-form-item label="坐席">
|
||||
<el-input :model-value="resetPasswordAgent.name" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" required>
|
||||
<el-input
|
||||
v-model="resetPasswordForm.newPassword"
|
||||
type="password"
|
||||
placeholder="请输入新密码(6-128位)"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" required>
|
||||
<el-input
|
||||
v-model="resetPasswordForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入新密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="resetPasswordDialogVisible = false">取消</el-button>
|
||||
<el-button type="warning" @click="handleResetPassword">确认重置</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -177,7 +217,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import AgentTable from '@/components/AgentTable.vue'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { unbindOtp as unbindOtpApi } from '@/api/admin'
|
||||
import { unbindOtp as unbindOtpApi, resetAgentPassword } from '@/api/admin'
|
||||
import type { Agent, AgentFilterStatus } from '@/types'
|
||||
import { SKILL_TAGS } from '@/types'
|
||||
|
||||
@@ -276,6 +316,48 @@ const addForm = reactive({
|
||||
maxLoad: 5,
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 重置密码对话框
|
||||
// ==========================================================================
|
||||
const resetPasswordDialogVisible = ref<boolean>(false)
|
||||
const resetPasswordAgent = ref<Agent | null>(null)
|
||||
const resetPasswordForm = reactive({
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
/** 打开重置密码对话框 */
|
||||
function openResetPasswordDialog(agent: Agent): void {
|
||||
resetPasswordAgent.value = agent
|
||||
resetPasswordForm.newPassword = ''
|
||||
resetPasswordForm.confirmPassword = ''
|
||||
resetPasswordDialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 执行重置密码 */
|
||||
async function handleResetPassword(): Promise<void> {
|
||||
if (!resetPasswordAgent.value) return
|
||||
|
||||
// 验证密码
|
||||
if (!resetPasswordForm.newPassword || resetPasswordForm.newPassword.length < 6) {
|
||||
ElMessage.error('密码长度不能少于6位')
|
||||
return
|
||||
}
|
||||
if (resetPasswordForm.newPassword !== resetPasswordForm.confirmPassword) {
|
||||
ElMessage.error('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await resetAgentPassword(resetPasswordAgent.value.id, resetPasswordForm.newPassword)
|
||||
ElMessage.success('密码已重置')
|
||||
resetPasswordDialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error)
|
||||
ElMessage.error('重置密码失败')
|
||||
}
|
||||
}
|
||||
|
||||
const addFormRules: FormRules = {
|
||||
userId: [{ required: true, message: '请输入企微用户ID', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
<template>
|
||||
<!-- ================================================================== -->
|
||||
<!-- 满意度评价统计页面 — 展示评价数据与详细记录 -->
|
||||
<!-- ================================================================== -->
|
||||
<div class="evaluation-stats">
|
||||
<!-- 统计卡片区域 -->
|
||||
<div class="stats-cards">
|
||||
<!-- 总评价数 -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon total-icon">
|
||||
<el-icon><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<div class="stat-card-value">{{ stats.total_count }}</div>
|
||||
<div class="stat-card-label">总评价数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平均星级 -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon star-icon">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<div class="stat-card-value">{{ stats.avg_star_rating }}</div>
|
||||
<div class="stat-card-label">平均星级</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 满意占比 -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon satisfied-icon">
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<div class="stat-card-value">{{ satisfiedPercentage }}%</div>
|
||||
<div class="stat-card-label">满意</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 一般占比 -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon neutral-icon">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<div class="stat-card-value">{{ neutralPercentage }}%</div>
|
||||
<div class="stat-card-label">一般</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 不满意占比 -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon dissatisfied-icon">
|
||||
<el-icon><CircleClose /></el-icon>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<div class="stat-card-value">{{ dissatisfiedPercentage }}%</div>
|
||||
<div class="stat-card-label">不满意</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细数据区域 -->
|
||||
<div class="detail-section">
|
||||
<!-- 星级分布 -->
|
||||
<el-card class="distribution-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>星级分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="star-bars">
|
||||
<div v-for="item in stats.star_distribution" :key="item.label" class="star-bar-item">
|
||||
<span class="star-label">{{ item.label }}</span>
|
||||
<el-progress
|
||||
:percentage="item.percentage"
|
||||
:stroke-width="12"
|
||||
:show-text="false"
|
||||
:color="starColor(item.label)"
|
||||
/>
|
||||
<span class="star-count">{{ item.count }} ({{ item.percentage }}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 表情分布 -->
|
||||
<el-card class="distribution-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>满意度分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="emoji-bars">
|
||||
<div v-for="item in stats.emoji_distribution" :key="item.label" class="emoji-bar-item">
|
||||
<span class="emoji-label">
|
||||
<el-icon v-if="item.label === '满意'" color="#67c23a"><CircleCheck /></el-icon>
|
||||
<el-icon v-else-if="item.label === '一般'" color="#e6a23c"><InfoFilled /></el-icon>
|
||||
<el-icon v-else color="#f56c6c"><CircleClose /></el-icon>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<el-progress
|
||||
:percentage="item.percentage"
|
||||
:stroke-width="12"
|
||||
:show-text="false"
|
||||
:color="emojiColor(item.label)"
|
||||
/>
|
||||
<span class="emoji-count">{{ item.count }} ({{ item.percentage }}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 评价记录列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>评价记录</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="records" v-loading="loading" stripe style="width: 100%">
|
||||
<el-table-column prop="employee_name" label="评价员工" width="120" />
|
||||
<el-table-column prop="star_rating" label="星级" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-rate v-model="row.star_rating" disabled text-color="#ff9900" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="emoji" label="满意度" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="emojiTagType(row.emoji)" size="small">
|
||||
{{ emojiLabel(row.emoji) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="feedback_text" label="反馈内容" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.feedback_text" style="color: var(--text-secondary)">
|
||||
{{ row.feedback_text }}
|
||||
</span>
|
||||
<span v-else style="color: var(--text-muted)">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="评价时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadStats"
|
||||
@current-change="loadStats"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// =============================================================================
|
||||
// 满意度评价统计页面逻辑
|
||||
// =============================================================================
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Document, Star, CircleCheck, CircleClose, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { getEvaluationStats, type EvaluationStatsItem, type EvaluationRecord } from '@/api/admin'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// ---------- 状态 ----------
|
||||
const loading = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
// 统计数据
|
||||
const stats = ref<{
|
||||
total_count: number
|
||||
avg_star_rating: number
|
||||
star_distribution: EvaluationStatsItem[]
|
||||
emoji_distribution: EvaluationStatsItem[]
|
||||
}>({
|
||||
total_count: 0,
|
||||
avg_star_rating: 0,
|
||||
star_distribution: [],
|
||||
emoji_distribution: [],
|
||||
})
|
||||
|
||||
// 评价记录
|
||||
const records = ref<EvaluationRecord[]>([])
|
||||
|
||||
// ---------- 计算属性 ----------
|
||||
const satisfiedPercentage = computed(() => {
|
||||
const item = stats.value.emoji_distribution.find(e => e.label === '满意')
|
||||
return item ? item.percentage : 0
|
||||
})
|
||||
|
||||
const neutralPercentage = computed(() => {
|
||||
const item = stats.value.emoji_distribution.find(e => e.label === '一般')
|
||||
return item ? item.percentage : 0
|
||||
})
|
||||
|
||||
const dissatisfiedPercentage = computed(() => {
|
||||
const item = stats.value.emoji_distribution.find(e => e.label === '不满意')
|
||||
return item ? item.percentage : 0
|
||||
})
|
||||
|
||||
// ---------- 数据加载 ----------
|
||||
async function loadStats() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await getEvaluationStats({
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
})
|
||||
if (data.code === 0) {
|
||||
stats.value = {
|
||||
total_count: data.data.total_count,
|
||||
avg_star_rating: data.data.avg_star_rating,
|
||||
star_distribution: data.data.star_distribution || [],
|
||||
emoji_distribution: data.data.emoji_distribution || [],
|
||||
}
|
||||
records.value = data.data.recent_evaluations || []
|
||||
total.value = data.data.total_count
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error('加载评价统计数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 工具函数 ----------
|
||||
function starColor(label: string): string {
|
||||
const starNum = parseInt(label.replace('星', ''))
|
||||
if (starNum >= 4) return '#67c23a'
|
||||
if (starNum >= 3) return '#e6a23c'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
function emojiColor(label: string): string {
|
||||
if (label === '满意') return '#67c23a'
|
||||
if (label === '一般') return '#e6a23c'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
function emojiTagType(emoji: string): 'success' | 'warning' | 'danger' {
|
||||
if (emoji === 'satisfied') return 'success'
|
||||
if (emoji === 'neutral') return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
function emojiLabel(emoji: string): string {
|
||||
const map: Record<string, string> = {
|
||||
satisfied: '满意',
|
||||
neutral: '一般',
|
||||
dissatisfied: '不满意',
|
||||
}
|
||||
return map[emoji] || emoji
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr: string): string {
|
||||
if (!dateStr) return '—'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
const second = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||
}
|
||||
|
||||
// ---------- 生命周期 ----------
|
||||
onMounted(() => loadStats())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.evaluation-stats {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 统计卡片区域 */
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.stat-card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.total-icon {
|
||||
background: linear-gradient(135deg, #409eff, #67b3ff);
|
||||
}
|
||||
|
||||
.star-icon {
|
||||
background: linear-gradient(135deg, #e6a23c, #f0c78a);
|
||||
}
|
||||
|
||||
.satisfied-icon {
|
||||
background: linear-gradient(135deg, #67c23a, #85ce61);
|
||||
}
|
||||
|
||||
.neutral-icon {
|
||||
background: linear-gradient(135deg, #e6a23c, #ebb563);
|
||||
}
|
||||
|
||||
.dissatisfied-icon {
|
||||
background: linear-gradient(135deg, #f56c6c, #f78989);
|
||||
}
|
||||
|
||||
.stat-card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-card-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-card-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 详细数据区域 */
|
||||
.detail-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.distribution-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.star-bars,
|
||||
.emoji-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.star-bar-item,
|
||||
.emoji-bar-item {
|
||||
display: grid;
|
||||
grid-template-columns: 50px 1fr 120px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.star-label,
|
||||
.emoji-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.star-count,
|
||||
.emoji-count {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 表格区域 */
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
.stats-cards {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.star-bar-item,
|
||||
.emoji-bar-item {
|
||||
grid-template-columns: 40px 1fr;
|
||||
}
|
||||
|
||||
.star-count,
|
||||
.emoji-count {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<!--
|
||||
=============================================================================
|
||||
企微IT智能服务台 — 知识库管理页
|
||||
=============================================================================
|
||||
说明:知识库FAQ管理
|
||||
- 分类标签筛选
|
||||
- 搜索功能
|
||||
- CRUD操作
|
||||
-->
|
||||
<template>
|
||||
<div class="knowledge-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-title">知识库管理</div>
|
||||
<div class="page-desc">FAQ手动维护 + 命中统计。分类管理、标签管理、搜索功能。</div>
|
||||
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="knowledge-toolbar">
|
||||
<!-- 搜索框 -->
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索问题标题或答案内容..."
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 分类筛选 -->
|
||||
<div class="category-tabs">
|
||||
<el-button
|
||||
v-for="cat in categories"
|
||||
:key="cat"
|
||||
:type="knowledgeStore.activeCategory === cat ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="handleCategoryChange(cat)"
|
||||
>
|
||||
{{ cat }} ({{ getCategoryCount(cat) }})
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<el-button type="primary" @click="openCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增知识
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="knowledgeStore.loading" class="loading-state">
|
||||
<el-skeleton :rows="5" animated />
|
||||
</div>
|
||||
|
||||
<!-- 知识列表 -->
|
||||
<div v-else-if="knowledgeStore.filteredItems.length > 0" class="knowledge-list">
|
||||
<el-card
|
||||
v-for="item in knowledgeStore.filteredItems"
|
||||
:key="item.id"
|
||||
class="knowledge-card"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<el-tag size="small" type="info">{{ item.category }}</el-tag>
|
||||
<span class="title-text">{{ item.title }}</span>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button size="small" @click="openEditDialog(item)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(item)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="card-content">
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="card-footer">
|
||||
<el-tag v-for="tag in item.tags" :key="tag" size="small" class="tag-item">
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<span class="stats">
|
||||
<el-icon><View /></el-icon> {{ item.view_count }}
|
||||
<el-icon><Document /></el-icon> {{ item.use_count }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-else description="暂无知识库条目" />
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<!-- ================================================================ -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑知识' : '新增知识'"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form :model="form" label-position="top">
|
||||
<el-form-item label="分类" required>
|
||||
<el-select v-model="form.category" placeholder="请选择分类" style="width: 100%">
|
||||
<el-option
|
||||
v-for="cat in categories.slice(1)"
|
||||
:key="cat"
|
||||
:label="cat"
|
||||
:value="cat"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="问题标题" required>
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
placeholder="请输入问题标题,如:VPN连接失败怎么办?"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="答案内容" required>
|
||||
<el-input
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入答案内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签(用逗号分隔)">
|
||||
<el-input
|
||||
v-model="form.tagsInput"
|
||||
placeholder="如:VPN, 网络, 连接(用逗号分隔)"
|
||||
@blur="handleTagsBlur"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
||||
{{ isEdit ? '保存' : '创建' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// ==========================================================================
|
||||
// 依赖导入
|
||||
// ==========================================================================
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useKnowledgeStore } from '@/stores/knowledge'
|
||||
import type { KnowledgeBaseItem } from '@/api/admin'
|
||||
|
||||
// ==========================================================================
|
||||
// Store
|
||||
// ==========================================================================
|
||||
const knowledgeStore = useKnowledgeStore()
|
||||
|
||||
// ==========================================================================
|
||||
// 常量
|
||||
// ==========================================================================
|
||||
const categories = ['全部', '电脑', '软件', '外设', '网络', '安全', '资产', '其他']
|
||||
|
||||
// ==========================================================================
|
||||
// 搜索和筛选
|
||||
// ==========================================================================
|
||||
const searchKeyword = ref<string>('')
|
||||
|
||||
function handleSearch(): void {
|
||||
knowledgeStore.loadKnowledge(
|
||||
knowledgeStore.activeCategory === '全部' ? undefined : knowledgeStore.activeCategory,
|
||||
searchKeyword.value || undefined
|
||||
)
|
||||
}
|
||||
|
||||
function handleCategoryChange(cat: string): void {
|
||||
knowledgeStore.setCategory(cat)
|
||||
}
|
||||
|
||||
function getCategoryCount(cat: string): number {
|
||||
if (cat === '全部') return knowledgeStore.items.length
|
||||
return knowledgeStore.items.filter((item) => item.category === cat).length
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 新增/编辑对话框
|
||||
// ==========================================================================
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const isEdit = ref<boolean>(false)
|
||||
const loading = ref<boolean>(false)
|
||||
const editingId = ref<string>('')
|
||||
|
||||
const form = ref({
|
||||
category: '其他',
|
||||
title: '',
|
||||
content: '',
|
||||
tagsInput: '',
|
||||
tags: [] as string[],
|
||||
})
|
||||
|
||||
function openCreateDialog(): void {
|
||||
isEdit.value = false
|
||||
editingId.value = ''
|
||||
form.value = {
|
||||
category: '其他',
|
||||
title: '',
|
||||
content: '',
|
||||
tagsInput: '',
|
||||
tags: [],
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog(item: KnowledgeBaseItem): void {
|
||||
isEdit.value = true
|
||||
editingId.value = item.id
|
||||
form.value = {
|
||||
category: item.category,
|
||||
title: item.title,
|
||||
content: item.content,
|
||||
tagsInput: item.tags.join(', '),
|
||||
tags: item.tags,
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleTagsBlur(): void {
|
||||
if (form.value.tagsInput.trim()) {
|
||||
form.value.tags = form.value.tagsInput.split(',').map((v) => v.trim()).filter((v) => v)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(): Promise<void> {
|
||||
if (!form.value.title.trim()) {
|
||||
ElMessage.warning('请输入问题标题')
|
||||
return
|
||||
}
|
||||
if (!form.value.content.trim()) {
|
||||
ElMessage.warning('请输入答案内容')
|
||||
return
|
||||
}
|
||||
|
||||
handleTagsBlur()
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const data = {
|
||||
category: form.value.category,
|
||||
title: form.value.title,
|
||||
content: form.value.content,
|
||||
tags: form.value.tags,
|
||||
}
|
||||
|
||||
let success: boolean
|
||||
if (isEdit.value) {
|
||||
success = await knowledgeStore.update(editingId.value, data)
|
||||
} else {
|
||||
success = await knowledgeStore.create(data)
|
||||
}
|
||||
|
||||
if (success) {
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
} else {
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(item: KnowledgeBaseItem): Promise<void> {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除知识 "${item.title}" 吗?`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
const success = await knowledgeStore.remove(item.id)
|
||||
if (success) {
|
||||
ElMessage.success('删除成功')
|
||||
} else {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 初始化
|
||||
// ==========================================================================
|
||||
onMounted(() => {
|
||||
knowledgeStore.loadKnowledge()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 工具栏 */
|
||||
.knowledge-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 分类标签 */
|
||||
.category-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 知识列表 */
|
||||
.knowledge-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 知识卡片 */
|
||||
.knowledge-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-content p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
padding: 24px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,509 @@
|
||||
<!--
|
||||
=============================================================================
|
||||
企微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>
|
||||
@@ -1,11 +1,11 @@
|
||||
<!--
|
||||
=============================================================================
|
||||
IT智能服务台 — 管理员登录页 (v1.1, 2026-07-04)
|
||||
IT智能服务台 — 管理员登录页 (v1.2, 2026-07-06)
|
||||
=============================================================================
|
||||
说明:从用户ID+姓名登录改为账号密码+OTP表单登录
|
||||
说明:智能检测企微登录状态,提供三种登录方式
|
||||
- PRD v1.5 §4.5: 企微免密登录/企微扫码登录/账号密码+OTP
|
||||
- 复用坐席端登录 API(POST /api/agents/login)
|
||||
- 登录后检查 role === 'admin',非 admin 提示"无管理权限"
|
||||
- 支持账号密码+OTP认证
|
||||
- 企微免密登录:检测到企微登录账号且有管理员角色,免密直接进入
|
||||
-->
|
||||
<template>
|
||||
<div class="login-page">
|
||||
@@ -25,6 +25,50 @@ IT智能服务台 — 管理员登录页 (v1.1, 2026-07-04)
|
||||
<p class="login-subtitle">管理后台</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录方式选择(企微已登录时显示) -->
|
||||
<div v-if="showLoginOptions" class="login-options">
|
||||
<p class="login-options-title">选择登录方式</p>
|
||||
<div class="login-options-btns">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="option-btn"
|
||||
:loading="wecomQuickLoading"
|
||||
@click="handleWecomQuickLogin"
|
||||
>
|
||||
<el-icon><Key /></el-icon>
|
||||
企微免密登录
|
||||
</el-button>
|
||||
<el-button
|
||||
size="large"
|
||||
class="option-btn"
|
||||
@click="showQrLogin"
|
||||
>
|
||||
<el-icon><Key /></el-icon>
|
||||
企微扫码登录
|
||||
</el-button>
|
||||
<el-button
|
||||
size="large"
|
||||
class="option-btn"
|
||||
@click="showPasswordLogin"
|
||||
>
|
||||
<el-icon><Lock /></el-icon>
|
||||
账号密码登录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 企微扫码登录面板 -->
|
||||
<div v-if="showQrPanel" class="qr-login">
|
||||
<p class="qr-title">企微扫码登录</p>
|
||||
<div class="qr-code">
|
||||
<img v-if="qrCode" :src="qrCode" alt="扫码登录二维码" />
|
||||
<div v-else class="qr-loading">加载中...</div>
|
||||
</div>
|
||||
<p class="qr-hint">请使用企业微信扫码登录</p>
|
||||
<el-button link type="primary" @click="showPasswordLogin">其他登录方式</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<el-form
|
||||
ref="formRef"
|
||||
@@ -105,7 +149,7 @@ IT智能服务台 — 管理员登录页 (v1.1, 2026-07-04)
|
||||
// ==========================================================================
|
||||
// 依赖导入
|
||||
// ==========================================================================
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
@@ -138,6 +182,31 @@ const requireOtp = ref(false)
|
||||
/** 错误信息 */
|
||||
const errorMsg = ref<string>('')
|
||||
|
||||
// ==========================================================================
|
||||
// 企微智能检测相关状态(PRD v1.5 §4.5)
|
||||
// ==========================================================================
|
||||
|
||||
/** 是否显示登录方式选择 */
|
||||
const showLoginOptions = ref(false)
|
||||
|
||||
/** 是否显示扫码登录面板 */
|
||||
const showQrPanel = ref(false)
|
||||
|
||||
/** 是否显示账号密码登录 */
|
||||
const showPasswordPanel = ref(true)
|
||||
|
||||
/** 企微用户ID(用于免密登录) */
|
||||
const wecomUserId = ref('')
|
||||
|
||||
/** 企微快捷登录loading */
|
||||
const wecomQuickLoading = ref(false)
|
||||
|
||||
/** 企微二维码 */
|
||||
const qrCode = ref('')
|
||||
|
||||
/** 企微检测中 */
|
||||
const wecomChecking = ref(false)
|
||||
|
||||
/** 表单校验规则 */
|
||||
const rules: FormRules = {
|
||||
userId: [
|
||||
@@ -156,6 +225,186 @@ const rules: FormRules = {
|
||||
// 方法
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 检测企微客户端登录状态(PRD v1.5 §4.5)
|
||||
* 页面加载时自动检测
|
||||
*/
|
||||
async function checkWecomClient(): Promise<void> {
|
||||
wecomChecking.value = true
|
||||
|
||||
try {
|
||||
// 首先尝试使用 UA 检测
|
||||
const isWxWork = /wxwork/i.test(navigator.userAgent)
|
||||
|
||||
// 不在企微环境,直接显示账号密码登录
|
||||
if (!isWxWork) {
|
||||
showLoginOptions.value = false
|
||||
showPasswordPanel.value = true
|
||||
wecomChecking.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 在企微客户端中,尝试使用企微 JS-SDK
|
||||
if (typeof (window as any).wx !== 'undefined') {
|
||||
try {
|
||||
// 获取当前页面 URL
|
||||
const currentUrl = window.location.href.split('#')[0]
|
||||
|
||||
// 调用后端接口获取 JS-SDK 签名配置
|
||||
const apiClient = (await import('@/api')).default
|
||||
const configResponse = await apiClient.get('/wecom/jsapi-config', {
|
||||
params: { url: currentUrl }
|
||||
})
|
||||
|
||||
const jsConfig = configResponse
|
||||
console.log('[Admin Login] 获取到企微 JS-SDK 配置:', jsConfig)
|
||||
|
||||
// 使用 wx.config 配置企微 JS-SDK
|
||||
await new Promise<void>((resolve) => {
|
||||
(window as any).wx.config({
|
||||
beta: true,
|
||||
debug: false,
|
||||
appId: jsConfig.corp_id,
|
||||
timestamp: jsConfig.timestamp,
|
||||
nonceStr: jsConfig.nonce_str,
|
||||
signature: jsConfig.signature,
|
||||
jsApiList: ['agentConfig']
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
|
||||
// 使用 wx.agentConfig 获取当前用户信息
|
||||
const userInfo = await new Promise<any>((resolve, reject) => {
|
||||
(window as any).wx.agentConfig({
|
||||
corpid: jsConfig.corp_id,
|
||||
agentid: jsConfig.agent_id,
|
||||
timestamp: jsConfig.timestamp,
|
||||
nonceStr: jsConfig.nonce_str,
|
||||
signature: jsConfig.signature,
|
||||
success: (res: any) => resolve(res),
|
||||
fail: (res: any) => reject(new Error(res.errMsg || 'agentConfig 失败'))
|
||||
})
|
||||
})
|
||||
|
||||
// 获取用户ID
|
||||
const userId = userInfo.userId
|
||||
if (userId) {
|
||||
console.log('[Admin Login] 获取到企微用户 ID:', userId)
|
||||
wecomUserId.value = userId
|
||||
|
||||
// 检测角色
|
||||
const roleResponse = await apiClient.get('/wecom/check-role', {
|
||||
params: { userid: userId }
|
||||
})
|
||||
const roleData = roleResponse
|
||||
console.log('[Admin Login] 用户角色:', roleData)
|
||||
|
||||
// 如果是管理员,显示免密登录选项
|
||||
if (roleData.role === 'admin') {
|
||||
showLoginOptions.value = true
|
||||
showPasswordPanel.value = false
|
||||
} else {
|
||||
// 非管理员,显示普通登录
|
||||
showLoginOptions.value = false
|
||||
showPasswordPanel.value = true
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Admin Login] 企微 JS-SDK 检测失败:', e)
|
||||
// 检测失败,显示登录选项
|
||||
showLoginOptions.value = true
|
||||
showPasswordPanel.value = true
|
||||
}
|
||||
} else {
|
||||
// 没有企微 JS-SDK
|
||||
showLoginOptions.value = true
|
||||
showPasswordPanel.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('企微客户端检测失败:', error)
|
||||
showLoginOptions.value = false
|
||||
showPasswordPanel.value = true
|
||||
} finally {
|
||||
wecomChecking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 企微免密登录
|
||||
*/
|
||||
async function handleWecomQuickLogin(): Promise<void> {
|
||||
wecomQuickLoading.value = true
|
||||
|
||||
try {
|
||||
const apiClient = (await import('@/api')).default
|
||||
|
||||
// 调用后端企微免密登录接口
|
||||
const response = await apiClient.post('/auth_wecom/jsdk-login', {
|
||||
userid: wecomUserId.value,
|
||||
login_source: 'wecom_jsdk_admin'
|
||||
})
|
||||
|
||||
const result = response
|
||||
|
||||
if (result?.token) {
|
||||
const { token, employee_id, name } = result
|
||||
|
||||
// 存储 token
|
||||
localStorage.setItem('admin_token', token)
|
||||
if (employee_id) {
|
||||
localStorage.setItem('admin_user_id', employee_id)
|
||||
}
|
||||
|
||||
// 存储到 store
|
||||
adminStore.token = token
|
||||
adminStore.adminUserId = employee_id
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} else {
|
||||
throw new Error('免密登录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('企微免密登录失败:', error)
|
||||
ElMessage.error('免密登录失败,请使用其他方式')
|
||||
} finally {
|
||||
wecomQuickLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示扫码登录
|
||||
*/
|
||||
async function showQrLogin(): Promise<void> {
|
||||
showLoginOptions.value = false
|
||||
showQrPanel.value = true
|
||||
showPasswordPanel.value = false
|
||||
|
||||
// 获取二维码
|
||||
try {
|
||||
const apiClient = (await import('@/api')).default
|
||||
const response = await apiClient.post('/auth_qrcode/create')
|
||||
const result = response
|
||||
if (result.code === 0) {
|
||||
qrCode.value = result.data.qrcode_png_base64
|
||||
? `data:image/png;base64,${result.data.qrcode_png_base64}`
|
||||
: result.data.qrcode_url
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取二维码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示账号密码登录
|
||||
*/
|
||||
function showPasswordLogin(): Promise<void> {
|
||||
showLoginOptions.value = false
|
||||
showQrPanel.value = false
|
||||
showPasswordPanel.value = true
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录
|
||||
*/
|
||||
@@ -185,6 +434,11 @@ async function handleLogin(): Promise<void> {
|
||||
errorMsg.value = errMsg
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检测企微客户端状态
|
||||
onMounted(() => {
|
||||
checkWecomClient()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -274,4 +528,66 @@ async function handleLogin(): Promise<void> {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 登录方式选择 */
|
||||
.login-options {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.login-options-title {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-options-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.option-btn {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 扫码登录 */
|
||||
.qr-login {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.qr-title {
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
margin: 0 auto 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qr-code img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.qr-hint {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -221,8 +221,8 @@ async function loadMatrix() {
|
||||
loading.value = true
|
||||
try {
|
||||
const resp = await apiClient.get('/admin/roles/permissions/matrix')
|
||||
if (resp.data?.code === 0 && resp.data.data) {
|
||||
matrixData.value = resp.data.data
|
||||
// 拦截器已统一返回 inner data(Scheme A),失败会 reject,故成功即有效数据
|
||||
matrixData.value = resp
|
||||
} else {
|
||||
ElMessage.error('拉取权限矩阵失败')
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<el-button type="primary">
|
||||
<el-button type="primary" @click="createDialogVisible = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增模板
|
||||
</el-button>
|
||||
@@ -54,6 +54,61 @@
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-else description="暂无待审核的快速回复模板" />
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- 新增模板对话框 -->
|
||||
<!-- ================================================================ -->
|
||||
<el-dialog
|
||||
v-model="createDialogVisible"
|
||||
title="新增快速回复模板"
|
||||
width="520px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form :model="createForm" label-position="top">
|
||||
<el-form-item label="分类" required>
|
||||
<el-select v-model="createForm.category" placeholder="请选择分类">
|
||||
<el-option
|
||||
v-for="cat in QUICK_REPLY_CATEGORIES"
|
||||
:key="cat"
|
||||
:label="cat"
|
||||
:value="cat"
|
||||
:disabled="cat === '全部'"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板标题" required>
|
||||
<el-input
|
||||
v-model="createForm.title"
|
||||
placeholder="请输入模板标题,如:VPN连接失败排查"
|
||||
maxlength="128"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板内容" required>
|
||||
<el-input
|
||||
v-model="createForm.content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入模板内容,支持变量替换,如:{employee_name}、{department}等"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="可用变量(用逗号分隔)">
|
||||
<el-input
|
||||
v-model="createForm.variables"
|
||||
placeholder="如:employee_name, department(用逗号分隔)"
|
||||
@blur="handleVariablesBlur"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序权重">
|
||||
<el-input-number v-model="createForm.sort_order" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="handleCreateConfirm">确认创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- 驳回原因对话框 -->
|
||||
<!-- ================================================================ -->
|
||||
@@ -92,6 +147,7 @@ import QuickReplyCard from '@/components/QuickReplyCard.vue'
|
||||
import { useQuickReplyStore } from '@/stores/quickReply'
|
||||
import type { QuickReplyTemplate } from '@/types'
|
||||
import { QUICK_REPLY_CATEGORIES } from '@/types'
|
||||
import { createQuickReplyTemplate } from '@/api/admin'
|
||||
|
||||
// ==========================================================================
|
||||
// Store
|
||||
@@ -103,6 +159,19 @@ const quickReplyStore = useQuickReplyStore()
|
||||
// ==========================================================================
|
||||
const categories = [...QUICK_REPLY_CATEGORIES]
|
||||
|
||||
// ==========================================================================
|
||||
// 新增模板对话框
|
||||
// ==========================================================================
|
||||
const createDialogVisible = ref<boolean>(false)
|
||||
const createForm = ref({
|
||||
category: '其他',
|
||||
title: '',
|
||||
content: '',
|
||||
variables: [] as string[],
|
||||
sort_order: 0,
|
||||
})
|
||||
const createLoading = ref<boolean>(false)
|
||||
|
||||
// ==========================================================================
|
||||
// 驳回对话框
|
||||
// ==========================================================================
|
||||
@@ -125,6 +194,71 @@ function getCategoryCount(cat: string): number {
|
||||
return quickReplyStore.replies.filter((r) => r.category === cat).length
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 新增模板操作
|
||||
// ==========================================================================
|
||||
|
||||
/** 处理变量输入(将逗号分隔的字符串转为数组) */
|
||||
function handleVariablesBlur(): void {
|
||||
const input = createForm.value.variables as unknown as string
|
||||
if (typeof input === 'string' && input.trim()) {
|
||||
createForm.value.variables = input.split(',').map((v) => v.trim()).filter((v) => v)
|
||||
}
|
||||
}
|
||||
|
||||
/** 确认创建 */
|
||||
async function handleCreateConfirm(): Promise<void> {
|
||||
if (!createForm.value.title.trim()) {
|
||||
ElMessage.warning('请输入模板标题')
|
||||
return
|
||||
}
|
||||
if (!createForm.value.content.trim()) {
|
||||
ElMessage.warning('请输入模板内容')
|
||||
return
|
||||
}
|
||||
|
||||
createLoading.value = true
|
||||
try {
|
||||
// 处理变量
|
||||
const variables = Array.isArray(createForm.value.variables)
|
||||
? createForm.value.variables
|
||||
: (createForm.value.variables as unknown as string)?.split(',').map((v: string) => v.trim()).filter((v: string) => v) || []
|
||||
|
||||
await createQuickReplyTemplate({
|
||||
id: '',
|
||||
category: createForm.value.category,
|
||||
title: createForm.value.title,
|
||||
content: createForm.value.content,
|
||||
variables,
|
||||
sort_order: createForm.value.sort_order,
|
||||
status: 'approved',
|
||||
version: 1,
|
||||
submitted_by: '',
|
||||
submitted_by_name: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
} as QuickReplyTemplate)
|
||||
|
||||
ElMessage.success('创建成功')
|
||||
createDialogVisible.value = false
|
||||
// 重置表单
|
||||
createForm.value = {
|
||||
category: '其他',
|
||||
title: '',
|
||||
content: '',
|
||||
variables: [],
|
||||
sort_order: 0,
|
||||
}
|
||||
// 刷新列表
|
||||
quickReplyStore.loadReplies()
|
||||
} catch (error) {
|
||||
console.error('创建失败:', error)
|
||||
ElMessage.error('创建失败')
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 审核操作
|
||||
// ==========================================================================
|
||||
|
||||
Reference in New Issue
Block a user