新增打手改昵称卡功能默认一张且一年冷却
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
-- 打手改昵称卡:默认每人 1 张,修改昵称后一年内锁定。
|
||||||
|
|
||||||
|
ALTER TABLE worker_users
|
||||||
|
ADD COLUMN IF NOT EXISTS rename_cards INTEGER NOT NULL DEFAULT 1,
|
||||||
|
ADD COLUMN IF NOT EXISTS last_renamed_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN worker_users.rename_cards IS '剩余改昵称卡数量,注册默认 1 张';
|
||||||
|
COMMENT ON COLUMN worker_users.last_renamed_at IS '最近一次修改昵称时间,修改后一年内禁止再次修改';
|
||||||
@@ -38,6 +38,8 @@ export type WorkerUserRow = {
|
|||||||
pending_unfreeze_amount?: number
|
pending_unfreeze_amount?: number
|
||||||
total_credited_amount?: number
|
total_credited_amount?: number
|
||||||
total_settled_amount?: number
|
total_settled_amount?: number
|
||||||
|
rename_cards?: number
|
||||||
|
last_renamed_at?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkerSessionRow = {
|
export type WorkerSessionRow = {
|
||||||
|
|||||||
@@ -325,3 +325,28 @@ function buildWorkerUserWhere({
|
|||||||
params,
|
params,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 消耗一张改名卡并更新昵称;条件更新保证并发下不会超发。 */
|
||||||
|
export async function consumeWorkerRenameCard(input: {
|
||||||
|
workerId: number
|
||||||
|
displayName: string
|
||||||
|
now: string
|
||||||
|
}): Promise<WorkerUserRow | null> {
|
||||||
|
const result = await query<{ id: number }>(
|
||||||
|
`
|
||||||
|
UPDATE worker_users
|
||||||
|
SET
|
||||||
|
display_name = $1,
|
||||||
|
rename_cards = rename_cards - 1,
|
||||||
|
last_renamed_at = $2,
|
||||||
|
updated_at = $2
|
||||||
|
WHERE id = $3 AND rename_cards > 0
|
||||||
|
RETURNING id
|
||||||
|
`,
|
||||||
|
[input.displayName, input.now, input.workerId],
|
||||||
|
)
|
||||||
|
if (!result.rows[0]) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return getWorkerUserById(input.workerId)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { uploadSingleFile } from '../file-upload.js'
|
|||||||
import { uploadFileAsset } from '../../services/file-storage/file-storage-service.js'
|
import { uploadFileAsset } from '../../services/file-storage/file-storage-service.js'
|
||||||
import {
|
import {
|
||||||
changeWorkerPassword,
|
changeWorkerPassword,
|
||||||
|
renameWorkerAccount,
|
||||||
createWorkerRechargeRequest,
|
createWorkerRechargeRequest,
|
||||||
createWorkerWithdrawRequest,
|
createWorkerWithdrawRequest,
|
||||||
createWorkerWithdrawalAccount,
|
createWorkerWithdrawalAccount,
|
||||||
@@ -26,6 +27,16 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/profile/rename',
|
||||||
|
requireActiveWorker,
|
||||||
|
createRouteHandler((req) => renameWorkerAccount(req.body || {}, getRequiredWorkerSession(req)), {
|
||||||
|
successMessage: '昵称已修改',
|
||||||
|
errorMessage: '修改昵称失败',
|
||||||
|
scope: '[worker/profile/rename]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/profile/wallet-ledgers',
|
'/profile/wallet-ledgers',
|
||||||
createRouteHandler(
|
createRouteHandler(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export * from './worker-auth-policy.js'
|
|||||||
export * from './worker-session-view.js'
|
export * from './worker-session-view.js'
|
||||||
export * from './worker-session-auth-service.js'
|
export * from './worker-session-auth-service.js'
|
||||||
export * from './worker-credentials-service.js'
|
export * from './worker-credentials-service.js'
|
||||||
|
export * from './worker-rename-service.js'
|
||||||
export * from './worker-sms-service.js'
|
export * from './worker-sms-service.js'
|
||||||
export * from './worker-registration-service.js'
|
export * from './worker-registration-service.js'
|
||||||
export * from './worker-login-service.js'
|
export * from './worker-login-service.js'
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { resolveVisibleDelaySeconds, resolveWorkerPermissions } from './worker-permission-utils.js'
|
import { resolveVisibleDelaySeconds, resolveWorkerPermissions } from './worker-permission-utils.js'
|
||||||
import { normalizeUploadedFiles } from './worker-input-normalizers.js'
|
import { normalizeUploadedFiles } from './worker-input-normalizers.js'
|
||||||
import { mapAcceptanceForResponse } from './work-order-material-mappers.js'
|
import { mapAcceptanceForResponse } from './work-order-material-mappers.js'
|
||||||
|
import { WORKER_RENAME_COOLDOWN_MS } from './worker-rename-service.js'
|
||||||
|
|
||||||
export const DEFAULT_LEVEL_KEY = 'vip1'
|
export const DEFAULT_LEVEL_KEY = 'vip1'
|
||||||
export const DEFAULT_LEVEL_NAME = 'VIP1'
|
export const DEFAULT_LEVEL_NAME = 'VIP1'
|
||||||
@@ -97,6 +98,11 @@ export function mapWorkerUser(worker: WorkerUserRow) {
|
|||||||
createdAt: worker.created_at,
|
createdAt: worker.created_at,
|
||||||
updatedAt: worker.updated_at,
|
updatedAt: worker.updated_at,
|
||||||
reviewedAt: worker.reviewed_at,
|
reviewedAt: worker.reviewed_at,
|
||||||
|
renameCards: Number(worker.rename_cards ?? 1),
|
||||||
|
lastRenamedAt: worker.last_renamed_at || null,
|
||||||
|
renameUnlockedAt: worker.last_renamed_at
|
||||||
|
? new Date(Date.parse(worker.last_renamed_at) + WORKER_RENAME_COOLDOWN_MS).toISOString()
|
||||||
|
: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import {
|
||||||
|
consumeWorkerRenameCard,
|
||||||
|
getWorkerUserByDisplayName,
|
||||||
|
} from '../../repositories/worker-platform/index.js'
|
||||||
|
import type { JsonObject } from '../../types/json.js'
|
||||||
|
import { createHttpError } from '../../utils/http.js'
|
||||||
|
import { nowIso } from '../../utils/time.js'
|
||||||
|
import { mapWorkerUser } from './mappers.js'
|
||||||
|
import { getRequiredWorker, type WorkerSession } from './worker-session-context-service.js'
|
||||||
|
|
||||||
|
/** 改名冷却期:修改昵称后一年内禁止再次修改。 */
|
||||||
|
export const WORKER_RENAME_COOLDOWN_MS = 365 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
const MAX_DISPLAY_NAME_LENGTH = 32
|
||||||
|
|
||||||
|
function normalizeDisplayName(value: unknown): string {
|
||||||
|
return String(value || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打手使用改名卡修改昵称;默认每人 1 张卡,修改后一年内锁定。昵称全平台唯一。 */
|
||||||
|
export async function renameWorkerAccount(payload: JsonObject = {}, session: WorkerSession) {
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
const nextDisplayName = normalizeDisplayName(payload.displayName || payload.nickname)
|
||||||
|
|
||||||
|
if (!nextDisplayName) {
|
||||||
|
throw createHttpError('请输入新的昵称', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_rename_display_name_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (nextDisplayName.length > MAX_DISPLAY_NAME_LENGTH) {
|
||||||
|
throw createHttpError(`昵称最长 ${MAX_DISPLAY_NAME_LENGTH} 个字符`, {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_rename_display_name_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (nextDisplayName === String(worker.display_name || '')) {
|
||||||
|
throw createHttpError('新昵称不能与当前昵称一致', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_rename_display_name_unchanged',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (Number(worker.rename_cards ?? 1) <= 0) {
|
||||||
|
throw createHttpError('改名卡已用完,无法再次修改昵称', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_rename_card_exhausted',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastRenamedMs = worker.last_renamed_at ? Date.parse(worker.last_renamed_at) : 0
|
||||||
|
if (Number.isFinite(lastRenamedMs) && lastRenamedMs > 0) {
|
||||||
|
const unlockedAtMs = lastRenamedMs + WORKER_RENAME_COOLDOWN_MS
|
||||||
|
if (Date.now() < unlockedAtMs) {
|
||||||
|
const unlockedAt = new Date(unlockedAtMs).toISOString()
|
||||||
|
throw createHttpError(
|
||||||
|
`修改昵称后一年内无法再次修改,${new Date(unlockedAtMs).toLocaleString('zh-CN')} 后才能修改`,
|
||||||
|
{ statusCode: 409, errorCode: 'worker_rename_cooldown', context: { unlockedAt } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await getWorkerUserByDisplayName(nextDisplayName)
|
||||||
|
if (existing && Number(existing.id) !== Number(worker.id)) {
|
||||||
|
throw createHttpError('该昵称已被使用,请更换昵称', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_display_name_taken',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await consumeWorkerRenameCard({
|
||||||
|
workerId: Number(worker.id),
|
||||||
|
displayName: nextDisplayName,
|
||||||
|
now: nowIso(),
|
||||||
|
}).catch((error: unknown) => {
|
||||||
|
if ((error as { code?: string }).code === '23505') {
|
||||||
|
throw createHttpError('该昵称已被使用,请更换昵称', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_display_name_taken',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError('改名卡已用完,无法再次修改昵称', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_rename_card_exhausted',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
worker: mapWorkerUser(updated),
|
||||||
|
reloginRequired: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Form,
|
Form,
|
||||||
Image,
|
Image,
|
||||||
@@ -24,6 +25,11 @@ export type PasswordFormValues = {
|
|||||||
confirmPassword?: string
|
confirmPassword?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RenameFormValues = {
|
||||||
|
displayName?: string
|
||||||
|
confirmed?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export function WorkerProfileAccountModals({
|
export function WorkerProfileAccountModals({
|
||||||
isMobile,
|
isMobile,
|
||||||
worker,
|
worker,
|
||||||
@@ -41,6 +47,11 @@ export function WorkerProfileAccountModals({
|
|||||||
submitPasswordChange,
|
submitPasswordChange,
|
||||||
contactOpen,
|
contactOpen,
|
||||||
setContactOpen,
|
setContactOpen,
|
||||||
|
renameOpen,
|
||||||
|
setRenameOpen,
|
||||||
|
submittingRename,
|
||||||
|
renameForm,
|
||||||
|
submitRename,
|
||||||
}: {
|
}: {
|
||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
worker: WorkerUser | undefined
|
worker: WorkerUser | undefined
|
||||||
@@ -58,6 +69,11 @@ export function WorkerProfileAccountModals({
|
|||||||
submitPasswordChange: (values: PasswordFormValues) => Promise<void>
|
submitPasswordChange: (values: PasswordFormValues) => Promise<void>
|
||||||
contactOpen: boolean
|
contactOpen: boolean
|
||||||
setContactOpen: (open: boolean) => void
|
setContactOpen: (open: boolean) => void
|
||||||
|
renameOpen: boolean
|
||||||
|
setRenameOpen: (open: boolean) => void
|
||||||
|
submittingRename: boolean
|
||||||
|
renameForm: FormInstance<RenameFormValues>
|
||||||
|
submitRename: (values: RenameFormValues) => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -220,6 +236,74 @@ export function WorkerProfileAccountModals({
|
|||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</Space>
|
</Space>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="修改昵称"
|
||||||
|
open={renameOpen}
|
||||||
|
width={isMobile ? '92%' : 560}
|
||||||
|
destroyOnHidden
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => setRenameOpen(false)}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="warning"
|
||||||
|
message="修改昵称后将锁定一年"
|
||||||
|
description={
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
|
<Typography.Text type="danger" strong>
|
||||||
|
昵称修改成功后,一年内无法再次修改,请谨慎操作!
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
当前剩余改名卡 {worker?.renameCards ?? 1} 张,本次修改昵称将消耗 1 张。
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form<RenameFormValues>
|
||||||
|
form={renameForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={(values) => void submitRename(values)}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
>
|
||||||
|
<Form.Item label="当前昵称">
|
||||||
|
<Typography.Text strong>
|
||||||
|
{worker?.displayName || worker?.username || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="displayName"
|
||||||
|
label="新昵称"
|
||||||
|
rules={[
|
||||||
|
{ required: true, whitespace: true, message: '请输入新的昵称' },
|
||||||
|
{ max: 32, message: '昵称最长 32 个字符' },
|
||||||
|
{
|
||||||
|
validator: (_, value) =>
|
||||||
|
value && value.trim() === String(worker?.displayName || '').trim()
|
||||||
|
? Promise.reject(new Error('新昵称不能与当前昵称一致'))
|
||||||
|
: Promise.resolve(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input allowClear placeholder="最长 32 个字符,全平台不可重复" autoComplete="off" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="confirmed"
|
||||||
|
valuePropName="checked"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: (_, value) =>
|
||||||
|
value ? Promise.resolve() : Promise.reject(new Error('请先勾选确认')),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Checkbox>我已知晓改名后将消耗 1 张改名卡,且一年内无法再次修改用户名</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" block loading={submittingRename} htmlType="submit">
|
||||||
|
确认修改昵称
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import {
|
|||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
|
UserOutlined,
|
||||||
WalletOutlined,
|
WalletOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Avatar, Button, Card, Descriptions, Progress, Space, Tag, Typography } from 'antd'
|
import { Avatar, Button, Card, Descriptions, Progress, Space, Tag, Tooltip, Typography } from 'antd'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
WorkerLevelProgress,
|
WorkerLevelProgress,
|
||||||
@@ -124,6 +125,9 @@ export function WorkerProfileDesktopActions({
|
|||||||
onAddWithdrawalAccount,
|
onAddWithdrawalAccount,
|
||||||
onAddAlipayQrCode,
|
onAddAlipayQrCode,
|
||||||
onChangePassword,
|
onChangePassword,
|
||||||
|
onRename,
|
||||||
|
renameDisabled,
|
||||||
|
renameHint,
|
||||||
onManageDevices,
|
onManageDevices,
|
||||||
onContact,
|
onContact,
|
||||||
onLogout,
|
onLogout,
|
||||||
@@ -136,6 +140,9 @@ export function WorkerProfileDesktopActions({
|
|||||||
onAddWithdrawalAccount: () => void
|
onAddWithdrawalAccount: () => void
|
||||||
onAddAlipayQrCode: () => void
|
onAddAlipayQrCode: () => void
|
||||||
onChangePassword: () => void
|
onChangePassword: () => void
|
||||||
|
onRename: () => void
|
||||||
|
renameDisabled: boolean
|
||||||
|
renameHint: string
|
||||||
onManageDevices: () => void
|
onManageDevices: () => void
|
||||||
onContact: () => void
|
onContact: () => void
|
||||||
onLogout: () => void
|
onLogout: () => void
|
||||||
@@ -177,6 +184,11 @@ export function WorkerProfileDesktopActions({
|
|||||||
补充支付宝收款码
|
补充支付宝收款码
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Tooltip title={renameHint}>
|
||||||
|
<Button icon={<UserOutlined />} disabled={renameDisabled} onClick={onRename}>
|
||||||
|
修改昵称
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
<Button icon={<LockOutlined />} onClick={onChangePassword}>
|
<Button icon={<LockOutlined />} onClick={onChangePassword}>
|
||||||
修改密码
|
修改密码
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
LockOutlined,
|
LockOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
|
UserSwitchOutlined,
|
||||||
WalletOutlined,
|
WalletOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Alert, Avatar, Button, Progress, Tag } from 'antd'
|
import { Alert, Avatar, Button, Progress, Tag } from 'antd'
|
||||||
@@ -37,6 +38,8 @@ type WorkerProfileMobileViewProps = {
|
|||||||
onRecharge: () => void
|
onRecharge: () => void
|
||||||
onWithdraw: () => void
|
onWithdraw: () => void
|
||||||
onChangePassword: () => void
|
onChangePassword: () => void
|
||||||
|
onRename: () => void
|
||||||
|
renameDisabled: boolean
|
||||||
onManageDevices: () => void
|
onManageDevices: () => void
|
||||||
onContact: () => void
|
onContact: () => void
|
||||||
onAddAlipayQrCode: () => void
|
onAddAlipayQrCode: () => void
|
||||||
@@ -59,6 +62,8 @@ export function WorkerProfileMobileView({
|
|||||||
onRecharge,
|
onRecharge,
|
||||||
onWithdraw,
|
onWithdraw,
|
||||||
onChangePassword,
|
onChangePassword,
|
||||||
|
onRename,
|
||||||
|
renameDisabled,
|
||||||
onManageDevices,
|
onManageDevices,
|
||||||
onContact,
|
onContact,
|
||||||
onAddAlipayQrCode,
|
onAddAlipayQrCode,
|
||||||
@@ -190,6 +195,14 @@ export function WorkerProfileMobileView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="worker-profile-mobile-tools-card">
|
<div className="worker-profile-mobile-tools-card">
|
||||||
|
<MobileTool
|
||||||
|
label="修改昵称"
|
||||||
|
icon={<UserSwitchOutlined />}
|
||||||
|
color="#4338ca"
|
||||||
|
background="#eef2ff"
|
||||||
|
disabled={renameDisabled}
|
||||||
|
onClick={onRename}
|
||||||
|
/>
|
||||||
<MobileTool
|
<MobileTool
|
||||||
label="修改密码"
|
label="修改密码"
|
||||||
icon={<LockOutlined />}
|
icon={<LockOutlined />}
|
||||||
|
|||||||
@@ -15,12 +15,18 @@ import {
|
|||||||
deleteWorkerSession,
|
deleteWorkerSession,
|
||||||
fetchWorkerWalletLedgers,
|
fetchWorkerWalletLedgers,
|
||||||
logoutWorker,
|
logoutWorker,
|
||||||
|
renameWorkerAccount,
|
||||||
saveWorkerAlipayWithdrawalQrCode,
|
saveWorkerAlipayWithdrawalQrCode,
|
||||||
} from '@/services/worker'
|
} from '@/services/worker'
|
||||||
import type { UploadedFile, WorkerSessionDevice } from '@/types/worker-platform'
|
import type { UploadedFile, WorkerSessionDevice } from '@/types/worker-platform'
|
||||||
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||||
import { clearWorkerSession } from '@/utils/worker-auth'
|
import { clearWorkerSession } from '@/utils/worker-auth'
|
||||||
import { WorkerProfileAccountModals, type PasswordFormValues } from './WorkerProfileAccountModals'
|
import {
|
||||||
|
WorkerProfileAccountModals,
|
||||||
|
type PasswordFormValues,
|
||||||
|
type RenameFormValues,
|
||||||
|
} from './WorkerProfileAccountModals'
|
||||||
import { WorkerProfileFinanceModals } from './WorkerProfileFinanceModals'
|
import { WorkerProfileFinanceModals } from './WorkerProfileFinanceModals'
|
||||||
import { WorkerProfileMobileView } from './WorkerProfileMobileView'
|
import { WorkerProfileMobileView } from './WorkerProfileMobileView'
|
||||||
import {
|
import {
|
||||||
@@ -80,6 +86,9 @@ export default function WorkerProfilePage() {
|
|||||||
const [withdrawalAccountOpen, setWithdrawalAccountOpen] = useState(false)
|
const [withdrawalAccountOpen, setWithdrawalAccountOpen] = useState(false)
|
||||||
const [alipayQrCodeOpen, setAlipayQrCodeOpen] = useState(false)
|
const [alipayQrCodeOpen, setAlipayQrCodeOpen] = useState(false)
|
||||||
const [passwordOpen, setPasswordOpen] = useState(false)
|
const [passwordOpen, setPasswordOpen] = useState(false)
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false)
|
||||||
|
const [submittingRename, setSubmittingRename] = useState(false)
|
||||||
|
const [renameForm] = Form.useForm<RenameFormValues>()
|
||||||
const [contactOpen, setContactOpen] = useState(false)
|
const [contactOpen, setContactOpen] = useState(false)
|
||||||
const [devicesOpen, setDevicesOpen] = useState(false)
|
const [devicesOpen, setDevicesOpen] = useState(false)
|
||||||
const [submittingRecharge, setSubmittingRecharge] = useState(false)
|
const [submittingRecharge, setSubmittingRecharge] = useState(false)
|
||||||
@@ -119,6 +128,15 @@ export default function WorkerProfilePage() {
|
|||||||
(account) => account.accountChannel === 'alipay',
|
(account) => account.accountChannel === 'alipay',
|
||||||
)
|
)
|
||||||
const canUseFinanceActions = worker?.status === 'active'
|
const canUseFinanceActions = worker?.status === 'active'
|
||||||
|
const renameCards = worker?.renameCards ?? 1
|
||||||
|
const renameLocked =
|
||||||
|
Boolean(worker?.renameUnlockedAt) && Date.parse(worker?.renameUnlockedAt || '') > Date.now()
|
||||||
|
const canRename = renameCards > 0 && !renameLocked
|
||||||
|
const renameHint = renameLocked
|
||||||
|
? `修改昵称后一年内不可再次修改,${formatDateTime(worker?.renameUnlockedAt || null)} 后可改`
|
||||||
|
: renameCards <= 0
|
||||||
|
? '改名卡已用完'
|
||||||
|
: `剩余改名卡 ${renameCards} 张,修改昵称后一年内不可再次修改`
|
||||||
|
|
||||||
async function copyInviteCode() {
|
async function copyInviteCode() {
|
||||||
if (!worker?.inviteCode) return
|
if (!worker?.inviteCode) return
|
||||||
@@ -326,6 +344,23 @@ export default function WorkerProfilePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitRename(values: RenameFormValues) {
|
||||||
|
setSubmittingRename(true)
|
||||||
|
try {
|
||||||
|
const response = await renameWorkerAccount({
|
||||||
|
displayName: String(values.displayName || '').trim(),
|
||||||
|
})
|
||||||
|
message.success(`昵称已修改为「${response.data.worker.displayName}」,一年内无法再次修改`)
|
||||||
|
setRenameOpen(false)
|
||||||
|
renameForm.resetFields()
|
||||||
|
await refreshAll()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '修改昵称失败')
|
||||||
|
} finally {
|
||||||
|
setSubmittingRename(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitPasswordChange(values: PasswordFormValues) {
|
async function submitPasswordChange(values: PasswordFormValues) {
|
||||||
setSubmittingPassword(true)
|
setSubmittingPassword(true)
|
||||||
try {
|
try {
|
||||||
@@ -440,6 +475,8 @@ export default function WorkerProfilePage() {
|
|||||||
onRecharge={openRechargeModal}
|
onRecharge={openRechargeModal}
|
||||||
onWithdraw={openWithdrawModal}
|
onWithdraw={openWithdrawModal}
|
||||||
onChangePassword={() => setPasswordOpen(true)}
|
onChangePassword={() => setPasswordOpen(true)}
|
||||||
|
onRename={() => setRenameOpen(true)}
|
||||||
|
renameDisabled={!canRename}
|
||||||
onManageDevices={() => setDevicesOpen(true)}
|
onManageDevices={() => setDevicesOpen(true)}
|
||||||
onContact={() => setContactOpen(true)}
|
onContact={() => setContactOpen(true)}
|
||||||
onAddAlipayQrCode={openAlipayQrCodeModal}
|
onAddAlipayQrCode={openAlipayQrCodeModal}
|
||||||
@@ -493,6 +530,9 @@ export default function WorkerProfilePage() {
|
|||||||
onAddWithdrawalAccount={openWithdrawalAccountModal}
|
onAddWithdrawalAccount={openWithdrawalAccountModal}
|
||||||
onAddAlipayQrCode={openAlipayQrCodeModal}
|
onAddAlipayQrCode={openAlipayQrCodeModal}
|
||||||
onChangePassword={() => setPasswordOpen(true)}
|
onChangePassword={() => setPasswordOpen(true)}
|
||||||
|
onRename={() => setRenameOpen(true)}
|
||||||
|
renameDisabled={!canRename}
|
||||||
|
renameHint={renameHint}
|
||||||
onManageDevices={() => setDevicesOpen(true)}
|
onManageDevices={() => setDevicesOpen(true)}
|
||||||
onContact={() => setContactOpen(true)}
|
onContact={() => setContactOpen(true)}
|
||||||
onLogout={submitLogout}
|
onLogout={submitLogout}
|
||||||
@@ -627,6 +667,11 @@ export default function WorkerProfilePage() {
|
|||||||
submitPasswordChange={submitPasswordChange}
|
submitPasswordChange={submitPasswordChange}
|
||||||
contactOpen={contactOpen}
|
contactOpen={contactOpen}
|
||||||
setContactOpen={setContactOpen}
|
setContactOpen={setContactOpen}
|
||||||
|
renameOpen={renameOpen}
|
||||||
|
setRenameOpen={setRenameOpen}
|
||||||
|
submittingRename={submittingRename}
|
||||||
|
renameForm={renameForm}
|
||||||
|
submitRename={submitRename}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -194,6 +194,13 @@ export function changeWorkerPassword(payload: { currentPassword: string; newPass
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renameWorkerAccount(payload: { displayName: string }) {
|
||||||
|
return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>(
|
||||||
|
'/api/v1/worker/profile/rename',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchWorkerHallOrders(params?: Record<string, unknown>) {
|
export function fetchWorkerHallOrders(params?: Record<string, unknown>) {
|
||||||
return apiGet<WorkerHallOrdersResponse>('/api/v1/worker/hall/orders', params)
|
return apiGet<WorkerHallOrdersResponse>('/api/v1/worker/hall/orders', params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ export type WorkerUser = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
reviewedAt: string | null
|
reviewedAt: string | null
|
||||||
|
/** 剩余改名卡数量,注册默认 1 张 */
|
||||||
|
renameCards: number
|
||||||
|
lastRenamedAt: string | null
|
||||||
|
/** 上次改名后的一年锁定期结束时间 */
|
||||||
|
renameUnlockedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkerSessionDevice = {
|
export type WorkerSessionDevice = {
|
||||||
|
|||||||
Reference in New Issue
Block a user