conditional-selection 1.0.0 → 1.0.2

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.
@@ -1,83 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import type {
3
- TConditionalSelection,
4
- TConditionalSelectionDisabledProps,
5
- } from '../types';
6
-
7
- interface IConditionalHandleProps {
8
- level: number;
9
- currentItemInfo: TConditionalSelection;
10
- isLast: boolean;
11
- disabledConfig: Required<TConditionalSelectionDisabledProps>;
12
- onCreate: () => void;
13
- onCreateChildRules: () => void;
14
- onDel: () => void;
15
- }
16
-
17
- function isEmptyValue(val: unknown): boolean {
18
- if (val === null || val === undefined || val === '') return true;
19
- if (Array.isArray(val)) return val.length === 0;
20
- if (val !== null && typeof val === 'object') return Object.keys(val as object).length === 0;
21
- return false;
22
- }
23
-
24
- function allHave(individual?: Record<string, any>): boolean {
25
- return (
26
- !!individual &&
27
- Object.keys(individual).length > 0 &&
28
- Object.keys(individual).every((key: string) => !isEmptyValue(individual[key]))
29
- );
30
- }
31
-
32
- /**
33
- * 规则操作按钮组(同级/子集/删除)
34
- */
35
- const ConditionalHandle: React.FC<IConditionalHandleProps> = ({
36
- level,
37
- currentItemInfo,
38
- isLast,
39
- disabledConfig,
40
- onCreate,
41
- onCreateChildRules,
42
- onDel,
43
- }) => {
44
- /** 是否显示同级新增按钮:当前节点 level >= 1 且是最后一个 */
45
- const showCreate = useMemo(() => currentItemInfo.level >= 1 && isLast, [currentItemInfo.level, isLast]);
46
-
47
- /** 是否显示子集新增按钮:全局 level > 1 且当前节点 level 未达到上限 */
48
- const showCreateChild = useMemo(() => level > 1 && currentItemInfo.level < level, [level, currentItemInfo.level]);
49
-
50
- /** 整体新增按钮是否显示:level > 1 && 当前条件已填写完整 && 未禁用新增 */
51
- const createShow = useMemo(
52
- () => !!(level > 1 && allHave(currentItemInfo.individual) && !disabledConfig.addItem),
53
- [level, currentItemInfo.individual, disabledConfig.addItem],
54
- );
55
-
56
- return (
57
- <div className="conditional-handle">
58
- {createShow && (
59
- <>
60
- {showCreate && (
61
- <span className="create-handle" onClick={onCreate}>
62
- <span style={{ color: '#c85000', marginRight: 4 }}>+</span>
63
- 同级
64
- </span>
65
- )}
66
- {showCreateChild && (
67
- <span className="create-handle" onClick={onCreateChildRules}>
68
- <span style={{ color: '#c85000', marginRight: 4 }}>+</span>
69
- 子级
70
- </span>
71
- )}
72
- </>
73
- )}
74
- {!disabledConfig.delItem && (
75
- <span className="del-handle" onClick={onDel}>
76
-
77
- </span>
78
- )}
79
- </div>
80
- );
81
- };
82
-
83
- export default ConditionalHandle;
@@ -1,234 +0,0 @@
1
- import { useMemo, useCallback } from 'react';
2
- import { useImmer } from 'use-immer';
3
- import { cloneDeep } from 'lodash-es';
4
- import {
5
- EConditionalSelectionLink,
6
- EConditionalSelectionFramework,
7
- type TConditionalSelection,
8
- type TConditionalSelectionProps,
9
- type TConditionalSelectionDisabledProps,
10
- } from '../types';
11
-
12
- function isObject(val: unknown): val is Record<string, any> {
13
- return val !== null && typeof val === 'object' && !Array.isArray(val);
14
- }
15
-
16
- function isEmptyValue(val: unknown): boolean {
17
- if (val === null || val === undefined || val === '') return true;
18
- if (Array.isArray(val)) return val.length === 0;
19
- if (isObject(val)) return Object.keys(val).length === 0;
20
- return false;
21
- }
22
-
23
- export function getTempRules(level = 0): TConditionalSelection {
24
- return cloneDeep({
25
- _id: crypto.randomUUID(),
26
- framework: EConditionalSelectionFramework.INDIVIDUAL,
27
- // INDIVIDUAL 节点只保留 individual,不初始化 group
28
- individual: {},
29
- level,
30
- }) as TConditionalSelection;
31
- }
32
-
33
- export function isGroup(ruleData: TConditionalSelection): boolean {
34
- return ruleData.framework === EConditionalSelectionFramework.GROUP;
35
- }
36
-
37
- function allHave(individual?: Record<string, any>): boolean {
38
- return (
39
- !!individual &&
40
- Object.keys(individual).length > 0 &&
41
- Object.keys(individual).every((key: string) => !isEmptyValue(individual[key]))
42
- );
43
- }
44
-
45
- function checkData(data: TConditionalSelection): boolean {
46
- if (isGroup(data) && Array.isArray(data.group)) {
47
- // GROUP 节点:递归检查 group 数组中的每个子项
48
- return data.group.some(item => checkData(item));
49
- }
50
- // INDIVIDUAL 节点:检查 individual 是否填写完整
51
- return !allHave(data.individual);
52
- }
53
-
54
- function subtractLevel(rulesData: TConditionalSelection): void {
55
- // 只有 GROUP 节点才有 group 数组
56
- if (isGroup(rulesData) && Array.isArray(rulesData.group) && rulesData.group.length > 1) {
57
- rulesData.group.forEach(item => subtractLevel(item));
58
- }
59
- rulesData.level = rulesData.level - 1 < 0 ? 0 : rulesData.level - 1;
60
- }
61
-
62
- /** 在树中按 _id 深度优先查找节点 */
63
- export function findNode(root: TConditionalSelection, id: string): TConditionalSelection | null {
64
- if (root._id === id) return root;
65
- if (Array.isArray(root.group)) {
66
- for (const child of root.group) {
67
- const found = findNode(child, id);
68
- if (found) return found;
69
- }
70
- }
71
- return null;
72
- }
73
-
74
- export interface IUseConditionalHandleReturn {
75
- rulesData: TConditionalSelection;
76
- setRulesData: (data: TConditionalSelection) => void;
77
- updateRulesData: (updater: (draft: TConditionalSelection) => void) => void;
78
- level: number;
79
- disabledConfig: Required<TConditionalSelectionDisabledProps>;
80
- createRules: (ruleData: TConditionalSelection) => void;
81
- createChildRules: (ruleData: TConditionalSelection) => void;
82
- delRules: (ruleData: TConditionalSelection, index: number) => void;
83
- getConditionalSelectionData: (validate?: boolean) => Promise<TConditionalSelection>;
84
- isGroup: (ruleData: TConditionalSelection) => boolean;
85
- getTempRules: (level?: number) => TConditionalSelection;
86
- }
87
-
88
- export function useConditionalHandle(props: TConditionalSelectionProps): IUseConditionalHandleReturn {
89
- const [rulesData, updateRulesData] = useImmer<TConditionalSelection>(() => getTempRules());
90
-
91
- // 对外保持 setRulesData 接口不变(直接替换整棵树)
92
- const setRulesData = useCallback((data: TConditionalSelection) => {
93
- updateRulesData(() => data);
94
- }, [updateRulesData]);
95
-
96
- // level 计算
97
- const level = useMemo(() => {
98
- const v = Math.abs(Number(props.maxLevel ?? 1)) || 1;
99
- if (Number(props.maxLevel ?? 1) < 1) {
100
- console.warn('[TConditionalSelection] Invalid props.maxLevel: maxLevel must be greater than 0.');
101
- }
102
- return v;
103
- }, [props.maxLevel]);
104
-
105
- // disabledConfig 计算
106
- const disabledConfig = useMemo<Required<TConditionalSelectionDisabledProps>>(() => {
107
- const isObj = isObject(props.disabled);
108
- return {
109
- addItem: !!(isObj ? (props.disabled as TConditionalSelectionDisabledProps)?.addItem : props.disabled),
110
- delItem: !!(isObj ? (props.disabled as TConditionalSelectionDisabledProps)?.delItem : props.disabled),
111
- linkChange: !!(isObj ? (props.disabled as TConditionalSelectionDisabledProps)?.linkChange : props.disabled),
112
- };
113
- }, [props.disabled]);
114
-
115
- // 创建同级规则:向当前 GROUP 节点的 group 数组追加一个新 INDIVIDUAL 节点
116
- const createRules = useCallback(
117
- (ruleData: TConditionalSelection) => {
118
- if (!isGroup(ruleData)) return;
119
- const maxLen = Math.abs(Number(props.zeroLevelMaxLength));
120
- if (maxLen) {
121
- if (Number(props.zeroLevelMaxLength) < 2) {
122
- console.warn(
123
- '[TConditionalSelection] Invalid props.zeroLevelMaxLength: zeroLevelMaxLength must be greater than 1.',
124
- );
125
- }
126
- if (ruleData.level === 0 && (ruleData.group as TConditionalSelection[]).length === maxLen) {
127
- console.warn(`最外层子项最多添加${maxLen}个条件`);
128
- return;
129
- }
130
- }
131
- updateRulesData(draft => {
132
- const target = findNode(draft, ruleData._id);
133
- if (target && Array.isArray(target.group)) {
134
- target.group.push(getTempRules(target.level + 1));
135
- }
136
- });
137
- },
138
- [props.zeroLevelMaxLength, updateRulesData],
139
- );
140
-
141
- // 创建子级规则:将当前 INDIVIDUAL 节点升级为 GROUP 节点
142
- const createChildRules = useCallback(
143
- (ruleData: TConditionalSelection) => {
144
- updateRulesData(draft => {
145
- const target = findNode(draft, ruleData._id);
146
- if (!target) return;
147
-
148
- if (target.framework === EConditionalSelectionFramework.INDIVIDUAL) {
149
- const firstChild: TConditionalSelection = {
150
- _id: crypto.randomUUID(),
151
- framework: EConditionalSelectionFramework.INDIVIDUAL,
152
- individual: cloneDeep(target.individual ?? {}),
153
- level: target.level + 1,
154
- };
155
- const secondChild = getTempRules(target.level + 1);
156
- target.framework = EConditionalSelectionFramework.GROUP;
157
- target.link = EConditionalSelectionLink.AND;
158
- target._id = crypto.randomUUID();
159
- target.group = [firstChild, secondChild];
160
- delete target.individual;
161
- } else {
162
- // 已是 GROUP,追加子项
163
- if (Array.isArray(target.group)) {
164
- target.group.push(getTempRules(target.level + 1));
165
- }
166
- }
167
- });
168
- },
169
- [updateRulesData],
170
- );
171
-
172
- // 删除规则
173
- const delRules = useCallback((ruleData: TConditionalSelection, index: number) => {
174
- updateRulesData(draft => {
175
- const target = findNode(draft, ruleData._id);
176
- if (!target || !Array.isArray(target.group)) return;
177
-
178
- target.group.splice(index, 1);
179
-
180
- if (target.framework === EConditionalSelectionFramework.GROUP && target.group.length === 1) {
181
- const onlyChild = target.group[0];
182
- if (
183
- onlyChild.framework === EConditionalSelectionFramework.GROUP &&
184
- Array.isArray(onlyChild.group) &&
185
- onlyChild.group.length > 1
186
- ) {
187
- // 唯一子项是 GROUP,提升到当前节点
188
- target.framework = EConditionalSelectionFramework.GROUP;
189
- target.link = onlyChild.link;
190
- target._id = onlyChild._id;
191
- target.group = onlyChild.group;
192
- delete target.individual;
193
- } else {
194
- // 唯一子项是 INDIVIDUAL,降级
195
- target.framework = EConditionalSelectionFramework.INDIVIDUAL;
196
- target._id = onlyChild._id;
197
- target.individual = cloneDeep(onlyChild.individual ?? {});
198
- delete target.link;
199
- delete target.group;
200
- }
201
- subtractLevel(onlyChild);
202
- }
203
- });
204
- }, [updateRulesData]);
205
-
206
- // 获取并校验数据
207
- const getConditionalSelectionData = useCallback(
208
- (validate = true): Promise<TConditionalSelection> => {
209
- return new Promise((resolve, reject) => {
210
- if (validate && checkData(rulesData)) {
211
- console.error('请填写完整');
212
- reject('含有未填写完整的项');
213
- return;
214
- }
215
- resolve(cloneDeep(rulesData));
216
- });
217
- },
218
- [rulesData],
219
- );
220
-
221
- return {
222
- rulesData,
223
- setRulesData,
224
- level,
225
- disabledConfig,
226
- createRules,
227
- createChildRules,
228
- delRules,
229
- getConditionalSelectionData,
230
- isGroup,
231
- getTempRules,
232
- updateRulesData,
233
- };
234
- }
@@ -1,109 +0,0 @@
1
- import { useEffect, useRef, useImperativeHandle, forwardRef, useCallback } from 'react';
2
- import { cloneDeep } from 'lodash-es';
3
- import ConditionalContent from './components/conditional-content';
4
- import { useConditionalHandle, getTempRules, isGroup, findNode } from './composables/useConditionalHandle';
5
- import { type TConditionalSelectionProps } from './types';
6
- import './index.less';
7
-
8
- export type { TConditionalSelectionProps };
9
-
10
- const ConditionalSelection = forwardRef<
11
- { getConditionalSelectionData: (validate?: boolean) => Promise<any> },
12
- TConditionalSelectionProps
13
- >((props, ref) => {
14
- const { conditionalRules, onChange, renderConditionRules, renderCreateCondition } = props;
15
-
16
- const { rulesData, setRulesData, level, disabledConfig, createRules, createChildRules, delRules, getConditionalSelectionData, updateRulesData } =
17
- useConditionalHandle(props);
18
-
19
- // 暴露 getConditionalSelectionData 给父组件
20
- useImperativeHandle(ref, () => ({ getConditionalSelectionData }), [getConditionalSelectionData]);
21
-
22
- // 监听 rulesData 变化并抛出 onChange
23
- const isFirstRender = useRef(true);
24
- useEffect(() => {
25
- if (isFirstRender.current) {
26
- isFirstRender.current = false;
27
- return;
28
- }
29
- onChange?.(rulesData);
30
- }, [rulesData]);
31
-
32
- //初始化
33
- useEffect(() => {
34
- if (conditionalRules && Object.keys(conditionalRules).length !== 0) {
35
- setRulesData(cloneDeep(conditionalRules));
36
- } else {
37
- setRulesData(getTempRules());
38
- }
39
- }, []);
40
-
41
- // 递归/slot相关 handler 用 useCallback 包裹,依赖项只放 updateRulesData/findNode/renderConditionRules
42
- const handleUpdateNode = useCallback((nodeId: string, patch: Partial<any>) => {
43
- updateRulesData(draft => {
44
- const target = findNode(draft, nodeId);
45
- if (!target) return;
46
- Object.assign(target, patch);
47
- });
48
- }, [updateRulesData]);
49
-
50
- const handleRenderConditionItem = useCallback(
51
- (item: any) => renderConditionRules?.(item, (val: Record<string, any>) => {
52
- updateRulesData(draft => {
53
- const target = findNode(draft, item._id);
54
- if (!target) return;
55
- target.individual = val;
56
- });
57
- }),
58
- [renderConditionRules, updateRulesData]
59
- );
60
-
61
- const handleRenderConditionRules = useCallback(
62
- (rulesData: any, change: (val: Record<string, any>) => void) =>
63
- renderConditionRules?.(rulesData, change),
64
- [renderConditionRules]
65
- );
66
-
67
- return (
68
- <div className="conditional-selection">
69
- {/* 新增条件按钮区域 */}
70
- {!disabledConfig.addItem &&
71
- (renderCreateCondition ? (
72
- renderCreateCondition({ createRules: createChildRules, rulesData })
73
- ) : (
74
- <span className="create-box" onClick={() => createChildRules(rulesData)}>
75
- <span style={{ color: '#00c8be', marginRight: 4 }}>➕</span>
76
- 添加条件
77
- </span>
78
- ))}
79
-
80
- {/* 规则内容区域 */}
81
- <div className="conditional-content">
82
- {isGroup(rulesData) && (rulesData.group as any[]).length > 0 ? (
83
- <ConditionalContent
84
- levelData={rulesData}
85
- level={level}
86
- disabledConfig={disabledConfig}
87
- onUpdateNode={handleUpdateNode}
88
- onCreateRules={createRules}
89
- onCreateChildRules={createChildRules}
90
- onDelRules={delRules}
91
- renderConditionItem={handleRenderConditionItem}
92
- />
93
- ) : (
94
- handleRenderConditionRules(rulesData, (val: Record<string, any>) => {
95
- updateRulesData(draft => {
96
- const target = findNode(draft, rulesData._id);
97
- if (!target) return;
98
- target.individual = val;
99
- });
100
- })
101
- )}
102
- </div>
103
- </div>
104
- );
105
- });
106
-
107
- ConditionalSelection.displayName = 'ConditionalSelection';
108
-
109
- export default ConditionalSelection;
@@ -1,91 +0,0 @@
1
- .conditional-selection {
2
- width: 100%;
3
-
4
- .create-box {
5
- display: inline-block;
6
- margin-bottom: 8px;
7
- cursor: pointer;
8
- }
9
-
10
- .conditional-content {
11
- width: 100%;
12
- }
13
- }
14
-
15
- .relative-box {
16
- position: relative;
17
- width: 100%;
18
-
19
- .conditional-list-box {
20
- flex: 1;
21
- }
22
-
23
- .conditional-item {
24
- display: flex;
25
- align-items: center;
26
- margin-top: 8px;
27
- }
28
-
29
- .conditional-list:first-child {
30
- margin-top: -8px;
31
- }
32
-
33
- .right-box {
34
- margin-left: 32px;
35
- }
36
- }
37
-
38
- .link-line {
39
- position: absolute;
40
- z-index: 999;
41
- height: 100%;
42
- margin: 0 4px;
43
-
44
- .link-label {
45
- position: absolute;
46
- top: 50%;
47
- width: 22px;
48
- color: #c85000;
49
- text-align: center;
50
- cursor: pointer;
51
- user-select: none;
52
- background-color: #f9e5e5;
53
- border-radius: 4px;
54
- transform: translateY(-50%);
55
- }
56
- }
57
-
58
- .link-line::before {
59
- position: absolute;
60
- top: 0;
61
- bottom: 0;
62
- left: 10px;
63
- width: 2px;
64
- content: '';
65
- background-color: #c85000;
66
- opacity: 0.2;
67
- }
68
-
69
- .conditional-handle {
70
- display: flex;
71
- gap: 8px;
72
- align-items: center;
73
-
74
- .create-handle {
75
- display: inline-block;
76
- width: 50px;
77
- cursor: pointer;
78
- font-size: 14px;
79
- }
80
-
81
- .del-handle {
82
- display: inline-block;
83
- font-size: 14px;
84
- width: 24px;
85
- cursor: pointer;
86
- }
87
-
88
- .del-handle:hover {
89
- color: red;
90
- }
91
- }
@@ -1,5 +0,0 @@
1
- import ConditionalSelection from './conditional-selection';
2
- export * from './types';
3
- export {
4
- ConditionalSelection
5
- }
@@ -1,53 +0,0 @@
1
- import type { ReactNode } from 'react';
2
-
3
- export enum EConditionalSelectionFramework {
4
- /** 内容组 */
5
- GROUP = 'group',
6
- /** 内容 */
7
- INDIVIDUAL = 'individual',
8
- }
9
-
10
- export enum EConditionalSelectionLink {
11
- AND = 'and',
12
- OR = 'or',
13
- }
14
-
15
- export type TConditionalSelection<T = any> = {
16
- _id: string;
17
- /** 结构标识 */
18
- framework: EConditionalSelectionFramework;
19
- /** 内容关系 */
20
- link?: EConditionalSelectionLink;
21
- group?: TConditionalSelection<T>[];
22
- individual?: Record<string, any>;
23
- level: number;
24
- }
25
-
26
- export type TConditionalSelectionDisabledProps = {
27
- addItem?: boolean;
28
- delItem?: boolean;
29
- linkChange?: boolean;
30
- };
31
-
32
- export type TConditionalSelectionProps = {
33
- /** 数据内容 */
34
- conditionalRules?: TConditionalSelection | null;
35
- /** deep 层级限制,需大于0,默认1 */
36
- maxLevel?: number;
37
- /** 最外层子项数量限制,需大于1,默认不限 */
38
- zeroLevelMaxLength?: number;
39
- /** 是否禁用新增、删除、关系变更 */
40
- disabled?: boolean | TConditionalSelectionDisabledProps;
41
- /** 数据变更回调 */
42
- onChange?: (value: TConditionalSelection | undefined) => void;
43
- /** 渲染条件行内容的插槽,参数为当前 INDIVIDUAL 节点和变更 handler */
44
- renderConditionRules?: (
45
- conditionRulesData: TConditionalSelection,
46
- change: (data: Record<string, any>) => void
47
- ) => ReactNode;
48
- /** 渲染创建条件按钮插槽(createCondition) */
49
- renderCreateCondition?: (params: {
50
- createRules: (ruleData: TConditionalSelection) => void;
51
- rulesData: TConditionalSelection;
52
- }) => ReactNode;
53
- };
package/packages/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from './ConditionalSelection'
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "allowSyntheticDefaultImports": true,
7
- "jsx": "preserve",
8
- "jsxImportSource": "react",
9
- "strict": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true
12
- },
13
- "include": ["packages/**/*"],
14
- "references": [{ "path": "./tsconfig.node.json" }]
15
- }
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "allowSyntheticDefaultImports": true,
7
- "allowImportingTsExtensions": true,
8
- "strict": true,
9
- "skipLibCheck": true,
10
- "emitDeclarationOnly": true,
11
- "types": ["node"],
12
- "composite": true
13
- },
14
- "include": ["vite.config.ts"]
15
- }
package/vite.config.ts DELETED
@@ -1,39 +0,0 @@
1
- import { defineConfig } from 'vite';
2
- import react from '@vitejs/plugin-react-swc'
3
- import path from 'node:path'
4
- import dts from 'vite-plugin-dts'
5
- export default defineConfig(({ command }) => {
6
- const isDev = command === 'serve'
7
- return {
8
- plugins: [react(), dts({
9
- outDir: 'dist/types',
10
- insertTypesEntry: true, //这是dts插件的配置,用于生成类型文件
11
- include: ['packages/**/*.ts', 'packages/**/*.tsx'],
12
- rollupTypes: true
13
- })],
14
- root: isDev ? path.resolve(__dirname, 'example') : undefined,
15
- server: {
16
- port: 3000,
17
- open: true,
18
- },
19
- build: {
20
- outDir: path.resolve(__dirname, 'dist'),
21
- lib: {
22
- entry: path.resolve(__dirname, 'packages/index.ts'),
23
- name: 'ui',
24
- formats: ['es', 'umd', 'cjs', 'iife'],
25
- fileName: (format) => `ui.${format}.js`,
26
- },
27
- emptyOutDir: false,
28
- rollupOptions: {
29
- external: ['react', 'react-dom'],
30
- output: {
31
- globals: {
32
- react: 'React',
33
- 'react-dom': 'ReactDOM',
34
- },
35
- },
36
- },
37
- },
38
- }
39
- });