在史诗007完成Allin系统后端模块移植的基础上,将allin_system-master/client目录中的UI模块移植到allin-packages目录下,作为独立的UI包,遵循项目现有的UI包结构和命名规范,实现UI模块的现代化重构和标准化管理。
当前相关功能:allin_system-master/client是一个基于Next.js + Ant Design的前端系统,根据分析有以下UI模块:
@d8d/area-management-ui包提供,直接复用即可,无需移植技术栈:Next.js、React 19、Ant Design 5、Jotai、Tailwind CSS、自定义fetch API
集成点:通过自定义API客户端与后端API集成,使用Jotai进行状态管理
@d8d/area-management-ui包执行本史诗时,必须参考以下架构文档:
特别强调:所有UI包开发必须严格遵循UI包开发规范,包括包结构、RPC客户端模式、组件开发、测试规范等。
根据对每个模块页面的分析,依赖关系如下:
channel管理页面 (app/admin/dashboard/channel/page.tsx)
company管理页面 (app/admin/dashboard/company/page.tsx)
disability管理页面 (app/admin/dashboard/disability/page.tsx)
disability_person管理页面 (app/admin/dashboard/disability_person/)
order管理页面 (app/admin/dashboard/order/)
platform管理页面 (app/admin/dashboard/platform/page.tsx)
region管理页面 (app/admin/dashboard/region/page.tsx)
@d8d/area-management-ui包提供相同功能,直接复用即可salary管理页面 (app/admin/dashboard/salary/page.tsx)
基于现有项目UI包命名规范,并考虑这是Allin系统专属模块,制定以下映射方案:
| 原页面名 | 独立UI包名 | 目录名 | 对应后端模块 | 说明 |
|---|---|---|---|---|
| channel | @d8d/allin-channel-management-ui |
channel-management-ui |
@d8d/allin-channel-module |
渠道管理UI |
| company | @d8d/allin-company-management-ui |
company-management-ui |
@d8d/allin-company-module |
公司管理UI |
| disability | @d8d/allin-disability-management-ui |
disability-management-ui |
@d8d/allin-disability-module |
残疾人管理UI |
| disability_person | @d8d/allin-disability-person-management-ui |
disability-person-management-ui |
@d8d/allin-disability-module |
残疾人个人管理UI |
| order | @d8d/allin-order-management-ui |
order-management-ui |
@d8d/allin-order-module |
订单管理UI |
| platform | @d8d/allin-platform-management-ui |
platform-management-ui |
@d8d/allin-platform-module |
平台管理UI |
| region | 复用现有@d8d/area-management-ui |
- | @d8d/geo-areas |
区域管理UI(已有现成包) |
| salary | @d8d/allin-salary-management-ui |
salary-management-ui |
@d8d/allin-salary-module |
薪资管理UI |
命名原则:
@d8d/allin-前缀,明确表明是Allin系统专属包-management-ui后缀,遵循现有UI包命名规范{模块名}-management-ui格式,便于在allin-packages目录中管理-mt后缀(本次移植为非多租户版本)由于这些UI模块是allin_system-master项目独有的业务模块(非通用模块),将在allin-packages目录下创建:
项目根目录/
├── allin-packages/ # Allin系统专属包目录
│ ├── channel-management-ui/ # 渠道管理UI
│ ├── company-management-ui/ # 公司管理UI
│ ├── disability-management-ui/ # 残疾人管理UI
│ ├── disability-person-management-ui/ # 残疾人个人管理UI
│ ├── order-management-ui/ # 订单管理UI
│ ├── platform-management-ui/ # 平台管理UI
│ # 区域管理UI已有`@d8d/area-management-ui`,无需创建新包
│ ├── salary-management-ui/ # 薪资管理UI
│ ├── channel-module/ # 后端模块(已存在)
│ ├── company-module/ # 后端模块(已存在)
│ ├── disability-module/ # 后端模块(已存在)
│ ├── platform-module/ # 后端模块(已存在)
│ ├── salary-module/ # 后端模块(已存在)
│ └── enums/ # 枚举常量包(已存在)
├── packages/ # 现有通用包目录(保持不变)
└── allin_system-master/ # 原始代码目录(移植源)
参考现有advertisement-management-ui结构,每个独立UI包应包含:
allin-packages/{module-name}-management-ui/
├── package.json # 包配置,workspace依赖管理
├── tsconfig.json # TypeScript配置
├── vitest.config.ts # 测试配置
├── src/
│ ├── index.ts # 主导出文件
│ ├── components/ # React组件
│ │ ├── index.ts # 组件导出
│ │ ├── {Module}Table.tsx # 数据表格组件
│ │ ├── {Module}Form.tsx # 表单组件
│ │ ├── {Module}Modal.tsx # 模态框组件
│ │ └── {Module}Filter.tsx # 筛选组件
│ ├── hooks/ # 自定义Hook
│ │ ├── index.ts
│ │ ├── use{Module}Query.ts # 数据查询Hook
│ │ └── use{Module}Mutation.ts # 数据变更Hook
│ ├── api/ # API客户端
│ │ ├── index.ts
│ │ ├── client.ts # Hono RPC客户端
│ │ └── types.ts # API类型定义
│ └── utils/ # 工具函数
│ ├── index.ts
│ └── formatters.ts
├── tests/ # 测试文件
│ ├── components/ # 组件测试
│ └── integration/ # 集成测试
└── dist/ # 构建输出
关键配置要求:
package.json中设置"type": "module"src/index.ts"@d8d/allin-{module}-module": "workspace:*"(对应后端模块)@d8d/allin-前缀,如@d8d/allin-channel-management-ui| 方面 | 当前项目(目标) | allin_system-master(源) | 差异程度 |
|---|---|---|---|
| UI框架 | React 19 + @d8d/shared-ui-components | React 19 + Ant Design 5 | 重大 |
| 状态管理 | React Query + React状态(useState/useContext) | Jotai | 中等 |
| 表单处理 | React Hook Form + Zod | Ant Design Form | 重大 |
| API客户端 | Hono RPC (rpcClient + ClientManager模式) | 自定义fetch API | 重大 |
| 样式方案 | Tailwind CSS + shadcn/ui | Ant Design + Tailwind CSS | 中等 |
| 组件库 | @d8d/shared-ui-components组件 | Ant Design组件 | 重大 |
| 构建工具 | unbuild | Next.js构建 | 重大 |
| 测试框架 | Vitest + Testing Library | 无测试 | 新增 |
@d8d/shared-ui-components、@tanstack/react-query、react-hook-form、zod、hono、shadcn/uiantd、jotai、自定义API客户端、dayjs// 原Ant Design表格组件
import { Table, Button, Space } from 'antd';
const ChannelTable = ({ data, loading, onEdit, onDelete }) => {
const columns = [
{ title: '渠道名称', dataIndex: 'channel_name', key: 'channel_name' },
{ title: '操作', key: 'action', render: (_, record) => (
<Space>
<Button onClick={() => onEdit(record)}>编辑</Button>
<Button danger onClick={() => onDelete(record)}>删除</Button>
</Space>
)}
];
return <Table dataSource={data} columns={columns} loading={loading} />;
};
// 转换后@d8d/shared-ui-components表格组件(使用rpcClient + ClientManager模式)
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
import { Button } from '@d8d/shared-ui-components/components/ui/button';
import { useQuery } from '@tanstack/react-query';
import { channelClientManager } from './api/channelClient';
const ChannelTable = () => {
const { data: channels, isLoading } = useQuery({
queryKey: ['channels'],
queryFn: async () => {
const res = await channelClientManager.get().channel.getChannelList.$get();
if (res.status !== 200) throw new Error('获取渠道列表失败');
return await res.json();
}
});
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>渠道名称</TableHead>
<TableHead>操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channels?.map((channel) => (
<TableRow key={channel.id}>
<TableCell>{channel.channelName}</TableCell>
<TableCell>
<Button variant="outline" onClick={() => onEdit(channel)}>编辑</Button>
<Button variant="destructive" onClick={() => onDelete(channel)}>删除</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};
// 原Ant Design表单
import { Form, Input, Button } from 'antd';
const ChannelForm = ({ initialValues, onSubmit }) => {
const [form] = Form.useForm();
return (
<Form form={form} initialValues={initialValues} onFinish={onSubmit}>
<Form.Item name="channel_name" label="渠道名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">提交</Button>
</Form.Item>
</Form>
);
};
// 转换后React Hook Form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
import { Input } from '@d8d/shared-ui-components/components/ui/input';
import { Button } from '@d8d/shared-ui-components/components/ui/button';
const channelSchema = z.object({
channelName: z.string().min(1, '渠道名称不能为空')
});
const ChannelForm = ({ initialValues, onSubmit }) => {
const form = useForm({
resolver: zodResolver(channelSchema),
defaultValues: initialValues
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="channelName"
render={({ field }) => (
<FormItem>
<FormLabel>渠道名称</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">提交</Button>
</form>
</Form>
);
};
// 原自定义fetch API
const api = {
getChannels: async () => {
const response = await fetch('/api/channel/getChannelList');
return response.json();
},
createChannel: async (data) => {
const response = await fetch('/api/channel/createChannel', {
method: 'POST',
body: JSON.stringify(data)
});
return response.json();
}
};
// 转换后Hono RPC客户端(遵循现有项目模式)
import { channelRoutes } from '@d8d/allin-channel-module';
import { rpcClient } from '@d8d/shared-ui-components/utils/hc'
class ChannelClientManager {
private static instance: ChannelClientManager;
private client: ReturnType<typeof rpcClient<typeof channelRoutes>> | null = null;
private constructor() {}
public static getInstance(): ChannelClientManager {
if (!ChannelClientManager.instance) {
ChannelClientManager.instance = new ChannelClientManager();
}
return ChannelClientManager.instance;
}
// 初始化客户端
public init(baseUrl: string = '/'): ReturnType<typeof rpcClient<typeof channelRoutes>> {
return this.client = rpcClient<typeof channelRoutes>(baseUrl);
}
// 获取客户端实例
public get(): ReturnType<typeof rpcClient<typeof channelRoutes>> {
if (!this.client) {
return this.init()
}
return this.client;
}
// 重置客户端(用于测试或重新初始化)
public reset(): void {
this.client = null;
}
}
// 导出单例实例
const channelClientManager = ChannelClientManager.getInstance();
// 导出默认客户端实例(延迟初始化)
export const channelClient = channelClientManager.get()
// 在React组件中的使用示例
import { channelClientManager } from './api/channelClient';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 类型定义(使用导出的channelClient进行类型推导)
import type { InferResponseType, InferRequestType } from 'hono/client';
import { channelClient } from './api/channelClient';
type ChannelResponse = InferResponseType<typeof channelClient.channel.getChannelList.$get, 200>['data'][0];
type CreateChannelRequest = InferRequestType<typeof channelClient.channel.createChannel.$post>['json'];
// React组件中的实际调用
const ChannelManagement: React.FC = () => {
const queryClient = useQueryClient();
// 查询数据 - 使用channelClientManager.get()获取客户端实例
const { data: channels, isLoading } = useQuery({
queryKey: ['channels'],
queryFn: async () => {
const res = await channelClientManager.get().channel.getChannelList.$get();
if (res.status !== 200) throw new Error('获取渠道列表失败');
return await res.json();
}
});
// 创建数据
const createMutation = useMutation({
mutationFn: async (data: CreateChannelRequest) => {
const res = await channelClientManager.get().channel.createChannel.$post({ json: data });
if (res.status !== 201) throw new Error('创建渠道失败');
return await res.json();
},
onSuccess: () => {
// 刷新数据
queryClient.invalidateQueries({ queryKey: ['channels'] });
}
});
// ...组件其他代码
};
// 原Jotai状态管理
import { atom, useAtom } from 'jotai';
const channelsAtom = atom([]);
const loadingAtom = atom(false);
const useChannels = () => {
const [channels, setChannels] = useAtom(channelsAtom);
const [loading, setLoading] = useAtom(loadingAtom);
const fetchChannels = async () => {
setLoading(true);
const data = await api.getChannels();
setChannels(data);
setLoading(false);
};
return { channels, loading, fetchChannels };
};
// 转换后React Query(使用rpcClient + ClientManager模式)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { channelClientManager } from './api/channelClient';
import type { InferRequestType } from 'hono/client';
import { channelClient } from './api/channelClient';
type CreateChannelRequest = InferRequestType<typeof channelClient.channel.createChannel.$post>['json'];
const useChannels = () => {
const queryClient = useQueryClient();
const { data: channels, isLoading } = useQuery({
queryKey: ['channels'],
queryFn: async () => {
const res = await channelClientManager.get().channel.getChannelList.$get();
if (res.status !== 200) throw new Error('获取渠道列表失败');
return await res.json();
}
});
const createMutation = useMutation({
mutationFn: async (data: CreateChannelRequest) => {
const res = await channelClientManager.get().channel.createChannel.$post({ json: data });
if (res.status !== 201) throw new Error('创建渠道失败');
return await res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['channels'] });
}
});
return { channels, isLoading, createChannel: createMutation.mutateAsync };
};
建议移植顺序:
@d8d/area-management-ui包,直接复用具体顺序:
allin-platform-management-ui(最简单)allin-channel-management-ui(独立)allin-company-management-ui(依赖platform)allin-salary-management-ui(中等复杂度)allin-disability-management-ui(中等复杂度)allin-disability-person-management-ui(高复杂度)allin-order-management-ui(高复杂度)
注:region管理UI已有@d8d/area-management-ui包,无需移植注:每个故事对应一个UI模块的完整移植,包括技术栈转换和组件测试。
目标:移植基础平台管理页面
验收标准:
allin-packages/platform-management-ui目录结构目标:将channel管理页面移植为独立UI包,完成技术栈转换并验证功能完整性
验收标准:
allin-packages/channel-management-ui目录结构@d8d/allin-channel-management-ui包名,workspace依赖@d8d/allin-channel-module后端模块集成验证组件测试要求:
tests/components/ChannelTable.test.tsx等目标:将company管理页面移植为独立UI包,处理对platform数据的依赖
验收标准:
allin-packages/company-management-ui目录结构@d8d/allin-platform-management-ui目标:移植薪资管理页面,包含复杂表单验证
验收标准:
allin-packages/salary-management-ui目录结构@d8d/area-management-ui的区域选择器组件@d8d/area-management-ui目标:将disability管理页面移植为独立UI包
验收标准:
allin-packages/disability-management-ui目录结构@d8d/area-management-ui的区域选择器组件用于残疾人区域信息管理@d8d/allin-disability-module和@d8d/area-management-ui目标:移植最复杂的残疾人个人管理页面,包含照片上传、备注管理等
验收标准:
allin-packages/disability-person-management-ui目录结构@d8d/file-management-ui集成@d8d/area-management-ui的区域选择器组件用于残疾人详细地址管理(省份→城市→区县三级联动)@d8d/allin-enums包中的枚举@d8d/area-management-ui)目标:移植复杂的订单管理页面,包含人员分配、资产关联等
验收标准:
allin-packages/order-management-ui目录结构@d8d/area-management-ui的区域选择器组件用于订单相关区域信息管理@d8d/allin-enums包中的订单状态枚举@d8d/area-management-ui)目标:将史诗008中创建的7个Allin系统UI包集成到Web client admin中,对应史诗007的最后一个任务(故事8:将Allin系统模块集成到packages/server)
验收标准:
依赖配置:在web/package.json中添加所有7个Allin系统UI包的workspace依赖
@d8d/allin-channel-management-ui@d8d/allin-company-management-ui@d8d/allin-disability-management-ui@d8d/allin-disability-person-management-ui@d8d/allin-order-management-ui@d8d/allin-platform-management-ui@d8d/allin-salary-management-uiAPI客户端初始化:在web/src/client/admin/api_init.ts中添加所有7个Allin系统UI包的API客户端初始化,路径必须与server包中的路由配置完全一致(参考packages/server/src/index.ts:139-144)
channelClientManager.init('/api/v1/channel') - 对应server中的/api/v1/channelcompanyClientManager.init('/api/v1/company') - 对应server中的/api/v1/companydisabilityClientManager.init('/api/v1/disability') - 对应server中的/api/v1/disability(注意:disability_person模块在server中注册为/api/v1/disability)orderClientManager.init('/api/v1/order') - 对应server中的/api/v1/orderplatformClientManager.init('/api/v1/platform') - 对应server中的/api/v1/platformsalaryClientManager.init('/api/v1/salary') - 对应server中的/api/v1/salarys后缀,与server包配置保持一致路由集成:在web/src/client/admin/routes.tsx中添加所有7个UI包的路由配置
/admin/channels → @d8d/allin-channel-management-ui/admin/companies → @d8d/allin-company-management-ui/admin/disabilities → @d8d/allin-disability-management-ui/admin/disability-persons → @d8d/allin-disability-person-management-ui/admin/orders → @d8d/allin-order-management-ui/admin/platforms → @d8d/allin-platform-management-ui/admin/salaries → @d8d/allin-salary-management-ui菜单集成:在web/src/client/admin/menu.tsx中添加对应的菜单项
权限配置:为每个菜单项配置相应的权限(参考现有权限模式)
图标配置:为每个菜单项配置合适的图标(使用lucide-react图标库)
API路径配置:确保API路径与后端路由对应(参考史诗007的server集成)
集成验证:验证所有路由正常工作,UI包正确加载,API客户端初始化成功
测试验证:通过E2E测试验证集成功能
技术要点:
与史诗007的对应关系:
目标:复用现有@d8d/area-management-ui包,无需重新移植
现状分析:
@d8d/area-management-ui已提供完整的区域管理功能@d8d/geo-areas后端模块已集成实施策略:
@d8d/area-management-ui包@d8d/area-management-ui的依赖AreaSelector、AreaTree等现有组件DisabledPhoto相关页面使用直接文件上传OrderPersonAsset相关页面使用文件上传@d8d/file-management-uifileId关联业务实体核心思想:复用现有的@d8d/file-management-ui包,保持统一的文件上传体验
组件集成:
// 在disability-person-management-ui中集成文件选择器
import { FileSelector } from '@d8d/file-management-ui';
const PhotoUploadField = ({ value, onChange }) => {
return (
<div>
<Label>照片上传</Label>
<FileSelector
accept="image/*"
onFileSelected={(fileId) => onChange(fileId)}
/>
{value && <FilePreview fileId={value} />}
</div>
);
};
表单集成:
// 残疾人照片表单
const disabilityPersonSchema = z.object({
// ...其他字段
photoFileId: z.number().optional(), // 文件ID字段
});
// 提交时只传递fileId
const onSubmit = (data) => {
// data.photoFileId 包含文件ID
await api.createDisabledPerson(data);
};
数据显示:
// 显示照片时使用文件预览组件
import { FilePreview } from '@d8d/file-management-ui';
const PhotoDisplay = ({ fileId }) => {
return <FilePreview fileId={fileId} />;
};
需要调整以下故事的文件上传处理:
故事6(disability-person-management-ui):
fileId而非文件内容故事7(order-management-ui):
@d8d/area-management-ui包"UI模块分析和技术栈分析已完成,关键发现:
@d8d/allin-前缀,-management-ui后缀,非多租户版本allin-packages/目录下创建UI包,与后端模块保持相同目录,遵循UI包开发规范新的故事拆分方案(按执行顺序编号):
@d8d/area-management-ui包(无需移植)每个故事的关键要求:
tests/components/测试文件,遵循UI包开发规范执行顺序: 按故事编号顺序执行即可:故事1 → 故事2 → 故事3 → 故事4 → 故事5 → 故事6 → 故事7 → 故事8
特别说明:
依赖关系说明:
技术栈转换关键点:
@d8d/file-management-ui文件选择器组件组件测试模板参考:
参考/packages/advertisement-management-ui/tests/components/
@testing-library/react进行组件测试文件上传集成方案:
发现allin_system-master中有文件上传功能(照片、资产文件),需要与现有@d8d/file-management-ui集成。
采用方案:复用现有文件管理UI包
FileSelector组件用于照片上传fileId参数(而非文件内容)FilePreview组件显示照片FileSelector组件用于资产文件上传实施要点:
故事8(Web client admin集成)关键要求:
web/package.json中添加所有7个Allin系统UI包的workspace依赖web/src/client/admin/api_init.ts中添加所有7个Allin系统UI包的API客户端初始化,API路径必须与server包中的路由配置完全一致(参考packages/server/src/index.ts:139-144)
/api/v1/channel、/api/v1/company等(没有s后缀)disability_person模块对应/api/v1/disability路径web/src/client/admin/routes.tsx中添加7个路由路径,对应每个UI包web/src/client/admin/menu.tsx中添加对应的菜单项,配置权限和图标史诗应在保持用户体验一致性的同时实现将UI模块从Ant Design架构移植到@d8d/shared-ui-components架构的标准化独立UI包,每个模块都要有完整的组件测试验证,并完成与现有文件管理UI包的集成,最后通过故事8将所有UI包集成到Web client admin中。"