diff --git a/package.json b/package.json index 56960c68..4977b339 100644 --- a/package.json +++ b/package.json @@ -84,9 +84,11 @@ "leva": "^0.9.35", "lodash.flatten": "^4.4.0", "lodash.get": "^4.4.2", + "lodash.isarray": "^4.0.0", "lodash.isempty": "^4.4.0", "lodash.isequal": "^4.5.0", "lodash.isnil": "^4.0.0", + "lodash.isobject": "^3.0.2", "lodash.merge": "^4.6.2", "lodash.omitby": "^4.6.0", "lodash.template": "^4.5.0", diff --git a/src/utils/c2d2c.tsx b/src/utils/c2d2c.tsx index c5caf3e0..d9e6ad60 100644 --- a/src/utils/c2d2c.tsx +++ b/src/utils/c2d2c.tsx @@ -1,6 +1,9 @@ import { JSONSchema } from '@/types/schema'; +import isArray from 'lodash.isarray'; import isEmpty from 'lodash.isempty'; +import isEqual from 'lodash.isequal'; import isNil from 'lodash.isnil'; +import isObject from 'lodash.isobject'; import omitBy from 'lodash.omitby'; import uniq from 'lodash.uniq'; import { ReactNodeElement } from '../types'; @@ -20,6 +23,79 @@ export const getDefaultValueFromSchema = (schema: JSONSchema) => { return schema.default; }; +/** + * 获取去重后的 props + * @param props + * @param schema + */ +export const getDiffPropsWithSchema = (props: any, schema?: JSONSchema) => { + // 如果没有 schema,则使用原来的 props + if (!schema) return props; + + const defaultProps = getDefaultValueFromSchema(schema); + + if (!defaultProps) return props; + + const filtered = Object.entries(props) + // 过滤掉默认值 + .filter((entry) => { + const [key, value] = entry; + + const defaultPropsValue = defaultProps[key]; + + // 如果该属性在默认值中不存在 + if (typeof defaultProps[key] === 'undefined') return true; + + // 或者与默认值不相等 + return !isEqual(defaultPropsValue, value); + }); + + return Object.fromEntries(filtered); +}; + +/** + * 根据组件名称、props 生成 props 和 schema生成symbolMaster名称 + */ +export const getSymbolMasterNameFromProps = ( + pkg: string, + component: string, + props: any, + schema?: JSONSchema, +) => { + const validProps = getDiffPropsWithSchema(props, schema); + + // 用一个递归方法来层层结构生成对象名称 + const genName = (propsObj: object, parentKey?: string): string => { + return Object.entries(propsObj) + .map((entry) => { + const [key, value] = entry; + + // 针对数组,需要结构它的内部对象 + if (isArray(value)) { + return `${key}=[${value.map((item) => `{${genName(item)}}`).join(',')}]`; + } + + // 嵌套对象 递归解析 + if (isObject(value)) { + return genName(value, key); + } + + // undefined 和 null 直接过滤 + if (isNil(value)) { + return ''; + } + // 普通的其他值直接返回 + return `${parentKey ? `${parentKey}.` : ''}${key}=${value}`; + }) + .filter((i) => i) + .join(','); + }; + + const propsStr = genName(validProps); + + return `${pkg.replace('/', '-')}/${component}/${propsStr}`; +}; + /** * 获取组件库导入代码 */ diff --git a/src/utils/tests/c2d2c.test.tsx b/src/utils/tests/c2d2c.test.tsx index 6bcf7051..320610ea 100644 --- a/src/utils/tests/c2d2c.test.tsx +++ b/src/utils/tests/c2d2c.test.tsx @@ -3,8 +3,10 @@ import { generateImportCode, generateJSXCode, getDefaultValueFromSchema, + getDiffPropsWithSchema, + getSymbolMasterNameFromProps, } from '../c2d2c'; -import { schema } from './schema'; +import { buttonSchema, menuSchema, radioGroup, schema, tagSchema } from './schema'; describe('getDefaultValueFromSchema', () => { it('获取默认值', () => { @@ -25,6 +27,105 @@ describe('getDefaultValueFromSchema', () => { }); }); +describe('getDiffPropsWithSchema', () => { + it('简单版本', () => { + const props = { checked: false, children: '未选中项', disabled: false }; + + expect(getDiffPropsWithSchema(props, radioGroup as any)).toEqual({ + children: '未选中项', + }); + }); + it('没有 schema的情况', () => { + const props = { checked: false, children: '未选中项', disabled: false }; + + expect(getDiffPropsWithSchema(props)).toEqual(props); + }); + it('schema 为空的情况', () => { + const props = { checked: false, children: '未选中项', disabled: false }; + + expect(getDiffPropsWithSchema(props, { type: 'object' })).toEqual(props); + }); +}); + +describe('getSymbolMasterNameFromProps', () => { + it('正常生成', () => { + const name = getSymbolMasterNameFromProps( + 'antd', + 'Button', + { + danger: false, + isSubmit: false, + children: '主按钮', + ghost: false, + size: 'middle', + type: 'dashed', + shape: '', + disabled: false, + alignment: { vertical: 'top', horizontal: 'left' }, + resize: { widthFollow: 'self', heightFollow: 'self' }, + }, + buttonSchema, + ); + + expect(name).toEqual( + 'antd/Button/children=主按钮,type=dashed,alignment.vertical=top,alignment.horizontal=left,resize.widthFollow=self,resize.heightFollow=self', + ); + }); + it('包含 null 或者 undefined 的 props 需要被过滤', () => { + const name = getSymbolMasterNameFromProps( + 'antd', + 'Tag', + { + color: null, + closable: true, + a: undefined, + children: '标签123', + alignment: { vertical: 'top', horizontal: 'left' }, + resize: { widthFollow: 'self', heightFollow: 'self' }, + }, + tagSchema, + ); + + expect(name).toEqual( + 'antd/Tag/closable=true,children=标签123,alignment.vertical=top,alignment.horizontal=left,resize.widthFollow=self,resize.heightFollow=self', + ); + }); + + it('包含数组、嵌套对象的处理方法', () => { + const props = { + size: 'large', + mode: 'vertical', + theme: 'dark', + items: [ + { label: '菜单项1', key: '2', disabled: false }, + { label: '菜单项2', key: '1', disabled: false }, + { label: '菜单项3', key: '3' }, + { label: '菜单项4' }, + { label: '' }, + ], + }; + const name = getSymbolMasterNameFromProps('antd', 'Menu', props, menuSchema); + expect(name).toEqual( + 'antd/Menu/size=large,theme=dark,items=[{label=菜单项1,key=2,disabled=false},{label=菜单项2,key=1,disabled=false},{label=菜单项3,key=3},{label=菜单项4},{label=}]', + ); + }); + + it('没有schema时,也可以正常生成', () => { + const name = getSymbolMasterNameFromProps('antd', 'Button', { + danger: false, + children: '主按钮', + size: 'middle', + shape: null, + a: undefined, + alignment: { vertical: 'top', horizontal: 'left' }, + }); + + expect(name).toEqual( + 'antd/Button/danger=false,children=主按钮,size=middle,alignment.vertical=top,alignment.horizontal=left', + ); + }); +}); + describe('generateImportCode', () => { it('合成代码', () => { const code = generateImportCode('antd', ['Button', 'Button', 'Alert']); diff --git a/src/utils/tests/schema.ts b/src/utils/tests/schema.ts index 88ffb61b..98d585f0 100644 --- a/src/utils/tests/schema.ts +++ b/src/utils/tests/schema.ts @@ -76,3 +76,367 @@ export const schema: any = { }, }, }; + +export const radioGroup = { + properties: { + checked: { + enumOptions: [], + title: '选中状态', + renderType: 'radioGroup', + type: 'boolean', + description: '"是否选中"', + tags: [], + default: false, + }, + disabled: { + title: '禁用状态', + renderType: 'radioGroup', + type: 'boolean', + description: '"是否禁用"', + default: false, + }, + children: { + className: 'ReactNode', + title: '内容', + renderType: 'string', + type: 'reactNode', + description: '"选项文本"', + default: '', + }, + }, + className: 'RadioProps', + type: 'object', + required: [], +}; + +export const buttonSchema: any = { + properties: { + danger: { + enumOptions: [ + { label: '正常', value: false }, + { label: '危险', value: true }, + ], + title: '操作意图', + renderType: 'radioGroup', + type: 'boolean', + group: 'type', + tags: { + renderType: 'radioGroup', + title: '操作意图', + default: 'false', + enumOptions: '[{"label": "正常", value: false },{"label": "危险", value: true}]', + category: 'type', + }, + default: false, + }, + style: { + tags: { ignore: '' }, + className: 'CSSProperties', + type: 'object', + hidden: true, + }, + isSubmit: { + tags: { default: 'false', ignore: '' }, + default: false, + type: 'boolean', + hidden: true, + }, + children: { + group: 'content', + className: 'ReactNode', + renderOptions: { + placeholder: '空值将无法正常显示', + autoFocus: true, + }, + title: '文本', + renderType: 'string', + type: 'reactNode', + description: '设置按钮文本', + tags: { + renderType: 'string', + title: '文本', + default: '""', + description: '设置按钮文本', + category: 'content', + renderOptions: '{ placeholder: "空值将无法正常显示", autoFocus:true }', + }, + default: '', + }, + loading: { + enumOptions: [ + { label: '默认', value: false }, + { label: '加载中', value: true }, + ], + oneOf: [ + { + properties: { delay: { type: 'number' } }, + className: '__type', + type: 'object', + required: [], + }, + { type: 'boolean' }, + ], + title: '加载状态', + renderType: 'radioGroup', + enum: [false, true], + group: 'status', + tags: { + category: 'status', + enumOptions: '[{"label": "默认", value: false },{"label": "加载中", value: true}]', + title: '加载状态', + renderType: 'radioGroup', + enumName: '["正常","加载中"]', + enum: '[false , true]', + default: 'false', + }, + default: false, + }, + size: { + group: 'style', + className: 'SizeType', + enumOptions: [ + { label: '大', value: 'large' }, + { label: '中', value: 'middle' }, + { label: '小', value: 'small' }, + ], + title: '大小', + renderType: 'radioGroup', + type: 'string', + enum: ['small', 'middle', 'large'], + tags: { + renderType: 'radioGroup', + title: '大小', + enumOptions: + '[{"label":"大","value": "large"},{"label":"中","value":"middle"},{"label":"小","value":"small"}]', + default: '"middle"', + category: 'style', + }, + default: 'middle', + }, + ghost: { + enumOptions: [ + { label: '默认', value: false }, + { label: '透明', value: true }, + ], + title: '底色', + renderType: 'radioGroup', + type: 'boolean', + group: 'style', + tags: { + renderType: 'radioGroup', + title: '底色', + default: 'false', + enumOptions: '[{"label": "默认", value: false },{"label": "透明", value: true}]', + category: 'style', + }, + default: false, + }, + type: { + group: 'type', + renderOptions: { layout: 'vertical' }, + title: '类型', + renderType: 'radioGroup', + enumNames: ['强调', '默认', '虚线', '链接', '文本'], + type: 'string', + enum: ['primary', 'default', 'dashed', 'link', 'text'], + tags: { + category: 'type', + renderOptions: '{ "layout": "vertical" }', + title: '类型', + renderType: 'radioGroup', + enumNames: '["强调", "默认", "虚线", "链接", "文本"]', + enum: '["primary","default","dashed","link","text"]', + default: '"default"', + }, + default: 'default', + }, + shape: { + group: 'style', + enumOptions: [ + { label: '默认', value: '' }, + { label: '圆形', value: 'circle' }, + { label: '胶囊', value: 'round' }, + ], + renderOptions: { validate: false }, + title: '形状', + renderType: 'radioGroup', + type: 'string', + enum: ['default', 'circle', 'round'], + tags: { + renderType: 'radioGroup', + title: '形状', + default: '""', + enumOptions: + '[{"label": "默认", value: "" },{"label": "圆形", value: \'circle\'},{"label": "胶囊", value: \'round\'}]', + category: 'style', + renderOptions: '{"validate": false }', + }, + default: '', + }, + disabled: { + enumOptions: [ + { label: '未禁用', value: false }, + { label: '禁用', value: true }, + ], + title: '禁用状态', + renderType: 'radioGroup', + type: 'boolean', + group: 'status', + tags: { + renderType: 'radioGroup', + title: '禁用状态', + default: 'false', + enumOptions: '[{"label": "未禁用", value: false },{"label": "禁用", value: true}]', + category: 'status', + }, + default: false, + }, + }, + className: 'ButtonProps', + type: 'object', + required: [], +}; + +export const tagSchema: any = { + properties: { + closeIcon: { + tags: { + title: '自定义关闭图标', + ignore: '', + description: '"设置自定义关闭图标"', + }, + className: 'ReactNode', + title: '自定义关闭图标', + type: 'reactNode', + description: '"设置自定义关闭图标"', + hidden: true, + }, + closeable: { + renderType: 'radioGroup', + tags: { + renderType: 'radioGroup', + title: '关闭图标', + default: 'false', + enumOptions: '[{label:"隐藏",value:false},{label:"显示",value: true}]', + }, + title: '关闭图标', + default: false, + type: 'boolean', + enumOptions: [ + { label: '隐藏', value: false }, + { label: '显示', value: true }, + ], + }, + children: { + className: 'ReactNode', + renderOptions: { allowClear: true }, + title: '文本', + renderType: 'string', + type: 'reactNode', + description: '"标签文本"', + tags: { + renderType: 'string', + title: '文本', + default: '""', + description: '"标签文本"', + renderOptions: '{ allowClear: true }', + }, + default: '', + }, + color: { + enum: [ + '', + 'green', + 'blue', + 'red', + 'gold', + 'yellow', + 'orange', + 'purple', + 'geekblue', + 'magenta', + 'volcano', + 'cyan', + 'lime', + 'pink', + ], + renderOptions: { validate: false }, + title: '颜色', + renderType: 'select', + enumNames: [ + '默认', + '绿色', + '蓝色', + '红色', + '金色', + '橙色', + '黄色', + '紫色', + '深蓝色', + '洋红', + '深橙色', + '青色', + '柠檬绿', + '粉色', + ], + type: 'string', + description: '"设置按钮内容"', + tags: { + renderOptions: '{"validate": false }', + title: '颜色', + renderType: 'select', + enumNames: + '["默认", "绿色", "蓝色", "红色", "金色","橙色","黄色", "紫色","深蓝色","洋红","深橙色","青色","柠檬绿", "粉色"]', + description: '"设置按钮内容"', + enum: '["","green","blue","red","gold","yellow","orange","purple", "geekblue", "magenta", "volcano", "cyan", "lime","pink"]', + default: '""', + }, + default: '', + }, + }, + className: 'TagProps', + type: 'object', + required: ['color', 'closeable'], +}; + +export const menuSchema: any = { + properties: { + items: { + type: 'array', + title: '菜单项', + items: { + type: 'object', + properties: { + label: { + type: 'string', + title: '标题', + renderProps: { placeholder: '请输入标题' }, + }, + disabled: { type: 'boolean', renderType: 'boolean' }, + key: { type: 'string' }, + }, + renderOptions: { exposedCnt: 2 }, + }, + }, + theme: { + title: '主题', + default: 'light', + enumOptions: [ + { value: 'light', label: '亮色' }, + { value: 'dark', label: '暗色' }, + ], + }, + mode: { + title: '模式', + default: 'vertical', + enumOptions: [ + { value: 'vertical', label: '垂直' }, + { value: 'horizontal', label: '水平' }, + { value: 'inline', label: '内嵌' }, + ], + }, + }, + className: 'MenuProps', + type: 'object', + required: [], +};