├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── main.js ├── page │ ├── DynamicFormTable.vue │ ├── components │ │ ├── DynamicForm.vue │ │ └── DynamicTable.vue │ └── utils │ │ ├── ListCompare.js │ │ ├── dynamicRenderingList.js │ │ ├── flattenResult.js │ │ ├── getIDsAndProps.js │ │ ├── helper.js │ │ └── index.js └── plugins │ └── element.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 前言 4 | 不知道大家有没有一个同感?天下产品一大抄,简直比程序员的CV大法还厉害! 5 | 6 | 产品一张图,交互全凭自己意会,比如产品经理常说的一句话:"你参考一下某夕夕,某猫,某东",实际上我们没有它们的后台账号,他就点了两下给我们看。 7 | 8 | ## 效果 9 | 言归正传,想必每一家电商公司,都有自己的商品中心,今天又懒得加班了,我带你实现商品中心的SKU,效果大致如下: 10 | 11 | ![](https://user-gold-cdn.xitu.io/2020/3/24/1710d35d038d617b?w=694&h=848&f=gif&s=178636) 12 | 13 | 看到这个效果,首先要找准算法方向,一是全组合,二是对比,大致从以下两点入手: 14 | 15 | ### 算法一:多数组实现全组合,要求如下: 16 | 17 | ![](https://user-gold-cdn.xitu.io/2020/3/25/1710d5eeb20ff3a2?w=1256&h=1346&f=png&s=253100) 18 | 19 | ### 算法二:两数组对比,要求如下: 20 | ![](https://user-gold-cdn.xitu.io/2020/3/25/1710d5f158bdd0c6?w=1308&h=1562&f=png&s=254263) 21 | 22 | ### 大佬看到这么清晰的要求,估计想法:"好简单!"。其实很多东西别人帮你理清楚了当然就觉得简单,实际操作时还是会棘手的。不过,我想在这儿讲个故事,就是下图啦! 23 | 24 | ![](https://user-gold-cdn.xitu.io/2020/3/25/1710d615901ccb61?w=790&h=2294&f=jpeg&s=219232) 25 | 26 | ### 算法一的实现如下: 27 | ![](https://user-gold-cdn.xitu.io/2020/3/25/1710d5f4b38bcf25?w=1312&h=914&f=png&s=182811) 28 | 29 | ### 算法二的实现如下: 30 | ![](https://user-gold-cdn.xitu.io/2020/3/25/1710d5f59ac341b1?w=1582&h=950&f=png&s=228244) 31 | 32 | 33 | ## 源代码 34 | ### 目录结构大致如下: 35 | ![](https://user-gold-cdn.xitu.io/2020/3/25/1710d8f7cde03747?w=966&h=760&f=png&s=127183) 36 | 37 | # dynamic-form-table 38 | ## Project setup 39 | ``` 40 | yarn install 41 | ``` 42 | 43 | ### Compiles and hot-reloads for development 44 | ``` 45 | yarn run serve 46 | ``` 47 | 48 | ### Compiles and minifies for production 49 | ``` 50 | yarn run build 51 | ``` 52 | 53 | ### Run your tests 54 | ``` 55 | yarn run test 56 | ``` 57 | 58 | ### Lints and fixes files 59 | ``` 60 | yarn run lint 61 | ``` -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamic-form-table", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.4", 12 | "element-ui": "^2.4.5", 13 | "vue": "^2.6.11" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^4.2.0", 17 | "@vue/cli-plugin-eslint": "^4.2.0", 18 | "@vue/cli-service": "^4.2.0", 19 | "babel-eslint": "^10.0.3", 20 | "eslint": "^6.7.2", 21 | "eslint-plugin-vue": "^6.1.2", 22 | "vue-cli-plugin-element": "^1.0.1", 23 | "vue-template-compiler": "^2.6.11" 24 | }, 25 | "eslintConfig": { 26 | "root": true, 27 | "env": { 28 | "node": true 29 | }, 30 | "extends": [ 31 | "plugin:vue/essential", 32 | "eslint:recommended" 33 | ], 34 | "parserOptions": { 35 | "parser": "babel-eslint" 36 | }, 37 | "rules": { 38 | "no-console":"off", 39 | "no-irregular-whitespace":"off" 40 | } 41 | }, 42 | "browserslist": [ 43 | "> 1%", 44 | "last 2 versions" 45 | ] 46 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TJH0420/Dynamic-Form-Table/efdcbac0666953bced389a89074facea5483d4ab/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TJH0420/Dynamic-Form-Table/efdcbac0666953bced389a89074facea5483d4ab/src/assets/logo.png -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import './plugins/element.js' 4 | 5 | Vue.config.productionTip = false 6 | 7 | new Vue({ 8 | render: h => h(App), 9 | }).$mount('#app') 10 | -------------------------------------------------------------------------------- /src/page/DynamicFormTable.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 183 | 184 | 185 | 187 | -------------------------------------------------------------------------------- /src/page/components/DynamicForm.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 98 | 99 | 101 | -------------------------------------------------------------------------------- /src/page/components/DynamicTable.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 150 | 151 | 152 | 154 | -------------------------------------------------------------------------------- /src/page/utils/ListCompare.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @description 重新选择后比较 相同保留 多的添加 少的删除 4 | * 5 | * @param list1 旧表 6 | * @param list2 新表 7 | */ 8 | export default function ListCompare(list1 = [], list2 = []) { 9 | const hash = new Map(); 10 | list2.forEach(({ tableEnName }, index) => hash.set(tableEnName, index)); 11 | const result = []; // 相同的保留 12 | result.push(...list1.filter(({ tableEnName }) => hash.has(tableEnName))); 13 | hash.clear(); 14 | list1.forEach(({ tableEnName }, index) => hash.set(tableEnName, index)); 15 | result.push(...list2.filter(({ tableEnName }) => !hash.has(tableEnName))); 16 | return result; 17 | } -------------------------------------------------------------------------------- /src/page/utils/dynamicRenderingList.js: -------------------------------------------------------------------------------- 1 | import { deepCloneHandler, hasOwnProperty } from './helper' 2 | 3 | /** 4 | * @description 根据选择的表单值 排列组合成 动态渲染出list 5 | * 6 | * @param defaultSelectedList 表单选择的结果List 7 | * @param defaultTableColumnList 默认表头 8 | * @param nameEn 英语名 9 | * @param nameLo 本地名 10 | */ 11 | export default function dynamicRenderingList({ defaultSelectedList = [], defaultTableColumnList = [], nameEn = '', nameLo = '' }) { 12 | const defaultSelectedListCopy = deepCloneHandler(defaultSelectedList); 13 | console.log('defaultSelectedListCopy', defaultSelectedListCopy) 14 | //判断arr内attributeSeleted为空情况 15 | if (isAllEmpty(defaultSelectedListCopy)) { 16 | return []; 17 | } 18 | 19 | // 组合成 tableShowList 20 | let queue = []; 21 | getQueue({}, 0, defaultSelectedListCopy, queue); 22 | 23 | //对str适当处理 24 | setQueue(queue, defaultTableColumnList, nameEn, nameLo) 25 | return queue; 26 | } 27 | 28 | function isAllEmpty(arr) { 29 | let emptyNum = 0; 30 | arr.forEach(item => { 31 | if (!item.attributeSeleted.length) { 32 | item.attributeSeleted.push(""); 33 | emptyNum++; 34 | } 35 | }); 36 | console.log(emptyNum === arr.length); 37 | return emptyNum === arr.length ? true : false; 38 | } 39 | 40 | function setQueue(queue, defaultTableColumnList, nameEn, nameLo) { 41 | queue.forEach(item => { 42 | let tmp = ""; 43 | for (let name in item) { 44 | if (hasOwnProperty(item, name)) tmp += item[name]; 45 | } 46 | defaultTableColumnList.forEach(e => { 47 | if (e.prop === "tableEnName") item[e.prop] = nameEn + tmp; 48 | else if (e.prop === "tableLocalName") item[e.prop] = nameLo + tmp; 49 | else item[e.prop] = ""; 50 | }); 51 | }); 52 | } 53 | 54 | function getQueue(obj, i, arr, queue) { 55 | let arrLen = arr.length 56 | for (let j = 0; j < arr[i].attributeSeleted.length; j++) { 57 | if (i < arrLen - 1) { 58 | obj[arr[i].attributeId] = arr[i].attributeSeleted[j]; 59 | getQueue(obj, i + 1, arr, queue); 60 | } else { 61 | obj[arr[i].attributeId] = arr[i].attributeSeleted[j]; 62 | console.log("here", obj); 63 | queue.push(deepCloneHandler(obj)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/page/utils/flattenResult.js: -------------------------------------------------------------------------------- 1 | import { deepCloneHandler } from './helper' 2 | /** 3 | * @description 结果扁平化 和 表单结果填充 4 | * 5 | * @param resultList 返回的结果 6 | * @param defaultFormList 动态生成表单的List 7 | */ 8 | export default function flattenResult(resultList = [], defaultFormList = []) { 9 | const copyResultList = deepCloneHandler(resultList); 10 | const copyDefaultFormList = deepCloneHandler(defaultFormList); 11 | const map = new Map(); 12 | const valHash = new Set() 13 | copyResultList.forEach(item => { 14 | // 把值都拿出来 O(m*n) 15 | const { tableListIDs = [] } = item; 16 | tableListIDs.forEach(({ attributeId: id, attributeValues: value }) => { 17 | // 有值没存过 18 | if (map.has(id) && !valHash.has(value)) { 19 | valHash.add(value) 20 | map.get(id).push(value); 21 | } else if (!map.has(id)) { 22 | valHash.add(value) 23 | map.set(id, [value]); 24 | } 25 | item[id] = value; 26 | }); 27 | delete item.tableListIDs; 28 | }); 29 | // 表单结果填充 30 | copyDefaultFormList.forEach(item => { 31 | if (map.has(item.attributeId)) { 32 | item.attributeSeleted = [...map.get(item.attributeId)]; 33 | } 34 | }); 35 | return { copyResultList, copyDefaultFormList }; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/page/utils/getIDsAndProps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 获得转换 和 获得动态表头 3 | * 4 | * @param defaultFormList 动态生成表单的List 5 | */ 6 | export default function getIDsAndProps(defaultFormList = []) { 7 | const defaultTableIDs = []; 8 | const defaultTableProps = []; 9 | defaultFormList.forEach(element => { 10 | // 获得转换defaultTableIDs 11 | defaultTableIDs.push(element.attributeId); 12 | // 获得动态表头defaultTableProps 13 | defaultTableProps.push({ 14 | attributeId: element.attributeId, 15 | attributeName: element.attributeName 16 | }); 17 | }); 18 | return { defaultTableIDs, defaultTableProps }; 19 | } -------------------------------------------------------------------------------- /src/page/utils/helper.js: -------------------------------------------------------------------------------- 1 | export const deepCloneHandler = arr => { 2 | return JSON.parse(JSON.stringify(arr)); 3 | }; 4 | 5 | export const hasOwnProperty = () => Object.prototype.hasOwnProperty.call; -------------------------------------------------------------------------------- /src/page/utils/index.js: -------------------------------------------------------------------------------- 1 | import dynamicRenderingList from './dynamicRenderingList' 2 | import ListCompare from './ListCompare' 3 | import { deepCloneHandler, hasOwnProperty } from './helper' 4 | import flattenResult from './flattenResult' 5 | import getIDsAndProps from './getIDsAndProps' 6 | 7 | export { 8 | dynamicRenderingList, 9 | ListCompare, 10 | deepCloneHandler, 11 | hasOwnProperty, 12 | flattenResult, 13 | getIDsAndProps 14 | } 15 | 16 | export default { 17 | dynamicRenderingList, 18 | ListCompare, 19 | deepCloneHandler, 20 | hasOwnProperty, 21 | flattenResult, 22 | getIDsAndProps 23 | } -------------------------------------------------------------------------------- /src/plugins/element.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Element from 'element-ui' 3 | import 'element-ui/lib/theme-chalk/index.css' 4 | 5 | Vue.use(Element) 6 | --------------------------------------------------------------------------------