物品规则ui优化

This commit is contained in:
yml2213
2026-08-15 15:48:25 +08:00
parent bdb6bb8200
commit 54551a1aeb
2 changed files with 390 additions and 316 deletions
@@ -1,31 +1,22 @@
import { import {
CheckOutlined,
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
FormOutlined, FormOutlined,
PlusOutlined, PlusOutlined,
ReloadOutlined, ReloadOutlined,
SendOutlined,
SyncOutlined,
TeamOutlined,
WarningOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { import {
App, App,
Button, Button,
Card, Card,
Descriptions,
Drawer,
Form, Form,
Input, Input,
InputNumber, InputNumber,
Modal,
Popconfirm, Popconfirm,
Radio, Radio,
Select, Select,
Space, Space,
Statistic,
Switch, Switch,
Table, Table,
Tabs, Tabs,
@@ -33,66 +24,26 @@ import {
Typography, Typography,
} from 'antd' } from 'antd'
import type { TableColumnsType } from 'antd' import type { TableColumnsType } from 'antd'
import { useEffect, useState } from 'react' import { useState } from 'react'
import { useNavigate } from 'react-router'
import JsonPreview from '@/components/admin/JsonPreview'
import ImageUpload from '@/components/files/ImageUpload'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import PageHeader from '@/components/admin/PageHeader'
import { import {
acceptAdminWorkOrder,
createAdminMockWorkOrder,
creditAdminWorkerWallet,
deleteAdminWorkCategory,
deleteAdminWorkProductRule, deleteAdminWorkProductRule,
deleteAdminWorkerLevel,
fetchAdminWorkerFinanceConfig,
fetchAdminWorkerFinanceRequests,
fetchAdminWorkCategories, fetchAdminWorkCategories,
fetchAdminWorkOrderSharing,
fetchAdminWorkProductRules, fetchAdminWorkProductRules,
fetchAdminWorkerLevels,
fetchAdminWorkerPlatformSummary,
fetchAdminWorkerUsers,
fetchAdminWorkOrders,
markAdminWorkOrderProblem,
publishAdminWorkOrder,
reviewAdminWorkerFinanceRequest,
resolveAdminProblemWorkOrder,
reviewAdminWorkerUser,
saveAdminWorkerFinanceConfig,
saveAdminWorkCategory,
saveAdminWorkProductRule, saveAdminWorkProductRule,
saveAdminWorkerLevel,
submitAdminWorkOrderMaterial,
unpublishAdminWorkOrder,
updateAdminWorkOrderSharing,
} from '@/services/admin' } from '@/services/admin'
import type { import type { CollectField, WorkProductRule } from '@/types/worker-platform'
CollectField,
UploadedFile,
WorkCategory,
WorkOrder,
WorkOrderShare,
WorkProductRule,
WorkerFinanceConfig,
WorkerFinanceRequest,
WorkerLevel,
WorkerUser,
} from '@/types/worker-platform'
import {
ADMIN_DEFAULT_PAGE_SIZE,
buildAdminTablePagination,
} from '@/utils/admin-pagination'
import { formatAdminDateTime } from '@/utils/admin-time'
import { formatMoney } from './shared' import { formatMoney } from './shared'
export default function ProductRulesPanel() { export default function ProductRulesPanel() {
const { message } = App.useApp() const { message } = App.useApp()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [form] = Form.useForm() const [form] = Form.useForm()
const [editingRule, setEditingRule] = useState<WorkProductRule | null>(null) const [editingRule, setEditingRule] = useState<WorkProductRule | null>(null)
const [keyword, setKeyword] = useState('')
const [categoryId, setCategoryId] = useState<number | undefined>()
const rulesQuery = useQuery({ const rulesQuery = useQuery({
queryKey: ['admin-worker-platform-product-rules'], queryKey: ['admin-worker-platform-product-rules'],
queryFn: () => fetchAdminWorkProductRules(), queryFn: () => fetchAdminWorkProductRules(),
@@ -102,6 +53,57 @@ export default function ProductRulesPanel() {
queryFn: () => fetchAdminWorkCategories(), queryFn: () => fetchAdminWorkCategories(),
}) })
const allRules = rulesQuery.data?.data.items || []
const filteredRules = allRules.filter((rule) => {
if (categoryId && Number(rule.categoryId || 0) !== categoryId) return false
const keywordText = keyword.trim().toLowerCase()
if (!keywordText) return true
return [
rule.ruleKey,
rule.productName,
rule.skuCode,
rule.provider,
rule.shopId,
rule.categoryName,
].some((value) => String(value || '').toLowerCase().includes(keywordText))
})
function resetRuleForm() {
setEditingRule(null)
form.resetFields()
}
function editRule(rule: WorkProductRule) {
setEditingRule(rule)
form.setFieldsValue({
ruleKey: rule.ruleKey,
provider: rule.provider,
platform: rule.platform,
shopId: rule.shopId,
skuCode: rule.skuCode,
productName: rule.productName,
matchType: rule.matchType,
categoryId: rule.categoryId || undefined,
pricingMode: rule.unitPriceFen > 0 ? 'unit' : 'fixed',
rewardAmount: rule.rewardAmount / 100,
unitPrice: rule.unitPriceFen / 100,
requiredDepositAmount: rule.requiredDepositAmount / 100,
depositThresholdAmount: rule.depositThresholdAmount / 100,
sharingEnabled: rule.sharing?.enabled || false,
sharingTotalQuantity: rule.sharing?.totalQuantity || 1,
sharingUnitReward: (rule.sharing?.unitReward || 0) / 100,
sharingTotalAmount:
((rule.sharing?.totalQuantity || 1) * (rule.sharing?.unitReward || 0)) / 100,
timeoutMinutes: rule.timeoutMinutes || 0,
timeoutPolicy: rule.timeoutPolicy || 'reopen',
fieldsText: formatRuleFields(rule.requirement?.fields || []),
sortOrder: rule.sortOrder,
enabled: rule.enabled,
autoCreate: rule.autoCreate,
})
}
async function saveRule(values: { async function saveRule(values: {
ruleId?: number ruleId?: number
ruleKey: string ruleKey: string
@@ -151,14 +153,10 @@ export default function ProductRulesPanel() {
} }
} }
function resetRuleForm() { async function toggleRuleEnabled(rule: WorkProductRule, enabled: boolean) {
setEditingRule(null) try {
form.resetFields() await saveAdminWorkProductRule({
} ruleId: rule.ruleId,
function editRule(rule: WorkProductRule) {
setEditingRule(rule)
form.setFieldsValue({
ruleKey: rule.ruleKey, ruleKey: rule.ruleKey,
provider: rule.provider, provider: rule.provider,
platform: rule.platform, platform: rule.platform,
@@ -167,7 +165,6 @@ export default function ProductRulesPanel() {
productName: rule.productName, productName: rule.productName,
matchType: rule.matchType, matchType: rule.matchType,
categoryId: rule.categoryId || undefined, categoryId: rule.categoryId || undefined,
pricingMode: rule.unitPriceFen > 0 ? 'unit' : 'fixed',
rewardAmount: rule.rewardAmount / 100, rewardAmount: rule.rewardAmount / 100,
unitPrice: rule.unitPriceFen / 100, unitPrice: rule.unitPriceFen / 100,
requiredDepositAmount: rule.requiredDepositAmount / 100, requiredDepositAmount: rule.requiredDepositAmount / 100,
@@ -181,9 +178,16 @@ export default function ProductRulesPanel() {
timeoutPolicy: rule.timeoutPolicy || 'reopen', timeoutPolicy: rule.timeoutPolicy || 'reopen',
fieldsText: formatRuleFields(rule.requirement?.fields || []), fieldsText: formatRuleFields(rule.requirement?.fields || []),
sortOrder: rule.sortOrder, sortOrder: rule.sortOrder,
enabled: rule.enabled, enabled,
autoCreate: rule.autoCreate, autoCreate: rule.autoCreate,
}) })
message.success(`规则「${rule.ruleKey}」已${enabled ? '启用' : '停用'}`)
await queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-product-rules'],
})
} catch (error) {
message.error(error instanceof Error ? error.message : '修改状态失败')
}
} }
async function removeRule(rule: WorkProductRule) { async function removeRule(rule: WorkProductRule) {
@@ -201,23 +205,6 @@ export default function ProductRulesPanel() {
} }
} }
const [keyword, setKeyword] = useState('')
const [categoryId, setCategoryId] = useState<number | undefined>()
const filteredRules = (rulesQuery.data?.data.items || []).filter((rule) => {
if (categoryId && Number(rule.categoryId || 0) !== categoryId) return false
const keywordText = keyword.trim().toLowerCase()
if (!keywordText) return true
return [
rule.ruleKey,
rule.productName,
rule.skuCode,
rule.provider,
rule.shopId,
rule.categoryName,
].some((value) => String(value || '').toLowerCase().includes(keywordText))
})
function syncSharingRuleForm(next: { function syncSharingRuleForm(next: {
sharingTotalQuantity?: number sharingTotalQuantity?: number
sharingUnitReward?: number sharingUnitReward?: number
@@ -244,45 +231,82 @@ export default function ProductRulesPanel() {
const columns: TableColumnsType<WorkProductRule> = [ const columns: TableColumnsType<WorkProductRule> = [
{ {
title: '规则', title: '规则 / 商品',
minWidth: 260, key: 'rule',
width: '24%',
render: (_, row) => ( render: (_, row) => (
<div className="cell-stack"> <div className="cell-stack">
<Typography.Text strong> <Typography.Text strong title={row.productName || row.skuCode}>
{row.productName || row.skuCode} {row.productName || row.skuCode || row.ruleKey}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary">{row.ruleKey}</Typography.Text> <Space size={4} wrap style={{ marginTop: 2 }}>
<Tag color="blue" style={{ margin: 0, fontSize: 11, lineHeight: '18px' }}>
{row.ruleKey}
</Tag>
{row.provider ? (
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
{row.provider}
</Typography.Text>
) : null}
</Space>
</div> </div>
), ),
}, },
{ {
title: '店铺', title: '平台 / 店铺',
width: 120, key: 'platformShop',
width: '13%',
render: (_, row) => { render: (_, row) => {
const shopId = String(row.shopId || '') const shopId = String(row.shopId || '')
return shopId ? ( return (
<Tag color="geekblue">{shopId}</Tag> <div className="cell-stack">
<Typography.Text style={{ fontSize: 12 }}>
{row.platform || 'kuaishou'}
</Typography.Text>
{shopId ? (
<Tag color="geekblue" style={{ margin: 0, fontSize: 11, lineHeight: '18px' }}>
{shopId}
</Tag>
) : ( ) : (
<Tag></Tag> <Tag style={{ margin: 0, fontSize: 11, lineHeight: '18px' }}></Tag>
)}
</div>
) )
}, },
}, },
{ {
title: 'SKU', title: 'SKU',
dataIndex: 'skuCode', dataIndex: 'skuCode',
width: 180, width: '11%',
render: (value) => String(value || '-'), render: (value) => (
<Typography.Text code style={{ fontSize: 12 }}>
{String(value || '-')}
</Typography.Text>
),
},
{
title: '分类',
dataIndex: 'categoryName',
width: '11%',
render: (value) =>
value ? (
<Tag color="cyan" style={{ margin: 0 }}>{value}</Tag>
) : (
<Typography.Text type="secondary">-</Typography.Text>
),
}, },
{ title: '分类', width: 140, render: (_, row) => row.categoryName || '-' },
{ {
title: '接单金额', title: '接单金额',
width: 150, key: 'reward',
width: '13%',
render: (_, row) => ( render: (_, row) => (
<div className="cell-stack"> <div className="cell-stack">
<span>{formatMoney(row.rewardAmount)}</span> <Typography.Text strong style={{ color: '#3f8600', fontSize: 13 }}>
{formatMoney(row.rewardAmount)}
</Typography.Text>
{row.unitPriceFen > 0 ? ( {row.unitPriceFen > 0 ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}> <Typography.Text type="secondary" style={{ fontSize: 11 }}>
{formatMoney(row.unitPriceFen)} × : {formatMoney(row.unitPriceFen)}
</Typography.Text> </Typography.Text>
) : null} ) : null}
</div> </div>
@@ -290,36 +314,56 @@ export default function ProductRulesPanel() {
}, },
{ {
title: '押金', title: '押金',
width: 120, key: 'deposit',
render: (_, row) => formatMoney(row.requiredDepositAmount), width: '9%',
render: (_, row) => (
<Typography.Text type={row.requiredDepositAmount > 0 ? undefined : 'secondary'}>
{formatMoney(row.requiredDepositAmount)}
</Typography.Text>
),
}, },
{ {
title: '匹配', title: '匹配',
width: 130, dataIndex: 'matchType',
render: (_, row) => (row.matchType === 'exact' ? '精确' : '包含'), width: '7%',
render: (value) => (
<Tag color={value === 'exact' ? 'blue' : 'orange'} style={{ margin: 0 }}>
{value === 'exact' ? '精确' : '包含'}
</Tag>
),
}, },
{ {
title: '状态', title: '状态',
width: 150, key: 'status',
width: '6%',
render: (_, row) => ( render: (_, row) => (
<Space> <Space direction="vertical" size={2}>
<Tag color={row.enabled ? 'green' : 'default'}> <Switch
{row.enabled ? '启用' : '停用'} size="small"
</Tag> checked={row.enabled}
{row.autoCreate ? <Tag color="blue"></Tag> : null} checkedChildren="启用"
{row.sharing?.enabled ? ( unCheckedChildren="停用"
<Tag color="purple"> onChange={(checked) => toggleRuleEnabled(row, checked)}
{formatMoney(row.sharing.unitReward)}/ />
<Space size={2} wrap>
{row.autoCreate ? (
<Tag color="blue" style={{ fontSize: 10, margin: 0, padding: '0 4px' }}>
</Tag> </Tag>
) : null} ) : null}
{row.sharing?.enabled ? (
<Tag color="purple" style={{ fontSize: 10, margin: 0, padding: '0 4px' }}>
</Tag>
) : null}
</Space>
</Space> </Space>
), ),
}, },
{ {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
width: 104, width: '6%',
fixed: 'right',
render: (_, row) => ( render: (_, row) => (
<Space size={0} onClick={(event) => event.stopPropagation()}> <Space size={0} onClick={(event) => event.stopPropagation()}>
<Button <Button
@@ -331,7 +375,7 @@ export default function ProductRulesPanel() {
/> />
<Popconfirm <Popconfirm
title={`删除规则“${row.productName || row.ruleKey}”?`} title={`删除规则“${row.productName || row.ruleKey}”?`}
description="删除后不会影响已创建工单。" description="删除后不会影响已创建工单。"
okText="删除" okText="删除"
cancelText="取消" cancelText="取消"
okButtonProps={{ danger: true }} okButtonProps={{ danger: true }}
@@ -353,13 +397,19 @@ export default function ProductRulesPanel() {
return ( return (
<section className="platform-panel-stack"> <section className="platform-panel-stack">
<div className="product-rules-layout"> <div className="product-rules-layout">
{/* Left Form Card - 30% Width Proportional Split */}
<Card <Card
className="product-rules-form-card" className="product-rules-form-card"
title={editingRule ? `编辑物品规则 · ${editingRule.ruleKey}` : '新建物品规则'} title={
<Space size={6}>
<FormOutlined style={{ color: '#1677ff' }} />
<span>{editingRule ? `编辑规则 · ${editingRule.ruleKey}` : '新建物品规则'}</span>
</Space>
}
extra={ extra={
<Space> <Space size={4}>
{editingRule ? ( {editingRule ? (
<Button type="link" onClick={resetRuleForm}> <Button type="link" size="small" onClick={resetRuleForm}>
</Button> </Button>
) : null} ) : null}
@@ -378,7 +428,7 @@ export default function ProductRulesPanel() {
id="product-rule-form" id="product-rule-form"
form={form} form={form}
layout="vertical" layout="vertical"
className="worker-rule-form" className="worker-rule-form-30"
initialValues={{ initialValues={{
platform: 'kuaishou', platform: 'kuaishou',
matchType: 'contains', matchType: 'contains',
@@ -396,7 +446,7 @@ export default function ProductRulesPanel() {
}} }}
onFinish={saveRule} onFinish={saveRule}
> >
<div className="worker-rule-grid"> <div className="rule-form-grid-2col">
<Form.Item <Form.Item
label="规则标识" label="规则标识"
name="ruleKey" name="ruleKey"
@@ -407,11 +457,9 @@ export default function ProductRulesPanel() {
const key = String(value || '').trim() const key = String(value || '').trim()
if ( if (
!editingRule && !editingRule &&
(rulesQuery.data?.data.items || []).some( allRules.some((rule) => rule.ruleKey === key)
(rule) => rule.ruleKey === key,
)
) { ) {
return Promise.reject(new Error('规则标识已存在,请更换')) return Promise.reject(new Error('标识已存在'))
} }
return Promise.resolve() return Promise.resolve()
}, },
@@ -420,21 +468,23 @@ export default function ProductRulesPanel() {
> >
<Input placeholder="skin-training" disabled={Boolean(editingRule)} /> <Input placeholder="skin-training" disabled={Boolean(editingRule)} />
</Form.Item> </Form.Item>
<Form.Item label="来源" name="provider"> <Form.Item label="来源" name="provider">
<Input placeholder="留空通配,如 91kaquan" /> <Input placeholder="如 91kaquan" />
</Form.Item> </Form.Item>
<Form.Item label="平台" name="platform"> <Form.Item label="平台" name="platform">
<Input placeholder="kuaishou" /> <Input placeholder="kuaishou" />
</Form.Item> </Form.Item>
<Form.Item label="店铺" name="shopId"> <Form.Item label="店铺" name="shopId">
<Input placeholder="留空通配" /> <Input placeholder="留空通配" />
</Form.Item> </Form.Item>
<Form.Item label="SKU" name="skuCode"> <Form.Item label="SKU" name="skuCode">
<Input placeholder="优先精确匹配" /> <Input placeholder="优先精确匹配" />
</Form.Item> </Form.Item>
<Form.Item label="商品名" name="productName">
<Input placeholder="用于包含/精确匹配" />
</Form.Item>
<Form.Item label="匹配方式" name="matchType"> <Form.Item label="匹配方式" name="matchType">
<Select <Select
options={[ options={[
@@ -443,31 +493,35 @@ export default function ProductRulesPanel() {
]} ]}
/> />
</Form.Item> </Form.Item>
<Form.Item label="分类" name="categoryId">
<Form.Item label="商品名称" name="productName" className="span-2">
<Input placeholder="用于包含/精确匹配商品" />
</Form.Item>
<Form.Item label="所属分类" name="categoryId">
<Select <Select
allowClear allowClear
placeholder="选择分类"
loading={categoriesQuery.isLoading} loading={categoriesQuery.isLoading}
options={(categoriesQuery.data?.data.items || []).map( options={(categoriesQuery.data?.data.items || []).map((item) => ({
(item) => ({
value: item.categoryId, value: item.categoryId,
label: item.name, label: item.name,
}), }))}
)}
/> />
</Form.Item> </Form.Item>
<Form.Item
label="计价方式" <Form.Item label="计价方式" name="pricingMode">
name="pricingMode"
tooltip="按数量计价时,接单总价 = 单价 × 商品名称文字中的规格数量(如「指挥官秘钥15个」→ 15)"
>
<Radio.Group <Radio.Group
optionType="button"
buttonStyle="solid"
className="full-width" className="full-width"
options={[ options={[
{ value: 'fixed', label: '固定金额' }, { value: 'fixed', label: '固定金额' },
{ value: 'unit', label: '按数量 ×价' }, { value: 'unit', label: '按件计价' },
]} ]}
/> />
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
{({ getFieldValue }) => {({ getFieldValue }) =>
getFieldValue('pricingMode') === 'unit' ? ( getFieldValue('pricingMode') === 'unit' ? (
@@ -475,182 +529,144 @@ export default function ProductRulesPanel() {
label="单价" label="单价"
name="unitPrice" name="unitPrice"
rules={[{ required: true, message: '请填写单价' }]} rules={[{ required: true, message: '请填写单价' }]}
extra="接单总价 = 单价 × 商品名称文字中的数量"
> >
<InputNumber <InputNumber min={0.01} step={1} addonAfter="元" className="full-width" />
min={0.01}
step={1}
addonAfter="元"
className="full-width"
/>
</Form.Item> </Form.Item>
) : ( ) : (
<Form.Item <Form.Item label="接单金额" name="rewardAmount">
label="接单金额" <InputNumber min={0} step={1} addonAfter="元" className="full-width" />
name="rewardAmount"
extra="可填 0,但接单金额为 0 的工单不能发布到大厅"
>
<InputNumber
min={0}
step={1}
addonAfter="元"
className="full-width"
/>
</Form.Item> </Form.Item>
) )
} }
</Form.Item> </Form.Item>
<Form.Item label="押金阈值" name="depositThresholdAmount">
<InputNumber
min={0}
step={10}
addonAfter="元"
className="full-width"
/>
</Form.Item>
<Form.Item label="固定押金" name="requiredDepositAmount">
<InputNumber
min={0}
step={10}
addonAfter="元"
className="full-width"
/>
</Form.Item>
<Form.Item label="排序" name="sortOrder"> <Form.Item label="排序" name="sortOrder">
<InputNumber min={0} max={9999} className="full-width" /> <InputNumber min={0} max={9999} className="full-width" />
</Form.Item> </Form.Item>
<Form.Item label="启用" name="enabled" valuePropName="checked">
<Switch /> <Form.Item label="押金阈值" name="depositThresholdAmount">
<InputNumber min={0} step={10} addonAfter="元" className="full-width" />
</Form.Item> </Form.Item>
<Form.Item
label="自动创建" <Form.Item label="固定押金" name="requiredDepositAmount">
name="autoCreate" <InputNumber min={0} step={10} addonAfter="元" className="full-width" />
valuePropName="checked"
>
<Switch />
</Form.Item> </Form.Item>
<Form.Item
label="启用拼单" <Form.Item label="任务时限" name="timeoutMinutes">
name="sharingEnabled" <InputNumber min={0} step={5} addonAfter="分钟" className="full-width" />
valuePropName="checked"
tooltip="开启后按下方总数量与单价生成拼单工单"
>
<Switch />
</Form.Item> </Form.Item>
<Form.Item
label="默认任务时限" <Form.Item label="超时策略" name="timeoutPolicy">
name="timeoutMinutes"
tooltip="自动创建的工单继承该时限,打手抢单后开始计时;0 表示不限时"
>
<InputNumber min={0} step={5} addonAfter="分钟" />
</Form.Item>
<Form.Item
label="默认超时策略"
name="timeoutPolicy"
tooltip="reopen:释放押金退回大厅;cancel_release:取消退押金;cancel_deduct:取消扣押金"
>
<Select <Select
options={[ options={[
{ value: 'reopen', label: '释放押金退回大厅' }, { value: 'reopen', label: '释放押金退回大厅' },
{ value: 'cancel_release', label: '取消订单并退还押金' }, { value: 'cancel_release', label: '取消订单退押金' },
{ value: 'cancel_deduct', label: '取消订单并扣除押金' }, { value: 'cancel_deduct', label: '取消订单押金' },
]} ]}
/> />
</Form.Item> </Form.Item>
</div> </div>
<div className="form-switches-strip">
<Form.Item label="启用" name="enabled" valuePropName="checked">
<Switch size="small" />
</Form.Item>
<Form.Item label="自动创建" name="autoCreate" valuePropName="checked">
<Switch size="small" />
</Form.Item>
<Form.Item label="启用拼单" name="sharingEnabled" valuePropName="checked">
<Switch size="small" />
</Form.Item>
</div>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
{({ getFieldValue }) => {({ getFieldValue }) =>
getFieldValue('sharingEnabled') ? ( getFieldValue('sharingEnabled') ? (
<Space size={16} wrap style={{ marginBottom: 16 }}> <div className="sharing-box-inline">
<Form.Item <Form.Item
label="拼单数量" label="拼单数量"
name="sharingTotalQuantity" name="sharingTotalQuantity"
rules={[{ required: true, message: '请填写总数量' }]} rules={[{ required: true, message: '必填' }]}
> >
<InputNumber <InputNumber
min={1} min={1}
max={100000} max={100000}
addonAfter="份" addonAfter="份"
className="full-width"
onChange={(value) => onChange={(value) =>
syncSharingRuleForm({ syncSharingRuleForm({ sharingTotalQuantity: Number(value) || 0 })
sharingTotalQuantity: Number(value) || 0,
})
} }
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="拼单单价" label="拼单单价"
name="sharingUnitReward" name="sharingUnitReward"
rules={[{ required: true, message: '请填写单价' }]} rules={[{ required: true, message: '必填' }]}
tooltip="每个数量份对应的报酬,修改后总价自动重算"
> >
<InputNumber <InputNumber
min={0.01} min={0.01}
step={1} step={1}
addonAfter="元" addonAfter="元"
className="full-width"
onChange={(value) => onChange={(value) =>
syncSharingRuleForm({ syncSharingRuleForm({ sharingUnitReward: Number(value) || 0 })
sharingUnitReward: Number(value) || 0,
})
} }
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="拼单总价" label="拼单总价"
name="sharingTotalAmount" name="sharingTotalAmount"
rules={[{ required: true, message: '请填写总价' }]} rules={[{ required: true, message: '必填' }]}
tooltip="总价 = 单价 × 数量,修改总价会自动反推数量"
> >
<InputNumber <InputNumber
min={0.01} min={0.01}
step={1} step={1}
addonAfter="元" addonAfter="元"
className="full-width"
onChange={(value) => onChange={(value) =>
syncSharingRuleForm({ syncSharingRuleForm({ sharingTotalAmount: Number(value) || 0 })
sharingTotalAmount: Number(value) || 0,
})
} }
/> />
</Form.Item> </Form.Item>
</Space> </div>
) : null ) : null
} }
</Form.Item> </Form.Item>
{/* 资料字段: 默认展开 4 行全部清晰可见,无内嵌滚动条 */}
<Form.Item <Form.Item
label="资料字段" label="资料字段"
name="fieldsText" name="fieldsText"
extra="每行一个字段:key:名称,下拉项用 # 分隔(如 system:系统#安卓,苹果)" style={{ marginBottom: 0 }}
extra="格式:key:名称#选项1,选项2"
> >
<Input.TextArea rows={2} /> <Input.TextArea
rows={4}
placeholder="gameId:游戏编号&#10;gameNickname:游戏昵称&#10;system:系统#安卓,苹果&#10;serverZone:区服#QQ区,微信区"
/>
</Form.Item> </Form.Item>
</Form> </Form>
</Card> </Card>
{/* Right Table Card - 60% Width, Zero Horizontal Scrollbar */}
<Card <Card
className="product-rules-list-card" className="product-rules-list-card"
title={ title={
<div className="product-rules-list-heading"> <Space align="center" size={10}>
<span></span> <Typography.Text strong style={{ fontSize: 15 }}>
<Tabs
className="product-rules-category-tabs" </Typography.Text>
size="small" <Tag color="blue" style={{ margin: 0 }}>
activeKey={categoryId ? String(categoryId) : 'all'} {filteredRules.length}
onChange={(key) => setCategoryId(key === 'all' ? undefined : Number(key))} </Tag>
items={[ </Space>
{ key: 'all', label: '全部' },
...(categoriesQuery.data?.data.items || []).map((item) => ({
key: String(item.categoryId),
label: item.name,
})),
]}
/>
</div>
} }
extra={ extra={
<Space wrap> <Space size={8} wrap>
<Input.Search <Input.Search
allowClear allowClear
placeholder="搜索规则标识/商品/SKU/店铺" placeholder="搜索规则/商品/SKU/店铺"
style={{ width: 240 }} style={{ width: 200 }}
value={keyword} value={keyword}
onChange={(event) => setKeyword(event.target.value)} onChange={(event) => setKeyword(event.target.value)}
/> />
@@ -661,23 +677,49 @@ export default function ProductRulesPanel() {
> >
</Button> </Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={resetRuleForm}
>
</Button>
</Space> </Space>
} }
bordered={false} bordered={false}
> >
{/* Category Tabs Bar */}
<div className="product-rules-category-tabs-bar">
<Tabs
activeKey={categoryId ? String(categoryId) : 'all'}
onChange={(key) => setCategoryId(key === 'all' ? undefined : Number(key))}
items={[
{ key: 'all', label: `全部 (${allRules.length})` },
...(categoriesQuery.data?.data.items || []).map((item) => {
const count = allRules.filter(
(r) => Number(r.categoryId || 0) === item.categoryId,
).length
return {
key: String(item.categoryId),
label: `${item.name}${count ? ` (${count})` : ''}`,
}
}),
]}
/>
</div>
<Table<WorkProductRule> <Table<WorkProductRule>
rowKey="ruleId" rowKey="ruleId"
loading={rulesQuery.isLoading} loading={rulesQuery.isLoading}
dataSource={filteredRules} dataSource={filteredRules}
columns={columns} columns={columns}
pagination={false} pagination={false}
scroll={{ x: 1000, y: 'calc(100vh - 300px)' }}
rowClassName={(row) => rowClassName={(row) =>
editingRule?.ruleId === row.ruleId ? 'product-rule-row-active' : '' editingRule?.ruleId === row.ruleId ? 'product-rule-row-active' : ''
} }
onRow={(row) => ({ onRow={(row) => ({
onClick: () => editRule(row), onClick: () => editRule(row),
title: '点击快速编辑', title: '点击编辑此规则',
})} })}
/> />
</Card> </Card>
@@ -689,7 +731,8 @@ export default function ProductRulesPanel() {
function formatRuleFields(fields: CollectField[]) { function formatRuleFields(fields: CollectField[]) {
return fields return fields
.map((field) => { .map((field) => {
const options = Array.isArray(field.options) && field.options.length > 0 const options =
Array.isArray(field.options) && field.options.length > 0
? `#${field.options.join(',')}` ? `#${field.options.join(',')}`
: '' : ''
return `${field.key}:${field.label}${options}` return `${field.key}:${field.label}${options}`
+74 -43
View File
@@ -2496,89 +2496,120 @@ select {
width: 100%; width: 100%;
} }
/* 物品规则:左侧编辑表单 + 右侧可滚动列表 */ /* 物品规则:3:7 比例响应式双栏 (左侧 30% + 右侧 70%) */
.product-rules-layout { .product-rules-layout {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: flex-start; align-items: flex-start;
width: 100%;
} }
.product-rules-form-card { .product-rules-form-card {
width: 560px; flex: 0 0 calc(30% - 8px);
flex-shrink: 0; width: calc(30% - 8px);
min-width: 360px;
border-radius: 8px;
} }
/* 左侧表单一屏展示,不出现滚动条 */ /* 左侧表单彻底消除滚动条,紧凑舒适排版 */
.product-rules-form-card .ant-card-body { .product-rules-form-card .ant-card-body {
padding: 12px 14px;
overflow: visible; overflow: visible;
} }
.product-rules-form-card .worker-rule-form .ant-form-item { .worker-rule-form-30 .rule-form-grid-2col {
margin-bottom: 14px; display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 10px;
} }
.product-rules-form-card .worker-rule-form .ant-form-item-label { .worker-rule-form-30 .rule-form-grid-2col .span-2 {
padding-bottom: 2px; grid-column: span 2;
} }
.product-rules-form-card .worker-rule-form .ant-input-textarea textarea { .worker-rule-form-30 .ant-form-item {
min-height: 56px !important; margin-bottom: 7px;
}
.worker-rule-form-30 .ant-form-item-label {
padding-bottom: 1px;
}
.worker-rule-form-30 .ant-form-item-label > label {
font-size: 12px;
color: #374151;
font-weight: 500;
}
.worker-rule-form-30 .form-switches-strip {
display: flex;
align-items: center;
gap: 16px;
background: #f9fafb;
padding: 5px 10px;
border-radius: 6px;
margin-top: 3px;
margin-bottom: 7px;
}
.worker-rule-form-30 .form-switches-strip .ant-form-item {
margin-bottom: 0;
display: flex;
align-items: center;
gap: 6px;
}
.worker-rule-form-30 .sharing-box-inline {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
background: #f8fafc;
border: 1px dashed #cbd5e1;
padding: 6px 8px;
border-radius: 6px;
margin-bottom: 7px;
}
.worker-rule-form-30 .sharing-box-inline .ant-form-item {
margin-bottom: 0;
} }
.product-rules-list-card { .product-rules-list-card {
flex: 1; flex: 0 0 calc(70% - 8px);
width: calc(70% - 8px);
min-width: 0; min-width: 0;
} border-radius: 8px;
.product-rules-list-heading {
display: flex;
align-items: center;
gap: 20px;
min-width: 0;
}
.product-rules-category-tabs {
min-width: 0;
transform: translateY(-6px);
}
.product-rules-category-tabs .ant-tabs-nav {
margin: 0;
}
.product-rules-category-tabs .ant-tabs-content-holder {
display: none;
} }
.product-rules-list-card .ant-card-body { .product-rules-list-card .ant-card-body {
max-height: calc(100vh - 150px); padding: 12px 16px;
overflow-y: auto;
} }
.product-rules-list-card .ant-table-wrapper { .product-rules-category-tabs-bar {
max-height: calc(100vh - 230px); margin-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.product-rules-category-tabs-bar .ant-tabs-nav {
margin: 0;
} }
.product-rule-row-active > td { .product-rule-row-active > td {
background: #e6f4ff !important; background: #e6f4ff !important;
} }
@media (max-width: 1024px) { @media (max-width: 1080px) {
.product-rules-layout { .product-rules-layout {
flex-direction: column; flex-direction: column;
} }
.product-rules-form-card { .product-rules-form-card,
.product-rules-list-card {
flex: 1 1 100%;
width: 100%; width: 100%;
} }
} }
.worker-rule-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0 14px;
}
@media (max-width: 720px) { @media (max-width: 720px) {
.worker-page-head { .worker-page-head {
align-items: stretch; align-items: stretch;