From 20ed4875b87783de6076561976fb9a33778a1c2c Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Mon, 3 Aug 2026 12:38:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E4=B8=AA=E4=BA=BA=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/services/entitlement_service.py | 8 + .../backend/tests/test_entitlements_topics.py | 8 +- .../apps/user-client/src/App.vue | 96 +++--- .../user-client/src/components/ChatHeader.vue | 14 +- .../src/components/PersonalCenterDialog.vue | 273 ++++++++++++++++++ .../apps/user-client/src/styles.css | 116 +------- .../apps/user-client/src/types/api.ts | 2 + .../deployment/RELEASE_CHECKLIST.md | 2 + .../docs/qianwen_product_todo.md | 11 +- .../docs/一期全链路验收记录.md | 1 + 10 files changed, 355 insertions(+), 176 deletions(-) create mode 100644 ai_knowledge_base_v2/apps/user-client/src/components/PersonalCenterDialog.vue diff --git a/ai_knowledge_base_v2/apps/backend/app/services/entitlement_service.py b/ai_knowledge_base_v2/apps/backend/app/services/entitlement_service.py index a11cbf2..a299d2d 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/entitlement_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/entitlement_service.py @@ -20,6 +20,8 @@ class EntitlementView: plan_id: int | None name: str plan_type: str + description: str | None + validity_days: int | None monthly_topic_limit: int | None monthly_topic_used: int enable_growth_profile: bool @@ -96,6 +98,8 @@ class EntitlementService: plan_id=None, name="旧版每日额度", plan_type="legacy", + description="按每日问答额度提供基础服务。", + validity_days=None, monthly_topic_limit=None, monthly_topic_used=monthly_topic_used, enable_growth_profile=False, @@ -200,6 +204,8 @@ def entitlement_dict(view: EntitlementView) -> dict: "planId": view.plan_id, "name": view.name, "planType": view.plan_type, + "description": view.description, + "validityDays": view.validity_days, "monthlyTopicLimit": view.monthly_topic_limit, "monthlyTopicUsed": view.monthly_topic_used, "monthlyTopicRemaining": view.monthly_topic_remaining, @@ -238,6 +244,8 @@ def view_from_plan( plan_id=plan.id, name=plan.name, plan_type=plan.plan_type, + description=plan.description, + validity_days=plan.validity_days, monthly_topic_limit=plan.monthly_topic_limit, monthly_topic_used=monthly_topic_used, enable_growth_profile=bool(plan.enable_growth_profile), diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_entitlements_topics.py b/ai_knowledge_base_v2/apps/backend/tests/test_entitlements_topics.py index e27c71b..756576e 100644 --- a/ai_knowledge_base_v2/apps/backend/tests/test_entitlements_topics.py +++ b/ai_knowledge_base_v2/apps/backend/tests/test_entitlements_topics.py @@ -13,7 +13,7 @@ from app.models.chat import ChatMessage, ChatSession, TopicSession from app.models.entitlement import EntitlementPlan from app.models.user import User 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.topic_session_service import TopicSessionService @@ -44,6 +44,8 @@ def test_default_entitlement_uses_basic_plan_when_user_has_no_assignment(): id=10, name="大本营基础版", plan_type="basic", + description="基础知识问答权益", + validity_days=180, monthly_topic_limit=30, status=1, 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.name == "大本营基础版" + assert view.description == "基础知识问答权益" + assert view.validity_days == 180 assert view.source == "default" 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(): diff --git a/ai_knowledge_base_v2/apps/user-client/src/App.vue b/ai_knowledge_base_v2/apps/user-client/src/App.vue index 0909185..18901b3 100644 --- a/ai_knowledge_base_v2/apps/user-client/src/App.vue +++ b/ai_knowledge_base_v2/apps/user-client/src/App.vue @@ -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(null); @@ -33,7 +35,7 @@ const shareDraftContent = ref(""); const helpCardHistory = ref([]); const shareDraftHistory = ref([]); const reportHistory = ref([]); -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) {
正在启动...
diff --git a/ai_knowledge_base_v2/apps/user-client/src/components/PersonalCenterDialog.vue b/ai_knowledge_base_v2/apps/user-client/src/components/PersonalCenterDialog.vue new file mode 100644 index 0000000..8f9e122 --- /dev/null +++ b/ai_knowledge_base_v2/apps/user-client/src/components/PersonalCenterDialog.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/ai_knowledge_base_v2/apps/user-client/src/styles.css b/ai_knowledge_base_v2/apps/user-client/src/styles.css index dce2818..76639a8 100644 --- a/ai_knowledge_base_v2/apps/user-client/src/styles.css +++ b/ai_knowledge_base_v2/apps/user-client/src/styles.css @@ -1131,7 +1131,7 @@ textarea:focus-visible { z-index: 5; min-height: 82px; display: grid; - grid-template-columns: 44px minmax(0, 1fr) auto 52px; + grid-template-columns: 44px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: max(10px, env(safe-area-inset-top)) 14px 10px; @@ -1140,7 +1140,7 @@ textarea:focus-visible { } .header-menu-button, -.header-logout-button, +.header-profile-button, .history-header button { min-width: 44px; height: 44px; @@ -1177,28 +1177,21 @@ textarea:focus-visible { white-space: nowrap; } -.user-summary { - max-width: 92px; - display: grid; - gap: 2px; - color: var(--chat-muted); - font-size: 10px; - line-height: 1.45; +.header-profile-button { + max-width: 120px; + grid-template-columns: 18px minmax(0, auto); + gap: 6px; + padding: 0 11px; + font-size: 12px; + font-weight: 650; } -.user-summary span { +.header-profile-button span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.header-logout-button { - min-width: 52px; - padding: 0 8px; - font-size: 13px; - font-weight: 600; -} - .session-quota { min-height: 46px; display: flex; @@ -1711,27 +1704,6 @@ textarea:focus-visible { 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 { display: grid; gap: 12px; @@ -1765,64 +1737,6 @@ textarea:focus-visible { 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 { display: grid; grid-template-columns: 1fr 1fr; @@ -1867,7 +1781,7 @@ body.drawer-open { overflow: hidden; } @media (max-width: 390px) { .chat-header { - grid-template-columns: 44px minmax(0, 1fr) auto 48px; + grid-template-columns: 44px minmax(0, 1fr) auto; gap: 7px; padding-right: 10px; padding-left: 10px; @@ -1875,8 +1789,7 @@ body.drawer-open { overflow: hidden; } .assistant-summary h1 { font-size: 15px; } .assistant-summary p { font-size: 10px; } - .user-summary { max-width: 72px; font-size: 9px; } - .header-logout-button { min-width: 48px; padding: 0 5px; } + .header-profile-button { max-width: 94px; padding: 0 8px; } .session-quota { margin-right: 12px; margin-left: 12px; } .session-quota-actions { gap: 5px; } .quota-link { padding: 0 6px; font-size: 11px; } @@ -1887,8 +1800,9 @@ body.drawer-open { overflow: hidden; } } @media (max-width: 350px) { - .user-summary { display: none; } - .chat-header { grid-template-columns: 44px minmax(0, 1fr) 48px; } + .header-profile-button { width: 44px; grid-template-columns: 1fr; padding: 0; } + .header-profile-button span { display: none; } + .chat-header { grid-template-columns: 44px minmax(0, 1fr) 44px; } } @media (max-height: 680px) { diff --git a/ai_knowledge_base_v2/apps/user-client/src/types/api.ts b/ai_knowledge_base_v2/apps/user-client/src/types/api.ts index 2cc2658..939d834 100644 --- a/ai_knowledge_base_v2/apps/user-client/src/types/api.ts +++ b/ai_knowledge_base_v2/apps/user-client/src/types/api.ts @@ -19,6 +19,8 @@ export interface UserEntitlementSummary { planId: number | null; name: string; planType: string; + description?: string | null; + validityDays?: number | null; monthlyTopicLimit: number | null; monthlyTopicUsed: number; monthlyTopicRemaining: number | null; diff --git a/ai_knowledge_base_v2/deployment/RELEASE_CHECKLIST.md b/ai_knowledge_base_v2/deployment/RELEASE_CHECKLIST.md index d6140c4..0c059e7 100644 --- a/ai_knowledge_base_v2/deployment/RELEASE_CHECKLIST.md +++ b/ai_knowledge_base_v2/deployment/RELEASE_CHECKLIST.md @@ -46,6 +46,8 @@ - [ ] 用户端可以创建会话。 - [ ] 用户端可以提问并收到回答。 - [ ] 用户发送后输入框立即清空,思考状态与流式 Markdown 回答显示正常。 +- [ ] 用户端个人中心可查看账号、当前套餐、有效期、主题/问答用量和能力清单。 +- [ ] 深度版可查看近期与周期实修回顾;基础版明确显示权益边界,不展示成长评分或旧画像字段。 - [ ] 连续提问能够继续使用同一主题的上下文。 - [ ] 结束主题接口快速返回,用户可以立即新建主题或继续使用系统。 - [ ] 无命中问题返回固定兜底。 diff --git a/ai_knowledge_base_v2/docs/qianwen_product_todo.md b/ai_knowledge_base_v2/docs/qianwen_product_todo.md index 98cd475..bb41c9c 100644 --- a/ai_knowledge_base_v2/docs/qianwen_product_todo.md +++ b/ai_knowledge_base_v2/docs/qianwen_product_todo.md @@ -642,19 +642,23 @@ AI 日志增加: --- -### 10. 用户端“近期实修回顾” +### 10. 用户端个人中心与“近期实修回顾” - 优先级:P1 - 改动大小:M -- 影响范围:用户端、近期回顾接口、主题历史 +- 影响范围:用户端、用户权益接口、近期回顾接口、主题历史 - 依赖:主题会话机制、近期实修回顾 #### 目标 -帮助用户看见最近谈到的内容并回到当下,不形成成长成绩单或长期自我标签。 +为用户提供统一的账号、套餐、用量和权益入口;同时帮助用户看见最近谈到的内容并回到当下,不形成成长成绩单或长期自我标签。 #### 用户端建议展示 +- 账号与当前套餐; +- 套餐说明、生效时间和有效期; +- 本月主题、今日问答用量和剩余额度; +- 当前套餐包含的能力; - 最近主题; - 主题摘要; - 每周小结; @@ -676,6 +680,7 @@ AI 日志增加: - 2026-07-31:用户端原“我的档案”弹窗已扩展为实修记录视图,可查看实修回顾、最近主题沉淀、最近老师求助卡和最近班级分享稿;仍保持“我的实修记录”表达,不做后台画像式展示。 - 2026-08-03:升级为 V2“近期实修回顾”。移除常见情绪、身体/关系模式、做过或有效功课、近期变化等字段;旧版数据不再展示或注入 Agent,下一次主题沉淀时按最近30天、最多10个新版主题懒重建。 +- 2026-08-03:新增用户端“个人中心”,顶部头像入口统一展示账号、当前套餐、有效期、本月主题和今日问答用量、剩余额度及能力清单;近期回顾、周期回顾、求助卡和分享稿按栏目查看。基础版会明确提示未包含的能力,不做成长评分、打卡天数或结果排名。 --- diff --git a/ai_knowledge_base_v2/docs/一期全链路验收记录.md b/ai_knowledge_base_v2/docs/一期全链路验收记录.md index df9de6d..3b464c4 100644 --- a/ai_knowledge_base_v2/docs/一期全链路验收记录.md +++ b/ai_knowledge_base_v2/docs/一期全链路验收记录.md @@ -24,6 +24,7 @@ | 用户登录 | 通过 | 通过真实页面完成验证码登录并进入聊天页 | | 用户端流式问答 | 通过 | 显示“思考中”,随后流式展示 Markdown 回答;发送后输入框立即清空 | | 用户端主题上下文 | 通过 | 连续消息均绑定同一主题,历史消息与主题上下文可继续使用 | +| 用户端个人中心 | 通过 | 顶部统一入口展示账号、套餐说明、有效期、本月主题、今日问答、剩余额度和能力清单;近期回顾与生成记录分栏展示 | | 后台 Agent 预览 | 通过 | 可选择模拟学员和具体主题,能够加载权益、主题摘要、最近消息和近期实修回顾 | | Agent 运行追踪 | 通过 | 能看到 `load_debug_user_context`、模型路由等追踪节点 | | 主题沉淀 | 通过 | 结束主题快速入队;主题摘要与近期实修回顾后台生成;支持幂等、自动重试、重启恢复和管理员手动重试 |