新增好友赠送验收模式支持打手资料提交加好友冷却与模板配置

This commit is contained in:
yml2213
2026-08-22 11:19:49 +08:00
parent ba35d27976
commit 6aec4a7b40
36 changed files with 1646 additions and 52 deletions
@@ -1,5 +1,5 @@
import { MessageOutlined, PictureOutlined } from '@ant-design/icons'
import { Button, Card, Descriptions, Drawer, Tag, Typography } from 'antd'
import { MessageOutlined, PictureOutlined, UserAddOutlined } from '@ant-design/icons'
import { Button, Card, Descriptions, Drawer, Space, Steps, Tag, Typography } from 'antd'
import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
import ImagePreviewList from '@/components/files/ImagePreviewList'
@@ -11,19 +11,22 @@ import {
canRequestCancel,
canSubmitAcceptance,
canSubmitFeedback,
canSubmitGiftMaterial,
formatMoney,
formatStatus,
getAcceptanceSubmittedAt,
getDisplayOrderNo,
getGiftPhaseMeta,
getMaterialDetailItems,
getMaterialScreenshots,
getOrderStatusDisplay,
getOrderStatusHint,
getUniqueAcceptanceFiles,
getWorkerAcceptance,
getWorkerDraft,
GiftCountdownTag,
hasSubmittedAcceptance,
isFriendGiftOrder,
renderDetailFieldValues,
resolveStatusColor,
getStatusHint,
} from './worker-order-utils'
export function WorkerOrderDetailDrawer({
@@ -32,6 +35,7 @@ export function WorkerOrderDetailDrawer({
orderTimelineVisible,
onClose,
onOpenAcceptance,
onOpenGiftMaterial,
onOpenCancelRequest,
onOpenFeedback,
}: {
@@ -40,6 +44,7 @@ export function WorkerOrderDetailDrawer({
orderTimelineVisible: boolean
onClose: () => void
onOpenAcceptance: (order: WorkOrder) => void
onOpenGiftMaterial: (order: WorkOrder) => void
onOpenCancelRequest: (order: WorkOrder) => void
onOpenFeedback: (order: WorkOrder) => void
}) {
@@ -55,7 +60,18 @@ export function WorkerOrderDetailDrawer({
{order ? (
<div className="worker-order-detail-stack">
<div className="worker-order-detail-actions">
<Tag color={resolveStatusColor(order.status)}>{formatStatus(order.status)}</Tag>
<Tag color={getOrderStatusDisplay(order).color}>
{getOrderStatusDisplay(order).label}
</Tag>
{canSubmitGiftMaterial(order) ? (
<Button
type="primary"
icon={<UserAddOutlined />}
onClick={() => onOpenGiftMaterial(order)}
>
提交好友赠送资料
</Button>
) : null}
{canEditAcceptanceImages(order) ? (
<Button
type={canSubmitAcceptance(order) ? 'primary' : 'default'}
@@ -82,9 +98,11 @@ export function WorkerOrderDetailDrawer({
<Descriptions.Item label="分类">{order.categoryName || '-'}</Descriptions.Item>
<Descriptions.Item label="平台订单号">{getDisplayOrderNo(order)}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={resolveStatusColor(order.status)}>{formatStatus(order.status)}</Tag>
<Tag color={getOrderStatusDisplay(order).color}>
{getOrderStatusDisplay(order).label}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="阶段说明">{getStatusHint(order.status)}</Descriptions.Item>
<Descriptions.Item label="阶段说明">{getOrderStatusHint(order)}</Descriptions.Item>
<Descriptions.Item label="接单奖励">
{formatMoney(order.rewardAmount)}
</Descriptions.Item>
@@ -118,6 +136,8 @@ export function WorkerOrderDetailDrawer({
) : null}
</Card>
{isFriendGiftOrder(order) && order.gift ? <GiftProgressCard order={order} /> : null}
<Card title="验收信息" size="small">
<AcceptanceSummary order={order} />
</Card>
@@ -141,6 +161,97 @@ export function WorkerOrderDetailDrawer({
)
}
function GiftProgressCard({ order }: { order: WorkOrder }) {
const gift = order.gift
if (!gift) return null
const phaseMeta = getGiftPhaseMeta(gift.phase)
const stepIndex =
gift.phase === 'material_required'
? 0
: gift.phase === 'material_submitted'
? 1
: gift.phase === 'friend_countdown'
? 2
: 3
const boosterFields = gift.boosterMaterial?.fields || {}
const boosterFieldLabels: Record<string, string> = {
gameNickname: '游戏昵称',
gameId: '游戏ID',
serverZone: '区服',
system: '系统',
}
const boosterItems = Object.entries(boosterFields).filter(([, value]) =>
String(value || '').trim(),
)
return (
<Card title="好友赠送进度" size="small">
<div className="worker-order-detail-stack">
<Space size={8} wrap>
<Tag color={phaseMeta.color}>{phaseMeta.label}</Tag>
{gift.phase === 'friend_countdown' ? (
<GiftCountdownTag giftAvailableAt={gift.giftAvailableAt} />
) : null}
{gift.phase === 'gift_ready' ? (
<Tag color="green">可赠送,完成后请提交赠送凭证</Tag>
) : null}
</Space>
<Steps
size="small"
current={stepIndex}
items={[
{ title: '提交资料' },
{ title: '添加好友' },
{ title: '冷却等待' },
{ title: '赠送交付' },
]}
/>
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="冷却时长">{gift.cooldownHours} 小时</Descriptions.Item>
<Descriptions.Item label="已加好友">
{formatDateTime(gift.friendAddedAt)}
</Descriptions.Item>
<Descriptions.Item label="可赠送时间" span={2}>
{gift.giftAvailableAt ? (
<Space size={8} wrap>
<span>{formatDateTime(gift.giftAvailableAt)}</span>
{gift.phase === 'friend_countdown' ? (
<GiftCountdownTag giftAvailableAt={gift.giftAvailableAt} />
) : null}
</Space>
) : (
'-'
)}
</Descriptions.Item>
</Descriptions>
{boosterItems.length > 0 || (gift.boosterMaterial?.screenshots || []).length > 0 ? (
<div>
<Typography.Text type="secondary">已提交的本人资料</Typography.Text>
<div style={{ marginTop: 8 }}>
{boosterItems.length > 0 ? (
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
{boosterItems.map(([key, value]) => (
<Descriptions.Item key={key} label={boosterFieldLabels[key] || key}>
<span className="worker-order-detail-value">{String(value)}</span>
</Descriptions.Item>
))}
</Descriptions>
) : null}
{(gift.boosterMaterial?.screenshots || []).length > 0 ? (
<div style={{ marginTop: 8 }}>
<ImagePreviewList files={gift.boosterMaterial.screenshots} size={64} />
</div>
) : null}
</div>
</div>
) : (
<Typography.Text type="secondary">尚未提交本人资料</Typography.Text>
)}
</div>
</Card>
)
}
function AcceptanceSummary({ order }: { order: WorkOrder }) {
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order))
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order))
@@ -25,19 +25,21 @@ import {
canRequestCancel,
canSubmitAcceptance,
canSubmitFeedback,
canSubmitGiftMaterial,
formatMoney,
formatStatus,
getCopyOrderInfoItems,
getDisplayOrderNo,
getMaterialDetailItems,
getMaterialScreenshots,
getOrderStatusDisplay,
getSettlementLabel,
getUniqueAcceptanceFiles,
getWorkerAcceptance,
getWorkerDraft,
GiftCountdownTag,
hasSubmittedAcceptance,
isFriendGiftOrder,
isVipEvidencePending,
resolveStatusColor,
} from './worker-order-utils'
export function WorkerOrderMobileList({
@@ -61,6 +63,7 @@ export function WorkerOrderMobileList({
onPageChange,
onOpenDetail,
onOpenAcceptance,
onOpenGiftMaterial,
onRemindAcceptance,
onOpenCancelRequest,
onOpenFeedback,
@@ -88,6 +91,7 @@ export function WorkerOrderMobileList({
onPageChange: (page: number, pageSize: number) => void
onOpenDetail: (order: WorkOrder) => void
onOpenAcceptance: (order: WorkOrder) => void
onOpenGiftMaterial: (order: WorkOrder) => void
onRemindAcceptance: (order: WorkOrder) => void
onOpenCancelRequest: (order: WorkOrder) => void
onOpenFeedback: (order: WorkOrder) => void
@@ -159,6 +163,7 @@ export function WorkerOrderMobileList({
order={order}
onOpenDetail={onOpenDetail}
onOpenAcceptance={onOpenAcceptance}
onOpenGiftMaterial={onOpenGiftMaterial}
onRemindAcceptance={onRemindAcceptance}
onOpenCancelRequest={onOpenCancelRequest}
onOpenFeedback={onOpenFeedback}
@@ -188,6 +193,7 @@ function WorkerOrderMobileCard({
order,
onOpenDetail,
onOpenAcceptance,
onOpenGiftMaterial,
onRemindAcceptance,
onOpenCancelRequest,
onOpenFeedback,
@@ -221,6 +227,7 @@ function WorkerOrderMobileCard({
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order))
const noteText = getWorkerAcceptance(order)?.note
const workerNote = String(order.workerNote || '').trim()
const statusDisplay = getOrderStatusDisplay(order)
return (
<article className="worker-order-mobile-card">
@@ -244,10 +251,15 @@ function WorkerOrderMobileCard({
<span className="worker-order-mobile-orderno-text">{getDisplayOrderNo(order)}</span>
</div>
<div className="worker-order-mobile-header-right">
<Tag color={resolveStatusColor(order.status)}>{formatStatus(order.status)}</Tag>
<Tag color={statusDisplay.color}>{statusDisplay.label}</Tag>
{order.status === 'in_progress' && order.deadlineAt ? (
<DeadlineCountdown deadlineAt={order.deadlineAt} />
) : null}
{isFriendGiftOrder(order) &&
order.status === 'in_progress' &&
order.gift?.phase === 'friend_countdown' ? (
<GiftCountdownTag giftAvailableAt={order.gift.giftAvailableAt} />
) : null}
</div>
</div>
@@ -371,6 +383,16 @@ function WorkerOrderMobileCard({
<Button size="small" icon={<EyeOutlined />} onClick={() => onOpenDetail(order)}>
详情
</Button>
{canSubmitGiftMaterial(order) ? (
<Button
size="small"
type="primary"
className="worker-order-mobile-action-submit"
onClick={() => onOpenGiftMaterial(order)}
>
提交资料
</Button>
) : null}
{canEditAcceptanceImages(order) ? (
<Button
size="small"
@@ -1,5 +1,5 @@
import { PictureOutlined } from '@ant-design/icons'
import { Button, Form, Input, Modal, Space, Tag, Typography } from 'antd'
import { Button, Form, Input, Modal, Radio, Space, Tag, Typography } from 'antd'
import type { FormInstance } from 'antd'
import ImageUpload from '@/components/files/ImageUpload'
@@ -22,6 +22,13 @@ export type AcceptanceFormValues = { note?: string }
export type WorkerNoteFormValues = { note?: string }
export type CancelRequestFormValues = { reason?: string }
export type FeedbackFormValues = { content?: string }
export type GiftMaterialFormValues = {
gameNickname: string
gameId: string
serverZone: string
system?: string
note?: string
}
type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'default'
@@ -58,6 +65,12 @@ export function WorkerOrderModals({
onAcceptanceFilesChange,
onSubmitAcceptance,
onSaveAcceptanceImages,
giftMaterialOrder,
giftMaterialForm,
giftMaterialFiles,
onCloseGiftMaterial,
onGiftMaterialFilesChange,
onSubmitGiftMaterial,
}: {
isMobile: boolean
feedbackOrder: WorkOrder | null
@@ -82,6 +95,12 @@ export function WorkerOrderModals({
onAcceptanceFilesChange: (files: UploadedFile[]) => void
onSubmitAcceptance: (values: AcceptanceFormValues) => void | Promise<void>
onSaveAcceptanceImages: (values: AcceptanceFormValues) => void | Promise<void>
giftMaterialOrder: WorkOrder | null
giftMaterialForm: FormInstance<GiftMaterialFormValues>
giftMaterialFiles: UploadedFile[]
onCloseGiftMaterial: () => void
onGiftMaterialFilesChange: (files: UploadedFile[]) => void
onSubmitGiftMaterial: (values: GiftMaterialFormValues) => void | Promise<void>
}) {
const feedbackItems = feedbacks.filter((item) => item.workOrderId === feedbackOrder?.workOrderId)
return (
@@ -266,6 +285,66 @@ export function WorkerOrderModals({
</Form.Item>
</Form>
</Modal>
<Modal
title={`好友赠送资料 · ${getDisplayOrderNo(giftMaterialOrder)}`}
open={Boolean(giftMaterialOrder)}
width={isMobile ? '96%' : 620}
onCancel={onCloseGiftMaterial}
onOk={() => giftMaterialForm.submit()}
okText="提交资料"
destroyOnHidden
>
<Typography.Paragraph type="secondary">
请填写本人游戏账号信息并上传主页截图;后台核对后会添加好友,好友冷却结束后才能赠送。
</Typography.Paragraph>
<Form form={giftMaterialForm} layout="vertical" onFinish={onSubmitGiftMaterial}>
<Form.Item
label="游戏昵称"
name="gameNickname"
rules={[{ required: true, whitespace: true, message: '请填写游戏昵称' }]}
>
<Input placeholder="本人游戏昵称" maxLength={50} />
</Form.Item>
<Form.Item
label="游戏ID"
name="gameId"
rules={[{ required: true, whitespace: true, message: '请填写游戏ID' }]}
>
<Input placeholder="本人游戏ID" maxLength={50} />
</Form.Item>
<Form.Item
label="区服"
name="serverZone"
rules={[{ required: true, whitespace: true, message: '请填写区服' }]}
>
<Input placeholder="例如:QQ区 / 微信区 / 具体区服名称" maxLength={50} />
</Form.Item>
<Form.Item label="系统" name="system" initialValue="">
<Radio.Group>
<Radio.Button value="">不区分</Radio.Button>
<Radio.Button value="安卓">安卓</Radio.Button>
<Radio.Button value="苹果">苹果</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
label="主页截图"
required
extra="至少上传一张本人游戏主页截图,供后台核对身份。"
>
<ImageUpload
scene="worker-gift-material"
scope="worker"
value={giftMaterialFiles}
onChange={onGiftMaterialFilesChange}
maxCount={5}
showPasteHint
/>
</Form.Item>
<Form.Item label="备注" name="note">
<Input.TextArea rows={2} maxLength={200} showCount placeholder="选填" />
</Form.Item>
</Form>
</Modal>
</>
)
}
@@ -19,19 +19,21 @@ import {
canRequestCancel,
canSubmitAcceptance,
canSubmitFeedback,
canSubmitGiftMaterial,
formatMoney,
formatStatus,
getAcceptanceSubmittedAt,
getCopyOrderInfoItems,
getDisplayOrderNo,
getMaterialDetailItems,
getMaterialScreenshots,
getOrderStatusDisplay,
getUniqueAcceptanceFiles,
getWorkerAcceptance,
getWorkerDraft,
GiftCountdownTag,
hasSubmittedAcceptance,
isFriendGiftOrder,
isVipEvidencePending,
resolveStatusColor,
} from './worker-order-utils'
type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'default'
@@ -39,6 +41,7 @@ type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'd
export function buildWorkerOrderTableColumns({
onOpenDetail,
onOpenAcceptance,
onOpenGiftMaterial,
onRemindAcceptance,
onOpenCancelRequest,
onOpenFeedback,
@@ -48,6 +51,7 @@ export function buildWorkerOrderTableColumns({
}: {
onOpenDetail: (order: WorkOrder) => void
onOpenAcceptance: (order: WorkOrder) => void
onOpenGiftMaterial: (order: WorkOrder) => void
onRemindAcceptance: (order: WorkOrder) => void
onOpenCancelRequest: (order: WorkOrder) => void
onOpenFeedback: (order: WorkOrder) => void
@@ -96,25 +100,33 @@ export function buildWorkerOrderTableColumns({
{
title: '状态',
width: 130,
render: (_, row) => (
<div className="cell-stack">
<Space wrap size={4}>
<Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>
{row.status === 'in_progress' && row.deadlineAt ? (
<DeadlineCountdown deadlineAt={row.deadlineAt} />
render: (_, row) => {
const statusDisplay = getOrderStatusDisplay(row)
return (
<div className="cell-stack">
<Space wrap size={4}>
<Tag color={statusDisplay.color}>{statusDisplay.label}</Tag>
{row.status === 'in_progress' && row.deadlineAt ? (
<DeadlineCountdown deadlineAt={row.deadlineAt} />
) : null}
{isFriendGiftOrder(row) &&
row.status === 'in_progress' &&
row.gift?.phase === 'friend_countdown' ? (
<GiftCountdownTag giftAvailableAt={row.gift.giftAvailableAt} />
) : null}
</Space>
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
<Typography.Text
type="danger"
ellipsis={{ tooltip: row.problemNote }}
style={{ fontSize: 12 }}
>
{row.problemNote}
</Typography.Text>
) : null}
</Space>
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
<Typography.Text
type="danger"
ellipsis={{ tooltip: row.problemNote }}
style={{ fontSize: 12 }}
>
{row.problemNote}
</Typography.Text>
) : null}
</div>
),
</div>
)
},
},
{
title: '奖励 / 押金',
@@ -205,6 +217,11 @@ export function buildWorkerOrderTableColumns({
onClick={() => onOpenDetail(row)}
/>
</Tooltip>
{canSubmitGiftMaterial(row) ? (
<Button type="primary" onClick={() => onOpenGiftMaterial(row)}>
提交资料
</Button>
) : null}
{canEditAcceptanceImages(row) ? (
<Button
icon={<PictureOutlined />}
@@ -14,10 +14,11 @@ import {
remindWorkerAcceptance,
saveWorkerAcceptanceDraft,
saveWorkerOrderNote,
supplementWorkerAcceptedOrderEvidence,
submitWorkerAcceptance,
submitWorkerCancelRequest,
submitWorkerGiftMaterial,
submitWorkerOrderFeedback,
supplementWorkerAcceptedOrderEvidence,
} from '@/services/worker'
import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
import { useIsMobile } from '@/utils/use-is-mobile'
@@ -29,6 +30,7 @@ import {
type AcceptanceFormValues,
type CancelRequestFormValues,
type FeedbackFormValues,
type GiftMaterialFormValues,
type WorkerNoteFormValues,
WorkerOrderModals,
} from './WorkerOrderModals'
@@ -42,6 +44,7 @@ import {
getWorkerAcceptance,
getWorkerDraft,
hasSubmittedAcceptance,
isGiftSubmissionBlocked,
patchWorkOrderReadState,
toPngBlob,
} from './worker-order-utils'
@@ -165,7 +168,10 @@ export default function WorkerOrdersPage() {
const [cancelRequestOrder, setCancelRequestOrder] = useState<WorkOrder | null>(null)
const [feedbackOrder, setFeedbackOrder] = useState<WorkOrder | null>(null)
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
const [giftMaterialOrder, setGiftMaterialOrder] = useState<WorkOrder | null>(null)
const [giftMaterialFiles, setGiftMaterialFiles] = useState<UploadedFile[]>([])
const [form] = Form.useForm<AcceptanceFormValues>()
const [giftMaterialForm] = Form.useForm<GiftMaterialFormValues>()
const [noteForm] = Form.useForm<WorkerNoteFormValues>()
const [cancelRequestForm] = Form.useForm<CancelRequestFormValues>()
const [feedbackForm] = Form.useForm<FeedbackFormValues>()
@@ -292,6 +298,43 @@ export default function WorkerOrdersPage() {
setDetailOrder(null)
}
function openGiftMaterialModal(order: WorkOrder) {
markOrderAsRead(order)
setGiftMaterialOrder(order)
setGiftMaterialFiles([])
giftMaterialForm.resetFields()
setDetailOrder(null)
}
async function submitGiftMaterial(values: GiftMaterialFormValues) {
if (!giftMaterialOrder) return
if (giftMaterialFiles.length === 0) {
message.error('请上传主页截图')
return
}
const fields: Record<string, string> = {
gameNickname: String(values.gameNickname || '').trim(),
gameId: String(values.gameId || '').trim(),
serverZone: String(values.serverZone || '').trim(),
}
const system = String(values.system || '').trim()
if (system) fields.system = system
try {
await submitWorkerGiftMaterial(giftMaterialOrder.workOrderId, {
fields,
screenshots: giftMaterialFiles,
note: String(values.note || '').trim(),
})
message.success('资料已提交,等待后台确认加好友')
setGiftMaterialOrder(null)
setGiftMaterialFiles([])
giftMaterialForm.resetFields()
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '提交资料失败')
}
}
function openNoteModal(order: WorkOrder) {
markOrderAsRead(order)
setNoteOrder(order)
@@ -386,6 +429,11 @@ export default function WorkerOrdersPage() {
return
}
if (isGiftSubmissionBlocked(editingOrder)) {
message.error('好友赠送冷却未结束,暂不能提交验收')
return
}
const canAutoAccept = canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
if (acceptanceFiles.length === 0 && !canAutoAccept) {
message.error('请上传验收图片')
@@ -437,6 +485,7 @@ export default function WorkerOrdersPage() {
const columns = buildWorkerOrderTableColumns({
onOpenDetail: openOrderDetail,
onOpenAcceptance: openAcceptanceModal,
onOpenGiftMaterial: openGiftMaterialModal,
onRemindAcceptance: (order) => {
void submitAcceptanceReminder(order)
},
@@ -519,6 +568,7 @@ export default function WorkerOrdersPage() {
}}
onOpenDetail={openOrderDetail}
onOpenAcceptance={openAcceptanceModal}
onOpenGiftMaterial={openGiftMaterialModal}
onRemindAcceptance={(order) => {
void submitAcceptanceReminder(order)
}}
@@ -574,6 +624,7 @@ export default function WorkerOrdersPage() {
orderTimelineVisible={orderTimelineVisible}
onClose={() => setDetailOrder(null)}
onOpenAcceptance={openAcceptanceModal}
onOpenGiftMaterial={openGiftMaterialModal}
onOpenCancelRequest={openCancelRequestModal}
onOpenFeedback={openFeedbackModal}
/>
@@ -614,6 +665,16 @@ export default function WorkerOrdersPage() {
onAcceptanceFilesChange={setAcceptanceFiles}
onSubmitAcceptance={submitAcceptance}
onSaveAcceptanceImages={saveAcceptanceImages}
giftMaterialOrder={giftMaterialOrder}
giftMaterialForm={giftMaterialForm}
giftMaterialFiles={giftMaterialFiles}
onCloseGiftMaterial={() => {
setGiftMaterialOrder(null)
setGiftMaterialFiles([])
giftMaterialForm.resetFields()
}}
onGiftMaterialFilesChange={setGiftMaterialFiles}
onSubmitGiftMaterial={submitGiftMaterial}
/>
</>
)
@@ -1,4 +1,5 @@
import { Descriptions, Space, Tag, Typography } from 'antd'
import { useEffect, useState } from 'react'
import type {
CollectField,
@@ -32,10 +33,65 @@ export function patchWorkOrderReadState(current: unknown, workOrderId: number) {
}
}
/** 好友赠送验收模式(整单,不参与拼单)。 */
export function isFriendGiftOrder(order: WorkOrder) {
return order.acceptanceMode === 'friend_gift' && !order.myShare
}
export function getGiftPhaseMeta(phase: string): { label: string; color: string } {
const metas: Record<string, { label: string; color: string }> = {
material_required: { label: '待提交资料', color: 'orange' },
material_submitted: { label: '待后台加好友', color: 'processing' },
friend_countdown: { label: '赠送冷却中', color: 'gold' },
gift_ready: { label: '可赠送', color: 'green' },
}
return metas[String(phase || '').trim()] || { label: String(phase || '-'), color: 'default' }
}
/** 好友赠送模式:待提交本人资料阶段。 */
export function canSubmitGiftMaterial(order: WorkOrder) {
return (
isFriendGiftOrder(order) &&
['in_progress', 'problem'].includes(order.status) &&
order.gift?.phase === 'material_required'
)
}
/** 好友赠送模式:冷却未结束前不能提交赠送凭证。 */
export function isGiftSubmissionBlocked(order: WorkOrder) {
if (!isFriendGiftOrder(order)) return false
if (!['in_progress', 'problem'].includes(order.status)) return false
return order.gift?.phase !== 'gift_ready'
}
/** 好友赠送单进行中按赠送阶段展示状态,其他模式维持原状态文案。 */
export function getOrderStatusDisplay(order: WorkOrder): { label: string; color: string } {
if (isFriendGiftOrder(order) && order.status === 'in_progress' && order.gift) {
return getGiftPhaseMeta(order.gift.phase)
}
return { label: formatStatus(order.status), color: resolveStatusColor(order.status) }
}
export function getOrderStatusHint(order: WorkOrder) {
if (isFriendGiftOrder(order) && order.status === 'in_progress' && order.gift) {
const hints: Record<string, string> = {
material_required: '请提交本人游戏资料(昵称/ID/区服 + 主页截图),后台核对后添加好友。',
material_submitted: '资料已提交,等待后台添加游戏好友并确认。',
friend_countdown: '好友已添加,冷却结束后才能赠送。',
gift_ready: '冷却已结束,赠送完成后请上传赠送凭证并提交验收。',
}
return hints[order.gift.phase] || getStatusHint(order.status)
}
return getStatusHint(order.status)
}
export function canSubmitAcceptance(order: WorkOrder) {
if (order.myShare) {
return order.myShare.status === 'joined' && order.status !== 'accepted'
}
if (isFriendGiftOrder(order)) {
return ['in_progress', 'problem'].includes(order.status) && order.gift?.phase === 'gift_ready'
}
return ['in_progress', 'problem'].includes(order.status)
}
@@ -59,6 +115,8 @@ export function canSubmitFeedback(order: WorkOrder) {
}
export function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) {
// 好友赠送模式必须由后台人工验收赠送凭证,不适用 VIP 免审核直通。
if (isFriendGiftOrder(order)) return false
if (!workerCanAutoAcceptWithoutEvidence) return false
if (order.myShare) return order.myShare.status === 'joined' && order.status !== 'accepted'
return order.sharing?.enabled !== true && ['in_progress', 'problem'].includes(order.status)
@@ -265,6 +323,23 @@ export function getAcceptanceSubmittedAt(order: WorkOrder): string | null {
return order.submittedAt || acceptanceSubmittedAt || null
}
/** 好友赠送冷却倒计时:按分钟刷新,展示“X天X小时后可赠送”。 */
export function GiftCountdownTag({ giftAvailableAt }: { giftAvailableAt: string | null }) {
const [now, setNow] = useState(() => Date.now())
useEffect(() => {
const timer = window.setInterval(() => setNow(Date.now()), 30_000)
return () => window.clearInterval(timer)
}, [])
const targetMs = Date.parse(String(giftAvailableAt || ''))
if (!Number.isFinite(targetMs)) return null
const remaining = targetMs - now
if (remaining <= 0) return <Tag color="green">可赠送</Tag>
const hours = Math.ceil(remaining / 3_600_000)
const days = Math.floor(hours / 24)
const restHours = hours % 24
return <Tag color="gold">{days > 0 ? `${days}天${restHours}小时` : `${hours}小时`}后可赠送</Tag>
}
export function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)