接单平台新增 VIP 等级体系与邀请绑定

- 等级按累计验收通过工单数自动升级(阈值后台可配),VIP1-5 默认配置 10/30/50/100
- 存量打手统一归 VIP1,原 regular 等级停用
- 注册支持邀请码绑定邀请人,每人生成唯一邀请码
- 新增 worker_level_logs 升级流水表
- 个人中心展示邀请码与升级进度,管理端等级表单支持升级阈值配置
This commit is contained in:
yml2213
2026-08-01 14:50:08 +08:00
parent 6e635b2499
commit 51d72cc283
9 changed files with 353 additions and 21 deletions
@@ -34,6 +34,7 @@ export default function WorkerLoginPage() {
password: string
displayName?: string
phone?: string
inviteCode?: string
}) {
setLoading(true)
try {
@@ -98,6 +99,9 @@ export default function WorkerLoginPage() {
<Form.Item label="手机号" name="phone">
<Input />
</Form.Item>
<Form.Item label="邀请码(选填)" name="inviteCode">
<Input maxLength={8} placeholder="填写邀请人邀请码" />
</Form.Item>
<Form.Item
label="密码"
name="password"
@@ -1,5 +1,6 @@
import {
BankOutlined,
CopyOutlined,
CustomerServiceOutlined,
HistoryOutlined,
LockOutlined,
@@ -23,6 +24,7 @@ import {
Input,
InputNumber,
Modal,
Progress,
Row,
Select,
Space,
@@ -114,9 +116,20 @@ export default function WorkerProfilePage() {
const worker = profileQuery.data?.data.worker
const summary = profileQuery.data?.data.summary
const levelProgress = profileQuery.data?.data.levelProgress
const financeConfig = profileQuery.data?.data.financeConfig
const canUseFinanceActions = worker?.status === 'active'
async function copyInviteCode() {
if (!worker?.inviteCode) return
try {
await navigator.clipboard.writeText(worker.inviteCode)
message.success('邀请码已复制')
} catch {
message.error('复制失败,请手动复制')
}
}
const ledgersQuery = useQuery({
queryKey: ['worker-wallet-ledgers', ledgerType, ledgerPage, ledgerPageSize],
queryFn: () =>
@@ -453,6 +466,40 @@ export default function WorkerProfilePage() {
<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={copyInviteCode}
>
</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>
<Row gutter={[16, 16]}>
@@ -542,6 +589,25 @@ export default function WorkerProfilePage() {
<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={copyInviteCode}
/>
) : 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="免押额度">
{formatMoney(worker.level?.permissions.depositFreeAmount || 0)}
</Descriptions.Item>