资料字段支持下拉选项与买家截图,大厅按 VIP 等级延迟可见
资料字段: - CollectField 支持 type(text/select) 与 options,规则字段语法 key:名称#选项1,选项2 - 默认字段改为游戏编号/游戏昵称/系统(安卓,苹果)/区服(QQ区,微信区),旧默认模板字段自动替换为新版 - 采集页与管理端补资料支持单选下拉与买家主页截图上传(新增公开上传接口) - 管理端工单详情展示买家截图 VIP 延迟可见: - 等级权限新增 visibleDelaySeconds(默认 VIP1=20s~VIP5=0s,可后台配置) - 大厅按等级延迟过滤新发布工单,页面提示延迟规则
This commit is contained in:
@@ -107,6 +107,7 @@ export default function LevelsPanel() {
|
||||
depositFreeAmount: level.depositFreeAmount,
|
||||
maxActiveOrders: level.maxActiveOrders,
|
||||
upgradeThreshold: level.upgradeThreshold,
|
||||
visibleDelaySeconds: level.visibleDelaySeconds,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -121,6 +122,7 @@ export default function LevelsPanel() {
|
||||
depositFreeAmount?: number
|
||||
maxActiveOrders?: number
|
||||
upgradeThreshold?: number
|
||||
visibleDelaySeconds?: number
|
||||
}) {
|
||||
try {
|
||||
await saveAdminWorkerLevel(values)
|
||||
@@ -140,6 +142,7 @@ export default function LevelsPanel() {
|
||||
depositFreeAmount: level.depositFreeAmount,
|
||||
maxActiveOrders: level.maxActiveOrders,
|
||||
upgradeThreshold: level.upgradeThreshold,
|
||||
visibleDelaySeconds: level.visibleDelaySeconds,
|
||||
status: level.status === 'active' ? 'disabled' : 'active',
|
||||
})
|
||||
message.success(level.status === 'active' ? '等级已停用' : '等级已启用')
|
||||
@@ -174,6 +177,12 @@ export default function LevelsPanel() {
|
||||
},
|
||||
{ title: '最大同时接单', dataIndex: 'maxActiveOrders' },
|
||||
{ title: '升级所需累计完成', dataIndex: 'upgradeThreshold' },
|
||||
{
|
||||
title: '可见延迟',
|
||||
width: 110,
|
||||
render: (_, row) =>
|
||||
row.visibleDelaySeconds > 0 ? `${row.visibleDelaySeconds} 秒` : '立即',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 110,
|
||||
@@ -217,6 +226,7 @@ export default function LevelsPanel() {
|
||||
depositFreeAmount: 0,
|
||||
maxActiveOrders: 1,
|
||||
upgradeThreshold: 0,
|
||||
visibleDelaySeconds: 20,
|
||||
}}
|
||||
onFinish={saveLevel}
|
||||
>
|
||||
@@ -247,6 +257,13 @@ export default function LevelsPanel() {
|
||||
>
|
||||
<InputNumber min={0} max={100000} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="大厅可见延迟(秒)"
|
||||
name="visibleDelaySeconds"
|
||||
tooltip="发布到大厅后该等级打手延迟可见秒数,默认 VIP1=20 / VIP2=15 / VIP3=10 / VIP4=5 / VIP5=0,填 -1 使用默认"
|
||||
>
|
||||
<InputNumber min={-1} max={3600} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
|
||||
@@ -231,7 +231,7 @@ export default function ProductRulesPanel() {
|
||||
sharingUnitReward: 5,
|
||||
sharingTotalAmount: 50,
|
||||
fieldsText:
|
||||
'gameAccount:游戏账号\nserverName:区服\nroleName:角色名',
|
||||
'gameId:游戏编号\ngameNickname:游戏昵称\nsystem:系统#安卓,苹果\nserverZone:区服#QQ区,微信区',
|
||||
}}
|
||||
onFinish={saveRule}
|
||||
>
|
||||
@@ -382,7 +382,11 @@ export default function ProductRulesPanel() {
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item label="资料字段" name="fieldsText">
|
||||
<Form.Item
|
||||
label="资料字段"
|
||||
name="fieldsText"
|
||||
extra="每行一个字段:key:名称,下拉项用 # 分隔(如 system:系统#安卓,苹果)"
|
||||
>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
|
||||
@@ -88,6 +88,7 @@ import {
|
||||
getAcceptanceSubmittedAt,
|
||||
getMaterialFields,
|
||||
getMaterialDetailItems,
|
||||
getMaterialScreenshots,
|
||||
getRequirementFields,
|
||||
renderDetailFieldValues,
|
||||
renderMaterialSummary,
|
||||
@@ -111,6 +112,7 @@ export default function WorkOrdersPanel() {
|
||||
const [problemOrder, setProblemOrder] = useState<WorkOrder | null>(null)
|
||||
const [resolutionOrder, setResolutionOrder] = useState<WorkOrder | null>(null)
|
||||
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
|
||||
const [materialScreenshots, setMaterialScreenshots] = useState<UploadedFile[]>([])
|
||||
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
||||
const [problemForm] = Form.useForm()
|
||||
const [resolutionForm] = Form.useForm()
|
||||
@@ -247,19 +249,24 @@ export default function WorkOrdersPanel() {
|
||||
resolutionForm.resetFields()
|
||||
}
|
||||
|
||||
async function submitMaterial(values: { fields?: Record<string, string> }) {
|
||||
async function submitMaterial(values: {
|
||||
fields?: Record<string, string>
|
||||
screenshots?: UploadedFile[]
|
||||
}) {
|
||||
if (!materialOrder) return
|
||||
const succeeded = await runAction(
|
||||
() =>
|
||||
submitAdminWorkOrderMaterial(
|
||||
materialOrder.workOrderId,
|
||||
values.fields || {},
|
||||
values.screenshots || [],
|
||||
),
|
||||
'订单资料已保存',
|
||||
)
|
||||
if (!succeeded) return
|
||||
setMaterialOrder(null)
|
||||
materialForm.resetFields()
|
||||
setMaterialScreenshots([])
|
||||
}
|
||||
|
||||
async function submitSourceSync(values: { platformOrderId?: string }) {
|
||||
@@ -279,6 +286,7 @@ export default function WorkOrdersPanel() {
|
||||
function openMaterialModal(row: WorkOrder) {
|
||||
setMaterialOrder(row)
|
||||
materialForm.setFieldsValue({ fields: getMaterialFields(row) })
|
||||
setMaterialScreenshots([])
|
||||
}
|
||||
|
||||
function openDetailDrawer(row: WorkOrder) {
|
||||
@@ -633,6 +641,12 @@ export default function WorkOrdersPanel() {
|
||||
getMaterialDetailItems(detailOrder),
|
||||
'当前还没有填写资料',
|
||||
)}
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Typography.Text type="secondary">买家主页截图</Typography.Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<ImagePreviewList files={getMaterialScreenshots(detailOrder)} size={72} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="资料要求" size="small">
|
||||
@@ -775,13 +789,31 @@ export default function WorkOrdersPanel() {
|
||||
name={['fields', field.key]}
|
||||
rules={
|
||||
field.required
|
||||
? [{ required: true, message: `请填写${field.label}` }]
|
||||
? [{ required: true, message: `请选择/填写${field.label}` }]
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Input />
|
||||
{field.type === 'select' && (field.options || []).length > 0 ? (
|
||||
<Select
|
||||
options={(field.options || []).map((option) => ({
|
||||
label: option,
|
||||
value: option,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<Input />
|
||||
)}
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item label="买家主页截图">
|
||||
<ImageUpload
|
||||
scene="collect-material"
|
||||
scope="admin"
|
||||
maxCount={5}
|
||||
value={materialScreenshots}
|
||||
onChange={setMaterialScreenshots}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -164,6 +164,26 @@ export function getAcceptanceSubmittedAt(order: WorkOrder): string | null {
|
||||
return order.submittedAt || acceptanceSubmittedAt || null
|
||||
}
|
||||
|
||||
export function getMaterialScreenshots(order: WorkOrder): UploadedFile[] {
|
||||
const material = asRecord(order.material)
|
||||
const collect = asRecord(material.collect)
|
||||
const screenshots = Array.isArray(collect.screenshots) ? collect.screenshots : []
|
||||
return screenshots
|
||||
.map((item) => {
|
||||
const file = asRecord(item)
|
||||
const url = String(file.url || '').trim()
|
||||
if (!url) return null
|
||||
return {
|
||||
objectKey: String(file.objectKey || file.object_key || '').trim(),
|
||||
filename: String(file.originalFilename || file.filename || '').trim(),
|
||||
url,
|
||||
mediumUrl: String(file.mediumUrl || file.medium_url || '').trim(),
|
||||
thumbnailUrl: String(file.thumbnailUrl || file.thumbnail_url || '').trim(),
|
||||
} as UploadedFile
|
||||
})
|
||||
.filter((item): item is UploadedFile => Boolean(item))
|
||||
}
|
||||
|
||||
export function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
|
||||
Reference in New Issue
Block a user