├── .eslintignore ├── .npmrc ├── src ├── components │ ├── zx-color-picker │ │ ├── readme.md │ │ └── color-picker.vue │ ├── zx-form-daterange │ │ ├── readme.md │ │ └── daterange.vue │ ├── commons │ │ ├── regexp.js │ │ ├── index.js │ │ ├── url.js │ │ ├── file.js │ │ ├── margin-padding.scss │ │ ├── utils.js │ │ ├── is.js │ │ ├── compress-image.js │ │ └── common.scss │ ├── zx-upload │ │ ├── zx-image-upload │ │ │ ├── readme.md │ │ │ ├── template.vue │ │ │ └── image-upload.js │ │ ├── zx-file-upload │ │ │ ├── readme.md │ │ │ ├── template.vue │ │ │ └── file-upload.js │ │ └── utils │ │ │ ├── cosUpload.js │ │ │ └── cos-auth.js │ ├── zx-validator │ │ ├── util.js │ │ ├── readme.md │ │ ├── directive.js │ │ ├── rules.js │ │ └── validator.vue │ ├── vue-json-form │ │ ├── objec-path.js │ │ ├── prefix-suffix.vue │ │ ├── readme.md │ │ ├── template.vue │ │ └── form.js │ ├── index.js │ ├── zx-form-item │ │ ├── readme.md │ │ ├── template.vue │ │ └── form-item.js │ ├── zx-add-minus │ │ ├── readme.md │ │ ├── add-minus.vue │ │ └── script.js │ └── zx-select-remote │ │ ├── readme.md │ │ ├── style.scss │ │ └── select-remote.vue ├── assets │ └── vue.png ├── App.vue ├── main.js ├── pages │ ├── AppHome.vue │ ├── cityData.js │ ├── form-demo3.vue │ ├── form-demo6.vue │ ├── form-demo5.vue │ ├── form-demo4-async-valid.vue │ ├── form-demo7.vue │ └── form-demo1.vue └── router │ └── index.js ├── public ├── favicon.ico └── index.html ├── babel.config.js ├── env ├── env.dev.js ├── env.ut.js ├── env.prod.js └── env.test.js ├── .editorconfig ├── .prettierrc.json ├── .gitignore ├── lib └── vue-json-form │ ├── package.json │ ├── README.md │ └── index.css ├── vue.config.js ├── LICENSE ├── .eslintrc ├── rollup.config.js ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org -------------------------------------------------------------------------------- /src/components/zx-color-picker/readme.md: -------------------------------------------------------------------------------- 1 | ## 颜色选择组件 2 | 在原UI的右边加上input框 -------------------------------------------------------------------------------- /src/components/zx-form-daterange/readme.md: -------------------------------------------------------------------------------- 1 | ## 为什么要写这个组件 2 | 在表单配置中为了方便的支持startField和endField -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springalskey/vue-json-form/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springalskey/vue-json-form/HEAD/src/assets/vue.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /env/env.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: 'dev', 3 | routerBase: '/', 4 | publicPath: '/', 5 | }; 6 | -------------------------------------------------------------------------------- /env/env.ut.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: 'ut', 3 | routerBase: '/', 4 | publicPath: '/', 5 | }; 6 | -------------------------------------------------------------------------------- /env/env.prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: 'prod', 3 | routerBase: '/', 4 | publicPath: '/', 5 | }; 6 | -------------------------------------------------------------------------------- /env/env.test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: 'dev', 3 | routerBase: '/', 4 | publicPath: '/', 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/commons/regexp.js: -------------------------------------------------------------------------------- 1 | export default { 2 | URL: /^(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/ 3 | }; 4 | -------------------------------------------------------------------------------- /src/components/zx-upload/zx-image-upload/readme.md: -------------------------------------------------------------------------------- 1 | ## 文件上传 2 | ## example 3 | 4 | ```html 5 | 6 | ``` 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "htmlWhitespaceSensitivity": "ignore", 7 | "arrowParens": "avoid", 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/components/zx-validator/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断值是否为空 3 | */ 4 | export function isEmpty(value) { 5 | return typeof value === 'undefined' || value === '' || value === null; 6 | } 7 | 8 | export function trim(val) { 9 | if (typeof val === 'string') { 10 | return val.trim(); 11 | } 12 | return val; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | .history 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? -------------------------------------------------------------------------------- /src/components/zx-upload/zx-file-upload/readme.md: -------------------------------------------------------------------------------- 1 | ## 文件上传 2 | 3 | ```html 4 | 5 | ``` 6 | 7 | ## video example 8 | 9 | ```html on-completed = {videoUrl,coverImgUrl,videoWidth,videoHeight} 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import './components'; 3 | import App from '@/App'; 4 | import router from '@/router'; 5 | import ElementUI from 'element-ui'; 6 | import 'element-ui/lib/theme-chalk/index.css'; 7 | 8 | Vue.config.productionTip = false; 9 | 10 | Vue.use(ElementUI, { size: 'small' }); 11 | 12 | new Vue({ 13 | el: '#app', 14 | router, 15 | render: h => h(App) 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/zx-upload/utils/cosUpload.js: -------------------------------------------------------------------------------- 1 | class CosUpload { 2 | async uploadFile(options) { 3 | // 上传文件 4 | if (window.uploadFile) { 5 | let result = await window.uploadFile(options); 6 | return result; 7 | } 8 | return { 9 | // 上传后的URL 10 | path: 'https://esys-1254463895.cos.ap-shanghai.myqcloud.com/biz/2023/04/27/6993ad27447d02285c4b800ade982ec1.jpeg', 11 | // 如果是视频返回封面 12 | poster: '' 13 | }; 14 | } 15 | } 16 | 17 | export default new CosUpload(); 18 | -------------------------------------------------------------------------------- /src/components/commons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import './common.scss'; 3 | import is from './is'; 4 | import * as url from './url'; 5 | import * as utils from './utils'; 6 | import * as file from './file'; 7 | 8 | const tools = Object.assign(utils, url, file); 9 | 10 | // 封装插件 11 | Vue.use({ 12 | install(Vue) { 13 | Vue.prototype.$$is = is; 14 | Vue.prototype.$$utils = tools; 15 | Vue.prototype.$$log = (...args) => { 16 | console.log(...JSON.parse(JSON.stringify(args))); 17 | }; 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /lib/vue-json-form/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-json/vue-json-form", 3 | "version": "0.0.2", 4 | "private": false, 5 | "author": "springalsky@gmail.com", 6 | "homepage": "https://github.com/springalskey/vue-json-form", 7 | "main": "index.js", 8 | "license": "MIT", 9 | "files": [ 10 | "*" 11 | ], 12 | "peerDependencies": { 13 | "vue": "2.6.11", 14 | "element-ui": "2.15.1", 15 | "crypto-js": "3.1.2" 16 | }, 17 | "scripts": {}, 18 | "description": "vue-json-form base on Vue2 and ElementUI" 19 | } 20 | -------------------------------------------------------------------------------- /src/components/vue-json-form/objec-path.js: -------------------------------------------------------------------------------- 1 | export function setValueByObjectPath(obj, path, value) { 2 | let a = path.split('.'); 3 | let o = obj; 4 | while (a.length - 1) { 5 | let n = a.shift(); 6 | if (!(n in o)) o[n] = {}; 7 | o = o[n]; 8 | } 9 | o[a[0]] = value; 10 | } 11 | 12 | export function getValueByObjectPath(obj, path) { 13 | path = path.replace(/\[(\w+)\]/g, '.$1'); 14 | path = path.replace(/^\./, ''); 15 | let a = path.split('.'); 16 | let o = obj; 17 | while (a.length) { 18 | let n = a.shift(); 19 | if (!(n in o)) return; 20 | o = o[n]; 21 | } 22 | return o; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/zx-validator/readme.md: -------------------------------------------------------------------------------- 1 | ## 内置校验规则 2 | 3 | required,max,min,maxlen,minlen,url,email,money,mobile,regexp 4 | 除了 required 的验证规则,其他的规则当值等于[null, undefined, '']会返回 true,校验通过。如果需要校验空值,则需要添加 required=true。 5 | 6 | ## 自定义组件如何在验证”失败时显示红色外边框“ 7 | 8 | 含以下情况时红色 border 将会被作用(优先级按照以下顺序): 9 | 10 | 1. class="form-valid-failure-redborder" 11 | 2. class="el-select-selection" 12 | 3. 标签 13 | 14 | ## 默认错误信息展示位置 15 | 16 | :validator="item.label" 17 | :field="item.field" 18 | v-bind="item.rules" // 等等多个验证规则 19 | 将错误信息插入到同时“包含以上这些属性的标签”的最后。 20 | 21 | ## 组件内置展示错误信息的插槽 22 | 23 | form-items 底部包含

