feat(admin): 新增后台用户列表与详情操作
This commit is contained in:
+2
-2
@@ -194,9 +194,9 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB
|
||||
## 5. 已知限制
|
||||
|
||||
1. 移动端订单列表/详情与消息中心已接入真实接口;下单、支付、交接、归还、结账等操作仍待迁移。
|
||||
2. 后台已完成仪表盘、订单列表/详情、商品列表/详情与商品审核,其余页面仍为统一占位。
|
||||
2. 后台已完成仪表盘、订单列表/详情、商品列表/详情与审核、用户列表/详情与人工操作,其余页面仍为统一占位。
|
||||
3. 分销后台已完成主题、Logo、登录页与 Shell 轻改,页面结构未重写。
|
||||
4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`;其余页面仍需核对。
|
||||
4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`;其余页面仍需核对。
|
||||
5. SVG 中文尚未转曲。
|
||||
6. 入口包已降至 495 kB(gzip 161 kB);AntD 共享基础设施仍在入口,后续可拆独立入口。
|
||||
7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理与审核在本批次提交。
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Phase 2 验收报告(后台用户管理)
|
||||
|
||||
> 日期:2026-10-30
|
||||
> 项目:解忧商行(hfb_jysh)
|
||||
> 范围:`/console-jy8k3m/users`
|
||||
> 结论:**本切片通过,用户列表、详情抽屉与人工操作已接真实接口**
|
||||
|
||||
---
|
||||
|
||||
## 1. 接口与权限
|
||||
|
||||
| 能力 | 接口 | 权限码 |
|
||||
| --- | --- | --- |
|
||||
| 用户列表 | `GET /api/admin/users` | `user:view` |
|
||||
| 冻结 | `POST /api/admin/users/:id/freeze` | `user:freeze` |
|
||||
| 解冻 | `POST /api/admin/users/:id/unfreeze` | `user:unfreeze` |
|
||||
| 免押额度 | `POST /api/admin/users/:id/deposit-free-quota` | `user:deposit_free` |
|
||||
| 成长值 | `POST /api/admin/users/:id/growth-points` | `user:growth_points` |
|
||||
| 实名补录 | `POST /api/admin/users/:id/manual-realname` | `user:manual_realname` |
|
||||
| 撤销实名 | `POST /api/admin/users/:id/revoke-realname` | `user:revoke_realname` |
|
||||
| 余额调账 | `POST /api/admin/users/:id/wallet/adjust` | `user:wallet_adjust` |
|
||||
|
||||
前端菜单权限沿用 `user:view`,与后端正向一致。后端未提供用户详情接口,详情抽屉直接使用列表 `UserDTO`,避免多余请求。
|
||||
|
||||
## 2. 用户列表
|
||||
|
||||
`frontend/src/features/admin/AdminUsersPage.tsx`
|
||||
|
||||
- 筛选:关键词(手机号 / 昵称 / 用户 ID)、账号状态、是否有免押额度
|
||||
- 列:用户(昵称/手机号/UID)、实名与风险、余额、免押额度、成长、业务量、账号状态、注册时间、详情
|
||||
- 分页 10 / 20 / 50,翻页保留旧数据
|
||||
- 点击昵称、行或「详情」打开抽屉
|
||||
- 无 `user:view` 时隐藏详情入口
|
||||
|
||||
## 3. 用户详情抽屉
|
||||
|
||||
`frontend/src/features/admin/AdminUserDetailDrawer.tsx`
|
||||
|
||||
- 标题区:昵称、手机号、UID、账号/实名/风险标签
|
||||
- 分段:账号概览、钱包、免押额度、成长与活跃
|
||||
- 快捷入口:订单明细(`/orders?user_id=`,需 `order:list`)、商品明细(`/listings?owner_id=`,需 `listing:view`)
|
||||
- 操作按钮按权限与状态显示:
|
||||
- 正常用户 + `user:freeze`:冻结(必填原因)
|
||||
- 冻结用户 + `user:unfreeze`:解冻(必填原因)
|
||||
- `user:deposit_free`:设置手动免押额度(元 → 分)
|
||||
- `user:growth_points`:调整成长值(目标值 + 原因)
|
||||
- 未实名 + `user:manual_realname`:实名补录(姓名 / 证件号 / 原因)
|
||||
- 已实名 + `user:revoke_realname`:撤销实名(必填原因)
|
||||
- `user:wallet_adjust`:加款/扣款、金额、原因、外部流水号
|
||||
- 操作成功后更新抽屉数据并刷新列表
|
||||
|
||||
## 4. 配套改动
|
||||
|
||||
- `AdminListingsPage` 支持从 URL 读取 `owner_id` 与 `keyword`,供用户详情跳转商品明细。
|
||||
- `admin/labels.ts` 新增用户状态、实名状态、风险状态映射。
|
||||
- `admin/api.ts` 新增用户列表与全部人工操作接口。
|
||||
|
||||
## 5. 验证结果
|
||||
|
||||
```text
|
||||
npx tsc --noEmit 通过
|
||||
npm run build 通过(入口 495.51 kB,gzip 161.02 kB)
|
||||
GET /api/admin/users?page=1&page_size=5 ok,返回 UserDTO 全字段
|
||||
GET /console-jy8k3m/users 200
|
||||
GET /console-jy8k3m/listings?owner_id=1 200
|
||||
```
|
||||
|
||||
开发库当前仅 1 个用户,冻结、调账、实名等写操作未执行,避免污染开发数据;按钮权限与请求体已按后端 DTO 核对。
|
||||
|
||||
## 6. 已知限制
|
||||
|
||||
1. 后端无用户详情接口,抽屉无法独立刷新单个用户,依赖列表数据与操作返回值。
|
||||
2. 未提供风险状态人工调整(后端暂无对应接口)。
|
||||
3. 调账、冻结等操作未接入二次审批或批量处理。
|
||||
4. 审计日志页面尚未迁移,操作记录只能在数据层核对。
|
||||
|
||||
## 7. 下一步建议
|
||||
|
||||
1. 后台财务看板与退款/提现审核。
|
||||
2. 后台申诉仲裁。
|
||||
3. 商品审核调价与转移号主。
|
||||
4. 移动端下单、支付与交接/归还/结账。
|
||||
+1
-1
@@ -395,7 +395,7 @@ hfb_jysh/
|
||||
|
||||
### Phase 4:管理后台核心(4–5 周)
|
||||
|
||||
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单列表/详情、商品列表/详情与商品审核;退款、提现、用户、申诉与系统配置待迁移。
|
||||
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单列表/详情、商品列表/详情与审核、用户列表/详情与人工操作;退款、提现、申诉与系统配置待迁移。
|
||||
|
||||
- 后台登录、工作台、订单列表、订单详情。
|
||||
- 商品列表、商品详情、审核。
|
||||
|
||||
@@ -73,6 +73,11 @@ const AdminListingsPage = lazy(() =>
|
||||
default: module.AdminListingsPage,
|
||||
}))
|
||||
)
|
||||
const AdminUsersPage = lazy(() =>
|
||||
import('@/features/admin/AdminUsersPage').then(module => ({
|
||||
default: module.AdminUsersPage,
|
||||
}))
|
||||
)
|
||||
const AdminLayout = lazy(() =>
|
||||
import('@/layouts/AdminLayout').then(module => ({ default: module.AdminLayout }))
|
||||
)
|
||||
@@ -83,7 +88,6 @@ const AdminSectionPlaceholder = lazy(() =>
|
||||
const adminSections: Array<{ path: string; title: string; description: string }> = [
|
||||
{ path: 'orders/refund-review', title: '退款审核', description: '审核退款申请并核对资金影响。' },
|
||||
{ path: 'disputes', title: '申诉仲裁', description: '处理订单争议、证据与仲裁结果。' },
|
||||
{ path: 'users', title: '用户管理', description: '查看用户、实名状态与交易记录。' },
|
||||
{ path: 'finance', title: '财务看板', description: '查看收入、退款、结算与资金趋势。' },
|
||||
{ path: 'withdrawals', title: '提现审核', description: '审核提现申请与打款确认。' },
|
||||
{ path: 'distribution/sites', title: '分销站点', description: '管理分站、规则、结算与站点钱包。' },
|
||||
@@ -124,6 +128,7 @@ export function AppRouter() {
|
||||
<Route path="orders" element={<AdminOrdersPage />} />
|
||||
<Route path="listings" element={<AdminListingsPage />} />
|
||||
<Route path="listings/review" element={<AdminListingsPage mode="review" />} />
|
||||
<Route path="users" element={<AdminUsersPage />} />
|
||||
{adminSections.map(section => (
|
||||
<Route
|
||||
key={section.path}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { Alert, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
|
||||
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
import { fetchAdminListings, fetchAdminPendingListings } from '@/features/admin/api'
|
||||
import { AdminListingDetailDrawer } from '@/features/admin/AdminListingDetailDrawer'
|
||||
@@ -129,12 +130,19 @@ function buildColumns(
|
||||
|
||||
export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
||||
const isReviewMode = mode === 'review'
|
||||
const [searchParams] = useSearchParams()
|
||||
const initialOwnerId = Number(searchParams.get('owner_id')) || undefined
|
||||
const initialKeyword = searchParams.get('keyword')?.trim() ?? ''
|
||||
const [draft, setDraft] = useState<AdminListingQuery>(() => ({
|
||||
...emptyFilters,
|
||||
keyword: initialKeyword,
|
||||
owner_id: initialOwnerId,
|
||||
review_status: isReviewMode ? 'pending' : '',
|
||||
}))
|
||||
const [applied, setApplied] = useState<AdminListingQuery>(() => ({
|
||||
...emptyFilters,
|
||||
keyword: initialKeyword,
|
||||
owner_id: initialOwnerId,
|
||||
review_status: isReviewMode ? 'pending' : '',
|
||||
}))
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
adjustAdminUserGrowthPoints,
|
||||
adjustAdminUserWallet,
|
||||
freezeAdminUser,
|
||||
manualAdminUserRealname,
|
||||
revokeAdminUserRealname,
|
||||
setAdminUserDepositFreeQuota,
|
||||
unfreezeAdminUser,
|
||||
} from '@/features/admin/api'
|
||||
import { DetailDrawer, type DetailSection } from '@/features/admin/components/DetailDrawer'
|
||||
import { StatusTag } from '@/features/admin/components/StatusTag'
|
||||
import {
|
||||
readStatusMeta,
|
||||
realnameStatusMap,
|
||||
riskStatusMap,
|
||||
userStatusMap,
|
||||
} from '@/features/admin/labels'
|
||||
import type { AdminUserItem } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { emptyText, formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
type UserAction =
|
||||
| 'freeze'
|
||||
| 'unfreeze'
|
||||
| 'quota'
|
||||
| 'growth'
|
||||
| 'manual-realname'
|
||||
| 'revoke-realname'
|
||||
| 'wallet'
|
||||
|
||||
const actionCopy: Record<UserAction, { title: string; okText: string; hint: string }> = {
|
||||
freeze: {
|
||||
title: '冻结用户',
|
||||
okText: '确认冻结',
|
||||
hint: '冻结后用户无法继续下单或发布商品,请填写原因。',
|
||||
},
|
||||
unfreeze: {
|
||||
title: '解冻用户',
|
||||
okText: '确认解冻',
|
||||
hint: '解冻后用户恢复交易能力,请填写解冻原因。',
|
||||
},
|
||||
quota: {
|
||||
title: '设置免押额度',
|
||||
okText: '保存额度',
|
||||
hint: '手动额度会与等级额度合并计算,0 表示取消手动额度。',
|
||||
},
|
||||
growth: {
|
||||
title: '调整成长值',
|
||||
okText: '保存成长值',
|
||||
hint: '成长值会直接影响租客等级与折扣,请谨慎修改。',
|
||||
},
|
||||
'manual-realname': {
|
||||
title: '实名补录',
|
||||
okText: '提交实名',
|
||||
hint: '用于人工核验后补录实名信息,证件号只用于实名校验。',
|
||||
},
|
||||
'revoke-realname': {
|
||||
title: '撤销实名',
|
||||
okText: '确认撤销',
|
||||
hint: '撤销后用户需要重新实名,请填写原因。',
|
||||
},
|
||||
wallet: {
|
||||
title: '调整可用余额',
|
||||
okText: '确认调账',
|
||||
hint: '只调整可用余额,不影响冻结余额;每次调账都会写入流水与审计日志。',
|
||||
},
|
||||
}
|
||||
|
||||
function buildSections(user: AdminUserItem): DetailSection[] {
|
||||
return [
|
||||
{
|
||||
key: 'overview',
|
||||
title: '账号概览',
|
||||
fields: [
|
||||
{ key: 'uid', label: '用户 ID', value: <span className="admin-mono">UID {user.id}</span> },
|
||||
{ key: 'phone', label: '手机号', value: formatText(user.phone) },
|
||||
{ key: 'nickname', label: '昵称', value: formatText(user.nickname) },
|
||||
{
|
||||
key: 'status',
|
||||
label: '账号状态',
|
||||
value: <StatusTag meta={readStatusMeta(userStatusMap, user.status)} />,
|
||||
},
|
||||
{
|
||||
key: 'realname',
|
||||
label: '实名状态',
|
||||
value: <StatusTag meta={readStatusMeta(realnameStatusMap, user.realname_status)} />,
|
||||
},
|
||||
{
|
||||
key: 'risk',
|
||||
label: '风险状态',
|
||||
value: <StatusTag meta={readStatusMeta(riskStatusMap, user.risk_status)} />,
|
||||
},
|
||||
{ key: 'credit', label: '信用分', value: formatText(user.credit_score) },
|
||||
{ key: 'created', label: '注册时间', value: formatDateTime(user.created_at) },
|
||||
{ key: 'updated', label: '更新时间', value: formatDateTime(user.updated_at) },
|
||||
{ key: 'last_login', label: '最后登录', value: formatDateTime(user.last_login_at) },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'wallet',
|
||||
title: '钱包',
|
||||
fields: [
|
||||
{ key: 'available', label: '可用余额', value: formatMoney(user.available_balance_cent) },
|
||||
{ key: 'frozen', label: '冻结余额', value: formatMoney(user.frozen_balance_cent) },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'deposit_free',
|
||||
title: '免押额度',
|
||||
fields: [
|
||||
{ key: 'manual', label: '手动额度', value: formatMoney(user.deposit_free_manual_quota_cent) },
|
||||
{ key: 'level', label: '等级额度', value: formatMoney(user.deposit_free_level_quota_cent) },
|
||||
{
|
||||
key: 'effective',
|
||||
label: '有效额度',
|
||||
value: formatMoney(user.deposit_free_effective_quota_cent),
|
||||
},
|
||||
{ key: 'used', label: '已使用', value: formatMoney(user.deposit_free_used_cent) },
|
||||
{
|
||||
key: 'remaining',
|
||||
label: '剩余额度',
|
||||
value: formatMoney(user.deposit_free_remaining_cent),
|
||||
},
|
||||
{
|
||||
key: 'quota_record',
|
||||
label: '额度记录',
|
||||
value: formatMoney(user.deposit_free_quota_cent),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'growth',
|
||||
title: '成长与活跃',
|
||||
fields: [
|
||||
{
|
||||
key: 'level',
|
||||
label: '成长等级',
|
||||
value: user.renter_growth_level_name || user.renter_growth_level || emptyText,
|
||||
},
|
||||
{
|
||||
key: 'points',
|
||||
label: '成长值',
|
||||
value: user.renter_growth_points.toLocaleString('zh-CN'),
|
||||
},
|
||||
{ key: 'orders', label: '订单数', value: String(user.order_count) },
|
||||
{ key: 'listings', label: '商品数', value: String(user.listing_count) },
|
||||
{ key: 'disputes', label: '申诉数', value: String(user.dispute_count) },
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
interface AdminUserDetailDrawerProps {
|
||||
user: AdminUserItem | null
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onUpdated: (user: AdminUserItem) => void
|
||||
}
|
||||
|
||||
export function AdminUserDetailDrawer({
|
||||
user,
|
||||
open,
|
||||
onClose,
|
||||
onUpdated,
|
||||
}: AdminUserDetailDrawerProps) {
|
||||
const { message } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const can = (code: string) => Boolean(permissions?.includes('*') || permissions?.includes(code))
|
||||
const [action, setAction] = useState<UserAction | null>(null)
|
||||
const [reason, setReason] = useState('')
|
||||
const [amountYuan, setAmountYuan] = useState<number | null>(null)
|
||||
const [targetPoints, setTargetPoints] = useState<number | null>(null)
|
||||
const [realnameName, setRealnameName] = useState('')
|
||||
const [realnameIDNo, setRealnameIDNo] = useState('')
|
||||
const [walletDirection, setWalletDirection] = useState<'in' | 'out'>('in')
|
||||
const [walletReference, setWalletReference] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const openAction = (next: UserAction) => {
|
||||
setReason('')
|
||||
setWalletDirection('in')
|
||||
setWalletReference('')
|
||||
if (next === 'quota') {
|
||||
setAmountYuan(user ? user.deposit_free_manual_quota_cent / 100 : 0)
|
||||
}
|
||||
if (next === 'wallet') {
|
||||
setAmountYuan(null)
|
||||
}
|
||||
if (next === 'growth') {
|
||||
setTargetPoints(user?.renter_growth_points ?? 0)
|
||||
}
|
||||
if (next === 'manual-realname') {
|
||||
setRealnameName('')
|
||||
setRealnameIDNo('')
|
||||
}
|
||||
setAction(next)
|
||||
}
|
||||
|
||||
const closeAction = () => {
|
||||
if (submitting) return
|
||||
setAction(null)
|
||||
setReason('')
|
||||
}
|
||||
|
||||
const submitAction = async () => {
|
||||
if (!user || !action) return
|
||||
const reasonText = reason.trim()
|
||||
const needReason = action !== 'quota'
|
||||
if (needReason && !reasonText) {
|
||||
message.warning('请填写原因')
|
||||
return
|
||||
}
|
||||
if (action === 'quota' && (amountYuan == null || amountYuan < 0)) {
|
||||
message.warning('请填写有效的免押额度')
|
||||
return
|
||||
}
|
||||
if (action === 'growth' && (targetPoints == null || targetPoints < 0)) {
|
||||
message.warning('请填写有效的成长值')
|
||||
return
|
||||
}
|
||||
if (action === 'manual-realname' && (!realnameName.trim() || !realnameIDNo.trim())) {
|
||||
message.warning('请填写姓名与证件号')
|
||||
return
|
||||
}
|
||||
if (action === 'wallet' && (amountYuan == null || amountYuan <= 0)) {
|
||||
message.warning('请填写大于 0 的调账金额')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
let updated: AdminUserItem
|
||||
if (action === 'freeze') {
|
||||
updated = await freezeAdminUser(user.id, reasonText)
|
||||
message.success('用户已冻结')
|
||||
} else if (action === 'unfreeze') {
|
||||
updated = await unfreezeAdminUser(user.id, reasonText)
|
||||
message.success('用户已解冻')
|
||||
} else if (action === 'quota') {
|
||||
updated = await setAdminUserDepositFreeQuota(user.id, Math.round((amountYuan ?? 0) * 100))
|
||||
message.success('免押额度已更新')
|
||||
} else if (action === 'growth') {
|
||||
updated = await adjustAdminUserGrowthPoints(user.id, targetPoints ?? 0, reasonText)
|
||||
message.success('成长值已更新')
|
||||
} else if (action === 'manual-realname') {
|
||||
updated = await manualAdminUserRealname(user.id, {
|
||||
name: realnameName.trim(),
|
||||
id_no: realnameIDNo.trim(),
|
||||
reason: reasonText,
|
||||
})
|
||||
message.success('实名信息已补录')
|
||||
} else if (action === 'revoke-realname') {
|
||||
updated = await revokeAdminUserRealname(user.id, reasonText)
|
||||
message.success('实名已撤销')
|
||||
} else {
|
||||
updated = await adjustAdminUserWallet(user.id, {
|
||||
direction: walletDirection,
|
||||
amount_cent: Math.round((amountYuan ?? 0) * 100),
|
||||
reason: reasonText,
|
||||
reference_no: walletReference.trim() || undefined,
|
||||
})
|
||||
message.success('余额已调整')
|
||||
}
|
||||
setAction(null)
|
||||
onUpdated(updated)
|
||||
} catch (error) {
|
||||
const text =
|
||||
error && typeof error === 'object' && 'response' in error
|
||||
? ((error as { response?: { data?: { message?: string } } }).response?.data?.message ??
|
||||
'操作失败')
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: '操作失败'
|
||||
message.error(text)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const copy = action ? actionCopy[action] : null
|
||||
const isVerified = user?.realname_status === 'verified'
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailDrawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={user ? user.nickname || user.phone : '用户详情'}
|
||||
subtitle={
|
||||
user ? (
|
||||
<span>
|
||||
{user.phone} · <span className="admin-mono">UID {user.id}</span>
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
tags={
|
||||
user ? (
|
||||
<Space size={6} wrap>
|
||||
<StatusTag meta={readStatusMeta(userStatusMap, user.status)} />
|
||||
<StatusTag meta={readStatusMeta(realnameStatusMap, user.realname_status)} />
|
||||
<StatusTag meta={readStatusMeta(riskStatusMap, user.risk_status)} />
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
sections={user ? buildSections(user) : []}
|
||||
emptyDescription="请选择一个用户"
|
||||
footer={
|
||||
user ? (
|
||||
<Space wrap>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => navigate(`/orders?user_id=${user.id}`)}
|
||||
disabled={!can('order:list')}
|
||||
>
|
||||
订单明细
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => navigate(`/listings?owner_id=${user.id}`)}
|
||||
disabled={!can('listing:view')}
|
||||
>
|
||||
商品明细
|
||||
</Button>
|
||||
{user.status === 'active' && can('user:freeze') ? (
|
||||
<Button size="small" danger onClick={() => openAction('freeze')}>
|
||||
冻结
|
||||
</Button>
|
||||
) : null}
|
||||
{user.status === 'frozen' && can('user:unfreeze') ? (
|
||||
<Button size="small" onClick={() => openAction('unfreeze')}>
|
||||
解冻
|
||||
</Button>
|
||||
) : null}
|
||||
{can('user:deposit_free') ? (
|
||||
<Button size="small" onClick={() => openAction('quota')}>
|
||||
免押额度
|
||||
</Button>
|
||||
) : null}
|
||||
{can('user:growth_points') ? (
|
||||
<Button size="small" onClick={() => openAction('growth')}>
|
||||
成长值
|
||||
</Button>
|
||||
) : null}
|
||||
{!isVerified && can('user:manual_realname') ? (
|
||||
<Button size="small" onClick={() => openAction('manual-realname')}>
|
||||
实名补录
|
||||
</Button>
|
||||
) : null}
|
||||
{isVerified && can('user:revoke_realname') ? (
|
||||
<Button size="small" onClick={() => openAction('revoke-realname')}>
|
||||
撤销实名
|
||||
</Button>
|
||||
) : null}
|
||||
{can('user:wallet_adjust') ? (
|
||||
<Button size="small" onClick={() => openAction('wallet')}>
|
||||
调整余额
|
||||
</Button>
|
||||
) : null}
|
||||
<Button size="small" onClick={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Button onClick={onClose}>关闭</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={action !== null}
|
||||
title={copy?.title}
|
||||
okText={copy?.okText}
|
||||
cancelText="取消"
|
||||
confirmLoading={submitting}
|
||||
onOk={() => void submitAction()}
|
||||
onCancel={closeAction}
|
||||
>
|
||||
<Typography.Paragraph type="secondary">{copy?.hint}</Typography.Paragraph>
|
||||
|
||||
{action === 'quota' ? (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
precision={2}
|
||||
value={amountYuan}
|
||||
addonBefore="¥"
|
||||
placeholder="手动免押额度"
|
||||
onChange={value => setAmountYuan(typeof value === 'number' ? value : null)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{action === 'growth' ? (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
precision={0}
|
||||
value={targetPoints}
|
||||
addonAfter="成长值"
|
||||
onChange={value => setTargetPoints(typeof value === 'number' ? value : null)}
|
||||
/>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
value={reason}
|
||||
maxLength={200}
|
||||
showCount
|
||||
placeholder="调整原因(必填)"
|
||||
onChange={event => setReason(event.target.value)}
|
||||
/>
|
||||
</Space>
|
||||
) : null}
|
||||
|
||||
{action === 'manual-realname' ? (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||
<Input
|
||||
value={realnameName}
|
||||
placeholder="真实姓名"
|
||||
onChange={event => setRealnameName(event.target.value)}
|
||||
/>
|
||||
<Input
|
||||
value={realnameIDNo}
|
||||
placeholder="身份证号"
|
||||
onChange={event => setRealnameIDNo(event.target.value)}
|
||||
/>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
value={reason}
|
||||
maxLength={200}
|
||||
placeholder="补录原因(必填)"
|
||||
onChange={event => setReason(event.target.value)}
|
||||
/>
|
||||
</Space>
|
||||
) : null}
|
||||
|
||||
{action === 'wallet' ? (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||
<Select
|
||||
value={walletDirection}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'in', label: '加款' },
|
||||
{ value: 'out', label: '扣款' },
|
||||
]}
|
||||
onChange={value => setWalletDirection(value)}
|
||||
/>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={0.01}
|
||||
precision={2}
|
||||
value={amountYuan}
|
||||
addonBefore="¥"
|
||||
placeholder="调账金额"
|
||||
onChange={value => setAmountYuan(typeof value === 'number' ? value : null)}
|
||||
/>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
value={reason}
|
||||
maxLength={200}
|
||||
placeholder="调账原因(必填)"
|
||||
onChange={event => setReason(event.target.value)}
|
||||
/>
|
||||
<Input
|
||||
value={walletReference}
|
||||
placeholder="外部流水号(可选)"
|
||||
onChange={event => setWalletReference(event.target.value)}
|
||||
/>
|
||||
</Space>
|
||||
) : null}
|
||||
|
||||
{['freeze', 'unfreeze', 'revoke-realname'].includes(action ?? '') ? (
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
value={reason}
|
||||
maxLength={200}
|
||||
showCount
|
||||
placeholder="请填写原因(必填)"
|
||||
onChange={event => setReason(event.target.value)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{user && action === 'freeze' ? (
|
||||
<Typography.Paragraph style={{ marginTop: 12, marginBottom: 0 }} type="secondary">
|
||||
冻结对象:{user.nickname || user.phone}(UID {user.id})
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
{user && action === 'wallet' ? (
|
||||
<Typography.Paragraph style={{ marginTop: 12, marginBottom: 0 }} type="secondary">
|
||||
当前可用余额 {formatMoney(user.available_balance_cent)},冻结余额{' '}
|
||||
{formatMoney(user.frozen_balance_cent)}。
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
{user && action === 'quota' ? (
|
||||
<Typography.Paragraph style={{ marginTop: 12, marginBottom: 0 }} type="secondary">
|
||||
等级额度 {formatMoney(user.deposit_free_level_quota_cent)},当前手动额度{' '}
|
||||
{formatMoney(user.deposit_free_manual_quota_cent)}。
|
||||
{user.deposit_free_quota_cent > 0 ? <Tag color="gold">含历史额度记录</Tag> : null}
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { Alert, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
|
||||
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
|
||||
import { fetchAdminUsers } from '@/features/admin/api'
|
||||
import { AdminUserDetailDrawer } from '@/features/admin/AdminUserDetailDrawer'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import { DataTable } from '@/features/admin/components/DataTable'
|
||||
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
|
||||
import { StatusTag } from '@/features/admin/components/StatusTag'
|
||||
import {
|
||||
readStatusMeta,
|
||||
realnameStatusMap,
|
||||
riskStatusMap,
|
||||
userStatusMap,
|
||||
userStatusOptions,
|
||||
} from '@/features/admin/labels'
|
||||
import type { AdminUserItem, AdminUserQuery } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatMoney } from '@/shared/utils/format'
|
||||
|
||||
const emptyFilters: AdminUserQuery = {
|
||||
keyword: '',
|
||||
status: '',
|
||||
has_deposit_free_quota: false,
|
||||
}
|
||||
|
||||
function buildColumns(
|
||||
onOpen: (row: AdminUserItem) => void,
|
||||
canView: boolean
|
||||
): TableColumnsType<AdminUserItem> {
|
||||
return [
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'phone',
|
||||
width: 220,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
{canView ? (
|
||||
<button type="button" className="admin-order-link" onClick={() => onOpen(row)}>
|
||||
{row.nickname || row.phone}
|
||||
</button>
|
||||
) : (
|
||||
<strong>{row.nickname || row.phone}</strong>
|
||||
)}
|
||||
<span>{row.phone}</span>
|
||||
<span className="admin-mono">UID {row.id}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '实名 / 风险',
|
||||
key: 'identity',
|
||||
width: 130,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack admin-stack--tiny">
|
||||
<StatusTag meta={readStatusMeta(realnameStatusMap, row.realname_status)} />
|
||||
<StatusTag meta={readStatusMeta(riskStatusMap, row.risk_status)} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '余额',
|
||||
key: 'balance',
|
||||
width: 140,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-amount">
|
||||
<strong>{formatMoney(row.available_balance_cent)}</strong>
|
||||
<span>冻结 {formatMoney(row.frozen_balance_cent)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '免押额度',
|
||||
key: 'deposit_free',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-amount">
|
||||
<strong>{formatMoney(row.deposit_free_effective_quota_cent)}</strong>
|
||||
<span>剩余 {formatMoney(row.deposit_free_remaining_cent)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '成长',
|
||||
key: 'growth',
|
||||
width: 130,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>{row.renter_growth_level_name || row.renter_growth_level || '—'}</span>
|
||||
<span>{row.renter_growth_points.toLocaleString('zh-CN')} 成长值</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '业务',
|
||||
key: 'activity',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<Space size={4} wrap>
|
||||
<Tag>订单 {row.order_count}</Tag>
|
||||
<Tag>商品 {row.listing_count}</Tag>
|
||||
<Tag>申诉 {row.dispute_count}</Tag>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '账号状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: value => <StatusTag meta={readStatusMeta(userStatusMap, value)} />,
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 96,
|
||||
fixed: 'right',
|
||||
render: (_, row) =>
|
||||
canView ? (
|
||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => onOpen(row)}>
|
||||
详情
|
||||
</Button>
|
||||
) : (
|
||||
<Typography.Text type="secondary">无权限</Typography.Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function AdminUsersPage() {
|
||||
const [draft, setDraft] = useState<AdminUserQuery>(emptyFilters)
|
||||
const [applied, setApplied] = useState<AdminUserQuery>(emptyFilters)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [detailUser, setDetailUser] = useState<AdminUserItem | null>(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<AdminUserQuery>) => {
|
||||
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 (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 用户"
|
||||
title="用户管理"
|
||||
description="查看用户实名、风险、钱包、免押额度与交易活跃度;点击昵称查看详情。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">共 {data?.total ?? 0} 位用户</Tag>
|
||||
{isFetching && !isPending ? <Tag color="processing">正在更新</Tag> : null}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void refetch()} loading={isFetching}>
|
||||
刷新数据
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
onSearch={applyFilters}
|
||||
onReset={hasActiveFilters ? resetFilters : undefined}
|
||||
loading={isFetching}
|
||||
>
|
||||
<FilterField label="关键词" width={300}>
|
||||
<Input
|
||||
allowClear
|
||||
value={draft.keyword}
|
||||
placeholder="手机号 / 昵称 / 用户 ID"
|
||||
onChange={event => patchFilter({ keyword: event.target.value })}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="账号状态" width={150}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.status || undefined}
|
||||
placeholder="全部状态"
|
||||
options={userStatusOptions}
|
||||
onChange={value => patchFilter({ status: value ?? '' })}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="免押额度" width={160}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.has_deposit_free_quota ? '1' : undefined}
|
||||
placeholder="全部用户"
|
||||
options={[{ value: '1', label: '有免押额度' }]}
|
||||
onChange={value => patchFilter({ has_deposit_free_quota: value === '1' })}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
{isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="用户列表没有加载出来"
|
||||
description={errorText}
|
||||
action={
|
||||
<Button size="small" onClick={() => void refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminUserItem>
|
||||
title={<span>用户列表</span>}
|
||||
extra={
|
||||
<Typography.Text type="secondary">
|
||||
{data ? `第 ${data.page} 页 · 当前 ${data.items.length} 条` : '加载中'}
|
||||
</Typography.Text>
|
||||
}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isFetching}
|
||||
total={data?.total ?? 0}
|
||||
page={data?.page ?? page}
|
||||
pageSize={data?.page_size ?? pageSize}
|
||||
onPageChange={(nextPage, nextPageSize) => {
|
||||
setPage(nextPageSize !== pageSize ? 1 : nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
}}
|
||||
onRowClick={canView ? row => openDetail(row) : undefined}
|
||||
emptyDescription="没有匹配的用户"
|
||||
/>
|
||||
|
||||
{canView ? (
|
||||
<AdminUserDetailDrawer
|
||||
user={detailUser}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
onUpdated={updated => {
|
||||
setDetailUser(updated)
|
||||
void refetch()
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<string, unknown> = { ...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<ApiResponse<Partial<AdminUserPage>>>('/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<ApiResponse<AdminUserItem>>(
|
||||
`/admin/users/${id}/freeze`,
|
||||
{ reason }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function unfreezeAdminUser(id: number | string, reason: string) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminUserItem>>(
|
||||
`/admin/users/${id}/unfreeze`,
|
||||
{ reason }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function setAdminUserDepositFreeQuota(id: number | string, amountCent: number) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminUserItem>>(
|
||||
`/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<ApiResponse<AdminUserItem>>(
|
||||
`/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<ApiResponse<AdminUserItem>>(
|
||||
`/admin/users/${id}/manual-realname`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function revokeAdminUserRealname(id: number | string, reason: string) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminUserItem>>(
|
||||
`/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<ApiResponse<AdminUserItem>>(
|
||||
`/admin/users/${id}/wallet/adjust`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -155,3 +155,27 @@ export const offlineSettlementStatusOptions = ['pending', 'settled'].map(value =
|
||||
value,
|
||||
label: offlineSettlementStatusMap[value].label,
|
||||
}))
|
||||
|
||||
export const userStatusMap: Record<string, StatusMeta> = {
|
||||
active: { label: '正常', tone: 'success' },
|
||||
frozen: { label: '已冻结', tone: 'red' },
|
||||
disabled: { label: '已禁用', tone: 'default' },
|
||||
}
|
||||
|
||||
export const realnameStatusMap: Record<string, StatusMeta> = {
|
||||
unverified: { label: '未实名', tone: 'default' },
|
||||
pending: { label: '认证中', tone: 'gold' },
|
||||
verified: { label: '已实名', tone: 'success' },
|
||||
rejected: { label: '认证失败', tone: 'red' },
|
||||
}
|
||||
|
||||
export const riskStatusMap: Record<string, StatusMeta> = {
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user