拆分打手个人中心
This commit is contained in:
@@ -0,0 +1,225 @@
|
|||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Descriptions,
|
||||||
|
Form,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Typography,
|
||||||
|
} from 'antd'
|
||||||
|
import type { FormInstance } from 'antd'
|
||||||
|
|
||||||
|
import type { WorkerFinanceConfig, WorkerSessionDevice, WorkerUser } from '@/types/worker-platform'
|
||||||
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
|
import { isWorkerPasswordStrong, WORKER_PASSWORD_RULE_MESSAGE } from '@/utils/worker-password'
|
||||||
|
|
||||||
|
import { formatMoney, formatWorkerStatus } from './worker-profile-view-utils'
|
||||||
|
|
||||||
|
export type PasswordFormValues = {
|
||||||
|
currentPassword?: string
|
||||||
|
newPassword?: string
|
||||||
|
confirmPassword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileAccountModals({
|
||||||
|
isMobile,
|
||||||
|
worker,
|
||||||
|
financeConfig,
|
||||||
|
devicesOpen,
|
||||||
|
setDevicesOpen,
|
||||||
|
devices,
|
||||||
|
maxDevices,
|
||||||
|
devicesLoading,
|
||||||
|
removeWorkerDevice,
|
||||||
|
passwordOpen,
|
||||||
|
setPasswordOpen,
|
||||||
|
submittingPassword,
|
||||||
|
passwordForm,
|
||||||
|
submitPasswordChange,
|
||||||
|
contactOpen,
|
||||||
|
setContactOpen,
|
||||||
|
}: {
|
||||||
|
isMobile: boolean
|
||||||
|
worker: WorkerUser | undefined
|
||||||
|
financeConfig: WorkerFinanceConfig | undefined
|
||||||
|
devicesOpen: boolean
|
||||||
|
setDevicesOpen: (open: boolean) => void
|
||||||
|
devices: WorkerSessionDevice[]
|
||||||
|
maxDevices: number
|
||||||
|
devicesLoading: boolean
|
||||||
|
removeWorkerDevice: (device: WorkerSessionDevice) => Promise<void>
|
||||||
|
passwordOpen: boolean
|
||||||
|
setPasswordOpen: (open: boolean) => void
|
||||||
|
submittingPassword: boolean
|
||||||
|
passwordForm: FormInstance<PasswordFormValues>
|
||||||
|
submitPasswordChange: (values: PasswordFormValues) => Promise<void>
|
||||||
|
contactOpen: boolean
|
||||||
|
setContactOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
title="登录设备"
|
||||||
|
open={devicesOpen}
|
||||||
|
width={isMobile ? '92%' : 720}
|
||||||
|
destroyOnHidden
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => setDevicesOpen(false)}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="info"
|
||||||
|
message={`所有设备合计最多同时在线 ${maxDevices} 台,删除旧设备后才能在新设备登录。`}
|
||||||
|
/>
|
||||||
|
<Table<WorkerSessionDevice>
|
||||||
|
size="small"
|
||||||
|
loading={devicesLoading}
|
||||||
|
rowKey="sessionId"
|
||||||
|
dataSource={devices}
|
||||||
|
pagination={false}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '设备',
|
||||||
|
render: (_, item) => (
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
|
<Typography.Text strong>{item.deviceName || item.deviceType}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">{item.ipAddress || '未知 IP'}</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: '最近活动', render: (_, item) => formatDateTime(item.lastSeenAt) },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 100,
|
||||||
|
render: (_, item) => (
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
type="link"
|
||||||
|
disabled={item.current}
|
||||||
|
onClick={() => void removeWorkerDevice(item)}
|
||||||
|
>
|
||||||
|
{item.current ? '当前设备' : '删除'}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="修改密码"
|
||||||
|
open={passwordOpen}
|
||||||
|
width={isMobile ? '92%' : 480}
|
||||||
|
destroyOnHidden
|
||||||
|
confirmLoading={submittingPassword}
|
||||||
|
onCancel={() => {
|
||||||
|
setPasswordOpen(false)
|
||||||
|
passwordForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => passwordForm.submit()}
|
||||||
|
>
|
||||||
|
<Form form={passwordForm} layout="vertical" onFinish={submitPasswordChange}>
|
||||||
|
<Form.Item
|
||||||
|
label="当前密码"
|
||||||
|
name="currentPassword"
|
||||||
|
rules={[{ required: true, message: '请输入当前密码' }]}
|
||||||
|
>
|
||||||
|
<Input.Password autoComplete="current-password" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="新密码"
|
||||||
|
name="newPassword"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入新密码' },
|
||||||
|
{
|
||||||
|
validator: (_, value) =>
|
||||||
|
!value || isWorkerPasswordStrong(value)
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error(WORKER_PASSWORD_RULE_MESSAGE)),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
extra={WORKER_PASSWORD_RULE_MESSAGE}
|
||||||
|
>
|
||||||
|
<Input.Password autoComplete="new-password" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="确认新密码"
|
||||||
|
name="confirmPassword"
|
||||||
|
dependencies={['newPassword']}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请再次输入新密码' },
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
return !value || value === getFieldValue('newPassword')
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error('两次输入的新密码不一致'))
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password autoComplete="new-password" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="联系管理员"
|
||||||
|
open={contactOpen}
|
||||||
|
width={isMobile ? '92%' : 500}
|
||||||
|
destroyOnHidden
|
||||||
|
footer={
|
||||||
|
<Button type="primary" onClick={() => setContactOpen(false)}>
|
||||||
|
我知道了
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
onCancel={() => setContactOpen(false)}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={16} className="full-width">
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="info"
|
||||||
|
message="如需取消已接订单,请扫码联系管理员处理。充值、提现与审核问题也由后台管理员人工处理。"
|
||||||
|
/>
|
||||||
|
{financeConfig?.adminContact?.wechatQrCodeImage ? (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
width={200}
|
||||||
|
src={
|
||||||
|
financeConfig.adminContact.wechatQrCodeImage.mediumUrl ||
|
||||||
|
financeConfig.adminContact.wechatQrCodeImage.url
|
||||||
|
}
|
||||||
|
alt="管理员微信二维码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
管理员暂未配置微信二维码,请按下方账号信息联系。
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
<Descriptions column={1} bordered size="small">
|
||||||
|
<Descriptions.Item label="账号">
|
||||||
|
<Typography.Text copyable>{worker?.username || '-'}</Typography.Text>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="昵称">{worker?.displayName || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="手机号">
|
||||||
|
<Typography.Text copyable>{worker?.phone || '-'}</Typography.Text>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="账号状态">
|
||||||
|
{formatWorkerStatus(worker?.status || '')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="当前余额">
|
||||||
|
{formatMoney(worker?.wallet.availableAmount)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<Typography.Paragraph className="worker-profile-help-text">
|
||||||
|
建议沟通时一起提供:申请类型、金额、订单号或转账备注、收款方式,以及当前账号信息。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import {
|
||||||
|
BankOutlined,
|
||||||
|
CopyOutlined,
|
||||||
|
CustomerServiceOutlined,
|
||||||
|
GlobalOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
LogoutOutlined,
|
||||||
|
WalletOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { Avatar, Button, Card, Descriptions, Progress, Space, Tag, Typography } from 'antd'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
WorkerLevelProgress,
|
||||||
|
WorkerProfileSummary,
|
||||||
|
WorkerUser,
|
||||||
|
WorkerWithdrawalAccount,
|
||||||
|
} from '@/types/worker-platform'
|
||||||
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatMoney,
|
||||||
|
formatWorkerStatus,
|
||||||
|
formatWithdrawChannel,
|
||||||
|
resolveWorkerInitial,
|
||||||
|
resolveWorkerStatusColor,
|
||||||
|
} from './worker-profile-view-utils'
|
||||||
|
|
||||||
|
export function WorkerProfileDesktopIdentity({
|
||||||
|
worker,
|
||||||
|
summary,
|
||||||
|
levelProgress,
|
||||||
|
onCopyInviteCode,
|
||||||
|
}: {
|
||||||
|
worker: WorkerUser
|
||||||
|
summary: WorkerProfileSummary | undefined
|
||||||
|
levelProgress: WorkerLevelProgress | null | undefined
|
||||||
|
onCopyInviteCode: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card size="small">
|
||||||
|
<Space size={16} wrap align="start">
|
||||||
|
<Avatar size={64}>{resolveWorkerInitial(worker)}</Avatar>
|
||||||
|
<Space direction="vertical" size={2}>
|
||||||
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
|
{worker.displayName || worker.username}
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Text type="secondary">账号:{worker.username}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">手机号:{worker.phone || '-'}</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Space size={[8, 8]} wrap>
|
||||||
|
<Tag color={resolveWorkerStatusColor(worker.status)}>
|
||||||
|
{formatWorkerStatus(worker.status)}
|
||||||
|
</Tag>
|
||||||
|
<Tag color={worker.workerType === 'internal' ? 'blue' : 'default'}>
|
||||||
|
{worker.workerType === 'internal' ? '内部打手' : '外部打手'}
|
||||||
|
</Tag>
|
||||||
|
<Tag>{worker.level?.name || '未分级'}</Tag>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
<Space direction="vertical" size={4} style={{ marginTop: 12, width: '100%' }}>
|
||||||
|
<Space wrap size={[8, 8]}>
|
||||||
|
<Typography.Text type="secondary">我的邀请码:</Typography.Text>
|
||||||
|
<Typography.Text strong>{worker.inviteCode || '-'}</Typography.Text>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
disabled={!worker.inviteCode}
|
||||||
|
onClick={onCopyInviteCode}
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
{levelProgress && levelProgress.nextThreshold !== null ? (
|
||||||
|
<Space direction="vertical" size={0} style={{ width: '100%' }}>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
已累计完成 {summary?.acceptedOrderCount || 0} 单,距下一等级还需{' '}
|
||||||
|
{Math.max(0, levelProgress.nextThreshold - (summary?.acceptedOrderCount || 0))} 单
|
||||||
|
</Typography.Text>
|
||||||
|
<Progress
|
||||||
|
percent={levelProgress.progressPercent}
|
||||||
|
size="small"
|
||||||
|
format={(percent) => `${percent}%`}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
已累计完成 {summary?.acceptedOrderCount || 0} 单,当前已是最高等级
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileDesktopWallet({ worker }: { worker: WorkerUser }) {
|
||||||
|
const items = [
|
||||||
|
['可用余额', worker.wallet.availableAmount, '可用于抢单冻结或提现申请'],
|
||||||
|
['冻结押金', worker.wallet.frozenDepositAmount, '抢单后冻结,验收后进入待解冻'],
|
||||||
|
['待解冻押金', worker.wallet.pendingUnfreezeAmount, '验收后按配置天数解冻到账'],
|
||||||
|
['累计结算', worker.wallet.totalSettledAmount, '已结算入账的报酬合计'],
|
||||||
|
] as const
|
||||||
|
return (
|
||||||
|
<Card size="small" className="worker-profile-wallet-card">
|
||||||
|
<div className="worker-profile-wallet-strip">
|
||||||
|
{items.map(([label, amount, note]) => (
|
||||||
|
<div key={label} className="worker-profile-wallet-item">
|
||||||
|
<span className="worker-profile-wallet-label">{label}</span>
|
||||||
|
<strong className="worker-profile-wallet-value">{formatMoney(amount)}</strong>
|
||||||
|
<small className="worker-profile-wallet-note">{note}</small>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileDesktopActions({
|
||||||
|
canUseFinanceActions,
|
||||||
|
canAddWithdrawalAccount,
|
||||||
|
needsAlipayQrCode,
|
||||||
|
onRecharge,
|
||||||
|
onWithdraw,
|
||||||
|
onAddWithdrawalAccount,
|
||||||
|
onAddAlipayQrCode,
|
||||||
|
onChangePassword,
|
||||||
|
onManageDevices,
|
||||||
|
onContact,
|
||||||
|
onLogout,
|
||||||
|
}: {
|
||||||
|
canUseFinanceActions: boolean
|
||||||
|
canAddWithdrawalAccount: boolean
|
||||||
|
needsAlipayQrCode: boolean
|
||||||
|
onRecharge: () => void
|
||||||
|
onWithdraw: () => void
|
||||||
|
onAddWithdrawalAccount: () => void
|
||||||
|
onAddAlipayQrCode: () => void
|
||||||
|
onChangePassword: () => void
|
||||||
|
onManageDevices: () => void
|
||||||
|
onContact: () => void
|
||||||
|
onLogout: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card title="快捷操作" size="small">
|
||||||
|
<Space wrap size={[12, 12]}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<WalletOutlined />}
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
onClick={onRecharge}
|
||||||
|
>
|
||||||
|
充值申请
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<BankOutlined />}
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
onClick={onWithdraw}
|
||||||
|
>
|
||||||
|
提现申请
|
||||||
|
</Button>
|
||||||
|
{canAddWithdrawalAccount ? (
|
||||||
|
<Button
|
||||||
|
icon={<BankOutlined />}
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
onClick={onAddWithdrawalAccount}
|
||||||
|
>
|
||||||
|
添加提现信息
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{needsAlipayQrCode ? (
|
||||||
|
<Button
|
||||||
|
icon={<BankOutlined />}
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
onClick={onAddAlipayQrCode}
|
||||||
|
>
|
||||||
|
补充支付宝收款码
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button icon={<LockOutlined />} onClick={onChangePassword}>
|
||||||
|
修改密码
|
||||||
|
</Button>
|
||||||
|
<Button icon={<GlobalOutlined />} onClick={onManageDevices}>
|
||||||
|
登录设备
|
||||||
|
</Button>
|
||||||
|
<Button icon={<CustomerServiceOutlined />} onClick={onContact}>
|
||||||
|
联系管理员
|
||||||
|
</Button>
|
||||||
|
<Button danger icon={<LogoutOutlined />} onClick={onLogout}>
|
||||||
|
退出登录
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileDesktopAccountDetails({
|
||||||
|
worker,
|
||||||
|
summary,
|
||||||
|
withdrawalAccounts,
|
||||||
|
canUseFinanceActions,
|
||||||
|
onCopyInviteCode,
|
||||||
|
onAddWithdrawalAccount,
|
||||||
|
}: {
|
||||||
|
worker: WorkerUser
|
||||||
|
summary: WorkerProfileSummary | undefined
|
||||||
|
withdrawalAccounts: WorkerWithdrawalAccount[]
|
||||||
|
canUseFinanceActions: boolean
|
||||||
|
onCopyInviteCode: () => void
|
||||||
|
onAddWithdrawalAccount: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card size="small">
|
||||||
|
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
|
||||||
|
<Descriptions.Item label="账号">{worker.username}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="昵称">{worker.displayName || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="手机号">{worker.phone || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="账号状态">
|
||||||
|
<Tag color={resolveWorkerStatusColor(worker.status)}>
|
||||||
|
{formatWorkerStatus(worker.status)}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="当前等级">{worker.level?.name || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="我的邀请码">
|
||||||
|
<Space size={4}>
|
||||||
|
{worker.inviteCode || '-'}
|
||||||
|
{worker.inviteCode ? (
|
||||||
|
<Button size="small" type="link" icon={<CopyOutlined />} onClick={onCopyInviteCode} />
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="邀请人">
|
||||||
|
{worker.inviter ? worker.inviter.displayName || worker.inviter.username : '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="已累计完成">
|
||||||
|
{summary?.acceptedOrderCount || 0} 单
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="超时订单">{summary?.timeoutOrderCount || 0} 单</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="累计充值">
|
||||||
|
{formatMoney(worker.wallet.totalCreditedAmount)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="提现中金额">
|
||||||
|
{formatMoney(summary?.pendingWithdrawAmount)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="提现信息" span={2}>
|
||||||
|
{withdrawalAccounts.length > 0 ? (
|
||||||
|
<Space direction="vertical" size={2}>
|
||||||
|
{withdrawalAccounts.map((account) => (
|
||||||
|
<Space key={account.accountChannel} wrap size={4}>
|
||||||
|
<Typography.Text>{formatWithdrawChannel(account.accountChannel)}</Typography.Text>
|
||||||
|
<Typography.Text>{account.accountName}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{account.accountChannel === 'wechat'
|
||||||
|
? '已上传收款二维码'
|
||||||
|
: `${account.accountNoMasked} / ${account.alipayQrCodeImage ? '已上传收款二维码' : '未上传收款二维码'}`}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">收款人姓名和账号已锁定</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
onClick={onAddWithdrawalAccount}
|
||||||
|
>
|
||||||
|
添加提现信息
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="免押额度">
|
||||||
|
{formatMoney(worker.level?.permissions.depositFreeAmount || 0)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="最大同时接单">
|
||||||
|
{worker.level?.permissions.maxActiveOrders || 0} 单
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="审核备注">{worker.reviewNote || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="注册时间">{formatDateTime(worker.createdAt)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="最近更新时间">
|
||||||
|
{formatDateTime(worker.updatedAt)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
DatePicker,
|
||||||
|
Descriptions,
|
||||||
|
Form,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
} from 'antd'
|
||||||
|
import type { FormInstance } from 'antd'
|
||||||
|
|
||||||
|
import ImageUpload from '@/components/files/ImageUpload'
|
||||||
|
import type {
|
||||||
|
UploadedFile,
|
||||||
|
WorkerFinanceConfig,
|
||||||
|
WorkerProfileSummary,
|
||||||
|
WorkerUser,
|
||||||
|
WorkerWithdrawalAccount,
|
||||||
|
} from '@/types/worker-platform'
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatMoney,
|
||||||
|
formatWithdrawChannel,
|
||||||
|
hasRechargeConfig,
|
||||||
|
resolveAvailableForWithdraw,
|
||||||
|
} from './worker-profile-view-utils'
|
||||||
|
|
||||||
|
type RechargeFormValues = {
|
||||||
|
amount?: number
|
||||||
|
note?: string
|
||||||
|
paidAt?: string
|
||||||
|
proofFiles?: UploadedFile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithdrawFormValues = {
|
||||||
|
amount?: number
|
||||||
|
accountChannel?: string
|
||||||
|
note?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithdrawalAccountFormValues = {
|
||||||
|
accountChannel?: string
|
||||||
|
accountName?: string
|
||||||
|
accountNo?: string
|
||||||
|
alipayQrCodeImages?: UploadedFile[]
|
||||||
|
wechatQrCodeImages?: UploadedFile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlipayQrCodeFormValues = {
|
||||||
|
alipayQrCodeImages?: UploadedFile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileFinanceModals({
|
||||||
|
isMobile,
|
||||||
|
worker,
|
||||||
|
summary,
|
||||||
|
financeConfig,
|
||||||
|
withdrawalAccounts,
|
||||||
|
selectedWithdrawalAccount,
|
||||||
|
selectedWithdrawChannel,
|
||||||
|
selectedWithdrawalAccountChannel,
|
||||||
|
rechargeOpen,
|
||||||
|
setRechargeOpen,
|
||||||
|
submittingRecharge,
|
||||||
|
rechargeForm,
|
||||||
|
submitRecharge,
|
||||||
|
withdrawalAccountOpen,
|
||||||
|
setWithdrawalAccountOpen,
|
||||||
|
submittingWithdrawalAccount,
|
||||||
|
withdrawalAccountForm,
|
||||||
|
submitWithdrawalAccount,
|
||||||
|
alipayQrCodeOpen,
|
||||||
|
setAlipayQrCodeOpen,
|
||||||
|
alipayQrCodeForm,
|
||||||
|
submitAlipayQrCode,
|
||||||
|
withdrawOpen,
|
||||||
|
setWithdrawOpen,
|
||||||
|
submittingWithdraw,
|
||||||
|
withdrawForm,
|
||||||
|
submitWithdraw,
|
||||||
|
}: {
|
||||||
|
isMobile: boolean
|
||||||
|
worker: WorkerUser | undefined
|
||||||
|
summary: WorkerProfileSummary | undefined
|
||||||
|
financeConfig: WorkerFinanceConfig | undefined
|
||||||
|
withdrawalAccounts: WorkerWithdrawalAccount[]
|
||||||
|
selectedWithdrawalAccount: WorkerWithdrawalAccount | undefined
|
||||||
|
selectedWithdrawChannel: string | undefined
|
||||||
|
selectedWithdrawalAccountChannel: string | undefined
|
||||||
|
rechargeOpen: boolean
|
||||||
|
setRechargeOpen: (open: boolean) => void
|
||||||
|
submittingRecharge: boolean
|
||||||
|
rechargeForm: FormInstance<RechargeFormValues>
|
||||||
|
submitRecharge: (values: RechargeFormValues) => Promise<void>
|
||||||
|
withdrawalAccountOpen: boolean
|
||||||
|
setWithdrawalAccountOpen: (open: boolean) => void
|
||||||
|
submittingWithdrawalAccount: boolean
|
||||||
|
withdrawalAccountForm: FormInstance<WithdrawalAccountFormValues>
|
||||||
|
submitWithdrawalAccount: (values: WithdrawalAccountFormValues) => Promise<void>
|
||||||
|
alipayQrCodeOpen: boolean
|
||||||
|
setAlipayQrCodeOpen: (open: boolean) => void
|
||||||
|
alipayQrCodeForm: FormInstance<AlipayQrCodeFormValues>
|
||||||
|
submitAlipayQrCode: (values: AlipayQrCodeFormValues) => Promise<void>
|
||||||
|
withdrawOpen: boolean
|
||||||
|
setWithdrawOpen: (open: boolean) => void
|
||||||
|
submittingWithdraw: boolean
|
||||||
|
withdrawForm: FormInstance<WithdrawFormValues>
|
||||||
|
submitWithdraw: (values: WithdrawFormValues) => Promise<void>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
title="充值申请"
|
||||||
|
open={rechargeOpen}
|
||||||
|
width={isMobile ? '92%' : 540}
|
||||||
|
destroyOnHidden
|
||||||
|
confirmLoading={submittingRecharge}
|
||||||
|
onCancel={() => {
|
||||||
|
setRechargeOpen(false)
|
||||||
|
rechargeForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => rechargeForm.submit()}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={rechargeForm}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{ amount: undefined, note: '' }}
|
||||||
|
onFinish={submitRecharge}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type={hasRechargeConfig(financeConfig) ? 'info' : 'warning'}
|
||||||
|
message={
|
||||||
|
hasRechargeConfig(financeConfig)
|
||||||
|
? '请先按下方收款信息完成转账,再提交充值申请,管理员核对后会手动入账。'
|
||||||
|
: '管理员暂未配置完整收款信息,如无法转账请先联系管理员确认。'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size={12}
|
||||||
|
className="full-width"
|
||||||
|
style={{ marginTop: 16, marginBottom: 16 }}
|
||||||
|
>
|
||||||
|
{financeConfig?.recharge.qrCodeImage ? (
|
||||||
|
<Image
|
||||||
|
width={180}
|
||||||
|
src={
|
||||||
|
financeConfig.recharge.qrCodeImage.mediumUrl ||
|
||||||
|
financeConfig.recharge.qrCodeImage.url
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Descriptions column={1} bordered size="small">
|
||||||
|
<Descriptions.Item label="收款通道">
|
||||||
|
{financeConfig?.recharge.channelName || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="收款人">
|
||||||
|
{financeConfig?.recharge.accountName || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="收款账号">
|
||||||
|
{financeConfig?.recharge.accountNo ? (
|
||||||
|
<Typography.Text copyable>{financeConfig.recharge.accountNo}</Typography.Text>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="充值说明">
|
||||||
|
{financeConfig?.recharge.instructions || '请转账后填写备注说明,方便管理员核对。'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Space>
|
||||||
|
<Form.Item
|
||||||
|
label="充值金额"
|
||||||
|
name="amount"
|
||||||
|
rules={[{ required: true, message: '请输入充值金额' }]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0.01} step={1} addonAfter="元" className="full-width" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="付款时间"
|
||||||
|
name="paidAt"
|
||||||
|
rules={[{ required: true, message: '请选择付款时间' }]}
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
showTime
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="选择转账完成时间"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="付款凭证"
|
||||||
|
name="proofFiles"
|
||||||
|
extra="上传转账成功截图,便于管理员快速核对入账"
|
||||||
|
>
|
||||||
|
<ImageUpload scene="worker-finance-proof" scope="worker" maxCount={5} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="备注说明" name="note">
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder="填写转账备注、支付时间、截图说明等,方便管理员尽快核对。"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="添加提现信息"
|
||||||
|
open={withdrawalAccountOpen}
|
||||||
|
width={isMobile ? '92%' : 500}
|
||||||
|
destroyOnHidden
|
||||||
|
confirmLoading={submittingWithdrawalAccount}
|
||||||
|
onCancel={() => {
|
||||||
|
setWithdrawalAccountOpen(false)
|
||||||
|
withdrawalAccountForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => withdrawalAccountForm.submit()}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="warning"
|
||||||
|
message="支付宝和微信可分别添加一次;收款人姓名和账号添加后不可自行修改。历史支付宝账户缺少二维码时,仅可补充一次。"
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<Form form={withdrawalAccountForm} layout="vertical" onFinish={submitWithdrawalAccount}>
|
||||||
|
<Form.Item
|
||||||
|
label="提现方式"
|
||||||
|
name="accountChannel"
|
||||||
|
rules={[{ required: true, message: '请选择提现方式' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
...(!withdrawalAccounts.some((account) => account.accountChannel === 'alipay')
|
||||||
|
? [{ value: 'alipay', label: '支付宝' }]
|
||||||
|
: []),
|
||||||
|
...(!withdrawalAccounts.some((account) => account.accountChannel === 'wechat')
|
||||||
|
? [{ value: 'wechat', label: '微信收款(仅支持 100 元及以下提现)' }]
|
||||||
|
: []),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="收款人姓名"
|
||||||
|
name="accountName"
|
||||||
|
rules={[{ required: true, message: '请输入收款人姓名' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入真实姓名" />
|
||||||
|
</Form.Item>
|
||||||
|
{selectedWithdrawalAccountChannel === 'wechat' ? (
|
||||||
|
<Form.Item
|
||||||
|
label="微信收款二维码"
|
||||||
|
name="wechatQrCodeImages"
|
||||||
|
rules={[{ required: true, message: '请上传微信收款二维码' }]}
|
||||||
|
extra="微信提现仅使用收款二维码,不需要填写微信账号。"
|
||||||
|
>
|
||||||
|
<ImageUpload scene="worker-withdrawal-wechat-qr" scope="worker" maxCount={1} />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label="支付宝账号"
|
||||||
|
name="accountNo"
|
||||||
|
rules={[{ required: true, message: '请输入支付宝账号' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入支付宝账号" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="支付宝收款二维码"
|
||||||
|
name="alipayQrCodeImages"
|
||||||
|
rules={[{ required: true, message: '请上传支付宝收款二维码' }]}
|
||||||
|
>
|
||||||
|
<ImageUpload scene="worker-withdrawal-alipay-qr" scope="worker" maxCount={1} />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="补充支付宝收款二维码"
|
||||||
|
open={alipayQrCodeOpen}
|
||||||
|
width={isMobile ? '92%' : 500}
|
||||||
|
destroyOnHidden
|
||||||
|
onCancel={() => {
|
||||||
|
setAlipayQrCodeOpen(false)
|
||||||
|
alipayQrCodeForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => alipayQrCodeForm.submit()}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="info"
|
||||||
|
message="历史账户仅可补充一次支付宝收款二维码,保存后不可自行修改;已登记的收款人姓名和账号保持锁定。"
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<Form form={alipayQrCodeForm} layout="vertical" onFinish={submitAlipayQrCode}>
|
||||||
|
<Form.Item
|
||||||
|
label="支付宝收款二维码"
|
||||||
|
name="alipayQrCodeImages"
|
||||||
|
rules={[{ required: true, message: '请上传支付宝收款二维码' }]}
|
||||||
|
>
|
||||||
|
<ImageUpload scene="worker-withdrawal-alipay-qr" scope="worker" maxCount={1} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="提现申请"
|
||||||
|
open={withdrawOpen}
|
||||||
|
width={isMobile ? '92%' : 540}
|
||||||
|
destroyOnHidden
|
||||||
|
confirmLoading={submittingWithdraw}
|
||||||
|
onCancel={() => {
|
||||||
|
setWithdrawOpen(false)
|
||||||
|
withdrawForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => withdrawForm.submit()}
|
||||||
|
>
|
||||||
|
<Form form={withdrawForm} layout="vertical" onFinish={submitWithdraw}>
|
||||||
|
{financeConfig?.withdraw.instructions ? (
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="info"
|
||||||
|
message={financeConfig.withdraw.instructions}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="warning"
|
||||||
|
message={`每天最多提现 3 次(支付宝、微信共享次数);100 元及以下支持微信和支付宝,超过 100 元仅支持支付宝;有押金的工单验收通过后,押金需 ${Number(financeConfig?.depositUnfreezeDays ?? 3)} 天解冻到账后方可提现。`}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
|
||||||
|
<Descriptions.Item label="提现账户">
|
||||||
|
{formatWithdrawChannel(selectedWithdrawalAccount?.accountChannel || '')} ·{' '}
|
||||||
|
{selectedWithdrawalAccount?.accountName || '-'} ·{' '}
|
||||||
|
{selectedWithdrawalAccount?.accountChannel === 'wechat'
|
||||||
|
? '收款二维码'
|
||||||
|
: selectedWithdrawalAccount?.accountNoMasked || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<Form.Item label="可提现余额">
|
||||||
|
<Typography.Text strong>
|
||||||
|
{formatMoney(resolveAvailableForWithdraw(worker, summary))}
|
||||||
|
</Typography.Text>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="提现方式"
|
||||||
|
name="accountChannel"
|
||||||
|
rules={[{ required: true, message: '请选择提现方式' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={withdrawalAccounts.map((account) => ({
|
||||||
|
value: account.accountChannel,
|
||||||
|
label:
|
||||||
|
account.accountChannel === 'wechat'
|
||||||
|
? '微信收款 · 收款二维码'
|
||||||
|
: `${formatWithdrawChannel(account.accountChannel)} · ${account.accountNoMasked}`,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="提现金额"
|
||||||
|
name="amount"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入提现金额' },
|
||||||
|
{
|
||||||
|
validator(_, value) {
|
||||||
|
return Number(value || 0) > 100 && selectedWithdrawChannel !== 'alipay'
|
||||||
|
? Promise.reject(
|
||||||
|
new Error('超过 100 元仅支持支付宝提现,请联系客服修改提现信息'),
|
||||||
|
)
|
||||||
|
: Promise.resolve()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0.01} step={1} addonAfter="元" className="full-width" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="备注说明" name="note">
|
||||||
|
<Input.TextArea rows={4} placeholder="可补充到账要求、手机号、开户行等说明。" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
import {
|
||||||
|
BankOutlined,
|
||||||
|
CopyOutlined,
|
||||||
|
CustomerServiceOutlined,
|
||||||
|
GlobalOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
LogoutOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
WalletOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { Alert, Avatar, Button, Progress, Tag } from 'antd'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import type { WorkerLevelProgress, WorkerProfileSummary, WorkerUser } from '@/types/worker-platform'
|
||||||
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatMoney,
|
||||||
|
formatWorkerStatus,
|
||||||
|
resolveWorkerInitial,
|
||||||
|
resolveWorkerStatusColor,
|
||||||
|
} from './worker-profile-view-utils'
|
||||||
|
|
||||||
|
type WorkerProfileMobileViewProps = {
|
||||||
|
worker: WorkerUser
|
||||||
|
summary: WorkerProfileSummary | undefined
|
||||||
|
levelProgress: WorkerLevelProgress | null | undefined
|
||||||
|
canUseFinanceActions: boolean
|
||||||
|
needsAlipayQrCode: boolean
|
||||||
|
activeTab: string
|
||||||
|
loading: boolean
|
||||||
|
ledgerRecords: ReactNode
|
||||||
|
requestRecords: ReactNode
|
||||||
|
onActiveTabChange: (tab: string) => void
|
||||||
|
onRefresh: () => void
|
||||||
|
onCopyInviteCode: () => void
|
||||||
|
onRecharge: () => void
|
||||||
|
onWithdraw: () => void
|
||||||
|
onChangePassword: () => void
|
||||||
|
onManageDevices: () => void
|
||||||
|
onContact: () => void
|
||||||
|
onAddAlipayQrCode: () => void
|
||||||
|
onLogout: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileMobileView({
|
||||||
|
worker,
|
||||||
|
summary,
|
||||||
|
levelProgress,
|
||||||
|
canUseFinanceActions,
|
||||||
|
needsAlipayQrCode,
|
||||||
|
activeTab,
|
||||||
|
loading,
|
||||||
|
ledgerRecords,
|
||||||
|
requestRecords,
|
||||||
|
onActiveTabChange,
|
||||||
|
onRefresh,
|
||||||
|
onCopyInviteCode,
|
||||||
|
onRecharge,
|
||||||
|
onWithdraw,
|
||||||
|
onChangePassword,
|
||||||
|
onManageDevices,
|
||||||
|
onContact,
|
||||||
|
onAddAlipayQrCode,
|
||||||
|
onLogout,
|
||||||
|
}: WorkerProfileMobileViewProps) {
|
||||||
|
return (
|
||||||
|
<div className="worker-profile-mobile-page">
|
||||||
|
<div className="worker-profile-mobile-hero">
|
||||||
|
<div className="worker-profile-mobile-user-row">
|
||||||
|
<Avatar size={52} className="worker-profile-mobile-avatar">
|
||||||
|
{resolveWorkerInitial(worker)}
|
||||||
|
</Avatar>
|
||||||
|
<div className="worker-profile-mobile-user-info">
|
||||||
|
<div className="worker-profile-mobile-name-row">
|
||||||
|
<strong className="worker-profile-mobile-name">
|
||||||
|
{worker.displayName || worker.username}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div className="worker-profile-mobile-tags-row">
|
||||||
|
<span className="worker-profile-mobile-vip-badge">
|
||||||
|
{worker.level?.name || '未分级'}
|
||||||
|
</span>
|
||||||
|
<Tag
|
||||||
|
color={resolveWorkerStatusColor(worker.status)}
|
||||||
|
className="worker-profile-mobile-tag"
|
||||||
|
>
|
||||||
|
{formatWorkerStatus(worker.status)}
|
||||||
|
</Tag>
|
||||||
|
<Tag className="worker-profile-mobile-tag">
|
||||||
|
{worker.workerType === 'internal' ? '内部打手' : '外部打手'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div className="worker-profile-mobile-account-text">
|
||||||
|
账号: {worker.username} {worker.phone ? `· ${worker.phone}` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
shape="circle"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
className="worker-profile-mobile-refresh-btn"
|
||||||
|
loading={loading}
|
||||||
|
onClick={onRefresh}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="worker-profile-mobile-hero-bottom">
|
||||||
|
<div className="worker-profile-mobile-invite-bar">
|
||||||
|
<span className="worker-profile-mobile-invite-label">我的邀请码:</span>
|
||||||
|
<strong className="worker-profile-mobile-invite-code">
|
||||||
|
{worker.inviteCode || '-'}
|
||||||
|
</strong>
|
||||||
|
{worker.inviteCode ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="worker-profile-mobile-copy-btn"
|
||||||
|
onClick={onCopyInviteCode}
|
||||||
|
>
|
||||||
|
<CopyOutlined /> 复制
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{levelProgress && levelProgress.nextThreshold !== null ? (
|
||||||
|
<div className="worker-profile-mobile-level-box">
|
||||||
|
<div className="worker-profile-mobile-level-text">
|
||||||
|
已完成 {summary?.acceptedOrderCount || 0} 单 · 距下一级还需{' '}
|
||||||
|
{Math.max(0, levelProgress.nextThreshold - (summary?.acceptedOrderCount || 0))} 单
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
percent={levelProgress.progressPercent}
|
||||||
|
size="small"
|
||||||
|
strokeColor={{ '0%': '#f97316', '100%': '#fbbf24' }}
|
||||||
|
showInfo={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="worker-profile-mobile-level-text">
|
||||||
|
已完成 {summary?.acceptedOrderCount || 0} 单 · 当前已是最高等级
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!canUseFinanceActions ? (
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="warning"
|
||||||
|
message="当前账号尚未通过审核,充值申请、提现申请和接单功能暂不可用。"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="worker-profile-mobile-wallet-card">
|
||||||
|
<div className="worker-profile-mobile-wallet-top">
|
||||||
|
<div className="worker-profile-mobile-wallet-main">
|
||||||
|
<span className="worker-profile-mobile-wallet-title">可用余额</span>
|
||||||
|
<strong className="worker-profile-mobile-wallet-amount">
|
||||||
|
{formatMoney(worker.wallet.availableAmount)}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div className="worker-profile-mobile-wallet-actions">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="middle"
|
||||||
|
icon={<WalletOutlined />}
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
className="worker-profile-mobile-recharge-btn"
|
||||||
|
onClick={onRecharge}
|
||||||
|
>
|
||||||
|
充值
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="middle"
|
||||||
|
icon={<BankOutlined />}
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
className="worker-profile-mobile-withdraw-btn"
|
||||||
|
onClick={onWithdraw}
|
||||||
|
>
|
||||||
|
提现
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="worker-profile-mobile-wallet-grid">
|
||||||
|
<MobileWalletItem label="冻结押金" amount={worker.wallet.frozenDepositAmount} />
|
||||||
|
<MobileWalletItem label="待解冻押金" amount={worker.wallet.pendingUnfreezeAmount} />
|
||||||
|
<MobileWalletItem label="累计结算" amount={worker.wallet.totalSettledAmount} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="worker-profile-mobile-tools-card">
|
||||||
|
<MobileTool
|
||||||
|
label="修改密码"
|
||||||
|
icon={<LockOutlined />}
|
||||||
|
color="#3b82f6"
|
||||||
|
background="#eff6ff"
|
||||||
|
onClick={onChangePassword}
|
||||||
|
/>
|
||||||
|
<MobileTool
|
||||||
|
label="登录设备"
|
||||||
|
icon={<GlobalOutlined />}
|
||||||
|
color="#0f766e"
|
||||||
|
background="#f0fdfa"
|
||||||
|
onClick={onManageDevices}
|
||||||
|
/>
|
||||||
|
<MobileTool
|
||||||
|
label="联系客服"
|
||||||
|
icon={<CustomerServiceOutlined />}
|
||||||
|
color="#16a34a"
|
||||||
|
background="#f0fdf4"
|
||||||
|
onClick={onContact}
|
||||||
|
/>
|
||||||
|
<MobileTool
|
||||||
|
label="邀请好友"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
color="#d97706"
|
||||||
|
background="#fef3c7"
|
||||||
|
disabled={!worker.inviteCode}
|
||||||
|
onClick={onCopyInviteCode}
|
||||||
|
/>
|
||||||
|
{needsAlipayQrCode ? (
|
||||||
|
<MobileTool
|
||||||
|
label="补充支付宝收款码"
|
||||||
|
icon={<BankOutlined />}
|
||||||
|
color="#7c3aed"
|
||||||
|
background="#f5f3ff"
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
onClick={onAddAlipayQrCode}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<MobileTool
|
||||||
|
label="退出登录"
|
||||||
|
icon={<LogoutOutlined />}
|
||||||
|
color="#ef4444"
|
||||||
|
background="#fef2f2"
|
||||||
|
onClick={onLogout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="worker-profile-mobile-sections-card">
|
||||||
|
<div className="worker-profile-mobile-tab-nav">
|
||||||
|
<MobileTab
|
||||||
|
active={activeTab === 'ledger'}
|
||||||
|
label="资金流水"
|
||||||
|
onClick={() => onActiveTabChange('ledger')}
|
||||||
|
/>
|
||||||
|
<MobileTab
|
||||||
|
active={activeTab === 'requests'}
|
||||||
|
label="申请记录"
|
||||||
|
onClick={() => onActiveTabChange('requests')}
|
||||||
|
/>
|
||||||
|
<MobileTab
|
||||||
|
active={activeTab === 'account'}
|
||||||
|
label="账号详情"
|
||||||
|
onClick={() => onActiveTabChange('account')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="worker-profile-mobile-tab-body">
|
||||||
|
{activeTab === 'ledger' ? ledgerRecords : null}
|
||||||
|
{activeTab === 'requests' ? requestRecords : null}
|
||||||
|
{activeTab === 'account' ? (
|
||||||
|
<WorkerProfileMobileAccountDetails worker={worker} summary={summary} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileWalletItem({ label, amount }: { label: string; amount: number }) {
|
||||||
|
return (
|
||||||
|
<div className="worker-profile-mobile-wallet-col">
|
||||||
|
<span className="worker-profile-mobile-sub-label">{label}</span>
|
||||||
|
<strong className="worker-profile-mobile-sub-val">{formatMoney(amount)}</strong>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileTool({
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
color,
|
||||||
|
background,
|
||||||
|
disabled = false,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
icon: ReactNode
|
||||||
|
color: string
|
||||||
|
background: string
|
||||||
|
disabled?: boolean
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="worker-profile-mobile-tool-item"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="worker-profile-mobile-tool-icon" style={{ background, color }}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<span>{label}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileTab({
|
||||||
|
active,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
active: boolean
|
||||||
|
label: string
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`worker-profile-mobile-tab-btn ${active ? 'active' : ''}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkerProfileMobileAccountDetails({
|
||||||
|
worker,
|
||||||
|
summary,
|
||||||
|
}: {
|
||||||
|
worker: WorkerUser
|
||||||
|
summary: WorkerProfileSummary | undefined
|
||||||
|
}) {
|
||||||
|
const items: Array<[string, ReactNode]> = [
|
||||||
|
['账号', worker.username],
|
||||||
|
['昵称', worker.displayName || '-'],
|
||||||
|
['手机号', worker.phone || '-'],
|
||||||
|
[
|
||||||
|
'账号状态',
|
||||||
|
<Tag key="worker-status" color={resolveWorkerStatusColor(worker.status)}>
|
||||||
|
{formatWorkerStatus(worker.status)}
|
||||||
|
</Tag>,
|
||||||
|
],
|
||||||
|
['当前等级', worker.level?.name || '-'],
|
||||||
|
['免押额度', formatMoney(worker.level?.permissions.depositFreeAmount || 0)],
|
||||||
|
['最大同时接单', `${worker.level?.permissions.maxActiveOrders || 0} 单`],
|
||||||
|
['邀请人', worker.inviter ? worker.inviter.displayName || worker.inviter.username : '-'],
|
||||||
|
['已完成订单', `${summary?.acceptedOrderCount || 0} 单`],
|
||||||
|
['累计充值', formatMoney(worker.wallet.totalCreditedAmount)],
|
||||||
|
['注册时间', formatDateTime(worker.createdAt)],
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="worker-profile-mobile-tab-pane">
|
||||||
|
<div className="worker-profile-mobile-account-list">
|
||||||
|
{items.map(([label, value]) => (
|
||||||
|
<div key={label} className="worker-profile-mobile-account-item">
|
||||||
|
<span className="worker-profile-mobile-acc-label">{label}</span>
|
||||||
|
<span className="worker-profile-mobile-acc-val">{value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,368 @@
|
|||||||
|
import { ReloadOutlined } from '@ant-design/icons'
|
||||||
|
import { Button, Card, Empty, Pagination, Select, Space, Spin, Table, Tag } from 'antd'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
WorkerFinanceRequest,
|
||||||
|
WorkerListResponse,
|
||||||
|
WorkerWalletLedger,
|
||||||
|
} from '@/types/worker-platform'
|
||||||
|
import type { ApiEnvelope } from '@/lib/http'
|
||||||
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatLedgerType,
|
||||||
|
formatMoney,
|
||||||
|
formatRequestStatus,
|
||||||
|
formatRequestType,
|
||||||
|
formatWithdrawChannel,
|
||||||
|
maskAccountNo,
|
||||||
|
renderRechargeRequestSummary,
|
||||||
|
resolveLedgerTagColor,
|
||||||
|
resolveRequestStatusColor,
|
||||||
|
workerFinanceRequestColumns,
|
||||||
|
workerLedgerColumns,
|
||||||
|
} from './worker-profile-view-utils'
|
||||||
|
|
||||||
|
const LEDGER_TYPE_OPTIONS = [
|
||||||
|
{ value: '', label: '全部流水' },
|
||||||
|
{ value: 'manual_credit', label: '人工充值' },
|
||||||
|
{ value: 'withdraw_paid', label: '提现打款' },
|
||||||
|
{ value: 'deposit_freeze', label: '冻结押金' },
|
||||||
|
{ value: 'deposit_release', label: '释放押金' },
|
||||||
|
{ value: 'deposit_pending_unfreeze', label: '押金待解冻' },
|
||||||
|
{ value: 'deposit_unfreeze', label: '押金已解冻' },
|
||||||
|
{ value: 'deposit_deduction', label: '扣除押金' },
|
||||||
|
{ value: 'reward_settlement', label: '结算报酬' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const REQUEST_TYPE_OPTIONS = [
|
||||||
|
{ value: '', label: '全部类型' },
|
||||||
|
{ value: 'recharge', label: '充值申请' },
|
||||||
|
{ value: 'withdraw', label: '提现申请' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const REQUEST_STATUS_OPTIONS = [
|
||||||
|
{ value: '', label: '全部状态' },
|
||||||
|
{ value: 'pending', label: '待处理' },
|
||||||
|
{ value: 'approved', label: '已通过' },
|
||||||
|
{ value: 'rejected', label: '已驳回' },
|
||||||
|
]
|
||||||
|
|
||||||
|
type LedgerRecordsProps = {
|
||||||
|
data: ApiEnvelope<WorkerListResponse<WorkerWalletLedger>> | undefined
|
||||||
|
error: unknown
|
||||||
|
loading: boolean
|
||||||
|
refreshing: boolean
|
||||||
|
ledgerType: string
|
||||||
|
onLedgerTypeChange: (ledgerType: string) => void
|
||||||
|
onReload: () => void
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
onPageChange: (page: number, pageSize: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestRecordsProps = {
|
||||||
|
data: ApiEnvelope<WorkerListResponse<WorkerFinanceRequest>> | undefined
|
||||||
|
error: unknown
|
||||||
|
loading: boolean
|
||||||
|
refreshing: boolean
|
||||||
|
requestType: string
|
||||||
|
requestStatus: string
|
||||||
|
onRequestTypeChange: (requestType: string) => void
|
||||||
|
onRequestStatusChange: (requestStatus: string) => void
|
||||||
|
onReload: () => void
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
onPageChange: (page: number, pageSize: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileMobileLedgerRecords({
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
refreshing,
|
||||||
|
ledgerType,
|
||||||
|
onLedgerTypeChange,
|
||||||
|
onReload,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
onPageChange,
|
||||||
|
}: LedgerRecordsProps) {
|
||||||
|
const items = data?.data.items || []
|
||||||
|
const pagination = data?.data.pagination
|
||||||
|
return (
|
||||||
|
<div className="worker-profile-mobile-tab-pane">
|
||||||
|
<div className="worker-profile-mobile-filter-row">
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={ledgerType}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
options={LEDGER_TYPE_OPTIONS}
|
||||||
|
onChange={onLedgerTypeChange}
|
||||||
|
/>
|
||||||
|
<Button size="small" icon={<ReloadOutlined />} loading={refreshing} onClick={onReload}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="worker-profile-mobile-records">
|
||||||
|
{loading ? (
|
||||||
|
<div className="worker-hall-loading">
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<Empty description="暂无钱包流水" />
|
||||||
|
) : (
|
||||||
|
items.map((row) => (
|
||||||
|
<div key={row.ledgerId} className="worker-profile-record-card">
|
||||||
|
<div className="worker-profile-record-row-head">
|
||||||
|
<Tag color={resolveLedgerTagColor(row.ledgerType)}>
|
||||||
|
{formatLedgerType(row.ledgerType)}
|
||||||
|
</Tag>
|
||||||
|
<strong
|
||||||
|
className={row.amount >= 0 ? 'worker-amount-positive' : 'worker-amount-negative'}
|
||||||
|
style={{ fontSize: 16 }}
|
||||||
|
>
|
||||||
|
{row.amount >= 0 ? '+' : ''}
|
||||||
|
{formatMoney(row.amount)}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div className="worker-profile-record-row-meta">
|
||||||
|
<span>变动后余额: {formatMoney(row.balanceAfter)}</span>
|
||||||
|
<span>冻结押金: {formatMoney(row.frozenAfter)}</span>
|
||||||
|
</div>
|
||||||
|
{row.note ? <div className="worker-profile-record-note">说明: {row.note}</div> : null}
|
||||||
|
{row.relatedPlatformOrderId ? (
|
||||||
|
<div className="worker-profile-record-sub">订单号 {row.relatedPlatformOrderId}</div>
|
||||||
|
) : null}
|
||||||
|
<div className="worker-profile-record-time">
|
||||||
|
{formatDateTime(String(row.createdAt || ''))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{(pagination?.total || 0) > 0 ? (
|
||||||
|
<div className="worker-orders-mobile-pagination">
|
||||||
|
<Pagination
|
||||||
|
current={pagination?.page || page}
|
||||||
|
pageSize={pagination?.pageSize || pageSize}
|
||||||
|
total={pagination?.total || 0}
|
||||||
|
simple
|
||||||
|
onChange={onPageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileMobileRequestRecords({
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
refreshing,
|
||||||
|
requestType,
|
||||||
|
requestStatus,
|
||||||
|
onRequestTypeChange,
|
||||||
|
onRequestStatusChange,
|
||||||
|
onReload,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
onPageChange,
|
||||||
|
}: RequestRecordsProps) {
|
||||||
|
const items = data?.data.items || []
|
||||||
|
const pagination = data?.data.pagination
|
||||||
|
return (
|
||||||
|
<div className="worker-profile-mobile-tab-pane">
|
||||||
|
<div className="worker-profile-mobile-filter-row">
|
||||||
|
<Space size={6} wrap>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={requestType}
|
||||||
|
style={{ width: 110 }}
|
||||||
|
options={REQUEST_TYPE_OPTIONS}
|
||||||
|
onChange={onRequestTypeChange}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={requestStatus}
|
||||||
|
style={{ width: 110 }}
|
||||||
|
options={REQUEST_STATUS_OPTIONS}
|
||||||
|
onChange={onRequestStatusChange}
|
||||||
|
/>
|
||||||
|
<Button size="small" icon={<ReloadOutlined />} loading={refreshing} onClick={onReload} />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div className="worker-profile-mobile-records">
|
||||||
|
{loading ? (
|
||||||
|
<div className="worker-hall-loading">
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<Empty description="暂无申请记录" />
|
||||||
|
) : (
|
||||||
|
items.map((row) => (
|
||||||
|
<div key={row.requestId} className="worker-profile-record-card">
|
||||||
|
<div className="worker-profile-record-row-head">
|
||||||
|
<Space size={4}>
|
||||||
|
<Tag color={row.requestType === 'withdraw' ? 'green' : 'blue'}>
|
||||||
|
{formatRequestType(row.requestType)}
|
||||||
|
</Tag>
|
||||||
|
<Tag color={resolveRequestStatusColor(row.status)}>
|
||||||
|
{formatRequestStatus(row.status)}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
<strong style={{ fontSize: 16 }}>{formatMoney(row.amount)}</strong>
|
||||||
|
</div>
|
||||||
|
{row.requestType === 'withdraw' ? (
|
||||||
|
<div className="worker-profile-record-row-meta">
|
||||||
|
收款: {formatWithdrawChannel(row.accountChannel)} / {row.accountName || '-'} (
|
||||||
|
{maskAccountNo(row.accountNo)})
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="worker-profile-record-row-meta">
|
||||||
|
{renderRechargeRequestSummary(row)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{row.note ? <div className="worker-profile-record-note">备注: {row.note}</div> : null}
|
||||||
|
{row.reviewedNote ? (
|
||||||
|
<div className="worker-profile-record-note" style={{ color: '#d97706' }}>
|
||||||
|
审核备注: {row.reviewedNote}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="worker-profile-record-time">
|
||||||
|
{formatDateTime(String(row.createdAt || ''))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{(pagination?.total || 0) > 0 ? (
|
||||||
|
<div className="worker-orders-mobile-pagination">
|
||||||
|
<Pagination
|
||||||
|
current={pagination?.page || page}
|
||||||
|
pageSize={pagination?.pageSize || pageSize}
|
||||||
|
total={pagination?.total || 0}
|
||||||
|
simple
|
||||||
|
onChange={onPageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileDesktopLedgerRecords({
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refreshing,
|
||||||
|
ledgerType,
|
||||||
|
onLedgerTypeChange,
|
||||||
|
onReload,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
onPageChange,
|
||||||
|
}: LedgerRecordsProps) {
|
||||||
|
const pagination = data?.data.pagination
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Select
|
||||||
|
value={ledgerType}
|
||||||
|
style={{ width: 170 }}
|
||||||
|
options={LEDGER_TYPE_OPTIONS}
|
||||||
|
onChange={onLedgerTypeChange}
|
||||||
|
/>
|
||||||
|
<Button icon={<ReloadOutlined />} loading={refreshing} onClick={onReload}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table<WorkerWalletLedger>
|
||||||
|
rowKey="ledgerId"
|
||||||
|
size="small"
|
||||||
|
loading={loading}
|
||||||
|
dataSource={data?.data.items || []}
|
||||||
|
columns={workerLedgerColumns}
|
||||||
|
locale={{ emptyText: resolveEmptyText(error, '读取资金流水失败', '暂无钱包流水') }}
|
||||||
|
pagination={{
|
||||||
|
current: pagination?.page || page,
|
||||||
|
pageSize: pagination?.pageSize || pageSize,
|
||||||
|
total: pagination?.total || 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: [10, 20, 50],
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
onChange: onPageChange,
|
||||||
|
}}
|
||||||
|
scroll={{ x: 980 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerProfileDesktopRequestRecords({
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refreshing,
|
||||||
|
requestType,
|
||||||
|
requestStatus,
|
||||||
|
onRequestTypeChange,
|
||||||
|
onRequestStatusChange,
|
||||||
|
onReload,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
onPageChange,
|
||||||
|
}: RequestRecordsProps) {
|
||||||
|
const pagination = data?.data.pagination
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Select
|
||||||
|
value={requestType}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
options={REQUEST_TYPE_OPTIONS}
|
||||||
|
onChange={onRequestTypeChange}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={requestStatus}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
options={REQUEST_STATUS_OPTIONS}
|
||||||
|
onChange={onRequestStatusChange}
|
||||||
|
/>
|
||||||
|
<Button icon={<ReloadOutlined />} loading={refreshing} onClick={onReload}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table<WorkerFinanceRequest>
|
||||||
|
rowKey="requestId"
|
||||||
|
size="small"
|
||||||
|
loading={loading}
|
||||||
|
dataSource={data?.data.items || []}
|
||||||
|
columns={workerFinanceRequestColumns}
|
||||||
|
locale={{ emptyText: resolveEmptyText(error, '读取申请记录失败', '暂无申请记录') }}
|
||||||
|
pagination={{
|
||||||
|
current: pagination?.page || page,
|
||||||
|
pageSize: pagination?.pageSize || pageSize,
|
||||||
|
total: pagination?.total || 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: [10, 20, 50],
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
onChange: onPageChange,
|
||||||
|
}}
|
||||||
|
scroll={{ x: 980 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEmptyText(error: unknown, fallback: string, emptyText: string) {
|
||||||
|
if (!error) return emptyText
|
||||||
|
return error instanceof Error ? error.message : fallback
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import { Tag, Typography } from 'antd'
|
||||||
|
import type { TableColumnsType } from 'antd'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
WorkerFinanceRequest,
|
||||||
|
WorkerProfileSummary,
|
||||||
|
WorkerUser,
|
||||||
|
WorkerWalletLedger,
|
||||||
|
} from '@/types/worker-platform'
|
||||||
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
|
|
||||||
|
export function resolveAvailableForWithdraw(
|
||||||
|
worker: WorkerUser | undefined,
|
||||||
|
summary: WorkerProfileSummary | undefined,
|
||||||
|
) {
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
Number(worker?.wallet.availableAmount || 0) - Number(summary?.pendingWithdrawAmount || 0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkerInitial(worker: WorkerUser | undefined) {
|
||||||
|
return String(worker?.displayName || worker?.username || '打')
|
||||||
|
.trim()
|
||||||
|
.slice(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMoney(value: number | undefined) {
|
||||||
|
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWorkerStatus(status: string) {
|
||||||
|
if (status === 'active') return '已启用'
|
||||||
|
if (status === 'pending_review') return '待处理'
|
||||||
|
if (status === 'rejected') return '已冻结'
|
||||||
|
if (status === 'disabled') return '已停用'
|
||||||
|
return status || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkerStatusColor(status: string) {
|
||||||
|
if (status === 'active') return 'green'
|
||||||
|
if (status === 'pending_review') return 'gold'
|
||||||
|
if (status === 'rejected') return 'red'
|
||||||
|
if (status === 'disabled') return 'default'
|
||||||
|
return 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatLedgerType(ledgerType: string) {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
manual_credit: '人工充值',
|
||||||
|
withdraw_paid: '提现打款',
|
||||||
|
deposit_freeze: '冻结押金',
|
||||||
|
deposit_release: '释放押金',
|
||||||
|
deposit_pending_unfreeze: '押金待解冻',
|
||||||
|
deposit_unfreeze: '押金已解冻',
|
||||||
|
deposit_deduction: '扣除押金',
|
||||||
|
reward_settlement: '结算报酬',
|
||||||
|
}
|
||||||
|
return labels[ledgerType] || ledgerType || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLedgerTagColor(ledgerType: string) {
|
||||||
|
if (ledgerType === 'manual_credit') return 'blue'
|
||||||
|
if (ledgerType === 'withdraw_paid') return 'purple'
|
||||||
|
if (ledgerType === 'deposit_freeze') return 'gold'
|
||||||
|
if (ledgerType === 'deposit_release') return 'green'
|
||||||
|
if (ledgerType === 'deposit_pending_unfreeze') return 'orange'
|
||||||
|
if (ledgerType === 'deposit_unfreeze') return 'green'
|
||||||
|
if (ledgerType === 'deposit_deduction') return 'red'
|
||||||
|
if (ledgerType === 'reward_settlement') return 'cyan'
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatRequestType(requestType: string) {
|
||||||
|
if (requestType === 'recharge') return '充值申请'
|
||||||
|
if (requestType === 'withdraw') return '提现申请'
|
||||||
|
return requestType || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatRequestStatus(status: string) {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
pending: '待处理',
|
||||||
|
approved: '已通过',
|
||||||
|
rejected: '已拒绝',
|
||||||
|
cancelled: '已取消',
|
||||||
|
}
|
||||||
|
return labels[status] || status || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasRechargeConfig(
|
||||||
|
financeConfig:
|
||||||
|
| {
|
||||||
|
recharge?: { accountName?: string; accountNo?: string; qrCodeImage?: unknown }
|
||||||
|
}
|
||||||
|
| undefined,
|
||||||
|
) {
|
||||||
|
return Boolean(
|
||||||
|
financeConfig?.recharge?.accountName ||
|
||||||
|
financeConfig?.recharge?.accountNo ||
|
||||||
|
financeConfig?.recharge?.qrCodeImage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderRechargeRequestSummary(row: WorkerFinanceRequest) {
|
||||||
|
const snapshot = getRechargeSnapshot(row)
|
||||||
|
if (!snapshot.channelName && !snapshot.accountName && !snapshot.accountNo) {
|
||||||
|
return <Typography.Text type="secondary">线下充值,等待管理员入账</Typography.Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text>
|
||||||
|
{[snapshot.channelName, snapshot.accountName].filter(Boolean).join(' / ') || '线下充值'}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{snapshot.accountNo ? maskAccountNo(snapshot.accountNo) : '等待管理员核对'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRequestStatusColor(status: string) {
|
||||||
|
if (status === 'approved') return 'green'
|
||||||
|
if (status === 'pending') return 'gold'
|
||||||
|
if (status === 'rejected') return 'red'
|
||||||
|
if (status === 'cancelled') return 'default'
|
||||||
|
return 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWithdrawChannel(channel: string) {
|
||||||
|
if (channel === 'alipay') return '支付宝'
|
||||||
|
if (channel === 'wechat') return '微信收款'
|
||||||
|
if (channel === 'bank') return '银行卡'
|
||||||
|
return channel || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function maskAccountNo(value: string) {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return '-'
|
||||||
|
if (text.length <= 8) return text
|
||||||
|
return `${text.slice(0, 4)} **** ${text.slice(-4)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const workerLedgerColumns: TableColumnsType<WorkerWalletLedger> = [
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
width: 180,
|
||||||
|
render: (value) => formatDateTime(String(value || '')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
width: 150,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Tag color={resolveLedgerTagColor(row.ledgerType)}>{formatLedgerType(row.ledgerType)}</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '金额',
|
||||||
|
width: 120,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
className={row.amount >= 0 ? 'worker-amount-positive' : 'worker-amount-negative'}
|
||||||
|
>
|
||||||
|
{row.amount >= 0 ? '+' : ''}
|
||||||
|
{formatMoney(row.amount)}
|
||||||
|
</Typography.Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: '余额', width: 120, render: (_, row) => formatMoney(row.balanceAfter) },
|
||||||
|
{ title: '冻结押金', width: 120, render: (_, row) => formatMoney(row.frozenAfter) },
|
||||||
|
{
|
||||||
|
title: '说明',
|
||||||
|
minWidth: 260,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text>{row.note || '-'}</Typography.Text>
|
||||||
|
{row.relatedPlatformOrderId ? (
|
||||||
|
<Typography.Text type="secondary">订单号 {row.relatedPlatformOrderId}</Typography.Text>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const workerFinanceRequestColumns: TableColumnsType<WorkerFinanceRequest> = [
|
||||||
|
{
|
||||||
|
title: '申请时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
width: 180,
|
||||||
|
render: (value) => formatDateTime(String(value || '')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
width: 120,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Tag color={row.requestType === 'withdraw' ? 'green' : 'blue'}>
|
||||||
|
{formatRequestType(row.requestType)}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '金额',
|
||||||
|
width: 120,
|
||||||
|
render: (_, row) => <Typography.Text strong>{formatMoney(row.amount)}</Typography.Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '收款信息',
|
||||||
|
minWidth: 220,
|
||||||
|
render: (_, row) =>
|
||||||
|
row.requestType === 'withdraw' ? (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text>
|
||||||
|
{formatWithdrawChannel(row.accountChannel)} / {row.accountName || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">{maskAccountNo(row.accountNo)}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderRechargeRequestSummary(row)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
width: 130,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Tag color={resolveRequestStatusColor(row.status)}>{formatRequestStatus(row.status)}</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 260,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text>{row.note || '-'}</Typography.Text>
|
||||||
|
{row.reviewedNote ? (
|
||||||
|
<Typography.Text type="secondary">审核备注:{row.reviewedNote}</Typography.Text>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function getRechargeSnapshot(row: WorkerFinanceRequest) {
|
||||||
|
const payload = asRecord(row.payload)
|
||||||
|
const financeConfig = asRecord(payload.financeConfig)
|
||||||
|
return {
|
||||||
|
channelName: String(financeConfig.channelName || '').trim(),
|
||||||
|
accountName: String(financeConfig.accountName || '').trim(),
|
||||||
|
accountNo: String(financeConfig.accountNo || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> {
|
||||||
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user