refactor(frontend): remove generic AI-slop styling

This commit is contained in:
Jesse_Chen
2026-08-13 11:05:00 +08:00
parent f6a57db97e
commit cf7598b337
24 changed files with 377 additions and 1114 deletions
File diff suppressed because it is too large Load Diff
+4 -15
View File
@@ -294,7 +294,7 @@ function MembershipContent() {
<div className="membership-hero-copy">
<p className="membership-greeting">{account?.user.email ? `账户:${account.user.email}` : "欢迎回来"}</p>
<h2 id="membership-hero-title">
{activeSubscription ? "保持会员状态,持续获得完整权益" : "开通会员,解锁完整咨询体验"}
{activeSubscription ? "查看当前会员权益" : "选择会员套餐"}
</h2>
<div className="membership-summary">
<article className="membership-summary-card">
@@ -310,14 +310,6 @@ function MembershipContent() {
</article>
</div>
</div>
<div className="membership-celestial-visual" aria-hidden="true">
<span className="membership-orbit membership-orbit-outer" />
<span className="membership-orbit membership-orbit-inner" />
<span className="membership-orbit membership-orbit-mini" />
<span className="membership-orbit-dot membership-orbit-dot-primary" />
<span className="membership-orbit-dot membership-orbit-dot-secondary" />
<span className="membership-celestial-mark" />
</div>
</section>
<section className="membership-segment" aria-label="套餐与点数">
@@ -367,13 +359,10 @@ function MembershipContent() {
>
<header className="membership-card-header">
<div>
<span className="membership-card-kicker">{planDisplayLabel(alias ?? "monthly")}</span>
<h3>{product.name}</h3>
<p className="membership-card-meta">{planDisplayLabel(alias ?? "monthly")}</p>
</div>
<div className="membership-card-badges">
{recommended && <span className="membership-badge membership-badge--recommended">推荐</span>}
{isCurrent && <span className="membership-badge membership-badge--current">当前套餐</span>}
</div>
{(isCurrent || recommended) && <span className={`membership-status${isCurrent ? " is-current" : ""}`}>{isCurrent ? "当前套餐" : "推荐"}</span>}
</header>
<p className="membership-card-price">
{formatPrice(product.priceCents, product.currency)}
@@ -418,8 +407,8 @@ function MembershipContent() {
{creditPacks.map((product) => (
<article className="membership-credit-card" key={product.id}>
<div className="membership-credit-copy">
<span className="membership-card-kicker">{product.credits > 0 ? `${product.credits} 点` : "点数包"}</span>
<h3>{product.name}</h3>
<p className="membership-card-meta">{product.credits > 0 ? `${product.credits} 点` : "点数包"}</p>
<p>{product.description || "按需补充咨询点数"}</p>
</div>
<p className="membership-card-price">{formatPrice(product.priceCents, product.currency)}</p>
+4 -52
View File
@@ -3,8 +3,6 @@
import Link from "next/link";
import dynamic from "next/dynamic";
import { ArrowUp, ArrowUpRight, Sparkles, Square, X } from "lucide-react";
import { useGSAP } from "@gsap/react";
import { gsap } from "gsap";
import { useEffect, useRef, useState } from "react";
import type { FormEvent, KeyboardEvent } from "react";
import { AppSidebar } from "@/components/app-sidebar";
@@ -114,8 +112,6 @@ import {
} from "@/lib/public-models";
import { selfHostedOtpActions } from "@/modules/identity/client";
gsap.registerPlugin(useGSAP);
const BirthTimeRectification = dynamic(
() => import("@/components/birth-time-rectification").then((module) => module.BirthTimeRectification),
{
@@ -1061,7 +1057,6 @@ export default function Home() {
const closeButton = useRef<HTMLButtonElement>(null);
const onboardingPaywallShown = useRef(false);
const composerInput = useRef<HTMLTextAreaElement>(null);
const starterWorkbench = useRef<HTMLDivElement>(null);
const pendingConsultation = useRef<PendingConsultation | null>(null);
const cancellationRequests = useRef(new Map<string, Promise<void>>());
const cancellationFeedbackRequest = useRef<string | null>(null);
@@ -3125,39 +3120,6 @@ export default function Home() {
}
useGSAP(() => {
if (!starterHomeVisible || !starterWorkbench.current) return;
const motion = gsap.matchMedia();
motion.add("(prefers-reduced-motion: no-preference)", () => {
const timeline = gsap.timeline({ defaults: { ease: "power3.out" } });
timeline
.from(".starter-hero-copy > *", {
autoAlpha: 0,
duration: 0.62,
stagger: 0.07,
y: 18,
})
.from(".starter-celestial-visual", {
autoAlpha: 0,
duration: 0.76,
scale: 0.94,
}, 0.08)
.from(".product-entrypoint-card", {
autoAlpha: 0,
duration: 0.54,
stagger: 0.08,
y: 20,
}, 0.2)
.from(".starter-theme-card", {
autoAlpha: 0,
duration: 0.46,
stagger: 0.055,
y: 14,
}, 0.34);
});
return () => motion.revert();
}, { dependencies: [activeSession?.id, starterHomeVisible], scope: starterWorkbench });
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!profileComplete) {
@@ -3178,7 +3140,7 @@ export default function Home() {
if (!hydrated || (!account && !accountError)) {
return (
<main className="app-loading" aria-busy="true" aria-live="polite">
<AppLoadingIndicator title="正在和星星对口供" detail="顺便同步你的账户与对话记录" />
<AppLoadingIndicator title="正在载入账户" detail="同步个人资料与对话记录" />
</main>
);
}
@@ -3315,7 +3277,6 @@ export default function Home() {
<form className="profile-form onboarding-card onboarding-step-card birth-time-transition-card" onSubmit={saveOnboardingBirth} aria-busy={birthTimeAssessmentPhase !== null}>
<div className="onboarding-card-heading">
<div className="onboarding-card-heading-copy">
<span className="onboarding-card-kicker">先完成出生资料</span>
<b>出生日期与时间</b>
</div>
</div>
@@ -3372,10 +3333,10 @@ export default function Home() {
{profileComplete && presetMessageFinished && !rectificationSurfaceOpen && (onboardingPending ? (
<div className="starter-loading" role="status">
<AppLoadingIndicator title="正在准备三个入门问题…" detail="我会根据你的资料,整理适合开始的方向。" />
<AppLoadingIndicator title="正在准备问题" detail="根据你的资料整理三个起点。" />
</div>
) : (
<div className="starter-list starter-workbench" ref={starterWorkbench} aria-label="Jyotisha 推荐的初始问题">
<div className="starter-list starter-workbench" aria-label="Jyotisha 推荐的初始问题">
<section className="starter-hero" aria-labelledby="starter-heading">
<div className="starter-hero-copy">
<p className="starter-greeting">{daypartGreeting},{profile.name.trim()}。</p>
@@ -3384,13 +3345,6 @@ export default function Home() {
? "从此刻最在意的事开始,我会结合你的星盘证据,帮你把问题拆得更清楚。"
: "你可以先了解一般占星知识;完成生时校正后,再讨论个人星盘结论。"}</p>
</div>
<div className="starter-celestial-visual" aria-hidden="true">
<span className="starter-orbit starter-orbit-outer" />
<span className="starter-orbit starter-orbit-inner" />
<span className="starter-orbit-dot starter-orbit-dot-primary" />
<span className="starter-orbit-dot starter-orbit-dot-secondary" />
<span className="starter-celestial-mark" />
</div>
</section>
<section className="product-entrypoints" aria-label="常用占星入口">
@@ -3403,7 +3357,6 @@ export default function Home() {
onClick={draftDailyStarlanguageQuestion}
/>
<div className="product-entrypoint-copy">
<span className="product-entrypoint-kicker">{personalChartAvailable ? "每日观察" : "一般知识"}</span>
<h2 id="daily-starlanguage-title">{personalChartAvailable ? "今日星语" : "如何看每日趋势"}</h2>
<p>{personalChartAvailable ? dailyStarlanguage?.trend : "了解印度占星通常会用哪些因素观察一天的主题。"}</p>
</div>
@@ -3421,7 +3374,6 @@ export default function Home() {
onClick={() => void openRectificationFromHomepage()}
/>
<div className="product-entrypoint-copy">
<span className="product-entrypoint-kicker">出生资料</span>
<h2 id="birth-rectification-title">生时校正</h2>
<p>{rectificationEntrySummary?.hasResumableCase
? "新建一段独立校正;未完成的记录仍可从左侧历史会话继续。"
@@ -3438,7 +3390,7 @@ export default function Home() {
<section className="starter-themes" aria-labelledby="starter-themes-heading">
<div className="starter-section-heading">
<h2 id="starter-themes-heading">或者,从一个主题开始</h2>
<h2 id="starter-themes-heading">从一个主题开始</h2>
<p>选择最接近你当下的问题。</p>
</div>
<div className="starter-theme-accordion">
@@ -11,7 +11,7 @@ export default function ReportNotFound() {
<p className="max-w-md text-sm text-ink-secondary">
该报告不存在、已删除,或不属于当前账号。
</p>
<Button render={<Link href="/" />} variant="outline">
<Button render={<Link href="/" />} nativeButton={false} variant="outline">
返回对话
</Button>
</main>
+61 -1
View File
@@ -31,7 +31,67 @@ export function AdminApp({ children }: { children: ReactNode }) {
const notificationProvider = useNotificationProvider();
return (
<div className="admin-app-shell">
<ConfigProvider componentSize="large" theme={{ algorithm: theme.darkAlgorithm, token: { colorPrimary: "#c8a96b" } }}>
<ConfigProvider
componentSize="middle"
theme={{
algorithm: theme.defaultAlgorithm,
token: {
colorPrimary: "#85432f",
colorInfo: "#85432f",
colorSuccess: "#28633e",
colorWarning: "#8a641f",
colorError: "#9a2f2f",
colorBgBase: "#fbfaf7",
colorBgContainer: "#fbfaf7",
colorBgLayout: "#f3f2ee",
colorBorder: "#d8d6cf",
colorText: "#1d1d1f",
colorTextSecondary: "#5f5f59",
borderRadius: 6,
borderRadiusLG: 8,
boxShadowSecondary: "0 1px 2px rgba(29, 29, 31, .06)",
boxShadowTertiary: "none",
fontFamily: "StyreneB, Inter, system-ui, sans-serif",
},
components: {
Layout: {
bodyBg: "#f3f2ee",
siderBg: "#ebe9e3",
lightSiderBg: "#ebe9e3",
headerBg: "#fbfaf7",
triggerBg: "#ebe9e3",
lightTriggerBg: "#ebe9e3",
},
Menu: {
itemBg: "transparent",
subMenuItemBg: "transparent",
itemSelectedBg: "rgba(255, 255, 255, .72)",
itemSelectedColor: "#1d1d1f",
itemHoverBg: "rgba(255, 255, 255, .46)",
itemBorderRadius: 6,
subMenuItemBorderRadius: 6,
},
Card: {
headerBg: "transparent",
headerFontSize: 16,
headerHeight: 48,
bodyPadding: 18,
headerPadding: 18,
},
Table: {
headerBg: "#ebe9e3",
headerColor: "#5f5f59",
rowHoverBg: "#f3f2ee",
cellPaddingBlock: 10,
cellPaddingInline: 12,
},
Tag: {
defaultBg: "#f3f2ee",
defaultColor: "#5f5f59",
},
},
}}
>
<AntdApp>
<Refine
routerProvider={routerProvider}
@@ -97,7 +97,7 @@ export default function AdministratorsResource() {
return (
<Card
title="管理员"
extra={canManage ? <Button type="primary" icon={<PlusOutlined />} onClick={() => openAssignment()}>按邮箱分配角色</Button> : <Tag color="gold">RBAC</Tag>}
extra={canManage ? <Button type="primary" icon={<PlusOutlined />} onClick={() => openAssignment()}>按邮箱分配角色</Button> : <Typography.Text type="secondary">只读权限</Typography.Text>}
>
<Typography.Paragraph type="secondary">
六类系统角色可在此分配和撤销。每次变更都需要邮箱验证码与操作原因;最后一位 Owner 受服务端保护,不能被撤销。
@@ -1,6 +1,6 @@
"use client";
import { Descriptions, Tag, type TableColumnsType } from "antd";
import { Descriptions, type TableColumnsType } from "antd";
import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table";
@@ -18,7 +18,7 @@ type AuditRecord = {
const columns: TableColumnsType<AuditRecord> = [
{ title: "操作者", dataIndex: "actorEmail", sorter: true },
{ title: "角色", dataIndex: "actorRole", render: (value) => <Tag>{value}</Tag> },
{ title: "角色", dataIndex: "actorRole" },
{ title: "动作", dataIndex: "action", sorter: true },
{ title: "目标 ID", dataIndex: "targetId" },
{ title: "Request ID", dataIndex: "requestId" },
@@ -470,9 +470,7 @@ export function UsageResource() {
{
title: "资金来源",
dataIndex: "source",
render: (value) => (
<Tag color={value === "subscription" ? "green" : "blue"}>{value}</Tag>
),
render: (value) => value,
},
{
title: "模型",
@@ -541,9 +539,7 @@ export function ModelReleasesResource() {
{
title: "动作",
dataIndex: "action",
render: (value) => (
<Tag color={value === "publish" ? "green" : "gold"}>{value}</Tag>
),
render: (value) => value,
},
{
title: "管理员",
@@ -1,6 +1,6 @@
"use client";
import { Tag, type TableColumnsType } from "antd";
import { type TableColumnsType } from "antd";
import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table";
@@ -19,7 +19,7 @@ type TransactionRecord = {
const columns: TableColumnsType<TransactionRecord> = [
{ title: "用户", dataIndex: "email", render: (value) => value || "—" },
{ title: "类型", dataIndex: "type", sorter: true, render: (value) => <Tag>{value}</Tag> },
{ title: "类型", dataIndex: "type", sorter: true },
{ title: "变动", dataIndex: "amount", sorter: true, render: (value) => value > 0 ? `+${value}` : value },
{ title: "余额", dataIndex: "balanceAfter", sorter: true },
{ title: "请求 ID", dataIndex: "requestId" },
@@ -4,7 +4,7 @@ import { PlusOutlined } from "@ant-design/icons";
import { useGetIdentity } from "@refinedev/core";
import { useTable } from "@refinedev/antd";
import { List } from "@refinedev/antd";
import { App, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Table, Tag, Typography, type TableColumnsType } from "antd";
import { App, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Table, Typography, type TableColumnsType } from "antd";
import { useState } from "react";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
@@ -122,7 +122,7 @@ export default function FeatureFlagsManagement() {
const columns: TableColumnsType<FeatureFlag> = [
{ title: "开关", render: (_, item) => <Space direction="vertical" size={0}><Text strong>{item.flagKey}</Text><Text type="secondary">v{item.version}</Text></Space> },
{ title: "状态", render: (_, item) => <Space><Tag color={item.status === "published" ? "green" : "gold"}>{item.status}</Tag>{item.enabled ? <Tag color="blue">开启</Tag> : <Tag>关闭</Tag>}</Space> },
{ title: "状态", render: (_, item) => <Text>{item.status === "published" ? "已发布" : item.status === "draft" ? "草稿" : item.status} · {item.enabled ? "已开启" : "已关闭"}</Text> },
{ title: "灰度", dataIndex: "rolloutPercentage", render: (value) => `${value}%` },
{ title: "配置", dataIndex: "config", render: (value) => <Text code>{JSON.stringify(value)}</Text> },
{ title: "发布时间", dataIndex: "publishedAt", render: formatAdminDate },
+18 -19
View File
@@ -12,7 +12,6 @@ import {
Radio,
Space,
Spin,
Tag,
Typography,
} from "antd";
import { useEffect, useState } from "react";
@@ -97,7 +96,7 @@ export default function MfaSecurity() {
if (!data) return;
setEnrollment(data);
setBackupCodes(data.backupCodes);
setNotice("请先保存恢复码,再使用认证器生成的 6 位验证码完成 enrollment。未验证前 MFA 不会启用。");
setNotice("请先保存恢复码,再使用认证器生成的 6 位验证码完成 MFA 绑定。未验证前 MFA 不会启用。");
}
async function verify({ code }: { code: string }) {
@@ -105,14 +104,14 @@ export default function MfaSecurity() {
if (!data) return;
setStatus(data);
setEnrollment(undefined);
setNotice("当前管理员 session 已完成真实第二因素验证。高风险操作仍需随后完成权限范围内的邮箱验证码。");
setNotice("当前管理员会话已完成真实第二因素验证。高风险操作仍需随后完成权限范围内的邮箱验证码。");
}
async function recover({ code }: { code: string }) {
const data = await run("recover", () => mfaRequest<MfaStatus>({ action: "recover", code }));
if (!data) return;
setStatus(data);
setNotice("恢复码已消费,当前 session 已完成 MFA 验证。请在恢复访问后重新生成恢复码。");
setNotice("恢复码已消费,当前会话已完成 MFA 验证。请在恢复访问后重新生成恢复码。");
}
async function regenerate({ password }: { password: string }) {
@@ -132,7 +131,7 @@ export default function MfaSecurity() {
setEnrollment(undefined);
setBackupCodes(undefined);
setNotice(data.required
? "MFA 已禁用。该角色要求 MFA,因此高风险写入现已关闭,重新 enrollment 后才能恢复。"
? "MFA 已禁用。该角色要求 MFA,因此高风险写入现已关闭,重新绑定后才能恢复。"
: "MFA 已禁用,当前 MFA 与邮箱重认证证明均已撤销。");
}
@@ -144,7 +143,7 @@ export default function MfaSecurity() {
<div>
<Title level={2}>安全验证</Title>
<Paragraph type="secondary">
管理员 MFA 使用 Better Auth 的真实 TOTP 与一次性恢复码。MFA 证明只绑定当前 server session,短时有效且可随 session 撤销。
管理员 MFA 使用 Better Auth 的真实 TOTP 与一次性恢复码。MFA 证明只绑定当前服务端会话,短时有效且可随会话撤销。
</Paragraph>
</div>
@@ -154,24 +153,24 @@ export default function MfaSecurity() {
type="warning"
showIcon
message="当前角色强制要求 MFA"
description="在 enrollment 完成前,所有高风险写入都会 fail closed。"
description="在 MFA 绑定完成前,所有高风险写入均默认拒绝。"
/> : null}
<Card title="当前状态">
<Descriptions column={{ xs: 1, sm: 2 }}>
<Descriptions.Item label="角色要求">
<Tag color={status?.required ? "red" : "default"}>{status?.required ? "必须 MFA" : "可选 MFA"}</Tag>
<Text type={status?.required ? "danger" : "secondary"}>{status?.required ? "必须 MFA" : "可选 MFA"}</Text>
</Descriptions.Item>
<Descriptions.Item label="Enrollment">
<Tag color={status?.enrolled ? "green" : "orange"}>{status?.enrolled ? "已启用" : "未启用"}</Tag>
<Descriptions.Item label="MFA 绑定">
<Text type={status?.enrolled ? undefined : "warning"}>{status?.enrolled ? "已启用" : "未启用"}</Text>
</Descriptions.Item>
<Descriptions.Item label="当前 session">
<Tag color={status?.verified ? "green" : "default"}>{status?.verified ? "已验证" : "未验证"}</Tag>
<Descriptions.Item label="当前会话">
<Text type={status?.verified ? undefined : "secondary"}>{status?.verified ? "已验证" : "未验证"}</Text>
</Descriptions.Item>
<Descriptions.Item label="高风险写入前置能力">
<Tag color={status?.highRiskWritesEnabled ? "green" : "red"}>
<Text type={status?.highRiskWritesEnabled ? undefined : "danger"}>
{status?.highRiskWritesEnabled ? "可发起邮箱重认证" : "已关闭"}
</Tag>
</Text>
</Descriptions.Item>
</Descriptions>
</Card>
@@ -182,11 +181,11 @@ export default function MfaSecurity() {
<Form.Item name="password" label="当前密码" rules={[{ required: true, message: "请输入当前密码" }]}>
<Input.Password autoComplete="current-password" maxLength={128} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={action === "enroll"}>开始 enrollment</Button>
<Button type="primary" htmlType="submit" loading={action === "enroll"}>开始绑定</Button>
</Form>
</Card> : null}
{enrollment ? <Card title="完成 enrollment">
{enrollment ? <Card title="完成 MFA 绑定">
<Alert
type="info"
showIcon
@@ -208,7 +207,7 @@ export default function MfaSecurity() {
</Form>
</Card> : null}
{status?.enrolled && !status.verified ? <Card title="验证当前 session">
{status?.enrolled && !status.verified ? <Card title="验证当前会话">
<Paragraph>高权限操作前,先用认证器验证码或一次性恢复码完成真实第二因素。</Paragraph>
<Radio.Group value={factor} onChange={(event) => setFactor(event.target.value as Factor)}>
<Radio.Button value="totp">认证器验证码</Radio.Button>
@@ -234,7 +233,7 @@ export default function MfaSecurity() {
/>
</Form.Item>
<Button type="primary" htmlType="submit" loading={action === "verify" || action === "recover"}>
验证当前 session
验证当前会话
</Button>
</Form>
</Card> : null}
@@ -255,7 +254,7 @@ export default function MfaSecurity() {
<Divider />
<div>
<Title level={4}>禁用 MFA</Title>
<Paragraph type="secondary">禁用会轮换 Better Auth session,并撤销当前 MFA 与邮箱重认证证明。</Paragraph>
<Paragraph type="secondary">禁用会轮换 Better Auth 会话,并撤销当前 MFA 与邮箱重认证证明。</Paragraph>
<Form layout="inline" onFinish={disable}>
<Form.Item name="password" rules={[{ required: true, message: "请输入当前密码" }]}>
<Input.Password placeholder="当前密码" autoComplete="current-password" maxLength={128} />
@@ -17,7 +17,6 @@ import {
Space,
Switch,
Table,
Tag,
Typography,
type TableColumnsType,
} from "antd";
@@ -298,8 +297,8 @@ export default function ModelManagement() {
{ title: "供应商", render: (_, item) => <Space direction="vertical" size={0}><Text strong>{item.name}</Text><Text type="secondary">{item.code}</Text></Space> },
{ title: "类型", dataIndex: "providerType", render: (value: ProviderType) => providerTypeLabels[value] },
{ title: "地址", dataIndex: "baseUrl", render: (value, item) => value ?? (item.providerType === "anthropic" ? "Anthropic 官方" : "OpenAI 官方") },
{ title: "密钥状态", render: (_, item) => <Tag color={item.secretConfigured ? "green" : "red"}>{item.secretConfigured ? "已配置" : "未配置"}</Tag> },
{ title: "状态", dataIndex: "enabled", render: (value) => value ? <Tag color="green">启用</Tag> : <Tag>停用</Tag> },
{ title: "密钥状态", render: (_, item) => <Text type={item.secretConfigured ? undefined : "danger"}>{item.secretConfigured ? "已配置" : "未配置"}</Text> },
{ title: "状态", dataIndex: "enabled", render: (value) => <Text type={value ? undefined : "secondary"}>{value ? "启用" : "停用"}</Text> },
{ title: "更新时间", dataIndex: "updatedAt", render: formatAdminDate },
{ title: "操作", render: (_, item) => canWrite ? <Space><Button type="link" onClick={() => openProvider(item)}>编辑</Button><Button type="link" onClick={() => openModel(undefined, item.id)}>添加模型</Button></Space> : null },
];
@@ -307,11 +306,11 @@ export default function ModelManagement() {
const modelColumns: TableColumnsType<ModelVersion> = [
{ title: "模型", render: (_, item) => <Space direction="vertical" size={0}><Text strong>{item.label}</Text><Text type="secondary">{item.modelId} · v{item.version}</Text></Space> },
{ title: "供应商模型", render: (_, item) => `${item.providerCode} / ${item.providerModel}` },
{ title: "档位", dataIndex: "modelTier", render: (value: ModelVersion["modelTier"]) => <Tag>{modelTierLabels[value]}</Tag> },
{ title: "档位", dataIndex: "modelTier", render: (value: ModelVersion["modelTier"]) => modelTierLabels[value] },
{ title: "点数", dataIndex: "creditCost" },
{ title: "成本/百万 Token", render: (_, item) => `$${(item.inputCostMicrousdPerMillion / 1_000_000).toFixed(4)} / $${(item.outputCostMicrousdPerMillion / 1_000_000).toFixed(4)}` },
{ title: "路由", render: (_, item) => <Space direction="vertical" size={0}>{item.isDefault && <Tag color="blue">默认</Tag>}<Text type="secondary">回退模型:{item.fallbackModelId ?? "—"}</Text></Space> },
{ title: "状态", render: (_, item) => <Space><Tag color={item.status === "published" ? "green" : "gold"}>{modelStatusLabels[item.status] ?? item.status}</Tag>{item.enabled ? <Tag color="blue">启用</Tag> : <Tag>停用</Tag>}</Space> },
{ title: "路由", render: (_, item) => <Space direction="vertical" size={0}>{item.isDefault && <Text>默认模型</Text>}<Text type="secondary">回退模型:{item.fallbackModelId ?? "—"}</Text></Space> },
{ title: "状态", render: (_, item) => <Text>{modelStatusLabels[item.status] ?? item.status} · {item.enabled ? "启用" : "停用"}</Text> },
{ title: "发布时间", dataIndex: "publishedAt", render: formatAdminDate },
{
title: "操作",
@@ -2,7 +2,7 @@
import { PlusOutlined } from "@ant-design/icons";
import { List } from "@refinedev/antd";
import { Alert, App, Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Row, Col, Space, Switch, Table, Tag, Typography, type TableColumnsType } from "antd";
import { Alert, App, Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Row, Col, Space, Switch, Table, Typography, type TableColumnsType } from "antd";
import { useCallback, useEffect, useState } from "react";
const { Text } = Typography;
@@ -134,7 +134,7 @@ export function PackageManagement() {
{ title: "价格", dataIndex: "priceCents", align: "right", render: formatMoney },
{ title: "点数", dataIndex: "credits", align: "right" },
{ title: "排序", dataIndex: "sortOrder", align: "right" },
{ title: "状态", dataIndex: "enabled", render: (enabled) => <Tag color={enabled ? "green" : "default"}>{enabled ? "启用" : "停用"}</Tag> },
{ title: "状态", dataIndex: "enabled", render: (enabled) => <Text type={enabled ? undefined : "secondary"}>{enabled ? "启用" : "停用"}</Text> },
{ title: "操作", key: "actions", fixed: "right", render: (_, item) => <Space><Button type="link" onClick={() => openEditModal(item)}>编辑</Button>{item.enabled && <Popconfirm title="停用此套餐?" description="停用后用户将无法继续购买。" okText="停用" cancelText="取消" onConfirm={() => disablePackage(item.id)}><Button type="link" danger loading={disablingId === item.id}>停用</Button></Popconfirm>}</Space> },
];
@@ -234,7 +234,7 @@ export default function PaymentManagement() {
>
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<Text type="secondary">配置兼容标准 Z-Pay / 易支付协议的支付网关、商户凭据、回调地址与对话页开关。</Text>
{epaySettings && <Space wrap><Tag color={epaySettings.source === "database" ? "blue" : "default"}>来源:{{ database: "数据库", environment: "环境变量", unconfigured: "未配置" }[epaySettings.source]}</Tag><Tag color={epaySettings.complete ? "green" : "orange"}>{epaySettings.complete ? "配置完整" : "配置不完整"}</Tag><Tag color={epaySettings.keyConfigured ? "green" : "orange"}>{epaySettings.keyConfigured ? "密钥已配置" : "密钥未配置"}</Tag><Tag color={epaySettings.chatEnabled ? "green" : "default"}>{epaySettings.chatEnabled ? "对话支付开放" : "对话支付关闭"}</Tag></Space>}
{epaySettings && <Text type="secondary">来源:{{ database: "数据库", environment: "环境变量", unconfigured: "未配置" }[epaySettings.source]} · {epaySettings.complete ? "配置完整" : "配置不完整"} · {epaySettings.keyConfigured ? "密钥已配置" : "密钥未配置"} · {epaySettings.chatEnabled ? "对话支付开放" : "对话支付关闭"}</Text>}
{epayError && <Alert type="error" showIcon message="易支付配置读取失败" description={epayError} action={<Button size="small" onClick={() => void loadEpaySettings()}>重试</Button>} />}
<Alert type="info" showIcon message="商户密钥不会回显" description="密钥输入框始终为空;更新现有数据库配置时留空会保留原密钥。首次从环境变量迁移到数据库时必须重新输入密钥。" />
<Form<EpaySettingsForm> form={epayForm} layout="vertical" onFinish={prepareEpaySettings} requiredMark="optional">
@@ -17,7 +17,6 @@ import {
Space,
Switch,
Table,
Tag,
Typography,
type TableColumnsType,
} from "antd";
@@ -225,11 +224,11 @@ export default function ProductManagement() {
dataIndex: "name",
render: (_, item) => <Space direction="vertical" size={0}><Text strong>{item.name}</Text><Text type="secondary">{item.code} · v{item.version}</Text></Space>,
},
{ title: "类型", dataIndex: "productType", render: (value: Product["productType"]) => <Tag>{productTypeLabels[value]}</Tag> },
{ title: "类型", dataIndex: "productType", render: (value: Product["productType"]) => productTypeLabels[value] },
{ title: "周期", render: (_, item) => item.billingPeriod === "none" ? billingPeriodLabels.none : `${item.intervalCount} 个周期(${billingPeriodLabels[item.billingPeriod]})` },
{ title: "价格", render: (_, item) => `¥${(item.priceCents / 100).toFixed(2)}` },
{ title: "权益", dataIndex: "entitlements", render: (items: Entitlement[]) => <Space wrap>{items.map((item) => <Tag key={`${item.featureKey}-${item.allowanceType}`}>{item.featureKey}:{allowanceTypeLabels[item.allowanceType] ?? item.allowanceType}{item.allowanceCount === null ? "" : ` ${item.allowanceCount}`} · {resetPeriodLabels[item.resetPeriod] ?? item.resetPeriod}</Tag>)}</Space> },
{ title: "状态", render: (_, item) => <Space><Tag color={item.status === "published" ? "green" : "gold"}>{item.status}</Tag>{item.enabled ? <Tag color="blue">可售</Tag> : <Tag>停用</Tag>}</Space> },
{ title: "权益", dataIndex: "entitlements", render: (items: Entitlement[]) => <div className="admin-text-list">{items.map((item) => `${item.featureKey}:${allowanceTypeLabels[item.allowanceType] ?? item.allowanceType}${item.allowanceCount === null ? "" : ` ${item.allowanceCount}`} · ${resetPeriodLabels[item.resetPeriod] ?? item.resetPeriod}`).join(";") || "—"}</div> },
{ title: "状态", render: (_, item) => <Text>{item.status === "published" ? "已发布" : item.status === "draft" ? "草稿" : item.status} · {item.enabled ? "可售" : "停用"}</Text> },
{ title: "更新时间", dataIndex: "updatedAt", render: formatAdminDate },
{
title: "操作",
@@ -1,7 +1,7 @@
"use client";
import { useTable } from "@refinedev/antd";
import { Alert, Card, Space, Table, Tag } from "antd";
import { Alert, Card, Table, Typography } from "antd";
interface RoleRecord {
id: string;
@@ -30,8 +30,8 @@ export default function RolesResource() {
columns={[
{ title: "角色", dataIndex: "name" },
{ title: "说明", dataIndex: "description" },
{ title: "MFA", dataIndex: "requiresMfa", render: (value: boolean) => value ? <Tag color="red">必需</Tag> : <Tag>普通</Tag> },
{ title: "权限", dataIndex: "permissions", render: (permissions: string[]) => <Space wrap>{permissions.map((permission) => <Tag key={permission}>{permission}</Tag>)}</Space> },
{ title: "MFA", dataIndex: "requiresMfa", render: (value: boolean) => <Typography.Text type={value ? "danger" : "secondary"}>{value ? "必需" : "普通"}</Typography.Text> },
{ title: "权限", dataIndex: "permissions", render: (permissions: string[]) => <div className="admin-text-list">{permissions.join(" · ") || "—"}</div> },
]}
/>
</Card>
@@ -1,7 +1,7 @@
"use client";
import { useGetIdentity } from "@refinedev/core";
import { App, Button, Space, Tag, Typography, type TableColumnsType } from "antd";
import { App, Button, Space, Typography, type TableColumnsType } from "antd";
import { useState } from "react";
import { ReasonActionModal } from "./reason-action-modal";
@@ -92,7 +92,7 @@ export default function UsersPage() {
const columns: TableColumnsType<UserRecord> = [
{ title: "邮箱", dataIndex: "email", sorter: true },
{ title: "姓名", dataIndex: "name", sorter: true, render: (value) => value || "—" },
{ title: "角色", dataIndex: "role", render: (value) => <Tag>{value}</Tag> },
{ title: "角色", dataIndex: "role" },
{ title: "积分", dataIndex: "credits", sorter: true },
{
title: "出生资料",
@@ -115,7 +115,7 @@ export default function UsersPage() {
},
},
{ title: "邮箱验证", dataIndex: "emailVerified", render: (value) => value ? "已验证" : "未验证" },
{ title: "状态", dataIndex: "banned", render: (value) => value ? <Tag color="red">已禁用</Tag> : <Tag color="green">正常</Tag> },
{ title: "状态", dataIndex: "banned", render: (value) => <Text type={value ? "danger" : "secondary"}>{value ? "已禁用" : "正常"}</Text> },
{ title: "注册时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate },
...(canReset ? [{
title: "操作",
@@ -290,7 +290,6 @@ export function EmailOtpLogin({
<strong>Jyotisha</strong>
</div>
<div>
<p className="auth-kicker">Vedic astrology · 印度占星</p>
<h2>
在星图与当下之间,<br />找到可以行动的
<span className="phrase-nowrap">线索。</span>
@@ -131,7 +131,7 @@ export function PersonalReportCenter() {
<FileText aria-hidden="true" className="size-8 text-primary" />
<h1>请先登录</h1>
<p>登录后即可生成和查看你的个人报告。</p>
<Button render={<Link href="/login" />}>去登录</Button>
<Button render={<Link href="/login" />} nativeButton={false}>去登录</Button>
</main>
);
}
@@ -140,30 +140,22 @@ export function PersonalReportCenter() {
<main className="report-center-shell">
<header className="report-center-topbar">
<Link href="/" className="report-center-back"><ArrowLeft aria-hidden="true" />返回对话</Link>
<span>Jyotisha Reports</span>
</header>
<section className="report-center-hero">
<div>
<p className="report-center-kicker">PERSONAL INTELLIGENCE REPORT</p>
<h1>个人报告</h1>
<p>基于你已确认的出生资料生成,与某一次对话无关。创建后会在后台继续处理,你无需停留等待。</p>
<p className="report-center-meta" aria-label="报告概览">
共 {state.reports.length} 份 · 已完成 {state.reports.filter((report) => report.status === "ready").length} 份 · {hasGenerating ? "有报告生成中" : latestReady ? "最新报告可查看" : "尚待生成"}
</p>
</div>
<GeneratePersonalReportButton onCreated={() => void load()} />
</section>
<section className="report-center-summary" aria-label="报告概览">
<div><strong>{state.reports.length}</strong><span>全部报告</span></div>
<div><strong>{state.reports.filter((report) => report.status === "ready").length}</strong><span>已完成</span></div>
<div><strong>{hasGenerating ? "进行中" : latestReady ? "可查看" : "待生成"}</strong><span>当前状态</span></div>
</section>
<section className="report-center-section">
<div className="report-center-section-heading">
<div>
<p className="report-center-kicker">REPORT ARCHIVE</p>
<h2>报告记录</h2>
</div>
<h2>报告记录</h2>
<button type="button" onClick={() => void load(true)} aria-label="刷新报告列表">
<RefreshCw aria-hidden="true" className={state.phase === "loading" ? "animate-spin" : ""} />刷新
</button>
@@ -185,11 +177,10 @@ export function PersonalReportCenter() {
</div>
) : (
<div className="report-center-list">
{state.reports.map((report, index) => {
{state.reports.map((report) => {
const copy = STATUS_COPY[report.status];
return (
<article className="report-center-card" key={report.id}>
<div className="report-center-card-index">{String(index + 1).padStart(2, "0")}</div>
<div className="report-center-card-body">
<div className={`report-center-status is-${report.status}`}>
<StatusIcon status={report.status} />{copy.label}
@@ -199,11 +190,11 @@ export function PersonalReportCenter() {
<small>{formatDate(report.createdAt)} · {report.themes.join(" / ") || "综合主题"}</small>
</div>
{report.status === "ready" ? (
<Button render={<Link href={`/reports/${encodeURIComponent(report.id)}`} />} variant="outline">
<Button render={<Link href={`/reports/${encodeURIComponent(report.id)}`} />} nativeButton={false} variant="outline">
查看报告
</Button>
) : report.status === "generating" ? (
<Button render={<Link href={`/reports/${encodeURIComponent(report.id)}`} />} variant="ghost">
<Button render={<Link href={`/reports/${encodeURIComponent(report.id)}`} />} nativeButton={false} variant="ghost">
查看进度
</Button>
) : (
@@ -62,7 +62,7 @@ function claimStatusLabel(status: string): string {
return CLAIM_STATUS_LABELS[status as ClaimStatus] ?? status;
}
function badgeClass(status: string): string {
function claimStatusClass(status: string): string {
switch (status) {
case "multi_system_consensus":
return "is-success";
@@ -77,10 +77,10 @@ function badgeClass(status: string): string {
}
}
function ClaimBadge({ status }: { status: string }) {
function ClaimStatus({ status }: { status: string }) {
return (
<span className={`personal-report-claim-badge personal-report-avoid-break ${badgeClass(status)}`}>
{claimStatusLabel(status)}
<span className={`personal-report-claim-status personal-report-avoid-break ${claimStatusClass(status)}`}>
证据:{claimStatusLabel(status)}
</span>
);
}
@@ -138,9 +138,6 @@ function ReportCover({ document, d1 }: { document: ReportDocumentV1; d1: ChartV1
<header className="personal-report-cover personal-report-section">
<div className="personal-report-cover-grid">
<div>
<p className="personal-report-eyebrow">
个人命盘研读 · {document.presentationMode === "research" ? "研究模式" : "标准模式"}
</p>
<h1>{document.subject.displayName}</h1>
<p className="personal-report-deck">{document.executiveSummary.headline}</p>
</div>
@@ -168,7 +165,6 @@ function ExecutiveSummarySection({ document }: { document: ReportDocumentV1 }) {
return (
<section aria-labelledby="report-summary" className="personal-report-section personal-report-summary">
<div className="personal-report-section-heading">
<p>Executive judgement</p>
<h2 id="report-summary">核心判断</h2>
</div>
<NarrativeText text={summary.summary} />
@@ -180,7 +176,7 @@ function ExecutiveSummarySection({ document }: { document: ReportDocumentV1 }) {
{summary.priorities.length > 1 && (
<ol className="personal-report-priorities">
{summary.priorities.slice(1).map((priority, index) => (
<li key={index}><span>{String(index + 2).padStart(2, "0")}</span>{priority}</li>
<li key={index}>{priority}</li>
))}
</ol>
)}
@@ -265,13 +261,12 @@ function AdditionalChartsSection({ charts }: { charts: ChartV1[] }) {
return (
<section aria-labelledby="report-additional-charts" className="personal-report-section personal-report-additional-charts">
<div className="personal-report-section-heading">
<p>Supporting charts</p>
<h2 id="report-additional-charts">分盘证据</h2>
</div>
<div className={charts.length === 1 ? "personal-report-chart-grid is-single" : "personal-report-chart-grid"}>
{charts.map((chart) => (
<article key={chart.id} className="personal-report-chart-card">
<header><h3>{chart.title}</h3><ClaimBadge status={chart.claimStatus} /></header>
<header><h3>{chart.title}</h3><ClaimStatus status={chart.claimStatus} /></header>
<VedicChartSvg chart={chart} ariaLabel={`${chart.title}命盘,共 ${chart.houses.length} 宫数据`} />
<PlanetTable chart={chart} />
</article>
@@ -303,15 +298,13 @@ function ThematicNarrativeSection({ document }: { document: ReportDocumentV1 })
return (
<section aria-labelledby="report-themes" className="personal-report-section personal-report-themes">
<div className="personal-report-section-heading">
<p>Interpretation</p>
<h2 id="report-themes">主题解读与行动建议</h2>
</div>
{document.thematicNarrative.map((section, index) => (
{document.thematicNarrative.map((section) => (
<article key={section.id} aria-labelledby={`theme-${section.id}`} className="personal-report-theme">
<header>
<span>{String(index + 1).padStart(2, "0")}</span>
<h3 id={`theme-${section.id}`}>{section.title}</h3>
<ClaimBadge status={section.claimStatus} />
<ClaimStatus status={section.claimStatus} />
</header>
<NarrativeText text={section.narrative} />
{(section.actions.length > 0 || section.caveats.length > 0) && (
@@ -336,7 +329,7 @@ function AuditTable({ appendix }: { appendix: EvidenceAppendix }) {
return (
<div className="personal-report-table-wrap">
<table className="personal-report-audit-table">
<caption className="sr-only">Technique Audit Table</caption>
<caption className="sr-only">技法审计表</caption>
<thead><tr><th>技法</th><th>状态</th><th>使用</th><th>说明</th></tr></thead>
<tbody>
{appendix.techniqueAudit.map((row) => (
@@ -361,14 +354,14 @@ function EvidenceAppendixSection({ document }: { document: ReportDocumentV1 }) {
return (
<section aria-labelledby="report-appendix" className="personal-report-section personal-report-appendix">
<div className="personal-report-section-heading">
<p>Method & evidence</p><h2 id="report-appendix">证据附录</h2>
<h2 id="report-appendix">证据附录</h2>
</div>
<p className="personal-report-appendix-intro">Technique Audit、冲突、计算证据与 blocked 技法。打印时始终展开。</p>
<p className="personal-report-appendix-intro">技法审计、冲突、计算证据与受限技法。打印时始终展开。</p>
{hasContent && (
<details className="personal-report-appendix-details" open={expandedByDefault}>
<summary className="personal-report-screen-only personal-report-disclosure">展开 / 收起附录</summary>
<div id="report-appendix-body" className="personal-report-print-always">
<h3>Technique Audit Table</h3><AuditTable appendix={appendix} />
<h3>技法审计表</h3><AuditTable appendix={appendix} />
{appendix.conflicts.length > 0 && (
<><h3>冲突与降级</h3><ul className="personal-report-conflicts">
{appendix.conflicts.map((conflict) => (
@@ -391,7 +384,7 @@ function EvidenceAppendixSection({ document }: { document: ReportDocumentV1 }) {
</table></div></>
)}
{appendix.blockedTechniques.length > 0 && (
<><h3>Blocked 技法</h3><ul className="personal-report-blocked-list">
<><h3>受限技法</h3><ul className="personal-report-blocked-list">
{appendix.blockedTechniques.map((name, index) => <li key={`${name}-${index}`}>{name}</li>)}
</ul></>
)}
@@ -172,7 +172,7 @@ export function PersonalReportPage({ reportId }: { reportId: string }) {
<p className="max-w-md text-sm text-ink-secondary">
生成完成后页面会自动显示;你也可以返回报告中心,后台会继续处理。
</p>
<Button render={<Link href="/reports" />} variant="ghost">返回报告中心</Button>
<Button render={<Link href="/reports" />} nativeButton={false} variant="ghost">返回报告中心</Button>
</>
)}
</main>
@@ -186,7 +186,7 @@ export function PersonalReportPage({ reportId }: { reportId: string }) {
<p className="max-w-md text-sm text-ink-secondary">
个人报告仅对登录用户开放。请登录后重试。
</p>
<Button render={<Link href="/login" />} variant="default">
<Button render={<Link href="/login" />} nativeButton={false} variant="default">
去登录
</Button>
</main>
@@ -200,7 +200,7 @@ export function PersonalReportPage({ reportId }: { reportId: string }) {
<p className="max-w-md text-sm text-ink-secondary">
该报告不存在、已删除,或不属于当前账号。
</p>
<Button render={<Link href="/reports" />} variant="outline">
<Button render={<Link href="/reports" />} nativeButton={false} variant="outline">
返回报告中心
</Button>
</main>
@@ -218,7 +218,7 @@ export function PersonalReportPage({ reportId }: { reportId: string }) {
<p className="max-w-md text-sm text-ink-secondary">
生成过程中出现问题,未产出可用报告。请稍后重试。
</p>
<Button render={<Link href="/reports" />} variant="outline">
<Button render={<Link href="/reports" />} nativeButton={false} variant="outline">
返回报告中心重新生成
</Button>
</main>
+4 -2
View File
@@ -78,8 +78,10 @@ test("admin navigation exposes separated RBAC and billing resources", () => {
assert.match(adminApp, /ShoppingOutlined/);
});
test("admin shell uses large controls without the return-to-chat sider override", () => {
assert.match(adminApp, /<ConfigProvider componentSize="large"/);
test("admin shell uses compact operational controls without the return-to-chat sider override", () => {
assert.match(adminApp, /componentSize="middle"/);
assert.match(adminApp, /algorithm: theme\.defaultAlgorithm/);
assert.doesNotMatch(adminApp, /theme\.darkAlgorithm/);
assert.match(adminApp, /<ThemedLayout>\{children\}<\/ThemedLayout>/);
assert.doesNotMatch(adminApp, /AdminSider|ThemedSider|ArrowLeftOutlined|返回对话|next\/link|Menu\.Item/);
});
+20 -22
View File
@@ -258,12 +258,10 @@ test("payment and plan copy points users to the separate orders page", () => {
assert.doesNotMatch(pageSource, /本页会自动刷新余额与订单记录|本页「订单记录」|用于咨询、报告与生时校正等服务/);
});
test("orbit / ring guidance visual is present in the hero", () => {
assert.match(pageSource, /membership-celestial-visual/);
assert.match(pageSource, /membership-orbit membership-orbit-outer/);
assert.match(pageSource, /membership-orbit membership-orbit-inner/);
assert.match(pageSource, /membership-celestial-mark/);
assert.match(cssBlock(".membership-hero"), /overflow:\s*hidden/);
test("membership hero stays content-led without decorative orbit artwork", () => {
assert.doesNotMatch(pageSource, /membership-celestial-visual|membership-orbit|membership-celestial-mark/);
assert.doesNotMatch(globalStyles, /membership-celestial-visual|membership-orbit|membership-celestial-mark/);
assert.match(cssBlock(".membership-hero"), /background:\s*var\(--color-canvas-soft\)/);
});
test("redeem modal surface contains title, balance, input, redeem, rules and close", () => {
@@ -295,13 +293,14 @@ test("desktop uses three columns, tablet two and mobile one with monthly first",
assert.doesNotMatch(cssBlock(".membership-plan-card--recommended"), /grid-column/);
});
test("segment tabs use a rounded container and active card treatment", () => {
assert.match(cssBlock(".membership-tabs"), /border-radius:\s*var\(--radius-lg\)/);
assert.match(cssBlock(".membership-tabs"), /background:\s*var\(--color-canvas-soft\)/);
assert.match(cssBlock(".membership-tab[data-active]"), /background:\s*var\(--color-canvas\)/);
assert.match(cssBlock(".membership-tab[data-active]"), /box-shadow:\s*var\(--shadow-soft\)/);
test("segment tabs use a quiet underline treatment", () => {
assert.match(cssBlock(".membership-tabs"), /border-bottom:\s*1px solid var\(--color-border\)/);
assert.doesNotMatch(cssBlock(".membership-tabs"), /border-radius|background|box-shadow/);
assert.match(cssBlock(".membership-tab[data-active]"), /border-bottom-color:\s*var\(--color-action\)/);
assert.match(cssBlock(".membership-tab[data-active]"), /background:\s*transparent/);
assert.match(cssBlock(".membership-tab[data-active]"), /box-shadow:\s*none/);
assert.match(cssBlock(".membership-plan-card--highlighted"), /outline:\s*2px solid color-mix\(in srgb, var\(--color-action\)/);
assert.match(cssBlock(".membership-payment-error"), /background:\s*var\(--color-danger-muted\)/);
assert.match(cssBlock(".membership-payment-error"), /background:\s*transparent/);
});
test("membership shell is capped within the 1120-1200 range", () => {
@@ -310,22 +309,21 @@ test("membership shell is capped within the 1120-1200 range", () => {
});
test("membership page reuses the warm parchment / terracotta tokens", () => {
assert.match(cssBlock(".membership-hero"), /linear-gradient\(135deg, var\(--color-canvas\) 0%, var\(--color-canvas-soft\) 100%\)/);
assert.match(cssBlock(".membership-hero"), /background:\s*var\(--color-canvas-soft\)/);
assert.doesNotMatch(cssBlock(".membership-hero"), /linear-gradient/);
assert.doesNotMatch(cssBlock(".membership-hero"), /#fbfaf7|#f5f1eb/);
assert.match(cssBlock(".membership-plan-card--recommended"), /color-mix\(in srgb, var\(--color-action\)/);
assert.match(cssBlock(".membership-plan-card--current"), /border-color:\s*var\(--color-action\)/);
assert.match(cssBlock(".membership-title"), /var\(--color-ink\)/);
assert.match(cssBlock(".membership-badge--recommended"), /background: var\(--color-action\)/);
assert.match(cssBlock(".membership-status"), /color:\s*var\(--color-action\)/);
assert.doesNotMatch(globalStyles, /membership-badge--recommended/);
});
test("membership page scrolls within a fixed viewport and the recommended card has a terracotta top bar", () => {
test("membership page scrolls within a fixed viewport and cards stay flat", () => {
assert.match(cssBlock(".membership-page"), /height:\s*100dvh/);
assert.match(cssBlock(".membership-page"), /overflow-y:\s*auto/);
assert.match(globalStyles, /\.membership-plan-card,[\s\S]*\.membership-credit-card\s*\{[^}]*position:\s*relative/);
assert.match(globalStyles, /\.membership-plan-card,[\s\S]*\.membership-credit-card\s*\{[^}]*overflow:\s*hidden/);
assert.match(cssBlock(".membership-plan-card--recommended::before"), /background:\s*var\(--color-action\)/);
assert.match(cssBlock(".membership-plan-card--recommended"), /background:\s*color-mix\(in srgb, var\(--color-action-soft\)/);
assert.match(cssBlock(".membership-plan-card--recommended"), /border-color:\s*color-mix\(in srgb, var\(--color-action\)/);
assert.match(cssBlock(".membership-plan-card--recommended"), /box-shadow:/);
assert.match(cssBlock(".membership-plan-card, .membership-credit-card"), /box-shadow:\s*none/);
assert.match(cssBlock(".membership-plan-card--recommended"), /background:\s*var\(--color-canvas\)/);
assert.doesNotMatch(globalStyles, /membership-plan-card--recommended::before/);
});
test("membership lib keeps helpers and the balance sync contract", () => {
+2 -2
View File
@@ -54,10 +54,10 @@ test("renders the selected answer-first order: cover -> summary -> D1 -> themes
assert.ok(positions.appendix < positions.disclaimer, "appendix comes before the disclaimer");
});
test("Technique Audit Table is never placed before the summary", () => {
test("技法审计表不会出现在核心判断之前", () => {
const markup = render(canonicalFixture);
const summaryAt = markup.indexOf("核心判断");
const auditAt = markup.indexOf("Technique Audit Table");
const auditAt = markup.indexOf("技法审计表");
assert.ok(summaryAt >= 0 && auditAt >= 0, "both summary and audit table render");
assert.ok(auditAt > summaryAt, "audit table must appear after the summary");
});