优化小程序阅读页面逻辑,动态更新章节价格和免费状态,确保用户体验流畅。移除硬编码价格,支持通过接口返回的值进行展示。更新用户地址管理逻辑,简化获取地址的流程,提升代码可读性。
This commit is contained in:
@@ -289,8 +289,7 @@ export function DashboardPage() {
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{users
|
||||
.slice(-5)
|
||||
.reverse()
|
||||
.slice(0, 5)
|
||||
.map((u) => (
|
||||
<div
|
||||
key={u.id}
|
||||
|
||||
@@ -73,6 +73,7 @@ interface User {
|
||||
id: string
|
||||
nickname: string
|
||||
phone: string
|
||||
avatar?: string | null
|
||||
referralCode?: string
|
||||
}
|
||||
|
||||
@@ -81,6 +82,7 @@ interface Order {
|
||||
userId: string
|
||||
userNickname?: string
|
||||
userPhone?: string
|
||||
userAvatar?: string | null
|
||||
productType?: string
|
||||
type?: string
|
||||
productId?: string
|
||||
@@ -122,6 +124,7 @@ export function DistributionPage() {
|
||||
}, [activeTab])
|
||||
|
||||
async function loadInitialData() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const overviewData = await get<{ success?: boolean; overview?: DistributionOverview }>(
|
||||
'/api/admin/distribution/overview',
|
||||
@@ -135,6 +138,8 @@ export function DistributionPage() {
|
||||
setUsers(usersData?.users || [])
|
||||
} catch (e) {
|
||||
console.error('[Admin] 用户数据加载失败:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,21 +155,16 @@ export function DistributionPage() {
|
||||
try {
|
||||
const ordersData = await get<{ success?: boolean; orders?: Order[] }>('/api/orders')
|
||||
if (ordersData?.success && ordersData.orders) {
|
||||
const enriched = ordersData.orders.map((order) => {
|
||||
const user = usersArr.find((u) => u.id === order.userId)
|
||||
const referrer = order.referrerId
|
||||
? usersArr.find((u) => u.id === order.referrerId)
|
||||
: null
|
||||
return {
|
||||
...order,
|
||||
amount: parseFloat(String(order.amount)) || 0,
|
||||
userNickname: user?.nickname || order.userNickname || '未知用户',
|
||||
userPhone: user?.phone || order.userPhone || '-',
|
||||
referrerNickname: referrer?.nickname || null,
|
||||
referrerCode: referrer?.referralCode ?? null,
|
||||
type: order.productType || order.type,
|
||||
}
|
||||
})
|
||||
const enriched = ordersData.orders.map((order) => ({
|
||||
...order,
|
||||
amount: parseFloat(String(order.amount)) || 0,
|
||||
userNickname: order.userNickname ?? usersArr.find((u) => u.id === order.userId)?.nickname ?? '未知用户',
|
||||
userPhone: order.userPhone || usersArr.find((u) => u.id === order.userId)?.phone || '-',
|
||||
userAvatar: order.userAvatar ?? usersArr.find((u) => u.id === order.userId)?.avatar ?? null,
|
||||
referrerNickname: order.referrerNickname ?? (order.referrerId ? usersArr.find((u) => u.id === order.referrerId)?.nickname : null) ?? null,
|
||||
referrerCode: order.referrerCode ?? (order.referrerId ? usersArr.find((u) => u.id === order.referrerId)?.referralCode : null) ?? null,
|
||||
type: order.productType || order.type,
|
||||
}))
|
||||
setOrders(enriched)
|
||||
} else setOrders([])
|
||||
} catch {
|
||||
@@ -217,7 +217,8 @@ export function DistributionPage() {
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
// 概览数据由 loadInitialData 控制 loading,避免一进页就被这里立刻关掉
|
||||
if (tab !== 'overview') setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,9 +648,18 @@ export function DistributionPage() {
|
||||
{order.id?.slice(0, 12)}...
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<p className="text-white text-sm">{order.userNickname}</p>
|
||||
<p className="text-gray-500 text-xs">{order.userPhone}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm text-[#38bdac] overflow-hidden shrink-0">
|
||||
{order.userAvatar ? (
|
||||
<img src={order.userAvatar} className="w-full h-full object-cover" alt="" />
|
||||
) : (
|
||||
(order.userNickname || '?').charAt(0)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white text-sm">{order.userNickname || '未知用户'}</p>
|
||||
<p className="text-gray-500 text-xs">{order.userPhone || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import {
|
||||
Search,
|
||||
UserPlus,
|
||||
Trash2,
|
||||
Edit3,
|
||||
Key,
|
||||
@@ -31,6 +30,8 @@ import {
|
||||
RefreshCw,
|
||||
Users,
|
||||
Eye,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
import { UserDetailModal } from '@/components/modules/user/UserDetailModal'
|
||||
import { get, del, post, put } from '@/api/client'
|
||||
@@ -49,6 +50,7 @@ interface User {
|
||||
pendingEarnings?: number | string
|
||||
withdrawnEarnings?: number | string
|
||||
referralCount?: number
|
||||
purchasedSectionCount?: number
|
||||
createdAt: string
|
||||
updatedAt?: string | null
|
||||
}
|
||||
@@ -56,6 +58,10 @@ interface User {
|
||||
export function UsersPage() {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize] = useState(15)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [totalPages, setTotalPages] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [, setError] = useState<string | null>(null)
|
||||
const [showUserModal, setShowUserModal] = useState(false)
|
||||
@@ -81,13 +87,29 @@ export function UsersPage() {
|
||||
hasFullBook: false,
|
||||
})
|
||||
|
||||
async function loadUsers() {
|
||||
async function loadUsers(overridePage?: number) {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await get<{ success?: boolean; users?: User[]; error?: string }>('/api/db/users')
|
||||
if (data?.success) setUsers(data.users || [])
|
||||
else setError(data?.error || '加载失败')
|
||||
const params = new URLSearchParams()
|
||||
params.set('page', String(overridePage ?? page))
|
||||
params.set('pageSize', String(pageSize))
|
||||
if (searchTerm.trim()) params.set('search', searchTerm.trim())
|
||||
const data = await get<{
|
||||
success?: boolean
|
||||
users?: User[]
|
||||
total?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
totalPages?: number
|
||||
error?: string
|
||||
}>(`/api/db/users?${params.toString()}`)
|
||||
if (data?.success) {
|
||||
setUsers(data.users || [])
|
||||
setTotal(data.total ?? 0)
|
||||
setTotalPages(data.totalPages ?? 0)
|
||||
if (overridePage != null) setPage(overridePage)
|
||||
} else setError(data?.error || '加载失败')
|
||||
} catch (err) {
|
||||
console.error('Load users error:', err)
|
||||
setError('网络错误,请检查连接')
|
||||
@@ -98,13 +120,11 @@ export function UsersPage() {
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers()
|
||||
}, [])
|
||||
}, [page])
|
||||
|
||||
const filteredUsers = users.filter(
|
||||
(u) =>
|
||||
(u.nickname || '').includes(searchTerm) ||
|
||||
(u.phone || '').includes(searchTerm),
|
||||
)
|
||||
const handleSearch = () => {
|
||||
loadUsers(1)
|
||||
}
|
||||
|
||||
async function handleDelete(userId: string) {
|
||||
if (!confirm('确定要删除这个用户吗?')) return
|
||||
@@ -132,48 +152,24 @@ export function UsersPage() {
|
||||
setShowUserModal(true)
|
||||
}
|
||||
|
||||
const handleAddUser = () => {
|
||||
setEditingUser(null)
|
||||
setFormData({
|
||||
phone: '',
|
||||
nickname: '',
|
||||
password: '',
|
||||
isAdmin: false,
|
||||
hasFullBook: false,
|
||||
})
|
||||
setShowUserModal(true)
|
||||
}
|
||||
|
||||
async function handleSaveUser() {
|
||||
if (!editingUser) return
|
||||
if (!formData.phone || !formData.nickname) {
|
||||
alert('请填写手机号和昵称')
|
||||
return
|
||||
}
|
||||
setIsSaving(true)
|
||||
try {
|
||||
if (editingUser) {
|
||||
const data = await put<{ success?: boolean; error?: string }>('/api/db/users', {
|
||||
id: editingUser.id,
|
||||
nickname: formData.nickname,
|
||||
isAdmin: formData.isAdmin,
|
||||
hasFullBook: formData.hasFullBook,
|
||||
...(formData.password && { password: formData.password }),
|
||||
})
|
||||
if (!data?.success) {
|
||||
alert('更新失败: ' + (data?.error || '未知错误'))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
const data = await post<{ success?: boolean; error?: string }>('/api/db/users', {
|
||||
phone: formData.phone,
|
||||
nickname: formData.nickname,
|
||||
password: formData.password,
|
||||
isAdmin: formData.isAdmin,
|
||||
})
|
||||
if (!data?.success) {
|
||||
alert('创建失败: ' + (data?.error || '未知错误'))
|
||||
return
|
||||
}
|
||||
const data = await put<{ success?: boolean; error?: string }>('/api/db/users', {
|
||||
id: editingUser.id,
|
||||
nickname: formData.nickname,
|
||||
isAdmin: formData.isAdmin,
|
||||
hasFullBook: formData.hasFullBook,
|
||||
...(formData.password && { password: formData.password }),
|
||||
})
|
||||
if (!data?.success) {
|
||||
alert('更新失败: ' + (data?.error || '未知错误'))
|
||||
return
|
||||
}
|
||||
setShowUserModal(false)
|
||||
loadUsers()
|
||||
@@ -253,32 +249,39 @@ export function UsersPage() {
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">用户管理</h2>
|
||||
<p className="text-gray-400 mt-1">共 {users.length} 位注册用户</p>
|
||||
<p className="text-gray-400 mt-1">共 {total} 位注册用户</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={loadUsers}
|
||||
onClick={() => loadUsers()}
|
||||
disabled={isLoading}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索用户..."
|
||||
className="pl-10 bg-[#0f2137] border-gray-700 text-white placeholder:text-gray-500 w-64"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500 pointer-events-none" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索昵称/手机/ID..."
|
||||
className="pl-10 bg-[#0f2137] border-gray-700 text-white placeholder:text-gray-500 w-56"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSearch}
|
||||
disabled={isLoading}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent shrink-0"
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={handleAddUser} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<UserPlus className="w-4 h-4 mr-2" />
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -286,12 +289,8 @@ export function UsersPage() {
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
{editingUser ? (
|
||||
<Edit3 className="w-5 h-5 text-[#38bdac]" />
|
||||
) : (
|
||||
<UserPlus className="w-5 h-5 text-[#38bdac]" />
|
||||
)}
|
||||
{editingUser ? '编辑用户' : '添加用户'}
|
||||
<Edit3 className="w-5 h-5 text-[#38bdac]" />
|
||||
编辑用户
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
@@ -469,6 +468,7 @@ export function UsersPage() {
|
||||
const r = ref as {
|
||||
id?: string
|
||||
nickname?: string
|
||||
avatar?: string | null
|
||||
phone?: string
|
||||
hasOpenId?: boolean
|
||||
status?: string
|
||||
@@ -478,8 +478,12 @@ export function UsersPage() {
|
||||
return (
|
||||
<div key={r.id || i} className="flex items-center justify-between bg-[#0a1628] rounded-lg p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm text-[#38bdac]">
|
||||
{r.nickname?.charAt(0) || '?'}
|
||||
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm text-[#38bdac] overflow-hidden shrink-0">
|
||||
{r.avatar ? (
|
||||
<img src={r.avatar} className="w-full h-full object-cover" alt="" />
|
||||
) : (
|
||||
r.nickname?.charAt(0) || '?'
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white text-sm">{r.nickname}</div>
|
||||
@@ -545,7 +549,7 @@ export function UsersPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id} className="hover:bg-[#0a1628] border-gray-700/50">
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -608,6 +612,10 @@ export function UsersPage() {
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
|
||||
全书已购
|
||||
</Badge>
|
||||
) : (user.purchasedSectionCount ?? 0) > 0 ? (
|
||||
<Badge className="bg-blue-500/20 text-blue-400 hover:bg-blue-500/20 border-0">
|
||||
已付费{(user.purchasedSectionCount ?? 0)}章
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-gray-500 border-gray-600">
|
||||
未购买
|
||||
@@ -686,16 +694,45 @@ export function UsersPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredUsers.length === 0 && (
|
||||
{users.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-12 text-gray-500">
|
||||
暂无用户数据
|
||||
{searchTerm ? '未找到匹配用户' : '暂无用户数据'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{!isLoading && totalPages > 0 && (
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-gray-700/50">
|
||||
<span className="text-sm text-gray-400">
|
||||
第 {page} / {totalPages} 页,共 {total} 条
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
下一页
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user