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 {