背景: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/。
This commit is contained in:
1 parent
140dc386c1
commit
3e4bf48841
8 files changed
+1174
-215
No files matched your search
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable max-lines-per-function -- 页面编排筛选、表格与弹窗,集中便于追踪数据流 */
|
||||
import { Button, Input, Select, Steps } from '@douyinfe/semi-ui-19';
|
||||
import type { CreateCustomerInput } from '@lingxi/shared';
|
||||
/* eslint-disable max-lines-per-function, complexity -- 四步向导集中编排步骤、行业选择与提交校验 */
|
||||
import { Button, Input, Select } from '@douyinfe/semi-ui-19';
|
||||
import type { CreateCustomerInput, IndustryTreeItem } from '@lingxi/shared';
|
||||
import { notifySuccess, PageSkeleton } from '@lingxi/ui';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
@@ -10,9 +10,73 @@ import { useCreateCustomerMutation, useIndustriesQuery } from '@/hooks/queries/u
|
||||
import { useUserCandidatesQuery } from '@/hooks/queries/use-users';
|
||||
import { customerDetailPath, ROUTES } from '@/routes';
|
||||
|
||||
const STEPS = ['选择行业', '基础信息', '交付偏好', '行业资产包'];
|
||||
const STEPS = [
|
||||
{ key: 'industry', label: '选择行业', desc: '行业决定行业包与可用模板' },
|
||||
{ key: 'basic', label: '基础信息', desc: '名称、简称、负责人与对接人' },
|
||||
{ key: 'prefs', label: '交付偏好', desc: '命名、目录、规格与禁忌' },
|
||||
{ key: 'pack', label: '行业资产包', desc: '冷启动套用行业资产' },
|
||||
];
|
||||
|
||||
/** 新建客户(选定方案 B:左侧步骤导航 + 右侧表单) */
|
||||
function IndustryPicker({
|
||||
items,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
items: readonly IndustryTreeItem[];
|
||||
value: string | undefined;
|
||||
onChange: (id: string) => void;
|
||||
}): ReactNode {
|
||||
const groups = items
|
||||
.filter((root) => root.status === 'active')
|
||||
.map((root) => ({
|
||||
root,
|
||||
options:
|
||||
root.children.length === 0
|
||||
? [root]
|
||||
: root.children.filter((child) => child.status === 'active'),
|
||||
}))
|
||||
.filter((group) => group.options.length > 0);
|
||||
|
||||
return (
|
||||
<div className="lx-indpick">
|
||||
{groups.map((group) => (
|
||||
<div key={group.root.id} className="lx-indpick-group">
|
||||
<div className="lx-indpick-root">
|
||||
<span className="lx-indpick-dot" />
|
||||
<b>{group.root.name}</b>
|
||||
<span>
|
||||
{group.root.children.length === 0
|
||||
? '无二级行业,可直接选择'
|
||||
: `${group.root.children.length} 个二级行业`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="lx-indpick-opts">
|
||||
{group.options.map((option) => {
|
||||
const checked = option.id === value;
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
className={`lx-radio${checked ? ' is-on' : ''}`}
|
||||
onClick={() => {
|
||||
onChange(option.id);
|
||||
}}
|
||||
>
|
||||
<span className="lx-radio-dot" />
|
||||
<span>{option.name}</span>
|
||||
<i>{option.customerCount} 个客户</i>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{groups.length === 0 ? <div className="lx-client-empty">暂无可选行业</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 新建客户(选定方案 B:左侧带说明的步骤导航 + 右侧表单) */
|
||||
export function CustomerNewPage(): ReactNode {
|
||||
const navigate = useNavigate();
|
||||
const industries = useIndustriesQuery({ limit: 100 });
|
||||
@@ -32,21 +96,15 @@ export function CustomerNewPage(): ReactNode {
|
||||
const [taboo, setTaboo] = useState('绝对化用语,未授权肖像');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const industryOptions = useMemo(() => {
|
||||
const options: { value: string; label: string; leaf: boolean }[] = [];
|
||||
const industryLabel = useMemo(() => {
|
||||
for (const root of industries.data?.items ?? []) {
|
||||
if (root.status !== 'active') continue;
|
||||
if (root.children.length === 0) {
|
||||
options.push({ value: root.id, label: root.name, leaf: true });
|
||||
} else {
|
||||
for (const child of root.children) {
|
||||
if (child.status === 'active')
|
||||
options.push({ value: child.id, label: `${root.name} / ${child.name}`, leaf: true });
|
||||
}
|
||||
if (root.id === industryId) return root.name;
|
||||
for (const child of root.children) {
|
||||
if (child.id === industryId) return `${root.name} / ${child.name}`;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}, [industries.data]);
|
||||
return '未选择';
|
||||
}, [industries.data, industryId]);
|
||||
|
||||
const next = (): void => {
|
||||
setError(null);
|
||||
@@ -112,43 +170,64 @@ export function CustomerNewPage(): ReactNode {
|
||||
<h1>新建客户</h1>
|
||||
<p>分四步创建客户:选择行业 → 基础信息 → 交付偏好 → 行业资产包</p>
|
||||
</div>
|
||||
<div className="lx-client-header__actions">
|
||||
<Button onClick={() => void navigate(ROUTES.system.customers)}>取消</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="lx-client-newgrid">
|
||||
<aside className="lx-client-stepnav">
|
||||
<Steps current={current} direction="vertical">
|
||||
{STEPS.map((title) => (
|
||||
<Steps.Step key={title} title={title} />
|
||||
))}
|
||||
</Steps>
|
||||
<div className="lx-client-stepnav__head">
|
||||
<b>创建客户</b>
|
||||
<span>共 4 步</span>
|
||||
</div>
|
||||
{STEPS.map((step, index) => {
|
||||
const state = index === current ? ' is-on' : index < current ? ' is-done' : '';
|
||||
return (
|
||||
<button
|
||||
key={step.key}
|
||||
type="button"
|
||||
className={`lx-stepnav-item${state}`}
|
||||
onClick={() => {
|
||||
if (index < current) setCurrent(index);
|
||||
}}
|
||||
>
|
||||
<span className="lx-stepnav-num">{index < current ? '✓' : index + 1}</span>
|
||||
<span className="lx-stepnav-text">
|
||||
<b>{step.label}</b>
|
||||
<span>{step.desc}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</aside>
|
||||
<section className="lx-client-formcard">
|
||||
<div className="lx-client-formcard__head">
|
||||
<b>{STEPS[current]?.label}</b>
|
||||
<span>
|
||||
{current === 0
|
||||
? `${industryLabel} · 只能选择末级行业`
|
||||
: current === 1
|
||||
? '简称必填,用于全部自动命名'
|
||||
: current === 2
|
||||
? '命名、目录、规格与素材禁忌'
|
||||
: '行业包发布后可一键套用'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="lx-client-formcard__body">
|
||||
{current === 0 ? (
|
||||
<div className="lx-client-formgrid">
|
||||
<div className="lx-client-field is-full">
|
||||
<label>
|
||||
客户归属行业 <i>*</i>
|
||||
</label>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={industryId}
|
||||
placeholder="选择末级行业"
|
||||
optionList={industryOptions.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
}))}
|
||||
onChange={(value) => {
|
||||
setIndustryId(String(value));
|
||||
}}
|
||||
/>
|
||||
<div className="lx-client-hint">
|
||||
有子行业时必须选择二级;一级行业无子行业时可直接选择。
|
||||
</div>
|
||||
<>
|
||||
<div className="lx-client-section__head">
|
||||
<b>客户归属行业</b>
|
||||
<span className="lx-client-hint" style={{ marginTop: 0 }}>
|
||||
只能选择末级行业;一级行业没有二级时可直接选择
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<IndustryPicker
|
||||
items={industries.data?.items ?? []}
|
||||
value={industryId}
|
||||
onChange={setIndustryId}
|
||||
/>
|
||||
<div className="lx-client-hint">
|
||||
所属行业决定行业包;客户创建后不可跨行业移动,调整行业需停用后新建。
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
{current === 1 ? (
|
||||
<div className="lx-client-formgrid">
|
||||
@@ -239,7 +318,11 @@ export function CustomerNewPage(): ReactNode {
|
||||
<div className="lx-client-formcard__foot">
|
||||
<span className="lx-client-hint">步骤 {current + 1} / 4 · 可回退修改</span>
|
||||
<span className="spacer" />
|
||||
{current > 0 ? (
|
||||
{current === 0 ? (
|
||||
<Button theme="outline" onClick={() => void navigate(ROUTES.system.customers)}>
|
||||
取消
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCurrent((step) => step - 1);
|
||||
@@ -247,13 +330,18 @@ export function CustomerNewPage(): ReactNode {
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
) : null}
|
||||
)}
|
||||
{current < 3 ? (
|
||||
<Button type="primary" onClick={next}>
|
||||
<Button theme="solid" type="primary" onClick={next}>
|
||||
下一步
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" loading={create.isPending} onClick={() => void submit()}>
|
||||
<Button
|
||||
theme="solid"
|
||||
type="primary"
|
||||
loading={create.isPending}
|
||||
onClick={() => void submit()}
|
||||
>
|
||||
创建客户
|
||||
</Button>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user