更新管理后台布局,优化菜单项标签,新增支付配置项。同时,调整API响应字段命名,确保一致性,提升代码可读性和维护性。
This commit is contained in:
@@ -39,20 +39,20 @@ interface UserDetail {
|
||||
phone?: string
|
||||
nickname: string
|
||||
avatar?: string
|
||||
wechat_id?: string
|
||||
open_id?: string
|
||||
referral_code: string
|
||||
referred_by?: string
|
||||
has_full_book?: boolean
|
||||
is_admin?: boolean
|
||||
wechatId?: string
|
||||
openId?: string
|
||||
referralCode?: string
|
||||
referredBy?: string
|
||||
hasFullBook?: boolean
|
||||
isAdmin?: boolean
|
||||
earnings?: number
|
||||
pending_earnings?: number
|
||||
referral_count?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
pendingEarnings?: number
|
||||
referralCount?: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
tags?: string
|
||||
ckb_tags?: string
|
||||
ckb_synced_at?: string
|
||||
ckbTags?: string
|
||||
ckbSyncedAt?: string
|
||||
}
|
||||
|
||||
interface UserTrack {
|
||||
@@ -234,19 +234,19 @@ export function UserDetailModal({
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-bold text-white">{user.nickname}</h3>
|
||||
{user.is_admin && (
|
||||
{user.isAdmin && (
|
||||
<Badge className="bg-purple-500/20 text-purple-400 border-0">管理员</Badge>
|
||||
)}
|
||||
{user.has_full_book && (
|
||||
{user.hasFullBook && (
|
||||
<Badge className="bg-green-500/20 text-green-400 border-0">全书已购</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
{user.phone ? `📱 ${user.phone}` : '未绑定手机'}
|
||||
{user.wechat_id && ` · 💬 ${user.wechat_id}`}
|
||||
{user.wechatId && ` · 💬 ${user.wechatId}`}
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
ID: {user.id} · 推广码: {user.referral_code}
|
||||
ID: {user.id} · 推广码: {user.referralCode ?? '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
@@ -295,18 +295,18 @@ export function UserDetailModal({
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||
<p className="text-gray-400 text-sm">推荐人数</p>
|
||||
<p className="text-2xl font-bold text-white">{user.referral_count || 0}</p>
|
||||
<p className="text-2xl font-bold text-white">{user.referralCount ?? 0}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||
<p className="text-gray-400 text-sm">待提现</p>
|
||||
<p className="text-2xl font-bold text-yellow-400">
|
||||
¥{(user.pending_earnings || 0).toFixed(2)}
|
||||
¥{(user.pendingEarnings ?? 0).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||
<p className="text-gray-400 text-sm">创建时间</p>
|
||||
<p className="text-sm text-white">
|
||||
{user.created_at ? new Date(user.created_at).toLocaleDateString() : '-'}
|
||||
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -336,7 +336,7 @@ export function UserDetailModal({
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">同步状态:</span>
|
||||
{user.ckb_synced_at ? (
|
||||
{user.ckbSyncedAt ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 border-0 ml-1">已同步</Badge>
|
||||
) : (
|
||||
<Badge className="bg-gray-500/20 text-gray-400 border-0 ml-1">未同步</Badge>
|
||||
@@ -345,7 +345,7 @@ export function UserDetailModal({
|
||||
<div>
|
||||
<span className="text-gray-500">最后同步:</span>
|
||||
<span className="text-gray-300 ml-1">
|
||||
{user.ckb_synced_at ? new Date(user.ckb_synced_at).toLocaleString() : '-'}
|
||||
{user.ckbSyncedAt ? new Date(user.ckbSyncedAt).toLocaleString() : '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,10 +119,10 @@ function buildTree(sections: SectionListItem[]): Part[] {
|
||||
}))
|
||||
}
|
||||
|
||||
function parseTxtToJson(content: string, fileName: string): { id: string; title: string; price: number; content?: string; is_free?: boolean }[] {
|
||||
function parseTxtToJson(content: string, fileName: string): { id: string; title: string; price: number; content?: string; isFree?: boolean }[] {
|
||||
const lines = content.split('\n')
|
||||
const sections: { id: string; title: string; price: number; content?: string; is_free?: boolean }[] = []
|
||||
let currentSection: { id: string; title: string; price: number; content?: string; is_free?: boolean } | null = null
|
||||
const sections: { id: string; title: string; price: number; content?: string; isFree?: boolean }[] = []
|
||||
let currentSection: { id: string; title: string; price: number; content?: string; isFree?: boolean } | null = null
|
||||
let currentContent: string[] = []
|
||||
let sectionIndex = 1
|
||||
|
||||
@@ -137,7 +137,7 @@ function parseTxtToJson(content: string, fileName: string): { id: string; title:
|
||||
id: `import-${sectionIndex}`,
|
||||
title: titleMatch[1].replace(/^#+\s*/, '').trim(),
|
||||
price: 1,
|
||||
is_free: sectionIndex <= 3,
|
||||
isFree: sectionIndex <= 3,
|
||||
}
|
||||
currentContent = []
|
||||
sectionIndex++
|
||||
@@ -148,7 +148,7 @@ function parseTxtToJson(content: string, fileName: string): { id: string; title:
|
||||
id: `import-${sectionIndex}`,
|
||||
title: fileName.replace(/\.(txt|md|markdown)$/i, ''),
|
||||
price: 1,
|
||||
is_free: true,
|
||||
isFree: true,
|
||||
}
|
||||
currentContent.push(line)
|
||||
sectionIndex++
|
||||
|
||||
@@ -23,7 +23,7 @@ interface UserRow {
|
||||
id: string
|
||||
nickname?: string
|
||||
phone?: string
|
||||
referral_code?: string
|
||||
referralCode?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ export function DashboardPage() {
|
||||
: undefined
|
||||
const inviteCode =
|
||||
p.referralCode ||
|
||||
referrer?.referral_code ||
|
||||
referrer?.referralCode ||
|
||||
referrer?.nickname ||
|
||||
(p.referrerId ? String(p.referrerId).slice(0, 8) : '')
|
||||
const product = formatOrderProduct(p)
|
||||
|
||||
@@ -42,24 +42,22 @@ interface DistributionOverview {
|
||||
|
||||
interface Binding {
|
||||
id: string
|
||||
referrer_id: string
|
||||
referrer_name?: string
|
||||
referrer_code: string
|
||||
referee_id: string
|
||||
referee_phone?: string
|
||||
referee_nickname?: string
|
||||
bound_at: string
|
||||
expires_at: string
|
||||
referrerId: string
|
||||
referrerName?: string
|
||||
referrerCode: string
|
||||
refereeId: string
|
||||
refereePhone?: string
|
||||
refereeNickname?: string
|
||||
boundAt: string
|
||||
expiresAt: string
|
||||
status: 'active' | 'converted' | 'expired' | 'cancelled'
|
||||
commission?: number
|
||||
}
|
||||
|
||||
interface Withdrawal {
|
||||
id: string
|
||||
user_id?: string
|
||||
userId?: string
|
||||
user_name?: string
|
||||
userNickname?: string
|
||||
userName?: string
|
||||
userPhone?: string
|
||||
userAvatar?: string
|
||||
amount: number
|
||||
@@ -67,17 +65,15 @@ interface Withdrawal {
|
||||
account?: string
|
||||
name?: string
|
||||
status: string
|
||||
created_at?: string
|
||||
createdAt?: string
|
||||
processedAt?: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
nickname: string
|
||||
phone: string
|
||||
referral_code: string
|
||||
referralCode?: string
|
||||
}
|
||||
|
||||
interface Order {
|
||||
@@ -165,7 +161,7 @@ export function DistributionPage() {
|
||||
userNickname: user?.nickname || order.userNickname || '未知用户',
|
||||
userPhone: user?.phone || order.userPhone || '-',
|
||||
referrerNickname: referrer?.nickname || null,
|
||||
referrerCode: referrer?.referral_code || null,
|
||||
referrerCode: referrer?.referralCode ?? null,
|
||||
type: order.productType || order.type,
|
||||
}
|
||||
})
|
||||
@@ -197,9 +193,6 @@ export function DistributionPage() {
|
||||
if (withdrawalsData?.success && withdrawalsData.withdrawals) {
|
||||
const formatted = withdrawalsData.withdrawals.map((w) => ({
|
||||
...w,
|
||||
user_name: w.userNickname ?? w.user_name,
|
||||
created_at: w.created_at ?? w.createdAt,
|
||||
completed_at: w.processedAt ?? w.completed_at,
|
||||
account: w.account ?? '未绑定微信号',
|
||||
status:
|
||||
w.status === 'success' ? 'completed' : w.status === 'failed' ? 'rejected' : w.status,
|
||||
@@ -294,10 +287,10 @@ export function DistributionPage() {
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase()
|
||||
return (
|
||||
b.referee_nickname?.toLowerCase().includes(term) ||
|
||||
b.referee_phone?.includes(term) ||
|
||||
b.referrer_name?.toLowerCase().includes(term) ||
|
||||
b.referrer_code?.toLowerCase().includes(term)
|
||||
b.refereeNickname?.toLowerCase().includes(term) ||
|
||||
b.refereePhone?.includes(term) ||
|
||||
b.referrerName?.toLowerCase().includes(term) ||
|
||||
b.referrerCode?.toLowerCase().includes(term)
|
||||
)
|
||||
}
|
||||
return true
|
||||
@@ -308,7 +301,7 @@ export function DistributionPage() {
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase()
|
||||
return (
|
||||
w.user_name?.toLowerCase().includes(term) || w.account?.toLowerCase().includes(term)
|
||||
w.userName?.toLowerCase().includes(term) || w.account?.toLowerCase().includes(term)
|
||||
)
|
||||
}
|
||||
return true
|
||||
@@ -776,27 +769,27 @@ export function DistributionPage() {
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<p className="text-white font-medium">
|
||||
{binding.referee_nickname || '匿名用户'}
|
||||
{binding.refereeNickname || '匿名用户'}
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs">{binding.referee_phone}</p>
|
||||
<p className="text-gray-500 text-xs">{binding.refereePhone}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<p className="text-white">{binding.referrer_name || '-'}</p>
|
||||
<p className="text-white">{binding.referrerName || '-'}</p>
|
||||
<p className="text-gray-500 text-xs font-mono">
|
||||
{binding.referrer_code}
|
||||
{binding.referrerCode}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{binding.bound_at
|
||||
? new Date(binding.bound_at).toLocaleDateString('zh-CN')
|
||||
{binding.boundAt
|
||||
? new Date(binding.boundAt).toLocaleDateString('zh-CN')
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{binding.expires_at
|
||||
? new Date(binding.expires_at).toLocaleDateString('zh-CN')
|
||||
{binding.expiresAt
|
||||
? new Date(binding.expiresAt).toLocaleDateString('zh-CN')
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="p-4">{getStatusBadge(binding.status)}</td>
|
||||
@@ -874,11 +867,11 @@ export function DistributionPage() {
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-gray-600 flex items-center justify-center text-white text-sm font-medium">
|
||||
{(withdrawal.user_name || withdrawal.name || '?').slice(0, 1)}
|
||||
{(withdrawal.userName || withdrawal.name || '?').slice(0, 1)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-white font-medium">
|
||||
{withdrawal.user_name || withdrawal.name}
|
||||
{withdrawal.userName || withdrawal.name}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
@@ -907,10 +900,8 @@ export function DistributionPage() {
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{(withdrawal.created_at || withdrawal.createdAt)
|
||||
? new Date(
|
||||
withdrawal.created_at || withdrawal.createdAt || '',
|
||||
).toLocaleString('zh-CN')
|
||||
{withdrawal.createdAt
|
||||
? new Date(withdrawal.createdAt).toLocaleString('zh-CN')
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="p-4">{getStatusBadge(withdrawal.status)}</td>
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ReferralSettingsPage() {
|
||||
}
|
||||
const body = {
|
||||
key: 'referral_config',
|
||||
config: safeConfig,
|
||||
value: safeConfig,
|
||||
description: '分销 / 推广规则配置',
|
||||
}
|
||||
const res = await post<{ success?: boolean; error?: string }>('/api/db/config', body)
|
||||
|
||||
@@ -115,13 +115,13 @@ function mergeFromConfigList(list: unknown[]): ReturnType<typeof parseConfigResp
|
||||
const out: ReturnType<typeof parseConfigResponse> = {}
|
||||
for (const item of list) {
|
||||
if (!item || typeof item !== 'object') continue
|
||||
const row = item as { config_key?: string; config_value?: string }
|
||||
const key = row.config_key
|
||||
const row = item as { configKey?: string; configValue?: string }
|
||||
const key = row.configKey
|
||||
let val: unknown
|
||||
try {
|
||||
val = typeof row.config_value === 'string' ? JSON.parse(row.config_value) : row.config_value
|
||||
val = typeof row.configValue === 'string' ? JSON.parse(row.configValue) : row.configValue
|
||||
} catch {
|
||||
val = row.config_value
|
||||
val = row.configValue
|
||||
}
|
||||
if (key === 'feature_config' && val && typeof val === 'object') out.features = val as Partial<FeatureConfig>
|
||||
if (key === 'mp_config' && val && typeof val === 'object') out.mpConfig = val as Partial<MpConfig>
|
||||
@@ -205,7 +205,6 @@ export function SettingsPage() {
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await post('/api/db/settings', localSettings).catch(() => {})
|
||||
await post('/api/db/config', {
|
||||
key: 'free_chapters',
|
||||
value: freeChapters,
|
||||
|
||||
@@ -37,20 +37,20 @@ import { get, del, post, put } from '@/api/client'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
open_id?: string | null
|
||||
openId?: string | null
|
||||
phone?: string | null
|
||||
nickname: string
|
||||
wechat_id?: string | null
|
||||
wechatId?: string | null
|
||||
avatar?: string | null
|
||||
is_admin?: boolean | number
|
||||
has_full_book?: boolean | number
|
||||
referral_code: string
|
||||
isAdmin?: boolean | number
|
||||
hasFullBook?: boolean | number
|
||||
referralCode?: string
|
||||
earnings: number | string
|
||||
pending_earnings?: number | string
|
||||
withdrawn_earnings?: number | string
|
||||
referral_count: number
|
||||
created_at: string
|
||||
updated_at?: string | null
|
||||
pendingEarnings?: number | string
|
||||
withdrawnEarnings?: number | string
|
||||
referralCount?: number
|
||||
createdAt: string
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
export function UsersPage() {
|
||||
@@ -77,8 +77,8 @@ export function UsersPage() {
|
||||
phone: '',
|
||||
nickname: '',
|
||||
password: '',
|
||||
is_admin: false,
|
||||
has_full_book: false,
|
||||
isAdmin: false,
|
||||
hasFullBook: false,
|
||||
})
|
||||
|
||||
async function loadUsers() {
|
||||
@@ -126,8 +126,8 @@ export function UsersPage() {
|
||||
phone: user.phone || '',
|
||||
nickname: user.nickname || '',
|
||||
password: '',
|
||||
is_admin: !!(user.is_admin ?? false),
|
||||
has_full_book: !!(user.has_full_book ?? false),
|
||||
isAdmin: !!(user.isAdmin ?? false),
|
||||
hasFullBook: !!(user.hasFullBook ?? false),
|
||||
})
|
||||
setShowUserModal(true)
|
||||
}
|
||||
@@ -138,8 +138,8 @@ export function UsersPage() {
|
||||
phone: '',
|
||||
nickname: '',
|
||||
password: '',
|
||||
is_admin: false,
|
||||
has_full_book: false,
|
||||
isAdmin: false,
|
||||
hasFullBook: false,
|
||||
})
|
||||
setShowUserModal(true)
|
||||
}
|
||||
@@ -155,8 +155,8 @@ export function UsersPage() {
|
||||
const data = await put<{ success?: boolean; error?: string }>('/api/db/users', {
|
||||
id: editingUser.id,
|
||||
nickname: formData.nickname,
|
||||
is_admin: formData.is_admin,
|
||||
has_full_book: formData.has_full_book,
|
||||
isAdmin: formData.isAdmin,
|
||||
hasFullBook: formData.hasFullBook,
|
||||
...(formData.password && { password: formData.password }),
|
||||
})
|
||||
if (!data?.success) {
|
||||
@@ -168,7 +168,7 @@ export function UsersPage() {
|
||||
phone: formData.phone,
|
||||
nickname: formData.nickname,
|
||||
password: formData.password,
|
||||
is_admin: formData.is_admin,
|
||||
isAdmin: formData.isAdmin,
|
||||
})
|
||||
if (!data?.success) {
|
||||
alert('创建失败: ' + (data?.error || '未知错误'))
|
||||
@@ -329,15 +329,15 @@ export function UsersPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-gray-300">管理员权限</Label>
|
||||
<Switch
|
||||
checked={formData.is_admin}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, is_admin: checked })}
|
||||
checked={formData.isAdmin}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, isAdmin: checked })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-gray-300">已购全书</Label>
|
||||
<Switch
|
||||
checked={formData.has_full_book}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, has_full_book: checked })}
|
||||
checked={formData.hasFullBook}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, hasFullBook: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -559,19 +559,19 @@ export function UsersPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium text-white">{user.nickname}</p>
|
||||
{user.is_admin && (
|
||||
{user.isAdmin && (
|
||||
<Badge className="bg-purple-500/20 text-purple-400 hover:bg-purple-500/20 border-0 text-xs">
|
||||
管理员
|
||||
</Badge>
|
||||
)}
|
||||
{user.open_id && !user.id?.startsWith('user_') && (
|
||||
{user.openId && !user.id?.startsWith('user_') && (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0 text-xs">
|
||||
微信
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 font-mono">
|
||||
{user.open_id ? user.open_id.slice(0, 12) + '...' : user.id?.slice(0, 12)}
|
||||
{user.openId ? user.openId.slice(0, 12) + '...' : user.id?.slice(0, 12)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -584,27 +584,27 @@ export function UsersPage() {
|
||||
<span className="text-gray-300">{user.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
{user.wechat_id && (
|
||||
{user.wechatId && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<span className="text-gray-500">💬</span>
|
||||
<span className="text-gray-300">{user.wechat_id}</span>
|
||||
<span className="text-gray-300">{user.wechatId}</span>
|
||||
</div>
|
||||
)}
|
||||
{user.open_id && (
|
||||
{user.openId && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<span className="text-gray-500">🔗</span>
|
||||
<span className="text-gray-500 truncate max-w-[100px]" title={user.open_id}>
|
||||
{user.open_id.slice(0, 12)}...
|
||||
<span className="text-gray-500 truncate max-w-[100px]" title={user.openId}>
|
||||
{user.openId.slice(0, 12)}...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!user.phone && !user.wechat_id && !user.open_id && (
|
||||
{!user.phone && !user.wechatId && !user.openId && (
|
||||
<span className="text-gray-600 text-xs">未绑定</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.has_full_book ? (
|
||||
{user.hasFullBook ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
|
||||
全书已购
|
||||
</Badge>
|
||||
@@ -619,9 +619,9 @@ export function UsersPage() {
|
||||
<div className="text-white font-medium">
|
||||
¥{parseFloat(String(user.earnings || 0)).toFixed(2)}
|
||||
</div>
|
||||
{parseFloat(String(user.pending_earnings || 0)) > 0 && (
|
||||
{parseFloat(String(user.pendingEarnings || 0)) > 0 && (
|
||||
<div className="text-xs text-yellow-400">
|
||||
待提现: ¥{parseFloat(String(user.pending_earnings || 0)).toFixed(2)}
|
||||
待提现: ¥{parseFloat(String(user.pendingEarnings || 0)).toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
@@ -632,17 +632,17 @@ export function UsersPage() {
|
||||
tabIndex={0}
|
||||
>
|
||||
<Users className="w-3 h-3" />
|
||||
绑定{user.referral_count || 0}人
|
||||
绑定{user.referralCount || 0}人
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-[#38bdac] text-xs bg-[#38bdac]/10 px-2 py-0.5 rounded">
|
||||
{user.referral_code || '-'}
|
||||
{user.referralCode || '-'}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-400">
|
||||
{user.created_at ? new Date(user.created_at).toLocaleDateString() : '-'}
|
||||
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
|
||||
@@ -7,10 +7,8 @@ import { get, put } from '@/api/client'
|
||||
|
||||
interface Withdrawal {
|
||||
id: string
|
||||
user_id?: string
|
||||
userId?: string
|
||||
userNickname?: string
|
||||
user_name?: string
|
||||
userName?: string
|
||||
userPhone?: string
|
||||
userAvatar?: string
|
||||
referralCode?: string
|
||||
@@ -20,9 +18,7 @@ interface Withdrawal {
|
||||
transactionId?: string
|
||||
errorMessage?: string
|
||||
createdAt?: string
|
||||
created_at?: string
|
||||
processedAt?: string
|
||||
completed_at?: string
|
||||
method?: 'wechat' | 'alipay'
|
||||
account?: string
|
||||
name?: string
|
||||
@@ -66,11 +62,7 @@ export function WithdrawalsPage() {
|
||||
stats?: Partial<Stats>
|
||||
}>(`/api/admin/withdrawals?status=${filter}`)
|
||||
if (data?.success) {
|
||||
const list = (data.withdrawals || []).map((w) => ({
|
||||
...w,
|
||||
createdAt: w.created_at ?? w.createdAt,
|
||||
userNickname: w.user_name ?? w.userNickname,
|
||||
}))
|
||||
const list = data.withdrawals || []
|
||||
setWithdrawals(list)
|
||||
setStats({
|
||||
total: data.stats?.total ?? list.length,
|
||||
@@ -130,7 +122,7 @@ export function WithdrawalsPage() {
|
||||
try {
|
||||
const data = await put<{ success?: boolean; error?: string }>(
|
||||
'/api/admin/withdrawals',
|
||||
{ id, action: 'reject', reason },
|
||||
{ id, action: 'reject', errorMessage: reason },
|
||||
)
|
||||
if (data?.success) loadWithdrawals()
|
||||
else alert('操作失败: ' + (data?.error ?? ''))
|
||||
@@ -310,27 +302,27 @@ export function WithdrawalsPage() {
|
||||
{withdrawals.map((w) => (
|
||||
<tr key={w.id} className="hover:bg-[#0a1628] transition-colors">
|
||||
<td className="p-4 text-gray-400">
|
||||
{new Date(w.created_at ?? w.createdAt ?? '').toLocaleString()}
|
||||
{new Date(w.createdAt ?? '').toLocaleString()}
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{w.userAvatar ? (
|
||||
<img
|
||||
src={w.userAvatar}
|
||||
alt={w.userNickname ?? w.user_name ?? ''}
|
||||
alt={w.userName ?? ''}
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm text-[#38bdac]">
|
||||
{(w.userNickname ?? w.user_name ?? '?').charAt(0)}
|
||||
{(w.userName ?? '?').charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
{w.userNickname ?? w.user_name ?? '未知'}
|
||||
{w.userName ?? '未知'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{w.userPhone ?? w.referralCode ?? (w.user_id ?? w.userId ?? '').slice(0, 10)}
|
||||
{w.userPhone ?? w.referralCode ?? (w.userId ?? '').slice(0, 10)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -385,9 +377,7 @@ export function WithdrawalsPage() {
|
||||
)}
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{(w.processedAt ?? w.completed_at)
|
||||
? new Date(w.processedAt ?? w.completed_at ?? '').toLocaleString()
|
||||
: '-'}
|
||||
{w.processedAt ? new Date(w.processedAt).toLocaleString() : '-'}
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
{(w.status === 'pending' || w.status === 'pending_confirm') && (
|
||||
|
||||
Reference in New Issue
Block a user