feat: 完善周期报告与用户行为分析
- 支持可配置周报月报模板与登录后异步补生成\n- 增加用户行为埋点和后台分析页面\n- 移除主题额度并保留实修回顾结算\n- 修复历史会话续聊上下文丢失
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 } = {}) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user