{
+ return [
+ {
+ title: '用户',
+ dataIndex: 'phone',
+ width: 220,
+ fixed: 'left',
+ render: (_, row) => (
+
+ {canView ? (
+
+ ) : (
+ {row.nickname || row.phone}
+ )}
+ {row.phone}
+ UID {row.id}
+
+ ),
+ },
+ {
+ title: '实名 / 风险',
+ key: 'identity',
+ width: 130,
+ render: (_, row) => (
+
+
+
+
+ ),
+ },
+ {
+ title: '余额',
+ key: 'balance',
+ width: 140,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.available_balance_cent)}
+ 冻结 {formatMoney(row.frozen_balance_cent)}
+
+ ),
+ },
+ {
+ title: '免押额度',
+ key: 'deposit_free',
+ width: 150,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.deposit_free_effective_quota_cent)}
+ 剩余 {formatMoney(row.deposit_free_remaining_cent)}
+
+ ),
+ },
+ {
+ title: '成长',
+ key: 'growth',
+ width: 130,
+ render: (_, row) => (
+
+ {row.renter_growth_level_name || row.renter_growth_level || '—'}
+ {row.renter_growth_points.toLocaleString('zh-CN')} 成长值
+
+ ),
+ },
+ {
+ title: '业务',
+ key: 'activity',
+ width: 150,
+ render: (_, row) => (
+
+ 订单 {row.order_count}
+ 商品 {row.listing_count}
+ 申诉 {row.dispute_count}
+
+ ),
+ },
+ {
+ title: '账号状态',
+ dataIndex: 'status',
+ width: 110,
+ render: value => ,
+ },
+ {
+ title: '注册时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 96,
+ fixed: 'right',
+ render: (_, row) =>
+ canView ? (
+ } onClick={() => onOpen(row)}>
+ 详情
+
+ ) : (
+ 无权限
+ ),
+ },
+ ]
+}
+
+export function AdminUsersPage() {
+ const [draft, setDraft] = useState(emptyFilters)
+ const [applied, setApplied] = useState(emptyFilters)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+ const [detailUser, setDetailUser] = useState(null)
+ const [detailOpen, setDetailOpen] = useState(false)
+ const permissions = useAdminSessionStore(state => state.admin?.permissions)
+ const canView = Boolean(permissions?.includes('*') || permissions?.includes('user:view'))
+
+ const { data, isPending, isFetching, isError, error, refetch } = useQuery({
+ queryKey: ['admin-users', applied, page, pageSize],
+ queryFn: () => fetchAdminUsers({ ...applied, page, page_size: pageSize }),
+ placeholderData: keepPreviousData,
+ })
+
+ const openDetail = (user: AdminUserItem) => {
+ if (!canView) return
+ setDetailUser(user)
+ setDetailOpen(true)
+ }
+
+ const columns = useMemo(() => buildColumns(openDetail, canView), [canView])
+
+ const patchFilter = (patch: Partial) => {
+ setDraft(current => ({ ...current, ...patch }))
+ }
+
+ const applyFilters = () => {
+ setApplied({ ...draft, keyword: draft.keyword?.trim() ?? '' })
+ setPage(1)
+ }
+
+ const resetFilters = () => {
+ setDraft(emptyFilters)
+ setApplied(emptyFilters)
+ setPage(1)
+ }
+
+ const hasActiveFilters =
+ Boolean(draft.keyword?.trim()) || Boolean(draft.status) || Boolean(draft.has_deposit_free_quota)
+ const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。'
+
+ return (
+
+ )
+}
diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts
index bc2da53..07efe08 100644
--- a/frontend/src/features/admin/api.ts
+++ b/frontend/src/features/admin/api.ts
@@ -10,6 +10,9 @@ import type {
AdminOrderPage,
AdminOrderQuery,
AdminUser,
+ AdminUserItem,
+ AdminUserPage,
+ AdminUserQuery,
DashboardData,
} from '@/features/admin/types'
@@ -130,3 +133,87 @@ export async function markAbnormalAdminListing(id: number | string, reason: stri
)
return data.data
}
+
+export async function fetchAdminUsers(query: AdminUserQuery) {
+ const params: Record = { ...compactParams(query) }
+ if (query.has_deposit_free_quota) {
+ params.has_deposit_free_quota = true
+ } else {
+ delete params.has_deposit_free_quota
+ }
+ const { data } = await adminClient.get>>('/admin/users', {
+ params,
+ })
+ return {
+ items: data.data.items ?? [],
+ total: data.data.total ?? 0,
+ page: data.data.page ?? query.page ?? 1,
+ page_size: data.data.page_size ?? query.page_size ?? 20,
+ }
+}
+
+export async function freezeAdminUser(id: number | string, reason: string) {
+ const { data } = await adminClient.post>(
+ `/admin/users/${id}/freeze`,
+ { reason }
+ )
+ return data.data
+}
+
+export async function unfreezeAdminUser(id: number | string, reason: string) {
+ const { data } = await adminClient.post>(
+ `/admin/users/${id}/unfreeze`,
+ { reason }
+ )
+ return data.data
+}
+
+export async function setAdminUserDepositFreeQuota(id: number | string, amountCent: number) {
+ const { data } = await adminClient.post>(
+ `/admin/users/${id}/deposit-free-quota`,
+ { amount_cent: amountCent }
+ )
+ return data.data
+}
+
+export async function adjustAdminUserGrowthPoints(
+ id: number | string,
+ targetPoints: number,
+ reason: string
+) {
+ const { data } = await adminClient.post>(
+ `/admin/users/${id}/growth-points`,
+ { target_points: targetPoints, reason }
+ )
+ return data.data
+}
+
+export async function manualAdminUserRealname(
+ id: number | string,
+ payload: { name: string; id_no: string; reason: string }
+) {
+ const { data } = await adminClient.post>(
+ `/admin/users/${id}/manual-realname`,
+ payload
+ )
+ return data.data
+}
+
+export async function revokeAdminUserRealname(id: number | string, reason: string) {
+ const { data } = await adminClient.post>(
+ `/admin/users/${id}/revoke-realname`,
+ { reason }
+ )
+ return data.data
+}
+
+export async function adjustAdminUserWallet(
+ id: number | string,
+ payload: { direction: 'in' | 'out'; amount_cent: number; reason: string; reference_no?: string }
+) {
+ const { data } = await adminClient.post>(
+ `/admin/users/${id}/wallet/adjust`,
+ payload
+ )
+ return data.data
+}
diff --git a/frontend/src/features/admin/labels.ts b/frontend/src/features/admin/labels.ts
index f24f0be..d6c344a 100644
--- a/frontend/src/features/admin/labels.ts
+++ b/frontend/src/features/admin/labels.ts
@@ -155,3 +155,27 @@ export const offlineSettlementStatusOptions = ['pending', 'settled'].map(value =
value,
label: offlineSettlementStatusMap[value].label,
}))
+
+export const userStatusMap: Record = {
+ active: { label: '正常', tone: 'success' },
+ frozen: { label: '已冻结', tone: 'red' },
+ disabled: { label: '已禁用', tone: 'default' },
+}
+
+export const realnameStatusMap: Record = {
+ unverified: { label: '未实名', tone: 'default' },
+ pending: { label: '认证中', tone: 'gold' },
+ verified: { label: '已实名', tone: 'success' },
+ rejected: { label: '认证失败', tone: 'red' },
+}
+
+export const riskStatusMap: Record = {
+ normal: { label: '正常', tone: 'success' },
+ watch: { label: '观察', tone: 'gold' },
+ restricted: { label: '受限', tone: 'orange' },
+ blocked: { label: '已拦截', tone: 'red' },
+}
+
+export const userStatusOptions = toOptions(userStatusMap)
+export const realnameStatusOptions = toOptions(realnameStatusMap)
+export const riskStatusOptions = toOptions(riskStatusMap)
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index 96ad01f..af2b500 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -262,3 +262,45 @@ export interface AdminListingPage {
page: number
page_size: number
}
+
+export interface AdminUserQuery {
+ page?: number
+ page_size?: number
+ keyword?: string
+ status?: string
+ has_deposit_free_quota?: boolean
+}
+
+export interface AdminUserItem {
+ id: number
+ phone: string
+ nickname: string
+ realname_status: string
+ risk_status: string
+ credit_score: number
+ deposit_free_quota_cent: number
+ deposit_free_manual_quota_cent: number
+ deposit_free_level_quota_cent: number
+ deposit_free_effective_quota_cent: number
+ deposit_free_used_cent: number
+ deposit_free_remaining_cent: number
+ renter_growth_points: number
+ renter_growth_level: string
+ renter_growth_level_name: string
+ available_balance_cent: number
+ frozen_balance_cent: number
+ status: string
+ order_count: number
+ listing_count: number
+ dispute_count: number
+ last_login_at?: string | null
+ created_at: string
+ updated_at: string
+}
+
+export interface AdminUserPage {
+ items: AdminUserItem[]
+ total: number
+ page: number
+ page_size: number
+}