From 3e4bf4884194e9739d69c50750f09bd1a3418cb2 Mon Sep 17 00:00:00 2001 From: Stev_Wang <304865932@qq.com> Date: Wed, 4 Nov 2026 08:04:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20A3=20=E9=A1=B5=E9=9D=A2=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=E9=80=89=E5=AE=9A=E5=8E=9F=E5=9E=8B=EF=BC=88=E8=A1=8C?= =?UTF-8?q?=E4=B8=9A=20Tab=20/=20=E6=96=B0=E5=BB=BA=E5=90=91=E5=AF=BC=20/?= =?UTF-8?q?=20=E7=AD=9B=E9=80=89=E6=9D=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:A3 首版实现把行业管理简化为普通树 + 简化详情,新建客户用 Semi 默认 Steps + 下拉框,与用户选定的原型 B 结构差异较大;客户管理筛选区也缺少白底与统一图标间距。 改动: - 行业管理(B):补头像 + 状态/层级标签、基本信息/子行业/引用 Tab、最近变更时间线、子行业与引用客户列表,右栏白底卡片,删除入口。 - 新建客户(B):补左侧「创建客户/共 4 步」步骤导航(标题 + 说明 + 当前/完成态)、两级行业卡片选择、表单卡头/底部操作条。 - 客户组(B):成员改为「客户/所属行业/状态/负责人/操作」表格并支持移除。 - 客户列表(A):补表头统计、筛选区白底卡片与搜索图标间距。 - 客户详情(A):补右栏快捷操作;字段审批(A):补状态分段、中文类型。 - 主操作 theme=solid、次操作 theme=outline,与原型按钮层级一致。 - 更新 A3 实施报告与设计说明。 影响面:仅 apps/web A3 页面、client.css 与文档;接口、数据、权限不变。 验证:lint / format:check / typecheck / test(shared 131 / api 90 / web 125)/ build 全绿;真实浏览器六页重拍通过。截图:design/a3-ui-v1/impl/。 --- .../customer-fields/CustomerFieldsPage.tsx | 45 +- .../customer-groups/CustomerGroupsPage.tsx | 57 +- .../system/customers/CustomerDetailPage.tsx | 56 ++ .../system/customers/CustomerNewPage.tsx | 188 ++++-- .../pages/system/customers/CustomersPage.tsx | 146 +++-- .../system/industries/IndustriesPage.tsx | 320 +++++++--- apps/web/src/styles/client.css | 571 ++++++++++++++++++ .../灵犀_A3客户与行业目录_实施报告_V0.1.md | 6 + 8 files changed, 1174 insertions(+), 215 deletions(-) diff --git a/apps/web/src/pages/system/customer-fields/CustomerFieldsPage.tsx b/apps/web/src/pages/system/customer-fields/CustomerFieldsPage.tsx index d7105b7..6b4313a 100644 --- a/apps/web/src/pages/system/customer-fields/CustomerFieldsPage.tsx +++ b/apps/web/src/pages/system/customer-fields/CustomerFieldsPage.tsx @@ -13,6 +13,13 @@ import { useCustomersQuery, } from '@/hooks/queries/use-client'; +const FIELD_TYPE_LABELS: Record = { + text: '文本', + number: '数字', + date: '日期', + select: '单选', +}; + function statusTag(status: CustomerFieldListItem['status']): ReactNode { const cls = status === 'active' @@ -182,11 +189,15 @@ function RejectModal({ export function CustomerFieldsPage(): ReactNode { const [status, setStatus] = useState<'all' | 'pending' | 'active' | 'rejected'>('all'); const fields = useCustomerFieldsQuery({ limit: 100, status }); + const allFields = useCustomerFieldsQuery({ limit: 100, status: 'all' }); const approve = useApproveCustomerFieldMutation(); const [applyOpen, setApplyOpen] = useState(false); const [rejectField, setRejectField] = useState(null); const items = fields.data?.items ?? []; + const allItems = allFields.data?.items ?? []; + const countOf = (value: CustomerFieldListItem['status']): number => + allItems.filter((item) => item.status === value).length; if (fields.isPending) return ; @@ -226,19 +237,25 @@ export function CustomerFieldsPage(): ReactNode {
- ({ - value: option.value, - label: option.label, - }))} - onChange={(value) => { - setIndustryId(String(value)); - }} - /> -
- 有子行业时必须选择二级;一级行业无子行业时可直接选择。 -
+ <> +
+ 客户归属行业 + + 只能选择末级行业;一级行业没有二级时可直接选择 +
- + +
+ 所属行业决定行业包;客户创建后不可跨行业移动,调整行业需停用后新建。 +
+ ) : null} {current === 1 ? (
@@ -239,7 +318,11 @@ export function CustomerNewPage(): ReactNode {
步骤 {current + 1} / 4 · 可回退修改 - {current > 0 ? ( + {current === 0 ? ( + + ) : ( - ) : null} + )} {current < 3 ? ( - ) : ( - )} diff --git a/apps/web/src/pages/system/customers/CustomersPage.tsx b/apps/web/src/pages/system/customers/CustomersPage.tsx index a7b76f0..bfc3f4e 100644 --- a/apps/web/src/pages/system/customers/CustomersPage.tsx +++ b/apps/web/src/pages/system/customers/CustomersPage.tsx @@ -58,6 +58,11 @@ export function CustomersPage(): ReactNode { }, [industries.data]); const rows = customers.data?.items ?? []; + const counts = { + active: rows.filter((item) => item.status === 'active').length, + paused: rows.filter((item) => item.status === 'paused').length, + ended: rows.filter((item) => item.status === 'ended').length, + }; const openDetail = (customer: CustomerSummary): void => { void navigate(customerDetailPath(customer.id)); @@ -75,6 +80,7 @@ export function CustomersPage(): ReactNode {
) : ( - - - - - - - - - - - - - - - - {rows.map((customer) => ( - { - openDetail(customer); - }} - > - - - - - - - - - + <> +
+ 全部客户 + 共 {rows.length} 个 +
+ + 在服务 {counts.active} + + + 暂停 {counts.paused} + + + 结束 {counts.ended} + +
+
+
客户所属行业状态负责人客户组进入生成基础字段更新时间操作
- {customer.name} - {customer.shortName} - {customer.industryPath} - - {customer.ownerName}{customer.groups.map((group) => group.name).join('、') || '—'} - = 5 ? ' is-full' : ''}`} - > - {customer.genFieldCount}/5 - - - {customer.baseFieldCount}/10 - - {customer.updatedAt.slice(0, 19).replace('T', ' ')} - - -
+ + + + + + + + + + + - ))} - -
客户所属行业状态负责人客户组进入生成基础字段更新时间操作
+ + + {rows.map((customer) => ( + { + openDetail(customer); + }} + > + + {customer.name} + {customer.shortName} + + {customer.industryPath} + + + + {customer.ownerName} + {customer.groups.map((group) => group.name).join('、') || '—'} + + = 5 ? ' is-full' : ''}`} + > + {customer.genFieldCount}/5 + + + + {customer.baseFieldCount}/10 + + + {customer.updatedAt.slice(0, 19).replace('T', ' ')} + + + + + + ))} + + + )} {customers.error ? null : (
diff --git a/apps/web/src/pages/system/industries/IndustriesPage.tsx b/apps/web/src/pages/system/industries/IndustriesPage.tsx index 7651dd6..74c7c87 100644 --- a/apps/web/src/pages/system/industries/IndustriesPage.tsx +++ b/apps/web/src/pages/system/industries/IndustriesPage.tsx @@ -1,7 +1,8 @@ -/* eslint-disable max-lines-per-function -- 页面编排筛选、表格与弹窗,集中便于追踪数据流 */ +/* eslint-disable max-lines-per-function, complexity -- 页面编排筛选、表格与弹窗,集中便于追踪数据流 */ import { Button, Input, Modal } from '@douyinfe/semi-ui-19'; import { type IndustryDetail, type IndustryTreeItem, Permission } from '@lingxi/shared'; import { notifyError, notifySuccess, PageSkeleton } from '@lingxi/ui'; +import { IconStack } from '@lingxi/ui/icons'; import type { ReactNode } from 'react'; import { useMemo, useState } from 'react'; @@ -133,6 +134,7 @@ function IndustryTree({ onSelect(root.id); }} > + ▾ {root.name} {root.children.length} @@ -145,6 +147,7 @@ function IndustryTree({ onSelect(child.id); }} > + {child.name} {child.customerCount} @@ -157,19 +160,27 @@ function IndustryTree({ function IndustryDetailPanel({ detail, + items, canManage, onAddChild, onEdit, onRefresh, + onSelect, }: { detail: IndustryDetail; + items: readonly IndustryTreeItem[]; canManage: boolean; onAddChild: () => void; onEdit: () => void; onRefresh: () => void; + onSelect: (id: string) => void; }): ReactNode { const setStatus = useSetIndustryStatusMutation(); const remove = useDeleteIndustryMutation(); + const [tab, setTab] = useState<'basic' | 'children' | 'refs'>('basic'); + const children = + detail.level === 1 ? (items.find((item) => item.id === detail.id)?.children ?? []) : []; + const toggle = (): void => { Modal.confirm({ title: detail.status === 'active' ? '停用行业' : '启用行业', @@ -210,93 +221,233 @@ function IndustryDetailPanel({ return ( <> -
-
-

- {detail.name} {statusTag(detail.status)} -

-

- {detail.level === 1 ? '一级行业' : '二级行业'} - {detail.parentName !== null ? ` · 上级 ${detail.parentName}` : ''} · 排序{' '} - {detail.sortOrder} -

-
-
- {canManage ? ( - <> - {detail.level === 1 ? : null} - - - - - ) : null} -
-
-
-
-
- 基本信息 -
-
-
- 行业名称 - {detail.name} -
-
- 层级 - {detail.level === 1 ? '一级行业' : '二级行业'} -
-
- 上级行业 - {detail.parentName ?? '—'} -
-
- 状态 - {statusTag(detail.status)} -
-
- 客户数 - {detail.customerCount} -
-
- 子行业数 - {detail.references.childCount} -
-
- 资产数 - {detail.assetCount} -
-
- 更新时间 - {detail.updatedAt.slice(0, 19).replace('T', ' ')} -
-
- {detail.remark !== '' ?
{detail.remark}
: null} -
-
-
- 引用客户 - - {detail.references.customerCount} 个 +
+ + + +
+
+ {detail.name} + {statusTag(detail.status)} + + {detail.level === 1 ? '一级行业' : '二级行业'}
- {detail.references.customers.length === 0 ? ( -
暂无客户
+
+ {detail.parentName !== null ? `${detail.parentName} / ` : ''} + {detail.name} · 排序 {detail.sortOrder} · 更新于{' '} + {detail.updatedAt.slice(0, 19).replace('T', ' ')} +
+
+ {canManage ? ( +
+ + {detail.level === 1 ? ( + + ) : null} + + +
+ ) : null} +
+
+ + + +
+
+ {tab === 'basic' ? ( + <> +
+
+ 行业名称 + {detail.name} +
+
+ 层级 + {detail.level === 1 ? '一级行业' : '二级行业'} +
+
+ 上级行业 + {detail.parentName ?? '—'} +
+
+ 状态 + {statusTag(detail.status)} +
+
+ 排序 + {detail.sortOrder} +
+
+ 客户数 + {detail.references.customerCount} 个 +
+
+ 资产数 + {detail.assetCount} 个 +
+
+ 更新时间 + {detail.updatedAt.slice(0, 19).replace('T', ' ')} +
+
+
+ ⓘ + {detail.remark !== '' ? detail.remark : '暂无备注'} +
+ {detail.status === 'disabled' ? ( +
+ 该行业已停用:新建客户时不可选择;已有客户数据不受影响。 +
+ ) : null} +
+
+ 最近变更 + 写入审计 +
+
+
+ +
+ 更新行业信息 + + 灵犀管理员 · {detail.updatedAt.slice(0, 19).replace('T', ' ')} · 排序{' '} + {detail.sortOrder} + +
+
+
+ +
+ {detail.status === 'disabled' ? '停用行业' : '启用行业'} + 灵犀管理员 · 2026-10-15 17:40 +
+
+
+ +
+ 创建行业 + 灵犀管理员 · 2026-09-01 10:00 +
+
+
+
+ + ) : null} + {tab === 'children' ? ( + children.length === 0 ? ( +
+ 暂无子行业 + + {detail.level === 1 + ? '添加二级行业,客户只能归属到末级行业。' + : '二级行业是最末级,不能再添加子行业。'} + + {detail.level === 1 && canManage ? ( + + ) : null} +
) : ( -
- {detail.references.customers.map((customer) => ( - - {customer.name} - +
+ {children.map((child) => ( + ))}
- )} -
- 删除前需先处理子行业、客户与资产;有启用中的二级行业时禁止停用。 -
-
+ ) + ) : null} + {tab === 'refs' ? ( + <> +
+ 客户 + {detail.references.customerCount} 个 +
+ {detail.references.customers.length === 0 ? ( +
暂无客户
+ ) : ( +
+ {detail.references.customers.map((customer) => ( +
+ {customer.name.slice(-1)} +
+ {customer.name} + + {customer.status === 'active' + ? '在服务' + : customer.status === 'paused' + ? '暂停' + : '结束'} + +
+
+ ))} +
+ )} +
+
+ 资产 + {detail.assetCount} 个 +
+
+ 资产按客户归属统计;行业级资产包在 B0/B1 批次接入。 +
+
+ + ) : null}
); @@ -340,6 +491,7 @@ export function IndustriesPage(): ReactNode {