feat: add periodic practice reports
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>[]) =>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user