@hzab/form-render-mobile 0.0.20 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +1 -1
- package/package.json +1 -1
- package/src/components/Checkbox/index.tsx +147 -0
- package/src/components/Radio/index.tsx +70 -0
- package/src/components/UserSelect/index.module.less +61 -0
- package/src/components/UserSelect/index.tsx +197 -0
- package/src/components/index.ts +3 -0
- package/src/index.less +4 -0
- package/src/index.tsx +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Checkbox as AntdCheckbox, Selector, List, Space } from "antd-mobile";
|
|
3
|
+
import { connect, useField, useFieldSchema } from "@formily/react";
|
|
4
|
+
import type { ISchema } from "@formily/react";
|
|
5
|
+
import { CheckboxProps, CheckboxGroupProps } from "antd-mobile/es/components/checkbox";
|
|
6
|
+
|
|
7
|
+
const checkboxColorMap = {
|
|
8
|
+
defaultColor: "#f5f5f5",
|
|
9
|
+
defaultTextColor: "#333333",
|
|
10
|
+
allCheckedColor: "#186DDA",
|
|
11
|
+
allCheckedTextColor: "#ffffff",
|
|
12
|
+
someCheckedColor: "#e7f1ff",
|
|
13
|
+
someCheckedTextColor: "#1677ff",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const CustomCheckbox: React.FC<CheckboxProps> = (props) => {
|
|
17
|
+
const field: any = useField();
|
|
18
|
+
return (
|
|
19
|
+
<AntdCheckbox
|
|
20
|
+
{...props}
|
|
21
|
+
checked={field.value.includes(props.value)}
|
|
22
|
+
onChange={() => {
|
|
23
|
+
const newValue = field.value.includes(props.value)
|
|
24
|
+
? field.value.filter((v: any) => v !== props.value)
|
|
25
|
+
: [...field.value, props.value];
|
|
26
|
+
field.setValue(newValue);
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
interface ICustomCheckboxGroupProps extends CheckboxGroupProps {
|
|
33
|
+
optionType?: "default" | "button";
|
|
34
|
+
indeterminate?: boolean; // 新增属性,用于控制是否显示半选状态
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CustomCheckboxGroup: React.FC<ICustomCheckboxGroupProps> = ({
|
|
38
|
+
children,
|
|
39
|
+
optionType,
|
|
40
|
+
indeterminate,
|
|
41
|
+
...props
|
|
42
|
+
}) => {
|
|
43
|
+
const field = useField<any>();
|
|
44
|
+
const fieldSchema: ISchema = useFieldSchema();
|
|
45
|
+
const isButtonMode = optionType === "button";
|
|
46
|
+
const enumOptions = (fieldSchema?.enum as []) || [];
|
|
47
|
+
const [selectorColor, setSelectorColor] = useState("#f5f5f5");
|
|
48
|
+
|
|
49
|
+
// 计算全选和半选状态
|
|
50
|
+
const { allChecked, noneChecked, someChecked } = useMemo(() => {
|
|
51
|
+
const allChecked = enumOptions?.every((option: any) => field.value?.includes(option.value));
|
|
52
|
+
const noneChecked = enumOptions?.every((option: any) => !field.value?.includes(option.value));
|
|
53
|
+
const someChecked = !allChecked && !noneChecked;
|
|
54
|
+
return { allChecked, noneChecked, someChecked };
|
|
55
|
+
}, [field.value, enumOptions]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (allChecked) {
|
|
59
|
+
setSelectorColor(checkboxColorMap.allCheckedColor);
|
|
60
|
+
} else {
|
|
61
|
+
if (someChecked) setSelectorColor(checkboxColorMap.someCheckedColor);
|
|
62
|
+
else setSelectorColor(checkboxColorMap.defaultColor);
|
|
63
|
+
}
|
|
64
|
+
}, [allChecked, someChecked]);
|
|
65
|
+
|
|
66
|
+
// 处理全选和半选的逻辑
|
|
67
|
+
const handleSelectAll = (checked: boolean) => {
|
|
68
|
+
if (checked) {
|
|
69
|
+
field.setValue(enumOptions?.map((option: any) => option.value));
|
|
70
|
+
} else {
|
|
71
|
+
field.setValue([]);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (isButtonMode && enumOptions.length > 0) {
|
|
76
|
+
// Selector模式
|
|
77
|
+
const options = (enumOptions as []).map((item: any) => ({
|
|
78
|
+
label: item.label,
|
|
79
|
+
value: item.value,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Space className="selector" wrap justify="end" style={{ width: "100%" }}>
|
|
84
|
+
{indeterminate && (
|
|
85
|
+
// 模拟半选、全选交互
|
|
86
|
+
<Selector
|
|
87
|
+
options={[{ label: "全选", value: "1" }]}
|
|
88
|
+
value={allChecked ? ["1"] : []}
|
|
89
|
+
showCheckMark={false}
|
|
90
|
+
style={{
|
|
91
|
+
"--color": selectorColor,
|
|
92
|
+
"--text-color":
|
|
93
|
+
allChecked || someChecked ? checkboxColorMap.someCheckedTextColor : checkboxColorMap.defaultTextColor,
|
|
94
|
+
"--checked-color": selectorColor,
|
|
95
|
+
"--checked-text-color": checkboxColorMap.allCheckedTextColor,
|
|
96
|
+
}}
|
|
97
|
+
onChange={([value]) => handleSelectAll(value === "1")}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
<Selector
|
|
101
|
+
{...(props as any)}
|
|
102
|
+
multiple
|
|
103
|
+
showCheckMark={false}
|
|
104
|
+
options={options}
|
|
105
|
+
value={field.value}
|
|
106
|
+
onChange={(values) => {
|
|
107
|
+
field.setValue(values);
|
|
108
|
+
}}
|
|
109
|
+
style={{
|
|
110
|
+
"--checked-color": checkboxColorMap.allCheckedColor,
|
|
111
|
+
"--checked-text-color": checkboxColorMap.allCheckedTextColor,
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
</Space>
|
|
115
|
+
);
|
|
116
|
+
} else {
|
|
117
|
+
// 默认Checkbox模式
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
{indeterminate && (
|
|
121
|
+
<AntdCheckbox checked={allChecked} indeterminate={someChecked} onChange={(e) => handleSelectAll(e)}>
|
|
122
|
+
全选
|
|
123
|
+
</AntdCheckbox>
|
|
124
|
+
)}
|
|
125
|
+
<AntdCheckbox.Group
|
|
126
|
+
{...props}
|
|
127
|
+
value={field.value}
|
|
128
|
+
onChange={(values) => {
|
|
129
|
+
field.setValue(values);
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{(enumOptions as []).map((option: any) => (
|
|
133
|
+
<AntdCheckbox key={option.value} value={option.value}>
|
|
134
|
+
{option.label}
|
|
135
|
+
</AntdCheckbox>
|
|
136
|
+
))}
|
|
137
|
+
</AntdCheckbox.Group>
|
|
138
|
+
</>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const Checkbox: any = connect(CustomCheckbox);
|
|
144
|
+
|
|
145
|
+
Checkbox.Group = connect(CustomCheckboxGroup);
|
|
146
|
+
|
|
147
|
+
export { Checkbox };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Radio as AntdRadio, Selector } from "antd-mobile";
|
|
3
|
+
import { connect, mapProps, useField, useFieldSchema } from "@formily/react";
|
|
4
|
+
import type { ISchema } from "@formily/react";
|
|
5
|
+
import { RadioProps, RadioGroupProps } from "antd-mobile/es/components/radio";
|
|
6
|
+
|
|
7
|
+
const CustomRadio: React.FC<RadioGroupProps> = (props) => {
|
|
8
|
+
const field: any = useField();
|
|
9
|
+
return <AntdRadio {...props} checked={field.value === props.value} onChange={field.onInput} />;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
interface ICustomRadioGroupProps extends RadioGroupProps {
|
|
13
|
+
optionType?: "default" | "button";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CustomRadioGroup: React.FC<ICustomRadioGroupProps> = ({ children, optionType, ...props }) => {
|
|
17
|
+
const field = useField<any>();
|
|
18
|
+
const fieldSchema: ISchema = useFieldSchema();
|
|
19
|
+
const isButtonMode = optionType === "button";
|
|
20
|
+
|
|
21
|
+
// 获取枚举值
|
|
22
|
+
const enumOptions = fieldSchema?.enum || [];
|
|
23
|
+
|
|
24
|
+
if (isButtonMode && enumOptions.length > 0) {
|
|
25
|
+
// Selector模式
|
|
26
|
+
const options = (enumOptions as []).map((item: any) => ({
|
|
27
|
+
label: item.label,
|
|
28
|
+
value: item.value,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Selector
|
|
33
|
+
{...(props as any)}
|
|
34
|
+
showCheckMark={false}
|
|
35
|
+
options={options}
|
|
36
|
+
value={[field.value]}
|
|
37
|
+
onChange={(values) => {
|
|
38
|
+
field.setValue(values[0]);
|
|
39
|
+
}}
|
|
40
|
+
style={{
|
|
41
|
+
"--checked-color": "#186DDA",
|
|
42
|
+
"--checked-text-color": "#FFFFFF",
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
// 默认Radio模式
|
|
48
|
+
return (
|
|
49
|
+
<AntdRadio.Group
|
|
50
|
+
{...props}
|
|
51
|
+
value={field.value}
|
|
52
|
+
onChange={(value) => {
|
|
53
|
+
field.setValue(value);
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{(enumOptions as []).map((option: any) => (
|
|
57
|
+
<AntdRadio key={option.value} value={option.value}>
|
|
58
|
+
{option.label}
|
|
59
|
+
</AntdRadio>
|
|
60
|
+
))}
|
|
61
|
+
</AntdRadio.Group>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const Radio: any = connect(CustomRadio);
|
|
67
|
+
|
|
68
|
+
Radio.Group = connect(CustomRadioGroup);
|
|
69
|
+
|
|
70
|
+
export { Radio };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
.popup-head {
|
|
2
|
+
&-action {
|
|
3
|
+
padding: 4px;
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
justify-content: space-between;
|
|
7
|
+
border-bottom: solid 1px #eee;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.search-warp {
|
|
11
|
+
padding: 12px;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.list {
|
|
16
|
+
max-height: 40vh;
|
|
17
|
+
width: 100%;
|
|
18
|
+
overflow-y: scroll;
|
|
19
|
+
padding-bottom: 12px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.search {
|
|
23
|
+
border: 1px solid #E6E6E6;
|
|
24
|
+
border-radius: 3px;
|
|
25
|
+
height: 22px;
|
|
26
|
+
padding: 6px;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
|
|
31
|
+
& input {
|
|
32
|
+
border: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
outline: none;
|
|
35
|
+
width: 100%;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.item {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: flex-start;
|
|
42
|
+
|
|
43
|
+
&-text {
|
|
44
|
+
padding-left: 10px;
|
|
45
|
+
|
|
46
|
+
&__name {
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&__phone, &__idCard {
|
|
52
|
+
font-size: 12px;
|
|
53
|
+
color: rgba(23,26,29,0.6);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.placeholder {
|
|
59
|
+
text-align: right;
|
|
60
|
+
color: rgba(23,26,29,0.6);
|
|
61
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { Popup, CheckList, Avatar, Button, type AvatarProps } from "antd-mobile";
|
|
3
|
+
import { useField, useFieldSchema } from "@formily/react";
|
|
4
|
+
import { IFieldState } from "@formily/core";
|
|
5
|
+
|
|
6
|
+
import styles from "./index.module.less";
|
|
7
|
+
|
|
8
|
+
type AvatarSize = "large" | "small" | "default" | number;
|
|
9
|
+
|
|
10
|
+
// 用户信息接口
|
|
11
|
+
interface IUser {
|
|
12
|
+
label: string;
|
|
13
|
+
value: string | number;
|
|
14
|
+
avatar?: string; // 头像 URL,可选
|
|
15
|
+
phone?: string; // 手机号
|
|
16
|
+
idCard?: string; // 身份证号
|
|
17
|
+
[key: string]: any; // 其他可能的扩展信息
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ISearchInputProps {
|
|
21
|
+
value?: string;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
onChange?: (value: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// UserSelect组件的Props类型定义
|
|
27
|
+
interface IUserSelectProps {
|
|
28
|
+
/**是否显示头像 */
|
|
29
|
+
isAvatar?: boolean;
|
|
30
|
+
/**选中值 */
|
|
31
|
+
value?: IUser[];
|
|
32
|
+
|
|
33
|
+
/**所有用户列表 */
|
|
34
|
+
options: IUser[];
|
|
35
|
+
|
|
36
|
+
/**多选模式 */
|
|
37
|
+
mode?: "multiple";
|
|
38
|
+
|
|
39
|
+
/**是否需要搜索 */
|
|
40
|
+
showSearch?: boolean;
|
|
41
|
+
|
|
42
|
+
/**头像大小 */
|
|
43
|
+
avatarSize?: AvatarSize;
|
|
44
|
+
|
|
45
|
+
/**自定义展示信息 */
|
|
46
|
+
infoRender?: React.ReactNode;
|
|
47
|
+
|
|
48
|
+
/**自定义展示key */
|
|
49
|
+
configKey?: {
|
|
50
|
+
phoneKey?: string;
|
|
51
|
+
idCardKey?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**头像形状 */
|
|
55
|
+
shape: "circle" | "square";
|
|
56
|
+
|
|
57
|
+
/**选中项变化时 */
|
|
58
|
+
onChange?: (value: IUser[]) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const SHOW_NAME_MAX = 3;
|
|
62
|
+
|
|
63
|
+
const SearchInput: React.FC<ISearchInputProps> = (props) => {
|
|
64
|
+
const { value, onChange, ...resetProps } = props;
|
|
65
|
+
const [searchValue, setSearchValue] = useState("");
|
|
66
|
+
|
|
67
|
+
const handleChange = (e) => {
|
|
68
|
+
const value = e.target.value;
|
|
69
|
+
setSearchValue?.(value);
|
|
70
|
+
onChange?.(value);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={styles["search"]}>
|
|
75
|
+
<input type="search" {...resetProps} value={searchValue} onChange={handleChange} />
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const UserSelect: React.FC<IUserSelectProps> = (props) => {
|
|
81
|
+
const { value, onChange, options, mode, isAvatar, avatarSize = 32, infoRender, configKey, shape = "square" } = props;
|
|
82
|
+
const field: any = useField<IFieldState>();
|
|
83
|
+
const [visible, setVisible] = useState(false);
|
|
84
|
+
const [filteredUsers, setFilteredUsers] = useState(options);
|
|
85
|
+
const [values, setValues] = useState<string[]>([]);
|
|
86
|
+
|
|
87
|
+
// 处理选中项变化
|
|
88
|
+
const handleSelectChange = (selected: string[]) => {
|
|
89
|
+
const selectedUsers = options.filter((user) => selected.includes(user.value.toString()));
|
|
90
|
+
field.setValue(selectedUsers);
|
|
91
|
+
if (onChange) {
|
|
92
|
+
onChange(selectedUsers);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const moreValue = useCallback((soucre: Record<string, any>, mainKey: string, secondaryKey?: string) => {
|
|
97
|
+
if (mainKey) return soucre[mainKey];
|
|
98
|
+
|
|
99
|
+
return soucre[secondaryKey];
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
const avatarShape = useMemo(() => {
|
|
103
|
+
if (shape === "circle") return "50%";
|
|
104
|
+
if (shape === "square") return "4px";
|
|
105
|
+
|
|
106
|
+
return "4px";
|
|
107
|
+
}, [shape]);
|
|
108
|
+
|
|
109
|
+
// 渲染用户信息
|
|
110
|
+
const renderUserInfo = (user: IUser) => (
|
|
111
|
+
<div className={styles["item"]}>
|
|
112
|
+
{isAvatar && <Avatar src={user.avatar} style={{ "--size": `${avatarSize}px`, "--border-radius": avatarShape }} />}
|
|
113
|
+
{infoRender ? (
|
|
114
|
+
infoRender
|
|
115
|
+
) : (
|
|
116
|
+
<div className={styles["item-text"]}>
|
|
117
|
+
<div className={styles["item-text__name"]}>{user.lable}</div>
|
|
118
|
+
<div className={styles["item-text__phone"]}>手机号:{moreValue(user, configKey?.phoneKey, "phone")}</div>
|
|
119
|
+
<div className={styles["item-text__idCard"]}>身份证号:{moreValue(user, configKey?.phoneKey, "idCard")}</div>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// 处理搜索框文本变化
|
|
126
|
+
const handleSearch = (searchText: string) => {
|
|
127
|
+
const lowercasedFilter = searchText.toLowerCase();
|
|
128
|
+
const filteredData = options.filter((item) => {
|
|
129
|
+
return Object.keys(item).some((key) => item[key].toString().toLowerCase().includes(lowercasedFilter));
|
|
130
|
+
});
|
|
131
|
+
setFilteredUsers(filteredData);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleCancel = () => {
|
|
135
|
+
setVisible(false);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleConfirm = () => {
|
|
139
|
+
const vals: string[] = field.value.map((it: IUser) => it.lable);
|
|
140
|
+
setValues(vals);
|
|
141
|
+
setVisible(false);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const valuesJoin = useMemo(() => {
|
|
145
|
+
if (values.length >= SHOW_NAME_MAX) {
|
|
146
|
+
const vals = values.slice(0, SHOW_NAME_MAX);
|
|
147
|
+
return `${vals.join("、")}...等${values.length}人`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return values.join("、");
|
|
151
|
+
}, [values]);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<div onClick={() => setVisible(true)} className={styles["placeholder"]}>
|
|
156
|
+
{valuesJoin || "请选择"}
|
|
157
|
+
</div>
|
|
158
|
+
<Popup
|
|
159
|
+
visible={visible}
|
|
160
|
+
onMaskClick={() => setVisible(false)}
|
|
161
|
+
bodyStyle={{
|
|
162
|
+
borderTopLeftRadius: "8px",
|
|
163
|
+
borderTopRightRadius: "8px",
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<div className={styles["popup-head"]}>
|
|
167
|
+
<div className={styles["popup-head-action"]}>
|
|
168
|
+
<Button fill="none" onClick={handleCancel}>
|
|
169
|
+
取消
|
|
170
|
+
</Button>
|
|
171
|
+
<Button color="primary" fill="none" onClick={handleConfirm}>
|
|
172
|
+
确定
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
<div className={styles["search-warp"]}>
|
|
176
|
+
<SearchInput onChange={handleSearch} placeholder="请输入关键字搜索" />
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div className={styles["list"]}>
|
|
180
|
+
<CheckList
|
|
181
|
+
multiple={mode === "multiple"}
|
|
182
|
+
value={value?.map((user) => user.value.toString())}
|
|
183
|
+
onChange={handleSelectChange}
|
|
184
|
+
>
|
|
185
|
+
{filteredUsers.map((user, index) => (
|
|
186
|
+
<CheckList.Item key={user.value.toString() + index} value={user.value.toString()}>
|
|
187
|
+
{renderUserInfo(user)}
|
|
188
|
+
</CheckList.Item>
|
|
189
|
+
))}
|
|
190
|
+
</CheckList>
|
|
191
|
+
</div>
|
|
192
|
+
</Popup>
|
|
193
|
+
</>
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export default UserSelect;
|
package/src/components/index.ts
CHANGED
package/src/index.less
CHANGED