feat: improve async feedback and export workflow

This commit is contained in:
2026-08-11 18:45:39 +08:00
parent ff4ba1148b
commit 83b259af2d
11 changed files with 248 additions and 28 deletions

View File

@@ -1119,7 +1119,12 @@ async function clearFeishuCache() {
<RetrievalLogView v-if="activeMenu === 'retrievals'" />
<AttentionManagementView v-if="activeMenu === 'attention'" />
<FeedbackManagementView v-if="activeMenu === 'feedback'" :can-delete="can('feedback.delete')" />
<FeedbackManagementView
v-if="activeMenu === 'feedback'"
:can-delete="can('feedback.delete')"
:can-detail="can('feedback.detail')"
:can-export="can('feedback.export')"
/>
<RecordAuditView v-if="activeMenu === 'records'" />
<AdminManagementView v-if="activeMenu === 'admins'" />
</section>

View File

@@ -1,25 +1,71 @@
<script setup lang="ts">
import { ElMessage, ElMessageBox } from "element-plus";
import { onMounted, ref } from "vue";
import { api } from "../services/api";
import type { FeedbackDetail, FeedbackItem } from "../types/api";
import AdminPagination from "./AdminPagination.vue";
defineProps<{ canDelete: boolean; canDetail: boolean; canExport: boolean }>();
const props = defineProps<{ canDelete: boolean }>();
const loading = ref(false);
const exporting = ref(false);
const items = ref<FeedbackItem[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const readStatus = ref("all");
const dateRange = ref<[string, string] | []>([]);
const detail = ref<FeedbackDetail | null>(null);
async function load() {
loading.value = true;
try {
const result = await api.feedbackList({ readStatus: readStatus.value, page: page.value, pageSize: 20 });
const result = await api.feedbackList({
readStatus: readStatus.value,
startDate: dateRange.value[0],
endDate: dateRange.value[1],
page: page.value,
pageSize: pageSize.value,
});
items.value = result.items;
total.value = result.total;
} catch (error) { ElMessage.error(error instanceof Error ? error.message : "反馈列表加载失败"); }
finally { loading.value = false; }
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "反馈列表加载失败");
} finally {
loading.value = false;
}
}
function applyFilters() {
page.value = 1;
void load();
}
function changePage(nextPage: number, nextPageSize: number) {
page.value = nextPage;
pageSize.value = nextPageSize;
void load();
}
async function exportExcel() {
if (dateRange.value.length !== 2) {
ElMessage.warning("请先选择导出的时间范围");
return;
}
exporting.value = true;
try {
await api.exportFeedback({
readStatus: readStatus.value,
startDate: dateRange.value[0],
endDate: dateRange.value[1],
});
ElMessage.success("反馈 Excel 已导出");
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "反馈导出失败");
} finally {
exporting.value = false;
}
}
async function open(item: FeedbackItem) {
@@ -40,16 +86,23 @@ onMounted(load);
<template>
<section class="feedback-page">
<header><div><h2>反馈管理</h2><p>查看用户针对 AI 回答提交的问题与当时对话</p></div><el-segmented v-model="readStatus" :options="[{ label: '全部', value: 'all' }, { label: '未读', value: 'unread' }, { label: '已读', value: 'read' }]" @change="page=1; load()" /></header>
<header>
<div><h2>反馈管理</h2><p>查看用户针对 AI 回答提交的问题与当时对话</p></div>
</header>
<section class="feedback-toolbar" aria-label="反馈筛选与导出">
<el-segmented v-model="readStatus" :options="[{ label: '全部', value: 'all' }, { label: '未读', value: 'unread' }, { label: '已读', value: 'read' }]" @change="applyFilters" />
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" unlink-panels @change="applyFilters" />
<el-button v-if="canExport" type="primary" :loading="exporting" @click="exportExcel">导出 Excel</el-button>
</section>
<el-table v-loading="loading" :data="items" stripe>
<el-table-column label="状态" width="88"><template #default="{ row }"><el-tag :type="row.isRead ? 'info' : 'danger'">{{ row.isRead ? '已读' : '未读' }}</el-tag></template></el-table-column>
<el-table-column prop="userName" label="用户" width="130" />
<el-table-column prop="content" label="反馈内容" min-width="240" show-overflow-tooltip />
<el-table-column prop="messageContent" label="反馈的 AI 回答" min-width="280" show-overflow-tooltip />
<el-table-column prop="createdAt" label="提交时间" width="180" />
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="open(row)">查看</el-button><el-button v-if="canDelete" link type="danger" @click="remove(row)">删除</el-button></template></el-table-column>
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button v-if="canDetail" link type="primary" @click="open(row)">查看</el-button><el-button v-if="canDelete" link type="danger" @click="remove(row)">删除</el-button></template></el-table-column>
</el-table>
<el-pagination v-if="total > 20" v-model:current-page="page" :page-size="20" :total="total" layout="prev, pager, next, total" @current-change="load" />
<AdminPagination v-if="total" :page="page" :page-size="pageSize" :total="total" @change="changePage" />
<el-drawer :model-value="Boolean(detail)" title="反馈详情" size="620px" @close="detail = null">
<template v-if="detail">
<section class="feedback-summary"><span>用户反馈</span><strong>{{ detail.content }}</strong><small>{{ detail.userName }} · {{ detail.createdAt }}</small></section>
@@ -63,8 +116,11 @@ onMounted(load);
</template>
<style scoped>
.feedback-page header { display:flex; justify-content:space-between; align-items:flex-start; gap:24px; margin-bottom:22px; }
.feedback-page header { display:flex; justify-content:space-between; align-items:flex-start; gap:24px; margin-bottom:18px; }
.feedback-page h2 { margin:0 0 6px; }.feedback-page header p { margin:0; color:#7b8494; }
.feedback-toolbar { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:18px; padding:14px 16px; border:1px solid #e7ebf0; border-radius:12px; background:#fafbfc; }
.feedback-toolbar :deep(.el-date-editor) { width:300px; }
.feedback-summary { display:grid; gap:9px; padding:16px; border-radius:12px; background:#f7f9fc; }.feedback-summary span,.feedback-summary small { color:#87909f; }
.feedback-history { display:grid; gap:12px; }.feedback-history article { padding:14px 16px; border:1px solid #e6eaf0; border-radius:12px; }.feedback-history article.user { margin-left:42px; background:#f5f8ff; }.feedback-history article.assistant { margin-right:42px; }.feedback-history article.target { border:2px solid #f0a020; background:#fffaf0; }.feedback-history article div { display:flex; align-items:center; justify-content:space-between; }.feedback-history p { white-space:pre-wrap; line-height:1.65; margin:8px 0 0; }
@media (max-width: 900px) { .feedback-toolbar { align-items:stretch; }.feedback-toolbar :deep(.el-date-editor) { width:100%; } }
</style>

View File

@@ -332,7 +332,9 @@ export const api = {
deleteAttention: (id: number) => request<null>(`/admin/attention/${id}`, { method: "DELETE" }),
clearFeishuCache: () =>
request<{ cleared: number; message: string }>("/admin/feishu/cache/clear", { method: "POST", body: "{}" }),
feedbackList: (query: { readStatus?: string; page?: number; pageSize?: number } = {}) => request<PageResult<import("../types/api").FeedbackItem>>(`/feedback/admin/list${queryString(query)}`),
feedbackList: (query: { readStatus?: string; startDate?: string; endDate?: string; page?: number; pageSize?: number } = {}) => request<PageResult<import("../types/api").FeedbackItem>>(`/feedback/admin/list${queryString(query)}`),
exportFeedback: (query: { readStatus?: string; startDate: string; endDate: string }) =>
download(`/feedback/admin/export${queryString(query)}`, `反馈记录_${query.startDate}_${query.endDate}.xlsx`),
feedbackDetail: (id: number) => request<import("../types/api").FeedbackDetail>(`/feedback/admin/${id}`),
deleteFeedback: (id: number) => request<null>(`/feedback/admin/${id}`, { method: "DELETE" }),
};