refactor(frontend): remove generic AI-slop styling
This commit is contained in:
+212
-926
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user