feat(order-service): 重构分类资料收集与订单状态链路
This commit is contained in:
@@ -0,0 +1,689 @@
|
||||
import {
|
||||
ArrowDownOutlined,
|
||||
ArrowUpOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import {
|
||||
archiveAdminOrderServiceProfile,
|
||||
fetchAdminOrderServiceAutoPublishJobs,
|
||||
fetchAdminOrderServiceProfiles,
|
||||
publishAdminOrderServiceProfile,
|
||||
retryAdminOrderServiceAutoPublishJob,
|
||||
saveAdminOrderServiceProfile,
|
||||
type AdminOrderServiceAutoPublishJob,
|
||||
type AdminOrderServiceProfile,
|
||||
type OrderServiceField,
|
||||
} from '@/services/admin/worker-platform'
|
||||
import { uploadFile } from '@/services/files'
|
||||
import type { UploadedFile } from '@/types/worker-platform'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
|
||||
type ScreenshotForm = {
|
||||
enabled: boolean
|
||||
required: boolean
|
||||
minCount: number
|
||||
maxCount: number
|
||||
label: string
|
||||
helpText: string
|
||||
}
|
||||
|
||||
type AutoPublishForm = {
|
||||
enabled: boolean
|
||||
requireVoucherConsume: boolean
|
||||
retryMax: number
|
||||
}
|
||||
|
||||
type OrderServiceFormValues = {
|
||||
name: string
|
||||
title: string
|
||||
description: string
|
||||
submitText: string
|
||||
successText: string
|
||||
screenshots: ScreenshotForm
|
||||
autoPublish: AutoPublishForm
|
||||
}
|
||||
|
||||
const DEFAULT_SCREENSHOTS: ScreenshotForm = {
|
||||
enabled: true,
|
||||
required: true,
|
||||
minCount: 1,
|
||||
maxCount: 5,
|
||||
label: '买家主页截图',
|
||||
helpText: '',
|
||||
}
|
||||
|
||||
const DEFAULT_AUTO_PUBLISH: AutoPublishForm = {
|
||||
enabled: false,
|
||||
requireVoucherConsume: true,
|
||||
retryMax: 3,
|
||||
}
|
||||
|
||||
export default function OrderServicePanel() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm<OrderServiceFormValues>()
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null)
|
||||
const [fields, setFields] = useState<OrderServiceField[]>([])
|
||||
const [guideImage, setGuideImage] = useState<UploadedFile | Record<string, unknown> | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [publishing, setPublishing] = useState(false)
|
||||
const [retryingJobId, setRetryingJobId] = useState(0)
|
||||
|
||||
const profilesQuery = useQuery({
|
||||
queryKey: ['admin-order-service-profiles'],
|
||||
queryFn: fetchAdminOrderServiceProfiles,
|
||||
})
|
||||
const jobsQuery = useQuery({
|
||||
queryKey: ['admin-order-service-auto-publish-jobs', 'failed'],
|
||||
queryFn: () => fetchAdminOrderServiceAutoPublishJobs('failed'),
|
||||
})
|
||||
|
||||
const categories = profilesQuery.data?.data.categories || []
|
||||
const profiles = profilesQuery.data?.data.profiles || []
|
||||
const failedJobs = jobsQuery.data?.data.items || []
|
||||
const selectedCategory = categories.find((item) => item.categoryId === selectedCategoryId) || null
|
||||
const profile = profiles.find((item) => item.categoryId === selectedCategoryId) || null
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCategoryId !== null || categories.length === 0) return
|
||||
setSelectedCategoryId(categories[0]?.categoryId || null)
|
||||
}, [categories, selectedCategoryId])
|
||||
|
||||
useEffect(() => {
|
||||
applyProfile(profile)
|
||||
}, [profile, selectedCategoryId])
|
||||
|
||||
function applyProfile(nextProfile: AdminOrderServiceProfile | null) {
|
||||
form.setFieldsValue({
|
||||
name: nextProfile?.name || selectedCategory?.name || '',
|
||||
title: nextProfile?.title || '',
|
||||
description: nextProfile?.description || '',
|
||||
submitText: nextProfile?.submitText || '提交资料',
|
||||
successText: nextProfile?.successText || '资料已提交,请等待处理',
|
||||
screenshots: nextProfile?.screenshots || DEFAULT_SCREENSHOTS,
|
||||
autoPublish: nextProfile?.autoPublish || DEFAULT_AUTO_PUBLISH,
|
||||
})
|
||||
setFields(normalizeFieldOrder(nextProfile?.fields || []))
|
||||
setGuideImage(nextProfile?.guideImage || null)
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-order-service-profiles'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-order-service-auto-publish-jobs'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-categories'] }),
|
||||
])
|
||||
}
|
||||
|
||||
function addField() {
|
||||
setFields((current) =>
|
||||
normalizeFieldOrder([
|
||||
...current,
|
||||
{
|
||||
key: `field${Date.now().toString(36)}`,
|
||||
label: '新字段',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '',
|
||||
helpText: '',
|
||||
options: [],
|
||||
sortOrder: (current.length + 1) * 10,
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function updateField(index: number, patch: Partial<OrderServiceField>) {
|
||||
setFields((current) =>
|
||||
current.map((row, rowIndex) => (rowIndex === index ? { ...row, ...patch } : row)),
|
||||
)
|
||||
}
|
||||
|
||||
function moveField(index: number, direction: -1 | 1) {
|
||||
const next = index + direction
|
||||
if (next < 0 || next >= fields.length) return
|
||||
setFields((current) => {
|
||||
const rows = [...current]
|
||||
const [row] = rows.splice(index, 1)
|
||||
if (!row) return current
|
||||
rows.splice(next, 0, row)
|
||||
return normalizeFieldOrder(rows)
|
||||
})
|
||||
}
|
||||
|
||||
function removeField(index: number) {
|
||||
setFields((current) => normalizeFieldOrder(current.filter((_, rowIndex) => rowIndex !== index)))
|
||||
}
|
||||
|
||||
async function handleSave(values: OrderServiceFormValues) {
|
||||
if (!selectedCategoryId) return
|
||||
const normalizedFields = normalizeFieldOrder(fields)
|
||||
const duplicateKeys = findDuplicateKeys(normalizedFields)
|
||||
if (duplicateKeys.length > 0) {
|
||||
message.error(`字段 Key 不能重复:${duplicateKeys.join('、')}`)
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
await saveAdminOrderServiceProfile({
|
||||
categoryId: selectedCategoryId,
|
||||
name: values.name || selectedCategory?.name || '订单服务配置',
|
||||
title: values.title || '',
|
||||
description: values.description || '',
|
||||
guideImage: guideImage || {},
|
||||
fields: normalizedFields,
|
||||
screenshots: values.screenshots,
|
||||
autoPublish: values.autoPublish,
|
||||
submitText: values.submitText,
|
||||
successText: values.successText,
|
||||
})
|
||||
message.success('草稿已保存;点击“发布新版本”后才会影响新工单')
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublish() {
|
||||
if (!profile) {
|
||||
message.warning('请先保存配置草稿')
|
||||
return
|
||||
}
|
||||
setPublishing(true)
|
||||
try {
|
||||
const response = await publishAdminOrderServiceProfile(profile.id)
|
||||
message.success(`已发布 v${response.data.versionNo}`)
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '发布失败')
|
||||
} finally {
|
||||
setPublishing(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleArchive() {
|
||||
if (!profile) return
|
||||
setPublishing(true)
|
||||
try {
|
||||
await archiveAdminOrderServiceProfile(profile.id)
|
||||
message.success('配置已归档;已有工单快照不受影响')
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '归档失败')
|
||||
} finally {
|
||||
setPublishing(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRetryJob(job: AdminOrderServiceAutoPublishJob) {
|
||||
setRetryingJobId(job.id)
|
||||
try {
|
||||
await retryAdminOrderServiceAutoPublishJob(job.id)
|
||||
message.success('自动发布任务已重新排队')
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '重试失败')
|
||||
} finally {
|
||||
setRetryingJobId(0)
|
||||
}
|
||||
}
|
||||
|
||||
const categoryColumns = useMemo(
|
||||
() => [
|
||||
{ title: '分类', dataIndex: 'name' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (value: string) => (
|
||||
<Tag color={value === 'active' ? 'green' : 'default'}>
|
||||
{value === 'active' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '订单服务',
|
||||
width: 180,
|
||||
render: (_: unknown, row: { categoryId: number }) => {
|
||||
const rowProfile = profiles.find((item) => item.categoryId === row.categoryId)
|
||||
if (!rowProfile) return <Tag>未配置</Tag>
|
||||
if (rowProfile.status === 'draft') {
|
||||
return (
|
||||
<Tag color={rowProfile.currentVersionNo ? 'orange' : 'default'}>
|
||||
{rowProfile.currentVersionNo
|
||||
? `有未发布修改(线上 v${rowProfile.currentVersionNo})`
|
||||
: '草稿'}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tag color={rowProfile.status === 'published' ? 'green' : 'default'}>
|
||||
{rowProfile.status === 'published'
|
||||
? `已发布 v${rowProfile.currentVersionNo || 1}`
|
||||
: rowProfile.status}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 90,
|
||||
render: (_: unknown, row: { categoryId: number }) => (
|
||||
<Button size="small" onClick={() => setSelectedCategoryId(row.categoryId)}>
|
||||
配置
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[profiles],
|
||||
)
|
||||
|
||||
const jobColumns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: '订单',
|
||||
render: (_: unknown, row: AdminOrderServiceAutoPublishJob) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong>{row.productName || '-'}</Text>
|
||||
<Text type="secondary">{row.platformOrderId || '-'}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '重试',
|
||||
width: 90,
|
||||
render: (_: unknown, row: AdminOrderServiceAutoPublishJob) =>
|
||||
`${row.attemptCount}/${row.maxAttempts}`,
|
||||
},
|
||||
{
|
||||
title: '失败原因',
|
||||
render: (_: unknown, row: AdminOrderServiceAutoPublishJob) => (
|
||||
<Text type="danger">{row.lastError || '未知错误'}</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
width: 180,
|
||||
render: (_: unknown, row: AdminOrderServiceAutoPublishJob) =>
|
||||
row.updatedAt ? formatAdminDateTime(row.updatedAt) : '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
render: (_: unknown, row: AdminOrderServiceAutoPublishJob) => (
|
||||
<Button
|
||||
size="small"
|
||||
loading={retryingJobId === row.id}
|
||||
onClick={() => void handleRetryJob(row)}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[retryingJobId],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<Card
|
||||
size="small"
|
||||
title="订单服务配置"
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={profilesQuery.isFetching || jobsQuery.isFetching}
|
||||
onClick={() => void refresh()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="一个分类使用一套订单服务配置"
|
||||
description="接单模板仍通过“所属分类”绑定分类;客户资料字段只在这里维护,不再在接单模板里重复配置。"
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<PercentTable
|
||||
rowKey="categoryId"
|
||||
size="small"
|
||||
loading={profilesQuery.isLoading}
|
||||
dataSource={categories}
|
||||
columns={categoryColumns}
|
||||
pagination={false}
|
||||
rowClassName={(row: { categoryId: number }) =>
|
||||
row.categoryId === selectedCategoryId ? 'ant-table-row-selected' : ''
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{selectedCategory ? (
|
||||
<Card
|
||||
size="small"
|
||||
title={`配置:${selectedCategory.name}`}
|
||||
extra={
|
||||
<Space size={6} wrap>
|
||||
<Tag color={profile?.status === 'published' ? 'green' : 'orange'}>
|
||||
{profile?.status === 'published'
|
||||
? `线上 v${profile.currentVersionNo || 1}`
|
||||
: profile?.currentVersionNo
|
||||
? `线上 v${profile.currentVersionNo},当前有草稿`
|
||||
: '未发布'}
|
||||
</Tag>
|
||||
<Button loading={publishing} onClick={() => void handlePublish()}>
|
||||
发布新版本
|
||||
</Button>
|
||||
{profile ? (
|
||||
<Button danger loading={publishing} onClick={() => void handleArchive()}>
|
||||
归档
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSave}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item label="配置名称" name="name" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item label="客户页标题" name="title">
|
||||
<Input placeholder="例如:蛋仔派对资料填写" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item label="页面说明" name="description">
|
||||
<Input placeholder="显示在客户页顶部" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label="提交按钮文案" name="submitText">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label="提交成功文案" name="successText">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card size="small" title="用户引导图(一张)" style={{ marginBottom: 16 }}>
|
||||
<ImageUpload
|
||||
scene="order-service"
|
||||
scope="admin"
|
||||
maxCount={1}
|
||||
value={guideImage ? [guideImage as UploadedFile] : []}
|
||||
onChange={(files) => setGuideImage(files[0] || null)}
|
||||
uploader={(file, scene) => uploadFile(file, scene, 'admin')}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
title="资料字段"
|
||||
extra={
|
||||
<Button icon={<PlusOutlined />} onClick={addField} size="small">
|
||||
添加字段
|
||||
</Button>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
{fields.length === 0 ? (
|
||||
<Empty description="至少添加一个资料字段后才能发布" />
|
||||
) : (
|
||||
<Space direction="vertical" className="full-width">
|
||||
{fields.map((field, index) => (
|
||||
<Card key={`${field.key}-${index}`} size="small">
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col xs={24} md={5}>
|
||||
<Input
|
||||
addonBefore="Key"
|
||||
value={field.key}
|
||||
onChange={(event) => updateField(index, { key: event.target.value })}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} md={5}>
|
||||
<Input
|
||||
addonBefore="名称"
|
||||
value={field.label}
|
||||
onChange={(event) => updateField(index, { label: event.target.value })}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} md={4}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={field.type}
|
||||
onChange={(value) => updateField(index, { type: value, options: [] })}
|
||||
options={[
|
||||
{ value: 'text', label: '文本' },
|
||||
{ value: 'textarea', label: '长文本' },
|
||||
{ value: 'select', label: '下拉' },
|
||||
{ value: 'radio', label: '单选' },
|
||||
{ value: 'number', label: '数字' },
|
||||
{ value: 'date', label: '日期' },
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} md={4}>
|
||||
<Space>
|
||||
<Text type="secondary">必填</Text>
|
||||
<Switch
|
||||
checked={field.required}
|
||||
onChange={(value) => updateField(index, { required: value })}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} md={6}>
|
||||
<Space>
|
||||
<Button
|
||||
disabled={index === 0}
|
||||
icon={<ArrowUpOutlined />}
|
||||
onClick={() => moveField(index, -1)}
|
||||
/>
|
||||
<Button
|
||||
disabled={index === fields.length - 1}
|
||||
icon={<ArrowDownOutlined />}
|
||||
onClick={() => moveField(index, 1)}
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => removeField(index)}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Input
|
||||
addonBefore="占位提示"
|
||||
value={field.placeholder || ''}
|
||||
onChange={(event) =>
|
||||
updateField(index, { placeholder: event.target.value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Input
|
||||
addonBefore="填写说明"
|
||||
value={field.helpText || ''}
|
||||
onChange={(event) =>
|
||||
updateField(index, { helpText: event.target.value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
{(field.type === 'select' || field.type === 'radio') && (
|
||||
<Col span={24}>
|
||||
<Input
|
||||
addonBefore="选项"
|
||||
placeholder="选项1,选项2,选项3"
|
||||
value={(field.options || []).join(',')}
|
||||
onChange={(event) =>
|
||||
updateField(index, {
|
||||
options: event.target.value
|
||||
.split(/[,,]/)
|
||||
.map((option) => option.trim())
|
||||
.filter(Boolean),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Card size="small" title="截图规则">
|
||||
<Form.Item name={['screenshots', 'label']} label="截图标题">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={['screenshots', 'helpText']} label="截图说明">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space size={24} wrap>
|
||||
<Form.Item
|
||||
name={['screenshots', 'enabled']}
|
||||
label="启用"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['screenshots', 'required']}
|
||||
label="必填"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={['screenshots', 'minCount']} label="最少">
|
||||
<Input type="number" style={{ width: 90 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['screenshots', 'maxCount']} label="最多">
|
||||
<Input type="number" style={{ width: 90 }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Card size="small" title="自动发布并核销">
|
||||
<Space direction="vertical" className="full-width">
|
||||
<Space size={24} wrap>
|
||||
<Form.Item
|
||||
name={['autoPublish', 'enabled']}
|
||||
label="开启"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['autoPublish', 'requireVoucherConsume']}
|
||||
label="要求核销"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={['autoPublish', 'retryMax']} label="额外重试次数">
|
||||
<Input type="number" min={0} max={10} style={{ width: 120 }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Text type="secondary">
|
||||
开启后,客户提交完整资料会按当前工单快照自动发布;要求核销时,核销失败不会发布到大厅。
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Space style={{ marginTop: 16 }}>
|
||||
<Button
|
||||
htmlType="button"
|
||||
onClick={() => {
|
||||
form.resetFields()
|
||||
applyProfile(profile)
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button htmlType="submit" loading={saving} type="primary">
|
||||
保存草稿
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
) : (
|
||||
<Card size="small">
|
||||
<Empty description="请选择分类" />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
title="自动发布失败任务"
|
||||
extra={<Tag color={failedJobs.length > 0 ? 'red' : 'default'}>{failedJobs.length}</Tag>}
|
||||
>
|
||||
<PercentTable
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={jobsQuery.isLoading}
|
||||
dataSource={failedJobs}
|
||||
columns={jobColumns}
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeFieldOrder(fields: OrderServiceField[]) {
|
||||
return fields.map((field, index) => ({
|
||||
...field,
|
||||
sortOrder: (index + 1) * 10,
|
||||
}))
|
||||
}
|
||||
|
||||
function findDuplicateKeys(fields: OrderServiceField[]) {
|
||||
const seen = new Set<string>()
|
||||
const duplicates = new Set<string>()
|
||||
for (const field of fields) {
|
||||
const key = String(field.key || '').trim()
|
||||
if (!key) continue
|
||||
if (seen.has(key)) duplicates.add(key)
|
||||
seen.add(key)
|
||||
}
|
||||
return [...duplicates]
|
||||
}
|
||||
Reference in New Issue
Block a user