| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- <template>
- <div class="app-container">
- <!-- 搜索栏 -->
- <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
- <el-form-item label="会话ID" prop="sessionId">
- <el-input v-model="queryParams.sessionId" placeholder="请输入会话ID" clearable size="small" @keyup.enter.native="handleQuery" />
- </el-form-item>
- <el-form-item label="评分状态" prop="status">
- <el-select v-model="queryParams.status" placeholder="全部" clearable size="small">
- <el-option label="待评分" value="pending" />
- <el-option label="已评分" value="scored" />
- <el-option label="已复核" value="reviewed" />
- <el-option label="有争议" value="disputed" />
- </el-select>
- </el-form-item>
- <el-form-item label="日期范围" prop="dateRange">
- <el-date-picker v-model="queryParams.dateRange" type="daterange" range-separator="至"
- start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" size="small"
- style="width:240px" />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
- <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
- </el-form-item>
- </el-form>
- <!-- 统计卡片 -->
- <el-row :gutter="16" class="mb8">
- <el-col :span="4">
- <el-card shadow="hover"><div class="stat-card">
- <div class="stat-value">{{ stats.total || 0 }}</div><div class="stat-label">总记录</div>
- </div></el-card>
- </el-col>
- <el-col :span="4">
- <el-card shadow="hover"><div class="stat-card">
- <div class="stat-value" style="color:#E6A23C">{{ stats.pending || 0 }}</div><div class="stat-label">待评分</div>
- </div></el-card>
- </el-col>
- <el-col :span="4">
- <el-card shadow="hover"><div class="stat-card">
- <div class="stat-value" style="color:#67C23A">{{ stats.scored || 0 }}</div><div class="stat-label">已评分</div>
- </div></el-card>
- </el-col>
- <el-col :span="4">
- <el-card shadow="hover"><div class="stat-card">
- <div class="stat-value" style="color:#409EFF">{{ stats.avgScore || '-' }}</div><div class="stat-label">平均评分</div>
- </div></el-card>
- </el-col>
- <el-col :span="4">
- <el-card shadow="hover"><div class="stat-card">
- <div class="stat-value" style="color:#F56C6C">{{ stats.disputed || 0 }}</div><div class="stat-label">有争议</div>
- </div></el-card>
- </el-col>
- </el-row>
- <!-- 工具栏 -->
- <el-row :gutter="10" class="mb8">
- <el-col :span="1.5">
- <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增评分</el-button>
- </el-col>
- <el-col :span="1.5">
- <el-button type="warning" plain icon="el-icon-s-data" size="mini" @click="handleBatchScore">
- 批量评分
- </el-button>
- </el-col>
- <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
- </el-row>
- <!-- 主表格 -->
- <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange" border>
- <el-table-column type="selection" width="50" align="center" />
- <el-table-column label="ID" align="center" prop="id" width="70" />
- <el-table-column label="会话ID" align="center" prop="sessionId" width="100" show-overflow-tooltip />
- <el-table-column label="AI回复内容" align="center" prop="aiReply" min-width="200" show-overflow-tooltip />
- <el-table-column label="准确性" align="center" width="90">
- <template slot-scope="scope">
- <el-rate v-model="scope.row.accuracyScore" disabled :max="5" show-score
- text-color="#ff9900" score-template="{value}" />
- </template>
- </el-table-column>
- <el-table-column label="相关性" align="center" width="90">
- <template slot-scope="scope">
- <el-rate v-model="scope.row.relevanceScore" disabled :max="5" show-score
- text-color="#ff9900" score-template="{value}" />
- </template>
- </el-table-column>
- <el-table-column label="合规性" align="center" width="90">
- <template slot-scope="scope">
- <el-rate v-model="scope.row.complianceScore" disabled :max="5" show-score
- text-color="#ff9900" score-template="{value}" />
- </template>
- </el-table-column>
- <el-table-column label="综合分" align="center" width="80">
- <template slot-scope="scope">
- <el-tag :type="getTotalScoreType(scope.row)">{{ formatTotalScore(scope.row) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="评分状态" align="center" width="90">
- <template slot-scope="scope">
- <el-tag :type="scope.row.status==='pending'?'warning':scope.row.status==='scored'?'success':scope.row.status==='disputed'?'danger':'info'" size="small">
- {{ scope.row.status==='pending'?'待评分':scope.row.status==='scored'?'已评分':scope.row.status==='disputed'?'有争议':'已复核' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="评分人" align="center" prop="reviewer" width="100" />
- <el-table-column label="评分时间" align="center" prop="reviewTime" width="160" />
- <el-table-column label="操作" align="center" width="220" fixed="right">
- <template slot-scope="scope">
- <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)">详情</el-button>
- <el-button size="mini" type="text" icon="el-icon-edit"
- @click="handleScore(scope.row)">{{ scope.row.status === 'pending' ? '评分' : '修改' }}</el-button>
- <el-button size="mini" type="text" icon="el-icon-s-opportunity"
- @click="handleAppeal(scope.row)" v-if="scope.row.status === 'scored'">申诉</el-button>
- <el-button size="mini" type="text" icon="el-icon-delete"
- style="color:#F56C6C" @click="handleDelete(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum"
- :limit.sync="queryParams.pageSize" @pagination="getList" />
- <!-- 详情弹窗 -->
- <el-dialog title="评分详情" :visible.sync="detailVisible" width="800px" append-to-body>
- <template v-if="detail">
- <el-descriptions :column="2" border size="small">
- <el-descriptions-item label="ID">{{ detail.id }}</el-descriptions-item>
- <el-descriptions-item label="会话ID">{{ detail.sessionId }}</el-descriptions-item>
- <el-descriptions-item label="客户消息" :span="2">
- <div style="background:#f5f7fa;padding:8px;border-radius:4px">{{ detail.customerMsg }}</div>
- </el-descriptions-item>
- <el-descriptions-item label="AI回复" :span="2">
- <div style="background:#ecf5ff;padding:8px;border-radius:4px">{{ detail.aiReply }}</div>
- </el-descriptions-item>
- <el-descriptions-item label="准确性评分">{{ detail.accuracyScore }}/5</el-descriptions-item>
- <el-descriptions-item label="相关性评分">{{ detail.relevanceScore }}/5</el-descriptions-item>
- <el-descriptions-item label="合规性评分">{{ detail.complianceScore }}/5</el-descriptions-item>
- <el-descriptions-item label="综合评分">{{ formatTotalScore(detail) }}</el-descriptions-item>
- <el-descriptions-item label="评分说明" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
- <el-descriptions-item label="评分人">{{ detail.reviewer || '-' }}</el-descriptions-item>
- <el-descriptions-item label="评分时间">{{ detail.reviewTime || '-' }}</el-descriptions-item>
- </el-descriptions>
- </template>
- </el-dialog>
- <!-- 评分弹窗 -->
- <el-dialog :title="scoreForm.id ? '修改评分' : 'AI回复质量评分'" :visible.sync="scoreVisible" width="650px" append-to-body>
- <el-form ref="scoreFormRef" :model="scoreForm" :rules="scoreRules" label-width="100px">
- <el-form-item label="会话ID" prop="sessionId">
- <el-input v-model="scoreForm.sessionId" placeholder="请输入会话ID" />
- </el-form-item>
- <el-form-item label="客户消息" prop="customerMsg">
- <el-input v-model="scoreForm.customerMsg" type="textarea" :rows="3" placeholder="请输入客户原始消息" />
- </el-form-item>
- <el-form-item label="AI回复" prop="aiReply">
- <el-input v-model="scoreForm.aiReply" type="textarea" :rows="4" placeholder="请输入AI回复内容" />
- </el-form-item>
- <el-divider content-position="left">评分维度</el-divider>
- <el-form-item label="准确性" prop="accuracyScore">
- <el-rate v-model="scoreForm.accuracyScore" :max="5" show-score text-color="#ff9900" />
- <span class="score-desc">AI回复内容是否准确无误</span>
- </el-form-item>
- <el-form-item label="相关性" prop="relevanceScore">
- <el-rate v-model="scoreForm.relevanceScore" :max="5" show-score text-color="#ff9900" />
- <span class="score-desc">回复是否与客户问题相关</span>
- </el-form-item>
- <el-form-item label="合规性" prop="complianceScore">
- <el-rate v-model="scoreForm.complianceScore" :max="5" show-score text-color="#ff9900" />
- <span class="score-desc">回复是否符合行业合规要求</span>
- </el-form-item>
- <el-form-item label="语气语调">
- <el-rate v-model="scoreForm.toneScore" :max="5" show-score text-color="#ff9900" />
- <span class="score-desc">回复的语气是否友好得体</span>
- </el-form-item>
- <el-form-item label="评分说明" prop="remark">
- <el-input v-model="scoreForm.remark" type="textarea" :rows="3" placeholder="评分理由及改进建议" />
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button @click="scoreVisible = false">取消</el-button>
- <el-button type="primary" @click="submitScore" :loading="submitting">提交评分</el-button>
- </div>
- </el-dialog>
- <!-- 申诉弹窗 -->
- <el-dialog title="评分申诉" :visible.sync="appealVisible" width="500px" append-to-body>
- <el-form :model="appealForm" label-width="80px">
- <el-form-item label="申诉原因">
- <el-input v-model="appealForm.reason" type="textarea" :rows="4" placeholder="请说明申诉原因..." />
- </el-form-item>
- <el-form-item label="建议评分">
- <el-rate v-model="appealForm.suggestedScore" :max="5" show-score text-color="#ff9900" />
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button @click="appealVisible = false">取消</el-button>
- <el-button type="primary" @click="submitAppeal">提交申诉</el-button>
- </div>
- </el-dialog>
- </div>
- </template>
- <script>
- import { listQualityRecords, getQualityRecord, submitQualityReview,
- updateQualityReview, deleteQualityRecord, getQualityStats } from '@/api/workflow/lobster'
- export default {
- name: 'AiChatQuality',
- data() {
- return {
- loading: false, showSearch: true, submitting: false,
- total: 0, list: [], selectedIds: [],
- stats: { total: 0, pending: 0, scored: 0, avgScore: 0, disputed: 0 },
- queryParams: { pageNum: 1, pageSize: 10, sessionId: null, status: null, dateRange: null },
- detailVisible: false, detail: null,
- scoreVisible: false,
- scoreForm: {
- id: null, sessionId: '', customerMsg: '', aiReply: '',
- accuracyScore: 0, relevanceScore: 0, complianceScore: 0,
- toneScore: 0, remark: ''
- },
- scoreRules: {
- sessionId: [{ required: true, message: '会话ID不能为空', trigger: 'blur' }],
- customerMsg: [{ required: true, message: '客户消息不能为空', trigger: 'blur' }],
- aiReply: [{ required: true, message: 'AI回复不能为空', trigger: 'blur' }],
- accuracyScore: [{ required: true, message: '请评分', trigger: 'change' }],
- relevanceScore: [{ required: true, message: '请评分', trigger: 'change' }],
- complianceScore: [{ required: true, message: '请评分', trigger: 'change' }]
- },
- appealVisible: false, appealForm: { recordId: null, reason: '', suggestedScore: 0 }
- }
- },
- created() { this.getList(); this.getStats() },
- methods: {
- async getList() {
- this.loading = true
- try {
- const res = await listQualityRecords(this.queryParams)
- const data = res.data || {}
- this.list = data.rows || data.list || []
- this.total = data.total || 0
- } catch (e) {
- this.list = []
- this.total = 0
- } finally { this.loading = false }
- },
- async getStats() {
- try {
- const res = await getQualityStats()
- this.stats = res.data || {}
- } catch (e) { /* 统计API暂未就绪时静默处理 */ }
- },
- handleQuery() { this.queryParams.pageNum = 1; this.getList() },
- resetQuery() { this.$refs.queryForm.resetFields(); this.handleQuery() },
- handleSelectionChange(rows) { this.selectedIds = rows.map(r => r.id) },
- formatTotalScore(row) {
- const scores = [row.accuracyScore, row.relevanceScore, row.complianceScore].filter(s => s > 0)
- if (scores.length === 0) return '-'
- return (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1)
- },
- getTotalScoreType(row) {
- const s = parseFloat(this.formatTotalScore(row))
- if (isNaN(s)) return 'info'
- if (s >= 4) return 'success'
- if (s >= 3) return 'warning'
- return 'danger'
- },
- handleAdd() {
- this.scoreForm = { id: null, sessionId: '', customerMsg: '', aiReply: '',
- accuracyScore: 3, relevanceScore: 3, complianceScore: 3, toneScore: 3, remark: '' }
- this.scoreVisible = true
- },
- handleScore(row) {
- this.scoreForm = { ...row, id: row.id }
- this.scoreVisible = true
- },
- async submitScore() {
- this.$refs.scoreFormRef.validate(async valid => {
- if (!valid) return
- this.submitting = true
- try {
- const data = { ...this.scoreForm }
- if (this.scoreForm.id) {
- await updateQualityReview(data)
- this.$message.success('评分已更新')
- } else {
- await submitQualityReview(data)
- this.$message.success('评分已提交')
- }
- this.scoreVisible = false
- this.getList()
- this.getStats()
- } catch (e) {
- this.$message.error('操作失败:' + (e.message || ''))
- } finally { this.submitting = false }
- })
- },
- async handleDetail(row) {
- try {
- const res = await getQualityRecord(row.id)
- this.detail = res.data || res
- this.detailVisible = true
- } catch (e) {
- this.$message.error('获取详情失败')
- }
- },
- handleAppeal(row) {
- this.appealForm.recordId = row.id
- this.appealVisible = true
- },
- submitAppeal() {
- this.$message.success('申诉已提交,等待审核')
- this.appealVisible = false
- },
- handleDelete(row) {
- this.$confirm('确认删除该评分记录?', '警告', { type: 'warning' }).then(async () => {
- try {
- await deleteQualityRecord(row.id)
- this.$message.success('删除成功')
- this.getList()
- this.getStats()
- } catch (e) { this.$message.error('删除失败') }
- }).catch(() => {})
- },
- handleBatchScore() {
- if (this.selectedIds.length === 0) {
- this.$message.warning('请至少选择一条记录')
- return
- }
- this.$message.info(`已选择 ${this.selectedIds.length} 条记录,请进入评分弹窗逐条处理`)
- this.handleAdd()
- }
- }
- }
- </script>
- <style scoped>
- .stat-card { text-align: center; padding: 10px 0; }
- .stat-value { font-size: 24px; font-weight: bold; color: #409EFF; }
- .stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
- .score-desc { font-size: 12px; color: #909399; margin-left: 12px; }
- </style>
|