diff --git a/frontend/src/app/app.tsx b/frontend/src/app/app.tsx index ff565095..8c0976bf 100644 --- a/frontend/src/app/app.tsx +++ b/frontend/src/app/app.tsx @@ -2,6 +2,7 @@ import { lazy, Suspense } from 'react' import { BrowserRouter, Navigate, Route, Routes } from 'react-router' import { AssetLibraryPage } from '@/pages/asset-library' +import { SubscriptionPage } from '@/pages/subscription' import { AccountPage } from '@/pages/account' import { AdminRedemptionCodesPage } from '@/pages/admin-redemption-codes' import { CharacterDetailPage } from '@/pages/character-detail' @@ -73,6 +74,7 @@ export function AppRoutes() { }> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/app/layout/app-header.tsx b/frontend/src/app/layout/app-header.tsx index 0e178c56..e7e3d5aa 100644 --- a/frontend/src/app/layout/app-header.tsx +++ b/frontend/src/app/layout/app-header.tsx @@ -455,6 +455,21 @@ export function AppHeader({ accountMenu.state, )}`} > +
+
+

Free

+

+ 当前订阅 · 标准创作 +

+
+ + 升级 + +
))} + + 订阅与充值 ↗ + + +
+ + {recharge ? ( +
+
+

积分充值

+

+ 无需开通会员。每 ¥1 获得 12.5 积分,不足一积分向上补足。 +

+
+ {amounts.map((item) => ( + + ))} +
+ + {!valid && ( +

+ 请输入不低于 5 元的整数金额。 +

+ )} +
+
+

本次将获得

+ + {valid ? credits.toLocaleString('zh-CN') : '—'} + 积分 + +
+
+ 充值金额 + {valid ? `¥${value}` : '—'} +
+
+ 当前方案 + Free +
+ + 充值只增加积分,不改变会员资格。 +
+
+ ) : ( +
+ {plans.map((plan, index) => ( +
setSelectedPlan(plan.name)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + setSelectedPlan(plan.name) + } + }} + role="button" + tabIndex={0} + aria-pressed={selectedPlan === plan.name} + > +
+ {index === 1 && 最受欢迎} +
+

{plan.name}

+
+ ¥ + {plan.price} + / 月 +
+
+ {plan.credits.toLocaleString('zh-CN')} + 积分 + 省 {plan.saving} +
+ +
+

+ + 完整标准制作流程 +

+

+ + 保留已有资产与下载能力 +

+

+ + 会员专属能力筹备中 +

+
+

约 ¥{plan.unit} / 积分

+
+ ))} +
+ )} +
+
+

+ 当前订阅方案:Free +

+

Free 包含的功能

+

Free 同样支持标准制作、局部重做与透明素材导出;生成任务按实际报价消耗积分。

+
+
+

暂未开放的会员权益

+

+ 更好的模型、高质量处理、三渲二暂未开放。会员期限、积分有效期与续购规则将在开售前说明。 +

+
+
+ { + if (event.target === event.currentTarget) dialog.current?.close() + }} + aria-labelledby="purchase-title" + > + +

{selection?.name}

+
+ ¥{selection?.price} + {selection?.credits.toLocaleString('zh-CN')} 积分 +
+

支付还没有接入,请联系管理员。当前不会扣款,也不会变更积分或会员资格。

+ +
+ + ) +} diff --git a/frontend/src/pages/subscription/subscription.css b/frontend/src/pages/subscription/subscription.css new file mode 100644 index 00000000..bfc3cd65 --- /dev/null +++ b/frontend/src/pages/subscription/subscription.css @@ -0,0 +1,431 @@ +.subscription-page { + max-width: 1180px; + margin: auto; + padding: 68px 36px 70px; + color: var(--color-app-ink); +} +.subscription-toolbar, +.subscription-creditline, +.subscription-plan-head, +.subscription-button { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.subscription-plan-head { + min-height: 1.125rem; +} +.subscription-heading { + display: grid; + place-items: center; + padding: 23px 0 18px; +} +.subscription-heading h1 { + font-family: var(--font-serif); + font-size: clamp(27px, 3vw, 38px); + font-weight: 500; + letter-spacing: -2px; + margin: 0; +} +.subscription-heading h1 > span { + color: var(--color-app-faint); +} +.subscription-intro { + font-size: 13px; + line-height: 1.9; + color: var(--color-app-muted); +} +.subscription-toolbar { + justify-content: center; + margin: 17px 0 14px; +} +.subscription-toolbar > span { + font-size: 11px; + color: var(--color-app-faint); +} +.subscription-tabs { + display: flex; + padding: 4px; + background: var(--color-app-surface-muted); + border: 1px solid var(--color-app-line); + border-radius: 10px; + gap: 4px; +} +.subscription-tabs button { + padding: 9px 20px; + font-size: 13px; + border-radius: 7px; + color: var(--color-app-muted); +} +.subscription-tabs button[aria-pressed='true'] { + background: var(--color-app-surface-raised); + color: var(--color-app-ink); + box-shadow: 0 1px 4px #0000000a; +} +.subscription-plans { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; + align-items: stretch; +} +.subscription-plan { + border: 1px solid var(--color-app-line); + background: var(--color-app-surface); + border-radius: 14px; + padding: 25px 26px 20px; + position: relative; + cursor: pointer; + display: flex; + min-height: 500px; + flex-direction: column; + transition: + background-color 0.15s, + box-shadow 0.15s, + transform 0.15s; +} +.subscription-plan:hover { + transform: translateY(-1px); +} +.subscription-plan.selected { + background: var(--color-app-surface-raised); + box-shadow: 0 0 0 2px var(--color-app-ink); +} +.subscription-plan:focus-visible { + outline: 2px solid var(--color-app-ink); + outline-offset: 4px; +} +.subscription-recommended { + position: absolute; + top: 24px; + right: 26px; + font-size: 10px; + background: var(--color-app-ink); + color: var(--color-app-canvas); + padding: 4px 9px; + border-radius: 4px; + margin-left: auto; +} +.subscription-plan h2 { + font-family: var(--font-serif); + font-size: 30px; + letter-spacing: -1px; + margin: 17px 0 8px; +} +.subscription-price { + display: flex; + align-items: baseline; + font-size: 49px; + letter-spacing: -2px; + font-weight: 550; + margin: 25px 0 9px; + gap: 3px; +} +.subscription-price > span { + font-size: 23px; + margin-right: 3px; +} +.subscription-price small { + font-size: 12px; + letter-spacing: 0; + color: var(--color-app-faint); + font-weight: 400; + margin-left: 7px; +} +.subscription-creditline { + justify-content: flex-start; + font-size: 12px; + gap: 7px; +} +.subscription-creditline strong { + font-size: 17px; +} +.subscription-creditline em { + font-style: normal; + margin-left: auto; + font-size: 10px; + border: 1px solid var(--color-app-line); + border-radius: 4px; + padding: 3px 5px; + color: var(--color-app-muted); +} +.subscription-button { + border: 1px solid var(--color-app-line-strong); + border-radius: 8px; + width: 100%; + padding: 12px 14px; + font-size: 13px; + font-weight: 550; + margin: 24px 0; + transition: + transform 0.15s, + opacity 0.15s; + background: transparent; +} +.subscription-button.primary { + background: var(--color-app-ink); + border-color: var(--color-app-ink); + color: var(--color-app-canvas); +} +.subscription-button:hover { + transform: translateY(-1px); + opacity: 0.85; +} +.subscription-button:disabled { + opacity: 0.35; + cursor: not-allowed; + transform: none; +} +.subscription-plan-details { + border-top: 1px solid var(--color-app-line); + padding-top: 9px; + min-height: 108px; +} +.subscription-plan-details p { + display: flex; + align-items: center; + gap: 9px; + font-size: 12px; + margin-top: 13px; + color: var(--color-app-muted); +} +.subscription-plan-details p > span { + font-size: 9px; + margin-left: auto; + color: var(--color-app-faint); +} +.subscription-unit { + margin-top: auto; + padding-top: 23px; + font-size: 10px; + color: var(--color-app-faint); + font-family: monospace; +} +.subscription-footnote { + display: grid; + grid-template-columns: 1.3fr 1fr; + gap: 65px; + margin-top: 35px; + padding-top: 26px; + border-top: 1px solid var(--color-app-line); +} +.subscription-footnote h3 { + font-family: var(--font-serif); + font-size: 19px; + margin: 10px 0; +} +.subscription-footnote h4 { + font-size: 12px; + margin: 5px 0 12px; +} +.subscription-footnote p { + font-size: 11px; + line-height: 1.9; + color: var(--color-app-muted); +} +.subscription-current-plan { + margin: 0 0 10px; + font-size: 14px; + color: var(--color-app-muted); +} +.subscription-current-plan strong { + color: var(--color-app-ink); + font-size: 18px; + font-weight: 650; +} +.recharge-panel { + display: grid; + grid-template-columns: 1.6fr 1fr; + border: 1px solid var(--color-app-line); + border-radius: 14px; + overflow: hidden; + background: var(--color-app-surface); +} +.recharge-options { + padding: 30px; +} +.recharge-options h2 { + font-family: var(--font-serif); + font-size: 24px; + margin: 13px 0; +} +.recharge-amounts { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + margin: 26px 0; +} +.recharge-amounts button { + border: 1px solid var(--color-app-line); + border-radius: 8px; + padding: 14px; + background: var(--color-app-surface-raised); +} +.recharge-amounts button[aria-pressed='true'] { + border-color: var(--color-app-ink); + box-shadow: inset 0 0 0 1px var(--color-app-ink); +} +.recharge-amounts strong { + display: block; + font-size: 22px; + font-weight: 500; +} +.recharge-amounts span { + font-size: 11px; + color: var(--color-app-muted); +} +.recharge-custom { + font-size: 12px; +} +.recharge-custom > span { + margin-left: 10px; + font-size: 10px; + color: var(--color-app-faint); +} +.recharge-custom > div { + display: flex; + align-items: center; + border: 1px solid var(--color-app-line); + border-radius: 8px; + padding: 12px; + margin-top: 10px; + gap: 10px; +} +.recharge-custom input { + width: 100%; + outline: none; + background: transparent; +} +.recharge-custom > div:focus-within { + outline: 2px solid var(--color-app-ink); + outline-offset: 2px; +} +.recharge-summary { + background: var(--color-app-surface-muted); + padding: 34px; + display: flex; + flex-direction: column; + justify-content: center; +} +.recharge-summary > p { + font-size: 12px; + margin: 18px 0 8px; + color: var(--color-app-muted); +} +.recharge-summary > strong { + font-size: 46px; + font-weight: 500; + letter-spacing: -1px; +} +.recharge-summary > strong span { + font-size: 12px; + letter-spacing: 0; + color: var(--color-app-muted); +} +.recharge-summary hr { + border-color: var(--color-app-line); + margin: 22px 0; +} +.recharge-summary > div { + display: flex; + justify-content: space-between; + font-size: 12px; + margin: 8px 0; +} +.recharge-summary small { + font-size: 10px; + color: var(--color-app-muted); + text-align: center; +} +.subscription-error { + font-size: 12px; + color: var(--color-app-danger); + margin-top: 8px; +} +.subscription-dialog { + margin: auto; + width: min(440px, calc(100vw - 32px)); + padding: 36px; + background: var(--color-app-surface-raised); + color: var(--color-app-ink); + border: 1px solid var(--color-app-line); + border-radius: 16px; +} +.subscription-dialog::backdrop { + background: #0006; + backdrop-filter: blur(5px); +} +.subscription-dialog-close { + position: absolute; + right: 16px; + top: 16px; + padding: 5px; +} +.subscription-dialog h2 { + font-size: 30px; + font-family: var(--font-serif); + margin: 20px 0; +} +.subscription-dialog-total { + font-size: 40px; + margin-bottom: 22px; + display: flex; + justify-content: space-between; + align-items: center; +} +.subscription-dialog-total span { + font-size: 14px; +} +.subscription-dialog > p:not(.subscription-eyebrow) { + font-size: 13px; + line-height: 1.8; + color: var(--color-app-muted); +} +.subscription-dialog .subscription-button { + margin-bottom: 0; + justify-content: center; +} +.subscription-page button { + cursor: pointer; +} +.subscription-page button:focus-visible, +.subscription-page a:focus-visible { + outline: 2px solid var(--color-app-ink); + outline-offset: 4px; +} +@media (max-width: 760px) { + .subscription-page { + padding: 64px 20px 45px; + } + .subscription-heading { + padding: 20px 0 16px; + } + .subscription-plans { + grid-template-columns: 1fr; + } + .subscription-plan { + padding: 24px; + } + .subscription-toolbar > span { + display: none; + } + .subscription-footnote { + grid-template-columns: 1fr; + gap: 20px; + } + .recharge-panel { + grid-template-columns: 1fr; + } + .recharge-options, + .recharge-summary { + padding: 24px; + } +} +@media (prefers-reduced-motion: reduce) { + .subscription-button { + transition: none; + } + .subscription-button:hover { + transform: none; + } +}