新增好友赠送验收模式支持打手资料提交加好友冷却与模板配置
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user