抽取充值模态框 #87
@@ -16,6 +16,13 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-16 充值弹窗迁入 shared 并组件自带样式
|
||||
|
||||
- 背景:未来 Tauri 客户端需要复用主站和图片画布已有的泥点充值弹窗,但原实现位于 `src/components/platform-entry` 并依赖 `src/index.css` 中的全局样式。
|
||||
- 决策:充值弹窗的正式实现迁入 `packages/shared/src/components/PlatformProfileRechargeModal/`,采用 `index.tsx` + `index.css` 目录结构;组件自带可复用样式,主站和图片画布改为直接引用 shared 组件.支付渠道选择、建单、确认、登录和宿主桥接继续留在业务侧 controller,不下沉到 shared。当前版本仍遵循 2026-07-12 决策,不展示会员商品或会员购买入口。
|
||||
- 影响范围:`packages/shared` 组件导出、主站和图片画布充值弹窗引用、未来 Tauri Web 客户端复用方式。
|
||||
- 验证方式:充值弹窗定向测试、资金 ViewModel 定向测试、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-07-14 后台账号采用 owner 引导账号与一级 Tab 实时授权
|
||||
|
||||
- 背景:后台此前只支持一组环境变量管理员,所有 `/admin/api/*` 共用统一 admin 门禁,无法给运营、审核等人员分配独立账号和页面范围。
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
.platform-profile-recharge-modal-backdrop {
|
||||
background: var(--platform-overlay-fill);
|
||||
color: var(--platform-text-strong);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal,
|
||||
.platform-recharge-modal {
|
||||
border: 1px solid var(--platform-modal-border);
|
||||
background: var(--platform-modal-fill);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
0 24px 80px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__close {
|
||||
border: 1px solid rgba(255, 255, 255, 0.32);
|
||||
background: var(--platform-profile-chip-fill);
|
||||
color: var(--platform-text-soft);
|
||||
box-shadow: 0 10px 24px rgba(112, 57, 30, 0.08);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__close:hover:not(:disabled) {
|
||||
background: var(--platform-profile-chip-hover-fill);
|
||||
color: var(--platform-text-strong);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__subpanel {
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
background: var(--platform-subpanel-fill);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__interactive-card {
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
border-color 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
background 160ms ease;
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__interactive-card:hover:not(:disabled) {
|
||||
border-color: var(--platform-surface-hover-border);
|
||||
background: var(--platform-subpanel-fill);
|
||||
box-shadow: 0 12px 26px rgba(112, 57, 30, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__interactive-card:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__primary-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
box-shadow: var(--platform-profile-action-shadow);
|
||||
transition:
|
||||
opacity 160ms ease,
|
||||
transform 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__primary-button:hover:not(:disabled) {
|
||||
box-shadow: 0 16px 34px rgba(182, 98, 63, 0.28);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__primary-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__status-error {
|
||||
border: 1px solid var(--platform-button-danger-border);
|
||||
background: var(--platform-button-danger-fill);
|
||||
color: var(--platform-button-danger-text);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__empty {
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
background: var(--platform-subpanel-fill);
|
||||
color: var(--platform-text-soft);
|
||||
}
|
||||
|
||||
.platform-profile-recharge-modal__badge-warning {
|
||||
border: 1px solid var(--platform-warm-border);
|
||||
background: var(--platform-warm-bg);
|
||||
color: var(--platform-warm-text);
|
||||
}
|
||||
+3
-2
@@ -4,8 +4,9 @@ import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { ProfileRechargeProduct } from '../../../packages/shared/src/contracts/runtime';
|
||||
import { PlatformProfileRechargeModal } from './PlatformProfileRechargeModal';
|
||||
import type { ProfileRechargeProduct } from '@/packages/shared/src';
|
||||
|
||||
import { PlatformProfileRechargeModal } from '../../index';
|
||||
|
||||
function buildNormalMembership() {
|
||||
return {
|
||||
+264
-66
@@ -1,30 +1,33 @@
|
||||
import QRCode from 'qrcode';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import './index.css';
|
||||
|
||||
import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts';
|
||||
import QRCode from 'qrcode';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useId, useState } from 'react';
|
||||
|
||||
import type {
|
||||
ProfileRechargeCenterResponse,
|
||||
ProfileRechargeProduct,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
|
||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { formatRechargePrice } from '../rpg-entry/rpgEntryProfileFundsViewModel';
|
||||
import { PlatformProfileModalShell } from './PlatformProfileModalShell';
|
||||
import type { NativeWechatPaymentState } from './usePlatformProfileCenterController';
|
||||
} from '../../contracts/runtime';
|
||||
import { formatMudPointCount, formatRechargePrice } from '../../utils/format';
|
||||
|
||||
const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180;
|
||||
|
||||
export type PlatformProfileRechargeNativePaymentState = {
|
||||
orderId: string;
|
||||
productTitle: string;
|
||||
amountCents: number;
|
||||
codeUrl: string;
|
||||
expiresAt: string;
|
||||
isConfirming: boolean;
|
||||
confirmMessage?: string;
|
||||
};
|
||||
|
||||
export type PlatformProfileRechargeModalProps = {
|
||||
center: ProfileRechargeCenterResponse | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
submittingProductId: string | null;
|
||||
nativePayment: NativeWechatPaymentState | null;
|
||||
nativePayment: PlatformProfileRechargeNativePaymentState | null;
|
||||
onClose: () => void;
|
||||
onRetry: () => void;
|
||||
onBuy: (product: ProfileRechargeProduct) => void;
|
||||
@@ -32,9 +35,228 @@ export type PlatformProfileRechargeModalProps = {
|
||||
onCloseNativePayment: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成微信 Native 支付二维码图片,保持首页现有二维码尺寸与容错行为。
|
||||
*/
|
||||
function joinClassNames(
|
||||
...classNames: Array<string | false | null | undefined>
|
||||
) {
|
||||
return classNames.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function PlatformProfileRechargeModalShell({
|
||||
title,
|
||||
description,
|
||||
onClose,
|
||||
closeLabel,
|
||||
closeDisabled = false,
|
||||
size = 'sm',
|
||||
zIndexClassName = 'z-[80]',
|
||||
panelClassName,
|
||||
bodyClassName = 'px-5 py-5',
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
onClose: () => void;
|
||||
closeLabel?: string;
|
||||
closeDisabled?: boolean;
|
||||
size?: 'sm' | 'md';
|
||||
zIndexClassName?: string;
|
||||
panelClassName: string;
|
||||
bodyClassName?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const titleId = useId();
|
||||
const descriptionId = useId();
|
||||
const sizeClassName = size === 'md' ? 'max-w-xl' : 'max-w-md';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={joinClassNames(
|
||||
'platform-profile-recharge-modal-backdrop fixed inset-0 flex items-center justify-center px-4 py-6',
|
||||
zIndexClassName,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
aria-describedby={description ? descriptionId : undefined}
|
||||
className={joinClassNames(
|
||||
'platform-profile-recharge-modal flex max-h-[min(92vh,58rem)] w-full flex-col overflow-hidden rounded-[1.75rem]',
|
||||
sizeClassName,
|
||||
panelClassName,
|
||||
)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3 border-b border-white/10 px-5 py-4">
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
id={titleId}
|
||||
className="text-base font-black text-[var(--platform-text-strong)]"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{description ? (
|
||||
<div
|
||||
id={descriptionId}
|
||||
className="mt-1 text-xs font-semibold text-[var(--platform-text-soft)]"
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={closeLabel ?? `关闭${title}`}
|
||||
title={closeLabel ?? `关闭${title}`}
|
||||
disabled={closeDisabled}
|
||||
onClick={onClose}
|
||||
className="platform-profile-recharge-modal__close flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-lg font-black disabled:cursor-not-allowed disabled:opacity-45"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className={joinClassNames('min-h-0 flex-1', bodyClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformRechargeActionButton({
|
||||
children,
|
||||
className,
|
||||
disabled,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={joinClassNames(
|
||||
'platform-profile-recharge-modal__primary-button inline-flex items-center justify-center gap-2 rounded-2xl px-4 py-2 text-xs font-black',
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformRechargeAsyncStatePanel({
|
||||
errorState,
|
||||
isLoading,
|
||||
loadingState,
|
||||
isEmpty,
|
||||
emptyState,
|
||||
children,
|
||||
}: {
|
||||
errorState?: ReactNode;
|
||||
isLoading?: boolean;
|
||||
loadingState?: ReactNode;
|
||||
isEmpty?: boolean;
|
||||
emptyState?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
if (errorState !== undefined && errorState !== null) {
|
||||
return <>{errorState}</>;
|
||||
}
|
||||
if (isLoading) {
|
||||
return <>{loadingState}</>;
|
||||
}
|
||||
if (isEmpty) {
|
||||
return <>{emptyState}</>;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
function PlatformRechargeSkeletonList({
|
||||
count,
|
||||
containerClassName,
|
||||
itemClassName,
|
||||
}: {
|
||||
count: number;
|
||||
containerClassName?: string;
|
||||
itemClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={joinClassNames(
|
||||
'animate-pulse rounded-xl bg-zinc-100',
|
||||
itemClassName,
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformRechargeEmptyState({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={joinClassNames(
|
||||
'platform-profile-recharge-modal__empty min-w-0 rounded-[1rem] px-4 py-5 text-center text-sm font-semibold',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformRechargeStatusMessage({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={joinClassNames(
|
||||
'platform-profile-recharge-modal__status-error rounded-xl px-3 py-2 text-xs',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformRechargePillBadge({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={joinClassNames(
|
||||
'platform-profile-recharge-modal__badge-warning inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-[10px] font-black',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function useWechatNativeQrCode(codeUrl: string | null) {
|
||||
const [qrImageUrl, setQrImageUrl] = useState<string | null>(null);
|
||||
|
||||
@@ -96,7 +318,6 @@ function useNativePaymentRemaining(expiresAt: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/** 充值套餐行,复用后端下发的商品与逐档首充资格。 */
|
||||
function RechargeProductCard({
|
||||
product,
|
||||
submittingProductId,
|
||||
@@ -114,27 +335,18 @@ function RechargeProductCard({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<PlatformSubpanel
|
||||
as="button"
|
||||
<button
|
||||
type="button"
|
||||
surface="platform"
|
||||
onClick={() => onBuy(product)}
|
||||
disabled={Boolean(submittingProductId)}
|
||||
interactive
|
||||
radius="sm"
|
||||
padding="none"
|
||||
aria-label={`${formatMudPointCount(product.pointsAmount)}泥点${bonusLabel ? ` ${bonusLabel}` : ''} ${formatRechargePrice(product.priceCents)} 购买`}
|
||||
className="platform-recharge-product-row platform-interactive-card relative grid min-h-[4.5rem] grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-3 px-3.5 py-3 text-left"
|
||||
className="platform-recharge-product-row platform-subpanel platform-interactive-card platform-profile-recharge-modal__subpanel platform-profile-recharge-modal__interactive-card relative grid min-h-[4.5rem] grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-3 rounded-[1rem] px-3.5 py-3 text-left transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--platform-warm-border)] disabled:cursor-not-allowed disabled:opacity-55"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
{badgeLabel ? (
|
||||
<PlatformPillBadge
|
||||
tone="warning"
|
||||
size="xxs"
|
||||
className="mb-1.5 max-w-[7rem] truncate px-2 py-0.5"
|
||||
>
|
||||
<PlatformRechargePillBadge className="mb-1.5 max-w-[7rem] truncate px-2 py-0.5">
|
||||
{badgeLabel}
|
||||
</PlatformPillBadge>
|
||||
</PlatformRechargePillBadge>
|
||||
) : null}
|
||||
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<span className="text-sm font-black text-[var(--platform-text-strong)]">
|
||||
@@ -150,10 +362,10 @@ function RechargeProductCard({
|
||||
<span className="text-sm font-black tabular-nums text-[var(--platform-text-strong)]">
|
||||
{formatRechargePrice(product.priceCents)}
|
||||
</span>
|
||||
<span className="platform-primary-button rounded-full px-3 py-1.5 text-xs font-black">
|
||||
<span className="platform-profile-recharge-modal__primary-button rounded-full px-3 py-1.5 text-xs font-black">
|
||||
{submitting ? '处理中' : '购买'}
|
||||
</span>
|
||||
</PlatformSubpanel>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,7 +375,7 @@ function PlatformProfileWechatNativePaymentModal({
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: {
|
||||
nativePayment: NativeWechatPaymentState;
|
||||
nativePayment: PlatformProfileRechargeNativePaymentState;
|
||||
nativeQrImageUrl: string | null;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
@@ -171,7 +383,7 @@ function PlatformProfileWechatNativePaymentModal({
|
||||
const remaining = useNativePaymentRemaining(nativePayment.expiresAt);
|
||||
|
||||
return (
|
||||
<PlatformProfileModalShell
|
||||
<PlatformProfileRechargeModalShell
|
||||
title="微信扫码支付"
|
||||
description="请使用微信扫描二维码完成付款"
|
||||
onClose={onClose}
|
||||
@@ -179,7 +391,7 @@ function PlatformProfileWechatNativePaymentModal({
|
||||
closeDisabled={nativePayment.isConfirming}
|
||||
size="sm"
|
||||
zIndexClassName="z-[85]"
|
||||
panelClassName="platform-recharge-modal !max-w-sm rounded-[1.4rem]"
|
||||
panelClassName="!max-w-sm rounded-[1.4rem]"
|
||||
bodyClassName="px-5 py-5"
|
||||
>
|
||||
<div className="text-center">
|
||||
@@ -214,9 +426,7 @@ function PlatformProfileWechatNativePaymentModal({
|
||||
<span className="text-xs font-semibold text-slate-500">生成中</span>
|
||||
)}
|
||||
</div>
|
||||
<PlatformActionButton
|
||||
surface="profile"
|
||||
size="xs"
|
||||
<PlatformRechargeActionButton
|
||||
className="mt-4 disabled:cursor-not-allowed"
|
||||
onClick={onConfirm}
|
||||
disabled={nativePayment.isConfirming || remaining.isExpired}
|
||||
@@ -226,18 +436,17 @@ function PlatformProfileWechatNativePaymentModal({
|
||||
: remaining.isExpired
|
||||
? '已过期'
|
||||
: '我已支付'}
|
||||
</PlatformActionButton>
|
||||
</PlatformRechargeActionButton>
|
||||
{nativePayment.confirmMessage ? (
|
||||
<div className="mt-3 text-xs font-semibold text-[var(--platform-text-soft)]">
|
||||
{nativePayment.confirmMessage}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</PlatformProfileModalShell>
|
||||
</PlatformProfileRechargeModalShell>
|
||||
);
|
||||
}
|
||||
|
||||
/** 主站与编辑器共用的泥点购买弹窗。 */
|
||||
export function PlatformProfileRechargeModal({
|
||||
center,
|
||||
isLoading,
|
||||
@@ -259,7 +468,7 @@ export function PlatformProfileRechargeModal({
|
||||
|
||||
return (
|
||||
<>
|
||||
<PlatformProfileModalShell
|
||||
<PlatformProfileRechargeModalShell
|
||||
title="购买更多泥点"
|
||||
description={
|
||||
currentBalance === undefined
|
||||
@@ -269,33 +478,26 @@ export function PlatformProfileRechargeModal({
|
||||
onClose={onClose}
|
||||
closeLabel="关闭购买更多泥点"
|
||||
size="md"
|
||||
panelClassName="platform-recharge-modal !max-w-[34rem] rounded-[1.4rem]"
|
||||
panelClassName="!max-w-[34rem] rounded-[1.4rem]"
|
||||
bodyClassName="max-h-[min(76vh,36rem)] overflow-y-auto px-5 py-5"
|
||||
>
|
||||
<PlatformAsyncStatePanel
|
||||
<PlatformRechargeAsyncStatePanel
|
||||
errorState={
|
||||
error ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="profile"
|
||||
size="xs"
|
||||
className="mt-4 rounded-2xl font-semibold"
|
||||
>
|
||||
<PlatformRechargeStatusMessage className="mt-4 rounded-2xl font-semibold">
|
||||
<div>{error}</div>
|
||||
<PlatformActionButton
|
||||
surface="profile"
|
||||
size="xs"
|
||||
<PlatformRechargeActionButton
|
||||
className="mt-3"
|
||||
onClick={onRetry}
|
||||
>
|
||||
重新加载
|
||||
</PlatformActionButton>
|
||||
</PlatformStatusMessage>
|
||||
</PlatformRechargeActionButton>
|
||||
</PlatformRechargeStatusMessage>
|
||||
) : null
|
||||
}
|
||||
isLoading={isLoading}
|
||||
loadingState={
|
||||
<PlatformProfileSkeletonList
|
||||
<PlatformRechargeSkeletonList
|
||||
count={4}
|
||||
containerClassName="grid gap-2.5"
|
||||
itemClassName="h-[4.5rem] rounded-lg bg-white/10"
|
||||
@@ -303,13 +505,9 @@ export function PlatformProfileRechargeModal({
|
||||
}
|
||||
isEmpty={products.length === 0}
|
||||
emptyState={
|
||||
<PlatformEmptyState
|
||||
surface="subpanel"
|
||||
size="inline"
|
||||
className="mt-1"
|
||||
>
|
||||
<PlatformRechargeEmptyState className="mt-1">
|
||||
暂无可购买套餐
|
||||
</PlatformEmptyState>
|
||||
</PlatformRechargeEmptyState>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-2.5">
|
||||
@@ -322,8 +520,8 @@ export function PlatformProfileRechargeModal({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</PlatformAsyncStatePanel>
|
||||
</PlatformProfileModalShell>
|
||||
</PlatformRechargeAsyncStatePanel>
|
||||
</PlatformProfileRechargeModalShell>
|
||||
{nativePayment ? (
|
||||
<PlatformProfileWechatNativePaymentModal
|
||||
nativePayment={nativePayment}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './components/PlatformMudPointWalletEntry';
|
||||
export * from './components/PlatformProfileRechargeModal';
|
||||
export * from './contracts/auth';
|
||||
export type * from './contracts/bigFish';
|
||||
export * from './contracts/common';
|
||||
|
||||
@@ -8,3 +8,8 @@ export function formatMudPointCount(value: number, compact = false) {
|
||||
}
|
||||
return normalizedValue.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
export function formatRechargePrice(priceCents: number) {
|
||||
const yuan = priceCents / 100;
|
||||
return `¥${Number.isInteger(yuan) ? yuan.toFixed(0) : yuan.toFixed(2)}`;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { shouldShowRechargeEntry } from '../../services/payment/paymentPlatform'
|
||||
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog';
|
||||
import { PlatformProfileRechargeModal } from '../platform-entry/PlatformProfileRechargeModal';
|
||||
import { PlatformProfileRechargeModal } from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
|
||||
import { PlatformProfileRewardCodeRedeemModal } from '../platform-entry/PlatformProfileRewardCodeRedeemModal';
|
||||
import { PlatformProfileWalletLedgerModal } from '../platform-entry/PlatformProfileWalletLedgerModal';
|
||||
import {
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
type ProfileTaskCenterResponse,
|
||||
type ProfileWalletLedgerResponse,
|
||||
type RedeemProfileRewardCodeResponse,
|
||||
type WechatNativePayment,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { PlatformProfileRechargeNativePaymentState } from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
|
||||
import {
|
||||
clearStoredAccessToken,
|
||||
refreshStoredAccessToken,
|
||||
@@ -85,13 +85,8 @@ export type WechatRechargeOrderConfirmationState = {
|
||||
orderId: string;
|
||||
};
|
||||
|
||||
export type NativeWechatPaymentState = WechatNativePayment & {
|
||||
orderId: string;
|
||||
productTitle: string;
|
||||
amountCents: number;
|
||||
isConfirming: boolean;
|
||||
confirmMessage?: string;
|
||||
};
|
||||
export type NativeWechatPaymentState =
|
||||
PlatformProfileRechargeNativePaymentState;
|
||||
|
||||
function isWechatJsapiMissingIdentityError(error: unknown) {
|
||||
return (
|
||||
|
||||
@@ -132,7 +132,7 @@ import {
|
||||
ProfileStatCardSkeleton,
|
||||
} from '../platform-entry/PlatformProfilePrimitives';
|
||||
import { PlatformProfileQrScannerModal } from '../platform-entry/PlatformProfileQrScannerModal';
|
||||
import { PlatformProfileRechargeModal } from '../platform-entry/PlatformProfileRechargeModal';
|
||||
import { PlatformProfileRechargeModal } from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
|
||||
import { PlatformProfileReferralModal } from '../platform-entry/PlatformProfileReferralModal';
|
||||
import { PlatformProfileRewardCodeRedeemModal } from '../platform-entry/PlatformProfileRewardCodeRedeemModal';
|
||||
import { PlatformProfileWalletLedgerModal } from '../platform-entry/PlatformProfileWalletLedgerModal';
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ProfileWalletLedgerEntry,
|
||||
ProfileWalletLedgerResponse,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
export { formatRechargePrice } from '../../../packages/shared/src/utils/format';
|
||||
|
||||
const PROFILE_WALLET_LEDGER_SOURCE_LABELS = {
|
||||
new_user_registration_reward: '注册赠送',
|
||||
@@ -84,11 +85,6 @@ export function buildWalletLedgerPresentation(
|
||||
};
|
||||
}
|
||||
|
||||
export function formatRechargePrice(priceCents: number) {
|
||||
const yuan = priceCents / 100;
|
||||
return `¥${Number.isInteger(yuan) ? yuan.toFixed(0) : yuan.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function formatMembershipPeriodPrefix(periodDays: number) {
|
||||
if (periodDays === 30) {
|
||||
return '每月';
|
||||
|
||||
@@ -9398,14 +9398,6 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.platform-recharge-modal {
|
||||
border: 1px solid var(--platform-modal-border);
|
||||
background: var(--platform-modal-fill);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
0 24px 80px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.platform-overlay {
|
||||
background: var(--platform-overlay-fill);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user