549 lines
30 KiB
Vue
549 lines
30 KiB
Vue
<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>
|