feat: 完善周期报告与用户行为分析
- 支持可配置周报月报模板与登录后异步补生成\n- 增加用户行为埋点和后台分析页面\n- 移除主题额度并保留实修回顾结算\n- 修复历史会话续聊上下文丢失
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
LifeBuoy,
|
||||
LogOut,
|
||||
PackageCheck,
|
||||
RefreshCw,
|
||||
Share2,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
@@ -38,6 +39,8 @@ const emit = defineEmits<{
|
||||
copyShareDraft: [draft: ShareDraft];
|
||||
deleteHelpCard: [cardId: number, done: (success: boolean) => void];
|
||||
deleteShareDraft: [draftId: number, done: (success: boolean) => void];
|
||||
sectionChange: [section: CenterSection];
|
||||
refreshReports: [];
|
||||
}>();
|
||||
|
||||
const activeSection = ref<CenterSection>(props.initialSection);
|
||||
@@ -52,17 +55,13 @@ const canCards = computed(() => canHelpCards.value || canShareDrafts.value);
|
||||
const displayName = computed(() => props.user.nickname?.trim() || props.user.name);
|
||||
const nameInitial = computed(() => displayName.value.slice(0, 1));
|
||||
const maskedPhone = computed(() => props.user.phone.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2"));
|
||||
const topicUsed = computed(() => entitlement.value?.monthlyTopicUsed ?? 0);
|
||||
const topicLimit = computed(() => entitlement.value?.monthlyTopicLimit ?? null);
|
||||
const topicRemaining = computed(() => entitlement.value?.monthlyTopicRemaining ?? null);
|
||||
const topicPercent = computed(() => usagePercent(topicUsed.value, topicLimit.value));
|
||||
const dailyRemaining = computed(() => Math.max(0, props.user.dailyLimit - props.user.todayUsed));
|
||||
const dailyPercent = computed(() => usagePercent(props.user.todayUsed, props.user.dailyLimit));
|
||||
|
||||
const capabilities = computed(() => [
|
||||
{ label: "知识问答", enabled: true },
|
||||
{ label: "近期实修回顾", enabled: Boolean(entitlement.value?.enableGrowthProfile) },
|
||||
{ label: "周期实修回顾", enabled: Boolean(entitlement.value?.enablePeriodicReports) },
|
||||
{ label: "实修回顾", enabled: Boolean(entitlement.value?.enableGrowthProfile) },
|
||||
{ label: "周期报告", enabled: Boolean(entitlement.value?.enablePeriodicReports) },
|
||||
{ label: "老师求助卡", enabled: Boolean(entitlement.value?.allowHelpCard) },
|
||||
{ label: "班级分享稿", enabled: Boolean(entitlement.value?.allowShareDraft) },
|
||||
]);
|
||||
@@ -86,6 +85,8 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(activeSection, (section) => emit("sectionChange", section), { immediate: true });
|
||||
|
||||
watch(
|
||||
[canHelpCards, canShareDrafts],
|
||||
() => {
|
||||
@@ -120,6 +121,16 @@ function settlementStatusLabel(status: string) {
|
||||
}[status] || status;
|
||||
}
|
||||
|
||||
function reportStatusLabel(status: string) {
|
||||
return {
|
||||
pending: "等待生成",
|
||||
running: "正在生成",
|
||||
success: "已完成",
|
||||
failed: "生成失败",
|
||||
empty: "暂无可整理内容",
|
||||
}[status] || status;
|
||||
}
|
||||
|
||||
function submitDeleteCard() {
|
||||
if (!deletingCard.value || props.operationPending) return;
|
||||
const target = deletingCard.value;
|
||||
@@ -185,14 +196,6 @@ function submitDeleteCard() {
|
||||
|
||||
<section class="usage-section">
|
||||
<h3>使用情况</h3>
|
||||
<article class="usage-card">
|
||||
<div>
|
||||
<span>本月主题</span>
|
||||
<strong>{{ topicLimit === null ? `${topicUsed} 个 · 不限` : `${topicUsed}/${topicLimit} 个` }}</strong>
|
||||
</div>
|
||||
<div class="usage-track" aria-hidden="true"><i :style="{ width: `${topicPercent}%` }" /></div>
|
||||
<small>{{ topicRemaining === null ? '当前权益不限制本月主题数量' : `本月还可开启 ${topicRemaining} 个主题` }}</small>
|
||||
</article>
|
||||
<article class="usage-card">
|
||||
<div>
|
||||
<span>今日问答</span>
|
||||
@@ -252,22 +255,48 @@ function submitDeleteCard() {
|
||||
<h3>周期实修报告</h3>
|
||||
<p>系统会根据已经沉淀的主题,自动整理完整自然周和自然月的实修回顾。</p>
|
||||
</div>
|
||||
<span>{{ reports.length }} 份</span>
|
||||
<button type="button" class="report-refresh" :disabled="loading" @click="$emit('refreshReports')">
|
||||
<RefreshCw :size="14" :class="{ spinning: loading }" aria-hidden="true" />
|
||||
<span>{{ loading ? "刷新中" : "刷新状态" }}</span>
|
||||
</button>
|
||||
</header>
|
||||
<p v-if="loading" class="center-empty">正在加载周期报告...</p>
|
||||
<p v-else-if="!entitlement?.enablePeriodicReports" class="center-empty">当前权益未包含周期报告,已有聊天和主题回顾不会受到影响。</p>
|
||||
<section v-else-if="reports.length" class="record-group report-history-list">
|
||||
<details v-for="item in reports" :key="item.id" class="report-record">
|
||||
<summary>
|
||||
<CalendarDays :size="16" aria-hidden="true" />
|
||||
<span>{{ item.title }}</span>
|
||||
<small>{{ item.reportTypeLabel }}</small>
|
||||
</summary>
|
||||
<time>{{ formatDate(item.periodStart) }} 至 {{ formatDate(item.periodEnd) }}</time>
|
||||
<pre>{{ item.content }}</pre>
|
||||
</details>
|
||||
<section v-else-if="reports.length" class="record-group report-history-list" aria-live="polite">
|
||||
<template v-for="item in reports" :key="item.id">
|
||||
<article v-if="item.status === 'pending' || item.status === 'running'" class="report-record report-state generating">
|
||||
<span class="report-spinner" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>{{ reportStatusLabel(item.status) }},你可以先使用其他功能,稍后回来查看。</small>
|
||||
</div>
|
||||
</article>
|
||||
<article v-else-if="item.status === 'failed'" class="report-record report-state failed">
|
||||
<CalendarDays :size="17" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>本期回顾暂时未能生成,可以稍后刷新或联系运营老师。</small>
|
||||
</div>
|
||||
</article>
|
||||
<article v-else-if="item.status === 'empty'" class="report-record report-state empty">
|
||||
<CalendarDays :size="17" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>这个周期还没有可用于整理的对话记录。</small>
|
||||
</div>
|
||||
</article>
|
||||
<details v-else class="report-record">
|
||||
<summary>
|
||||
<CalendarDays :size="16" aria-hidden="true" />
|
||||
<span>{{ item.title }}</span>
|
||||
<small>{{ item.reportTypeLabel }}</small>
|
||||
</summary>
|
||||
<time>{{ formatDate(item.periodStart) }} 至 {{ formatDate(item.periodEnd) }}</time>
|
||||
<pre>{{ item.content }}</pre>
|
||||
</details>
|
||||
</template>
|
||||
</section>
|
||||
<p v-else class="center-empty">暂时还没有周期报告。完成主题沉淀后,系统会在完整自然周或自然月结束后自动生成,并展示在这里。</p>
|
||||
<p v-else class="center-empty">暂时还没有周期报告。完成对话回顾整理后,系统会在完整自然周或自然月结束后自动生成,并展示在这里。</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="center-section records-section">
|
||||
@@ -455,6 +484,21 @@ function submitDeleteCard() {
|
||||
.compact-record { padding: 11px 12px; }
|
||||
.compact-record p { margin: 5px 0 0; color: var(--chat-muted); font-size: 12px; line-height: 1.65; white-space: pre-wrap; }
|
||||
.report-record { overflow: hidden; }
|
||||
.report-refresh { flex: 0 0 auto; min-width: 94px; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 12px; border: 1px solid rgba(31, 118, 93, .24); border-radius: 10px; background: #fff; color: var(--chat-brand-dark); box-shadow: 0 2px 8px rgba(27, 68, 55, .04); cursor: pointer; font-size: 12px; font-weight: 700; line-height: 1; white-space: nowrap; transition: border-color .18s ease, background .18s ease, box-shadow .18s ease, transform .18s ease; }
|
||||
.report-refresh:hover:not(:disabled) { border-color: rgba(31, 118, 93, .42); background: var(--chat-brand-soft); box-shadow: 0 4px 12px rgba(27, 68, 55, .08); }
|
||||
.report-refresh:active:not(:disabled) { transform: translateY(1px); }
|
||||
.report-refresh:focus-visible { outline: 2px solid rgba(31, 118, 93, .28); outline-offset: 2px; }
|
||||
.report-refresh:disabled { cursor: default; opacity: .58; }
|
||||
.report-refresh .spinning { animation: report-spin .8s linear infinite; }
|
||||
.report-state { display: grid; grid-template-columns: 22px minmax(0, 1fr); align-items: center; gap: 9px; padding: 12px; }
|
||||
.report-state div { display: grid; gap: 4px; min-width: 0; }
|
||||
.report-state strong { overflow: hidden; color: var(--chat-brand-dark); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.report-state small { color: var(--chat-weak); font-size: 11px; line-height: 1.5; }
|
||||
.report-state.failed { border-color: #ead6d1; background: #fff9f7; }
|
||||
.report-state.empty { background: #f7f9f8; }
|
||||
.report-spinner { width: 16px; height: 16px; border: 2px solid #dce9e5; border-top-color: var(--chat-brand); border-radius: 50%; animation: report-spin .8s linear infinite; }
|
||||
@keyframes report-spin { to { transform: rotate(360deg); } }
|
||||
@media (prefers-reduced-motion: reduce) { .report-spinner, .report-refresh .spinning { animation: none; } }
|
||||
.report-record summary { min-height: 44px; display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; align-items: center; gap: 7px; padding: 0 11px; color: var(--chat-brand-dark); cursor: pointer; }
|
||||
.report-record summary span { overflow: hidden; font-size: 12px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.report-record summary small { color: var(--chat-weak); font-size: 10px; }
|
||||
|
||||
Reference in New Issue
Block a user