feat: 新增用户端个人中心
This commit is contained in:
@@ -20,6 +20,8 @@ class EntitlementView:
|
|||||||
plan_id: int | None
|
plan_id: int | None
|
||||||
name: str
|
name: str
|
||||||
plan_type: str
|
plan_type: str
|
||||||
|
description: str | None
|
||||||
|
validity_days: int | None
|
||||||
monthly_topic_limit: int | None
|
monthly_topic_limit: int | None
|
||||||
monthly_topic_used: int
|
monthly_topic_used: int
|
||||||
enable_growth_profile: bool
|
enable_growth_profile: bool
|
||||||
@@ -96,6 +98,8 @@ class EntitlementService:
|
|||||||
plan_id=None,
|
plan_id=None,
|
||||||
name="旧版每日额度",
|
name="旧版每日额度",
|
||||||
plan_type="legacy",
|
plan_type="legacy",
|
||||||
|
description="按每日问答额度提供基础服务。",
|
||||||
|
validity_days=None,
|
||||||
monthly_topic_limit=None,
|
monthly_topic_limit=None,
|
||||||
monthly_topic_used=monthly_topic_used,
|
monthly_topic_used=monthly_topic_used,
|
||||||
enable_growth_profile=False,
|
enable_growth_profile=False,
|
||||||
@@ -200,6 +204,8 @@ def entitlement_dict(view: EntitlementView) -> dict:
|
|||||||
"planId": view.plan_id,
|
"planId": view.plan_id,
|
||||||
"name": view.name,
|
"name": view.name,
|
||||||
"planType": view.plan_type,
|
"planType": view.plan_type,
|
||||||
|
"description": view.description,
|
||||||
|
"validityDays": view.validity_days,
|
||||||
"monthlyTopicLimit": view.monthly_topic_limit,
|
"monthlyTopicLimit": view.monthly_topic_limit,
|
||||||
"monthlyTopicUsed": view.monthly_topic_used,
|
"monthlyTopicUsed": view.monthly_topic_used,
|
||||||
"monthlyTopicRemaining": view.monthly_topic_remaining,
|
"monthlyTopicRemaining": view.monthly_topic_remaining,
|
||||||
@@ -238,6 +244,8 @@ def view_from_plan(
|
|||||||
plan_id=plan.id,
|
plan_id=plan.id,
|
||||||
name=plan.name,
|
name=plan.name,
|
||||||
plan_type=plan.plan_type,
|
plan_type=plan.plan_type,
|
||||||
|
description=plan.description,
|
||||||
|
validity_days=plan.validity_days,
|
||||||
monthly_topic_limit=plan.monthly_topic_limit,
|
monthly_topic_limit=plan.monthly_topic_limit,
|
||||||
monthly_topic_used=monthly_topic_used,
|
monthly_topic_used=monthly_topic_used,
|
||||||
enable_growth_profile=bool(plan.enable_growth_profile),
|
enable_growth_profile=bool(plan.enable_growth_profile),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from app.models.chat import ChatMessage, ChatSession, TopicSession
|
|||||||
from app.models.entitlement import EntitlementPlan
|
from app.models.entitlement import EntitlementPlan
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.services.chat_service import ChatService
|
from app.services.chat_service import ChatService
|
||||||
from app.services.entitlement_service import EntitlementService
|
from app.services.entitlement_service import EntitlementService, entitlement_dict
|
||||||
from app.services.rag_service import RagResult, RagService
|
from app.services.rag_service import RagResult, RagService
|
||||||
from app.services.topic_session_service import TopicSessionService
|
from app.services.topic_session_service import TopicSessionService
|
||||||
|
|
||||||
@@ -44,6 +44,8 @@ def test_default_entitlement_uses_basic_plan_when_user_has_no_assignment():
|
|||||||
id=10,
|
id=10,
|
||||||
name="大本营基础版",
|
name="大本营基础版",
|
||||||
plan_type="basic",
|
plan_type="basic",
|
||||||
|
description="基础知识问答权益",
|
||||||
|
validity_days=180,
|
||||||
monthly_topic_limit=30,
|
monthly_topic_limit=30,
|
||||||
status=1,
|
status=1,
|
||||||
sort_order=10,
|
sort_order=10,
|
||||||
@@ -55,8 +57,12 @@ def test_default_entitlement_uses_basic_plan_when_user_has_no_assignment():
|
|||||||
|
|
||||||
assert view.plan_id == 10
|
assert view.plan_id == 10
|
||||||
assert view.name == "大本营基础版"
|
assert view.name == "大本营基础版"
|
||||||
|
assert view.description == "基础知识问答权益"
|
||||||
|
assert view.validity_days == 180
|
||||||
assert view.source == "default"
|
assert view.source == "default"
|
||||||
assert view.monthly_topic_remaining == 27
|
assert view.monthly_topic_remaining == 27
|
||||||
|
assert entitlement_dict(view)["description"] == "基础知识问答权益"
|
||||||
|
assert entitlement_dict(view)["validityDays"] == 180
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_teacher_plan_is_not_exposed_or_used_as_default():
|
def test_legacy_teacher_plan_is_not_exposed_or_used_as_default():
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import ChatComposer from "./components/ChatComposer.vue";
|
|||||||
import ChatHeader from "./components/ChatHeader.vue";
|
import ChatHeader from "./components/ChatHeader.vue";
|
||||||
import LoginPanel from "./components/LoginPanel.vue";
|
import LoginPanel from "./components/LoginPanel.vue";
|
||||||
import MessageList, { type DisplayMessage } from "./components/MessageList.vue";
|
import MessageList, { type DisplayMessage } from "./components/MessageList.vue";
|
||||||
|
import PersonalCenterDialog from "./components/PersonalCenterDialog.vue";
|
||||||
import SessionDrawer from "./components/SessionDrawer.vue";
|
import SessionDrawer from "./components/SessionDrawer.vue";
|
||||||
import SessionQuota from "./components/SessionQuota.vue";
|
import SessionQuota from "./components/SessionQuota.vue";
|
||||||
import { ApiError, api, clearToken, getToken, streamChat } from "./services/api";
|
import { ApiError, api, clearToken, getToken, streamChat } from "./services/api";
|
||||||
@@ -22,7 +23,8 @@ const loadingSession = ref(false);
|
|||||||
const loadingSessions = ref(false);
|
const loadingSessions = ref(false);
|
||||||
const sessionOperationPending = ref(false);
|
const sessionOperationPending = ref(false);
|
||||||
const logoutDialogOpen = 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 helpCardDialogOpen = ref(false);
|
||||||
const shareDraftDialogOpen = ref(false);
|
const shareDraftDialogOpen = ref(false);
|
||||||
const practiceReview = ref<PracticeReviewResult | null>(null);
|
const practiceReview = ref<PracticeReviewResult | null>(null);
|
||||||
@@ -33,7 +35,7 @@ const shareDraftContent = ref("");
|
|||||||
const helpCardHistory = ref<TeacherHelpCard[]>([]);
|
const helpCardHistory = ref<TeacherHelpCard[]>([]);
|
||||||
const shareDraftHistory = ref<ShareDraft[]>([]);
|
const shareDraftHistory = ref<ShareDraft[]>([]);
|
||||||
const reportHistory = ref<PeriodicReport[]>([]);
|
const reportHistory = ref<PeriodicReport[]>([]);
|
||||||
const profileLoading = ref(false);
|
const personalCenterLoading = ref(false);
|
||||||
const finishingTopic = ref(false);
|
const finishingTopic = ref(false);
|
||||||
const generatingHelpCard = ref(false);
|
const generatingHelpCard = ref(false);
|
||||||
const generatingShareDraft = 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() {
|
async function generateHelpCard() {
|
||||||
if (!activeSessionId.value || sending.value || generatingHelpCard.value) return;
|
if (!activeSessionId.value || sending.value || generatingHelpCard.value) return;
|
||||||
generatingHelpCard.value = true;
|
generatingHelpCard.value = true;
|
||||||
@@ -330,24 +322,27 @@ async function copyShareDraft() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openPracticeReview() {
|
async function openPersonalCenter(section: "overview" | "review" | "records" = "overview") {
|
||||||
profileDialogOpen.value = true;
|
personalCenterSection.value = section;
|
||||||
profileLoading.value = true;
|
personalCenterOpen.value = true;
|
||||||
|
personalCenterLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const [profileResult, helpCards, shareDrafts, reports] = await Promise.all([
|
const [latestUser, profileResult, helpCards, shareDrafts, reports] = await Promise.all([
|
||||||
|
api.profile(),
|
||||||
api.practiceReview(),
|
api.practiceReview(),
|
||||||
api.helpCards(10),
|
api.helpCards(10),
|
||||||
api.shareDrafts(10),
|
api.shareDrafts(10),
|
||||||
api.periodicReports(10),
|
api.periodicReports(10),
|
||||||
]);
|
]);
|
||||||
|
user.value = latestUser;
|
||||||
practiceReview.value = profileResult;
|
practiceReview.value = profileResult;
|
||||||
helpCardHistory.value = helpCards;
|
helpCardHistory.value = helpCards;
|
||||||
shareDraftHistory.value = shareDrafts;
|
shareDraftHistory.value = shareDrafts;
|
||||||
reportHistory.value = reports;
|
reportHistory.value = reports;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "近期实修回顾加载失败");
|
handleError(error, "个人中心加载失败");
|
||||||
} finally {
|
} finally {
|
||||||
profileLoading.value = false;
|
personalCenterLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +435,7 @@ function clearUserState() {
|
|||||||
sessions.value = [];
|
sessions.value = [];
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
activeSessionId.value = null;
|
activeSessionId.value = null;
|
||||||
|
personalCenterOpen.value = false;
|
||||||
statusText.value = "请先登录";
|
statusText.value = "请先登录";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +471,12 @@ async function copyText(text: string) {
|
|||||||
<div v-if="booting" class="booting">正在启动...</div>
|
<div v-if="booting" class="booting">正在启动...</div>
|
||||||
<LoginPanel v-else-if="!user" @logged-in="onLoggedIn" />
|
<LoginPanel v-else-if="!user" @logged-in="onLoggedIn" />
|
||||||
<template v-else>
|
<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
|
<SessionQuota
|
||||||
:used="user.todayUsed"
|
:used="user.todayUsed"
|
||||||
:limit="user.dailyLimit"
|
:limit="user.dailyLimit"
|
||||||
@@ -484,7 +485,7 @@ async function copyText(text: string) {
|
|||||||
:generating-help-card="generatingHelpCard"
|
:generating-help-card="generatingHelpCard"
|
||||||
:generating-share-draft="generatingShareDraft"
|
:generating-share-draft="generatingShareDraft"
|
||||||
@finish-topic="finishCurrentTopic"
|
@finish-topic="finishCurrentTopic"
|
||||||
@open-profile="openPracticeReview"
|
@open-profile="openPersonalCenter('review')"
|
||||||
@generate-help-card="generateHelpCard"
|
@generate-help-card="generateHelpCard"
|
||||||
@generate-share-draft="generateShareDraft"
|
@generate-share-draft="generateShareDraft"
|
||||||
/>
|
/>
|
||||||
@@ -512,49 +513,18 @@ async function copyText(text: string) {
|
|||||||
|
|
||||||
<div v-if="toastText" class="chat-toast" role="status">{{ toastText }}</div>
|
<div v-if="toastText" class="chat-toast" role="status">{{ toastText }}</div>
|
||||||
|
|
||||||
<AppDialog v-if="profileDialogOpen" title="近期实修回顾" labelled-by="growth-profile-title" @close="profileDialogOpen = false">
|
<PersonalCenterDialog
|
||||||
<section class="growth-profile-dialog" :aria-busy="profileLoading">
|
v-if="personalCenterOpen && user"
|
||||||
<p v-if="profileLoading" class="profile-empty">正在加载近期回顾...</p>
|
:user="user"
|
||||||
<template v-else-if="practiceReview?.profile">
|
:initial-section="personalCenterSection"
|
||||||
<pre>{{ practiceReview.profile.reviewText }}</pre>
|
:loading="personalCenterLoading"
|
||||||
<div v-if="practiceReview.profile.currentFocus" class="growth-profile-fields">
|
:practice-review="practiceReview"
|
||||||
<p><strong>近期关注</strong>{{ practiceReview.profile.currentFocus }}</p>
|
:reports="reportHistory"
|
||||||
</div>
|
:help-cards="helpCardHistory"
|
||||||
</template>
|
:share-drafts="shareDraftHistory"
|
||||||
<p v-else class="profile-empty">还没有近期回顾。你可以在完成一次主题对话后点击“沉淀本主题”。</p>
|
@close="personalCenterOpen = false"
|
||||||
<div v-if="practiceReview?.recentSummaries.length" class="recent-topic-summaries">
|
@logout="personalCenterOpen = false; logoutDialogOpen = true"
|
||||||
<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>
|
|
||||||
|
|
||||||
<AppDialog v-if="helpCardDialogOpen" title="老师求助卡" labelled-by="help-card-title" @close="helpCardDialogOpen = false">
|
<AppDialog v-if="helpCardDialogOpen" title="老师求助卡" labelled-by="help-card-title" @close="helpCardDialogOpen = false">
|
||||||
<section class="help-card-dialog">
|
<section class="help-card-dialog">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Menu } from "@lucide/vue";
|
import { Menu, UserRound } from "@lucide/vue";
|
||||||
|
|
||||||
import type { UserProfile } from "../types/api";
|
import type { UserProfile } from "../types/api";
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ defineProps<{
|
|||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
openHistory: [];
|
openHistory: [];
|
||||||
logout: [];
|
openPersonalCenter: [];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -25,11 +25,9 @@ defineEmits<{
|
|||||||
<p>{{ statusText }}</p>
|
<p>{{ statusText }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-summary">
|
<button type="button" class="header-profile-button" aria-label="打开个人中心" @click="$emit('openPersonalCenter')">
|
||||||
<span v-if="user.nickname">昵称:{{ user.nickname }}</span>
|
<UserRound :size="18" aria-hidden="true" />
|
||||||
<span>用户名:{{ user.name }}</span>
|
<span>{{ user.nickname?.trim() || user.name }}</span>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<button type="button" class="header-logout-button" @click="$emit('logout')">退出</button>
|
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CalendarDays, CheckCircle2, CircleMinus, LogOut, PackageCheck, Sparkles } from "@lucide/vue";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
import type { PeriodicReport, PracticeReviewResult, ShareDraft, TeacherHelpCard, UserProfile } from "../types/api";
|
||||||
|
import AppDialog from "./AppDialog.vue";
|
||||||
|
|
||||||
|
type CenterSection = "overview" | "review" | "records";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: UserProfile;
|
||||||
|
initialSection: CenterSection;
|
||||||
|
loading: boolean;
|
||||||
|
practiceReview: PracticeReviewResult | null;
|
||||||
|
reports: PeriodicReport[];
|
||||||
|
helpCards: TeacherHelpCard[];
|
||||||
|
shareDrafts: ShareDraft[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
close: [];
|
||||||
|
logout: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const activeSection = ref<CenterSection>(props.initialSection);
|
||||||
|
const entitlement = computed(() => props.user.entitlement);
|
||||||
|
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?.allowHelpCard) },
|
||||||
|
{ label: "班级分享稿", enabled: Boolean(entitlement.value?.allowShareDraft) },
|
||||||
|
]);
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value?: string | null) {
|
||||||
|
if (!value) return "长期有效";
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
|
return date.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function settlementStatusLabel(status: string) {
|
||||||
|
return {
|
||||||
|
pending: "等待整理",
|
||||||
|
running: "整理中",
|
||||||
|
success: "已完成",
|
||||||
|
fallback: "已完成",
|
||||||
|
failed: "整理失败",
|
||||||
|
}[status] || status;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppDialog title="个人中心" labelled-by="personal-center-title" @close="$emit('close')">
|
||||||
|
<section class="personal-center" :aria-busy="loading">
|
||||||
|
<header class="member-card">
|
||||||
|
<div class="member-avatar" aria-hidden="true">{{ nameInitial }}</div>
|
||||||
|
<div class="member-copy">
|
||||||
|
<strong>{{ displayName }}</strong>
|
||||||
|
<span>{{ maskedPhone }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="member-plan">{{ entitlement?.name || '基础服务' }}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div v-if="activeSection === 'overview'" class="center-section">
|
||||||
|
<article class="plan-card">
|
||||||
|
<div class="section-heading">
|
||||||
|
<PackageCheck :size="18" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<span>当前权益</span>
|
||||||
|
<strong>{{ entitlement?.name || '基础服务' }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>{{ entitlement?.description || '提供当前账号可使用的问答与陪伴能力。' }}</p>
|
||||||
|
<dl class="plan-meta">
|
||||||
|
<div>
|
||||||
|
<dt>生效时间</dt>
|
||||||
|
<dd>{{ entitlement?.effectiveAt ? formatDate(entitlement.effectiveAt) : '当前已生效' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>有效期至</dt>
|
||||||
|
<dd>{{ formatDate(entitlement?.expiredAt) }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<strong>{{ user.todayUsed }}/{{ user.dailyLimit }} 次</strong>
|
||||||
|
</div>
|
||||||
|
<div class="usage-track" aria-hidden="true"><i :style="{ width: `${dailyPercent}%` }" /></div>
|
||||||
|
<small>今日还可使用 {{ dailyRemaining }} 次,次日自动恢复</small>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="capability-section">
|
||||||
|
<h3>当前可用能力</h3>
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in capabilities" :key="item.label" :class="{ unavailable: !item.enabled }">
|
||||||
|
<CheckCircle2 v-if="item.enabled" :size="17" aria-hidden="true" />
|
||||||
|
<CircleMinus v-else :size="17" aria-hidden="true" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<small>{{ item.enabled ? '已包含' : '当前权益未包含' }}</small>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="activeSection === 'review'" class="center-section review-section">
|
||||||
|
<p v-if="loading" class="center-empty">正在加载近期回顾...</p>
|
||||||
|
<template v-else-if="practiceReview?.profile">
|
||||||
|
<article class="review-card primary-review">
|
||||||
|
<div class="section-heading">
|
||||||
|
<Sparkles :size="18" aria-hidden="true" />
|
||||||
|
<strong>近期实修回顾</strong>
|
||||||
|
</div>
|
||||||
|
<p>{{ practiceReview.profile.reviewText }}</p>
|
||||||
|
<div v-if="practiceReview.profile.currentFocus" class="current-focus">
|
||||||
|
<span>近期关注</span>
|
||||||
|
<p>{{ practiceReview.profile.currentFocus }}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
<p v-else-if="!entitlement?.enableGrowthProfile" class="center-empty">当前权益未包含近期实修回顾,你仍然可以正常使用知识问答和当前已开放的其他能力。</p>
|
||||||
|
<p v-else class="center-empty">还没有近期回顾。完成一次主题对话并沉淀后,这里会帮助你简单回看近期谈到的内容。</p>
|
||||||
|
|
||||||
|
<section v-if="practiceReview?.recentSummaries.length" class="record-group">
|
||||||
|
<h3>最近谈到的主题</h3>
|
||||||
|
<article v-for="item in practiceReview.recentSummaries" :key="item.id" class="compact-record">
|
||||||
|
<time>{{ formatDate(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>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="reports.length" class="record-group">
|
||||||
|
<h3>周期实修回顾</h3>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="center-section records-section">
|
||||||
|
<section class="record-group">
|
||||||
|
<h3>老师求助卡</h3>
|
||||||
|
<article v-for="item in helpCards" :key="`help-${item.id}`" class="compact-record">
|
||||||
|
<time>{{ formatDate(item.createdAt) }} · {{ item.copied ? '已复制' : '未复制' }}</time>
|
||||||
|
<p>{{ item.content.slice(0, 160) }}{{ item.content.length > 160 ? '…' : '' }}</p>
|
||||||
|
</article>
|
||||||
|
<p v-if="!loading && !helpCards.length" class="center-empty small">还没有生成过求助卡。</p>
|
||||||
|
</section>
|
||||||
|
<section class="record-group">
|
||||||
|
<h3>班级分享稿</h3>
|
||||||
|
<article v-for="item in shareDrafts" :key="`share-${item.id}`" class="compact-record">
|
||||||
|
<time>{{ formatDate(item.createdAt) }} · {{ item.copied ? '已复制' : '未复制' }}</time>
|
||||||
|
<p>{{ item.content.slice(0, 160) }}{{ item.content.length > 160 ? '…' : '' }}</p>
|
||||||
|
</article>
|
||||||
|
<p v-if="!loading && !shareDrafts.length" class="center-empty small">还没有生成过分享稿。</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="personal-center-footer">
|
||||||
|
<button type="button" class="center-logout" @click="$emit('logout')">
|
||||||
|
<LogOut :size="17" aria-hidden="true" />
|
||||||
|
退出当前账号
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dialog-primary center-done" @click="$emit('close')">完成</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</AppDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.personal-center { display: grid; gap: 16px; color: var(--chat-text); }
|
||||||
|
.member-card { display: grid; grid-template-columns: 46px minmax(0, 1fr) auto; align-items: center; gap: 11px; }
|
||||||
|
.member-avatar { width: 46px; height: 46px; display: grid; place-items: center; border-radius: 15px; background: linear-gradient(145deg, #dceee7, #edf7f3); color: var(--chat-brand-dark); font-size: 19px; font-weight: 800; }
|
||||||
|
.member-copy { min-width: 0; display: grid; gap: 3px; }
|
||||||
|
.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 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; }
|
||||||
|
.plan-card, .review-card, .usage-card, .compact-record, .report-record { border: 1px solid var(--chat-border); border-radius: 14px; background: #fff; }
|
||||||
|
.plan-card { padding: 14px; background: linear-gradient(145deg, #f2f8f5, #fff); }
|
||||||
|
.section-heading { display: flex; align-items: center; gap: 9px; color: var(--chat-brand-dark); }
|
||||||
|
.section-heading > div { display: grid; gap: 2px; }
|
||||||
|
.section-heading span { color: var(--chat-muted); font-size: 11px; }
|
||||||
|
.section-heading strong { font-size: 15px; }
|
||||||
|
.plan-card > p { margin: 10px 0 12px; color: var(--chat-muted); font-size: 13px; line-height: 1.65; }
|
||||||
|
.plan-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 0; }
|
||||||
|
.plan-meta div { padding: 9px 10px; border-radius: 10px; background: rgba(255, 255, 255, 0.82); }
|
||||||
|
.plan-meta dt { color: var(--chat-weak); font-size: 10px; }
|
||||||
|
.plan-meta dd { margin: 4px 0 0; color: var(--chat-text); font-size: 12px; font-weight: 650; }
|
||||||
|
.usage-section, .capability-section, .record-group { display: grid; gap: 9px; }
|
||||||
|
.usage-section h3, .capability-section h3, .record-group h3 { margin: 0; font-size: 14px; }
|
||||||
|
.usage-card { display: grid; gap: 7px; padding: 12px; }
|
||||||
|
.usage-card > div:first-child { display: flex; justify-content: space-between; gap: 10px; font-size: 13px; }
|
||||||
|
.usage-card strong { color: var(--chat-brand-dark); }
|
||||||
|
.usage-card small, .compact-record time, .report-record time { color: var(--chat-weak); font-size: 11px; }
|
||||||
|
.usage-track { height: 6px; overflow: hidden; border-radius: 999px; background: #e8efec; }
|
||||||
|
.usage-track i { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #4fa486, #1f765d); }
|
||||||
|
.capability-section ul { display: grid; gap: 1px; margin: 0; padding: 0; overflow: hidden; border: 1px solid var(--chat-border); border-radius: 14px; list-style: none; background: var(--chat-border); }
|
||||||
|
.capability-section li { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; align-items: center; gap: 7px; min-height: 42px; padding: 0 12px; background: #fff; color: var(--chat-brand-dark); font-size: 13px; }
|
||||||
|
.capability-section li small { color: var(--chat-muted); font-size: 10px; }
|
||||||
|
.capability-section li.unavailable { color: var(--chat-weak); }
|
||||||
|
.primary-review { padding: 14px; background: var(--chat-brand-soft); }
|
||||||
|
.primary-review > p { margin: 11px 0 0; font-size: 13px; line-height: 1.75; white-space: pre-wrap; }
|
||||||
|
.current-focus { margin-top: 12px; padding-top: 11px; border-top: 1px solid rgba(31, 107, 82, 0.12); }
|
||||||
|
.current-focus span { color: var(--chat-muted); font-size: 11px; }
|
||||||
|
.current-focus p { margin: 4px 0 0; color: var(--chat-brand-dark); font-size: 13px; line-height: 1.65; }
|
||||||
|
.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-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; }
|
||||||
|
.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; }
|
||||||
|
.center-empty { margin: 0; padding: 22px 14px; border-radius: 14px; background: #f6f8f7; color: var(--chat-muted); font-size: 13px; line-height: 1.7; text-align: center; }
|
||||||
|
.center-empty.small { padding: 14px; font-size: 12px; }
|
||||||
|
.personal-center-footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||||
|
.center-logout { min-height: 42px; display: inline-flex; align-items: center; gap: 7px; padding: 0 10px; border: 0; background: transparent; color: #8b5757; font-size: 12px; }
|
||||||
|
.center-done { width: auto; min-width: 92px; min-height: 42px; padding: 0 18px; border: 0; border-radius: 12px; }
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.member-plan { max-width: 96px; }
|
||||||
|
.center-tabs button { padding: 0 4px; font-size: 12px; }
|
||||||
|
.plan-meta { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1131,7 +1131,7 @@ textarea:focus-visible {
|
|||||||
z-index: 5;
|
z-index: 5;
|
||||||
min-height: 82px;
|
min-height: 82px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 44px minmax(0, 1fr) auto 52px;
|
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: max(10px, env(safe-area-inset-top)) 14px 10px;
|
padding: max(10px, env(safe-area-inset-top)) 14px 10px;
|
||||||
@@ -1140,7 +1140,7 @@ textarea:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-menu-button,
|
.header-menu-button,
|
||||||
.header-logout-button,
|
.header-profile-button,
|
||||||
.history-header button {
|
.history-header button {
|
||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
@@ -1177,28 +1177,21 @@ textarea:focus-visible {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-summary {
|
.header-profile-button {
|
||||||
max-width: 92px;
|
max-width: 120px;
|
||||||
display: grid;
|
grid-template-columns: 18px minmax(0, auto);
|
||||||
gap: 2px;
|
gap: 6px;
|
||||||
color: var(--chat-muted);
|
padding: 0 11px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
line-height: 1.45;
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-summary span {
|
.header-profile-button span {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-logout-button {
|
|
||||||
min-width: 52px;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-quota {
|
.session-quota {
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1711,27 +1704,6 @@ textarea:focus-visible {
|
|||||||
line-height: 1.75;
|
line-height: 1.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.growth-profile-dialog {
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
max-height: 60vh;
|
|
||||||
overflow: auto;
|
|
||||||
padding-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.growth-profile-dialog pre {
|
|
||||||
margin: 0;
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid var(--chat-border);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: var(--chat-brand-soft);
|
|
||||||
color: var(--chat-text);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.75;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-card-dialog {
|
.help-card-dialog {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -1765,64 +1737,6 @@ textarea:focus-visible {
|
|||||||
box-shadow: 0 0 0 3px rgba(20, 148, 119, 0.1);
|
box-shadow: 0 0 0 3px rgba(20, 148, 119, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.growth-profile-fields,
|
|
||||||
.recent-topic-summaries {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.growth-profile-fields p,
|
|
||||||
.profile-empty {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--chat-muted);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.growth-profile-fields strong {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
color: var(--chat-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-topic-summaries h3 {
|
|
||||||
margin: 4px 0 0;
|
|
||||||
color: var(--chat-text);
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-topic-summaries article {
|
|
||||||
padding: 11px 12px;
|
|
||||||
border: 1px solid var(--chat-border);
|
|
||||||
border-radius: 13px;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-topic-summaries time {
|
|
||||||
color: var(--chat-weak);
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-topic-summaries p {
|
|
||||||
margin: 5px 0 0;
|
|
||||||
color: var(--chat-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.periodic-report-list .report-title {
|
|
||||||
color: var(--chat-text);
|
|
||||||
font-weight: 650;
|
|
||||||
}
|
|
||||||
|
|
||||||
.periodic-report-list pre {
|
|
||||||
max-height: 260px;
|
|
||||||
margin: 8px 0 0;
|
|
||||||
overflow: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-actions {
|
.dialog-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -1867,7 +1781,7 @@ body.drawer-open { overflow: hidden; }
|
|||||||
|
|
||||||
@media (max-width: 390px) {
|
@media (max-width: 390px) {
|
||||||
.chat-header {
|
.chat-header {
|
||||||
grid-template-columns: 44px minmax(0, 1fr) auto 48px;
|
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
@@ -1875,8 +1789,7 @@ body.drawer-open { overflow: hidden; }
|
|||||||
|
|
||||||
.assistant-summary h1 { font-size: 15px; }
|
.assistant-summary h1 { font-size: 15px; }
|
||||||
.assistant-summary p { font-size: 10px; }
|
.assistant-summary p { font-size: 10px; }
|
||||||
.user-summary { max-width: 72px; font-size: 9px; }
|
.header-profile-button { max-width: 94px; padding: 0 8px; }
|
||||||
.header-logout-button { min-width: 48px; padding: 0 5px; }
|
|
||||||
.session-quota { margin-right: 12px; margin-left: 12px; }
|
.session-quota { margin-right: 12px; margin-left: 12px; }
|
||||||
.session-quota-actions { gap: 5px; }
|
.session-quota-actions { gap: 5px; }
|
||||||
.quota-link { padding: 0 6px; font-size: 11px; }
|
.quota-link { padding: 0 6px; font-size: 11px; }
|
||||||
@@ -1887,8 +1800,9 @@ body.drawer-open { overflow: hidden; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 350px) {
|
@media (max-width: 350px) {
|
||||||
.user-summary { display: none; }
|
.header-profile-button { width: 44px; grid-template-columns: 1fr; padding: 0; }
|
||||||
.chat-header { grid-template-columns: 44px minmax(0, 1fr) 48px; }
|
.header-profile-button span { display: none; }
|
||||||
|
.chat-header { grid-template-columns: 44px minmax(0, 1fr) 44px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 680px) {
|
@media (max-height: 680px) {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export interface UserEntitlementSummary {
|
|||||||
planId: number | null;
|
planId: number | null;
|
||||||
name: string;
|
name: string;
|
||||||
planType: string;
|
planType: string;
|
||||||
|
description?: string | null;
|
||||||
|
validityDays?: number | null;
|
||||||
monthlyTopicLimit: number | null;
|
monthlyTopicLimit: number | null;
|
||||||
monthlyTopicUsed: number;
|
monthlyTopicUsed: number;
|
||||||
monthlyTopicRemaining: number | null;
|
monthlyTopicRemaining: number | null;
|
||||||
|
|||||||
@@ -46,6 +46,8 @@
|
|||||||
- [ ] 用户端可以创建会话。
|
- [ ] 用户端可以创建会话。
|
||||||
- [ ] 用户端可以提问并收到回答。
|
- [ ] 用户端可以提问并收到回答。
|
||||||
- [ ] 用户发送后输入框立即清空,思考状态与流式 Markdown 回答显示正常。
|
- [ ] 用户发送后输入框立即清空,思考状态与流式 Markdown 回答显示正常。
|
||||||
|
- [ ] 用户端个人中心可查看账号、当前套餐、有效期、主题/问答用量和能力清单。
|
||||||
|
- [ ] 深度版可查看近期与周期实修回顾;基础版明确显示权益边界,不展示成长评分或旧画像字段。
|
||||||
- [ ] 连续提问能够继续使用同一主题的上下文。
|
- [ ] 连续提问能够继续使用同一主题的上下文。
|
||||||
- [ ] 结束主题接口快速返回,用户可以立即新建主题或继续使用系统。
|
- [ ] 结束主题接口快速返回,用户可以立即新建主题或继续使用系统。
|
||||||
- [ ] 无命中问题返回固定兜底。
|
- [ ] 无命中问题返回固定兜底。
|
||||||
|
|||||||
@@ -642,19 +642,23 @@ AI 日志增加:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10. 用户端“近期实修回顾”
|
### 10. 用户端个人中心与“近期实修回顾”
|
||||||
|
|
||||||
- 优先级:P1
|
- 优先级:P1
|
||||||
- 改动大小:M
|
- 改动大小:M
|
||||||
- 影响范围:用户端、近期回顾接口、主题历史
|
- 影响范围:用户端、用户权益接口、近期回顾接口、主题历史
|
||||||
- 依赖:主题会话机制、近期实修回顾
|
- 依赖:主题会话机制、近期实修回顾
|
||||||
|
|
||||||
#### 目标
|
#### 目标
|
||||||
|
|
||||||
帮助用户看见最近谈到的内容并回到当下,不形成成长成绩单或长期自我标签。
|
为用户提供统一的账号、套餐、用量和权益入口;同时帮助用户看见最近谈到的内容并回到当下,不形成成长成绩单或长期自我标签。
|
||||||
|
|
||||||
#### 用户端建议展示
|
#### 用户端建议展示
|
||||||
|
|
||||||
|
- 账号与当前套餐;
|
||||||
|
- 套餐说明、生效时间和有效期;
|
||||||
|
- 本月主题、今日问答用量和剩余额度;
|
||||||
|
- 当前套餐包含的能力;
|
||||||
- 最近主题;
|
- 最近主题;
|
||||||
- 主题摘要;
|
- 主题摘要;
|
||||||
- 每周小结;
|
- 每周小结;
|
||||||
@@ -676,6 +680,7 @@ AI 日志增加:
|
|||||||
|
|
||||||
- 2026-07-31:用户端原“我的档案”弹窗已扩展为实修记录视图,可查看实修回顾、最近主题沉淀、最近老师求助卡和最近班级分享稿;仍保持“我的实修记录”表达,不做后台画像式展示。
|
- 2026-07-31:用户端原“我的档案”弹窗已扩展为实修记录视图,可查看实修回顾、最近主题沉淀、最近老师求助卡和最近班级分享稿;仍保持“我的实修记录”表达,不做后台画像式展示。
|
||||||
- 2026-08-03:升级为 V2“近期实修回顾”。移除常见情绪、身体/关系模式、做过或有效功课、近期变化等字段;旧版数据不再展示或注入 Agent,下一次主题沉淀时按最近30天、最多10个新版主题懒重建。
|
- 2026-08-03:升级为 V2“近期实修回顾”。移除常见情绪、身体/关系模式、做过或有效功课、近期变化等字段;旧版数据不再展示或注入 Agent,下一次主题沉淀时按最近30天、最多10个新版主题懒重建。
|
||||||
|
- 2026-08-03:新增用户端“个人中心”,顶部头像入口统一展示账号、当前套餐、有效期、本月主题和今日问答用量、剩余额度及能力清单;近期回顾、周期回顾、求助卡和分享稿按栏目查看。基础版会明确提示未包含的能力,不做成长评分、打卡天数或结果排名。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
| 用户登录 | 通过 | 通过真实页面完成验证码登录并进入聊天页 |
|
| 用户登录 | 通过 | 通过真实页面完成验证码登录并进入聊天页 |
|
||||||
| 用户端流式问答 | 通过 | 显示“思考中”,随后流式展示 Markdown 回答;发送后输入框立即清空 |
|
| 用户端流式问答 | 通过 | 显示“思考中”,随后流式展示 Markdown 回答;发送后输入框立即清空 |
|
||||||
| 用户端主题上下文 | 通过 | 连续消息均绑定同一主题,历史消息与主题上下文可继续使用 |
|
| 用户端主题上下文 | 通过 | 连续消息均绑定同一主题,历史消息与主题上下文可继续使用 |
|
||||||
|
| 用户端个人中心 | 通过 | 顶部统一入口展示账号、套餐说明、有效期、本月主题、今日问答、剩余额度和能力清单;近期回顾与生成记录分栏展示 |
|
||||||
| 后台 Agent 预览 | 通过 | 可选择模拟学员和具体主题,能够加载权益、主题摘要、最近消息和近期实修回顾 |
|
| 后台 Agent 预览 | 通过 | 可选择模拟学员和具体主题,能够加载权益、主题摘要、最近消息和近期实修回顾 |
|
||||||
| Agent 运行追踪 | 通过 | 能看到 `load_debug_user_context`、模型路由等追踪节点 |
|
| Agent 运行追踪 | 通过 | 能看到 `load_debug_user_context`、模型路由等追踪节点 |
|
||||||
| 主题沉淀 | 通过 | 结束主题快速入队;主题摘要与近期实修回顾后台生成;支持幂等、自动重试、重启恢复和管理员手动重试 |
|
| 主题沉淀 | 通过 | 结束主题快速入队;主题摘要与近期实修回顾后台生成;支持幂等、自动重试、重启恢复和管理员手动重试 |
|
||||||
|
|||||||
Reference in New Issue
Block a user