feat: 渠道展示、订阅套餐、系统配置全功能
- 新增 Channel / SubscriptionPlan / SystemConfig 三个数据模型 - Order 模型扩展支持订阅订单(order_type, plan_id, subscription_group_id) - Sub2API client 新增分组查询、订阅分配/续期、用户订阅查询 - 订单服务支持订阅履约流程(CAS 锁 + 分组消失安全处理) - 管理后台:渠道管理、订阅套餐管理、系统配置、Sub2API 分组同步 - 用户页面:双 Tab UI(按量付费/包月订阅)、渠道卡片、充值弹窗、订阅确认 - PaymentForm 支持 fixedAmount 固定金额模式 - 订单状态 API 返回 failedReason 用于订阅异常展示 - 数据库迁移脚本
This commit is contained in:
113
src/components/TopUpModal.tsx
Normal file
113
src/components/TopUpModal.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import type { Locale } from '@/lib/locale';
|
||||
import { pickLocaleText } from '@/lib/locale';
|
||||
|
||||
interface TopUpModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (amount: number) => void;
|
||||
amounts?: number[];
|
||||
isDark: boolean;
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const DEFAULT_AMOUNTS = [50, 100, 500, 1000];
|
||||
|
||||
export default function TopUpModal({ open, onClose, onConfirm, amounts, isDark, locale }: TopUpModalProps) {
|
||||
const amountOptions = amounts ?? DEFAULT_AMOUNTS;
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selected !== null) {
|
||||
onConfirm(selected);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
|
||||
<div
|
||||
className={[
|
||||
'relative mx-4 w-full max-w-md rounded-2xl border p-6 shadow-2xl',
|
||||
isDark ? 'border-slate-700 bg-slate-900 text-slate-100' : 'border-slate-200 bg-white text-slate-900',
|
||||
].join(' ')}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{pickLocaleText(locale, '选择充值金额', 'Select Amount')}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={[
|
||||
'flex h-8 w-8 items-center justify-center rounded-full transition-colors',
|
||||
isDark ? 'text-slate-400 hover:bg-slate-800 hover:text-slate-200' : 'text-slate-400 hover:bg-slate-100 hover:text-slate-600',
|
||||
].join(' ')}
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Amount grid */}
|
||||
<div className="mb-6 grid grid-cols-2 gap-3">
|
||||
{amountOptions.map((amount) => {
|
||||
const isSelected = selected === amount;
|
||||
return (
|
||||
<button
|
||||
key={amount}
|
||||
type="button"
|
||||
onClick={() => setSelected(amount)}
|
||||
className={[
|
||||
'flex flex-col items-center rounded-xl border-2 px-4 py-4 transition-all',
|
||||
isSelected
|
||||
? 'border-emerald-500 ring-2 ring-emerald-500/30'
|
||||
: isDark
|
||||
? 'border-slate-700 hover:border-slate-600'
|
||||
: 'border-slate-200 hover:border-slate-300',
|
||||
isSelected
|
||||
? isDark
|
||||
? 'bg-emerald-950/40'
|
||||
: 'bg-emerald-50'
|
||||
: isDark
|
||||
? 'bg-slate-800/60'
|
||||
: 'bg-slate-50',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
|
||||
{pickLocaleText(locale, `余额充值${amount}$`, `Balance +${amount}$`)}
|
||||
</span>
|
||||
<span className="mt-1 text-2xl font-bold text-emerald-500">
|
||||
¥{amount}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Confirm button */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={selected === null}
|
||||
onClick={handleConfirm}
|
||||
className={[
|
||||
'w-full rounded-xl py-3 text-sm font-semibold text-white transition-colors',
|
||||
selected !== null
|
||||
? 'bg-emerald-500 hover:bg-emerald-600 active:bg-emerald-700'
|
||||
: isDark
|
||||
? 'cursor-not-allowed bg-slate-700 text-slate-400'
|
||||
: 'cursor-not-allowed bg-slate-200 text-slate-400',
|
||||
].join(' ')}
|
||||
>
|
||||
{pickLocaleText(locale, '确认充值', 'Confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user