前言
公司用的 react + antd 的技术栈。因为antd自带的组件并不是很灵活。所以团队封装了了类似FormItem表单项的组件,方便使用。
比如,公司有下面的交互

这个页面,有一个渠道表单项,点击它,弹出下拉列表

并支持搜索

选择以及后续打开回填


渠道筛选项TXChannelPickerItem
首先该组件的使用直接套用在 Form.Item 里面即可,值直接由Form表单记录,使用的时候可以抛去心智负担
tsx
<Form.Item
name="channelId"
className="required_form_item"
label="渠道"
rules={[{ required: true, message: "请选择渠道" }]}
clickable
>
<TXChannelPickerItem />
</Form.Item>
);
TXChannelPickerItem 里面分为两部分,一部分就是表单项
的形式,一部分就是底部弹出层
tsx
export interface ITXChannelPickerItemProps {
value?: ITreeItem[];
onChange?: (v?: ITreeItem[]) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
transformData?: (value: TRecord[]) => IOption[];
/** @param 额外参数 */
extraReq?: Partial<IReqBusinessV1ChannelGetPage>;
/**@param 初始化时,是否需要回填任意一条数据 */
needBackFill?: boolean;
}
export const TXChannelPickerItem = function TXChannelPickerItem_(props: ITXChannelPickerItemProps) {
return (
<>
{/* 表单项内容 */}
<div></div>
{/* 底部弹出层内容 */}
<ChannelPicker/>
</>
);
};
先看表单项
内容
tsx
<>
{/* 上 */}
<div
className=" absolute top-0 left-0 w-full h-full opacity-0"
onClick={() => {
if (disabled) {
return;
}
channelRef.current?.openModal({
initValue: value,
});
}}
></div>
{/* 下 */}
<div className={className}>
<div className="w-max max-w-full overflow-hidden flex">
<span
className={classNames({
"text-[#ccccd6]": true,
hidden: !!text,
})}
>
{placeholder}
</span>
<div
className={classNames({
"flex-1 wes": true,
hidden: !text,
})}
>
{text}
</div>
</div>
</div>
</>;
上部分是用绝对定位盖在了筛选项上,同时满足一些情况下禁用的业务逻辑
下部分就是placeHolder和选择的值
然后再来看 弹出层
内容
tsx
const channelRef = useRef<IChannelPickerRef>(null);
<ChannelPicker
{...rest}
ref={channelRef}
onSelect={(_, full) => {
onChange?.(full);
}}
extraReq={{
enableFlag: true,
...extraReq,
}}
/>;
弹出层用的是antd的PickerView或者CascaderView实现的,弹出层组件内部会维护状态和相关业务逻辑。然后直接通过props将状态向父组件暴露
继续看ChannelPicker
内部是怎么划分的
tsx
// 选广告渠道
import { observer, useSyncProps, useWhen } from "@quarkunlimit/qu-mobx";
import { Popup } from "antd-mobile";
import { forwardRef, useImperativeHandle } from "react";
import { IChannelPickerProps, IChannelPickerRef } from "./interface";
import { PickerContent } from "./modules/PickerContent";
import { SearchRow } from "./modules/SearchRow";
import { TopOption } from "./modules/TopOption";
import { Provider, useStore } from "./store/RootStore";
const ChannelPicker = observer(
forwardRef<IChannelPickerRef, IChannelPickerProps>(
function ChannelPicker_(props, ref) {
const root = useStore();
useSyncProps(root, Object.keys(props), props);
const { logic } = root;
useImperativeHandle(ref, () => {
return {
openModal: logic.openModal,
closeModal: logic.closeModal,
};
});
useWhen(
() => true,
() => {
logic.init();
}
);
return (
<Popup
visible={logic.open}
bodyStyle={{
height: "70vh",
display: "flex",
flexDirection: "column",
}}
onMaskClick={logic.closeModal}
>
<TopOption />
<SearchRow />
<PickerContent />
</Popup>
);
}
)
);
export default observer(
forwardRef<IChannelPickerRef, IChannelPickerProps>(
function ChannelPickerPage(props, ref) {
return (
<Provider>
<ChannelPicker {...props} ref={ref} />
</Provider>
);
}
)
);
- 初始化时 init 调接口
- DOM结构分为顶部操作按钮、搜索框、数据源
其中 PickerContent 是通过CascaderView实现, 当然你也可以使用 PickerView 实现。PickerView还可以自定义渲染每一行的内容。而CascaderView只能通过 options 配置项形式渲染
tsx
import { observer } from "@quarkunlimit/qu-mobx";
import { useStore } from "../store/RootStore";
import { CascaderView } from "antd-mobile";
export const PickerContent = observer(function PickerContent_() {
const root = useStore();
const { logic, computed } = root;
return (
<CascaderView
style={{
"--height": "calc(70vh - 130px)",
}}
className="flex-1"
value={logic.value}
options={logic.dataSource}
loading={computed.loading}
onChange={logic.onChangeValue}
/>
);
});
至此,一个类似于Select交互操作的H5表单项就封装完成了。需要使用时,直接套用在Form.Item里面即可。
公司其余的组件也是这样的方式实现的
整体代码
tsx
import ChannelPicker from "@/pages/Picker/ChannelPicker";
import { IChannelPickerRef } from "@/pages/Picker/ChannelPicker/interface";
import { IReqBusinessV1ChannelGetPage } from "@/service/business/v1/channel/get-page";
import { IOption, ITreeItem, TRecord } from "@/utils/interface";
import { classNames } from "@/utils/tools";
import { useRef } from "react";
export interface ITXChannelPickerItemProps {
value?: ITreeItem[];
onChange?: (v?: ITreeItem[]) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
transformData?: (value: TRecord[]) => IOption[];
/** @param 额外参数 */
extraReq?: Partial<IReqBusinessV1ChannelGetPage>;
/**@param 是否需要回填任意一条数据 */
needBackFill?: boolean;
}
export const TXChannelPickerItem = function TXChannelPickerItem_(
props: ITXChannelPickerItemProps
) {
const {
value = [],
onChange,
placeholder = "请选择渠道",
disabled,
className,
extraReq,
...rest
} = props;
const channelRef = useRef<IChannelPickerRef>(null);
let text = "";
if (value.length > 0) {
text = value[value.length - 1].label;
}
return (
<>
<div
className=" absolute top-0 left-0 w-full h-full opacity-0"
onClick={() => {
if (disabled) {
return;
}
channelRef.current?.openModal({
initValue: value,
});
}}
></div>
<div className={className}>
<div className="w-max max-w-full overflow-hidden flex">
<span
className={classNames({
"text-[#ccccd6]": true,
hidden: !!text,
})}
>
{placeholder}
</span>
<div
className={classNames({
"flex-1 wes": true,
hidden: !text,
})}
>
{text}
</div>
</div>
</div>
<ChannelPicker
{...rest}
ref={channelRef}
onSelect={(_, full) => {
onChange?.(full);
}}
extraReq={{
enableFlag: true,
...extraReq,
}}
/>
</>
);
};
tsx
// 选广告渠道
import { observer, useSyncProps, useWhen } from "@quarkunlimit/qu-mobx";
import { Popup } from "antd-mobile";
import { forwardRef, useImperativeHandle } from "react";
import { IChannelPickerProps, IChannelPickerRef } from "./interface";
import { PickerContent } from "./modules/PickerContent";
import { SearchRow } from "./modules/SearchRow";
import { TopOption } from "./modules/TopOption";
import { Provider, useStore } from "./store/RootStore";
const ChannelPicker = observer(
forwardRef<IChannelPickerRef, IChannelPickerProps>(
function ChannelPicker_(props, ref) {
const root = useStore();
useSyncProps(root, Object.keys(props), props);
const { logic } = root;
useImperativeHandle(ref, () => {
return {
openModal: logic.openModal,
closeModal: logic.closeModal,
};
});
useWhen(
() => true,
() => {
logic.init();
}
);
return (
<Popup
visible={logic.open}
bodyStyle={{
height: "70vh",
display: "flex",
flexDirection: "column",
}}
onMaskClick={logic.closeModal}
>
<TopOption />
<SearchRow />
<PickerContent />
</Popup>
);
}
)
);
export default observer(
forwardRef<IChannelPickerRef, IChannelPickerProps>(
function ChannelPickerPage(props, ref) {
return (
<Provider>
<ChannelPicker {...props} ref={ref} />
</Provider>
);
}
)
);