feat: 新增用户端个人中心

This commit is contained in:
2026-08-03 12:38:00 +08:00
parent 7747334ea4
commit 20ed4875b8
10 changed files with 355 additions and 176 deletions

View File

@@ -6,6 +6,7 @@ import ChatComposer from "./components/ChatComposer.vue";
import ChatHeader from "./components/ChatHeader.vue";
import LoginPanel from "./components/LoginPanel.vue";
import MessageList, { type DisplayMessage } from "./components/MessageList.vue";
import PersonalCenterDialog from "./components/PersonalCenterDialog.vue";
import SessionDrawer from "./components/SessionDrawer.vue";
import SessionQuota from "./components/SessionQuota.vue";
import { ApiError, api, clearToken, getToken, streamChat } from "./services/api";
@@ -22,7 +23,8 @@ const loadingSession = ref(false);
const loadingSessions = ref(false);
const sessionOperationPending = ref(false);
const logoutDialogOpen = ref(false);
const profileDialogOpen = ref(false);
const personalCenterOpen = ref(false);
const personalCenterSection = ref<"overview" | "review" | "records">("overview");
const helpCardDialogOpen = ref(false);
const shareDraftDialogOpen = ref(false);
const practiceReview = ref<PracticeReviewResult | null>(null);
@@ -33,7 +35,7 @@ const shareDraftContent = ref("");
const helpCardHistory = ref<TeacherHelpCard[]>([]);
const shareDraftHistory = ref<ShareDraft[]>([]);
const reportHistory = ref<PeriodicReport[]>([]);
const profileLoading = ref(false);
const personalCenterLoading = ref(false);
const finishingTopic = ref(false);
const generatingHelpCard = ref(false);
const generatingShareDraft = ref(false);
@@ -254,16 +256,6 @@ async function watchTopicSettlement(summaryId: number) {
}
}
function settlementStatusLabel(status: string) {
return {
pending: "等待沉淀",
running: "沉淀中",
success: "已完成",
fallback: "已降级完成",
failed: "沉淀失败",
}[status] || status;
}
async function generateHelpCard() {
if (!activeSessionId.value || sending.value || generatingHelpCard.value) return;
generatingHelpCard.value = true;
@@ -330,24 +322,27 @@ async function copyShareDraft() {
}
}
async function openPracticeReview() {
profileDialogOpen.value = true;
profileLoading.value = true;
async function openPersonalCenter(section: "overview" | "review" | "records" = "overview") {
personalCenterSection.value = section;
personalCenterOpen.value = true;
personalCenterLoading.value = true;
try {
const [profileResult, helpCards, shareDrafts, reports] = await Promise.all([
const [latestUser, profileResult, helpCards, shareDrafts, reports] = await Promise.all([
api.profile(),
api.practiceReview(),
api.helpCards(10),
api.shareDrafts(10),
api.periodicReports(10),
]);
user.value = latestUser;
practiceReview.value = profileResult;
helpCardHistory.value = helpCards;
shareDraftHistory.value = shareDrafts;
reportHistory.value = reports;
} catch (error) {
handleError(error, "近期实修回顾加载失败");
handleError(error, "个人中心加载失败");
} finally {
profileLoading.value = false;
personalCenterLoading.value = false;
}
}
@@ -440,6 +435,7 @@ function clearUserState() {
sessions.value = [];
messages.value = [];
activeSessionId.value = null;
personalCenterOpen.value = false;
statusText.value = "请先登录";
}
@@ -475,7 +471,12 @@ async function copyText(text: string) {
<div v-if="booting" class="booting">正在启动...</div>
<LoginPanel v-else-if="!user" @logged-in="onLoggedIn" />
<template v-else>
<ChatHeader :user="user" :status-text="statusText" @open-history="drawerOpen = true" @logout="logoutDialogOpen = true" />
<ChatHeader
:user="user"
:status-text="statusText"
@open-history="drawerOpen = true"
@open-personal-center="openPersonalCenter('overview')"
/>
<SessionQuota
:used="user.todayUsed"
:limit="user.dailyLimit"
@@ -484,7 +485,7 @@ async function copyText(text: string) {
:generating-help-card="generatingHelpCard"
:generating-share-draft="generatingShareDraft"
@finish-topic="finishCurrentTopic"
@open-profile="openPracticeReview"
@open-profile="openPersonalCenter('review')"
@generate-help-card="generateHelpCard"
@generate-share-draft="generateShareDraft"
/>
@@ -512,49 +513,18 @@ async function copyText(text: string) {
<div v-if="toastText" class="chat-toast" role="status">{{ toastText }}</div>
<AppDialog v-if="profileDialogOpen" title="近期实修回顾" labelled-by="growth-profile-title" @close="profileDialogOpen = false">
<section class="growth-profile-dialog" :aria-busy="profileLoading">
<p v-if="profileLoading" class="profile-empty">正在加载近期回顾...</p>
<template v-else-if="practiceReview?.profile">
<pre>{{ practiceReview.profile.reviewText }}</pre>
<div v-if="practiceReview.profile.currentFocus" class="growth-profile-fields">
<p><strong>近期关注</strong>{{ practiceReview.profile.currentFocus }}</p>
</div>
</template>
<p v-else class="profile-empty">还没有近期回顾你可以在完成一次主题对话后点击沉淀本主题</p>
<div v-if="practiceReview?.recentSummaries.length" class="recent-topic-summaries">
<h3>最近主题沉淀</h3>
<article v-for="item in practiceReview.recentSummaries" :key="item.id">
<time>{{ item.generatedAt }} · {{ settlementStatusLabel(item.status) }}</time>
<p v-if="item.status === 'success' || item.status === 'fallback'">{{ item.summary }}</p>
<p v-else-if="item.status === 'failed'">沉淀暂时失败对话内容仍已保留</p>
<p v-else>正在后台整理本主题的实修记录可以先关闭页面或继续聊天</p>
</article>
</div>
<div v-if="reportHistory.length" class="recent-topic-summaries periodic-report-list">
<h3>周期实修回顾</h3>
<article v-for="item in reportHistory" :key="`report-${item.id}`">
<time>{{ item.reportTypeLabel }} · {{ item.periodStart }} {{ item.periodEnd }} · {{ item.generatedAt }}</time>
<p class="report-title">{{ item.title }}</p>
<pre>{{ item.content }}</pre>
</article>
</div>
<div v-if="helpCardHistory.length" class="recent-topic-summaries">
<h3>最近老师求助卡</h3>
<article v-for="item in helpCardHistory" :key="`help-${item.id}`">
<time>{{ item.createdAt }} · {{ item.copied ? '已复制' : '未复制' }}</time>
<p>{{ item.content.slice(0, 140) }}{{ item.content.length > 140 ? '…' : '' }}</p>
</article>
</div>
<div v-if="shareDraftHistory.length" class="recent-topic-summaries">
<h3>最近班级分享稿</h3>
<article v-for="item in shareDraftHistory" :key="`share-${item.id}`">
<time>{{ item.createdAt }} · {{ item.copied ? '已复制' : '未复制' }}</time>
<p>{{ item.content.slice(0, 140) }}{{ item.content.length > 140 ? '…' : '' }}</p>
</article>
</div>
</section>
</AppDialog>
<PersonalCenterDialog
v-if="personalCenterOpen && user"
:user="user"
:initial-section="personalCenterSection"
:loading="personalCenterLoading"
:practice-review="practiceReview"
:reports="reportHistory"
:help-cards="helpCardHistory"
:share-drafts="shareDraftHistory"
@close="personalCenterOpen = false"
@logout="personalCenterOpen = false; logoutDialogOpen = true"
/>
<AppDialog v-if="helpCardDialogOpen" title="老师求助卡" labelled-by="help-card-title" @close="helpCardDialogOpen = false">
<section class="help-card-dialog">