拆分打手个人中心
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