Files
QuestionProject/ai_knowledge_base_v2/apps/user-client/src/components/PersonalCenterDialog.vue

549 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import {
CalendarDays,
CheckCircle2,
ChevronDown,
CircleMinus,
Copy,
LifeBuoy,
LogOut,
PackageCheck,
RefreshCw,
Share2,
Sparkles,
Trash2,
} from "@lucide/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" | "reports" | "records";
type CardSection = "help" | "share";
const props = defineProps<{
user: UserProfile;
initialSection: CenterSection;
loading: boolean;
practiceReview: PracticeReviewResult | null;
reports: PeriodicReport[];
helpCards: TeacherHelpCard[];
shareDrafts: ShareDraft[];
operationPending: boolean;
}>();
const emit = defineEmits<{
close: [];
logout: [];
copyHelpCard: [card: TeacherHelpCard];
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);
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"));
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) },
]);
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(activeSection, (section) => emit("sectionChange", section), { 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)));
}
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;
}
function reportStatusLabel(status: string) {
return {
pending: "等待生成",
running: "正在生成",
success: "已完成",
failed: "生成失败",
empty: "暂无可整理内容",
}[status] || status;
}
function submitDeleteCard() {
if (!deletingCard.value || props.operationPending) return;
const target = deletingCard.value;
const done = (success: boolean) => {
if (success) deletingCard.value = null;
};
if (target.type === "help") {
emit("deleteHelpCard", target.id, done);
return;
}
emit("deleteShareDraft", target.id, done);
}
</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 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">
<aside v-if="entitlement?.lifecycleStatus === 'expired_fallback'" class="entitlement-notice expired">
<strong>权益已平稳切换</strong>
<span>{{ entitlement.previousPlanName || '专属权益' }}已于 {{ formatDate(entitlement.previousExpiredAt) }} 到期当前已自动切换为{{ entitlement.name }}已有聊天实修回顾和卡片记录仍会保留</span>
</aside>
<aside v-else-if="entitlement?.lifecycleStatus === 'expiring_7' || entitlement?.lifecycleStatus === 'expiring_30'" class="entitlement-notice">
<strong>权益即将到期</strong>
<span>当前权益有效至 {{ formatDate(entitlement.expiredAt) }}如需继续使用可提前联系运营老师确认续期</span>
</aside>
<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>{{ 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>
<span class="ai-generated-badge">AI生成</span>
</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>
</div>
<div v-else-if="activeSection === 'reports'" class="center-section reports-section">
<header class="card-history-head">
<div>
<h3>周期实修报告</h3>
<p>系统会根据已经沉淀的主题自动整理完整自然周和自然月的实修回顾</p>
</div>
<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" 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 }} · AI生成</small>
</summary>
<time>{{ formatDate(item.periodStart) }} {{ formatDate(item.periodEnd) }}</time>
<pre>{{ item.content }}</pre>
</details>
</template>
</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'"
@click="activeCardSection = 'help'"
>
<LifeBuoy :size="17" aria-hidden="true" />
<span>老师求助卡</span>
<small>{{ helpCards.length }}</small>
</button>
<button
v-if="canShareDrafts"
type="button"
role="tab"
:aria-selected="activeCardSection === 'share'"
@click="activeCardSection = 'share'"
>
<Share2 :size="17" aria-hidden="true" />
<span>班级分享稿</span>
<small>{{ shareDrafts.length }}</small>
</button>
</nav>
<section
v-if="activeCardSection === 'help'"
class="card-history-panel"
role="tabpanel"
aria-label="老师求助卡记录"
>
<header class="card-history-head">
<div>
<h3>老师求助卡</h3>
<p>需要老师确认方向时可回看已经整理过的求助信息</p>
</div>
<span> {{ helpCards.length }} </span>
</header>
<div v-if="helpCards.length" class="card-history-list">
<details v-for="item in helpCards" :key="`help-${item.id}`" class="card-history-item">
<summary>
<div>
<time>{{ formatDate(item.createdAt) }}</time>
<strong>{{ item.copied ? "已复制给老师" : "尚未复制" }} · AI生成</strong>
<p>{{ item.content.slice(0, 72) }}{{ item.content.length > 72 ? "…" : "" }}</p>
</div>
<ChevronDown :size="17" aria-hidden="true" />
</summary>
<div class="card-history-actions">
<button type="button" class="copy" @click="emit('copyHelpCard', item)">
<Copy :size="15" aria-hidden="true" />
{{ item.copied ? "再次复制" : "复制卡片" }}
</button>
<button type="button" class="delete" @click="deletingCard = { type: 'help', id: item.id }">
<Trash2 :size="15" aria-hidden="true" />
删除
</button>
</div>
<pre>{{ item.content }}</pre>
</details>
</div>
<p v-else-if="!loading" class="center-empty small">还没有生成过求助卡</p>
</section>
<section
v-else
class="card-history-panel"
role="tabpanel"
aria-label="班级分享稿记录"
>
<header class="card-history-head">
<div>
<h3>班级分享稿</h3>
<p>准备在班级群分享时可回看已经整理过的分享草稿</p>
</div>
<span> {{ shareDrafts.length }} </span>
</header>
<div v-if="shareDrafts.length" class="card-history-list">
<details v-for="item in shareDrafts" :key="`share-${item.id}`" class="card-history-item">
<summary>
<div>
<time>{{ formatDate(item.createdAt) }}</time>
<strong>{{ item.copied ? "已复制" : "尚未复制" }} · AI生成</strong>
<p>{{ item.content.slice(0, 72) }}{{ item.content.length > 72 ? "…" : "" }}</p>
</div>
<ChevronDown :size="17" aria-hidden="true" />
</summary>
<div class="card-history-actions">
<button type="button" class="copy" @click="emit('copyShareDraft', item)">
<Copy :size="15" aria-hidden="true" />
{{ item.copied ? "再次复制" : "复制卡片" }}
</button>
<button type="button" class="delete" @click="deletingCard = { type: 'share', id: item.id }">
<Trash2 :size="15" aria-hidden="true" />
删除
</button>
</div>
<pre>{{ item.content }}</pre>
</details>
</div>
<p v-else-if="!loading" 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>
<AppDialog
v-if="deletingCard"
title="删除卡片"
labelled-by="delete-card-title"
@close="deletingCard = null"
>
<p class="confirm-copy">
确定删除这张{{ deletingCard.type === "help" ? "老师求助卡" : "班级分享稿" }}删除后无法恢复
</p>
<template #footer>
<div class="dialog-actions">
<button type="button" class="dialog-secondary" :disabled="operationPending" @click="deletingCard = null">取消</button>
<button type="button" class="dialog-danger" :disabled="operationPending" @click="submitDeleteCard">
{{ operationPending ? "删除中..." : "删除" }}
</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: 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; }
.entitlement-notice { display: grid; gap: 4px; padding: 11px 13px; border: 1px solid #e7dcb9; border-radius: 12px; background: #fffaf0; color: #735f30; }
.entitlement-notice.expired { border-color: #e6d1cc; background: #fff7f5; color: #76504a; }
.entitlement-notice strong { font-size: 12px; }
.entitlement-notice span { font-size: 11px; line-height: 1.6; }
.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-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; }
.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; }
.card-type-tabs button[aria-selected="true"] { border-color: rgba(20, 148, 119, 0.42); background: var(--chat-brand-soft); color: var(--chat-brand-dark); box-shadow: 0 5px 16px rgba(27, 68, 55, 0.07); }
.card-type-tabs button span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-type-tabs button small { min-width: 23px; padding: 3px 6px; border-radius: 999px; background: #eef3f1; color: var(--chat-muted); font-size: 10px; text-align: center; }
.card-type-tabs button[aria-selected="true"] small { background: #dbeee7; color: var(--chat-brand-dark); }
.card-history-panel { min-width: 0; display: grid; gap: 10px; }
.card-history-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.card-history-head > div { min-width: 0; display: grid; gap: 3px; }
.card-history-head h3 { margin: 0; color: var(--chat-text); font-size: 14px; }
.card-history-head p { margin: 0; color: var(--chat-muted); font-size: 11px; line-height: 1.5; }
.card-history-head > span { flex: 0 0 auto; padding: 4px 7px; border-radius: 999px; background: #f1f5f3; color: var(--chat-muted); font-size: 10px; }
.card-history-list { display: grid; gap: 8px; }
.card-history-item { overflow: hidden; border: 1px solid var(--chat-border); border-radius: 13px; background: #fff; }
.card-history-item summary { min-height: 76px; display: grid; grid-template-columns: minmax(0, 1fr) 20px; align-items: center; gap: 8px; padding: 10px 11px; color: var(--chat-brand-dark); cursor: pointer; list-style: none; }
.card-history-item summary::-webkit-details-marker { display: none; }
.card-history-item summary > div { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 8px; }
.card-history-item time { color: var(--chat-weak); font-size: 10px; }
.card-history-item summary strong { color: var(--chat-brand-dark); font-size: 10px; font-weight: 700; }
.card-history-item summary p { grid-column: 1 / -1; display: -webkit-box; overflow: hidden; margin: 1px 0 0; color: var(--chat-muted); font-size: 12px; line-height: 1.55; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
.card-history-item summary > svg { transition: transform 0.18s ease; }
.card-history-item[open] summary > svg { transform: rotate(180deg); }
.card-history-item pre { max-height: 300px; overflow: auto; margin: 0; padding: 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; }
.card-history-actions { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; padding: 9px 10px 10px; border-top: 1px solid var(--chat-border); background: #fff; }
.card-history-actions button { min-height: 38px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 13px; border: 0; border-radius: 10px; font-size: 12px; font-weight: 700; }
.card-history-actions .copy { background: var(--chat-brand-soft); color: var(--chat-brand-dark); }
.card-history-actions .delete { background: #fff2f1; color: var(--chat-danger); }
.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>