feat: refine topic insights and learner experience

This commit is contained in:
2026-08-17 12:42:16 +08:00
parent 77399df060
commit 833763c461
27 changed files with 848 additions and 182 deletions

View File

@@ -12,12 +12,12 @@ import {
Sparkles,
Trash2,
} from "@lucide/vue";
import { computed, ref } from "vue";
import { computed, ref, watch } from "vue";
import type { PeriodicReport, PracticeReviewResult, ShareDraft, TeacherHelpCard, UserProfile } from "../types/api";
import AppDialog from "./AppDialog.vue";
type CenterSection = "overview" | "review" | "records";
type CenterSection = "overview" | "review" | "reports" | "records";
type CardSection = "help" | "share";
const props = defineProps<{
@@ -44,6 +44,11 @@ const activeSection = ref<CenterSection>(props.initialSection);
const activeCardSection = ref<CardSection>("help");
const deletingCard = ref<{ type: CardSection; id: number } | null>(null);
const entitlement = computed(() => props.user.entitlement);
const canReview = computed(() => Boolean(entitlement.value?.enableGrowthProfile));
const canReports = computed(() => Boolean(entitlement.value?.enablePeriodicReports));
const canHelpCards = computed(() => Boolean(entitlement.value?.allowHelpCard));
const canShareDrafts = computed(() => Boolean(entitlement.value?.allowShareDraft));
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"));
@@ -62,6 +67,37 @@ const capabilities = computed(() => [
{ label: "班级分享稿", enabled: Boolean(entitlement.value?.allowShareDraft) },
]);
watch(
[() => props.initialSection, canReview, canReports, canCards],
([initialSection]) => {
const requested = initialSection as CenterSection;
const available = requested === "overview"
|| (requested === "review" && canReview.value)
|| (requested === "reports" && canReports.value)
|| (requested === "records" && canCards.value);
if (!available || activeSection.value === "review" && !canReview.value
|| activeSection.value === "reports" && !canReports.value
|| activeSection.value === "records" && !canCards.value) {
activeSection.value = "overview";
} else {
activeSection.value = requested;
}
},
{ immediate: true },
);
watch(
[canHelpCards, canShareDrafts],
() => {
if (activeCardSection.value === "help" && !canHelpCards.value && canShareDrafts.value) {
activeCardSection.value = "share";
} else if (activeCardSection.value === "share" && !canShareDrafts.value && canHelpCards.value) {
activeCardSection.value = "help";
}
},
{ immediate: true },
);
function usagePercent(used: number, limit: number | null) {
if (limit === null || limit <= 0) return 0;
return Math.min(100, Math.max(0, Math.round((used / limit) * 100)));
@@ -112,8 +148,9 @@ function submitDeleteCard() {
<nav class="center-tabs" role="tablist" aria-label="个人中心栏目">
<button type="button" role="tab" :aria-selected="activeSection === 'overview'" @click="activeSection = 'overview'">账号权益</button>
<button type="button" role="tab" :aria-selected="activeSection === 'review'" @click="activeSection = 'review'">实修回顾</button>
<button type="button" role="tab" :aria-selected="activeSection === 'records'" @click="activeSection = 'records'">我的卡片</button>
<button v-if="canReview" type="button" role="tab" :aria-selected="activeSection === 'review'" @click="activeSection = 'review'">实修回顾</button>
<button v-if="canReports" type="button" role="tab" :aria-selected="activeSection === 'reports'" @click="activeSection = 'reports'">周期报告</button>
<button v-if="canCards" type="button" role="tab" :aria-selected="activeSection === 'records'" @click="activeSection = 'records'">我的卡片</button>
</nav>
<div v-if="activeSection === 'overview'" class="center-section">
@@ -207,8 +244,19 @@ function submitDeleteCard() {
</article>
</section>
<section v-if="reports.length" class="record-group">
<h3>周期实修回顾</h3>
</div>
<div v-else-if="activeSection === 'reports'" class="center-section reports-section">
<header class="card-history-head">
<div>
<h3>周期实修报告</h3>
<p>系统会根据已经沉淀的主题自动整理完整自然周和自然月的实修回顾</p>
</div>
<span>{{ reports.length }} </span>
</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" />
@@ -219,11 +267,13 @@ function submitDeleteCard() {
<pre>{{ item.content }}</pre>
</details>
</section>
<p v-else class="center-empty">暂时还没有周期报告完成主题沉淀后系统会在完整自然周或自然月结束后自动生成并展示在这里</p>
</div>
<div v-else class="center-section records-section">
<nav class="card-type-tabs" role="tablist" aria-label="我的卡片类型">
<button
v-if="canHelpCards"
type="button"
role="tab"
:aria-selected="activeCardSection === 'help'"
@@ -234,6 +284,7 @@ function submitDeleteCard() {
<small>{{ helpCards.length }}</small>
</button>
<button
v-if="canShareDrafts"
type="button"
role="tab"
:aria-selected="activeCardSection === 'share'"
@@ -364,7 +415,8 @@ function submitDeleteCard() {
.member-copy strong { overflow: hidden; font-size: 16px; text-overflow: ellipsis; white-space: nowrap; }
.member-copy span { color: var(--chat-muted); font-size: 12px; }
.member-plan { max-width: 125px; overflow: hidden; padding: 6px 9px; border-radius: 999px; background: var(--chat-brand-soft); color: #2f6d59; font-size: 11px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
.center-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; padding: 4px; border-radius: 13px; background: #f1f5f3; }
.center-tabs { display: flex; gap: 4px; padding: 4px; border-radius: 13px; background: #f1f5f3; }
.center-tabs button { flex: 1 1 0; min-width: 0; }
.center-tabs button { min-height: 36px; padding: 0 8px; border: 0; border-radius: 10px; background: transparent; color: var(--chat-muted); font-size: 13px; font-weight: 650; white-space: nowrap; }
.center-tabs button[aria-selected="true"] { background: #fff; color: var(--chat-brand-dark); box-shadow: 0 3px 12px rgba(27, 68, 55, 0.08); }
.center-section { display: grid; gap: 15px; }
@@ -408,6 +460,8 @@ function submitDeleteCard() {
.report-record summary small { color: var(--chat-weak); font-size: 10px; }
.report-record > time { display: block; padding: 0 12px 6px; }
.report-record pre { max-height: 260px; overflow: auto; margin: 0; padding: 11px 12px; border-top: 1px solid var(--chat-border); background: #fbfcfc; color: var(--chat-muted); font: inherit; font-size: 12px; line-height: 1.7; white-space: pre-wrap; }
.reports-section { align-content: start; }
.report-history-list { gap: 8px; }
.records-section { align-content: start; }
.card-type-tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 2px 0 4px; background: #fff; }
.card-type-tabs button { min-width: 0; min-height: 52px; display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; align-items: center; gap: 7px; padding: 0 11px; border: 1px solid var(--chat-border); border-radius: 13px; background: #fff; color: var(--chat-muted); font-size: 12px; font-weight: 700; text-align: left; }