From bb692754af6beab3c97e5b797ed1af19253c5171 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 4 Nov 2026 11:48:30 +0800 Subject: [PATCH] =?UTF-8?q?style(order-service):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B5=E5=88=86=E7=B1=BB=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E4=B8=8E=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/admin/panels/OrderServicePanel.tsx | 801 ++++++++++-------- apps/frontend/src/styles/admin.css | 386 +++++++++ 2 files changed, 841 insertions(+), 346 deletions(-) diff --git a/apps/frontend/src/pages/admin/panels/OrderServicePanel.tsx b/apps/frontend/src/pages/admin/panels/OrderServicePanel.tsx index 93e44b6d..111c896e 100644 --- a/apps/frontend/src/pages/admin/panels/OrderServicePanel.tsx +++ b/apps/frontend/src/pages/admin/panels/OrderServicePanel.tsx @@ -4,16 +4,21 @@ import { DeleteOutlined, PlusOutlined, ReloadOutlined, + SaveOutlined, + SearchOutlined, } from '@ant-design/icons' import { Alert, App, + Badge, Button, Card, Col, + Collapse, Empty, Form, Input, + InputNumber, Row, Select, Space, @@ -88,6 +93,7 @@ export default function OrderServicePanel() { const queryClient = useQueryClient() const [form] = Form.useForm() const [selectedCategoryId, setSelectedCategoryId] = useState(null) + const [categoryKeyword, setCategoryKeyword] = useState('') const [fields, setFields] = useState([]) const [guideImage, setGuideImage] = useState | null>(null) const [saving, setSaving] = useState(false) @@ -106,8 +112,27 @@ export default function OrderServicePanel() { const categories = profilesQuery.data?.data.categories || [] const profiles = profilesQuery.data?.data.profiles || [] const failedJobs = jobsQuery.data?.data.items || [] + const profileByCategoryId = useMemo( + () => new Map(profiles.map((item) => [item.categoryId, item])), + [profiles], + ) + const normalizedKeyword = categoryKeyword.trim().toLowerCase() + const filteredCategories = useMemo( + () => + categories.filter((item) => { + if (!normalizedKeyword) return true + return ( + item.name.toLowerCase().includes(normalizedKeyword) || + item.categoryKey.toLowerCase().includes(normalizedKeyword) + ) + }), + [categories, normalizedKeyword], + ) + const configuredCount = categories.filter((item) => + profileByCategoryId.has(item.categoryId), + ).length const selectedCategory = categories.find((item) => item.categoryId === selectedCategoryId) || null - const profile = profiles.find((item) => item.categoryId === selectedCategoryId) || null + const profile = (selectedCategoryId ? profileByCategoryId.get(selectedCategoryId) : null) || null useEffect(() => { if (selectedCategoryId !== null || categories.length === 0) return @@ -255,59 +280,10 @@ export default function OrderServicePanel() { } } - const categoryColumns = useMemo( - () => [ - { title: '分类', dataIndex: 'name' }, - { - title: '状态', - dataIndex: 'status', - width: 90, - render: (value: string) => ( - - {value === 'active' ? '启用' : '停用'} - - ), - }, - { - title: '订单服务', - width: 180, - render: (_: unknown, row: { categoryId: number }) => { - const rowProfile = profiles.find((item) => item.categoryId === row.categoryId) - if (!rowProfile) return 未配置 - if (rowProfile.status === 'draft') { - return ( - - {rowProfile.currentVersionNo - ? `有未发布修改(线上 v${rowProfile.currentVersionNo})` - : '草稿'} - - ) - } - return ( - - {rowProfile.status === 'published' - ? `已发布 v${rowProfile.currentVersionNo || 1}` - : rowProfile.status} - - ) - }, - }, - { - title: '操作', - width: 90, - render: (_: unknown, row: { categoryId: number }) => ( - - ), - }, - ], - [profiles], - ) - const jobColumns = useMemo( () => [ { + key: 'order', title: '订单', render: (_: unknown, row: AdminOrderServiceAutoPublishJob) => ( @@ -317,24 +293,28 @@ export default function OrderServicePanel() { ), }, { + key: 'attempts', title: '重试', width: 90, render: (_: unknown, row: AdminOrderServiceAutoPublishJob) => `${row.attemptCount}/${row.maxAttempts}`, }, { + key: 'error', title: '失败原因', render: (_: unknown, row: AdminOrderServiceAutoPublishJob) => ( {row.lastError || '未知错误'} ), }, { + key: 'updatedAt', title: '更新时间', width: 180, render: (_: unknown, row: AdminOrderServiceAutoPublishJob) => row.updatedAt ? formatAdminDateTime(row.updatedAt) : '-', }, { + key: 'actions', title: '操作', width: 100, render: (_: unknown, row: AdminOrderServiceAutoPublishJob) => ( @@ -352,10 +332,15 @@ export default function OrderServicePanel() { ) return ( -
+
+ 订单服务配置 + {configuredCount} 个分类已配置 + + } extra={ } > - - - row.categoryId === selectedCategoryId ? 'ant-table-row-selected' : '' - } - /> - - - {selectedCategory ? ( - - - {profile?.status === 'published' - ? `线上 v${profile.currentVersionNo || 1}` - : profile?.currentVersionNo - ? `线上 v${profile.currentVersionNo},当前有草稿` - : '未发布'} - - - {profile ? ( - - ) : null} - - } - > -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setGuideImage(files[0] || null)} - uploader={(file, scene) => uploadFile(file, scene, 'admin')} +
+ - } onClick={addField} size="small"> - 添加字段 - - } - style={{ marginBottom: 16 }} - > - {fields.length === 0 ? ( - - ) : ( - - {fields.map((field, index) => ( - - - - updateField(index, { key: event.target.value })} - /> - - - updateField(index, { label: event.target.value })} - /> - - - - updateField(index, { placeholder: event.target.value }) - } - /> - - - - updateField(index, { helpText: event.target.value }) - } - /> - - {(field.type === 'select' || field.type === 'radio') && ( - - - updateField(index, { - options: event.target.value - .split(/[,,]/) - .map((option) => option.trim()) - .filter(Boolean), - }) - } - /> - - )} - - - ))} - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ {selectedCategory ? ( + <> +
+
+ + {selectedCategory.name} + + {selectedCategory.status === 'active' ? '启用' : '停用'} + + + {profile?.status === 'published' + ? `线上 v${profile.currentVersionNo || 1}` + : profile?.currentVersionNo + ? `线上 v${profile.currentVersionNo} · 有草稿` + : '尚未发布'} + - - 开启后,客户提交完整资料会按当前工单快照自动发布;要求核销时,核销失败不会发布到大厅。 - + 分类标识:{selectedCategory.categoryKey} +
+ + + {profile ? ( + + ) : null} - - - +
- - - - - - - ) : ( - - - - )} + - 0 ? 'red' : 'default'}>{failedJobs.length}} - > - +
+
+
+
+ 基础信息 + 客户打开 `/o` 后看到的标题与提示。 +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ 用户引导图 + 首版一张,展示在字段上方 + setGuideImage(files[0] || null)} + uploader={(file, scene) => uploadFile(file, scene, 'admin')} + /> +
+
+
+ +
+
+
+ 资料字段 + 客户必须填写或选填的资料,可调整顺序。 +
+ +
+ + {fields.length === 0 ? ( + + ) : ( +
+ {fields.map((field, index) => ( +
+
+ + +
+
+ ))} +
+ )} +
+ + + +
+
+
+ 截图规则 + 控制客户上传买家主页截图的限制。 +
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+ + +
+
+
+ 自动发布并核销 + 客户提交后按工单快照自动处理。 +
+
+
+ + + + + + + + + +
+ + 要求核销时,核销失败不会发布到大厅,会进入失败任务。 + +
+ +
+ +
+ 保存只更新草稿,发布后才会影响新工单。 + + + + +
+
+ + ) : ( + + )} +
+
+ + + 自动发布失败任务 + 0 ? 'red' : 'default'} + /> + + ), + children: + failedJobs.length > 0 ? ( + + ) : ( + + ), + }, + ]} + />
) } @@ -687,3 +782,17 @@ function findDuplicateKeys(fields: OrderServiceField[]) { } return [...duplicates] } + +function resolveProfileStatus(profile: AdminOrderServiceProfile | undefined) { + if (!profile) return { label: '未配置', tone: 'muted' } + if (profile.status === 'published') { + return { label: `v${profile.currentVersionNo || 1}`, tone: 'success' } + } + if (profile.status === 'draft') { + return { + label: profile.currentVersionNo ? `草稿 · 线上 v${profile.currentVersionNo}` : '草稿', + tone: 'warning', + } + } + return { label: '已归档', tone: 'muted' } +} diff --git a/apps/frontend/src/styles/admin.css b/apps/frontend/src/styles/admin.css index 3585f6e2..1e68b8a4 100644 --- a/apps/frontend/src/styles/admin.css +++ b/apps/frontend/src/styles/admin.css @@ -1645,3 +1645,389 @@ .admin-share-row-cancelled:hover > td { background: #f5f5f5 !important; } + +/* 订单服务配置:分类导航 + 配置编辑器 */ +.order-service-shell { + overflow: hidden; + border-radius: 14px; +} + +.order-service-shell > .ant-card-head { + min-height: 54px; + background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%); +} + +.order-service-shell > .ant-card-body { + padding: 0; +} + +.order-service-layout { + display: grid; + grid-template-columns: 270px minmax(0, 1fr); + align-items: start; +} + +.order-service-sidebar { + position: sticky; + top: 12px; + display: flex; + flex-direction: column; + min-height: calc(100vh - 190px); + max-height: calc(100vh - 180px); + background: #f7f9fc; + border-right: 1px solid #edf0f5; +} + +.order-service-sidebar-head { + display: grid; + gap: 10px; + padding: 14px 14px 10px; + border-bottom: 1px solid #edf0f5; +} + +.order-service-category-list { + display: flex; + flex-direction: column; + gap: 4px; + min-height: 0; + padding: 8px; + overflow: auto; +} + +.order-service-category-item { + display: grid; + gap: 4px; + width: 100%; + padding: 9px 10px; + color: #25324a; + text-align: left; + background: transparent; + border: 1px solid transparent; + border-radius: 9px; + cursor: pointer; + transition: + background 0.16s ease, + border-color 0.16s ease, + box-shadow 0.16s ease; +} + +.order-service-category-item:hover { + background: #ffffff; + border-color: #e5eaf2; +} + +.order-service-category-item.is-active { + background: #ffffff; + border-color: #91caff; + box-shadow: 0 2px 8px rgba(22, 104, 220, 0.08); +} + +.order-service-category-main { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.order-service-category-name { + min-width: 0; + overflow: hidden; + font-size: 13px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.order-service-category-meta { + overflow: hidden; + color: #8a94a6; + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.order-service-status-pill { + flex: none; + padding: 1px 6px; + font-size: 10px; + line-height: 17px; + border: 1px solid #d9dde5; + border-radius: 999px; +} + +.order-service-status-pill.is-success { + color: #237804; + background: #f6ffed; + border-color: #b7eb8f; +} + +.order-service-status-pill.is-warning { + color: #ad6800; + background: #fffbe6; + border-color: #ffe58f; +} + +.order-service-status-pill.is-muted { + color: #8c8c8c; + background: #fafafa; +} + +.order-service-editor { + min-width: 0; + padding: 20px 22px 0; +} + +.order-service-editor-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding-bottom: 14px; +} + +.order-service-editor-head .ant-typography { + margin-bottom: 0; +} + +.order-service-tip { + margin-bottom: 14px; +} + +.order-service-section { + padding: 16px; + margin-bottom: 14px; + background: #ffffff; + border: 1px solid #edf0f5; + border-radius: 12px; +} + +.order-service-section-fill { + height: 100%; + margin-bottom: 0; +} + +.order-service-section-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; +} + +.order-service-section-head > div:first-child { + display: grid; + gap: 2px; +} + +.order-service-basic-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 230px; + gap: 18px; +} + +.order-service-basic-grid .ant-form-item { + margin-bottom: 12px; +} + +.order-service-guide { + display: grid; + align-content: start; + gap: 4px; + padding: 12px; + background: #f8fafc; + border: 1px dashed #d9e1ec; + border-radius: 10px; +} + +.order-service-guide .ant-upload-wrapper { + margin-top: 6px; +} + +.order-service-field-list { + display: grid; + gap: 8px; +} + +.order-service-field-row { + padding: 12px; + background: #fbfcfe; + border: 1px solid #edf0f5; + border-radius: 10px; +} + +.order-service-field-main { + display: grid; + grid-template-columns: minmax(120px, 0.9fr) minmax(150px, 1fr) 120px 70px 104px; + gap: 10px; + align-items: end; +} + +.order-service-field-main label, +.order-service-field-required { + display: grid; + gap: 4px; + min-width: 0; +} + +.order-service-field-required { + justify-items: start; +} + +.order-service-field-required .ant-switch { + margin-top: 5px; +} + +.order-service-field-actions { + display: flex; + gap: 4px; + align-items: center; + justify-content: flex-end; + min-height: 32px; +} + +.order-service-field-extra { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + margin-top: 8px; +} + +.order-service-field-extra > :last-child:nth-child(3) { + grid-column: 1 / -1; +} + +.order-service-switch-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.order-service-switch-grid.is-auto-publish { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.order-service-switch-grid .ant-form-item { + margin-bottom: 10px; +} + +.order-service-inline-tip { + display: block; + margin-top: 2px; + font-size: 12px; +} + +.order-service-save-bar { + position: sticky; + bottom: 0; + z-index: 4; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 4px 14px; + margin-top: 2px; + background: linear-gradient(180deg, rgba(245, 247, 251, 0) 0%, #f5f7fb 28%, #f5f7fb 100%); +} + +.order-service-jobs-collapse { + margin-top: 12px; + background: #ffffff; + border: 1px solid #edf0f5; + border-radius: 12px; +} + +.order-service-jobs-collapse .ant-collapse-header { + align-items: center !important; +} + +.order-service-jobs-collapse .ant-collapse-content-box { + padding-top: 0 !important; +} + +@media (max-width: 1200px) { + .order-service-field-main { + grid-template-columns: minmax(120px, 1fr) minmax(140px, 1fr) 110px 64px 104px; + } + + .order-service-basic-grid { + grid-template-columns: minmax(0, 1fr) 200px; + } +} + +@media (max-width: 960px) { + .order-service-layout { + grid-template-columns: 1fr; + } + + .order-service-sidebar { + position: static; + min-height: 0; + max-height: 280px; + border-right: 0; + border-bottom: 1px solid #edf0f5; + } + + .order-service-category-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + max-height: 200px; + } + + .order-service-editor { + padding: 16px 16px 0; + } + + .order-service-basic-grid { + grid-template-columns: 1fr; + } + + .order-service-guide { + grid-template-columns: minmax(0, 1fr) 160px; + } + + .order-service-guide > :not(.ant-upload-wrapper) { + grid-column: 1; + } + + .order-service-guide .ant-upload-wrapper { + grid-column: 2; + grid-row: 1 / span 3; + } +} + +@media (max-width: 720px) { + .order-service-editor-head, + .order-service-save-bar { + align-items: stretch; + flex-direction: column; + } + + .order-service-category-list { + grid-template-columns: 1fr; + } + + .order-service-field-main { + grid-template-columns: 1fr 1fr; + } + + .order-service-field-actions { + grid-column: 1 / -1; + justify-content: flex-start; + } + + .order-service-field-extra, + .order-service-switch-grid, + .order-service-switch-grid.is-auto-publish { + grid-template-columns: 1fr 1fr; + } + + .order-service-guide { + grid-template-columns: 1fr; + } + + .order-service-guide > :not(.ant-upload-wrapper), + .order-service-guide .ant-upload-wrapper { + grid-column: 1; + grid-row: auto; + } +}