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

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
@@ -208,6 +208,8 @@ export default function ProductRulesPanel() {
: undefined,
timeoutMinutes: rule.timeoutMinutes || 0,
timeoutPolicy: rule.timeoutPolicy || 'reopen',
acceptanceMode: rule.acceptanceMode || 'standard',
giftCooldownHours: rule.giftCooldownHours || 72,
fieldsText: formatRuleFields(rule.requirement?.fields || []),
sortOrder: rule.sortOrder,
enabled: rule.enabled,
@@ -244,6 +246,8 @@ export default function ProductRulesPanel() {
sharingTotalAmount?: number
timeoutMinutes?: number
timeoutPolicy?: string
acceptanceMode?: string
giftCooldownHours?: number
fieldsText?: string
enabled?: boolean
autoCreate?: boolean
@@ -251,6 +255,11 @@ export default function ProductRulesPanel() {
}) {
try {
const unitMode = values.pricingMode === 'unit'
const isFriendGift = values.acceptanceMode === 'friend_gift'
if (isFriendGift && values.sharingEnabled === true) {
message.error('好友赠送验收模式不支持拼单,请先关闭拼单')
return
}
const hasManualSharingOverride =
form.isFieldTouched('sharingTotalQuantity') || form.isFieldTouched('sharingUnitReward')
? Number(values.sharingTotalQuantity || 0) > 0 ||
@@ -262,6 +271,8 @@ export default function ProductRulesPanel() {
ruleId: editingRule?.ruleId,
ruleKey: editingRule?.ruleKey || '',
unitPrice: unitMode ? Number(values.unitPrice || 0) : 0,
acceptanceMode: isFriendGift ? 'friend_gift' : 'standard',
sharingEnabled: isFriendGift ? false : values.sharingEnabled === true,
sharingAutoFromOrder:
unitMode && values.sharingEnabled === true && !hasManualSharingOverride,
})
@@ -638,6 +649,11 @@ export default function ProductRulesPanel() {
</Tag>
) : null}
{row.acceptanceMode === 'friend_gift' ? (
<Tag color="orange" style={{ fontSize: 10, margin: 0, padding: '0 3px' }}>
</Tag>
) : null}
</Space>
</Space>
),
@@ -728,6 +744,8 @@ export default function ProductRulesPanel() {
enabled: true,
autoCreate: true,
sharingEnabled: false,
acceptanceMode: 'standard',
giftCooldownHours: 72,
fieldsText:
'gameId:游戏编号\ngameNickname:游戏昵称\nsystem:系统#安卓,苹果\nserverZone:区服#QQ区,微信区',
}}
@@ -794,6 +812,53 @@ export default function ProductRulesPanel() {
</Form.Item>
</div>
{/* 验收模式:好友赠送模式与拼单互斥 */}
<div className="form-grid-3col">
<Form.Item
label="验收模式"
name="acceptanceMode"
extra="好友赠送:打手接单后先提交本人资料,后台加好友并冷却后才能赠送交付"
>
<Radio.Group
optionType="button"
buttonStyle="solid"
className="full-width"
options={[
{ value: 'standard', label: '标准验收' },
{ value: 'friend_gift', label: '好友赠送' },
]}
onChange={(event) => {
if (event.target.value === 'friend_gift') {
form.setFieldsValue({ sharingEnabled: false })
}
}}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({ getFieldValue }) =>
getFieldValue('acceptanceMode') === 'friend_gift' ? (
<Form.Item
label="赠送冷却"
name="giftCooldownHours"
rules={[{ required: true, message: '请填写冷却小时数' }]}
extra="确认加好友后需冷却该时长才能赠送(默认 72 小时 = 3 天)"
>
<InputNumber
min={1}
max={720}
step={1}
addonAfter="小时"
className="full-width"
/>
</Form.Item>
) : (
<Form.Item label={<span />} />
)
}
</Form.Item>
</div>
{/* Row 3: 超时策略 + 来源 + 平台 */}
<div className="form-grid-3col">
<Form.Item label="超时策略" name="timeoutPolicy">
@@ -825,6 +890,7 @@ export default function ProductRulesPanel() {
{({ getFieldValue }) => {
const sharingEnabled = getFieldValue('sharingEnabled')
const unitMode = getFieldValue('pricingMode') === 'unit'
const friendGiftMode = getFieldValue('acceptanceMode') === 'friend_gift'
return (
<div className="switches-inline-group span-2">
@@ -834,8 +900,13 @@ export default function ProductRulesPanel() {
<Form.Item label="自动建单" name="autoCreate" valuePropName="checked">
<Switch size="small" />
</Form.Item>
<Form.Item label="启用拼单" name="sharingEnabled" valuePropName="checked">
<Switch size="small" />
<Form.Item
label="启用拼单"
name="sharingEnabled"
valuePropName="checked"
tooltip={friendGiftMode ? '好友赠送模式不支持拼单' : undefined}
>
<Switch size="small" disabled={friendGiftMode} />
</Form.Item>
{sharingEnabled ? (
@@ -72,10 +72,14 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void
rewardAmount: number
paymentAmount: number
materialComplete?: boolean
acceptanceMode?: string
}) {
setLoading(true)
try {
await createAdminMockWorkOrder(values)
await createAdminMockWorkOrder({
...values,
acceptanceMode: values.acceptanceMode || 'standard',
})
message.success('Mock 工单已生成')
await onCreated()
} catch (error) {
@@ -147,6 +151,20 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void
>
<Switch />
</Form.Item>
<Form.Item
className="worker-order-mock-field-complete"
label="验收模式"
name="acceptanceMode"
initialValue="standard"
>
<Select
style={{ width: 110 }}
options={[
{ value: 'standard', label: '标准验收' },
{ value: 'friend_gift', label: '好友赠送' },
]}
/>
</Form.Item>
<Form.Item className="worker-order-mock-action">
<Button type="primary" icon={<PlusOutlined />} htmlType="submit" loading={loading}>
@@ -49,6 +49,7 @@ import {
assignAdminWorkOrderToWorker,
cancelAdminWorkOrder,
cancelAdminWorkOrderShare,
confirmAdminWorkOrderFriend,
deductAdminWorkOrderPendingDeposit,
deleteAdminWorkOrder,
fetchAdminWorkCategories,
@@ -60,6 +61,7 @@ import {
pinAdminWorkOrder,
publishAdminWorkOrder,
reopenAdminCancelledWorkOrder,
resetAdminWorkOrderGiftMaterial,
resolveAdminProblemWorkOrder,
submitAdminWorkOrderMaterial,
unpublishAdminWorkOrder,
@@ -74,6 +76,8 @@ import { hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import {
DeadlineCountdown,
formatAcceptanceMode,
formatGiftPhase,
formatTimeoutPolicyLabel,
getAcceptanceFiles,
getAcceptanceImageUrls,
@@ -88,6 +92,7 @@ import {
formatMoney,
formatSharingShareStatus,
formatStatus,
resolveGiftPhaseColor,
resolveSharingShareStatusColor,
resolveStatusColor,
} from './shared'
@@ -117,6 +122,14 @@ const FILTERABLE_WORK_ORDER_STATUSES = [
] as const
const VIP_EVIDENCE_PENDING_STATUS = 'vip_evidence_pending'
const GIFT_PHASE_FILTER_OPTIONS = [
'material_required',
'material_submitted',
'friend_countdown',
'gift_ready',
] as const
type MaterialFormValues = {
fields?: Record<string, string>
rewardAmount?: number
@@ -145,6 +158,8 @@ export default function WorkOrdersPanel() {
const [keywordInput, setKeywordInput] = useState('')
const [workerId, setWorkerId] = useState<number | undefined>()
const [workerLevelIds, setWorkerLevelIds] = useState<number[]>([])
const [acceptanceMode, setAcceptanceMode] = useState<string | undefined>()
const [giftPhase, setGiftPhase] = useState<string | undefined>()
const [filterWorkerKeyword, setFilterWorkerKeyword] = useState('')
const [assignWorkerKeyword, setAssignWorkerKeyword] = useState('')
const [page, setPage] = useState(1)
@@ -220,6 +235,8 @@ export default function WorkOrdersPanel() {
workerId,
workerLevelIds.join(','),
targetWorkOrderId,
acceptanceMode || '',
giftPhase || '',
page,
pageSize,
createdRange ? createdRange[0] : '',
@@ -230,6 +247,8 @@ export default function WorkOrdersPanel() {
status:
selectedWorkOrderStatuses.length > 0 ? selectedWorkOrderStatuses.join(',') : undefined,
vipEvidencePending: vipEvidencePending || undefined,
acceptanceMode: acceptanceMode || undefined,
giftPhase: giftPhase || undefined,
workOrderId: targetWorkOrderId,
keyword,
workerId,
@@ -277,12 +296,26 @@ export default function WorkOrdersPanel() {
})
useEffect(() => {
setSelectedAcceptanceOrderIds([])
}, [statuses, keyword, workerId, workerLevelIds, targetWorkOrderId, page, pageSize])
}, [
statuses,
keyword,
workerId,
workerLevelIds,
targetWorkOrderId,
acceptanceMode,
giftPhase,
page,
pageSize,
])
useEffect(() => {
if (!showWorkerLevelFilter) setWorkerLevelIds([])
}, [showWorkerLevelFilter])
useEffect(() => {
if (acceptanceMode !== 'friend_gift') setGiftPhase(undefined)
}, [acceptanceMode])
function openAssignModal(row: WorkOrder) {
setAssignOrder(row)
setAssignWorkerId(undefined)
@@ -593,6 +626,8 @@ export default function WorkOrdersPanel() {
setStatuses([])
setWorkerId(undefined)
setWorkerLevelIds([])
setAcceptanceMode(undefined)
setGiftPhase(undefined)
setCreatedRange(null)
setPage(1)
}
@@ -716,6 +751,47 @@ export default function WorkOrdersPanel() {
})
}
function confirmFriendAdded(row: WorkOrder) {
const cooldownHours = Number(row.gift?.cooldownHours || 72)
modal.confirm({
title: '确认已添加打手为游戏好友?',
content: (
<div>
<Typography.Paragraph>
/ ID /
</Typography.Paragraph>
<Typography.Paragraph>
{cooldownHours}{' '}
</Typography.Paragraph>
</div>
),
okText: '已加好友,开始冷却',
cancelText: '取消',
onOk: () =>
runAction(
() => confirmAdminWorkOrderFriend(row.workOrderId),
`已确认加好友,${cooldownHours} 小时冷却开始`,
),
})
}
function requestResetGiftMaterial(row: WorkOrder) {
modal.confirm({
title: '重置打手好友赠送资料?',
content:
'打手已提交的资料、加好友时间与冷却进度将全部清零,订单回到“待打手提交资料”阶段,需打手重新提交后再次确认加好友。',
okText: '确认重置',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: () =>
runAction(
() => resetAdminWorkOrderGiftMaterial(row.workOrderId),
'已重置,等待打手重新提交资料',
),
})
}
const columns: TableColumnsType<WorkOrder> = [
{
title: '订单',
@@ -828,6 +904,12 @@ export default function WorkOrdersPanel() {
</Tag>
) : null}
{row.acceptanceMode === 'friend_gift' ? <Tag color="purple"></Tag> : null}
{row.acceptanceMode === 'friend_gift' && row.gift ? (
<Tag color={resolveGiftPhaseColor(row.gift.phase)}>
{formatGiftPhase(row.gift.phase)}
</Tag>
) : null}
</Space>
)
},
@@ -1003,6 +1085,20 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{row.capabilities?.canConfirmFriend ? (
<Button
type="primary"
icon={<UserAddOutlined />}
onClick={() => confirmFriendAdded(row)}
>
</Button>
) : null}
{row.capabilities?.canResetGiftFlow ? (
<Button danger onClick={() => requestResetGiftMaterial(row)}>
</Button>
) : null}
{row.capabilities?.canMarkProblem ? (
<Button icon={<WarningOutlined />} danger onClick={() => setProblemOrder(row)}>
@@ -1101,6 +1197,36 @@ export default function WorkOrdersPanel() {
}}
/>
) : null}
<Select
allowClear
value={acceptanceMode}
placeholder="全部验收模式"
style={{ width: 150 }}
options={[
{ value: 'standard', label: '标准验收' },
{ value: 'friend_gift', label: '好友赠送' },
]}
onChange={(nextMode) => {
setAcceptanceMode(nextMode)
setPage(1)
}}
/>
{acceptanceMode === 'friend_gift' ? (
<Select
allowClear
value={giftPhase}
placeholder="全部赠送阶段"
style={{ width: 170 }}
options={GIFT_PHASE_FILTER_OPTIONS.map((phase) => ({
value: phase,
label: <Tag color={resolveGiftPhaseColor(phase)}>{formatGiftPhase(phase)}</Tag>,
}))}
onChange={(nextPhase) => {
setGiftPhase(nextPhase)
setPage(1)
}}
/>
) : null}
<AdminRangePicker
value={createdRange ? [dayjs(createdRange[0]), dayjs(createdRange[1])] : null}
onChange={(dates) => {
@@ -1305,6 +1431,93 @@ export default function WorkOrdersPanel() {
</div>
</Card>
{detailOrder.acceptanceMode === 'friend_gift' && detailOrder.gift ? (
<Card
title="好友赠送进度"
size="small"
extra={
<Space>
<Tag color="purple">{formatAcceptanceMode(detailOrder.acceptanceMode)}</Tag>
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
{formatGiftPhase(detailOrder.gift.phase)}
</Tag>
{detailOrder.capabilities?.canConfirmFriend ? (
<Button
type="primary"
size="small"
onClick={() => confirmFriendAdded(detailOrder)}
>
</Button>
) : null}
{detailOrder.capabilities?.canResetGiftFlow ? (
<Button
size="small"
danger
onClick={() => requestResetGiftMaterial(detailOrder)}
>
</Button>
) : null}
</Space>
}
>
<div className="worker-order-detail-stack">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="冷却时长">
{detailOrder.gift.cooldownHours}
</Descriptions.Item>
<Descriptions.Item label="打手资料提交">
{formatAdminDateTime(detailOrder.gift.boosterMaterial?.submittedAt)}
</Descriptions.Item>
<Descriptions.Item label="已加好友">
{formatAdminDateTime(detailOrder.gift.friendAddedAt)}
</Descriptions.Item>
<Descriptions.Item label="可赠送时间">
{detailOrder.gift.giftAvailableAt
? formatAdminDateTime(detailOrder.gift.giftAvailableAt)
: '-'}
</Descriptions.Item>
</Descriptions>
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
{(
[
['gameNickname', '游戏昵称'],
['gameId', '游戏ID'],
['serverZone', '区服'],
['system', '系统'],
] as const
).map(([key, label]) => (
<Descriptions.Item key={key} label={label}>
<span className="worker-order-detail-value">
{String(detailOrder.gift?.boosterMaterial?.fields?.[key] || '').trim() ||
'-'}
</span>
</Descriptions.Item>
))}
<Descriptions.Item label="打手备注" span={2}>
<span className="worker-order-detail-value">
{String(detailOrder.gift?.boosterMaterial?.note || '').trim() || '-'}
</span>
</Descriptions.Item>
</Descriptions>
<div>
<Typography.Text type="secondary"></Typography.Text>
<div style={{ marginTop: 8 }}>
{detailOrder.gift.boosterMaterial?.screenshots?.length ? (
<ImagePreviewList
files={detailOrder.gift.boosterMaterial.screenshots}
size={72}
/>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
</div>
</div>
</Card>
) : null}
<Card title="验收信息" size="small">
<div className="worker-order-detail-stack">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
@@ -244,6 +244,31 @@ export function formatSharingShareStatus(status: string) {
return labels[status] || status
}
export function formatAcceptanceMode(mode: string) {
if (mode === 'friend_gift') return '好友赠送'
return '标准验收'
}
export function formatGiftPhase(phase: string) {
const labels: Record<string, string> = {
material_required: '待打手提交资料',
material_submitted: '待确认加好友',
friend_countdown: '赠送冷却中',
gift_ready: '可赠送',
}
return labels[phase] || phase
}
export function resolveGiftPhaseColor(phase: string) {
const colors: Record<string, string> = {
material_required: 'orange',
material_submitted: 'geekblue',
friend_countdown: 'gold',
gift_ready: 'green',
}
return colors[phase] || 'default'
}
export function resolveSharingShareStatusColor(status: string) {
if (status === 'accepted') return 'green'
if (status === 'submitted') return 'gold'