更新管理后台布局,优化菜单项标签,新增支付配置项。同时,调整API响应字段命名,确保一致性,提升代码可读性和维护性。

This commit is contained in:
乘风
2026-02-09 14:33:41 +08:00
parent bee72dc7f8
commit dfbe3eb427
77 changed files with 3041 additions and 240 deletions

View File

@@ -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>

View File

@@ -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++

View File

@@ -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)

View File

@@ -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>

View File

@@ -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)

View File

@@ -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,

View File

@@ -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">

View File

@@ -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') && (