From 54551a1aeb6ffa67656f683ea311ee842c9e9693 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 15 Aug 2026 15:48:25 +0800 Subject: [PATCH] =?UTF-8?q?=E7=89=A9=E5=93=81=E8=A7=84=E5=88=99ui=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/admin/panels/ProductRulesPanel.tsx | 589 ++++++++++-------- apps/frontend/src/styles/main.css | 117 ++-- 2 files changed, 390 insertions(+), 316 deletions(-) diff --git a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx index 928cb45e..5bc089be 100644 --- a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx @@ -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(null) + const [keyword, setKeyword] = useState('') + const [categoryId, setCategoryId] = useState() + 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() - - 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 = [ { - title: '规则', - minWidth: 260, + title: '规则 / 商品', + key: 'rule', + width: '24%', render: (_, row) => (
- - {row.productName || row.skuCode} + + {row.productName || row.skuCode || row.ruleKey} - {row.ruleKey} + + + {row.ruleKey} + + {row.provider ? ( + + {row.provider} + + ) : null} +
), }, { - title: '店铺', - width: 120, + title: '平台 / 店铺', + key: 'platformShop', + width: '13%', render: (_, row) => { const shopId = String(row.shopId || '') - return shopId ? ( - {shopId} - ) : ( - 通配 + return ( +
+ + {row.platform || 'kuaishou'} + + {shopId ? ( + + {shopId} + + ) : ( + 通配店铺 + )} +
) }, }, { title: 'SKU', dataIndex: 'skuCode', - width: 180, - render: (value) => String(value || '-'), + width: '11%', + render: (value) => ( + + {String(value || '-')} + + ), + }, + { + title: '分类', + dataIndex: 'categoryName', + width: '11%', + render: (value) => + value ? ( + {value} + ) : ( + - + ), }, - { title: '分类', width: 140, render: (_, row) => row.categoryName || '-' }, { title: '接单金额', - width: 150, + key: 'reward', + width: '13%', render: (_, row) => (
- {formatMoney(row.rewardAmount)} + + {formatMoney(row.rewardAmount)} + {row.unitPriceFen > 0 ? ( - - 按数量计价:{formatMoney(row.unitPriceFen)} × 数量 + + 按件: {formatMoney(row.unitPriceFen)} ) : null}
@@ -290,36 +314,56 @@ export default function ProductRulesPanel() { }, { title: '押金', - width: 120, - render: (_, row) => formatMoney(row.requiredDepositAmount), + key: 'deposit', + width: '9%', + render: (_, row) => ( + 0 ? undefined : 'secondary'}> + {formatMoney(row.requiredDepositAmount)} + + ), }, { title: '匹配', - width: 130, - render: (_, row) => (row.matchType === 'exact' ? '精确' : '包含'), + dataIndex: 'matchType', + width: '7%', + render: (value) => ( + + {value === 'exact' ? '精确' : '包含'} + + ), }, { title: '状态', - width: 150, + key: 'status', + width: '6%', render: (_, row) => ( - - - {row.enabled ? '启用' : '停用'} - - {row.autoCreate ? 自动创建 : null} - {row.sharing?.enabled ? ( - - 拼单 {formatMoney(row.sharing.unitReward)}/份 - - ) : null} + + toggleRuleEnabled(row, checked)} + /> + + {row.autoCreate ? ( + + 自动 + + ) : null} + {row.sharing?.enabled ? ( + + 拼单 + + ) : null} + ), }, { title: '操作', key: 'actions', - width: 104, - fixed: 'right', + width: '6%', render: (_, row) => ( event.stopPropagation()}> ) : 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} > -
+
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() { > + - + + + + - - - + + + +
+ +
+ + + + + + + + + +
+ {({ getFieldValue }) => getFieldValue('sharingEnabled') ? ( - +
- syncSharingRuleForm({ - sharingTotalQuantity: Number(value) || 0, - }) + syncSharingRuleForm({ sharingTotalQuantity: Number(value) || 0 }) } /> - syncSharingRuleForm({ - sharingUnitReward: Number(value) || 0, - }) + syncSharingRuleForm({ sharingUnitReward: Number(value) || 0 }) } /> - syncSharingRuleForm({ - sharingTotalAmount: Number(value) || 0, - }) + syncSharingRuleForm({ sharingTotalAmount: Number(value) || 0 }) } /> - +
) : null }
+ + {/* 资料字段: 默认展开 4 行全部清晰可见,无内嵌滚动条 */} - + + + {/* Right Table Card - 60% Width, Zero Horizontal Scrollbar */} - 物品规则 - setCategoryId(key === 'all' ? undefined : Number(key))} - items={[ - { key: 'all', label: '全部' }, - ...(categoriesQuery.data?.data.items || []).map((item) => ({ - key: String(item.categoryId), - label: item.name, - })), - ]} - /> -
+ + + 物品规则列表 + + + {filteredRules.length} 条规则 + + } extra={ - + setKeyword(event.target.value)} /> @@ -661,23 +677,49 @@ export default function ProductRulesPanel() { > 刷新 + } bordered={false} > + {/* Category Tabs Bar */} +
+ 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})` : ''}`, + } + }), + ]} + /> +
+ 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: '点击编辑此规则', })} /> @@ -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') diff --git a/apps/frontend/src/styles/main.css b/apps/frontend/src/styles/main.css index 1323e531..4ff93544 100644 --- a/apps/frontend/src/styles/main.css +++ b/apps/frontend/src/styles/main.css @@ -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;