feat: add periodic practice reports

This commit is contained in:
2026-07-31 16:37:25 +08:00
parent 88ed92d78c
commit 99e23b33ec
16 changed files with 602 additions and 3 deletions

View File

@@ -45,6 +45,7 @@ const users = ref<AdminUser[]>([]);
const selectedUserDetail = ref<AdminUserDetail | null>(null);
const userDetailOpen = ref(false);
const userDetailLoading = ref(false);
const userReportGenerating = ref("");
const userKeyword = ref("");
const entitlementPlans = ref<EntitlementPlan[]>([]);
const editingEntitlementPlanId = ref<number | null>(null);
@@ -416,6 +417,20 @@ async function openUserDetail(row: AdminUser) {
}
}
async function generateUserReport(type: "weekly" | "monthly" | "stage") {
if (!selectedUserDetail.value) return;
userReportGenerating.value = type;
try {
await api.generateUserReport(selectedUserDetail.value.user.id, { reportType: type });
selectedUserDetail.value = await api.userDetail(selectedUserDetail.value.user.id);
ElMessage.success("报告已生成");
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "报告生成失败");
} finally {
userReportGenerating.value = "";
}
}
async function loadRecordTab(tab = recordTab.value) {
loading.value = true;
try {
@@ -1682,6 +1697,39 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
</section>
<el-empty v-else description="该用户暂无成长档案沉淀" :image-size="64" />
<section class="user-report-head">
<div>
<h3 class="detail-title">周期报告</h3>
<p>基于已沉淀的主题摘要生成不读取整段原始聊天同一周期重复生成会覆盖旧报告</p>
</div>
<div class="user-report-actions">
<el-button size="small" :loading="userReportGenerating === 'weekly'" @click="generateUserReport('weekly')">生成周报</el-button>
<el-button size="small" :loading="userReportGenerating === 'monthly'" @click="generateUserReport('monthly')">生成月报</el-button>
<el-button size="small" :loading="userReportGenerating === 'stage'" @click="generateUserReport('stage')">生成阶段总结</el-button>
</div>
</section>
<el-collapse v-if="selectedUserDetail.recentReports.length" class="topic-summary-collapse">
<el-collapse-item
v-for="report in selectedUserDetail.recentReports"
:key="report.id"
:title="`#${report.id} ${report.title} / ${report.status}`"
>
<section class="topic-summary-card">
<div class="topic-summary-meta">
<span>类型{{ report.reportTypeLabel }}</span>
<span>周期{{ report.periodStart }} {{ report.periodEnd }}</span>
<span>来源主题{{ report.sourceTopicIds.length }}</span>
<span>来源摘要{{ report.sourceSummaryIds.length }}</span>
<span>模型{{ report.modelName || '-' }}</span>
<span>生成{{ report.generatedAt }}</span>
</div>
<p v-if="report.errorMessage" class="report-error">生成失败{{ report.errorMessage }}</p>
<pre>{{ report.content }}</pre>
</section>
</el-collapse-item>
</el-collapse>
<el-empty v-else description="该用户暂无周期报告" :image-size="64" />
<h3 class="detail-title">最近主题</h3>
<el-collapse v-if="selectedUserDetail.recentTopics.length" class="topic-summary-collapse">
<el-collapse-item

View File

@@ -29,6 +29,7 @@ import type {
UserImportResult,
UserEntitlementSummary,
PageResult,
PeriodicReportRecord,
PromptDetail,
PromptHistoryItem,
QuestionInsightSummary,
@@ -127,6 +128,9 @@ export const api = {
},
users: (query: { keyword?: string; page?: number; pageSize?: number } = {}) => request<PageResult<AdminUser>>(`/admin/user/list${queryString(query)}`),
userDetail: (id: number) => request<AdminUserDetail>(`/admin/user/${id}/detail`),
userReports: (id: number, limit = 20) => request<PeriodicReportRecord[]>(`/admin/user/${id}/reports${queryString({ limit })}`),
generateUserReport: (id: number, payload: { reportType: string; periodStart?: string | null; periodEnd?: string | null }) =>
request<PeriodicReportRecord>(`/admin/user/${id}/reports/generate`, { method: "POST", body: JSON.stringify(payload) }),
createUser: (payload: Record<string, unknown>) =>
request<AdminUser>("/admin/user", { method: "POST", body: JSON.stringify(payload) }),
importUsers: (students: Record<string, unknown>[]) =>

View File

@@ -2054,6 +2054,44 @@ textarea {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.user-report-head {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-end;
margin-top: 18px;
}
.user-report-head .detail-title {
margin-bottom: 4px;
}
.user-report-head p {
margin: 0 0 10px;
color: #71827c;
font-size: 13px;
}
.user-report-actions {
display: flex;
flex-wrap: nowrap;
gap: 8px;
margin-bottom: 10px;
}
.user-report-actions .el-button {
margin-left: 0;
white-space: nowrap;
}
.report-error {
margin: 0;
padding: 10px 12px;
border-radius: 10px;
background: #fff2f0;
color: #b42318;
}
.chat-summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));

View File

@@ -73,6 +73,7 @@ export interface AdminUserDetail {
recentTopics: TopicSessionRecord[];
recentHelpCards: TeacherHelpCardRecord[];
recentShareDrafts: ShareDraftRecord[];
recentReports: PeriodicReportRecord[];
}
export interface AdminUserMetrics {
@@ -503,6 +504,26 @@ export interface UserGrowthProfileRecord {
updatedAt: string;
}
export interface PeriodicReportRecord {
id: number;
userId: number;
reportType: "weekly" | "monthly" | "stage" | string;
reportTypeLabel: string;
periodStart: string;
periodEnd: string;
title: string;
content: string;
sourceSummaryIds: number[];
sourceTopicIds: number[];
modelName?: string | null;
status: "success" | "failed" | "empty" | string;
errorMessage?: string | null;
generatedBy: string;
generatedAt: string;
createdAt: string;
updatedAt: string;
}
export interface TopicSessionRecord {
id: number;
userId: number;