feat(ECR-020): QualityFeedback 问答质量反馈并 Closed

新增 ask_quality_feedback、运营/C端评分 API 与 admin-h5 问答反馈区;禁改消息/UGC/真支付。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-07 23:02:13 +08:00
co-authored by Cursor
parent e270a38393
commit 93227d3316
34 changed files with 932 additions and 9 deletions
+20
View File
@@ -275,6 +275,26 @@ export const adminApi = {
'/content-safety/evaluate',
{ text },
),
askFeedback: () =>
request<{
items: Array<{
id: string
thread_id: string
message_id?: string
source: string
rating: number
tag?: string
note?: string
created_at: string
}>
}>('GET', '/ask/feedback'),
createAskFeedback: (threadId: string, body: { rating: number; tag?: string; note?: string; message_id?: string }) =>
request<{
id: string
thread_id: string
rating: number
source: string
}>('POST', `/ask/threads/${threadId}/feedback`, body),
orders: () =>
request<{
items: Array<{
+70 -3
View File
@@ -2,23 +2,34 @@
import { onMounted, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { adminApi } from '@/api/client'
import { useAuthStore } from '@/stores/auth'
type Thread = Awaited<ReturnType<typeof adminApi.askThreads>>['items'][number]
type Detail = Awaited<ReturnType<typeof adminApi.askThread>>
type Feedback = Awaited<ReturnType<typeof adminApi.askFeedback>>['items'][number]
const auth = useAuthStore()
const loading = ref(false)
const error = ref('')
const items = ref<Thread[]>([])
const selected = ref<Detail | null>(null)
const detailErr = ref('')
const detailLoading = ref(false)
const feedback = ref<Feedback[]>([])
const rating = ref(4)
const tag = ref('helpful')
const note = ref('')
const fbMsg = ref('')
const canWriteFeedback = () => auth.can('admin.ask.feedback.write')
async function load() {
loading.value = true
error.value = ''
try {
const res = await adminApi.askThreads()
items.value = res.items || []
const [threads, fb] = await Promise.all([adminApi.askThreads(), adminApi.askFeedback()])
items.value = threads.items || []
feedback.value = fb.items || []
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
@@ -30,6 +41,7 @@ async function openThread(id: string) {
detailLoading.value = true
detailErr.value = ''
selected.value = null
fbMsg.value = ''
try {
selected.value = await adminApi.askThread(id)
} catch (e) {
@@ -39,6 +51,24 @@ async function openThread(id: string) {
}
}
async function submitFeedback() {
if (!selected.value) return
fbMsg.value = ''
try {
await adminApi.createAskFeedback(selected.value.id, {
rating: rating.value,
tag: tag.value,
note: note.value || undefined,
})
fbMsg.value = '已提交质量反馈'
note.value = ''
const fb = await adminApi.askFeedback()
feedback.value = fb.items || []
} catch (e) {
fbMsg.value = e instanceof Error ? e.message : '提交失败'
}
}
function fmtTime(iso?: string) {
if (!iso) return '—'
try {
@@ -54,7 +84,7 @@ onMounted(load)
<template>
<section>
<h1>问答会话</h1>
<p class="muted">只读 AskSessionView · 不可改写消息</p>
<p class="muted">AskSessionView · QualityFeedback · 不可改写消息</p>
<p v-if="loading" class="muted">加载中</p>
<p v-else-if="error" class="err">{{ error }}</p>
<div v-else class="layout">
@@ -76,6 +106,13 @@ onMounted(load)
</tr>
</tbody>
</table>
<h2 class="sub">近期反馈</h2>
<p v-if="!feedback.length" class="muted">暂无</p>
<ul v-else class="fb-list">
<li v-for="f in feedback.slice(0, 8)" :key="f.id">
{{ f.source }} · {{ f.rating }} · {{ f.tag || '' }} · {{ fmtTime(f.created_at) }}
</li>
</ul>
</div>
<div class="card detail">
<h2>会话详情</h2>
@@ -93,6 +130,25 @@ onMounted(load)
<time>{{ fmtTime(m.created_at) }}</time>
</li>
</ul>
<div v-if="canWriteFeedback()" class="fb-form">
<h3>质量反馈</h3>
<select v-model.number="rating">
<option :value="1">1</option>
<option :value="2">2</option>
<option :value="3">3</option>
<option :value="4">4</option>
<option :value="5">5</option>
</select>
<select v-model="tag">
<option value="helpful">helpful</option>
<option value="off_topic">off_topic</option>
<option value="unsafe">unsafe</option>
<option value="other">other</option>
</select>
<input v-model="note" type="text" placeholder="备注(可选)" />
<button class="btn" type="button" @click="submitFeedback">提交</button>
<span class="muted">{{ fbMsg }}</span>
</div>
</template>
<p v-else class="muted">选择左侧会话查看消息</p>
</div>
@@ -103,6 +159,8 @@ onMounted(load)
<style scoped>
h1 { margin: 0 0 0.35rem; font-size: 1.35rem; }
h2 { margin: 0 0 0.6rem; font-size: 1.05rem; }
h2.sub { margin-top: 1rem; font-size: 0.95rem; }
h3 { margin: 0.75rem 0 0.4rem; font-size: 0.95rem; }
.layout {
display: grid;
grid-template-columns: 1.1fr 1fr;
@@ -121,6 +179,15 @@ h2 { margin: 0 0 0.6rem; font-size: 1.05rem; }
.msgs p { margin: 0.25rem 0; white-space: pre-wrap; word-break: break-word; }
.msgs time { font-size: 0.75rem; color: var(--muted); }
.msgs .assistant { background: rgba(255, 244, 242, 0.6); }
.fb-list { margin: 0; padding-left: 1.1rem; font-size: 0.85rem; color: var(--muted); }
.fb-form {
display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center;
margin-top: 0.85rem; padding-top: 0.75rem; border-top: 1px solid var(--line);
}
.fb-form select, .fb-form input {
border: 1px solid var(--line); border-radius: 8px; padding: 0.4rem 0.55rem;
}
.fb-form input { min-width: 10rem; flex: 1; }
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
}