@@ -0,0 +1,445 @@
import React , { useState , useEffect , useCallback , useRef } from 'react'
import { View , Text , Image , ScrollView , Input } from '@tarojs/components'
import Taro , { useDidShow } from '@tarojs/taro'
import { pageUsers , updateUserStatus } from '@/api/system/user'
import type { User } from '@/api/system/user/model'
import { getCompressedImageUrl } from '@/utils/image'
definePageConfig ( {
navigationBarTitleText : '用户管理' ,
} )
// ─── Tab 配置 ──────────────────────────────────────────────────
type TabKey = 'all' | 'normal' | 'disabled'
const TABS : { key : TabKey ; label : string ; status? : number } [ ] = [
{ key : 'all' , label : '全部' } ,
{ key : 'normal' , label : '正常' , status : 0 } ,
{ key : 'disabled' , label : '已禁用' , status : 1 } ,
]
// ─── 页面组件 ──────────────────────────────────────────────────────
export default function StoreUsersPage() {
const [ activeTab , setActiveTab ] = useState < TabKey > ( 'all' )
// 用户列表与分页
const [ userList , setUserList ] = useState < User [ ] > ( [ ] )
const [ page , setPage ] = useState ( 1 )
const [ hasMore , setHasMore ] = useState ( true )
const [ loading , setLoading ] = useState ( false )
// 搜索
const [ keyword , setKeyword ] = useState ( '' )
const [ searchText , setSearchText ] = useState ( '' )
// 用户详情弹窗
const [ showDetail , setShowDetail ] = useState ( false )
const [ detailUser , setDetailUser ] = useState < User | null > ( null )
const pageSize = 20
const loadingRef = useRef ( false )
/** 加载用户列表 */
const loadUsers = useCallback ( async ( tab : TabKey , pageNo : number = 1 , append = false , kw? : string ) = > {
if ( loadingRef . current ) return
loadingRef . current = true
setLoading ( true )
try {
const tabConfig = TABS . find ( t = > t . key === tab )
const params : any = {
page : pageNo ,
limit : pageSize ,
}
if ( tabConfig ? . status !== undefined ) {
params . status = tabConfig . status
}
const effectiveKw = kw !== undefined ? kw : keyword
if ( effectiveKw ) params . keywords = effectiveKw
const res = await pageUsers ( params )
const list = res ? . list || [ ]
setUserList ( prev = > append ? [ . . . prev , . . . list ] : list )
setPage ( pageNo )
setHasMore ( list . length >= pageSize )
} catch ( e : any ) {
Taro . showToast ( { title : e.message || '加载失败' , icon : 'none' } )
if ( ! append ) setUserList ( [ ] )
} finally {
loadingRef . current = false
setLoading ( false )
}
} , [ keyword ] )
// 切换 tab 时重新加载
useEffect ( ( ) = > {
loadUsers ( activeTab , 1 )
} , [ activeTab , loadUsers ] )
// 页面重新显示时刷新
useDidShow ( ( ) = > {
loadUsers ( activeTab , 1 )
} )
/** 加载更多 */
const handleLoadMore = ( ) = > {
if ( hasMore && ! loadingRef . current ) {
loadUsers ( activeTab , page + 1 , true )
}
}
/** 执行搜索 */
const handleSearch = ( ) = > {
setKeyword ( searchText )
loadUsers ( activeTab , 1 , false , searchText )
}
/** 清除搜索 */
const handleClearSearch = ( ) = > {
setSearchText ( '' )
setKeyword ( '' )
loadUsers ( activeTab , 1 , false , '' )
}
/** 切换用户状态(禁用/启用) */
const handleToggleStatus = ( user : User ) = > {
const currentStatus = user . status ? ? 0
const newStatus = currentStatus === 0 ? 1 : 0
const actionText = newStatus === 1 ? '禁用' : '启用'
Taro . showModal ( {
title : '确认操作' ,
content : ` 确定要 ${ actionText } 用户「 ${ user . nickname || user . realName || user . phone || user . userId } 」吗? ${ newStatus === 1 ? '禁用后该用户将无法登录。' : '' } ` ,
confirmColor : newStatus === 1 ? '#ee0a24' : '#0e932e' ,
success : async ( res ) = > {
if ( ! res . confirm ) return
try {
Taro . showLoading ( { title : ` ${ actionText } 中... ` } )
await updateUserStatus ( user . userId , newStatus )
Taro . hideLoading ( )
Taro . showToast ( { title : ` ${ actionText } 成功 ` , icon : 'success' } )
// 更新列表和详情中的用户状态
setUserList ( prev = > prev . map ( u = >
u . userId === user . userId ? { . . . u , status : newStatus } : u
) )
if ( detailUser ? . userId === user . userId ) {
setDetailUser ( prev = > prev ? { . . . prev , status : newStatus } : null )
}
} catch ( e : any ) {
Taro . hideLoading ( )
Taro . showToast ( { title : e.message || ` ${ actionText } 失败 ` , icon : 'none' } )
}
} ,
} )
}
/** 打开用户详情 */
const openDetail = ( user : User ) = > {
setDetailUser ( user )
setShowDetail ( true )
}
/** 格式化时间 */
const formatTime = ( time? : string ) = > {
if ( ! time ) return '-'
// 兼容各种时间格式
return time . replace ( 'T' , ' ' ) . substring ( 0 , 19 )
}
/** 渲染单个用户卡片 */
const renderUserCard = ( user : User ) = > {
const isDisabled = user . status === 1
const displayName = user . nickname || user . realName || '未设置昵称'
return (
< View
key = { user . userId || user . id }
className = 'bg-white rounded-xl mx-3 mt-3 p-4 active:opacity-80'
onClick = { ( ) = > openDetail ( user ) }
>
{ /* 用户头部 */ }
< View className = 'flex items-center gap-3' >
{ /* 头像 */ }
< View className = 'w-12 h-12 rounded-full overflow-hidden flex-shrink-0 bg-gray-100' >
{ user . avatar ? (
< Image
className = 'w-full h-full'
src = { getCompressedImageUrl ( user . avatar , { width : 80 , quality : 80 } ) }
mode = 'aspectFill'
/ >
) : (
< View className = 'w-full h-full flex items-center justify-center' >
< Text className = 'text-2xl' > 👤 < / Text >
< / View >
) }
< / View >
{ /* 信息 */ }
< View className = 'flex-1 min-w-0' >
< View className = 'flex items-center gap-2' >
< Text className = 'text-sm text-gray-800 font-medium' style = { { overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
{ displayName }
< / Text >
< View
className = 'px-1.5 py-0.5 rounded text-[10px]'
style = { {
color : isDisabled ? '#dc2626' : '#16a34a' ,
background : isDisabled ? '#fef2f2' : '#f0fdf4' ,
} }
>
{ isDisabled ? '已禁用' : '正常' }
< / View >
< / View >
< Text className = 'text-xs text-gray-400 mt-0.5 block' >
{ user . phone || user . mobile || '未绑定手机' }
< / Text >
< / View >
{ /* 禁用/启用按钮 */ }
< View
className = { ` px-3 py-1.5 rounded-lg flex-shrink-0 ${ isDisabled ? 'bg-green-50' : 'bg-red-50' } ` }
onClick = { ( e ) = > {
e . stopPropagation ( )
handleToggleStatus ( user )
} }
>
< Text className = { ` text-xs ${ isDisabled ? 'text-green-500' : 'text-red-500' } ` } >
{ isDisabled ? '启用' : '禁用' }
< / Text >
< / View >
< / View >
{ /* 附加信息 */ }
< View className = 'flex items-center gap-4 mt-3 pt-3 border-t border-gray-50' >
{ user . memberLevelName && (
< View className = 'flex items-center gap-1' >
< Text className = 'text-[10px] text-gray-400' > 会 员 : < / Text >
< Text className = 'text-xs text-purple-500' > { user . memberLevelName } < / Text >
< / View >
) }
< View className = 'flex items-center gap-1' >
< Text className = 'text-[10px] text-gray-400' > 注 册 : < / Text >
< Text className = 'text-xs text-gray-500' > { formatTime ( user . createTime ) } < / Text >
< / View >
{ user . balance !== undefined && Number ( user . balance ) > 0 && (
< View className = 'flex items-center gap-1' >
< Text className = 'text-[10px] text-gray-400' > 余 额 : < / Text >
< Text className = 'text-xs text-orange-500' > ¥ { user . balance } < / Text >
< / View >
) }
< / View >
< / View >
)
}
return (
< View className = 'min-h-full bg-gray-50' >
{ /* 搜索栏 */ }
< View className = 'bg-white px-3 py-2 flex items-center gap-2' >
< View className = 'flex-1 flex items-center bg-gray-100 rounded-lg px-3 py-1.5' >
< Input
className = 'flex-1 text-sm'
placeholder = '搜索昵称 / 手机号'
value = { searchText }
onInput = { ( e ) = > setSearchText ( e . detail . value ) }
onConfirm = { handleSearch }
confirmType = 'search'
/ >
{ searchText ? (
< Text
className = 'text-gray-400 text-lg px-1'
onClick = { handleClearSearch }
> × < / Text >
) : null }
< / View >
< View
className = 'px-3 py-1.5 rounded-lg bg-blue-500'
onClick = { handleSearch }
>
< Text className = 'text-sm text-white' > 搜 索 < / Text >
< / View >
< / View >
{ /* Tab 栏 */ }
< View className = 'bg-white flex border-b border-gray-50' >
{ TABS . map ( tab = > (
< View
key = { tab . key }
className = { ` flex-1 text-center py-3 border-b-2 ${ activeTab === tab . key ? 'border-blue-500' : 'border-transparent' } ` }
onClick = { ( ) = > setActiveTab ( tab . key ) }
>
< Text className = { ` text-sm ${ activeTab === tab . key ? 'text-blue-500 font-medium' : 'text-gray-500' } ` } >
{ tab . label }
< / Text >
< / View >
) ) }
< / View >
{ /* 用户列表 */ }
< ScrollView
scrollY
style = { { height : 'calc(100vh - 100px)' } }
onScrollToLower = { handleLoadMore }
lowerThreshold = { 100 }
>
{ loading && userList . length === 0 ? (
< View className = 'flex justify-center items-center py-20' >
< Text className = 'text-gray-400' > 加 载 中 . . . < / Text >
< / View >
) : userList . length === 0 ? (
< View className = 'flex justify-center items-center py-20' >
< Text className = 'text-gray-400' > 暂 无 用 户 < / Text >
< / View >
) : (
userList . map ( renderUserCard )
) }
{ loading && userList . length > 0 && (
< View className = 'flex justify-center items-center py-4' >
< Text className = 'text-gray-400 text-sm' > 加 载 中 . . . < / Text >
< / View >
) }
{ ! hasMore && userList . length > 0 && (
< View className = 'flex justify-center items-center py-4' >
< Text className = 'text-gray-300 text-xs' > 没 有 更 多 了 < / Text >
< / View >
) }
< View className = 'h-6' / >
< / ScrollView >
{ /* 用户详情弹窗 */ }
{ showDetail && detailUser && (
< View className = 'fixed inset-0 z-50 flex items-end justify-center' >
{ /* 遮罩 */ }
< View className = 'absolute inset-0 bg-black/50' onClick = { ( ) = > setShowDetail ( false ) } / >
{ /* 弹窗内容 */ }
< View className = 'relative bg-white rounded-t-2xl w-full px-5 pt-6 pb-10 max-h-[75vh] overflow-y-auto' >
{ /* 标题 */ }
< View className = 'flex justify-between items-center mb-5' >
< Text className = 'text-lg font-medium text-gray-800' > 用 户 详 情 < / Text >
< Text className = 'text-gray-400 text-lg' onClick = { ( ) = > setShowDetail ( false ) } > × < / Text >
< / View >
{ /* 用户基本信息 */ }
< View className = 'flex items-center gap-4 mb-5' >
< View className = 'w-16 h-16 rounded-full overflow-hidden flex-shrink-0 bg-gray-100' >
{ detailUser . avatar ? (
< Image
className = 'w-full h-full'
src = { getCompressedImageUrl ( detailUser . avatar , { width : 100 , quality : 90 } ) }
mode = 'aspectFill'
/ >
) : (
< View className = 'w-full h-full flex items-center justify-center' >
< Text className = 'text-3xl' > 👤 < / Text >
< / View >
) }
< / View >
< View className = 'flex-1 min-w-0' >
< View className = 'flex items-center gap-2' >
< Text className = 'text-base font-medium text-gray-800' >
{ detailUser . nickname || detailUser . realName || '未设置昵称' }
< / Text >
< View
className = 'px-2 py-0.5 rounded text-xs'
style = { {
color : detailUser.status === 1 ? '#dc2626' : '#16a34a' ,
background : detailUser.status === 1 ? '#fef2f2' : '#f0fdf4' ,
} }
>
{ detailUser . status === 1 ? '已禁用' : '正常' }
< / View >
< / View >
< Text className = 'text-xs text-gray-400 mt-1 block' >
ID : { detailUser . userId || detailUser . id }
< / Text >
< / View >
< / View >
{ /* 详细信息列表 */ }
< View className = 'bg-gray-50 rounded-xl p-4 mb-5' >
{ /* 手机号 */ }
< View className = 'flex justify-between items-center py-2 border-b border-gray-100' >
< Text className = 'text-sm text-gray-500' > 手 机 号 < / Text >
< Text className = 'text-sm text-gray-800' > { detailUser . phone || detailUser . mobile || '-' } < / Text >
< / View >
{ /* 真实姓名 */ }
{ detailUser . realName && (
< View className = 'flex justify-between items-center py-2 border-b border-gray-100' >
< Text className = 'text-sm text-gray-500' > 真 实 姓 名 < / Text >
< Text className = 'text-sm text-gray-800' > { detailUser . realName } < / Text >
< / View >
) }
{ /* 性别 */ }
< View className = 'flex justify-between items-center py-2 border-b border-gray-100' >
< Text className = 'text-sm text-gray-500' > 性 别 < / Text >
< Text className = 'text-sm text-gray-800' >
{ detailUser . gender === 1 ? '男' : detailUser . gender === 2 ? '女' : '未知' }
< / Text >
< / View >
{ /* 会员等级 */ }
{ detailUser . memberLevelName && (
< View className = 'flex justify-between items-center py-2 border-b border-gray-100' >
< Text className = 'text-sm text-gray-500' > 会 员 等 级 < / Text >
< Text className = 'text-sm text-purple-500' > { detailUser . memberLevelName } < / Text >
< / View >
) }
{ /* 余额 */ }
{ detailUser . balance !== undefined && (
< View className = 'flex justify-between items-center py-2 border-b border-gray-100' >
< Text className = 'text-sm text-gray-500' > 余 额 < / Text >
< Text className = 'text-sm text-orange-500' > ¥ { detailUser . balance || '0.00' } < / Text >
< / View >
) }
{ /* 积分 */ }
{ detailUser . points !== undefined && (
< View className = 'flex justify-between items-center py-2 border-b border-gray-100' >
< Text className = 'text-sm text-gray-500' > 积 分 < / Text >
< Text className = 'text-sm text-gray-800' > { detailUser . points || 0 } < / Text >
< / View >
) }
{ /* 邮箱 */ }
{ detailUser . email && (
< View className = 'flex justify-between items-center py-2 border-b border-gray-100' >
< Text className = 'text-sm text-gray-500' > 邮 箱 < / Text >
< Text className = 'text-sm text-gray-800' > { detailUser . email } < / Text >
< / View >
) }
{ /* 注册时间 */ }
< View className = 'flex justify-between items-center py-2' >
< Text className = 'text-sm text-gray-500' > 注 册 时 间 < / Text >
< Text className = 'text-sm text-gray-800' > { formatTime ( detailUser . createTime ) } < / Text >
< / View >
< / View >
{ /* 操作按钮 */ }
< View className = 'flex gap-3' >
< View
className = 'flex-1 py-3 rounded-xl bg-gray-100 text-center'
onClick = { ( ) = > setShowDetail ( false ) }
>
< Text className = 'text-sm text-gray-600' > 关 闭 < / Text >
< / View >
< View
className = { ` flex-1 py-3 rounded-xl text-center ${ detailUser . status === 1 ? 'bg-green-500' : 'bg-red-500' } ` }
onClick = { ( ) = > handleToggleStatus ( detailUser ) }
>
< Text className = 'text-sm text-white' >
{ detailUser . status === 1 ? '启用用户' : '禁用用户' }
< / Text >
< / View >
< / View >
< / View >
< / View >
) }
< / View >
)
}