index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. <template>
  2. <div class="app-container">
  3. <!-- 搜索栏 -->
  4. <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
  5. <el-form-item label="会话ID" prop="sessionId">
  6. <el-input v-model="queryParams.sessionId" placeholder="请输入会话ID" clearable size="small" @keyup.enter.native="handleQuery" />
  7. </el-form-item>
  8. <el-form-item label="评分状态" prop="status">
  9. <el-select v-model="queryParams.status" placeholder="全部" clearable size="small">
  10. <el-option label="待评分" value="pending" />
  11. <el-option label="已评分" value="scored" />
  12. <el-option label="已复核" value="reviewed" />
  13. <el-option label="有争议" value="disputed" />
  14. </el-select>
  15. </el-form-item>
  16. <el-form-item label="日期范围" prop="dateRange">
  17. <el-date-picker v-model="queryParams.dateRange" type="daterange" range-separator="至"
  18. start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" size="small"
  19. style="width:240px" />
  20. </el-form-item>
  21. <el-form-item>
  22. <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  23. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  24. </el-form-item>
  25. </el-form>
  26. <!-- 统计卡片 -->
  27. <el-row :gutter="16" class="mb8">
  28. <el-col :span="4">
  29. <el-card shadow="hover"><div class="stat-card">
  30. <div class="stat-value">{{ stats.total || 0 }}</div><div class="stat-label">总记录</div>
  31. </div></el-card>
  32. </el-col>
  33. <el-col :span="4">
  34. <el-card shadow="hover"><div class="stat-card">
  35. <div class="stat-value" style="color:#E6A23C">{{ stats.pending || 0 }}</div><div class="stat-label">待评分</div>
  36. </div></el-card>
  37. </el-col>
  38. <el-col :span="4">
  39. <el-card shadow="hover"><div class="stat-card">
  40. <div class="stat-value" style="color:#67C23A">{{ stats.scored || 0 }}</div><div class="stat-label">已评分</div>
  41. </div></el-card>
  42. </el-col>
  43. <el-col :span="4">
  44. <el-card shadow="hover"><div class="stat-card">
  45. <div class="stat-value" style="color:#409EFF">{{ stats.avgScore || '-' }}</div><div class="stat-label">平均评分</div>
  46. </div></el-card>
  47. </el-col>
  48. <el-col :span="4">
  49. <el-card shadow="hover"><div class="stat-card">
  50. <div class="stat-value" style="color:#F56C6C">{{ stats.disputed || 0 }}</div><div class="stat-label">有争议</div>
  51. </div></el-card>
  52. </el-col>
  53. </el-row>
  54. <!-- 工具栏 -->
  55. <el-row :gutter="10" class="mb8">
  56. <el-col :span="1.5">
  57. <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增评分</el-button>
  58. </el-col>
  59. <el-col :span="1.5">
  60. <el-button type="warning" plain icon="el-icon-s-data" size="mini" @click="handleBatchScore">
  61. 批量评分
  62. </el-button>
  63. </el-col>
  64. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
  65. </el-row>
  66. <!-- 主表格 -->
  67. <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange" border>
  68. <el-table-column type="selection" width="50" align="center" />
  69. <el-table-column label="ID" align="center" prop="id" width="70" />
  70. <el-table-column label="会话ID" align="center" prop="sessionId" width="100" show-overflow-tooltip />
  71. <el-table-column label="AI回复内容" align="center" prop="aiReply" min-width="200" show-overflow-tooltip />
  72. <el-table-column label="准确性" align="center" width="90">
  73. <template slot-scope="scope">
  74. <el-rate v-model="scope.row.accuracyScore" disabled :max="5" show-score
  75. text-color="#ff9900" score-template="{value}" />
  76. </template>
  77. </el-table-column>
  78. <el-table-column label="相关性" align="center" width="90">
  79. <template slot-scope="scope">
  80. <el-rate v-model="scope.row.relevanceScore" disabled :max="5" show-score
  81. text-color="#ff9900" score-template="{value}" />
  82. </template>
  83. </el-table-column>
  84. <el-table-column label="合规性" align="center" width="90">
  85. <template slot-scope="scope">
  86. <el-rate v-model="scope.row.complianceScore" disabled :max="5" show-score
  87. text-color="#ff9900" score-template="{value}" />
  88. </template>
  89. </el-table-column>
  90. <el-table-column label="综合分" align="center" width="80">
  91. <template slot-scope="scope">
  92. <el-tag :type="getTotalScoreType(scope.row)">{{ formatTotalScore(scope.row) }}</el-tag>
  93. </template>
  94. </el-table-column>
  95. <el-table-column label="评分状态" align="center" width="90">
  96. <template slot-scope="scope">
  97. <el-tag :type="scope.row.status==='pending'?'warning':scope.row.status==='scored'?'success':scope.row.status==='disputed'?'danger':'info'" size="small">
  98. {{ scope.row.status==='pending'?'待评分':scope.row.status==='scored'?'已评分':scope.row.status==='disputed'?'有争议':'已复核' }}
  99. </el-tag>
  100. </template>
  101. </el-table-column>
  102. <el-table-column label="评分人" align="center" prop="reviewer" width="100" />
  103. <el-table-column label="评分时间" align="center" prop="reviewTime" width="160" />
  104. <el-table-column label="操作" align="center" width="220" fixed="right">
  105. <template slot-scope="scope">
  106. <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)">详情</el-button>
  107. <el-button size="mini" type="text" icon="el-icon-edit"
  108. @click="handleScore(scope.row)">{{ scope.row.status === 'pending' ? '评分' : '修改' }}</el-button>
  109. <el-button size="mini" type="text" icon="el-icon-s-opportunity"
  110. @click="handleAppeal(scope.row)" v-if="scope.row.status === 'scored'">申诉</el-button>
  111. <el-button size="mini" type="text" icon="el-icon-delete"
  112. style="color:#F56C6C" @click="handleDelete(scope.row)">删除</el-button>
  113. </template>
  114. </el-table-column>
  115. </el-table>
  116. <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum"
  117. :limit.sync="queryParams.pageSize" @pagination="getList" />
  118. <!-- 详情弹窗 -->
  119. <el-dialog title="评分详情" :visible.sync="detailVisible" width="800px" append-to-body>
  120. <template v-if="detail">
  121. <el-descriptions :column="2" border size="small">
  122. <el-descriptions-item label="ID">{{ detail.id }}</el-descriptions-item>
  123. <el-descriptions-item label="会话ID">{{ detail.sessionId }}</el-descriptions-item>
  124. <el-descriptions-item label="客户消息" :span="2">
  125. <div style="background:#f5f7fa;padding:8px;border-radius:4px">{{ detail.customerMsg }}</div>
  126. </el-descriptions-item>
  127. <el-descriptions-item label="AI回复" :span="2">
  128. <div style="background:#ecf5ff;padding:8px;border-radius:4px">{{ detail.aiReply }}</div>
  129. </el-descriptions-item>
  130. <el-descriptions-item label="准确性评分">{{ detail.accuracyScore }}/5</el-descriptions-item>
  131. <el-descriptions-item label="相关性评分">{{ detail.relevanceScore }}/5</el-descriptions-item>
  132. <el-descriptions-item label="合规性评分">{{ detail.complianceScore }}/5</el-descriptions-item>
  133. <el-descriptions-item label="综合评分">{{ formatTotalScore(detail) }}</el-descriptions-item>
  134. <el-descriptions-item label="评分说明" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
  135. <el-descriptions-item label="评分人">{{ detail.reviewer || '-' }}</el-descriptions-item>
  136. <el-descriptions-item label="评分时间">{{ detail.reviewTime || '-' }}</el-descriptions-item>
  137. </el-descriptions>
  138. </template>
  139. </el-dialog>
  140. <!-- 评分弹窗 -->
  141. <el-dialog :title="scoreForm.id ? '修改评分' : 'AI回复质量评分'" :visible.sync="scoreVisible" width="650px" append-to-body>
  142. <el-form ref="scoreFormRef" :model="scoreForm" :rules="scoreRules" label-width="100px">
  143. <el-form-item label="会话ID" prop="sessionId">
  144. <el-input v-model="scoreForm.sessionId" placeholder="请输入会话ID" />
  145. </el-form-item>
  146. <el-form-item label="客户消息" prop="customerMsg">
  147. <el-input v-model="scoreForm.customerMsg" type="textarea" :rows="3" placeholder="请输入客户原始消息" />
  148. </el-form-item>
  149. <el-form-item label="AI回复" prop="aiReply">
  150. <el-input v-model="scoreForm.aiReply" type="textarea" :rows="4" placeholder="请输入AI回复内容" />
  151. </el-form-item>
  152. <el-divider content-position="left">评分维度</el-divider>
  153. <el-form-item label="准确性" prop="accuracyScore">
  154. <el-rate v-model="scoreForm.accuracyScore" :max="5" show-score text-color="#ff9900" />
  155. <span class="score-desc">AI回复内容是否准确无误</span>
  156. </el-form-item>
  157. <el-form-item label="相关性" prop="relevanceScore">
  158. <el-rate v-model="scoreForm.relevanceScore" :max="5" show-score text-color="#ff9900" />
  159. <span class="score-desc">回复是否与客户问题相关</span>
  160. </el-form-item>
  161. <el-form-item label="合规性" prop="complianceScore">
  162. <el-rate v-model="scoreForm.complianceScore" :max="5" show-score text-color="#ff9900" />
  163. <span class="score-desc">回复是否符合行业合规要求</span>
  164. </el-form-item>
  165. <el-form-item label="语气语调">
  166. <el-rate v-model="scoreForm.toneScore" :max="5" show-score text-color="#ff9900" />
  167. <span class="score-desc">回复的语气是否友好得体</span>
  168. </el-form-item>
  169. <el-form-item label="评分说明" prop="remark">
  170. <el-input v-model="scoreForm.remark" type="textarea" :rows="3" placeholder="评分理由及改进建议" />
  171. </el-form-item>
  172. </el-form>
  173. <div slot="footer">
  174. <el-button @click="scoreVisible = false">取消</el-button>
  175. <el-button type="primary" @click="submitScore" :loading="submitting">提交评分</el-button>
  176. </div>
  177. </el-dialog>
  178. <!-- 申诉弹窗 -->
  179. <el-dialog title="评分申诉" :visible.sync="appealVisible" width="500px" append-to-body>
  180. <el-form :model="appealForm" label-width="80px">
  181. <el-form-item label="申诉原因">
  182. <el-input v-model="appealForm.reason" type="textarea" :rows="4" placeholder="请说明申诉原因..." />
  183. </el-form-item>
  184. <el-form-item label="建议评分">
  185. <el-rate v-model="appealForm.suggestedScore" :max="5" show-score text-color="#ff9900" />
  186. </el-form-item>
  187. </el-form>
  188. <div slot="footer">
  189. <el-button @click="appealVisible = false">取消</el-button>
  190. <el-button type="primary" @click="submitAppeal">提交申诉</el-button>
  191. </div>
  192. </el-dialog>
  193. </div>
  194. </template>
  195. <script>
  196. import { listQualityRecords, getQualityRecord, submitQualityReview,
  197. updateQualityReview, deleteQualityRecord, getQualityStats } from '@/api/workflow/lobster'
  198. export default {
  199. name: 'AiChatQuality',
  200. data() {
  201. return {
  202. loading: false, showSearch: true, submitting: false,
  203. total: 0, list: [], selectedIds: [],
  204. stats: { total: 0, pending: 0, scored: 0, avgScore: 0, disputed: 0 },
  205. queryParams: { pageNum: 1, pageSize: 10, sessionId: null, status: null, dateRange: null },
  206. detailVisible: false, detail: null,
  207. scoreVisible: false,
  208. scoreForm: {
  209. id: null, sessionId: '', customerMsg: '', aiReply: '',
  210. accuracyScore: 0, relevanceScore: 0, complianceScore: 0,
  211. toneScore: 0, remark: ''
  212. },
  213. scoreRules: {
  214. sessionId: [{ required: true, message: '会话ID不能为空', trigger: 'blur' }],
  215. customerMsg: [{ required: true, message: '客户消息不能为空', trigger: 'blur' }],
  216. aiReply: [{ required: true, message: 'AI回复不能为空', trigger: 'blur' }],
  217. accuracyScore: [{ required: true, message: '请评分', trigger: 'change' }],
  218. relevanceScore: [{ required: true, message: '请评分', trigger: 'change' }],
  219. complianceScore: [{ required: true, message: '请评分', trigger: 'change' }]
  220. },
  221. appealVisible: false, appealForm: { recordId: null, reason: '', suggestedScore: 0 }
  222. }
  223. },
  224. created() { this.getList(); this.getStats() },
  225. methods: {
  226. async getList() {
  227. this.loading = true
  228. try {
  229. const res = await listQualityRecords(this.queryParams)
  230. const data = res.data || {}
  231. this.list = data.rows || data.list || []
  232. this.total = data.total || 0
  233. } catch (e) {
  234. this.list = []
  235. this.total = 0
  236. } finally { this.loading = false }
  237. },
  238. async getStats() {
  239. try {
  240. const res = await getQualityStats()
  241. this.stats = res.data || {}
  242. } catch (e) { /* 统计API暂未就绪时静默处理 */ }
  243. },
  244. handleQuery() { this.queryParams.pageNum = 1; this.getList() },
  245. resetQuery() { this.$refs.queryForm.resetFields(); this.handleQuery() },
  246. handleSelectionChange(rows) { this.selectedIds = rows.map(r => r.id) },
  247. formatTotalScore(row) {
  248. const scores = [row.accuracyScore, row.relevanceScore, row.complianceScore].filter(s => s > 0)
  249. if (scores.length === 0) return '-'
  250. return (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1)
  251. },
  252. getTotalScoreType(row) {
  253. const s = parseFloat(this.formatTotalScore(row))
  254. if (isNaN(s)) return 'info'
  255. if (s >= 4) return 'success'
  256. if (s >= 3) return 'warning'
  257. return 'danger'
  258. },
  259. handleAdd() {
  260. this.scoreForm = { id: null, sessionId: '', customerMsg: '', aiReply: '',
  261. accuracyScore: 3, relevanceScore: 3, complianceScore: 3, toneScore: 3, remark: '' }
  262. this.scoreVisible = true
  263. },
  264. handleScore(row) {
  265. this.scoreForm = { ...row, id: row.id }
  266. this.scoreVisible = true
  267. },
  268. async submitScore() {
  269. this.$refs.scoreFormRef.validate(async valid => {
  270. if (!valid) return
  271. this.submitting = true
  272. try {
  273. const data = { ...this.scoreForm }
  274. if (this.scoreForm.id) {
  275. await updateQualityReview(data)
  276. this.$message.success('评分已更新')
  277. } else {
  278. await submitQualityReview(data)
  279. this.$message.success('评分已提交')
  280. }
  281. this.scoreVisible = false
  282. this.getList()
  283. this.getStats()
  284. } catch (e) {
  285. this.$message.error('操作失败:' + (e.message || ''))
  286. } finally { this.submitting = false }
  287. })
  288. },
  289. async handleDetail(row) {
  290. try {
  291. const res = await getQualityRecord(row.id)
  292. this.detail = res.data || res
  293. this.detailVisible = true
  294. } catch (e) {
  295. this.$message.error('获取详情失败')
  296. }
  297. },
  298. handleAppeal(row) {
  299. this.appealForm.recordId = row.id
  300. this.appealVisible = true
  301. },
  302. submitAppeal() {
  303. this.$message.success('申诉已提交,等待审核')
  304. this.appealVisible = false
  305. },
  306. handleDelete(row) {
  307. this.$confirm('确认删除该评分记录?', '警告', { type: 'warning' }).then(async () => {
  308. try {
  309. await deleteQualityRecord(row.id)
  310. this.$message.success('删除成功')
  311. this.getList()
  312. this.getStats()
  313. } catch (e) { this.$message.error('删除失败') }
  314. }).catch(() => {})
  315. },
  316. handleBatchScore() {
  317. if (this.selectedIds.length === 0) {
  318. this.$message.warning('请至少选择一条记录')
  319. return
  320. }
  321. this.$message.info(`已选择 ${this.selectedIds.length} 条记录,请进入评分弹窗逐条处理`)
  322. this.handleAdd()
  323. }
  324. }
  325. }
  326. </script>
  327. <style scoped>
  328. .stat-card { text-align: center; padding: 10px 0; }
  329. .stat-value { font-size: 24px; font-weight: bold; color: #409EFF; }
  330. .stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
  331. .score-desc { font-size: 12px; color: #909399; margin-left: 12px; }
  332. </style>