,放置错误提示语。假设是自定义组件,该组件中拥有多个表单元素,应当在该组件内部使用 form 包裹起来 24 | 25 | ## demo 26 | 27 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const $env = require(path.resolve('./env/env.' + process.env.ZX_ENV + '.js')); 4 | 5 | module.exports = { 6 | devServer: { 7 | open: true, 8 | hot: true, 9 | compress: true, 10 | disableHostCheck: true, 11 | historyApiFallback: true, 12 | overlay: { 13 | warnings: false, 14 | errors: false 15 | } 16 | }, 17 | // 保存时不校验eslint 18 | lintOnSave: false, 19 | publicPath: $env.publicPath, 20 | productionSourceMap: process.env.ZX_ENV !== 'prod', 21 | configureWebpack: { 22 | performance: { 23 | hints: false 24 | }, 25 | plugins: [ 26 | new webpack.DefinePlugin({ 27 | $ENV: JSON.stringify($env), 28 | 'process.env.ZX_ENV': JSON.stringify(process.env.ZX_ENV) 29 | }) 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/commons/url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 截取url参数转成object 3 | * @param {String} url 4 | * @return {Object} 5 | */ 6 | export function getQuery(url) { 7 | let result = {}; 8 | if (url && url.indexOf('?') !== -1) { 9 | let strs = url.substring(url.indexOf('?') + 1).split('&'); 10 | for (let i = 0; i < strs.length; i++) { 11 | let arr = strs[i].split('='); 12 | result[arr[0]] = decodeURIComponent(arr[1]); 13 | } 14 | } 15 | return result; 16 | } 17 | 18 | /** 19 | * 将object转成URL参数 20 | * @param {Object} queryObj 21 | * @return {String} '?id=1&host=2' 22 | */ 23 | export function toQuery(queryObj = {}) { 24 | let arr = []; 25 | for (let key in queryObj) { 26 | if (queryObj.hasOwnProperty(key)) { 27 | let value = encodeURIComponent(queryObj[key] || ''); 28 | arr.push(key + '=' + value); 29 | } 30 | } 31 | if (arr.length) return '?' + arr.join('&'); 32 | return ''; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/commons/file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * canvas转文件 3 | * @param {Canvas} canvas canvas对象 4 | */ 5 | export async function canvasToFile(canvas) { 6 | return new Promise((resolve, reject) => { 7 | canvas.toBlob(function (blob) { 8 | resolve(blob); 9 | }); 10 | }); 11 | } 12 | 13 | /** 14 | * 根据图片src获取图片宽高 15 | * @param {String}} src 图片src 16 | */ 17 | export function getImageSizeBySrc(src) { 18 | return new Promise((resolve, reject) => { 19 | let img = document.createElement('img'); 20 | img.src = src; 21 | img.onload = function () { 22 | let size = { 23 | width: this.width, 24 | height: this.height 25 | }; 26 | resolve(size); 27 | }; 28 | img.onerror = function () { 29 | reject(new Error('load error')); 30 | }; 31 | }); 32 | } 33 | 34 | // 返回.png|.pm4 35 | export function getFileExt(fileName) { 36 | let ext = fileName.substring(fileName.lastIndexOf('.')); 37 | return ext; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/zx-upload/zx-file-upload/template.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 41 | -------------------------------------------------------------------------------- /src/pages/AppHome.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-present, Quan Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "parser": "babel-eslint" 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 12 | "extends": ["plugin:vue/essential", "standard"], 13 | // required to lint *.vue files 14 | "plugins": ["import", "vue", "html"], 15 | // add your custom rules here 16 | "rules": { 17 | "camelcase": [0, { "properties": "never" }], 18 | "no-new": 0, 19 | "no-new-func": 0, 20 | "no-param-reassign": 0, 21 | "generator-star-spacing": "off", 22 | "no-tabs": "off", 23 | "vue/no-parsing-error": [2, { "x-invalid-end-tag": false }], 24 | "space-before-function-paren": 0, 25 | "object-curly-spacing": 0, 26 | "semi": ["error", "always"], 27 | "no-console": 0, 28 | "no-debugger": 0, 29 | "no-useless-escape": "off", 30 | "no-return-assign": "off", 31 | "no-proto": "off", 32 | "prefer-promise-reject-errors": "off", 33 | "import/no-duplicates": "off" 34 | }, 35 | "globals": { 36 | "$ENV": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/commons/margin-padding.scss: -------------------------------------------------------------------------------- 1 | // class= "p-r-1 p-t-1 p-b-1 p-l-1" 2 | // 生产0-30px 3 | 4 | @for $i from 0 through 30 { 5 | .p-t-#{$i} { 6 | padding-top: $i + px !important; 7 | } 8 | .p-b-#{$i} { 9 | padding-bottom: $i + px !important; 10 | } 11 | .p-l-#{$i} { 12 | padding-left: $i + px !important; 13 | } 14 | .p-r-#{$i} { 15 | padding-right: $i + px !important; 16 | } 17 | } 18 | 19 | // class= "m-r-1 m-t-1 m-b-1 m-l-1" 20 | // 生产0-30px 21 | @for $i from 0 through 30 { 22 | .m-t-#{$i} { 23 | margin-top: $i + px !important; 24 | } 25 | .m-b-#{$i} { 26 | margin-bottom: $i + px !important; 27 | } 28 | .m-l-#{$i} { 29 | margin-left: $i + px !important; 30 | } 31 | .m-r-#{$i} { 32 | margin-right: $i + px !important; 33 | } 34 | } 35 | 36 | // padding 37 | .p-0 { 38 | padding: 0; 39 | } 40 | .p-10 { 41 | padding: 10px; 42 | } 43 | .p-15 { 44 | padding: 15px; 45 | } 46 | .p-20 { 47 | padding: 20px; 48 | } 49 | .p-30 { 50 | padding: 30px; 51 | } 52 | 53 | // margin 54 | .p-0 { 55 | margin: 0; 56 | } 57 | .m-10 { 58 | margin: 10px; 59 | } 60 | .m-15 { 61 | margin: 15px; 62 | } 63 | .m-20 { 64 | margin: 20px; 65 | } 66 | .m-30 { 67 | margin: 30px; 68 | } -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import './commons/index'; 3 | import { definition } from './zx-validator/directive'; 4 | import Validator from './zx-validator/validator'; 5 | import VueJsonForm from './vue-json-form/template'; 6 | import ImageUpload from './zx-upload/zx-image-upload/template'; 7 | import FileUpload from './zx-upload/zx-file-upload/template'; 8 | import SelectRemote from './zx-select-remote/select-remote'; 9 | import FormItem from './zx-form-item/template'; 10 | import AddMinus from './zx-add-minus/add-minus'; 11 | import ColorPicker from './zx-color-picker/color-picker'; 12 | import Daterange from './zx-form-daterange/daterange'; 13 | 14 | Vue.component(Validator.name, Validator); 15 | Vue.component(VueJsonForm.name, VueJsonForm); 16 | Vue.component(ImageUpload.name, ImageUpload); 17 | Vue.component(FileUpload.name, FileUpload); 18 | Vue.component(SelectRemote.name, SelectRemote); 19 | Vue.component(FormItem.name, FormItem); 20 | Vue.component(AddMinus.name, AddMinus); 21 | Vue.component(ColorPicker.name, ColorPicker); 22 | Vue.component(Daterange.name, Daterange); 23 | 24 | // 防抖提交,在提交时置灰按钮 25 | Vue.directive('onsubmit', definition); 26 | 27 | // 封装插件 28 | Vue.use({ 29 | install(Vue) { 30 | Vue.prototype.$eventBus = new Vue(); 31 | // Vue.prototype.$$user = {}; 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /src/pages/cityData.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | value: 1, 4 | label: '东南', 5 | children: [ 6 | { 7 | value: 2, 8 | label: '上海', 9 | children: [ 10 | { value: 3, label: '普陀' }, 11 | { value: 4, label: '黄埔' }, 12 | { value: 5, label: '徐汇' } 13 | ] 14 | }, 15 | { 16 | value: 7, 17 | label: '江苏', 18 | children: [ 19 | { value: 8, label: '南京' }, 20 | { value: 9, label: '苏州' }, 21 | { value: 10, label: '无锡' } 22 | ] 23 | }, 24 | { 25 | value: 12, 26 | label: '浙江', 27 | children: [ 28 | { value: 13, label: '杭州' }, 29 | { value: 14, label: '宁波' }, 30 | { value: 15, label: '嘉兴' } 31 | ] 32 | } 33 | ] 34 | }, 35 | { 36 | value: 17, 37 | label: '西北', 38 | children: [ 39 | { 40 | value: 18, 41 | label: '陕西', 42 | children: [ 43 | { value: 19, label: '西安' }, 44 | { value: 20, label: '延安' } 45 | ] 46 | }, 47 | { 48 | value: 21, 49 | label: '新疆维吾尔族自治区', 50 | children: [ 51 | { value: 22, label: '乌鲁木齐' }, 52 | { value: 23, label: '克拉玛依' } 53 | ] 54 | } 55 | ] 56 | } 57 | ]; 58 | -------------------------------------------------------------------------------- /src/components/zx-form-item/readme.md: -------------------------------------------------------------------------------- 1 | # 普通组件 2 | 3 | 1. 只要有 v-model 暴露出来,就可以配置到 item 中去,v-model 是表单验证和 formData 的基础 4 | 2. 组件监听表单元素的 value 值的变化(immediate=true),组件初始化时和修改值都会调用配置中的 change 事件 5 | 6 | # 支持自定义组件类型(动态传递的组件) 7 | 8 | 自定义组件自动监听\$emit('input')事件,触发后修改 formData 数据 9 | 10 | ## type 等于'select'时,options 支持数组和函数: 11 | 12 | ```js 13 | { 14 | type: 'select', 15 | optionsAsync: (ctx, item) => { 16 | // return promise 17 | } 18 | } 19 | 20 | { 21 | type: 'select', 22 | async optionsAsync (ctx, item) { 23 | // return promise 24 | } 25 | } 26 | 27 | { 28 | type: 'select', 29 | optionsAsync: async (ctx, item) => { 30 | // return promise 31 | } 32 | } 33 | 34 | // 普通方式 35 | { 36 | type: 'select', 37 | options: [] 38 | } 39 | ``` 40 | 41 | ## type 等于'text'文本类型时,支持字符串和函数两种方式,使用 v-html 方式渲染 42 | 43 | ```js 44 | { 45 | type: 'text', 46 | textAsync: (ctx, item) => { 47 | // 支持return promise 48 | } 49 | } 50 | { 51 | type: 'text', 52 | text: '' 53 | } 54 | ``` 55 | 56 | ## destroyClearData 57 | 58 | ```js 59 | { 60 | type: 'select', 61 | options: [], 62 | vif: form => form.type === 2, 63 | attrs: { 64 | // 组件销毁时清空数据,value将设置为undefined 65 | destroyClearData: true 66 | } 67 | } 68 | ``` 69 | 70 | ## valid 验证时机 71 | 72 | 数据更改或触发 onchange 事件时,如果数据等于 null 或 undefined,不校验数据。但在提交的时候会校验。 73 | 当 onChange 或 onBlur 方法执行时,每次都会将结果存在 validResult 中,当点击提交时会去拿 validResult 验证结果,不再进行重复交验 74 | -------------------------------------------------------------------------------- /src/components/zx-color-picker/color-picker.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 56 | 57 | 72 | -------------------------------------------------------------------------------- /src/pages/form-demo3.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 62 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import vue from 'rollup-plugin-vue'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import { babel } from '@rollup/plugin-babel'; 5 | import scss from 'rollup-plugin-scss'; 6 | import json from '@rollup/plugin-json'; 7 | import image from '@rollup/plugin-image'; 8 | // 压缩 9 | import terser from '@rollup/plugin-terser'; 10 | import { cleandir } from 'rollup-plugin-cleandir'; 11 | 12 | const baseConfig = config => { 13 | return { 14 | plugins: [ 15 | cleandir(config.outputDir), 16 | vue(), 17 | scss({ 18 | fileName: config.styleName, 19 | outputStyle: 'compressed' 20 | }), 21 | json(), 22 | // built-in module 'buffer' 23 | nodeResolve({ preferBuiltins: true, extensions: ['.js', '.vue'] }), 24 | commonjs(), 25 | image(), 26 | babel({ 27 | exclude: 'node_modules/**', 28 | plugins: ['@babel/plugin-transform-runtime'], 29 | babelHelpers: 'runtime' 30 | }) 31 | ], 32 | external: ['vue', 'element-ui', 'crypto-js'] 33 | }; 34 | }; 35 | 36 | let configs = [ 37 | { 38 | input: { 39 | index: './src/components/index.js' 40 | }, 41 | output: [ 42 | { 43 | dir: 'lib/vue-json-form', 44 | format: 'esm', 45 | freeze: false, 46 | chunkFileNames: 'index-[hash].min.js', 47 | plugins: [ 48 | terser({ 49 | output: { 50 | ecma: 6 51 | } 52 | }) 53 | ] 54 | } 55 | ], 56 | ...baseConfig({ 57 | outputDir: 'lib', 58 | styleName: 'index.css' 59 | }) 60 | } 61 | ]; 62 | 63 | export default configs; 64 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | import AppHome from '../pages/AppHome'; 5 | 6 | Vue.use(Router); 7 | 8 | const router = new Router({ 9 | mode: 'history', 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'AppHome', 14 | component: AppHome 15 | }, 16 | { 17 | path: '/form-demo1', 18 | name: 'FormDemo1', 19 | meta: { 20 | title: 'form demo1' 21 | }, 22 | component: () => import('../pages/form-demo1') 23 | }, 24 | { 25 | path: '/form-demo3', 26 | name: 'FormDemo3', 27 | meta: { 28 | title: 'form demo3' 29 | }, 30 | component: () => import('../pages/form-demo3') 31 | }, 32 | { 33 | path: '/form-demo4', 34 | name: 'FormDemo4', 35 | meta: { 36 | title: 'form demo4' 37 | }, 38 | component: () => import('../pages/form-demo4-async-valid') 39 | }, 40 | { 41 | path: '/form-demo5', 42 | name: 'FormDemo5', 43 | meta: { 44 | title: 'form demo5' 45 | }, 46 | component: () => import('../pages/form-demo5') 47 | }, 48 | { 49 | path: '/form-demo6', 50 | name: 'FormDemo6', 51 | meta: { 52 | title: 'form demo6' 53 | }, 54 | component: () => import('../pages/form-demo6') 55 | }, 56 | { 57 | path: '/form-demo7', 58 | name: 'FormDemo7', 59 | meta: { 60 | title: 'form demo7' 61 | }, 62 | component: () => import('../pages/form-demo7') 63 | } 64 | ] 65 | }); 66 | 67 | router.beforeEach((to, from, next) => { 68 | document.title = to.meta.title || 'Vue Start'; 69 | next(); 70 | }); 71 | 72 | export default router; 73 | -------------------------------------------------------------------------------- /src/components/zx-add-minus/readme.md: -------------------------------------------------------------------------------- 1 | ## 增减组件 2 | 3 | 里面嵌套一层新的 form 表单,它完全支持表单的功能和特性 4 | 5 | ## demo 6 | 7 | ```js 8 | [ 9 | { 10 | formList: [ 11 | { 12 | type: 'add-minus', 13 | label: '增减', 14 | showLabel: false, 15 | field: 'rows', 16 | attrs: { 17 | // 最大增加个数 18 | maxlength: 5, 19 | // 表单元素是否inline 20 | inline: true, 21 | // 是否包含分割线 22 | divider: true 23 | }, 24 | rules: { required: true }, 25 | list: [ 26 | { 27 | type: 'input', 28 | label: '活动类型', 29 | field: 'type', 30 | rules: { 31 | required: true 32 | }, 33 | change (data) { 34 | } 35 | } 36 | ] 37 | } 38 | } 39 | ] 40 | // 数据格式 41 | 42 | value: [ 43 | { 44 | type1: 517, 45 | type2: 618, 46 | $idx: 0 47 | }, 48 | { 49 | type1: 315, 50 | type2: 308, 51 | $idx: 1 52 | } 53 | ]; 54 | 55 | addList: 56 | [ 57 | { 58 | $idx: 0, // 与value$idx对应 59 | list: [ 60 | { 61 | type: 'input', 62 | label: '活动类型1', 63 | field: 'type1, 64 | change: () => {} 65 | }, 66 | { 67 | type: 'input', 68 | label: '活动类型2', 69 | field: 'type2' 70 | } 71 | ], 72 | }, 73 | { 74 | $idx: 1, // 与value$idx对应 75 | list: [ 76 | { 77 | type: 'input', 78 | label: '活动类型1', 79 | field: 'type1, 80 | change: () => {} 81 | }, 82 | { 83 | type: 'input', 84 | label: '活动类型2', 85 | field: 'type2' 86 | } 87 | ] 88 | } 89 | ] 90 | 91 | 1. 如果value长,list短,需要补足list配置 92 | 93 | 2. 如果value短,list长,需要删除list多余配置 94 | 95 | 3. 如果value长度和list长度相同,不做改变 96 | 97 | ``` 98 | -------------------------------------------------------------------------------- /src/pages/form-demo6.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 77 | -------------------------------------------------------------------------------- /src/pages/form-demo5.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 74 | 84 | -------------------------------------------------------------------------------- /src/components/zx-add-minus/add-minus.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 95 | -------------------------------------------------------------------------------- /src/components/zx-select-remote/readme.md: -------------------------------------------------------------------------------- 1 | # select 远程搜索(单选或多选) 2 | 3 | 单选: 初次加载默认使用 label 属性或 getLabel()方法,初始化不触发查询 4 | 多选: 默认使用 id 数组查询,初始化触发查询 5 | 6 | ```html 7 | Props: 8 | __________________________________________________________________________________________ 9 | | Name | Type | Required | description | 10 | |——————————————————————————————————————————————————————————————————————————————————————————— 11 | | v-model | [String,Number,Array] | false | id或id数组 | | searchField | String 12 | | false | 搜索的字段名默认'q' | | valueField | String | true |option value字段名 13 | | | labelField | String | true |option label字段名 | | multiple | Boolean | 14 | false | 默认单选 | | label | String | false | 15 | 默认显示的label值(单选),表单配置中无须使用| | getLabel | Function | false | 16 | 默认显示的label值(单选),表单配置中使用 | | getData | Function | false | 17 | 搜索时调用方法,需返回Promise | | remote | Boolean | false 18 | |是否开启远程搜索,默认true | | fullWidth | Boolean | false 19 | |选中项宽度是否100%,默认false(多选) | | initSearch | Boolean | false 20 | |在单选时,如果只有id没有label,当该props等于true时,初始化会触发查询 | | 21 | getSelectedOptions| Array | false |在多选时,设置选中的值,用于编辑反显 | 22 | ——————————————————————————————————————————————————————————————————————————————————————————— 23 | ``` 24 | 25 | ## 普通 HTML 方式 26 | 27 | ```html 28 | 29 | 37 | 38 | 39 | 47 | ``` 48 | 49 | ## 配置使用方法: 50 | 51 | ```js 52 | this.formData.koubeiId = 11; 53 | this.formData.title = '口碑榜标题'; 54 | 55 | let config = [ 56 | { 57 | type: 'select-search', 58 | label: '口碑榜标题', 59 | field: 'koubeiId', 60 | rules: { required: true }, 61 | attrs: { 62 | // 自定义value/label 63 | valueField: 'id', 64 | labelField: 'title', 65 | searchField: 'q', 66 | clearable: true, 67 | // 获得表单值 68 | getLabel: form => form.title, 69 | // 异步加载data,返回数组 70 | getData: async (params = {}, ctx) => { 71 | let { data } = await ctx.axios.get('/api/reputation-lists', params); 72 | return data; 73 | } 74 | } 75 | } 76 | ]; 77 | ``` 78 | -------------------------------------------------------------------------------- /src/components/commons/utils.js: -------------------------------------------------------------------------------- 1 | import is from './is'; 2 | /** 3 | * 防抖函数 4 | * @param {Function} func 5 | * @param {Number} wait 6 | * @param {Boolean} immediate 7 | */ 8 | export function debounce(func, wait = 300, immediate) { 9 | let timeout; 10 | return function () { 11 | let context = this; 12 | let args = arguments; 13 | clearTimeout(timeout); 14 | timeout = setTimeout(function () { 15 | timeout = null; 16 | if (!immediate) func.apply(context, args); 17 | }, wait || 0); 18 | if (immediate && !timeout) func.apply(context, args); 19 | }; 20 | } 21 | 22 | export function throttle(func, delay = 300) { 23 | let timer = null; 24 | return function () { 25 | const context = this; 26 | const args = arguments; 27 | if (!timer) { 28 | timer = setTimeout(function () { 29 | func.apply(context, args); 30 | timer = null; 31 | }, delay); 32 | } 33 | }; 34 | } 35 | 36 | /** 37 | * 获取element距离页面顶部的距离 38 | * @param {Dom} element 39 | */ 40 | export function getElementTop(element) { 41 | let actualTop = element.offsetTop; 42 | let current = element.offsetParent; 43 | while (current !== null) { 44 | actualTop += current.offsetTop; 45 | current = current.offsetParent; 46 | } 47 | return actualTop; 48 | } 49 | 50 | /** 51 | * obj转formData 52 | * @param {Object} obj 53 | */ 54 | export function toFormData(obj) { 55 | let formData = new FormData(); 56 | for (let p in obj) { 57 | formData.append(p, obj[p]); 58 | } 59 | return formData; 60 | } 61 | 62 | // 生产uuid 63 | export function guid() { 64 | function s4() { 65 | return Math.floor((1 + Math.random()) * 0x10000) 66 | .toString(16) 67 | .substring(1); 68 | } 69 | return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; 70 | } 71 | 72 | // 深度克隆 73 | export function deepClone(obj) { 74 | if (typeof obj !== 'object') return; 75 | let newObj = obj instanceof Array ? [] : {}; 76 | for (let key in obj) { 77 | if (typeof obj[key] === 'object') { 78 | newObj[key] = deepClone(obj[key]); 79 | } else { 80 | newObj[key] = obj[key]; 81 | } 82 | } 83 | return newObj; 84 | } 85 | 86 | // 获取纯净的formData,剔除空数据 87 | export function getPureFormData(formData) { 88 | for (let v in formData) { 89 | if (is.empty(formData[v])) { 90 | delete formData[v]; 91 | } else if (v.includes('.')) { 92 | delete formData[v]; 93 | } else if (is.object(formData[v])) { 94 | this.getPureFormData(formData[v]); 95 | } 96 | } 97 | return formData; 98 | } 99 | -------------------------------------------------------------------------------- /src/components/zx-select-remote/style.scss: -------------------------------------------------------------------------------- 1 | .zx-select-remote { 2 | font-size: 12px; 3 | line-height: 1.2; 4 | position: relative; 5 | box-sizing: border-box; 6 | user-select: none; 7 | > div { 8 | background-color: #ffffff; 9 | border: 1px solid #dcdee2; 10 | border-radius: 3px; 11 | } 12 | .el-input-medium { 13 | height: 30px; 14 | } 15 | .tags-wrap { 16 | padding: 0 5px; 17 | background-color: #ffffff; 18 | .tag-selected { 19 | width: auto; 20 | display: inline-block; 21 | padding: 5px; 22 | border: 1px solid #e8eaec; 23 | border-radius: 3px; 24 | background: #f7f7f7; 25 | overflow: hidden; 26 | margin: 3px 0 0; 27 | margin-right: 5px; 28 | } 29 | .full-width { 30 | width: 100%; 31 | margin-right: 0px; 32 | display: flex; 33 | flex-direction: row; 34 | justify-content: space-between; 35 | align-items: center; 36 | &:first-child { 37 | margin-bottom: 0; 38 | margin-top: 3px; 39 | } 40 | &:last-child { 41 | margin-bottom: 0; 42 | } 43 | } 44 | } 45 | .el-input { 46 | border: 0; 47 | outline: none; 48 | .el-input__inner { 49 | border: 0; 50 | padding: 0 5px; 51 | text-indent: 10px; 52 | } 53 | &:focus { 54 | border: 0; 55 | box-shadow: none; 56 | } 57 | } 58 | .input-auto { 59 | width: 98px; 60 | display: inline-block; 61 | position: relative; 62 | vertical-align: top; 63 | line-height: normal; 64 | } 65 | .dropdown-options-wrap { 66 | width: 100%; 67 | .dropdown-options { 68 | position: absolute; 69 | max-width: calc(100% + 100px); 70 | min-width: 100%; 71 | top: calc(100% + 2px); 72 | z-index: 999; 73 | background-color: #ffffff; 74 | border: 1px solid #dcdee2; 75 | max-height: 200px; 76 | min-height: 100px; 77 | overflow-y: auto; 78 | overflow-x: hidden; 79 | white-space: nowrap; 80 | .option-selected { 81 | color: rgba(45, 140, 240, 0.9); 82 | } 83 | .option { 84 | cursor: pointer; 85 | padding: 5px; 86 | padding-left: 10px; 87 | &:hover { 88 | background-color: #f3f3f3; 89 | } 90 | } 91 | } 92 | .nodata { 93 | display: flex; 94 | align-items: center; 95 | justify-content: center; 96 | height: 40px; 97 | color: rgba(45, 140, 240, 0.9); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-json-form", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/springalskey/vue-json-form", 5 | "main": "src/components/index.js", 6 | "scripts": { 7 | "start": "npm run serve", 8 | "serve": "cross-env ZX_ENV=dev vue-cli-service serve", 9 | "lint": "vue-cli-service lint", 10 | "lib": "cross-env ZX_ENV=prod vue-cli-service build --target lib --name vue-json-form/index --dest lib src/components/index.js", 11 | "build:lib": "cross-env NODE_ENV=production rollup -c --bundleConfigAsCjs" 12 | }, 13 | "dependencies": { 14 | "core-js": "3.6.5", 15 | "crypto-js": "3.1.2", 16 | "element-ui": "2.15.1", 17 | "vue": "2.6.11", 18 | "vue-router": "3.5.1" 19 | }, 20 | "devDependencies": { 21 | "@babel/plugin-external-helpers": "^7.18.6", 22 | "@rollup/plugin-babel": "^6.0.3", 23 | "@rollup/plugin-commonjs": "^25.0.0", 24 | "@rollup/plugin-image": "^3.0.2", 25 | "@rollup/plugin-json": "^6.0.0", 26 | "@rollup/plugin-node-resolve": "15.0.2", 27 | "@rollup/plugin-terser": "^0.4.3", 28 | "@vue/cli": "4.5.11", 29 | "@vue/cli-plugin-babel": "4.5.0", 30 | "@vue/cli-plugin-eslint": "4.5.0", 31 | "@vue/cli-service": "4.5.0", 32 | "babel-eslint": "10.1.0", 33 | "babel-plugin-external-helpers": "^6.22.0", 34 | "cross-env": "^7.0.3", 35 | "eslint": "5.16.0", 36 | "eslint-config-standard": "12.0.0", 37 | "eslint-import-resolver-webpack": "0.8.4", 38 | "eslint-plugin-html": "5.0.5", 39 | "eslint-plugin-import": "2.17.3", 40 | "eslint-plugin-node": "8.0.1", 41 | "eslint-plugin-promise": "4.1.1", 42 | "eslint-plugin-standard": "4.0.0", 43 | "eslint-plugin-vue": "5.2.2", 44 | "husky": "1.3.1", 45 | "lint-staged": "8.1.6", 46 | "node-sass": "4.12.0", 47 | "rollup": "^3.23.0", 48 | "rollup-plugin-cleandir": "^2.0.0", 49 | "rollup-plugin-scss": "^4.0.0", 50 | "rollup-plugin-vue": "^5.1.9", 51 | "sass-loader": "8.0.0", 52 | "vue-template-compiler": "2.6.11" 53 | }, 54 | "eslintConfig": { 55 | "root": true, 56 | "env": { 57 | "node": true 58 | }, 59 | "extends": [ 60 | "plugin:vue/essential", 61 | "eslint:recommended" 62 | ], 63 | "parserOptions": { 64 | "parser": "babel-eslint" 65 | }, 66 | "rules": {} 67 | }, 68 | "browserslist": [ 69 | "> 1%", 70 | "last 2 versions", 71 | "not dead" 72 | ], 73 | "husky": { 74 | "hooks": { 75 | "pre-commit": "lint-staged" 76 | } 77 | }, 78 | "lint-staged": { 79 | "./src/**/*.{js,vue}": [ 80 | "eslint --fix", 81 | "git add" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/commons/is.js: -------------------------------------------------------------------------------- 1 | import REGEXP from './regexp'; 2 | 3 | let is = {}; 4 | 5 | is.object = obj => { 6 | return ( 7 | Object.prototype.toString.call(obj).toLowerCase() === '[object object]' 8 | ); 9 | }; 10 | 11 | is.url = url => { 12 | return REGEXP.URL.test(url); 13 | }; 14 | 15 | is.string = str => { 16 | return typeof str === 'string'; 17 | }; 18 | 19 | is.array = arr => { 20 | return Array.isArray(arr); 21 | }; 22 | 23 | is.function = fn => { 24 | return typeof fn === 'function'; 25 | }; 26 | 27 | // 是否线上和UT环境 28 | is.prod = () => { 29 | let origin = window.location.origin; 30 | if ($ENV.env === 'prod' || origin.includes('.zhizhi.info')) { 31 | return true; 32 | } 33 | return false; 34 | }; 35 | 36 | // 是否是json字符串 37 | is.json = str => { 38 | if (is.string(str)) { 39 | try { 40 | JSON.parse(str); 41 | return true; 42 | } catch (err) { 43 | return false; 44 | } 45 | } 46 | return false; 47 | }; 48 | 49 | // 小程序:判断登录角色是否为买家 50 | is.buyer = () => { 51 | let value = sessionStorage.getItem('USER_BASEINFO'); 52 | let userInfo = JSON.parse(value || '{}'); 53 | let buyerRole = 1; 54 | let isBuyer = is.empty(userInfo) || userInfo.userRole === buyerRole; 55 | return isBuyer; 56 | }; 57 | 58 | /** 59 | * 判断是否为空值 60 | * test case: 61 | * console.log(is.empty(-1)); // false 62 | * console.log(is.empty(0)); // false 63 | * console.log(is.empty(false)); // false 64 | * console.log(is.empty([undefined,0])); // false 65 | * console.log(is.empty([0,0])); // false 66 | * console.log(is.empty([undefined,false])); // false 67 | * console.log('') 68 | * console.log(is.empty(null)); // true 69 | * console.log(is.empty('')); // true 70 | * console.log(is.empty(undefined)); // true 71 | * console.log(is.empty({})); // true 72 | * console.log(is.empty([])); // true 73 | * console.log(is.empty(Infinity)); // true 74 | * console.log(is.empty(NaN)); // true 75 | * console.log(is.empty([[]])); // true 76 | * console.log(is.empty([undefined])); // true 77 | * console.log(is.empty([null,Infinity])); // true 78 | * console.log(is.empty(['',''])); // true 79 | */ 80 | is.empty = function(value) { 81 | if (Array.isArray(value)) { 82 | return value.length === 0 || value.every(v => is.empty(v)); 83 | } else if (is.object(value)) { 84 | var length = Object.keys(value).length; 85 | if (length === 0) { 86 | return true; 87 | } 88 | return false; 89 | } else if (typeof value === 'number' && !isFinite(value)) { 90 | return true; 91 | } 92 | return typeof value === 'undefined' || value === '' || value === null; 93 | }; 94 | 95 | export default is; 96 | -------------------------------------------------------------------------------- /src/components/vue-json-form/prefix-suffix.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 103 | 104 | 111 | -------------------------------------------------------------------------------- /lib/vue-json-form/README.md: -------------------------------------------------------------------------------- 1 | # vue-json-form 2 | vue-json-form base on Vue2 and ElementUI 3 | 4 | ## How to use 5 | ``` 6 | npm install @vue-json/vue-json-form 7 | ``` 8 | 9 | ## Usage 10 | 11 | ```js 12 | // main.js 13 | import '@vue-json/vue-json-form'; 14 | import '@vue-json/vue-json-form/index.css'; 15 | ``` 16 | ```html 17 | 18 | 19 | submit 20 | ``` 21 | ```js 22 | export default { 23 | data() { 24 | return { 25 | formData: { 26 | color: '#F0F0F0' 27 | }, 28 | list: [ 29 | { 30 | type: 'input', 31 | label: 'nickname', 32 | field: 'nickname', 33 | rules: { 34 | required: true, 35 | valid: ({ value, formData }) => { 36 | if (value.includes('null')) { 37 | return { 38 | matched: false, 39 | message: 'Null is not allowed' 40 | }; 41 | } 42 | return { matched: true }; 43 | } 44 | } 45 | }, 46 | { 47 | type: 'input-number', 48 | label: 'count', 49 | field: 'count', 50 | rules: {required: true, num: true } 51 | }, 52 | { 53 | type: 'radio', 54 | label: 'type', 55 | field: 'type', 56 | rules: { required: true }, 57 | options: [ 58 | { label: 'type1', value: 1 }, 59 | { label: 'type2', value: 2 } 60 | ] 61 | }, 62 | { 63 | type: 'select', 64 | label: 'province', 65 | field: 'province', 66 | rules: { required: true }, 67 | optionsAsync: async () => { 68 | return new Promise((resolve) => { 69 | setTimeout(() => { 70 | resolve([ 71 | { label: 'London', value: 1 }, 72 | { label: 'New York', value: 2 } 73 | ]) 74 | }, 300); 75 | }) 76 | } 77 | }, 78 | { 79 | type: 'switch', 80 | label: 'open', 81 | field: 'isOpen', 82 | rules: { required: true }, 83 | // vif control showOrHide 84 | vif: (formData) => formData.type === 1 85 | }, 86 | { 87 | type: 'daterange', 88 | label: 'daterange', 89 | field: 'dateRange', 90 | rules: { required: true }, 91 | attrs: { 92 | startField: 'beginDate', 93 | endField: 'endDate' 94 | } 95 | }, 96 | ] 97 | }; 98 | }, 99 | methods: { 100 | async onSubmit(valided, { formData }) { 101 | if (!valided) return; 102 | // TOTO: 103 | console.log(formData); 104 | } 105 | } 106 | }; 107 | ``` 108 | 109 | ## Services 110 | 111 | More features and services [https://zhizhi.info] 112 | 113 | 114 | ## License 115 | 116 | [MIT](https://opensource.org/licenses/MIT) 117 | 118 | Copyright (c) 2023-present, Quan Yang -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-json-form 2 | vue-json-form base on Vue2 and ElementUI 3 | 4 | ## How to use 5 | ``` 6 | npm install @vue-json/vue-json-form 7 | ``` 8 | 9 | ## Project setup 10 | 11 | ``` 12 | npm install 13 | ``` 14 | 15 | ### Project start 16 | 17 | ``` 18 | npm start 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```js 24 | // main.js 25 | import '@vue-json/vue-json-form'; 26 | import '@vue-json/vue-json-form/index.css'; 27 | ``` 28 | ```html 29 | 30 | 31 | submit 32 | ``` 33 | ```js 34 | export default { 35 | data() { 36 | return { 37 | formData: { 38 | color: '#F0F0F0' 39 | }, 40 | list: [ 41 | { 42 | type: 'input', 43 | label: 'nickname', 44 | field: 'nickname', 45 | rules: { 46 | required: true, 47 | valid: ({ value, formData }) => { 48 | if (value.includes('null')) { 49 | return { 50 | matched: false, 51 | message: 'Null is not allowed' 52 | }; 53 | } 54 | return { matched: true }; 55 | } 56 | } 57 | }, 58 | { 59 | type: 'input-number', 60 | label: 'count', 61 | field: 'count', 62 | rules: {required: true, num: true } 63 | }, 64 | { 65 | type: 'radio', 66 | label: 'type', 67 | field: 'type', 68 | rules: { required: true }, 69 | options: [ 70 | { label: 'type1', value: 1 }, 71 | { label: 'type2', value: 2 } 72 | ] 73 | }, 74 | { 75 | type: 'select', 76 | label: 'province', 77 | field: 'province', 78 | rules: { required: true }, 79 | optionsAsync: async () => { 80 | return new Promise((resolve) => { 81 | setTimeout(() => { 82 | resolve([ 83 | { label: 'London', value: 1 }, 84 | { label: 'New York', value: 2 } 85 | ]) 86 | }, 300); 87 | }) 88 | } 89 | }, 90 | { 91 | type: 'switch', 92 | label: 'open', 93 | field: 'isOpen', 94 | rules: { required: true }, 95 | // vif control showOrHide 96 | vif: (formData) => formData.type === 1 97 | }, 98 | { 99 | type: 'daterange', 100 | label: 'daterange', 101 | field: 'dateRange', 102 | rules: { required: true }, 103 | attrs: { 104 | startField: 'beginDate', 105 | endField: 'endDate' 106 | } 107 | }, 108 | ] 109 | }; 110 | }, 111 | methods: { 112 | async onSubmit(valided, { formData }) { 113 | if (!valided) return; 114 | // TOTO: 115 | console.log(formData); 116 | } 117 | } 118 | }; 119 | ``` 120 | 121 | ## Services 122 | 123 | More features and services [https://zhizhi.info] 124 | 125 | 126 | ## License 127 | 128 | [MIT](https://opensource.org/licenses/MIT) 129 | 130 | Copyright (c) 2023-present, Quan Yang -------------------------------------------------------------------------------- /src/pages/form-demo4-async-valid.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 126 | -------------------------------------------------------------------------------- /src/components/zx-validator/directive.js: -------------------------------------------------------------------------------- 1 | import is from '../commons/is'; 2 | 3 | // 防抖提交,在提交时置灰按钮 4 | let refs = {}; 5 | let definition = { 6 | bind(el, binding, vnode) { 7 | vnode.componentInstance.$on('click', async (e) => { 8 | e.stopPropagation(); 9 | el.disabled = true; 10 | let vid = el.getAttribute('vid'); 11 | if (!vid) { 12 | console.error('提交按钮vid属性必填'); 13 | return; 14 | } 15 | // 一个组件中(或一个页面中)vid不能重复 16 | let validatorEl = vnode.context.$el.querySelector( 17 | `div[vid="${vid}"]` 18 | ); 19 | let valided = await validAll(validatorEl); 20 | let formInstance = [refs[validatorEl.dataset.uuid]]; 21 | let { form, list } = formInstance[0]; 22 | let formData = JSON.parse(JSON.stringify(form)); 23 | getFormData(formData, list); 24 | let onSave = binding.value; 25 | let promise = onSave.call(vnode.context, valided, { 26 | formInstance, 27 | formData 28 | }); 29 | if (promise instanceof Promise) { 30 | promise.finally(() => { 31 | el.disabled = false; 32 | }); 33 | } else { 34 | console.error('v-onsubmit指令必须返回Promise'); 35 | } 36 | }); 37 | } 38 | }; 39 | 40 | /** 41 | * 获取formData,当formList配置中_vif=false时设删除字段属性,避免提交不需要的数据 42 | * @param {Object} formData 43 | * @param {Array} formList 44 | */ 45 | function getFormData(formData, formList = []) { 46 | for (let config of formList) { 47 | // key不为空代表有重复的field 48 | if (config.field && config.field.includes('.')) { 49 | delete formData[config.field]; 50 | if (!config.key && !config._vif) { 51 | let arr = config.field.split('.'); 52 | delete formData[arr[0]][arr[1]]; 53 | } 54 | } else if (!config.key && !config._vif && config.field) { 55 | delete formData[config.field]; 56 | } 57 | let list = config.list || config.formList; 58 | if (!is.empty(list)) { 59 | let data = config.deepData ? formData[config.field] : formData; 60 | if (!data || !is.object(data)) { 61 | break; 62 | } 63 | if (config._vif) { 64 | getFormData(data, list); 65 | } else { 66 | for (let item of list) { 67 | delete data[item.field]; 68 | if (item.list || item.formList) { 69 | getFormData(data, item.list || item.formList); 70 | } 71 | } 72 | } 73 | } 74 | } 75 | // 删除空数据 76 | for (let p in formData) { 77 | if (is.empty(formData[p])) { 78 | delete formData[p]; 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * 调用表单校验,会遍历所有的form进行验证,并返回校验是否通过。 85 | * 假设有一个表单里面嵌套一个表单(嵌套表单) 86 | * 只要某个表单中的任意一个字段值校验不通过,就返回false;当两个表单中所有字段校验通过才返回true。 87 | * @param {Dom} validatorEl 88 | */ 89 | export async function validAll(validatorEl) { 90 | let components = [refs[validatorEl.dataset.uuid]]; 91 | // 查询当前下面所有validator 92 | let nodeList = [...validatorEl.querySelectorAll('.form-validator')]; 93 | if (!is.empty(nodeList)) { 94 | nodeList.forEach(node => { 95 | let uuid = node.dataset.uuid; 96 | components.push(refs[uuid]); 97 | }); 98 | } 99 | components = components.reverse(); 100 | let valided = true; 101 | for (let item of components.entries()) { 102 | let component = item[1]; 103 | let bool = await component.valid(); 104 | if (!bool) { 105 | valided = false; 106 | } 107 | } 108 | return valided; 109 | } 110 | 111 | export { refs, definition }; 112 | -------------------------------------------------------------------------------- /src/pages/form-demo7.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 114 | 119 | -------------------------------------------------------------------------------- /src/components/zx-validator/rules.js: -------------------------------------------------------------------------------- 1 | import is from '../commons/is'; 2 | import { trim, isEmpty } from './util'; 3 | let rules = {}; 4 | 5 | addRule('mobile', /^1\d{10}$/, '{{name}}格式不正确'); 6 | addRule('num', /^\d+$/, '{{name}}格式必须为>=0的整数'); 7 | addRule('integer', /^[1-9]\d*$/, '{{name}}为>0的整数'); 8 | addRule('char', /^[a-zA-Z]+$/, '{{name}}由英文字母组成'); 9 | addRule('nchar', /^[a-zA-Z0-9]+$/, '{{name}}由英文字母和数字组成'); 10 | 11 | // 版本号:1.2.3,每一位最多三位数 12 | addRule('version', /^([1-9]{1,3}|0)\.\d{1,3}\.\d{1,3}$/, '{{name}}格式不正确'); 13 | // 16进制颜色值 14 | addRule('color', /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, '{{name}}格式不正确'); 15 | // 正数金额(^[1-9](\d+)?(\.\d{1,2})?$)|(^0$)|(^\d\.\d{1,2}$) 16 | // 正负数金额 17 | addRule( 18 | 'money', 19 | /(^-?[1-9](\d+)?(\.\d{1,2})?$)|(^-?0$)|(^-?\d\.\d{1,2}$)/, 20 | '{{name}}格式不正确' 21 | ); 22 | addRule( 23 | 'email', 24 | /^[\w.\-]+@(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\.)+[A-Za-z]{2,6}$/, 25 | '{{name}}格式不正确' 26 | ); 27 | addRule( 28 | 'url', 29 | /^(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, 30 | '{{name}}格式不正确' 31 | ); 32 | 33 | addRule( 34 | 'pathname', 35 | /^[a-z]+((\-)*[a-z0-9]+)*$/, 36 | '{{name}}必须以字母开头且由小写字母、数字、中划线组成' 37 | ); 38 | 39 | addRule( 40 | 'required', 41 | val => { 42 | // daterange值循环判空 43 | return !is.empty(trim(val)); 44 | }, 45 | '{{name}}不能为空' 46 | ); 47 | addRule( 48 | 'max', 49 | (val, rule) => { 50 | if (isEmpty(val)) return true; 51 | return Number(val) <= Number(rule); 52 | }, 53 | '{{name}}必须小于等于{{max}}' 54 | ); 55 | addRule( 56 | 'min', 57 | (val, rule) => { 58 | if (isEmpty(val)) return true; 59 | return Number(val) >= Number(rule); 60 | }, 61 | '{{name}}必须大于等于{{min}}' 62 | ); 63 | addRule( 64 | 'minlen', 65 | (val, rule) => { 66 | if (isEmpty(val)) return true; 67 | return trim(val).length >= Number(trim(rule)); 68 | }, 69 | '{{name}}的长度必须大于等于{{minlen}}' 70 | ); 71 | addRule( 72 | 'maxlen', 73 | (val, rule) => { 74 | if (isEmpty(val)) return true; 75 | return trim(val).length <= Number(trim(rule)); 76 | }, 77 | '{{name}}的长度必须小于等于{{maxlen}}' 78 | ); 79 | 80 | addRule( 81 | 'regexp', 82 | (val, rule) => { 83 | if (isEmpty(val)) return true; 84 | return new RegExp(rule, 'g').test(trim(val)); 85 | }, 86 | '{{name}}不符合规则' 87 | ); 88 | 89 | addRule( 90 | 'password', 91 | (val, rule) => { 92 | if (!rule) return true; 93 | return !( 94 | ( 95 | val.length < 8 || 96 | val.length > 16 || 97 | /^\d+$/.test(val) || 98 | /^[A-Za-z]+$/.test(val) || 99 | /^[^A-Za-z0-9]+$/.test(val) 100 | ) // 如果是特殊字符 101 | ); 102 | }, 103 | '密码长度8-16位,数字、字母、字符至少包含两种' 104 | ); 105 | 106 | function addRule(name, operator, message) { 107 | if (rules[name]) { 108 | console.error('Rule already exists'); 109 | return; 110 | } 111 | if (operator instanceof RegExp) { 112 | let fn = val => { 113 | if (isEmpty(val)) return true; 114 | return operator.test(val); 115 | }; 116 | rules[name] = [fn, message]; 117 | } else { 118 | rules[name] = [operator, message]; 119 | } 120 | } 121 | 122 | function setMessage(name, message) { 123 | if (rules[name]) { 124 | rules[name][1] = message; 125 | } 126 | } 127 | 128 | function getRule(name) { 129 | if (rules[name]) { 130 | return rules[name]; 131 | } 132 | } 133 | 134 | function getMessage(name) { 135 | return rules[name] ? rules[name][1] : ''; 136 | } 137 | 138 | export { rules, addRule, getMessage, setMessage, getRule }; 139 | -------------------------------------------------------------------------------- /src/components/commons/compress-image.js: -------------------------------------------------------------------------------- 1 | class Compress { 2 | changeFileToBaseURL(file, fn) { 3 | // 创建读取文件对象 4 | let fileReader = new FileReader(); 5 | // 如果file没定义返回null 6 | if (file === undefined) return fn(null); 7 | // 读取file文件,得到的结果为base64位 8 | fileReader.readAsDataURL(file); 9 | fileReader.onload = function() { 10 | // 把读取到的base64 11 | let imgBase64Data = this.result; 12 | fn(imgBase64Data); 13 | }; 14 | } 15 | 16 | dataURLtoFile(dataurl, filename) { 17 | let arr = dataurl.split(','); 18 | let mime = arr[0].match(/:(.*?);/)[1]; 19 | let bstr = atob(arr[1]); 20 | let n = bstr.length; 21 | let u8arr = new Uint8Array(n); 22 | while (n--) { 23 | u8arr[n] = bstr.charCodeAt(n); 24 | } 25 | return new File([u8arr], filename, { type: mime }); 26 | } 27 | 28 | /** 29 | * canvas压缩图片 30 | * @param {参数obj} param 31 | * @param {文件二进制流} param.file 必传 32 | * @param {目标压缩大小} param.targetSize 不传初始赋值-1 33 | * @param {输出图片宽度} param.width 不传初始赋值-1,等比缩放不用传高度 34 | * @param {输出图片名称} param.fileName 不传初始赋值image 35 | * @param {压缩图片程度} param.quality 不传初始赋值0.92。值范围0~1 36 | * @param {回调函数} param.success 必传 37 | */ 38 | compressImage(param) { 39 | // 如果没有回调函数就不执行 40 | if (param && param.success) { 41 | // 如果file没定义返回null 42 | if (param.file === undefined) return param.success(null); 43 | // 给参数附初始值 44 | param.targetSize = param.hasOwnProperty('targetSize') 45 | ? param.targetSize 46 | : -1; 47 | param.width = param.hasOwnProperty('width') ? param.width : -1; 48 | param.fileName = param.hasOwnProperty('fileName') 49 | ? param.fileName 50 | : 'image'; 51 | param.quality = param.hasOwnProperty('quality') ? param.quality : 0.92; 52 | let _this = this; 53 | // 得到文件类型 54 | let fileType = param.file.type; 55 | // console.log(fileType) //image/jpeg 56 | if (fileType.indexOf('image') === -1) { 57 | console.log('请选择图片文件'); 58 | return param.success(null); 59 | } 60 | // 如果当前size比目标size小,直接输出 61 | let size = param.file.size; 62 | if (param.targetSize > size) { 63 | return param.success(param.file); 64 | } 65 | // 读取file文件,得到的结果为base64位 66 | this.changeFileToBaseURL(param.file, function(base64) { 67 | if (base64) { 68 | let image = new Image(); 69 | image.src = base64; 70 | image.onload = function() { 71 | // 获得长宽比例 72 | let scale = this.width / this.height; 73 | // console.log(scale); 74 | // 创建一个canvas 75 | let canvas = document.createElement('canvas'); 76 | // 获取上下文 77 | let context = canvas.getContext('2d'); 78 | // 获取压缩后的图片宽度,如果width为-1,默认原图宽度 79 | canvas.width = param.width === -1 ? this.width : param.width; 80 | // 获取压缩后的图片高度,如果width为-1,默认原图高度 81 | canvas.height = 82 | param.width === -1 ? this.height : parseInt(param.width / scale); 83 | // 把图片绘制到canvas上面 84 | context.drawImage(image, 0, 0, canvas.width, canvas.height); 85 | // 压缩图片,获取到新的base64Url 86 | let newImageData = canvas.toDataURL(fileType, param.quality); 87 | // 将base64转化成文件流 88 | let resultFile = _this.dataURLtoFile(newImageData, param.fileName); 89 | // 判断如果targetSize有限制且压缩后的图片大小比目标大小大,就弹出错误 90 | if (param.targetSize !== -1 && param.targetSize < resultFile.size) { 91 | console.log('图片上传尺寸太大,请重新上传^_^'); 92 | param.success(null); 93 | } else { 94 | // 返回文件流 95 | param.success(resultFile); 96 | } 97 | }; 98 | } 99 | }); 100 | } 101 | } 102 | } 103 | 104 | export default function compressImage(param = {}) { 105 | new Compress().compressImage(param); 106 | } 107 | -------------------------------------------------------------------------------- /src/components/zx-form-daterange/daterange.vue: -------------------------------------------------------------------------------- 1 | 38 | 129 | 130 | 140 | -------------------------------------------------------------------------------- /src/components/vue-json-form/readme.md: -------------------------------------------------------------------------------- 1 | ## 简单的表单配置组件 2 | 3 | 1. 支持嵌套 form 表单的数据: 4 | 5 | ``` 6 | this.formData = { 7 | activityId: 12908, 8 | activityName: '618', 9 | coupon: { 10 | type: 1, 11 | name: '满20减10' 12 | } 13 | } 14 | ``` 15 | 16 | 2. 可配置基本的表单验证逻辑,支持 validator 组件中的所有验证规则和自定义函数校验,详情参考 zx-validator 组件文档。 17 | 18 | ## 基本用法 19 | 20 | ```html 21 | 只需要button提供vid和v-onsubmit指令即可。 我们假设有这么一个表单: 22 | 23 | 24 | 在页面其他任何地方有如下的保存按钮,和平常写法一样,使用vid进行关联即可 25 | 保存 26 | ``` 27 | 28 | 29 | ## demo 30 | 31 | ```html 32 | 33 | 保存 34 | ``` 35 | 36 | ```js 37 | export default { 38 | data() { 39 | return { 40 | formData: { 41 | color: '#f0f0f0' 42 | }, 43 | list: [ 44 | { 45 | type: 'color', 46 | label: '车身颜色', 47 | field: 'color', 48 | rules: { required: true } 49 | }, 50 | { 51 | type: 'input-number', 52 | label: '购买数量', 53 | field: 'buyCount', 54 | rules: { required: true } 55 | }, 56 | { 57 | type: 'switch', 58 | label: '是否显示', 59 | field: 'isShow', 60 | rules: { required: true } 61 | }, 62 | { 63 | type: 'radio', 64 | label: '活动类型', 65 | field: 'type', 66 | rules: { required: true }, 67 | options: [ 68 | { label: '单品活动', value: 1 }, 69 | { label: '满减活动', value: 2 } 70 | ] 71 | } 72 | ] 73 | }; 74 | }, 75 | methods: { 76 | async onSave(valided) { 77 | if (!valided) return; 78 | // 校验通过TOTO: 79 | } 80 | } 81 | }; 82 | ``` 83 | 84 | ## 触发 value 的 watch 监听 85 | 86 | 1. ”初始化组件时“ 87 | 1). 当 formList 中声明 field 之后,但是在 formData 中没有声明,会触发 watch(formData 新增属性) 88 | 2). 一个日期范围配置会触发一次 watch(假设配置了两个日期范围,就会触发 2 次) 89 | 2. 当某个值变更时 90 | 91 | ## 触发校验的时机 92 | 93 | 1. 数据更改或触发 onchange 事件时,如果数据等于 null 或 undefined,不校验数据。但在提交的时候会校验。 94 | 2. 当 onChange 或 onBlur 方法执行时,每次都会将结果存在 validResult 中,当点击提交时会去拿 validResult 验证结果,不再进行重复交验。 95 | 96 | ## 关于表单 value 赋值的优先级顺序 97 | 98 | formData 大于 config.defaultValue 大于 config.value 99 | 如果 formData 中没有值,就寻找 defaultValue,再寻找 value,如果都没有则默认 undefined 100 | 101 | # 普通组件 102 | 103 | 1. 只要有 v-model 暴露出来,就可以配置到 items 中去,v-model 是表单验证和 formData 的基础 104 | 2. 组件监听表单元素的 value 值的变化(immediate=true),组件初始化时和修改值都会调用配置中的 change 事件 105 | 106 | # 支持自定义组件类型(动态传递的组件) 107 | 108 | 自定义组件自动监听\$emit('input')事件,触发后会修改 formData 数据 109 | 110 | ```js 111 | { 112 | type: 'component', 113 | label: '车身', 114 | field: 'body', 115 | // 传递自定义组件 116 | component: MyComponent, 117 | rules: { required: true }, 118 | // 可设置自定义组件的props属性 119 | attrs: { clearable: true } 120 | } 121 | // MyComponent组件中必须执行$emit('input')事件,这样外围的formData才能拿到组件中的value,等同于执行formData.body = {MyComponent组件中的值} 122 | 123 | ## 自定义组件如何在验证”失败时显示红色外边框“以及“错误提示信息”,请移步validator组件中的readme.md 124 | 125 | ``` 126 | 127 | ## type 等于'select'时,options 支持数组和函数: 128 | 129 | ```js 130 | { 131 | type: 'select', 132 | optionsAsync: (ctx, item) => { 133 | // return promise 134 | } 135 | } 136 | 137 | { 138 | type: 'select', 139 | async optionsAsync (ctx, item) { 140 | // return promise 141 | } 142 | } 143 | 144 | { 145 | type: 'select', 146 | optionsAsync: async (ctx, item) => { 147 | // return promise 148 | } 149 | } 150 | 151 | // 普通方式 152 | { 153 | type: 'select', 154 | options: [] 155 | } 156 | ``` 157 | 158 | ## type 等于'text'文本类型时,支持字符串和函数两种方式,使用 v-html 方式渲染 159 | 160 | ```js 161 | { 162 | type: 'text', 163 | textAsync: (ctx, item) => { 164 | // 支持return promise 165 | } 166 | } 167 | { 168 | type: 'text', 169 | text: '' 170 | } 171 | ``` 172 | 173 | ## destroyClearData 174 | 175 | ```js 176 | { 177 | type: 'select', 178 | options: [], 179 | vif: form => form.type === 2, 180 | attrs: { 181 | // 组件销毁时清空数据,value将设置为undefined 182 | destroyClearData: true 183 | } 184 | } 185 | ``` 186 | -------------------------------------------------------------------------------- /src/components/zx-upload/zx-file-upload/file-upload.js: -------------------------------------------------------------------------------- 1 | import is from '../../commons/is'; 2 | import CosUpload from '../utils/cosUpload'; 3 | import { Message } from 'element-ui'; 4 | export default { 5 | name: 'zxFileUpload', 6 | props: { 7 | value: { 8 | type: String 9 | }, 10 | // 0:图片;1:视频;2:音频 3:其他文件 11 | type: { 12 | type: Number, 13 | default: 3 14 | }, 15 | beforeUpload: { 16 | type: Function 17 | }, 18 | successUpload: { 19 | type: Function 20 | }, 21 | // 单位kb 22 | maxSize: { 23 | type: Number, 24 | default: 1024 * 10 25 | }, 26 | format: { 27 | type: Array, 28 | default: () => ['.mp4'] 29 | }, 30 | readonly: { 31 | type: Boolean 32 | }, 33 | tips: { 34 | type: String 35 | }, 36 | buttonText: { 37 | type: String, 38 | default: '点击上传' 39 | }, 40 | // 1:上传到腾讯云,2:不上传暴露文件流给upload方法 41 | uploadType: { 42 | type: Number, 43 | default: 1 44 | }, 45 | upload: { 46 | type: Function 47 | }, 48 | // 是否展示input框 49 | showInput: { 50 | type: Boolean, 51 | default: true 52 | } 53 | }, 54 | data() { 55 | return { 56 | fileUrl: undefined 57 | }; 58 | }, 59 | watch: { 60 | value: { 61 | handler() { 62 | this.fileUrl = this.value; 63 | }, 64 | immediate: true 65 | } 66 | }, 67 | methods: { 68 | handleClick() { 69 | this.$refs.file.click(); 70 | let _this = this; 71 | this.$refs.file.onchange = async function () { 72 | let file = this.files[0]; 73 | let fileName = file.name; 74 | let extensions = fileName.substring(fileName.lastIndexOf('.')); 75 | let result = _this.valid(file, extensions); 76 | if (!result) { 77 | _this.$refs.file.value = ''; 78 | return; 79 | } 80 | // 上传参数 81 | let options = { 82 | fileName, 83 | file, 84 | extensions, 85 | type: _this.type 86 | }; 87 | try { 88 | if (_this.uploadType === 2) { 89 | _this.upload && _this.upload(options); 90 | _this.$emit('input', fileName); 91 | _this.$refs.file.value = ''; 92 | return; 93 | } 94 | let fileInfo = await CosUpload.uploadFile({ 95 | file: options.file, 96 | type: _this.type 97 | }); 98 | let url = fileInfo.path; 99 | if (url) { 100 | _this.fileUrl = url; 101 | _this.$emit('input', url); 102 | _this.$emit('change', url); 103 | // let isVideo = _this.isVideo(extensions.toLowerCase()); 104 | // // 当为视频时,截取第一帧作为默认封面 105 | // if (isVideo) { 106 | // let coverUrl = fileInfo.animatedImage; 107 | // let { width, height } = await this.$$utils.getImageSizeBySrc(coverUrl); 108 | // } 109 | _this.$emit('success', fileInfo); 110 | _this.successUpload && _this.successUpload(fileInfo); 111 | } 112 | _this.$refs.file.value = ''; 113 | } catch (err) { 114 | _this.$refs.file.value = ''; 115 | } 116 | }; 117 | }, 118 | 119 | valid(file, extensions) { 120 | if (extensions === '.ZIP') { 121 | Message.error('zip格式必后缀必须小写!'); 122 | return false; 123 | } 124 | if (this.format.indexOf(extensions.toLowerCase()) < 0) { 125 | Message.error('不支持该文件类型!'); 126 | return false; 127 | } 128 | let kb = file.size / 1024; 129 | const isGtMax = kb > this.maxSize; 130 | if (isGtMax) { 131 | if (this.maxSize / 1024 >= 1) { 132 | Message.error(`上传文件大小不能超过${this.maxSize / 1024}MB!`); 133 | } else { 134 | Message.error(`上传文件大小不能超过${this.maxSize}KB!`); 135 | } 136 | return false; 137 | } 138 | if (this.beforeUpload) { 139 | return this.beforeUpload.call(this.$vnode.context, file); 140 | } 141 | return true; 142 | }, 143 | isVideo(extensions) { 144 | let _exts = ['.mp4', '.avi', '.wmv']; 145 | return _exts.includes(extensions); 146 | }, 147 | getReadonly() { 148 | if (typeof this.readonly !== 'undefined') return this.readonly; 149 | if (is.prod()) return true; 150 | return false; 151 | }, 152 | onChange() { 153 | if (this.fileUrl !== this.value) { 154 | this.$emit('change', this.fileUrl); 155 | this.$emit('input', this.fileUrl); 156 | } 157 | } 158 | } 159 | }; 160 | -------------------------------------------------------------------------------- /src/components/commons/common.scss: -------------------------------------------------------------------------------- 1 | @import './margin-padding.scss'; 2 | 3 | // 边框颜色 4 | $border-color: #ebeef5; 5 | $base-font-size: 12px; 6 | $bg-color: #f5f7f9; 7 | $color-red: #f23030; 8 | $color-light-red: #ff4960; 9 | 10 | .clearfix:after { 11 | content: '.'; 12 | display: block; 13 | height: 0; 14 | clear: both; 15 | visibility: hidden; 16 | } 17 | .clearfix { 18 | *zoom: 1; 19 | } 20 | 21 | .relative { 22 | position: relative; 23 | } 24 | 25 | .full-width { 26 | width: 100% !important; 27 | } 28 | .full-height { 29 | height: 100% !important; 30 | } 31 | 32 | .hidden { 33 | display: none; 34 | } 35 | .pointer { 36 | cursor: pointer; 37 | } 38 | 39 | .text-center { 40 | text-align: center; 41 | } 42 | .text-left { 43 | text-align: left; 44 | } 45 | .text-right { 46 | text-align: right; 47 | } 48 | .text-nowrap { 49 | white-space: nowrap; 50 | } 51 | .line-through { 52 | text-decoration: line-through 53 | } 54 | 55 | 56 | // flex start 57 | .flex-row { 58 | display: flex; 59 | flex-direction: row; 60 | align-items: center; 61 | } 62 | .flex-column { 63 | display: flex; 64 | flex-direction: column; 65 | } 66 | .flex-center { 67 | display: flex; 68 | justify-content: center; 69 | align-items: center; 70 | } 71 | .flex-wrap { 72 | display: flex; 73 | flex-wrap: wrap; 74 | } 75 | .flex-start { 76 | display: flex; 77 | justify-content: flex-start; 78 | } 79 | .flex-end { 80 | display: flex; 81 | justify-content: flex-end; 82 | } 83 | .flex-space-between { 84 | display: flex; 85 | justify-content: space-between; 86 | } 87 | 88 | .flex-space-around { 89 | display: flex; 90 | justify-content: space-around; 91 | } 92 | 93 | .flex-space-evenly { 94 | display: flex; 95 | justify-content: space-evenly; 96 | } 97 | 98 | .flex-1 { 99 | flex: 1; 100 | } 101 | 102 | // flex end 103 | 104 | .label-required { 105 | &::before { 106 | content: '*'; 107 | display: inline-block; 108 | margin-right: 4px; 109 | line-height: 1; 110 | font-family: SimSun; 111 | font-size: $base-font-size; 112 | color: $color-red; 113 | } 114 | } 115 | // 字体加粗 116 | .bold { 117 | font-weight: bold; 118 | } 119 | .border-bottom-0 { 120 | border-bottom: 0px; 121 | } 122 | 123 | .border-1 { 124 | border: 1px solid $border-color; 125 | } 126 | 127 | .link { 128 | color: #2d8cf0; 129 | cursor: pointer; 130 | &:hover { 131 | text-decoration: underline; 132 | } 133 | } 134 | .f12 { 135 | font-size: 12px; 136 | } 137 | .f13 { 138 | font-size: 13px; 139 | } 140 | .f14 { 141 | font-size: 14px; 142 | } 143 | .f15 { 144 | font-size: 15px; 145 | } 146 | .f16 { 147 | font-size: 16px; 148 | } 149 | .f18 { 150 | font-size: 18px; 151 | } 152 | .f20 { 153 | font-size: 20px; 154 | } 155 | 156 | .t1 { 157 | color: #1f2f3d; 158 | } 159 | .t2 { 160 | color: #606266; 161 | } 162 | .t3 { 163 | color: #909399; 164 | } 165 | 166 | .ellipsis { 167 | text-overflow: ellipsis; 168 | display: -webkit-box; 169 | -webkit-box-orient: vertical; 170 | -webkit-line-clamp: 1; 171 | overflow: hidden; 172 | } 173 | 174 | .ellipsis-2 { 175 | text-overflow: ellipsis; 176 | display: -webkit-box; 177 | -webkit-box-orient: vertical; 178 | -webkit-line-clamp: 2; 179 | overflow: hidden; 180 | } 181 | 182 | .overflow-hidden { 183 | overflow: hidden; 184 | } 185 | 186 | .color-red { 187 | color: $color-red; 188 | } 189 | 190 | .color-white { 191 | color: #ffffff; 192 | } 193 | 194 | .border-bottom-1 { 195 | position: relative; 196 | &::after { 197 | position: absolute; 198 | width: 100%; 199 | z-index: 10; 200 | bottom: 0; 201 | left: 0; 202 | display: block; 203 | content: ''; 204 | height: 1px; 205 | transform: scaleY(.5); 206 | background-color: #e5e5e5; 207 | } 208 | } 209 | 210 | .border-top-1 { 211 | position: relative; 212 | &::after { 213 | position: absolute; 214 | width: 100%; 215 | z-index: 10; 216 | top: 0; 217 | left: 0; 218 | display: block; 219 | content: ''; 220 | height: 1px; 221 | transform: scaleY(.5); 222 | background-color: #e5e5e5; 223 | } 224 | } 225 | 226 | .border-right-1 { 227 | position: relative; 228 | &::before { 229 | position: absolute; 230 | width: 1px; 231 | z-index: 10; 232 | top: 0; 233 | right: 0; 234 | display: block; 235 | content: ''; 236 | height: 100%; 237 | transform: scaleX(.5); 238 | background-color: #e5e5e5; 239 | } 240 | } 241 | 242 | .border-left-1::before { 243 | position: absolute; 244 | width: 1px; 245 | z-index: 10; 246 | top: 0; 247 | left: 0; 248 | display: block; 249 | content: ''; 250 | height: 100%; 251 | transform: scaleX(0.5); 252 | background-color: #e5e5e5; 253 | } 254 | 255 | .border-bottom-dotted-1 { 256 | position: relative; 257 | &::after { 258 | position: absolute; 259 | width: 100%; 260 | z-index: 10; 261 | bottom: 0; 262 | left: 0; 263 | display: block; 264 | content: ''; 265 | border-bottom: 1px dotted #e5e5e5; 266 | transform: scaleY(.5); 267 | } 268 | } -------------------------------------------------------------------------------- /src/components/zx-upload/utils/cos-auth.js: -------------------------------------------------------------------------------- 1 | var CryptoJS = require('crypto-js'); 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | // 和 cam 保持一致的 url encode 7 | function camSafeUrlEncode(str) { 8 | return encodeURIComponent(str) 9 | .replace(/!/g, '%21') 10 | .replace(/'/g, '%27') 11 | .replace(/\(/g, '%28') 12 | .replace(/\)/g, '%29') 13 | .replace(/\*/g, '%2A'); 14 | } 15 | 16 | // v4 签名 17 | var CosAuthV4 = function(opt) { 18 | var pathname = opt.Pathname || '/'; 19 | var expires = opt.Expires; 20 | 21 | var ShortBucketName = ''; 22 | var AppId = ''; 23 | var match = opt.Bucket.match(/^(.+)-(\d+)$/); 24 | if (match) { 25 | ShortBucketName = match[1]; 26 | AppId = match[2]; 27 | } 28 | 29 | var random = parseInt(Math.random() * Math.pow(2, 32)); 30 | var now = parseInt(Date.now() / 1000); 31 | var e = now + (expires === undefined ? 900 : expires * 1 || 0); // 默认签名过期时间为当前时间 + 900s 32 | var path = 33 | '/' + 34 | AppId + 35 | '/' + 36 | ShortBucketName + 37 | encodeURIComponent(pathname).replace(/%2F/g, '/'); // 多次签名这里填空 38 | var plainText = 39 | 'a=' + 40 | AppId + 41 | '&b=' + 42 | ShortBucketName + 43 | '&k=' + 44 | opt.SecretId + 45 | '&e=' + 46 | e + 47 | '&t=' + 48 | now + 49 | '&r=' + 50 | random + 51 | '&f=' + 52 | path; 53 | var sha1Res = CryptoJS.HmacSHA1(plainText, opt.SecretKey); 54 | var strWordArray = CryptoJS.enc.Utf8.parse(plainText); 55 | var resWordArray = sha1Res.concat(strWordArray); 56 | var sign = resWordArray.toString(CryptoJS.enc.Base64); 57 | 58 | return sign; 59 | }; 60 | 61 | // v5 签名 62 | var CosAuth = function(opt) { 63 | if (!opt.SecretId) return console.error('missing param SecretId'); 64 | if (!opt.SecretKey) return console.error('missing param SecretKey'); 65 | 66 | if (opt.Version === '4.0') { 67 | return CosAuthV4(opt); 68 | } 69 | 70 | opt = opt || {}; 71 | 72 | var SecretId = opt.SecretId; 73 | var SecretKey = opt.SecretKey; 74 | var method = (opt.Method || 'get').toLowerCase(); 75 | var query = opt.Query || {}; 76 | var headers = opt.Headers || {}; 77 | var pathname = opt.Pathname || '/'; 78 | var expires = opt.Expires; 79 | 80 | var getObjectKeys = function(obj) { 81 | var list = []; 82 | for (var key in obj) { 83 | if (obj.hasOwnProperty(key)) { 84 | list.push(key); 85 | } 86 | } 87 | return list.sort(function(a, b) { 88 | a = a.toLowerCase(); 89 | b = b.toLowerCase(); 90 | return a === b ? 0 : a > b ? 1 : -1; 91 | }); 92 | }; 93 | 94 | var obj2str = function(obj) { 95 | var i, key, val; 96 | var list = []; 97 | var keyList = getObjectKeys(obj); 98 | for (i = 0; i < keyList.length; i++) { 99 | key = keyList[i]; 100 | val = obj[key] === undefined || obj[key] === null ? '' : '' + obj[key]; 101 | key = key.toLowerCase(); 102 | key = camSafeUrlEncode(key); 103 | val = camSafeUrlEncode(val) || ''; 104 | list.push(key + '=' + val); 105 | } 106 | return list.join('&'); 107 | }; 108 | 109 | // 签名有效起止时间 110 | var now = parseInt(new Date().getTime() / 1000) - 1; 111 | var exp = now + (expires === undefined ? 900 : expires * 1 || 0); // 默认签名过期时间为当前时间 + 900s 112 | 113 | // 要用到的 Authorization 参数列表 114 | var qSignAlgorithm = 'sha1'; 115 | var qAk = SecretId; 116 | var qSignTime = now + ';' + exp; 117 | var qKeyTime = now + ';' + exp; 118 | var qHeaderList = getObjectKeys(headers) 119 | .join(';') 120 | .toLowerCase(); 121 | var qUrlParamList = getObjectKeys(query) 122 | .join(';') 123 | .toLowerCase(); 124 | 125 | // 签名算法说明文档:https://www.qcloud.com/document/product/436/7778 126 | // 步骤一:计算 SignKey 127 | var signKey = CryptoJS.HmacSHA1(qKeyTime, SecretKey).toString(); 128 | 129 | // 步骤二:构成 FormatString 130 | var formatString = [ 131 | method, 132 | pathname, 133 | obj2str(query), 134 | obj2str(headers), 135 | '' 136 | ].join('\n'); 137 | 138 | // 步骤三:计算 StringToSign 139 | var stringToSign = [ 140 | 'sha1', 141 | qSignTime, 142 | CryptoJS.SHA1(formatString).toString(), 143 | '' 144 | ].join('\n'); 145 | 146 | // 步骤四:计算 Signature 147 | var qSignature = CryptoJS.HmacSHA1(stringToSign, signKey).toString(); 148 | 149 | // 步骤五:构造 Authorization 150 | var authorization = [ 151 | 'q-sign-algorithm=' + qSignAlgorithm, 152 | 'q-ak=' + qAk, 153 | 'q-sign-time=' + qSignTime, 154 | 'q-key-time=' + qKeyTime, 155 | 'q-header-list=' + qHeaderList, 156 | 'q-url-param-list=' + qUrlParamList, 157 | 'q-signature=' + qSignature 158 | ].join('&'); 159 | 160 | return authorization; 161 | }; 162 | 163 | if (typeof module === 'object') { 164 | module.exports = CosAuth; 165 | } else { 166 | window.CosAuth = CosAuth; 167 | } 168 | })(); 169 | -------------------------------------------------------------------------------- /src/components/zx-upload/zx-image-upload/template.vue: -------------------------------------------------------------------------------- 1 | 119 | 120 | 180 | -------------------------------------------------------------------------------- /src/components/vue-json-form/template.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 221 | -------------------------------------------------------------------------------- /src/components/vue-json-form/form.js: -------------------------------------------------------------------------------- 1 | import { setValueByObjectPath, getValueByObjectPath } from './objec-path'; 2 | import prefixSuffix from './prefix-suffix'; 3 | import { validAll } from '../zx-validator/directive'; 4 | import { debounce } from '../commons/utils'; 5 | export default { 6 | name: 'VueJsonForm', 7 | components: { 8 | prefixSuffix 9 | }, 10 | props: { 11 | vid: { 12 | type: String 13 | }, 14 | value: { 15 | type: Object, 16 | default: () => { 17 | return {}; 18 | } 19 | }, 20 | list: { 21 | type: Array, 22 | required: true, 23 | default: () => { 24 | return []; 25 | } 26 | }, 27 | className: { 28 | type: Array, 29 | default: () => { 30 | return []; 31 | } 32 | }, 33 | // 全局配置表单中各个组件触发验证的事件名(change|blur) 34 | trigger: { 35 | type: String 36 | }, 37 | // 全局配置表单中各个组件销毁时是否清空数据 38 | destroyClearData: { 39 | type: Boolean 40 | }, 41 | // 表单嵌套循环时,当前表单的索引 42 | rowIndex: { 43 | type: Number 44 | }, 45 | editable: { 46 | type: Boolean, 47 | default: true 48 | } 49 | }, 50 | data() { 51 | return { 52 | formData: {}, 53 | initFormData: {}, 54 | data: {}, 55 | // 所有节点数组 56 | leafItems: [] 57 | }; 58 | }, 59 | provide() { 60 | return { 61 | getFormVm: () => { 62 | return this; 63 | } 64 | }; 65 | }, 66 | created() { 67 | this.onChangeItemValue = debounce(this.onChangeItemValueFn, 0); 68 | this.init(); 69 | }, 70 | mounted() { 71 | // this.initUploadCompWidth(); 72 | }, 73 | methods: { 74 | // 初始化日期 75 | init() { 76 | this.formData = this.value; 77 | this.showFormItem(); 78 | this.initFormData = JSON.parse(JSON.stringify(this.formData)); 79 | // 初始化设置formData的值,读取配置中的defaultValue 80 | this.setFormData(); 81 | }, 82 | 83 | // 设置表单数据 84 | setFormData() { 85 | for (let item of this.list) { 86 | this.setValueItem(item); 87 | } 88 | }, 89 | 90 | /** 91 | * 如果formData不存在field字段,则设置默认值等于defaultValue 92 | * @param {Object} config 93 | */ 94 | setValueItem(config) { 95 | if (!config.field) { 96 | return; 97 | } 98 | // value必须为空数组的组件 99 | let arrDefaults = ['checkbox-group', 'add-minus']; 100 | if (config.field && config.field.includes('.')) { 101 | let v = getValueByObjectPath(this.value, config.field); 102 | this.$set(this.formData, config.field, v); 103 | } else if (!this.formData.hasOwnProperty(config.field)) { 104 | let value; 105 | if (config.hasOwnProperty('defaultValue')) { 106 | value = config.defaultValue; 107 | } 108 | if (arrDefaults.includes(config.type) && this.$$is.empty(value)) { 109 | value = []; 110 | } 111 | this.$set(this.formData, config.field, value); 112 | } else if (config.field) { 113 | let value = this.formData[config.field]; 114 | if (arrDefaults.includes(config.type) && this.$$is.empty(value)) { 115 | this.formData[config.field] = []; 116 | } 117 | } 118 | }, 119 | 120 | showFormItem(list) { 121 | list = list || this.list; 122 | for (let item of list) { 123 | if (item.vif) { 124 | this.$set(item, '_vif', item.vif(this.formData)); 125 | } else { 126 | this.$set(item, '_vif', true); 127 | } 128 | if (item.list) { 129 | this.showFormItem(item.list); 130 | } 131 | if (Array.isArray(item.suffix)) { 132 | this.showFormItem(item.suffix); 133 | } 134 | } 135 | }, 136 | 137 | // 设置上传组件宽度为100% 138 | initUploadCompWidth() { 139 | let wrapper = this.$refs.wrapper; 140 | let uploads = wrapper.querySelectorAll('.zx-image-upload'); 141 | let files = wrapper.querySelectorAll('.zx-file-upload'); 142 | uploads = [...uploads].concat([...files]); 143 | let clientWidth = wrapper.clientWidth; 144 | let labelWidth = this.$attrs['label-width'] || '0px'; 145 | labelWidth = Number(labelWidth.replace('px', '')); 146 | uploads.forEach(el => { 147 | if (el.offsetParent) { 148 | el.offsetParent.style.width = clientWidth - labelWidth + 'px'; 149 | } 150 | }); 151 | }, 152 | /** 153 | * 如果fieldList=[],校验整个表单。否则校验数组中的元素 154 | * @param {Array} fieldList 字段名数组 155 | */ 156 | async valid(fieldList = [], cacheMsg = false) { 157 | let validator = this.$refs.validator; 158 | if (!validator || !validator.$el) { 159 | return; 160 | } 161 | if (fieldList.length) { 162 | // 校验部分标签 163 | let validResult = await validator.valid(fieldList, cacheMsg); 164 | if (cacheMsg) { 165 | return { 166 | matched: validResult.matched, 167 | message: validResult.message 168 | }; 169 | } 170 | return validResult === true; 171 | } 172 | // 校验全部,返回布尔值 173 | let valided = await validAll(validator.$el); 174 | return valided; 175 | }, 176 | 177 | // 通过字段名获取配置 178 | findItem(field) { 179 | for (let item of this.list) { 180 | if (field === item.field && item._vif !== false) { 181 | return item; 182 | } 183 | } 184 | }, 185 | // value变化时,根据path设置value 186 | setPathValue() { 187 | for (let p in this.formData) { 188 | if (p.includes('.')) { 189 | setValueByObjectPath(this.formData, p, this.formData[p]); 190 | } 191 | } 192 | }, 193 | /** 194 | * 获取初始化formData 195 | */ 196 | getInitFormData() { 197 | return this.initFormData; 198 | }, 199 | /** 200 | * 清空表单值 201 | */ 202 | resetFields(formData = {}) { 203 | for (let item of this.leafItems) { 204 | if (item.validResult) { 205 | item.validResult = undefined; 206 | } 207 | } 208 | this.formData = formData; 209 | this.valid(); 210 | }, 211 | onChangeItemValueFn() { 212 | this.setPathValue(); 213 | this.showFormItem(); 214 | this.setFormData(); 215 | } 216 | }, 217 | watch: { 218 | value: { 219 | handler() { 220 | this.formData = this.value; 221 | this.onChangeItemValue(); 222 | } 223 | }, 224 | list: { 225 | handler() { 226 | this.onChangeItemValue(); 227 | } 228 | } 229 | } 230 | }; 231 | -------------------------------------------------------------------------------- /src/components/zx-add-minus/script.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'zxAddMinus', 3 | props: { 4 | value: { 5 | type: Array, 6 | default: () => [] 7 | }, 8 | item: { 9 | type: Object 10 | }, 11 | // 限制最大个数 12 | maxlength: { 13 | type: Number 14 | }, 15 | // 是否换行 16 | inline: { 17 | type: Boolean, 18 | default: true 19 | }, 20 | // 是否有分割线 21 | divider: { 22 | type: Boolean, 23 | default: true 24 | }, 25 | editable: { 26 | type: Boolean, 27 | default: true 28 | }, 29 | showAddIcon: { 30 | type: Boolean, 31 | default: true 32 | }, 33 | showDelIcon: { 34 | type: Boolean, 35 | default: true 36 | }, 37 | trigger: { 38 | type: String 39 | }, 40 | // 第一行是否保留(被删除) 41 | keepOne: { 42 | type: Boolean, 43 | default: true 44 | }, 45 | defaultRowData: { 46 | type: Object 47 | } 48 | }, 49 | inject: ['getFormVm'], 50 | data() { 51 | return { 52 | maxIdx: 0, 53 | addList: [], 54 | attrs: {} 55 | }; 56 | }, 57 | created() { 58 | let formVm = this.getFormVm(); 59 | this.attrs = formVm.$attrs; 60 | this.init(); 61 | this.item.getInstance && this.item.getInstance(this.getInstance(this)); 62 | }, 63 | methods: { 64 | /** 65 | * 初始化如果value有长度,那么addList展示和value保持一致; 66 | * 否则addList新加一行,value添加新数据 67 | */ 68 | init() { 69 | if (this.value.length) { 70 | let list = []; 71 | this.value.forEach((item, index) => { 72 | item.$idx = index; 73 | let rowConfig = this.$$utils.deepClone(this.item.list); 74 | list.push({ 75 | $idx: index, 76 | list: rowConfig 77 | }); 78 | }); 79 | this.addList = list; 80 | this.maxIdx = this.value.length - 1; 81 | } else if (this.keepOne) { 82 | this.addNewRow(); 83 | } 84 | }, 85 | /** 86 | * 添加新的一行配置和value 87 | */ 88 | addNewRow() { 89 | let rowConfig = this.getNewRowConfig(); 90 | let value = this.getNewRowValue(); 91 | this.addList.push(rowConfig); 92 | this.value.push(value); 93 | }, 94 | 95 | getInstance() { 96 | return this; 97 | }, 98 | /** 99 | * 添加行,index、target、value为新的行和数据 100 | */ 101 | async _onAdd(data) { 102 | if (this.maxlength && this.addList.length >= this.maxlength) { 103 | this.$message({ 104 | message: `超过最大限制${this.maxlength}`, 105 | type: 'warning' 106 | }); 107 | return; 108 | } 109 | // 添加value 110 | let rowConfig = this.getNewRowConfig(); 111 | let formData = this.getNewRowValue(data); 112 | // 添加新的rowConfig,和新formData 113 | let isAdd = await this.rowChange( 114 | 'add', 115 | this.addList.length, 116 | rowConfig, 117 | formData 118 | ); 119 | // 点击添加,只要不返回false就证明可以添加 120 | if (isAdd !== false) { 121 | this.value.push(formData); 122 | this.addList.push(rowConfig); 123 | } 124 | }, 125 | 126 | /** 127 | * 删除行,index、target、value为当前被删除行 128 | */ 129 | async _onDel(data) { 130 | let index = 0; 131 | if (this.$$is.object(data)) { 132 | index = this.value.findIndex(item => item.$idx === data.$idx); 133 | } else { 134 | index = data; 135 | } 136 | let delRow = this.addList[index]; 137 | let delRowValue = this.value[index]; 138 | // 删除前 139 | let isDel = await this.rowChange('delete', index, delRow, delRowValue); 140 | if (isDel !== false) { 141 | this.addList.splice(index, 1); 142 | this.value.splice(index, 1); 143 | } 144 | }, 145 | 146 | addRow(data = {}) { 147 | this._onAdd(data); 148 | }, 149 | 150 | delRow(data = {}) { 151 | this._onDel(data); 152 | }, 153 | 154 | /** 155 | * 只有点击添加和删除才会调用rowChange 156 | */ 157 | rowChange(type, index, target, value) { 158 | let rowChange = this.item.rowChange || this.$attrs.rowChange; 159 | if (rowChange) { 160 | let res = rowChange({ 161 | type: type, 162 | ...this.getParams(index, target, value) 163 | }); 164 | return res; 165 | } 166 | }, 167 | 168 | getParams(index, target, targetValue) { 169 | return { 170 | $forms: this.$refs.addMinus, 171 | index, 172 | values: this.value, 173 | targetValue, 174 | target, 175 | addList: this.addList, 176 | ctx: this.$vnode.context, 177 | formData: this.$vnode.context.formData 178 | }; 179 | }, 180 | 181 | // 获得新知行配置 182 | getNewRowConfig() { 183 | let maxIdx = this.getMaxIdx(); 184 | let rowConfig = this.$$utils.deepClone(this.item.list); 185 | for (let item of rowConfig) { 186 | delete item.validResult; 187 | delete item.$el; 188 | } 189 | return { 190 | $idx: maxIdx, 191 | list: rowConfig 192 | }; 193 | }, 194 | 195 | getNewRowValue(data) { 196 | if (!this.$$is.empty(data)) { 197 | return JSON.parse(JSON.stringify(data)); 198 | } 199 | // let initFormData = this.getFormVm().getInitFormData(); 200 | // let value = initFormData[this.item.field]; 201 | // let v = value && value.length ? value[0] : {}; 202 | // let formData = this.$$utils.deepClone(v); 203 | let formData = this.defaultRowData 204 | ? JSON.parse(JSON.stringify(this.defaultRowData)) 205 | : {}; 206 | // getNewRowConfig已计算过$idx 207 | formData.$idx = this.maxIdx; 208 | return formData; 209 | }, 210 | 211 | getMaxIdx() { 212 | this.maxIdx += 1; 213 | return this.maxIdx; 214 | }, 215 | /** 216 | * 如果value为空,清空addList并且新空行数据 217 | * 如果value不为空,删除addList中在value存在但addList不存在的项 218 | */ 219 | delDiff() { 220 | if (this.$$is.empty(this.value) && this.keepOne) { 221 | this.addList = []; 222 | this.$nextTick(() => { 223 | this.addNewRow(); 224 | }); 225 | return; 226 | } 227 | let delArr = []; 228 | this.addList.forEach(item => { 229 | let res = this.value.find(el => el.$idx === item.$idx); 230 | if (!res) { 231 | delArr.push(item.$idx); 232 | } 233 | }); 234 | this.addList = this.addList.filter(item => !delArr.includes(item.$idx)); 235 | } 236 | }, 237 | watch: { 238 | value: { 239 | handler() { 240 | // value.push 241 | if (this.value.length > this.addList.length) { 242 | let len1 = this.value.length; 243 | let len2 = this.addList.length; 244 | for (let i = 0; i < len1 - len2; i++) { 245 | let rowConfig = this.getNewRowConfig(); 246 | this.addList.push(rowConfig); 247 | } 248 | } else if (this.value.length < this.addList.length) { 249 | // value splice 250 | this.delDiff(); 251 | } 252 | // if (this.item && this.item.tableCellValueChange) { 253 | // this.item.tableCellValueChange(this.value); 254 | // } 255 | } 256 | } 257 | } 258 | }; 259 | -------------------------------------------------------------------------------- /src/components/zx-validator/validator.vue: -------------------------------------------------------------------------------- 1 | 6 | 255 | -------------------------------------------------------------------------------- /src/components/zx-select-remote/select-remote.vue: -------------------------------------------------------------------------------- 1 | 70 | 318 | 319 | -------------------------------------------------------------------------------- /src/components/zx-form-item/template.vue: -------------------------------------------------------------------------------- 1 | 294 | 295 | 340 | -------------------------------------------------------------------------------- /src/components/zx-form-item/form-item.js: -------------------------------------------------------------------------------- 1 | import is from '../commons/is'; 2 | import { setValueByObjectPath } from '../vue-json-form/objec-path'; 3 | 4 | export default { 5 | name: 'zxFormItem', 6 | props: { 7 | item: { 8 | type: Object 9 | }, 10 | value: {}, 11 | formData: { 12 | type: Object 13 | }, 14 | rowIndex: { 15 | type: Number 16 | }, 17 | editable: { 18 | type: Boolean, 19 | default: true 20 | } 21 | }, 22 | inject: ['getFormVm'], 23 | data() { 24 | return { 25 | RANGE_TYPES: ['datetimerange', 'daterange', 'timerange'], 26 | data: undefined, 27 | clearable: true 28 | }; 29 | }, 30 | watch: { 31 | value: { 32 | handler(newVal, oldVal) { 33 | if (newVal !== oldVal) { 34 | this.data = this.value; 35 | this.changeHandler(); 36 | if (this.item.hasOwnProperty('validResult')) { 37 | this.item.validResult = undefined; 38 | } 39 | } 40 | }, 41 | deep: true 42 | }, 43 | data(newVal, oldVal) { 44 | if (newVal !== oldVal && this.data !== this.value) { 45 | this.$emit('input', newVal); 46 | } 47 | } 48 | }, 49 | created() { 50 | // 初始化赋值 51 | this.data = this.value; 52 | this.clearable = this.getClearAble(); 53 | this.$nextTick(() => { 54 | this.getConfig(); 55 | }); 56 | }, 57 | mounted() { 58 | // 添加挂在$el 59 | this.item.$el = this.$refs.formItem; 60 | let uvId = this.getRootUVid(); 61 | if (uvId) { 62 | this.$eventBus.$on(uvId, data => { 63 | let params = this.getParam(); 64 | params.globalFormData = data; 65 | this.item.valueChange && this.item.valueChange(params); 66 | }); 67 | } 68 | }, 69 | methods: { 70 | // 找到最上层的form vid。 71 | // vid决定了valueChange的作用范围,valueChange被触发于vid标签下的表单中,且从下往上逐级拆分vid。 72 | getRootUVid() { 73 | let root; 74 | let parent = this.$parent; 75 | while (parent) { 76 | if (parent.$props && parent.$props.vid) { 77 | root = parent; 78 | parent = null; 79 | break; 80 | } 81 | parent = parent.$parent; 82 | } 83 | if (root && root.$props && root.$props.vid) { 84 | let vid = root.$props.vid; 85 | let uuid = root.$el.getAttribute('data-uuid'); 86 | let uvId = uuid + vid; 87 | return uvId; 88 | } 89 | }, 90 | getClearAble() { 91 | if (this.item.attrs) { 92 | if (this.item.attrs.clearable === false) { 93 | return false; 94 | } 95 | } 96 | return true; 97 | }, 98 | 99 | /** 100 | * 处理函数回调 101 | * option, data, text可为数组,函数 102 | */ 103 | async getConfig() { 104 | let param = this.getParam(); 105 | if (is.function(this.item.optionsAsync)) { 106 | let _options = await this.item.optionsAsync(param); 107 | this.$set(this.item, 'options', _options); 108 | } 109 | if (is.function(this.item.dataAsync)) { 110 | let _data = await this.item.dataAsync(param); 111 | this.$set(this.item, 'data', _data); 112 | } 113 | if (is.function(this.item.textAsync)) { 114 | let _text = await this.item.textAsync(param); 115 | this.$set(this.item, 'text', _text); 116 | } 117 | // 初始化调用changeHandler事件 118 | this.changeHandler(); 119 | }, 120 | getDateAttrs({ rules = {}, attrs = {} }, format) { 121 | let attr = Object.assign({}, rules, attrs); 122 | attr.placeholder = (attr.placeholder || '请选择') + this.item.label; 123 | attr['range-separator'] = attr['range-separator'] || '至'; 124 | attr['value-format'] = attr['value-format'] || format; 125 | if (attr.valid) { 126 | attr.valid = ''; 127 | } 128 | return attr; 129 | }, 130 | getAttrs({ rules = {}, attrs = {} }, placeholder = '请选择') { 131 | let attr = Object.assign({}, rules, attrs); 132 | if (attr.valid) { 133 | attr.valid = ''; 134 | } 135 | // 扁平数据结构不需要验证 136 | if (rules && rules.required && this.item.deepData !== false) { 137 | attr.validator = this.item.label; 138 | } 139 | attr.placeholder = attr.placeholder || placeholder + this.item.label; 140 | return attr; 141 | }, 142 | // 如果非通过鼠标事件触发onChange,则会出现表单初始化会校验不通过的提示 143 | // 目前校验过input、select通过formData改值不会触发 144 | onChange(e) { 145 | let v = this.isEmpty(e) ? '' : e; 146 | this.setPathValue(v); 147 | this.changeHandler(v); 148 | }, 149 | 150 | // value变化时,根据path设置value 151 | setPathValue(v) { 152 | if (this.item.field.includes('.')) { 153 | setValueByObjectPath(this.formData, this.item.field, v); 154 | } 155 | }, 156 | /** 157 | * 如果e等于undefined或者null,不需要触发校验。当点击保存按钮的时候,不会走这里的changeHandler方法。 158 | */ 159 | changeHandler(e) { 160 | let formVm = this.getFormVm(); 161 | this.$emit('input', this.data); 162 | this.$emit('change', this.data); 163 | if (is.function(this.item.change)) { 164 | // 给item添加value属性并赋值 165 | this.$set(this.item, 'value', this.data); 166 | // 防止change和watch导致执行两次item.change 167 | if (this.t) return; 168 | this.t = setTimeout(async () => { 169 | let needValid = await this.item.change(this.getParam(e)); 170 | this.t = null; 171 | if (this.isEmpty(e)) { 172 | return; 173 | } 174 | this.handleResult(needValid); 175 | }); 176 | } else if ( 177 | (this.validTrigger('change') && 178 | !this.item.blur && 179 | this.item.change !== false) || 180 | this.item.change === true 181 | ) { 182 | if (this.isEmpty(e)) { 183 | return; 184 | } 185 | this.$nextTick(async () => { 186 | let validResult = await formVm.valid([this.item.field], true); 187 | this.item.validResult = validResult; 188 | }); 189 | } 190 | }, 191 | // 如果value等于null|undefined|[undefined, undefined]|[null, null]返回true 192 | isEmpty(e) { 193 | if (typeof e === 'undefined' || e === null) { 194 | return true; 195 | } 196 | if (Array.isArray(e) && this.RANGE_TYPES.includes(this.item.type)) { 197 | return e.every(item => this.isEmpty(item)); 198 | } 199 | return false; 200 | }, 201 | /** 202 | * 获取form配置中是否存在trigger=“change”或trigger="blur" 203 | * @param {String} eventName 触发表单验证的事件名 (change| blur) 204 | */ 205 | validTrigger(eventName) { 206 | let formVm = this.getFormVm(); 207 | if (formVm.$attrs) { 208 | if (formVm.trigger === eventName) { 209 | return true; 210 | } 211 | } 212 | }, 213 | 214 | // 失去焦点事件 215 | async onBlur(e) { 216 | // 等于true代表需要失去焦点时验证当前元素 217 | let formVm = this.getFormVm(); 218 | this.$emit('input', this.data); 219 | if (is.function(this.item.blur)) { 220 | this.$set(this.item, 'value', this.data); 221 | // 如果blur函数返回true,代表需要校验 222 | if (is.function(this.item.blur)) { 223 | let needValid = this.item.blur(this.getParam(e)); 224 | this.handleResult(needValid); 225 | } 226 | } else if (this.validTrigger('blur') || this.item.blur === true) { 227 | this.$nextTick(async () => { 228 | let validResult = await formVm.valid([this.item.field], true); 229 | this.item.validResult = validResult; 230 | }); 231 | } 232 | }, 233 | onFocus(e) { 234 | if (is.function(this.item.focus)) { 235 | this.item.focus(this.getParam(e)); 236 | } 237 | }, 238 | // 按钮类型点击事件 239 | onClick(e) { 240 | if (this.item.click) { 241 | this.item.click(this.getParam(e)); 242 | } 243 | }, 244 | getParam(e) { 245 | let ctx = this.getContext(); 246 | let formVm = this.getFormVm(); 247 | return { 248 | formData: formVm.formData, 249 | ctx: ctx, 250 | config: this.item, 251 | value: this.data, 252 | vm: this, 253 | $form: formVm, 254 | findItem: formVm.findItem, 255 | e: e, 256 | rowIndex: this.rowIndex 257 | }; 258 | }, 259 | // 必须等于true才能触发校验 260 | async handleResult(needValid) { 261 | let formVm = this.getFormVm(); 262 | let validResult; 263 | if (needValid instanceof Promise) { 264 | let res = await needValid; 265 | if (res === true) { 266 | validResult = await formVm.valid([this.item.field], true); 267 | } 268 | } else if (needValid === true) { 269 | validResult = await formVm.valid([this.item.field], true); 270 | } 271 | // 判断是否需要change时验证 272 | if (this.validTrigger('change')) { 273 | validResult = await formVm.valid([this.item.field], true); 274 | } 275 | this.item.validResult = validResult; 276 | }, 277 | getContext() { 278 | let formVm = this.getFormVm(); 279 | return formVm.$vnode.context || formVm; 280 | }, 281 | getDisabled() { 282 | if (this.item.attrs) { 283 | if (this.$$is.function(this.item.attrs.disabled)) { 284 | let flag = this.item.attrs.disabled(this.getParam()); 285 | return flag; 286 | } 287 | return this.item.attrs.disabled; 288 | } 289 | return false; 290 | } 291 | }, 292 | computed: { 293 | displayLabel() { 294 | if (!this.editable) { 295 | // 执行display方法 296 | if (is.function(this.item.display)) { 297 | return this.item.display(this.getParam()); 298 | } 299 | // 支持mapList 300 | if (is.object(this.item.mapList)) { 301 | let label = this.item.mapList[this.value]; 302 | return label; 303 | } 304 | // 支持mapList 305 | if (Array.isArray(this.item.mapList)) { 306 | let { label } = 307 | this.item.mapList.find(item => item.value === this.value) || {}; 308 | return label; 309 | } 310 | } 311 | } 312 | }, 313 | beforeDestroy() { 314 | let formVm = this.getFormVm(); 315 | // 如果设置destroyClearData属性,在组件销毁时清空该属性的值。 316 | // 发生在触发v-if=true的时候,应用场景如级联操作中。 317 | if ( 318 | (this.item.attrs && this.item.attrs.destroyClearData) || 319 | formVm.destroyClearData || 320 | formVm.$attrs.destroyClearData 321 | ) { 322 | this.$emit('input', undefined); 323 | this.item.value = undefined; 324 | } 325 | if (this.item.validResult) { 326 | this.item.validResult = undefined; 327 | } 328 | } 329 | }; 330 | -------------------------------------------------------------------------------- /src/components/zx-upload/zx-image-upload/image-upload.js: -------------------------------------------------------------------------------- 1 | import is from '../../commons/is'; 2 | import { Message } from 'element-ui'; 3 | import CosUpload from '../utils/cosUpload'; 4 | import compressImage from '../../commons/compress-image'; 5 | export default { 6 | name: 'zxImageUpload', 7 | props: { 8 | value: { 9 | type: [String, Array] 10 | }, 11 | beforeUpload: { 12 | type: Function 13 | }, 14 | successUpload: { 15 | type: Function 16 | }, 17 | scale: { 18 | type: [Number, Array] 19 | }, 20 | // 单位kb 21 | maxSize: { 22 | type: Number, 23 | default: 5120 24 | }, 25 | // 单位px 26 | width: { 27 | type: Number 28 | }, 29 | height: { 30 | type: Number 31 | }, 32 | format: { 33 | type: Array, 34 | default: () => ['.png', '.jpg', '.jpeg'] 35 | }, 36 | tips: { 37 | type: String 38 | }, 39 | multiple: { 40 | type: Boolean, 41 | default: false 42 | }, 43 | readonly: { 44 | type: Boolean 45 | }, 46 | multipleMax: { 47 | type: Number 48 | }, 49 | // 0:图片;1:视频;2:音频 3:其他文件 50 | type: { 51 | type: Number, 52 | default: 0 53 | }, 54 | buttonText: { 55 | type: String, 56 | default: '点击上传' 57 | }, 58 | // 多选时展示预览图片列表 59 | showPreview: { 60 | type: Boolean, 61 | default: true 62 | }, 63 | // 向URL后面添加原始文件名称 64 | urlQueryAddSrouceName: { 65 | type: Boolean, 66 | default: false 67 | }, 68 | // 2种视图形式,默认带input输入框,第二种thumbnail缩略图形式 69 | viewType: { 70 | type: String, 71 | default: 'input' 72 | }, 73 | imageWidth: { 74 | type: String, 75 | default: '50px' 76 | }, 77 | imageHeight: { 78 | type: String, 79 | default: '50px' 80 | }, 81 | footprint: { 82 | type: Boolean, 83 | default: false 84 | } 85 | }, 86 | data() { 87 | return { 88 | reqCount: 0, 89 | imageUrl: undefined, 90 | supportExtensions: /.png|.jpg|.jpeg/gi, 91 | // 多选时选中的预览图片 92 | selectedIndex: undefined, 93 | // 上传成功的URL 94 | successArr: [], 95 | accept: undefined 96 | }; 97 | }, 98 | watch: { 99 | value: { 100 | handler() { 101 | if (this.multiple) { 102 | this.imageUrl = !is.empty(this.value) ? this.value[0] : ''; 103 | this.successArr = this.value || []; 104 | this.selectedIndex = 0; 105 | return; 106 | } 107 | this.imageUrl = this.value; 108 | }, 109 | immediate: true 110 | } 111 | }, 112 | created() { 113 | this.accept = this.format 114 | .map(ext => `image/${ext.replace('.', '')}`) 115 | .join(','); 116 | }, 117 | mounted() { 118 | if (this.multiple) { 119 | this.imageUrl = !is.empty(this.value) ? this.value[0] : ''; 120 | this.successArr = this.value || []; 121 | this.selectedIndex = 0; 122 | } else { 123 | this.imageUrl = this.value; 124 | } 125 | }, 126 | methods: { 127 | handleClick() { 128 | this.reqCount = 0; 129 | if (this.multipleMax && this.successArr.length >= this.multipleMax) { 130 | Message.error('最多只能上传' + this.multipleMax + '张图片'); 131 | return; 132 | } 133 | this.$refs.file.click(); 134 | let _this = this; 135 | this.$refs.file.onchange = async function () { 136 | let passedCount = 0; 137 | let filesArr = []; 138 | if (_this.multipleMax) { 139 | let count = _this.successArr.length + this.files.length; 140 | if (count > _this.multipleMax) { 141 | Message.error('最多只能上传' + _this.multipleMax + '张图片'); 142 | return; 143 | } 144 | } 145 | for (let file of this.files) { 146 | let fileName = file.name; 147 | let kb = file.size / 1024; 148 | if (kb > 500) { 149 | compressImage({ 150 | file: file, 151 | // targetSize: 500, 152 | fileName: fileName, 153 | quality: 0.6, 154 | success(resultFile) { 155 | if (resultFile) { 156 | file = resultFile; 157 | console.log(file.size / 1024, 'kb'); 158 | } 159 | } 160 | }); 161 | } 162 | let extensions = fileName.substring(fileName.lastIndexOf('.')); 163 | fileName = fileName.replace(extensions, ''); 164 | let imageSize; 165 | try { 166 | imageSize = await _this.getImageSize(file); 167 | } catch (err) { 168 | console.log(err); 169 | imageSize = {}; 170 | } 171 | let { width, height } = imageSize; 172 | let result = _this.valid(file, width, height, extensions); 173 | if (!result) { 174 | _this.$refs.file.value = ''; 175 | return; 176 | } else { 177 | filesArr.push({ 178 | fileName, 179 | file, 180 | extensions, 181 | height, 182 | width, 183 | useType: 5 184 | }); 185 | passedCount++; 186 | } 187 | } 188 | if (passedCount === this.files.length) { 189 | for (const [index, options] of filesArr.entries()) { 190 | let fileInfo = await _this.upload({ 191 | file: options.file 192 | }); 193 | let url = fileInfo.path; 194 | if (!url) { 195 | if (_this.multiple) { 196 | _this.$emit('change', _this.successArr); 197 | if (!_this.imageUrl) { 198 | _this.imageUrl = _this.successArr[0]; 199 | } 200 | return; 201 | } 202 | } 203 | // 追加原始名称 204 | if (_this.urlQueryAddSrouceName) { 205 | url = url + '?sourceName=' + options.fileName; 206 | } 207 | if (!_this.multiple) { 208 | _this.imageUrl = url; 209 | _this.$emit('change', url); 210 | _this.$emit('input', url); 211 | } else { 212 | _this.successArr.push(url); 213 | if (index === filesArr.length - 1) { 214 | _this.$emit('change', _this.successArr); 215 | _this.$emit('input', _this.successArr); 216 | if (!_this.imageUrl) { 217 | _this.imageUrl = _this.successArr[0]; 218 | } 219 | } 220 | } 221 | _this.$emit('success', fileInfo); 222 | _this.successUpload && _this.successUpload(fileInfo); 223 | } 224 | } 225 | }; 226 | }, 227 | // 输入后同步v-model绑定值 228 | handleInputBlur() { 229 | if (this.multiple) { 230 | this.$emit('input', this.value); 231 | this.$emit('change', this.value); 232 | return; 233 | } 234 | this.$emit('input', this.imageUrl); 235 | this.$emit('change', this.imageUrl); 236 | }, 237 | async upload(options) { 238 | let fileInfo = ''; 239 | try { 240 | fileInfo = await CosUpload.uploadFile({ 241 | file: options.file, 242 | type: this.type, 243 | footprint: this.footprint 244 | }); 245 | this.$refs.file.value = ''; 246 | } catch (err) { 247 | console.log(err); 248 | this.$refs.file.value = ''; 249 | } 250 | return fileInfo; 251 | }, 252 | 253 | valid(file, width, height, extensions) { 254 | let result = true; 255 | if (this.beforeUpload) { 256 | result = this.beforeUpload.call(this.$vnode.context, file); 257 | } else { 258 | if (this.format.indexOf(extensions.toLowerCase()) < 0) { 259 | Message.error('不支持该图片类型!'); 260 | return false; 261 | } 262 | let kb = file.size / 1024; 263 | const isGtMax = kb > this.maxSize; 264 | if (isGtMax) { 265 | if (this.maxSize / 1024 >= 1) { 266 | Message.error(`上传图片大小不能超过${this.maxSize / 1024}MB!`); 267 | } else { 268 | Message.error(`上传图片大小不能超过${this.maxSize}KB!`); 269 | } 270 | return false; 271 | } 272 | if (this.width && width !== this.width) { 273 | Message.error(`图片宽必须为${this.width}px!`); 274 | return false; 275 | } 276 | if (this.height && height !== this.height) { 277 | Message.error(`图片高必须为${this.height}px!`); 278 | return false; 279 | } 280 | const scale = width / height; 281 | if (!Array.isArray(this.scale)) { 282 | if (this.scale && scale !== this.scale) { 283 | Message.error('图片宽高比例不正确'); 284 | return false; 285 | } 286 | } else { 287 | if (this.scale && !this.scale.includes(scale)) { 288 | Message.error('图片宽高比例不正确'); 289 | return false; 290 | } 291 | } 292 | } 293 | return result; 294 | }, 295 | 296 | getImageSize(file) { 297 | return new Promise((resolve, reject) => { 298 | let result = {}; 299 | let reader = new FileReader(); 300 | reader.onload = e => { 301 | var data = e.target.result; 302 | var image = new Image(); 303 | image.onload = () => { 304 | result.width = image.width; 305 | result.height = image.height; 306 | resolve(result); 307 | }; 308 | image.onerror = () => { 309 | this.$refs.file.value = ''; 310 | reject(new Error('图片加载失败')); 311 | }; 312 | image.src = data; 313 | }; 314 | reader.readAsDataURL(file); 315 | }); 316 | }, 317 | onClickPreview(url, index) { 318 | this.selectedIndex = index; 319 | this.imageUrl = url; 320 | }, 321 | onDelete(url, index) { 322 | this.successArr.splice(index, 1); 323 | this.$emit('input', this.successArr); 324 | if (this.imageUrl === url) { 325 | this.imageUrl = undefined; 326 | } 327 | }, 328 | getReadonly() { 329 | if (this.multiple || is.prod()) return true; 330 | return false; 331 | }, 332 | onChange() { 333 | if (this.imageUrl !== this.value) { 334 | this.$emit('change', this.imageUrl); 335 | this.$emit('input', this.imageUrl); 336 | } 337 | }, 338 | onImageError(e) { 339 | this.reqCount++; 340 | if (this.reqCount < 10) { 341 | let src = e.target.src; 342 | let a = ''; 343 | if (src.includes('?')) { 344 | a = '&_t='; 345 | } else { 346 | a = '?_t='; 347 | } 348 | let t = setTimeout(() => { 349 | e.target.src = src + a + Math.random() * 1000; 350 | clearTimeout(t); 351 | }, 500); 352 | } 353 | } 354 | } 355 | }; 356 | -------------------------------------------------------------------------------- /lib/vue-json-form/index.css: -------------------------------------------------------------------------------- 1 | .p-t-0{padding-top:0px !important}.p-b-0{padding-bottom:0px !important}.p-l-0{padding-left:0px !important}.p-r-0{padding-right:0px !important}.p-t-1{padding-top:1px !important}.p-b-1{padding-bottom:1px !important}.p-l-1{padding-left:1px !important}.p-r-1{padding-right:1px !important}.p-t-2{padding-top:2px !important}.p-b-2{padding-bottom:2px !important}.p-l-2{padding-left:2px !important}.p-r-2{padding-right:2px !important}.p-t-3{padding-top:3px !important}.p-b-3{padding-bottom:3px !important}.p-l-3{padding-left:3px !important}.p-r-3{padding-right:3px !important}.p-t-4{padding-top:4px !important}.p-b-4{padding-bottom:4px !important}.p-l-4{padding-left:4px !important}.p-r-4{padding-right:4px !important}.p-t-5{padding-top:5px !important}.p-b-5{padding-bottom:5px !important}.p-l-5{padding-left:5px !important}.p-r-5{padding-right:5px !important}.p-t-6{padding-top:6px !important}.p-b-6{padding-bottom:6px !important}.p-l-6{padding-left:6px !important}.p-r-6{padding-right:6px !important}.p-t-7{padding-top:7px !important}.p-b-7{padding-bottom:7px !important}.p-l-7{padding-left:7px !important}.p-r-7{padding-right:7px !important}.p-t-8{padding-top:8px !important}.p-b-8{padding-bottom:8px !important}.p-l-8{padding-left:8px !important}.p-r-8{padding-right:8px !important}.p-t-9{padding-top:9px !important}.p-b-9{padding-bottom:9px !important}.p-l-9{padding-left:9px !important}.p-r-9{padding-right:9px !important}.p-t-10{padding-top:10px !important}.p-b-10{padding-bottom:10px !important}.p-l-10{padding-left:10px !important}.p-r-10{padding-right:10px !important}.p-t-11{padding-top:11px !important}.p-b-11{padding-bottom:11px !important}.p-l-11{padding-left:11px !important}.p-r-11{padding-right:11px !important}.p-t-12{padding-top:12px !important}.p-b-12{padding-bottom:12px !important}.p-l-12{padding-left:12px !important}.p-r-12{padding-right:12px !important}.p-t-13{padding-top:13px !important}.p-b-13{padding-bottom:13px !important}.p-l-13{padding-left:13px !important}.p-r-13{padding-right:13px !important}.p-t-14{padding-top:14px !important}.p-b-14{padding-bottom:14px !important}.p-l-14{padding-left:14px !important}.p-r-14{padding-right:14px !important}.p-t-15{padding-top:15px !important}.p-b-15{padding-bottom:15px !important}.p-l-15{padding-left:15px !important}.p-r-15{padding-right:15px !important}.p-t-16{padding-top:16px !important}.p-b-16{padding-bottom:16px !important}.p-l-16{padding-left:16px !important}.p-r-16{padding-right:16px !important}.p-t-17{padding-top:17px !important}.p-b-17{padding-bottom:17px !important}.p-l-17{padding-left:17px !important}.p-r-17{padding-right:17px !important}.p-t-18{padding-top:18px !important}.p-b-18{padding-bottom:18px !important}.p-l-18{padding-left:18px !important}.p-r-18{padding-right:18px !important}.p-t-19{padding-top:19px !important}.p-b-19{padding-bottom:19px !important}.p-l-19{padding-left:19px !important}.p-r-19{padding-right:19px !important}.p-t-20{padding-top:20px !important}.p-b-20{padding-bottom:20px !important}.p-l-20{padding-left:20px !important}.p-r-20{padding-right:20px !important}.p-t-21{padding-top:21px !important}.p-b-21{padding-bottom:21px !important}.p-l-21{padding-left:21px !important}.p-r-21{padding-right:21px !important}.p-t-22{padding-top:22px !important}.p-b-22{padding-bottom:22px !important}.p-l-22{padding-left:22px !important}.p-r-22{padding-right:22px !important}.p-t-23{padding-top:23px !important}.p-b-23{padding-bottom:23px !important}.p-l-23{padding-left:23px !important}.p-r-23{padding-right:23px !important}.p-t-24{padding-top:24px !important}.p-b-24{padding-bottom:24px !important}.p-l-24{padding-left:24px !important}.p-r-24{padding-right:24px !important}.p-t-25{padding-top:25px !important}.p-b-25{padding-bottom:25px !important}.p-l-25{padding-left:25px !important}.p-r-25{padding-right:25px !important}.p-t-26{padding-top:26px !important}.p-b-26{padding-bottom:26px !important}.p-l-26{padding-left:26px !important}.p-r-26{padding-right:26px !important}.p-t-27{padding-top:27px !important}.p-b-27{padding-bottom:27px !important}.p-l-27{padding-left:27px !important}.p-r-27{padding-right:27px !important}.p-t-28{padding-top:28px !important}.p-b-28{padding-bottom:28px !important}.p-l-28{padding-left:28px !important}.p-r-28{padding-right:28px !important}.p-t-29{padding-top:29px !important}.p-b-29{padding-bottom:29px !important}.p-l-29{padding-left:29px !important}.p-r-29{padding-right:29px !important}.p-t-30{padding-top:30px !important}.p-b-30{padding-bottom:30px !important}.p-l-30{padding-left:30px !important}.p-r-30{padding-right:30px !important}.m-t-0{margin-top:0px !important}.m-b-0{margin-bottom:0px !important}.m-l-0{margin-left:0px !important}.m-r-0{margin-right:0px !important}.m-t-1{margin-top:1px !important}.m-b-1{margin-bottom:1px !important}.m-l-1{margin-left:1px !important}.m-r-1{margin-right:1px !important}.m-t-2{margin-top:2px !important}.m-b-2{margin-bottom:2px !important}.m-l-2{margin-left:2px !important}.m-r-2{margin-right:2px !important}.m-t-3{margin-top:3px !important}.m-b-3{margin-bottom:3px !important}.m-l-3{margin-left:3px !important}.m-r-3{margin-right:3px !important}.m-t-4{margin-top:4px !important}.m-b-4{margin-bottom:4px !important}.m-l-4{margin-left:4px !important}.m-r-4{margin-right:4px !important}.m-t-5{margin-top:5px !important}.m-b-5{margin-bottom:5px !important}.m-l-5{margin-left:5px !important}.m-r-5{margin-right:5px !important}.m-t-6{margin-top:6px !important}.m-b-6{margin-bottom:6px !important}.m-l-6{margin-left:6px !important}.m-r-6{margin-right:6px !important}.m-t-7{margin-top:7px !important}.m-b-7{margin-bottom:7px !important}.m-l-7{margin-left:7px !important}.m-r-7{margin-right:7px !important}.m-t-8{margin-top:8px !important}.m-b-8{margin-bottom:8px !important}.m-l-8{margin-left:8px !important}.m-r-8{margin-right:8px !important}.m-t-9{margin-top:9px !important}.m-b-9{margin-bottom:9px !important}.m-l-9{margin-left:9px !important}.m-r-9{margin-right:9px !important}.m-t-10{margin-top:10px !important}.m-b-10{margin-bottom:10px !important}.m-l-10{margin-left:10px !important}.m-r-10{margin-right:10px !important}.m-t-11{margin-top:11px !important}.m-b-11{margin-bottom:11px !important}.m-l-11{margin-left:11px !important}.m-r-11{margin-right:11px !important}.m-t-12{margin-top:12px !important}.m-b-12{margin-bottom:12px !important}.m-l-12{margin-left:12px !important}.m-r-12{margin-right:12px !important}.m-t-13{margin-top:13px !important}.m-b-13{margin-bottom:13px !important}.m-l-13{margin-left:13px !important}.m-r-13{margin-right:13px !important}.m-t-14{margin-top:14px !important}.m-b-14{margin-bottom:14px !important}.m-l-14{margin-left:14px !important}.m-r-14{margin-right:14px !important}.m-t-15{margin-top:15px !important}.m-b-15{margin-bottom:15px !important}.m-l-15{margin-left:15px !important}.m-r-15{margin-right:15px !important}.m-t-16{margin-top:16px !important}.m-b-16{margin-bottom:16px !important}.m-l-16{margin-left:16px !important}.m-r-16{margin-right:16px !important}.m-t-17{margin-top:17px !important}.m-b-17{margin-bottom:17px !important}.m-l-17{margin-left:17px !important}.m-r-17{margin-right:17px !important}.m-t-18{margin-top:18px !important}.m-b-18{margin-bottom:18px !important}.m-l-18{margin-left:18px !important}.m-r-18{margin-right:18px !important}.m-t-19{margin-top:19px !important}.m-b-19{margin-bottom:19px !important}.m-l-19{margin-left:19px !important}.m-r-19{margin-right:19px !important}.m-t-20{margin-top:20px !important}.m-b-20{margin-bottom:20px !important}.m-l-20{margin-left:20px !important}.m-r-20{margin-right:20px !important}.m-t-21{margin-top:21px !important}.m-b-21{margin-bottom:21px !important}.m-l-21{margin-left:21px !important}.m-r-21{margin-right:21px !important}.m-t-22{margin-top:22px !important}.m-b-22{margin-bottom:22px !important}.m-l-22{margin-left:22px !important}.m-r-22{margin-right:22px !important}.m-t-23{margin-top:23px !important}.m-b-23{margin-bottom:23px !important}.m-l-23{margin-left:23px !important}.m-r-23{margin-right:23px !important}.m-t-24{margin-top:24px !important}.m-b-24{margin-bottom:24px !important}.m-l-24{margin-left:24px !important}.m-r-24{margin-right:24px !important}.m-t-25{margin-top:25px !important}.m-b-25{margin-bottom:25px !important}.m-l-25{margin-left:25px !important}.m-r-25{margin-right:25px !important}.m-t-26{margin-top:26px !important}.m-b-26{margin-bottom:26px !important}.m-l-26{margin-left:26px !important}.m-r-26{margin-right:26px !important}.m-t-27{margin-top:27px !important}.m-b-27{margin-bottom:27px !important}.m-l-27{margin-left:27px !important}.m-r-27{margin-right:27px !important}.m-t-28{margin-top:28px !important}.m-b-28{margin-bottom:28px !important}.m-l-28{margin-left:28px !important}.m-r-28{margin-right:28px !important}.m-t-29{margin-top:29px !important}.m-b-29{margin-bottom:29px !important}.m-l-29{margin-left:29px !important}.m-r-29{margin-right:29px !important}.m-t-30{margin-top:30px !important}.m-b-30{margin-bottom:30px !important}.m-l-30{margin-left:30px !important}.m-r-30{margin-right:30px !important}.p-0{padding:0}.p-10{padding:10px}.p-15{padding:15px}.p-20{padding:20px}.p-30{padding:30px}.p-0{margin:0}.m-10{margin:10px}.m-15{margin:15px}.m-20{margin:20px}.m-30{margin:30px}.clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden}.clearfix{*zoom:1}.relative{position:relative}.full-width{width:100% !important}.full-height{height:100% !important}.hidden{display:none}.pointer{cursor:pointer}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-nowrap{white-space:nowrap}.line-through{text-decoration:line-through}.flex-row{display:flex;flex-direction:row;align-items:center}.flex-column{display:flex;flex-direction:column}.flex-center{display:flex;justify-content:center;align-items:center}.flex-wrap{display:flex;flex-wrap:wrap}.flex-start{display:flex;justify-content:flex-start}.flex-end{display:flex;justify-content:flex-end}.flex-space-between{display:flex;justify-content:space-between}.flex-space-around{display:flex;justify-content:space-around}.flex-space-evenly{display:flex;justify-content:space-evenly}.flex-1{flex:1}.label-required::before{content:"*";display:inline-block;margin-right:4px;line-height:1;font-family:SimSun;font-size:12px;color:#f23030}.bold{font-weight:bold}.border-bottom-0{border-bottom:0px}.border-1{border:1px solid #ebeef5}.link{color:#2d8cf0;cursor:pointer}.link:hover{text-decoration:underline}.f12{font-size:12px}.f13{font-size:13px}.f14{font-size:14px}.f15{font-size:15px}.f16{font-size:16px}.f18{font-size:18px}.f20{font-size:20px}.t1{color:#1f2f3d}.t2{color:#606266}.t3{color:#909399}.ellipsis{text-overflow:ellipsis;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;overflow:hidden}.ellipsis-2{text-overflow:ellipsis;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}.overflow-hidden{overflow:hidden}.color-red{color:#f23030}.color-white{color:#fff}.border-bottom-1{position:relative}.border-bottom-1::after{position:absolute;width:100%;z-index:10;bottom:0;left:0;display:block;content:"";height:1px;transform:scaleY(0.5);background-color:#e5e5e5}.border-top-1{position:relative}.border-top-1::after{position:absolute;width:100%;z-index:10;top:0;left:0;display:block;content:"";height:1px;transform:scaleY(0.5);background-color:#e5e5e5}.border-right-1{position:relative}.border-right-1::before{position:absolute;width:1px;z-index:10;top:0;right:0;display:block;content:"";height:100%;transform:scaleX(0.5);background-color:#e5e5e5}.border-left-1::before{position:absolute;width:1px;z-index:10;top:0;left:0;display:block;content:"";height:100%;transform:scaleX(0.5);background-color:#e5e5e5}.border-bottom-dotted-1{position:relative}.border-bottom-dotted-1::after{position:absolute;width:100%;z-index:10;bottom:0;left:0;display:block;content:"";border-bottom:1px dotted #e5e5e5;transform:scaleY(0.5)} -------------------------------------------------------------------------------- /src/pages/form-demo1.vue: -------------------------------------------------------------------------------- 1 | 20 | 394 | 403 | --------------------------------------------------------------------------------