├── .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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
14 |
15 |
21 | {{ buttonText }}
22 |
23 |
24 |
{{ tips }}
25 |
26 |
27 |
28 |
29 |
41 |
--------------------------------------------------------------------------------
/src/pages/AppHome.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | demo1 - form
4 | demo3 - function valid
5 | demo4 - async valid
6 | demo5 - form inline
7 | demo6 - deep data
8 | demo7 - add minus
9 |
10 |
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 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
56 |
57 |
72 |
--------------------------------------------------------------------------------
/src/pages/form-demo3.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
自定义验证
4 |
5 |
14 |
15 | 保存
16 |
17 |
18 |
19 |
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 |
2 |
3 |
Deep Path Object
4 |
5 |
14 |
15 | 保存
16 |
17 |
18 |
初始值:
19 |
{{ JSON.stringify(initData, null, 2) }}
20 |
赋值后:
21 |
{{ JSON.stringify(formData, null, 2) }}
22 |
23 |
24 |
25 |
26 |
27 |
77 |
--------------------------------------------------------------------------------
/src/pages/form-demo5.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
formList
4 |
5 |
14 |
15 | 保存
16 |
17 |
18 |
19 |
20 |
21 |
74 |
84 |
--------------------------------------------------------------------------------
/src/components/zx-add-minus/add-minus.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
23 |
28 |
33 |
34 |
35 |
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 |
2 |
61 |
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 |
2 |
3 |
异步校验+动态赋值options
4 |
5 |
14 |
15 | 保存
16 |
17 |
18 |
19 |
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 |
2 |
3 |
增减
4 |
5 |
14 |
15 |
16 | 保存
17 |
18 | 删除第一个
19 | 删除第2个
20 | 清空value
21 |
22 |
23 |
24 |
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 |
2 |
3 |
13 |
14 |
25 |
26 |
37 |
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 |
2 |
3 |
10 |
79 |
84 |
92 |
93 |
97 |
98 |
99 |
100 |
105 |
106 |
107 |
108 |
109 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
180 |
--------------------------------------------------------------------------------
/src/components/vue-json-form/template.vue:
--------------------------------------------------------------------------------
1 |
2 |
94 |
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 |
2 |
3 |
4 |
5 |
6 |
255 |
--------------------------------------------------------------------------------
/src/components/zx-select-remote/select-remote.vue:
--------------------------------------------------------------------------------
1 |
2 |
69 |
70 |
318 |
319 |
--------------------------------------------------------------------------------
/src/components/zx-form-item/template.vue:
--------------------------------------------------------------------------------
1 |
2 |
293 |
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 |
2 |
3 |
4 |
13 | 清空
14 |
15 | 保存
16 |
17 |
18 |
19 |
20 |
394 |
403 |
--------------------------------------------------------------------------------