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