feat: 完善周期报告与用户行为分析

- 支持可配置周报月报模板与登录后异步补生成\n- 增加用户行为埋点和后台分析页面\n- 移除主题额度并保留实修回顾结算\n- 修复历史会话续聊上下文丢失
This commit is contained in:
2026-08-19 11:53:34 +08:00
parent 833763c461
commit efe835be81
75 changed files with 3325 additions and 578 deletions

View File

@@ -53,6 +53,7 @@ const AdminManagementView = defineAsyncComponent(
() => import("./components/AdminManagementView.vue"),
);
const FeedbackManagementView = defineAsyncComponent(() => import("./components/FeedbackManagementView.vue"));
const UserBehaviorAnalysisView = defineAsyncComponent(() => import("./components/UserBehaviorAnalysisView.vue"));
const admin = ref<AdminProfile | null>(null);
const activeMenu = ref("dashboard");
@@ -68,6 +69,7 @@ const menuPermissions: Record<string, string> = {
"content-generation": "content-generation.view", configs: "configs.view", sso: "sso.view",
records: "records.view", retrievals: "retrievals.view", attention: "attention.view", admins: "admins.view",
feedback: "feedback.view",
behavior: "behavior.view",
};
const entitlementPlans = ref<EntitlementPlan[]>([]);
@@ -717,6 +719,11 @@ async function clearFeishuCache() {
:class="{ active: activeMenu === 'feedback' }"
@click="switchMenu('feedback')"
>反馈管理</button>
<button
v-if="can('behavior.view')"
:class="{ active: activeMenu === 'behavior' }"
@click="switchMenu('behavior')"
>用户行为分析</button>
<button
v-if="can('attention.view')"
:class="{ active: activeMenu === 'attention' }"
@@ -1126,6 +1133,7 @@ async function clearFeishuCache() {
:can-detail="can('feedback.detail')"
:can-export="can('feedback.export')"
/>
<UserBehaviorAnalysisView v-if="activeMenu === 'behavior'" />
<RecordAuditView v-if="activeMenu === 'records'" />
<AdminManagementView v-if="activeMenu === 'admins'" />
</section>

View File

@@ -115,8 +115,6 @@ async function changeMessagePage(page: number, pageSize: number) {
><span>消息数{{ topic.messageCount }}</span
><span
>Token{{ topic.tokenInput }}/{{ topic.tokenOutput }}</span
><span
>额度{{ topic.quotaDeducted ? "已计入" : "不计入" }}</span
><span>开始{{ topic.startedAt }}</span
><span>结束{{ topic.endedAt || "-" }}</span>
</div>

View File

@@ -40,12 +40,19 @@ const sampleText = ref(
"我第一次参加带练,不太确定练习顺序。做到一半身体有些紧,我会担心自己是不是做错了,想请老师确认什么时候应该暂停。",
);
const typeMeta: Record<ContentGenerationType, { label: string; description: string }> = {
help_card: { label: "老师求助卡", description: "给老师确认方向,不会自动转人工" },
share_draft: { label: "班级分享稿", description: "用户自行核对、复制和分享" },
weekly_report: { label: "周报告", description: "根据一周内的全部聊天分批整理" },
monthly_report: { label: "月报告", description: "根据本月覆盖的周报告汇总" },
};
const dirty = computed(
() => form.templateContent !== saved.templateContent
|| form.instructionContent !== saved.instructionContent
|| JSON.stringify(form.variables) !== saved.variablesJson,
);
const typeLabel = computed(() => activeType.value === "help_card" ? "老师求助卡" : "班级分享稿");
const typeLabel = computed(() => typeMeta[activeType.value].label);
onMounted(loadAll);
@@ -95,6 +102,11 @@ async function switchType(type: ContentGenerationType) {
previewContent.value = "";
testUsedFallback.value = false;
activeSection.value = "variables";
sampleText.value = type === "weekly_report"
? "周一讨论了练习顺序;周三聊到身体紧绷时会急着判断对错;周末继续确认什么时候应该暂停。请把这些内容当作一周内多段完整聊天的模拟材料。"
: type === "monthly_report"
? "第一周主要讨论练习准备,第二周关注身体紧绷,第三周继续梳理表达时的不确定感,第四周回顾了暂停和观察的区别。请把这些内容当作多份周报告的模拟材料。"
: "我第一次参加带练,不太确定练习顺序。做到一半身体有些紧,我会担心自己是不是做错了,想请老师确认什么时候应该暂停。";
await loadAll();
}
@@ -262,7 +274,7 @@ function changeTypeLabel(value: string) {
<div class="page-head inline">
<div>
<h2>内容生成配置</h2>
<p>自定义卡片变量AI 提炼含义和内容排版每次保存立即生效并自动保留可回滚版本</p>
<p>统一配置求助卡分享稿周报告和月报告的变量AI 整理规则与排版保存立即生效并保留历史版本</p>
</div>
<div class="config-head-actions">
<el-button :loading="previewing" @click="refreshPreview">刷新预览</el-button>
@@ -277,14 +289,20 @@ function changeTypeLabel(value: string) {
<button type="button" :class="{ active: activeType === 'share_draft' }" @click="switchType('share_draft')">
<strong>班级分享稿</strong><span>用户自行核对复制和分享</span>
</button>
<button type="button" :class="{ active: activeType === 'weekly_report' }" @click="switchType('weekly_report')">
<strong>周报告</strong><span>根据一周内的全部聊天分批整理</span>
</button>
<button type="button" :class="{ active: activeType === 'monthly_report' }" @click="switchType('monthly_report')">
<strong>月报告</strong><span>根据本月覆盖的周报告汇总</span>
</button>
</nav>
<nav class="workflow-tabs" aria-label="内容配置步骤">
<button type="button" :class="{ active: activeSection === 'variables' }" @click="activeSection = 'variables'">
<span>1</span><div><strong>变量设置</strong><small>定义卡片字段和提炼含义</small></div>
<span>1</span><div><strong>变量设置</strong><small>定义内容字段和提炼含义</small></div>
</button>
<button type="button" :class="{ active: activeSection === 'layout' }" @click="activeSection = 'layout'; refreshPreview()">
<span>2</span><div><strong>卡片排版</strong><small>组合变量并查看实际效果</small></div>
<span>2</span><div><strong>内容排版</strong><small>组合变量并查看实际效果</small></div>
</button>
<button type="button" :class="{ active: activeSection === 'test' }" @click="activeSection = 'test'">
<span>3</span><div><strong>AI 测试</strong><small>用模拟材料验证提炼质量</small></div>
@@ -294,7 +312,7 @@ function changeTypeLabel(value: string) {
<section v-if="activeSection === 'variables'" class="generation-editor-column">
<article class="generation-panel">
<header>
<div><h3>自定义变量</h3><p>变量不再写死在代码中管理员可自行定义名称含义和取值方式保存后立即用于用户端生成</p></div>
<div><h3>自定义变量</h3><p>管理员可定义名称含义和取值方式周报的 AI 变量从完整聊天材料提炼月报的 AI 变量从周报材料提炼</p></div>
<el-button text :disabled="saving" @click="resetConfig">恢复系统默认</el-button>
</header>
<ContentGenerationVariableEditor
@@ -303,7 +321,7 @@ function changeTypeLabel(value: string) {
/>
</article>
<article class="generation-panel">
<header><div><h3>全局 AI 整理规则</h3><p>这里控制整张卡片的语气和边界每个字段具体提炼什么由上方变量含义决定</p></div></header>
<header><div><h3>全局 AI 整理规则</h3><p>这里控制当前内容的语气和边界每个字段具体提炼什么由上方变量含义决定</p></div></header>
<el-input v-model="form.instructionContent" type="textarea" :rows="7" resize="vertical" />
<small>{{ form.instructionContent.length }}/10000 字符</small>
</article>
@@ -313,7 +331,7 @@ function changeTypeLabel(value: string) {
<div class="generation-editor-column">
<article class="generation-panel">
<header>
<div><h3>卡片排版</h3><p>先把光标放到需要的位置再点击变量标题说明编号和空行都可以自由调整</p></div>
<div><h3>内容排版</h3><p>先把光标放到需要的位置再点击变量标题说明编号和空行都可以自由调整</p></div>
<el-button text :disabled="saving" @click="resetConfig">恢复系统默认</el-button>
</header>
<div class="variable-list">
@@ -327,13 +345,13 @@ function changeTypeLabel(value: string) {
<article class="locked-rules">
<strong>系统锁定的安全提醒</strong>
<p>{{ current?.lockedFooter }}</p>
<small>这段内容会固定追加在所有卡片末尾管理员不能删除</small>
<small>这段内容会固定追加在当前类型的所有生成内容末尾管理员不能删除</small>
</article>
</div>
<aside class="generation-preview-column">
<article class="generation-panel preview-panel">
<header>
<div><h3>排版效果预览</h3><p>使用每个变量设置预览示例填充正式生成时会换成真实提炼内容</p></div>
<div><h3>排版效果预览</h3><p>使用变量预览示例填充正式生成时周报读取完整聊天月报读取周报</p></div>
<el-button :loading="previewing" @click="refreshPreview">刷新</el-button>
</header>
<pre>{{ previewContent || "点击“刷新”查看效果" }}</pre>
@@ -348,7 +366,7 @@ function changeTypeLabel(value: string) {
<el-button type="primary" :loading="testing" @click="testGeneration">运行 AI 提炼测试</el-button>
</article>
<article class="generation-panel preview-panel test-result">
<header><div><h3>测试结果</h3><p>本次测试不会保存配置也不会生成正式用户卡片</p></div></header>
<header><div><h3>测试结果</h3><p>本次测试不会保存配置也不会生成正式用户内容</p></div></header>
<pre>{{ previewContent || "运行测试后在这里查看结果" }}</pre>
<el-alert v-if="testUsedFallback" title="本次测试使用了安全回退结果,模型未返回有效结构化内容。" type="warning" :closable="false" show-icon />
</article>
@@ -392,7 +410,7 @@ function changeTypeLabel(value: string) {
<div class="history-variables">
<span v-for="item in selectedHistory.variables" :key="item.name"><strong>{{ item.label }}</strong><code>{{ variableToken(item.name) }}</code></span>
</div>
<h4>卡片模板</h4><pre>{{ selectedHistory.templateContent }}</pre>
<h4>内容模板</h4><pre>{{ selectedHistory.templateContent }}</pre>
<h4>AI 整理规则</h4><pre>{{ selectedHistory.instructionContent }}</pre>
</template>
</div>
@@ -407,7 +425,7 @@ function changeTypeLabel(value: string) {
<style scoped>
.content-generation-page { display: grid; gap: 18px; }
.config-head-actions { display: flex; gap: 10px; }
.content-type-tabs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.content-type-tabs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.content-type-tabs button { min-height: 78px; display: grid; gap: 5px; padding: 15px 18px; border: 1px solid #dfe8e4; border-radius: 16px; background: #fff; color: #65746f; text-align: left; transition: border-color .18s ease, background .18s ease, transform .18s ease; }
.content-type-tabs button:hover { border-color: #91c8b5; transform: translateY(-1px); }
.content-type-tabs button.active { border-color: #43a783; background: #eff8f4; color: #176c51; box-shadow: 0 8px 24px rgba(23, 108, 81, .08); }
@@ -459,6 +477,6 @@ function changeTypeLabel(value: string) {
.history-variables { display: flex; flex-wrap: wrap; gap: 7px; }
.history-variables span { display: flex; gap: 6px; padding: 7px 9px; border-radius: 9px; background: #f2f7f5; color: #466158; font-size: 12px; }
.history-variables code { color: #16805e; }
@media (max-width: 1100px) { .generation-editor-layout { grid-template-columns: 1fr; } .preview-panel { position: static; } }
@media (max-width: 1100px) { .content-type-tabs { grid-template-columns: repeat(2, minmax(0, 1fr)); } .generation-editor-layout { grid-template-columns: 1fr; } .preview-panel { position: static; } }
@media (max-width: 720px) { .content-type-tabs, .workflow-tabs { grid-template-columns: 1fr; } .workflow-tabs button { border-right: 0; border-bottom: 1px solid #e7eeeb; } .workflow-tabs button:last-child { border-bottom: 0; } .config-head-actions { width: 100%; } .config-head-actions .el-button { flex: 1; } .generation-history-detail dl { grid-template-columns: 1fr 1fr; } }
</style>

View File

@@ -43,7 +43,7 @@ function token(name: string) {
<div class="variable-editor-head">
<div>
<strong>变量清单</strong>
<span>卡片最多配置 30 个变量AI 会严格按照含义说明提炼对应内容</span>
<span>每类内容最多配置 30 个变量AI 会严格按照含义说明提炼对应内容</span>
</div>
<el-button type="primary" plain @click="addVariable">+ 新增变量</el-button>
</div>
@@ -87,7 +87,7 @@ function token(name: string) {
/>
</label>
<label class="full">
<span>预览示例 <em>只用于排版预览不会进入正式卡片</em></span>
<span>预览示例 <em>只用于排版预览不会进入正式内容</em></span>
<el-input :model-value="item.sampleValue" maxlength="1000" @update:model-value="update(index, { sampleValue: String($event) })" />
</label>
</div>

View File

@@ -22,12 +22,10 @@ const form = reactive({
planType: "basic" as "basic" | "deep" | "addon",
description: "",
validityDays: null as number | null,
monthlyTopicLimit: 30 as number | null,
enableGrowthProfile: 0,
enablePeriodicReports: 0,
allowHelpCard: 1,
allowShareDraft: 1,
deductQuota: 1,
status: 1,
sortOrder: 10,
});
@@ -55,11 +53,6 @@ const capabilityOptions = [
title: "分享草稿",
description: "允许将对话内容整理为可分享草稿",
},
{
key: "deductQuota" as const,
title: "占用主题额度",
description: "新建主题时计入本月可用额度",
},
];
function planTypeLabel(type: string) {
@@ -77,12 +70,10 @@ function resetForm() {
planType: "basic",
description: "",
validityDays: null,
monthlyTopicLimit: 30,
enableGrowthProfile: 0,
enablePeriodicReports: 0,
allowHelpCard: 1,
allowShareDraft: 1,
deductQuota: 1,
status: 1,
sortOrder: 10,
});
@@ -119,12 +110,10 @@ async function editPlan(plan: EntitlementPlan) {
planType: plan.planType,
description: plan.description ?? "",
validityDays: plan.validityDays ?? null,
monthlyTopicLimit: plan.monthlyTopicLimit ?? null,
enableGrowthProfile: plan.enableGrowthProfile ? 1 : 0,
enablePeriodicReports: plan.enablePeriodicReports ? 1 : 0,
allowHelpCard: plan.allowHelpCard ? 1 : 0,
allowShareDraft: plan.allowShareDraft ? 1 : 0,
deductQuota: plan.deductQuota ? 1 : 0,
status: plan.status,
sortOrder: plan.sortOrder,
});
@@ -137,7 +126,7 @@ async function editPlan(plan: EntitlementPlan) {
<div class="page-head entitlement-page-head">
<div>
<h2>权益管理</h2>
<p>配置不同服务版本的主题额度与可用能力保存后可在用户管理中直接分配</p>
<p>配置不同服务版本的有效期与可用能力保存后可在用户管理中直接分配</p>
</div>
<div class="entitlement-summary" aria-label="权益版本统计">
<span><strong>{{ plans.length }}</strong> 个版本</span>
@@ -152,7 +141,7 @@ async function editPlan(plan: EntitlementPlan) {
<h3>{{ editingPlanId ? "编辑权益版本" : "新增权益版本" }}</h3>
<span class="editor-mode">{{ editingPlanId ? "编辑中" : "新建" }}</span>
</div>
<p>先设置版本和额度再决定学员可使用的产品能力</p>
<p>先设置版本信息再决定学员可使用的产品能力</p>
</div>
<el-button v-if="editingPlanId" @click="resetForm">退出编辑</el-button>
<el-button v-else @click="resetForm">重置表单</el-button>
@@ -187,10 +176,6 @@ async function editPlan(plan: EntitlementPlan) {
<el-input-number v-model="form.validityDays" :min="1" :max="3650" placeholder="留空即长期" />
<div class="field-help">不填写表示长期有效</div>
</el-form-item>
<el-form-item label="每月主题额度">
<el-input-number v-model="form.monthlyTopicLimit" :min="0" :max="99999" placeholder="留空即不限" />
<div class="field-help">不填写表示不限制主题数</div>
</el-form-item>
<el-form-item label="列表排序">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" />
<div class="field-help">数字越小在列表中越靠前</div>
@@ -242,9 +227,6 @@ async function editPlan(plan: EntitlementPlan) {
<el-table-column label="类型" width="130">
<template #default="{ row }">{{ planTypeLabel(row.planType) }}</template>
</el-table-column>
<el-table-column label="主题额度" width="110" align="center">
<template #default="{ row }">{{ row.monthlyTopicLimit ?? "不限" }}</template>
</el-table-column>
<el-table-column label="有效期" width="110" align="center">
<template #default="{ row }">{{ row.validityDays ? `${row.validityDays}` : "长期" }}</template>
</el-table-column>
@@ -255,7 +237,6 @@ async function editPlan(plan: EntitlementPlan) {
<el-tag v-if="row.enablePeriodicReports" type="success" effect="plain">周期报告</el-tag>
<el-tag v-if="row.allowHelpCard" effect="plain">求助卡片</el-tag>
<el-tag v-if="row.allowShareDraft" effect="plain">分享草稿</el-tag>
<el-tag :type="row.deductQuota ? 'warning' : 'info'" effect="plain">{{ row.deductQuota ? "计入额度" : "不计额度" }}</el-tag>
</div>
</template>
</el-table-column>

View File

@@ -69,9 +69,9 @@ const groupBlueprints: Record<string, SettingGroupBlueprint[]> = {
],
},
{
title: "主题与记忆",
description: "控制会话记忆,以及成功问答达到多少轮后开始自动提炼主题内容。",
keys: ["chat_context_message_count", "topic_auto_settle_successful_rounds"],
title: "对话记忆与实修回顾",
description: "控制会话记忆,以及成功问答达到多少轮后开始整理实修回顾资料。",
keys: ["chat_context_message_count", "practice_review_auto_settle_successful_rounds"],
},
{
title: "模型分流",

File diff suppressed because one or more lines are too long

View File

@@ -51,10 +51,10 @@ async function retryTopicSettlement(topicId: number) {
try {
await api.retryTopicSettlement(detail.value.user.id, topicId);
await loadDetail(detail.value.user.id);
ElMessage.success("主题沉淀任务已重新进入后台队列");
ElMessage.success("实修回顾整理任务已重新进入后台队列");
} catch (error) {
ElMessage.error(
error instanceof Error ? error.message : "主题沉淀重试失败",
error instanceof Error ? error.message : "实修回顾整理重试失败",
);
} finally {
settlementRetrying.value = null;
@@ -81,6 +81,14 @@ function formatMoney(value?: number | null, currency = "CNY") {
if (currency === "MIXED") return "多币种,见明细";
return `${currency || "CNY"} ${value.toFixed(6)}`;
}
function reportTriggerLabel(value: string) {
if (value.startsWith("lazy:")) return "学员活跃自动补齐";
if (value.startsWith("schedule:")) return "系统全局调度";
if (value.startsWith("dependency:")) return "月报依赖自动补周报";
if (value.startsWith("refresh:")) return "来源更新自动刷新";
return "管理员手动";
}
</script>
<template>
@@ -121,14 +129,6 @@ function formatMoney(value?: number | null, currency = "CNY") {
</section>
<section class="chat-summary user-metric-grid">
<div>
<span>本月主题</span
><strong
>{{ detail.metrics.monthTopics }}/{{
detail.user.entitlement?.monthlyTopicLimit ?? "不限"
}}</strong
>
</div>
<div>
<span>近30天活跃</span
><strong>{{ detail.metrics.recentActiveDays }} </strong>
@@ -141,10 +141,6 @@ function formatMoney(value?: number | null, currency = "CNY") {
<span>总消息</span
><strong>{{ detail.metrics.totalMessages }}</strong>
</div>
<div>
<span>主题总数</span
><strong>{{ detail.metrics.totalTopics }}</strong>
</div>
<div>
<span>AI 请求</span
><strong>{{ detail.metrics.aiRequestCount }}</strong>
@@ -204,7 +200,7 @@ function formatMoney(value?: number | null, currency = "CNY") {
</section>
<el-empty
v-else
description="该用户暂无新版近期实修回顾,将在下一次主题沉淀后生成"
description="该用户暂无新版近期实修回顾,将在下一次对话回顾整理后生成"
:image-size="64"
/>
@@ -212,7 +208,7 @@ function formatMoney(value?: number | null, currency = "CNY") {
<div>
<h3 class="detail-title">周期实修回顾</h3>
<p>
只基于本周期主题摘要回顾近期关注不评价成长结果同一周期重复生成会覆盖旧版本
只基于本周期对话摘要回顾近期关注不评价成长结果同一周期重复生成会覆盖旧版本
</p>
</div>
<div class="user-report-actions">
@@ -253,15 +249,16 @@ function formatMoney(value?: number | null, currency = "CNY") {
<span
>周期{{ report.periodStart }}
{{ report.periodEnd }}</span
><span>来源主题{{ report.sourceTopicIds.length }}</span>
<span>来源摘要{{ report.sourceSummaryIds.length }}</span
><span>模型{{ report.modelName || "-" }}</span>
>
<span v-if="report.reportType === 'weekly'">来源聊天{{ report.sourceMessageIds.length }}</span>
<span v-else-if="report.reportType === 'monthly'">来源周报{{ report.sourceReportIds.length }}</span>
<template v-else>
<span>来源主题{{ report.sourceTopicIds.length }}</span>
<span>来源摘要{{ report.sourceSummaryIds.length }}</span>
</template>
<span>模型{{ report.modelName || "-" }}</span>
<span
>触发方式{{
report.generatedBy.startsWith("schedule:")
? "系统定时"
: "管理员手动"
}}</span
>触发方式{{ reportTriggerLabel(report.generatedBy) }}</span
>
<span
>执行次数{{ report.attemptCount }}/{{
@@ -289,7 +286,7 @@ function formatMoney(value?: number | null, currency = "CNY") {
</el-collapse>
<el-empty v-else description="该用户暂无周期报告" :image-size="64" />
<h3 class="detail-title">最近主题</h3>
<h3 class="detail-title">最近对话片段</h3>
<el-collapse
v-if="detail.recentTopics.length"
class="topic-summary-collapse"
@@ -305,8 +302,6 @@ function formatMoney(value?: number | null, currency = "CNY") {
><span>消息数:{{ topic.messageCount }}</span>
<span
>Token{{ topic.tokenInput }}/{{ topic.tokenOutput }}</span
><span
>额度:{{ topic.quotaDeducted ? "已计入" : "不计入" }}</span
>
<span>开始:{{ topic.startedAt }}</span
><span>更新:{{ topic.updatedAt }}</span>
@@ -314,7 +309,7 @@ function formatMoney(value?: number | null, currency = "CNY") {
<template v-if="topic.summary">
<div class="topic-summary-meta">
<span
>沉淀状态:{{
>回顾整理状态:{{
reportStatusLabel(topic.summary.status)
}}</span
>

View File

@@ -419,12 +419,7 @@ async function deleteUser(row: AdminUser) {
<div class="entitlement-meta-row">
<el-tag size="small" :type="entitlementStatusType(row)">{{
entitlementStatusLabel(row)
}}</el-tag
><span
>本月主题 {{ row.entitlement?.monthlyTopicUsed ?? 0 }}/{{
row.entitlement?.monthlyTopicLimit ?? "不限"
}}</span
>
}}</el-tag>
</div>
</div>
</template>

View File

@@ -160,13 +160,13 @@ export const systemSettingSections: SystemSettingSection[] = [
description: "当前会话带入模型的最近历史消息条数;更早内容会滚动摘要,设为 0 将关闭会话记忆。修改后下一次提问立即生效。",
},
{
key: "topic_auto_settle_successful_rounds",
label: "开始自动沉淀轮数",
key: "practice_review_auto_settle_successful_rounds",
label: "实修回顾开始整理轮数",
type: "number",
defaultValue: 2,
min: 1,
max: 100,
description: "同一主题达到该数量的成功问答后,自动提炼主题标题和阶段摘要,但不会结束主题或重复扣减额度。",
description: "同一段对话达到该数量的成功问答后,自动整理阶段摘要,供近期实修回顾使用;不影响每日问答次数。",
},
{
key: "show_reference_sources",

View File

@@ -39,6 +39,9 @@ import type {
SystemConfigItem,
UserImportResult,
UserEntitlementSummary,
UserBehaviorOverview,
UserBehaviorTimeline,
UserBehaviorUserSummary,
PageResult,
PeriodicReportRecord,
PromptDetail,
@@ -151,6 +154,12 @@ export const api = {
},
peakTraffic: (grain: TrafficGrain) =>
request<PeakTrafficResult>(`/admin/dashboard/traffic${queryString({ grain })}`),
userBehaviorOverview: (query: { start?: string; end?: string } = {}) =>
request<UserBehaviorOverview>(`/admin/user-behavior/overview${queryString(query)}`),
userBehaviorUsers: (query: { start?: string; end?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
request<PageResult<UserBehaviorUserSummary>>(`/admin/user-behavior/users${queryString(query)}`),
userBehaviorTimeline: (userId: number, query: { start?: string; end?: string; page?: number; pageSize?: number } = {}) =>
request<UserBehaviorTimeline>(`/admin/user-behavior/user/${userId}/timeline${queryString(query)}`),
users: (query: { keyword?: string; planId?: number; entitlementStatus?: string; page?: number; pageSize?: number } = {}) => request<PageResult<AdminUser>>(`/admin/user/list${queryString(query)}`),
userDetail: (id: number) => request<AdminUserDetail>(`/admin/user/${id}/detail`),
userTopics: (id: number, query: { keyword?: string; page?: number; pageSize?: number } = {}) =>

View File

@@ -125,6 +125,50 @@ export interface PeakTrafficResult {
rows: PeakTrafficRow[];
}
export interface UserBehaviorOverview {
startDate: string;
endDate: string;
retentionDays: number;
totalEvents: number;
activeUsers: number;
pageDialogOpens: number;
buttonClicks: number;
daily: Array<{ date: string; eventCount: number; activeUsers: number }>;
eventRanking: Array<{
eventCode: string;
eventName: string;
eventType: "page" | "dialog" | "button";
count: number;
userCount: number;
}>;
}
export interface UserBehaviorUserSummary {
userId: number;
userName: string;
phone: string;
eventCount: number;
lastEventAt: string;
}
export interface UserBehaviorTimelineEvent {
id: number;
eventCode: string;
eventName: string;
eventType: "page" | "dialog" | "button";
targetType?: string | null;
targetId?: number | null;
occurredAt: string;
}
export interface UserBehaviorTimeline {
user: { userId: number; userName: string; phone: string };
items: UserBehaviorTimelineEvent[];
total: number;
page: number;
pageSize: number;
}
export interface PromptDetail {
id: number | null;
promptContent: string;
@@ -146,7 +190,7 @@ export interface PromptHistoryItem {
isCurrent: boolean;
}
export type ContentGenerationType = "help_card" | "share_draft";
export type ContentGenerationType = "help_card" | "share_draft" | "weekly_report" | "monthly_report";
export interface ContentGenerationVariable {
name: string;
@@ -221,8 +265,6 @@ export interface AdminUserDetail {
export interface AdminUserMetrics {
totalSessions: number;
totalMessages: number;
totalTopics: number;
monthTopics: number;
recentActiveDays: number;
lastMessageAt?: string | null;
aiRequestCount: number;
@@ -243,12 +285,10 @@ export interface EntitlementPlan {
planType: "basic" | "deep" | "addon";
description?: string | null;
validityDays?: number | null;
monthlyTopicLimit?: number | null;
enableGrowthProfile: boolean;
enablePeriodicReports: boolean;
allowHelpCard: boolean;
allowShareDraft: boolean;
deductQuota: boolean;
status: number;
sortOrder: number;
createdAt?: string | null;
@@ -259,14 +299,10 @@ export interface UserEntitlementSummary {
planId?: number | null;
name: string;
planType: string;
monthlyTopicLimit?: number | null;
monthlyTopicUsed: number;
monthlyTopicRemaining?: number | null;
enableGrowthProfile: boolean;
enablePeriodicReports: boolean;
allowHelpCard: boolean;
allowShareDraft: boolean;
deductQuota: boolean;
effectiveAt?: string | null;
expiredAt?: string | null;
source: string;
@@ -715,6 +751,8 @@ export interface PeriodicReportRecord {
content: string;
sourceSummaryIds: number[];
sourceTopicIds: number[];
sourceMessageIds: number[];
sourceReportIds: number[];
modelName?: string | null;
status: "success" | "failed" | "empty" | string;
errorMessage?: string | null;
@@ -739,7 +777,6 @@ export interface TopicSessionRecord {
messageCount: number;
tokenInput: number;
tokenOutput: number;
quotaDeducted: boolean;
startedAt: string;
endedAt?: string | null;
createdAt?: string | null;