@infly/react-ui
v0.1.12
Published
面向 React 后台应用的通用 UI 包。组件基于 Ant Design,不依赖路由、请求库、TanStack Query 或具体业务领域。
Readme
@infly/react-ui
面向 React 后台应用的通用 UI 包。组件基于 Ant Design,不依赖路由、请求库、TanStack Query 或具体业务领域。
使用方式
import { InflyList, createInflyAntdTheme } from "@infly/react-ui";
import "@infly/react-ui/styles.css";应用通过 Ant Design ConfigProvider 传入 createInflyAntdTheme() 的结果覆盖品牌 Token;公共包不替业务应用决定主色、布局或权限。
InflyForm
字段通过 InflyFieldConfig 配置。switchProps 除了支持 Ant Design SwitchProps,也支持 React ARIA 属性,可为开关提供明确的无障碍名称:
const fields = [
{
name: "canLoginAdmin",
label: "可登录后台",
type: "switch",
switchProps: { "aria-label": "可登录后台" },
},
] satisfies Array<InflyFieldConfig<EmployeeInput>>;业务代码需要主动提交、重置、校验或回填错误时,使用 useInflyForm 创建受控实例,不再直接调用 Ant Design Form.useForm():
const [form] = useInflyForm<EmployeeInput>();
<InflyForm
form={form}
fields={fields}
onFinish={(values) => saveEmployee(values)}
>
<Button type="primary" onClick={() => form.submit()}>
保存
</Button>
</InflyForm>;不需要主动操作时可以省略 form,InflyForm 会创建内部实例。children 渲染在字段网格之后,可放提交按钮或动态表单内容。
文本输入框右侧需要验证码按钮等组合控件时使用 controlAfter。组件内部使用 Space.Compact 并保持输入框的表单绑定,不需要使用已废弃的 Input.addonAfter:
{
name: "code",
label: "验证码",
controlAfter: <Button onClick={sendCode}>获取验证码</Button>,
}字段容器的说明、样式、必填标记等属性通过 itemProps 传入:
const quotaFields = [
{
name: "totalPoints",
label: "总额度",
type: "number",
required: true,
numberProps: { min: 1, precision: 0 },
itemProps: { extra: "请输入服务端允许范围内的整数" },
},
] satisfies Array<InflyFieldConfig<QuotaInput>>;不写入提交值的自定义区块可以省略 name,但必须提供稳定的 key:
{
key: "permissions",
label: "功能权限",
type: "custom",
itemProps: { required: true },
render: () => <Tree checkable treeData={treeData} />,
}动态数组使用 InflyFormList。它封装 Ant Design Form.List,仅向业务代码暴露行描述和 add/remove/move:
<InflyForm
form={form}
initialValues={{ items: [{ organizationId: undefined, points: undefined }] }}
onFinish={saveAllocations}
>
<InflyFormList name="items">
{(rows, { add, remove }) => (
<>
{rows.map((row) => (
<AllocationRow key={row.key} name={row.name} onRemove={() => remove(row.name)} />
))}
<Button onClick={() => add()}>继续添加</Button>
</>
)}
</InflyFormList>
</InflyForm>现有 fields、columns、layout 和表单属性语义保持不变;新增 API 均为可选扩展。
InflyList
旧的 dataProvider 模式继续由组件维护请求、分页和 loading:
<InflyList
rowKey="id"
columns={columns}
dataProvider={({ filters, page, pageSize }) => api.list({ filters, page, pageSize })}
/>需要 TanStack Query、URL 查询参数或其他外部状态时使用受控模式:
<InflyList
rowKey="id"
columns={columns}
searchFields={searchFields}
dataSource={query.data?.items ?? []}
loading={query.isFetching}
filterValues={filters}
pagination={{ current: page, pageSize, total: query.data?.total ?? 0 }}
onQueryChange={({ filters: nextFilters, page: nextPage, pageSize: nextPageSize }) => {
updateUrl({ filters: nextFilters, page: nextPage, pageSize: nextPageSize });
}}
/>dataProvider 与 dataSource/loading/pagination/onQueryChange 是 TypeScript 互斥数据源,不能同时传入。公共组件不持有 QueryClient,也不清理业务缓存。
筛选按钮和列表工具栏默认保持原有布局;业务页面需要显式调整时可传入对齐配置:
<InflyList
filterActionsAlign="start"
toolbarAlign="end"
toolbar={<Button>导出</Button>}
{...listProps}
/>filterActionsAlign:"start" | "end",未传时保持原有筛选按钮布局。toolbarAlign:"start" | "end" | "between",未传时保持原有工具栏两端布局。showRefresh默认false,只有页面显式传入时才显示刷新按钮。
InflyDetail
InflyDetail 统一后台详情页的只读和编辑行为:默认文案态只有一个“修改”入口;编辑态只有“取消”和一个主按钮“保存”。所有分组共用同一张表单和一次提交。
只读态的 colSpan 会自动归一化:当某行已占列数加上新字段 colSpan 超过 columns 时,当前行最后一个字段自动扩展为剩余列数(与 antd 行末自动填充等价,布局不变),避免 antd Descriptions 的布局警告。开发者无需为奇偶错配手工凑列。
const sections: Array<InflyDetailSection<ActivityValues>> = [
{
key: "basic",
title: "基本信息",
fields: [
{ name: "id", label: "活动ID", editable: false },
{ name: "name", label: "活动名称", editable: true, required: true },
{ name: "description", label: "活动说明", editable: true, type: "textarea" },
{ name: "status", label: "状态", editable: false },
],
},
];
<InflyDetail
mode={mode}
values={activity}
sections={sections}
canEdit={permissions.includes("activity.edit")}
submitting={updateMutation.isPending}
extra={<Button onClick={pauseActivity}>暂停活动</Button>}
onModeChange={setMode}
onRequestCancel={(dirty) => dirty ? confirmDiscard() : setMode("read")}
onSubmit={(values) => updateMutation.mutateAsync(values)}
/>约束:
editable: true才渲染输入控件,ID、状态、计算值和创建信息保持只读。extra仅承载状态流转等独立业务动作,不能再放第二个资料修改入口。- 组件不处理接口、权限码、业务状态机或缓存;页面负责
canEdit、提交和冲突处理。 onRequestCancel接收表单是否已修改,页面决定是否弹出放弃确认。
InflyAdminShell
InflyAdminShell 提供不绑定路由和业务 Session 的后台布局。应用传入品牌、菜单、当前用户和回调,公共包负责响应式侧栏、Header 和绿色运营后台视觉。
<InflyAdminShell
brand={{ mark: "积", title: "积分商城", subtitle: "运营后台" }}
menuItems={[{ key: "activities", label: "活动管理" }]}
selectedKeys={["activities"]}
user={{ name: "运营管理员", organizationName: "积分商城总公司" }}
onMenuSelect={(key) => navigate(`/${key}`)}
onLogout={logout}
>
<Outlet />
</InflyAdminShell>品牌区默认由 mark 徽标与 title / subtitle 文字组成。若品牌方提供完整品牌图(已含徽标与文字),可改用 brand.image 整图渲染,此时忽略 mark / title / subtitle:
<InflyAdminShell
brand={{ image: <img alt="积分商城运营后台" src={brandImage} /> }}
{...otherProps}
/>侧栏折叠后整张品牌图隐藏(与默认模式的文字隐藏行为一致)。
响应式行为
>= 1200px:使用固定侧栏和最大宽度为 1440px 的内容容器。768px - 1199px:收紧页面间距,InflyForm筛选和编辑网格自动降为两列。375px - 767px:侧栏切换为 Drawer,页面标题、筛选和详情改为单列;宽表格只在.infly-table-panel内横向滚动。
应用不需要传入新的 props。菜单选择仍调用 onMenuSelect,手机 Drawer 会在选择后自动关闭;桌面折叠行为和旧回调语义保持不变。
菜单展开状态会跟随 selectedKeys 同步:选中二级菜单时自动展开其父级;切换到其他一级菜单时自动收起旧父级。用户在当前路由内仍可手动展开或收起菜单分组。
如需复用与 points-mall-admin-web 一致的顶部栏毛玻璃滚动态,可传入:
<InflyAdminShell
headerEffect="frosted"
headerScrollTrigger={48}
{...otherProps}
/>headerEffect:"default" | "frosted",默认"default"。headerScrollTrigger:滚动阈值(像素),仅在headerEffect="frosted"时生效,默认48。
应用必须导入 @infly/react-ui/styles.css,并避免在 html、body 或根节点重新设置大于 375px 的 min-width,否则手机断点无法生效。
公共样式同时提供“跳到主要内容”的键盘入口、可见的 :focus-visible 状态和 prefers-reduced-motion 降级。业务应用仍负责具体字段、状态、权限和移动端是否需要专用卡片视图。
页面模块间距
InflyPage 的直接业务子模块默认按 16px 纵向间距排列。详情页的汇总卡、信息卡、明细列表应作为直接子模块使用,不要在业务页面通过临时 margin 重复定义模块间距。
验证
pnpm -C packages/infly-react-ui test --run
pnpm -C packages/infly-react-ui typecheck公共组件变更必须保持旧调用兼容,并为新增行为补充 Testing Library 测试。
