Commit bc4f9b65ceae6598167a6fa859f58e71cff973a9

Authored by 刘汉宸
1 parent ef2583ca

feat: 修改SchemaFilter组件

examples/router/routes.js
@@ -136,7 +136,7 @@ const _develops = [ @@ -136,7 +136,7 @@ const _develops = [
136 { 136 {
137 path: 'introduce', 137 path: 'introduce',
138 name: 'developSchemaIntroduce', 138 name: 'developSchemaIntroduce',
139 - meta: { title: '介绍' }, 139 + meta: { title: 'Schema 介绍' },
140 component: () => import('@/views/docs/develop/schema/introduce.md'), 140 component: () => import('@/views/docs/develop/schema/introduce.md'),
141 }, 141 },
142 { 142 {
examples/views/docs/component/schema-filter.md
1 # Schema Filter 筛选 1 # Schema Filter 筛选
2 2
3 -根据JSON Schema配置自动生成一个筛选条件表单 3 +通过配置JSON Schema的方式快速生成一个筛选表单
4 4
5 ## 基础用法 5 ## 基础用法
6 6
7 -配置`list`属性设置JSON Schema配置列表 7 +`schema`设置配置项,基本参数与`z-schema-form`相同;不同的是,由于本组件包含了展开状态,配置项的显示隐藏需要统一控制,因此本组件中不包含配置项的显隐逻辑。
8 8
9 -::: snippet 通过`list`配置项目 9 +::: snippet `schema`设置配置项,`loading`设置加载状态
10 10
11 ```html 11 ```html
12 <template> 12 <template>
13 - <div>  
14 - <pre class="demo-model">{{ model }}</pre>  
15 - <z-schema-filter v-model="model" :list="list" @search="onSearch" @reset="onReset"></z-schema-filter>  
16 - </div> 13 + <z-schema-filter v-model="form" :schema="schema" :loading="loading" @search="onSearch" @reset="onReset"></z-schema-filter>
17 </template> 14 </template>
18 15
19 <script> 16 <script>
20 export default { 17 export default {
21 data() { 18 data() {
22 return { 19 return {
23 - model: {},  
24 - list: [  
25 - { type: 'el-input', label: '姓名', key: 'name' },  
26 - { type: 'el-input', label: '年龄', key: 'age' },  
27 - { type: 'el-input', label: '省', key: 'province' },  
28 - { type: 'el-input', label: '市', key: 'city' },  
29 - { type: 'el-input', label: '区', key: 'area' },  
30 - { type: 'el-input', label: '地址', key: 'address' },  
31 - ]  
32 - } 20 + form: {
  21 + name: '',
  22 + age: '',
  23 + gender: '',
  24 + },
  25 + schema: {
  26 + items: [
  27 + { is: 'el-input', prop: 'name', label: '姓名' },
  28 + { is: 'el-input', prop: 'age', label: '年龄' },
  29 + { is: 'el-input', prop: 'gender', label: '性别' },
  30 + ]
  31 + },
  32 + loading: false
  33 + };
33 }, 34 },
34 methods: { 35 methods: {
35 - onSearch(params) {  
36 - console.log(JSON.parse(JSON.stringify(params))); 36 + onSearch(model) {
  37 + this.loading = true;
  38 + console.log(model);
  39 + setTimeout(() => {
  40 + this.loading = false;
  41 + }, 1500);
37 }, 42 },
38 onReset() { 43 onReset() {
39 console.log('reset'); 44 console.log('reset');
40 } 45 }
41 } 46 }
42 -} 47 +};
43 </script> 48 </script>
44 ``` 49 ```
45 50
46 ::: 51 :::
47 52
48 -## 自定义按钮  
49 -  
50 -支持自定义按钮占位及按钮组件  
51 -  
52 -::: snippet 配置`uncollapsedSpan`改变展开时操作区占位,默认值`24`;插槽`operation`自定义按钮内容; 53 +::: snippet 默认显示数量为`3`,超过则会显示展开按钮
53 54
54 ```html 55 ```html
55 <template> 56 <template>
56 - <z-schema-filter v-model="model" :list="list" :uncollapsedSpan="12">  
57 - <template #operation="{ size, handleSearch, handleReset, handleCollapse, showCollapsed, collapsed, loading }">  
58 - <div style="text-align: right;">  
59 - <el-button-group :size="size">  
60 - <el-button type="primary" round @click="handleSearch" :loading="loading" icon="el-icon-search"><span>查询</span></el-button>  
61 - <el-button @click="handleReset"><span>重置</span></el-button>  
62 - <el-button round v-if="showCollapsed" @click="handleCollapse">  
63 - <span>{{ collapsed ? '展开' : '收起' }}</span>  
64 - </el-button>  
65 - </el-button-group>  
66 - </div>  
67 - </template>  
68 - </z-schema-filter> 57 + <z-schema-filter v-model="form" :schema="schema"></z-schema-filter>
69 </template> 58 </template>
70 59
71 <script> 60 <script>
72 export default { 61 export default {
73 data() { 62 data() {
74 return { 63 return {
75 - model: {},  
76 - list: [  
77 - { type: 'el-input', label: '姓名', key: 'name' },  
78 - { type: 'el-input', label: '年龄', key: 'age' },  
79 - { type: 'el-input', label: '省', key: 'province' },  
80 - { type: 'el-input', label: '市', key: 'city' },  
81 - { type: 'el-input', label: '区', key: 'area' },  
82 - { type: 'el-input', label: '地址', key: 'address' },  
83 - ]  
84 - } 64 + form: {
  65 + name: '',
  66 + age: '',
  67 + gender: '',
  68 + },
  69 + schema: {
  70 + items: [
  71 + { is: 'el-input', prop: 'name', label: '姓名' },
  72 + { is: 'el-input', prop: 'age', label: '年龄' },
  73 + { is: 'el-input', prop: 'gender', label: '性别' },
  74 + { is: 'el-input', prop: 'address', label: '地址' },
  75 + { is: 'el-input', prop: 'package', label: '包裹' },
  76 + ]
  77 + },
  78 + };
85 }, 79 },
86 -} 80 +};
87 </script> 81 </script>
88 ``` 82 ```
89 83
90 ::: 84 :::
91 85
92 -## 自定义数据项 86 +## 默认展示数量
93 87
94 -支持自定义数据项,一般用于通过JSON Schema较难配置出的,较复杂的局部组件 88 +常见的业务场景下,经常会出现多个筛选条件的情况,可以默认展示多行的搜索项。
95 89
96 -::: snippet 插槽名与数据项`key`相同 90 +::: snippet `display`设置默认展示的搜索项数量,控制按钮栏占位会根据每行剩余搜索项数量自动计算。
97 91
98 ```html 92 ```html
99 <template> 93 <template>
100 - <z-schema-filter v-model="model" :list="list">  
101 - <template #name>  
102 - <el-input placeholder="请输入内容" v-model="model.name">  
103 - <el-button slot="append" icon="el-icon-search"></el-button>  
104 - </el-input>  
105 - </template>  
106 - </z-schema-filter> 94 + <z-schema-filter v-model="form" :schema="schema" :display="7"></z-schema-filter>
107 </template> 95 </template>
108 96
109 <script> 97 <script>
110 export default { 98 export default {
111 data() { 99 data() {
112 return { 100 return {
113 - model: {},  
114 - list: [  
115 - { type: 'el-input', label: '姓名', key: 'name' },  
116 - { type: 'el-input', label: '年龄', key: 'age' },  
117 - { type: 'el-input', label: '省', key: 'province' },  
118 - { type: 'el-input', label: '市', key: 'city' },  
119 - { type: 'el-input', label: '区', key: 'area' },  
120 - { type: 'el-input', label: '地址', key: 'address' },  
121 - ]  
122 - } 101 + form: {
  102 + name: '',
  103 + age: '',
  104 + gender: '',
  105 + },
  106 + schema: {
  107 + items: [
  108 + { is: 'el-input', prop: 'name', label: '姓名' },
  109 + { is: 'el-input', prop: 'age', label: '年龄' },
  110 + { is: 'el-input', prop: 'gender', label: '性别' },
  111 + { is: 'el-input', prop: 'item1', label: '搜索项1' },
  112 + { is: 'el-input', prop: 'item2', label: '搜索项2' },
  113 + { is: 'el-input', prop: 'item3', label: '搜索项3' },
  114 + { is: 'el-input', prop: 'item4', label: '搜索项4' },
  115 + { is: 'el-input', prop: 'item5', label: '搜索项5' },
  116 + { is: 'el-input', prop: 'item6', label: '搜索项6' },
  117 + ]
  118 + },
  119 + loading: false
  120 + };
123 }, 121 },
124 -} 122 + methods: {
  123 + onSearch(model) {
  124 + this.loading = true;
  125 + console.log(model);
  126 + setTimeout(() => {
  127 + this.loading = false;
  128 + }, 1500);
  129 + },
  130 + onReset() {
  131 + console.log('reset');
  132 + }
  133 + }
  134 +};
125 </script> 135 </script>
126 ``` 136 ```
127 137
128 ::: 138 :::
129 139
130 -## 展示数量 140 +## 自定义操作栏
131 141
132 -默认展示数量为`3`个,可以通过配置而改变 142 +特殊业务场景下,可以自定义操作栏
133 143
134 -::: snippet 设置`visibleNum`改变默认展示数量的大小,`span`改变每一项的占位数量 144 +::: snippet `operation`插槽替换默认操作栏
135 145
136 ```html 146 ```html
137 <template> 147 <template>
138 - <z-schema-filter :list="list" :visibleNum="2" :span="8" size="large"></z-schema-filter> 148 + <z-schema-filter v-model="form" :schema="schema" :display="2" :loading="loading" @search="onSearch" @reset="onReset">
  149 + <template #operation="{ search, reset, collapse, collapsed, loading }">
  150 + <el-button-group round style="display: flex; justify-content: flex-end">
  151 + <el-button round type="primary" @click="search" :loading="loading" icon="el-icon-search">查询</el-button>
  152 + <el-button round @click="reset">重置</el-button>
  153 + <el-button round @click="onClick">自定义按钮</el-button>
  154 + <el-button round @click="collapse">{{ collapsed ? '收起' : '展开' }}</el-button>
  155 + </el-button-group>
  156 + </template>
  157 + </z-schema-filter>
139 </template> 158 </template>
140 159
141 <script> 160 <script>
142 export default { 161 export default {
143 data() { 162 data() {
144 return { 163 return {
145 - list: [  
146 - { type: 'el-input', label: '姓名', key: 'name' },  
147 - { type: 'el-input-number', label: '年龄', key: 'age', style: { width: '100%' }, props: { min: 0 } },  
148 - { type: 'el-input', label: '省', key: 'province' },  
149 - { type: 'el-input', label: '市', key: 'city' },  
150 - { type: 'el-input', label: '区', key: 'area' },  
151 - { type: 'el-input', label: '地址', key: 'address' },  
152 - ]  
153 - } 164 + form: {
  165 + name: '',
  166 + age: '',
  167 + gender: '',
  168 + },
  169 + schema: {
  170 + items: [
  171 + { is: 'el-input', prop: 'name', label: '姓名' },
  172 + { is: 'el-input', prop: 'age', label: '年龄' },
  173 + { is: 'el-input', prop: 'gender', label: '性别' },
  174 + { is: 'el-input', prop: 'address', label: '地址' },
  175 + { is: 'el-input', prop: 'address', label: '地址' },
  176 + ]
  177 + },
  178 + loading: false,
  179 + };
154 }, 180 },
155 -} 181 + methods: {
  182 + onSearch(model) {
  183 + this.loading = true;
  184 + console.log(model);
  185 + setTimeout(() => {
  186 + this.loading = false;
  187 + }, 1500);
  188 + },
  189 + onReset() {
  190 + console.log('reset');
  191 + },
  192 + onClick() {
  193 + console.log('click');
  194 + }
  195 + }
  196 +};
156 </script> 197 </script>
157 ``` 198 ```
158 199
@@ -165,20 +206,18 @@ export default { @@ -165,20 +206,18 @@ export default {
165 参数|说明|类型|可选值|默认值 206 参数|说明|类型|可选值|默认值
166 -|-|-|-|- 207 -|-|-|-|-
167 value | 值 | Object | - | {} 208 value | 值 | Object | - | {}
168 -list | JSON Schema配置项列表 | Array | - | []  
169 -labelWidth | 默认Label宽度 | String | 110px  
170 -size | 大小 | String | - | small  
171 -span | 占位 | Number | - | 24  
172 -uncollapsedSpan | 展开时操作区占位 | Number | - | 24  
173 -visibleNum | 折叠时可视项数量 | Number | - | 3  
174 -loading | 加载状态 | Boolean | - | -  
175 -formProps | 默认el-form配置参数 | Object | - | {}  
176 -params | 自定义附加参数 | Object | - | - 209 +schema | JSON Schema配置项列表 | Object | - | {}
  210 +size | 大小 | String | - | -
  211 +loading | 加载状态 | Boolean | - | false
  212 +display | 默认收起时显示数量 | Number | - | 3
  213 +span | 子项占位 | Number | - | 6
  214 +collapsedSpan | 展开时操作栏占位,默认自动计算,特殊情况下可手动设置 | Boolean | - | -
  215 +uncollapsedSpan | 收起时操作栏占位,默认自动计算,特殊情况下可手动设置 | Boolean | - | -
177 216
178 ## Events 事件 217 ## Events 事件
179 218
180 事件名称|说明|回调参数 219 事件名称|说明|回调参数
181 -|-|- 220 -|-|-
182 input | 表单值变化 | model 221 input | 表单值变化 | model
183 -search | 查询 | model  
184 -reset | 重置表单 | -  
185 \ No newline at end of file 222 \ No newline at end of file
  223 +search | 搜索 | model
  224 +reset | 重置 | -
186 \ No newline at end of file 225 \ No newline at end of file
examples/views/docs/component/schema-form.md
@@ -590,4 +590,19 @@ export default { @@ -590,4 +590,19 @@ export default {
590 </script> 590 </script>
591 ``` 591 ```
592 592
593 -:::  
594 \ No newline at end of file 593 \ No newline at end of file
  594 +:::
  595 +
  596 +## API
  597 +
  598 +## Attribute 属性
  599 +
  600 +参数|说明|类型|可选值|默认值
  601 +-|-|-|-|-
  602 +value | 值 | Object | - | {}
  603 +schema | JSON Schema配置项列表 | Object | - | {}
  604 +
  605 +## Events 事件
  606 +
  607 +事件名称|说明|回调参数
  608 +-|-|-
  609 +input | 表单值变化 | model
595 \ No newline at end of file 610 \ No newline at end of file
packages/form/index.vue
@@ -8,8 +8,11 @@ @@ -8,8 +8,11 @@
8 </template> 8 </template>
9 9
10 <script> 10 <script>
  11 +import MIX_FORM from '../mixins/form';
  12 +
11 export default { 13 export default {
12 name: 'Form', 14 name: 'Form',
  15 + mixins: [MIX_FORM],
13 props: { 16 props: {
14 value: Object, 17 value: Object,
15 model: Object, 18 model: Object,
@@ -27,19 +30,5 @@ export default { @@ -27,19 +30,5 @@ export default {
27 zForm: this, 30 zForm: this,
28 }; 31 };
29 }, 32 },
30 - methods: {  
31 - validate(callback) {  
32 - return this.$refs.form.validate(callback);  
33 - },  
34 - validateField(props, callback) {  
35 - return this.$refs.form.validateField(props, callback);  
36 - },  
37 - resetFields() {  
38 - this.$refs.form.resetFields();  
39 - },  
40 - clearValidate(props) {  
41 - return this.$refs.form.clearValidate(props);  
42 - },  
43 - },  
44 }; 33 };
45 </script> 34 </script>
packages/mixins/form.js 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +export default {
  2 + methods: {
  3 + validate(callback) {
  4 + if (this.$refs.form) {
  5 + return this.$refs.form.validate(callback);
  6 + }
  7 + return false;
  8 + },
  9 + validateField(props, callback) {
  10 + if (this.$refs.form) {
  11 + return this.$refs.form.validateField(props, callback);
  12 + }
  13 + return false;
  14 + },
  15 + resetFields() {
  16 + if (this.$refs.form) {
  17 + this.$refs.form.resetFields();
  18 + }
  19 + },
  20 + clearValidate(props) {
  21 + if (this.$refs.form) {
  22 + return this.$refs.form.clearValidate(props);
  23 + }
  24 + return false;
  25 + },
  26 + },
  27 +};
packages/schema-filter/index.vue
1 <template> 1 <template>
2 - <z-schema-form  
3 - v-model="model"  
4 - class="z-schema-filter"  
5 - :list="formattedList"  
6 - :span="span"  
7 - :labelWidth="labelWidth"  
8 - :colClass="colVisibleRender"  
9 - :size="size"  
10 - :formProps="formProps"  
11 - :params="params"  
12 - > 2 + <z-schema-form ref="form" class="z-schema-filter" v-model="model" :schema="formattedSchema">
13 <template v-for="key in slotKeys"> 3 <template v-for="key in slotKeys">
14 - <template v-if="key === 'operation'">  
15 - <slot :name="key" :slot="key" v-bind="{ size, handleSearch, handleReset, handleCollapse, showCollapsed, collapsed, loading }"></slot>  
16 - </template>  
17 - <slot v-else :name="key" :slot="key"></slot> 4 + <slot :name="key" :slot="key" v-bind="slotProps"></slot>
18 </template> 5 </template>
19 - <div v-if="!slotKeys.includes('operation')" slot="operation" class="z-schema-filter__button-group">  
20 - <el-button-group :size="size">  
21 - <el-button type="primary" @click="handleSearch" :loading="loading" icon="el-icon-search"><span>查询</span></el-button>  
22 - <el-button @click="handleReset"><span>重置</span></el-button>  
23 - <el-button v-if="showCollapsed" @click="handleCollapse">  
24 - <span>{{ collapsed ? '展开' : '收起' }}</span>  
25 - </el-button>  
26 - </el-button-group>  
27 - </div> 6 + <el-button-group v-if="!slotKeys.includes('operation')" slot="operation" style="display: flex; justify-content: flex-end">
  7 + <el-button type="primary" @click="onSearch" :loading="loading" icon="el-icon-search">查询</el-button>
  8 + <el-button @click="onReset">重置</el-button>
  9 + <el-button v-if="showCollapsed" @click="onCollapse">{{ collapsed ? '收起' : '展开' }}</el-button>
  10 + </el-button-group>
28 </z-schema-form> 11 </z-schema-form>
29 </template> 12 </template>
30 13
31 <script> 14 <script>
  15 +import MIX_FORM from '../mixins/form';
  16 +import { cloneDeep } from '../utils';
  17 +
32 export default { 18 export default {
33 name: 'SchemaFilter', 19 name: 'SchemaFilter',
  20 + mixins: [MIX_FORM],
34 props: { 21 props: {
35 value: Object, 22 value: Object,
36 - list: Array,  
37 - labelWidth: {  
38 - type: String,  
39 - default: '110px',  
40 - },  
41 - size: {  
42 - type: String,  
43 - default: 'small', 23 + schema: {
  24 + type: Object,
  25 + default() {
  26 + return {};
  27 + },
44 }, 28 },
45 - span: { 29 + size: String,
  30 + loading: Boolean,
  31 + display: {
46 type: Number, 32 type: Number,
47 - default: 6, 33 + default: 3,
48 }, 34 },
49 - collapsedSpan: { 35 + span: {
50 type: Number, 36 type: Number,
51 default: 6, 37 default: 6,
52 }, 38 },
53 - uncollapsedSpan: {  
54 - type: Number,  
55 - default: 24,  
56 - },  
57 - visibleNum: {  
58 - type: Number,  
59 - default: 3,  
60 - },  
61 - loading: Boolean,  
62 - formProps: {  
63 - type: Object,  
64 - default: () => ({}),  
65 - },  
66 - params: Object, 39 + collapsedSpan: Number,
  40 + uncollapsedSpan: Number,
67 }, 41 },
68 data() { 42 data() {
69 return { 43 return {
70 - collapsed: true,  
71 - model: this.value || {}, 44 + collapsed: false,
  45 + model: this.value,
  46 + originData: {},
72 }; 47 };
73 }, 48 },
74 watch: { 49 watch: {
@@ -80,50 +55,93 @@ export default { @@ -80,50 +55,93 @@ export default {
80 }, 55 },
81 }, 56 },
82 computed: { 57 computed: {
83 - formattedList() {  
84 - return [...this.list, { key: 'operation', label: '', labelWidth: '0px', span: this.collapsed ? this.collapsedSpan : this.uncollapsedSpan }]; 58 + filterSize() {
  59 + return this.size || (this.$ELEMENT || {}).size;
  60 + },
  61 + formattedItems() {
  62 + const items = this.schema.items || [];
  63 + const result = [];
  64 + const visibleNumber = this.display - 1;
  65 + items.forEach((item, index) => {
  66 + if (!this.collapsed && index > visibleNumber && index < items.length) {
  67 + result.push({ ...item, if: true, show: false });
  68 + } else {
  69 + result.push({ ...item, if: true, show: true });
  70 + }
  71 + });
  72 + return result;
  73 + },
  74 + formattedSchema() {
  75 + return {
  76 + props: { span: this.span, 'label-width': '75px', size: this.filterSize || 'small', ...(this.schema.props || {}) },
  77 + items: [...this.formattedItems, { prop: 'operation', label: '', labelWidth: '0px', span: this.operationSpan }],
  78 + };
  79 + },
  80 + rowItemCount() {
  81 + return parseInt(24 / this.span);
  82 + },
  83 + rowItemRemain() {
  84 + return this.formattedItems.length % this.rowItemCount;
  85 + },
  86 + operationSpan() {
  87 + if (this.collapsed) {
  88 + if (this.collapsedSpan) {
  89 + return this.collapsedSpan;
  90 + }
  91 + return (this.rowItemCount - this.rowItemRemain) * this.span;
  92 + } else {
  93 + if (this.uncollapsedSpan) {
  94 + return this.uncollapsedSpan;
  95 + }
  96 + if (this.formattedItems.length < this.display) {
  97 + return this.span * (this.rowItemCount - this.rowItemRemain);
  98 + }
  99 + if (this.display < this.rowItemCount - 1) {
  100 + return this.span * this.display;
  101 + }
  102 + return this.span;
  103 + }
85 }, 104 },
86 showCollapsed() { 105 showCollapsed() {
87 - const { list, visibleNum } = this;  
88 - return list.length > visibleNum; 106 + return this.formattedItems.length > this.display;
89 }, 107 },
90 slotKeys() { 108 slotKeys() {
91 return Object.keys(this.$scopedSlots); 109 return Object.keys(this.$scopedSlots);
92 }, 110 },
  111 + slotProps() {
  112 + return {
  113 + size: this.size,
  114 + search: this.onSearch,
  115 + reset: this.onReset,
  116 + collapse: this.onCollapse,
  117 + collapsed: this.collapsed,
  118 + loading: this.loading,
  119 + };
  120 + },
  121 + },
  122 + created() {
  123 + const { originData, ...other } = this._data;
  124 + this.originData = cloneDeep(other);
93 }, 125 },
94 methods: { 126 methods: {
95 - // 渲染列表项class  
96 - colVisibleRender(item, index) {  
97 - if (this.collapsed) {  
98 - const visibleNumber = this.visibleNum ? this.visibleNum - 1 : 2;  
99 - return index > visibleNumber && index < this.list.length ? 'z-schema-filter__item hidden' : 'z-schema-filter__item';  
100 - }  
101 - return 'z-schema-filter__item';  
102 - },  
103 // 搜索 127 // 搜索
104 - handleSearch() {  
105 - this.$emit('search', this.model); 128 + onSearch() {
  129 + this.$refs.form.validate(valid => {
  130 + if (valid) {
  131 + this.$emit('search', this.model);
  132 + }
  133 + });
106 }, 134 },
107 // 重置 135 // 重置
108 - handleReset() {  
109 - this.model = {}; 136 + onReset() {
  137 + this.model = cloneDeep(this.originData).model;
  138 + this.$refs.form.resetFields();
110 this.$emit('reset'); 139 this.$emit('reset');
111 }, 140 },
112 // 折叠 141 // 折叠
113 - handleCollapse() { 142 + onCollapse() {
114 this.collapsed = !this.collapsed; 143 this.collapsed = !this.collapsed;
115 }, 144 },
116 }, 145 },
117 }; 146 };
118 </script> 147 </script>
119 -  
120 -<style>  
121 -.z-schema-filter__item.hidden {  
122 - display: none;  
123 -}  
124 -.z-schema-filter__button-group {  
125 - width: 100%;  
126 - box-sizing: border-box;  
127 - text-align: right;  
128 -}  
129 -</style>  
packages/schema-form/index copy.vue
@@ -1,216 +0,0 @@ @@ -1,216 +0,0 @@
1 -<style lang="scss">  
2 -.z-schema-form {  
3 - &__flex-wrap {  
4 - display: flex;  
5 - flex-wrap: wrap;  
6 - width: 100%;  
7 - }  
8 - &__group-title {  
9 - font-weight: bold;  
10 - padding: 15px 5px;  
11 - border-bottom: 1px solid #d9d9d9;  
12 - margin-bottom: 15px;  
13 - }  
14 - &__group-content {  
15 - margin: 15px 0px;  
16 - }  
17 - &__footer {  
18 - padding-top: 10px;  
19 - display: flex;  
20 - align-items: center;  
21 - justify-content: center;  
22 - }  
23 -}  
24 -</style>  
25 -  
26 -<template>  
27 - <el-form  
28 - ref="form"  
29 - class="z-schema-form"  
30 - :size="size"  
31 - :class="formClass"  
32 - :model="formModel"  
33 - :label-width="labelWidth"  
34 - :label-position="labelPosition || labelWidth ? 'right' : 'top'"  
35 - v-bind="formProps"  
36 - >  
37 - <schema-form-render  
38 - :title-class="titleClass"  
39 - :content-class="contentClass"  
40 - :item-class="itemClass"  
41 - :col-class="colClass"  
42 - :group-class="groupClass"  
43 - :list="formList"  
44 - :value="model"  
45 - :model="model"  
46 - :span="span"  
47 - :type="type"  
48 - :params="params"  
49 - @item-change="onItemChange"  
50 - @form-item-change="onFormItemChange"  
51 - @item-update="onItemUpdate"  
52 - >  
53 - <slot v-for="key in slotKeys" :name="key" :slot="key"></slot>  
54 - </schema-form-render>  
55 - <div v-if="$scopedSlots.footer" class="z-schema-form__footer">  
56 - <slot name="footer" :size="size" :validate="validate" :reset="reset" :model="model"></slot>  
57 - </div>  
58 - </el-form>  
59 -</template>  
60 -  
61 -<script>  
62 -import SchemaFormRender from './schema-form-render';  
63 -import { cloneDeep, set } from '../utils';  
64 -  
65 -export default {  
66 - name: 'SchemaForm',  
67 - components: { SchemaFormRender },  
68 - props: {  
69 - value: Object,  
70 - list: Array,  
71 - formClass: String,  
72 - titleClass: String,  
73 - contentClass: String,  
74 - itemClass: String,  
75 - colClass: [String, Function],  
76 - groupClass: String,  
77 - labelWidth: String,  
78 - labelPosition: String,  
79 - type: String,  
80 - size: {  
81 - type: String,  
82 - default: 'small',  
83 - },  
84 - span: {  
85 - type: Number,  
86 - default: 24,  
87 - },  
88 - formProps: {  
89 - type: Object,  
90 - default: () => ({}),  
91 - },  
92 - params: Object,  
93 - },  
94 - data() {  
95 - return {  
96 - model: {},  
97 - formModel: {},  
98 - formList: [],  
99 - };  
100 - },  
101 - computed: {  
102 - slotKeys() {  
103 - return Object.keys(this.$scopedSlots);  
104 - },  
105 - },  
106 - watch: {  
107 - value: {  
108 - handler(val = {}) {  
109 - this.model = val;  
110 - this.setFormModel(val);  
111 - },  
112 - immediate: true,  
113 - },  
114 - list: {  
115 - handler(val) {  
116 - // 深度克隆传入的列表,避免原始值被修改  
117 - const newList = cloneDeep(this.list);  
118 - // 生成列表值的全路径key,即列表项为对象时,对象内的key与上一级的key合并作为全路径key  
119 - const generateFullKey = (list, parentKey) => {  
120 - list.forEach(item => {  
121 - if (item.group && item.list) {  
122 - if (item.group.key) {  
123 - item.fullKey = `${parentKey ? `${parentKey}-${item.group.key}` : item.group.key}`;  
124 - } else {  
125 - item.fullKey = parentKey || item.key;  
126 - }  
127 - generateFullKey(item.list, item.fullKey);  
128 - } else {  
129 - item.fullKey = `${parentKey ? `${parentKey}-${item.key}` : item.key}`;  
130 - }  
131 - });  
132 - };  
133 - generateFullKey(newList);  
134 - this.formList = newList;  
135 - },  
136 - immediate: true,  
137 - },  
138 - },  
139 - methods: {  
140 - handleInput(value) {  
141 - console.log(value);  
142 - },  
143 - /**  
144 - * @description 表单项值变化  
145 - * @param {Object} item 表单项值对象  
146 - */  
147 - onItemChange(item) {  
148 - this.$emit('input', { ...this.model, ...item });  
149 - },  
150 - /**  
151 - * @description 表单相校验值变化  
152 - * @param {Object} item 表单项校验值对象  
153 - */  
154 - onFormItemChange(item) {  
155 - this.formModel = { ...this.formModel, ...item };  
156 - },  
157 - /**  
158 - * @description 校验表单  
159 - */  
160 - validate(cb) {  
161 - return new Promise(resolve => {  
162 - this.$refs.form.validate(valid => {  
163 - cb && cb(valid);  
164 - this.$emit('validate', valid, this.model);  
165 - return resolve(valid);  
166 - });  
167 - });  
168 - },  
169 - /**  
170 - * @description 重置表单  
171 - */  
172 - reset() {  
173 - this.$refs.form.clearValidate();  
174 - this.$emit('reset');  
175 - },  
176 - /**  
177 - * @description 根据表单值设置表单校验值  
178 - * @param {Object} value 表单值  
179 - */  
180 - setFormModel(value) {  
181 - let formModel = {};  
182 - // 递归深度解析表单值,将表单值扁平化为一层的对象并设置为表单校验值对象  
183 - const setFormModelValue = (list, parentKey) => {  
184 - list.forEach(item => {  
185 - item.fullKey = `${parentKey ? `${parentKey}-${item.key}` : item.key}`;  
186 - if (item.value instanceof Object) {  
187 - setFormModelValue(  
188 - Object.keys(item.value).map(key => ({ key, value: item.value[key] })),  
189 - item.fullKey,  
190 - );  
191 - } else {  
192 - formModel[item.fullKey] = item.value;  
193 - }  
194 - });  
195 - };  
196 - setFormModelValue(Object.keys(value).map(key => ({ key, value: value[key] })));  
197 - this.formModel = formModel;  
198 - },  
199 - /**  
200 - * @description 手动更新某一表单项的值  
201 - * @param {Object} param 需要更新的参数对象或者对象数组 { name => 表单项key,可嵌套; value => 更新的值 }  
202 - * @example { name: 'a.b.c', value: 123 }  
203 - * @example { name: 'd.0.e', value: ['f'] }  
204 - */  
205 - onItemUpdate(param) {  
206 - this.$nextTick(() => {  
207 - const newModel = cloneDeep(this.model);  
208 - Object.entries(param).forEach(entry => {  
209 - set(newModel, entry[0], entry[1]);  
210 - });  
211 - this.$emit('input', newModel);  
212 - });  
213 - },  
214 - },  
215 -};  
216 -</script>  
packages/schema-form/index.vue
1 <template> 1 <template>
2 - <z-form ref="form" v-model="model" v-bind="schema.props" v-on="schema.props"> 2 + <z-form ref="form" class="z-schema-form" v-model="model" v-bind="schema.props" v-on="schema.on">
3 <template v-for="(item, index) in schema.items"> 3 <template v-for="(item, index) in schema.items">
4 <template v-if="item.is"> 4 <template v-if="item.is">
5 - <z-form-item v-if="bindParam(item, 'if', true)" v-show="bindParam(item, 'show', true)" v-bind="bindItemProps(item)" :key="index"> 5 + <z-form-item v-if="bindParam(item, 'if')" v-show="bindParam(item, 'show')" v-bind="bindItemProps(item)" :key="index">
6 <slot v-if="$scopedSlots[item.prop]" :name="item.prop" :value="get(model, item.prop)" :onInput="value => onComponentInput({ value, item })" v-bind="slotProps"> </slot> 6 <slot v-if="$scopedSlots[item.prop]" :name="item.prop" :value="get(model, item.prop)" :onInput="value => onComponentInput({ value, item })" v-bind="slotProps"> </slot>
7 <item-render v-else :item="item" :value="get(model, item.prop)" :onInput="value => onComponentInput({ value, item })"></item-render> 7 <item-render v-else :item="item" :value="get(model, item.prop)" :onInput="value => onComponentInput({ value, item })"></item-render>
8 <slot :name="`label-${item.prop}`" slot="label" v-bind="slotProps"></slot> 8 <slot :name="`label-${item.prop}`" slot="label" v-bind="slotProps"></slot>
@@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
10 </z-form-item> 10 </z-form-item>
11 </template> 11 </template>
12 <template v-else> 12 <template v-else>
13 - <z-form-item v-if="bindParam(item, 'if', true)" v-show="bindParam(item, 'show', true)" v-bind="bindItemProps(item)" :key="index" :value="get(model, item.prop)"> 13 + <z-form-item v-if="bindParam(item, 'if')" v-show="bindParam(item, 'show')" v-bind="bindItemProps(item)" :key="index" :value="get(model, item.prop)">
14 <slot :name="item.prop" :value="get(model, item.prop)" :onInput="value => onComponentInput({ value, item })" v-bind="slotProps"></slot> 14 <slot :name="item.prop" :value="get(model, item.prop)" :onInput="value => onComponentInput({ value, item })" v-bind="slotProps"></slot>
15 </z-form-item> 15 </z-form-item>
16 </template> 16 </template>
@@ -20,10 +20,12 @@ @@ -20,10 +20,12 @@
20 </template> 20 </template>
21 21
22 <script> 22 <script>
  23 +import MIX_FORM from '../mixins/form';
23 import { cloneDeep, get, set } from '../utils'; 24 import { cloneDeep, get, set } from '../utils';
24 25
25 export default { 26 export default {
26 name: 'SchemaForm', 27 name: 'SchemaForm',
  28 + mixins: [MIX_FORM],
27 components: { 29 components: {
28 ItemRender: { 30 ItemRender: {
29 functional: true, 31 functional: true,
@@ -102,11 +104,16 @@ export default { @@ -102,11 +104,16 @@ export default {
102 }, 104 },
103 methods: { 105 methods: {
104 get, 106 get,
105 - bindParam(item, key, def) { 107 + bindParam(item, key) {
106 if (typeof item[key] === 'function') { 108 if (typeof item[key] === 'function') {
107 return item[key]({ model: this.model }); 109 return item[key]({ model: this.model });
108 } 110 }
109 - return def || item[key]; 111 + if (['if', 'show'].includes(key)) {
  112 + if (['', null, undefined].includes(item[key])) {
  113 + return true;
  114 + }
  115 + }
  116 + return item[key];
110 }, 117 },
111 bindItemProps(item) { 118 bindItemProps(item) {
112 const { children, is, props, on, ...other } = item || {}; 119 const { children, is, props, on, ...other } = item || {};
@@ -115,18 +122,6 @@ export default { @@ -115,18 +122,6 @@ export default {
115 return result; 122 return result;
116 }, {}); 123 }, {});
117 }, 124 },
118 - validate(callback) {  
119 - return this.$refs.form.validate(callback);  
120 - },  
121 - validateField(props, callback) {  
122 - return this.$refs.form.validateField(props, callback);  
123 - },  
124 - resetFields() {  
125 - this.$refs.form.resetFields();  
126 - },  
127 - clearValidate(props) {  
128 - return this.$refs.form.clearValidate(props);  
129 - },  
130 onComponentInput({ value, item }) { 125 onComponentInput({ value, item }) {
131 set(this.model, item.prop, value); 126 set(this.model, item.prop, value);
132 }, 127 },
packages/schema-form/schema-form-render.vue
@@ -1,246 +0,0 @@ @@ -1,246 +0,0 @@
1 -<template>  
2 - <!-- 在row上使用flex,防止表单组件大小不一导致错位 -->  
3 - <component :is="rowComponent" class="z-schema-form__flex-wrap">  
4 - <template v-for="(item, index) in list">  
5 - <!-- 表单项有设置分组时 -->  
6 - <component  
7 - :is="colComponent"  
8 - v-if="item.group && item.list"  
9 - :key="index"  
10 - :span="type === 'div' ? undefined : item.group.span || 24"  
11 - :style="{ width: type === 'div' ? '100%' : undefined }"  
12 - :class="colClassRender(item, index, colClass)"  
13 - >  
14 - <component :is="rowComponent" class="z-schema-form__flex-wrap" :class="groupClass || 'z-schema-form__group'">  
15 - <!-- 表单分组标题 -->  
16 - <component :is="rowComponent" :class="titleClass || 'z-schema-form__group-title'" v-if="item.group.title" style="width: 100%;">  
17 - {{ item.group.title || item.group }}  
18 - </component>  
19 - <!-- 递归本组件 -->  
20 - <schema-form-render  
21 - :title-class="titleClass"  
22 - :item-class="itemClass"  
23 - :content-class="contentClass"  
24 - :group-class="groupClass"  
25 - :class="contentClass || 'z-schema-form__group-content'"  
26 - :list="item.list"  
27 - :value="value"  
28 - :model="itemKey ? model[itemKey] || {} : model"  
29 - :itemKey="item.group.key"  
30 - :type="type"  
31 - @item-change="onItemChange"  
32 - @form-item-change="onFormItemChange"  
33 - @item-update="onItemUpdate"  
34 - :span="type === 'div' ? undefined : span * (24 / (item.group.span || 24))"  
35 - ></schema-form-render>  
36 - </component>  
37 - </component>  
38 - <!-- 正常无分组表单项 -->  
39 - <template v-else>  
40 - <component  
41 - :is="colComponent"  
42 - v-if="bindItemVisible(item, 'visible')"  
43 - v-show="bindItemVisible(item, 'show')"  
44 - :span="type === 'div' ? undefined : item.span || span"  
45 - :key="index"  
46 - :style="{ width: type === 'div' && item.style && item.style.width.includes('%') ? item.style.width : undefined, paddingRight: '10px' }"  
47 - :class="colClassRender(item, index, colClass)"  
48 - >  
49 - <el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.fullKey" :rules="item | bindItemRulesFilter(model)" :class="itemClass || 'z-schema-form__item'">  
50 - <slot v-if="$scopedSlots[item.fullKey]" :name="item.fullKey" :value="itemValue(item)" :model1="value"></slot>  
51 - <template v-else>  
52 - <!-- 自定义组件 -->  
53 - <dynamic-render  
54 - v-if="typeof item.type === 'function'"  
55 - :render="  
56 - item.type($createElement, {  
57 - model: value,  
58 - config: {  
59 - props: { ...propsFormatter(item.props), value: itemValue(item) },  
60 - style: item.style || { width: '100%' },  
61 - on: {  
62 - ...bindItemEvent(item),  
63 - input: v => onInput({ value: v, item }),  
64 - },  
65 - },  
66 - })  
67 - "  
68 - ></dynamic-render>  
69 - <component  
70 - v-else  
71 - :is="item.type"  
72 - :value="itemValue(item)"  
73 - @input="v => onInput({ value: v, item })"  
74 - v-on="bindItemEvent(item)"  
75 - v-bind="propsFormatter(item.props)"  
76 - :style="item.style || { width: '100%' }"  
77 - ></component>  
78 - </template>  
79 - </el-form-item>  
80 - </component>  
81 - </template>  
82 - </template>  
83 - </component>  
84 -</template>  
85 -  
86 -<script>  
87 -export default {  
88 - name: 'SchemaFormRender',  
89 - components: {  
90 - DynamicRender: {  
91 - functional: true,  
92 - render(h, context) {  
93 - return context.props.render;  
94 - },  
95 - },  
96 - },  
97 - props: {  
98 - list: Array,  
99 - model: Object,  
100 - value: Object,  
101 - itemKey: String,  
102 - titleClass: String,  
103 - contentClass: String,  
104 - itemClass: String,  
105 - colClass: [String, Function],  
106 - groupClass: String,  
107 - type: String,  
108 - span: Number,  
109 - params: Object,  
110 - },  
111 - computed: {  
112 - rowComponent() {  
113 - return this.type === 'div' ? 'div' : 'el-row';  
114 - },  
115 - colComponent() {  
116 - return this.type === 'div' ? 'div' : 'el-col';  
117 - },  
118 - },  
119 - filters: {  
120 - /**  
121 - * @description 绑定表单项规则  
122 - * @param {Object} item 表单项配置  
123 - * @returns {Function} 事件函数  
124 - */  
125 - bindItemRulesFilter(item = {}, model) {  
126 - if (item.rules) {  
127 - if (typeof item.rules === 'function') {  
128 - return item.rules(model);  
129 - } else {  
130 - return item.rules;  
131 - }  
132 - } else {  
133 - return undefined;  
134 - }  
135 - },  
136 - },  
137 - methods: {  
138 - /**  
139 - * @description 渲染col class  
140 - * @param {Object} item 表单项配置  
141 - * @param {Object} index 表单项渲染下标  
142 - * @param {Object} colClass 表单项配置  
143 - * @return {String} col class  
144 - */  
145 - colClassRender(item, index, colClass) {  
146 - if (colClass instanceof Function) {  
147 - return colClass(item, index);  
148 - } else {  
149 - return colClass;  
150 - }  
151 - },  
152 - /**  
153 - * @description 根据表单项的key查询该值  
154 - * @param {Object} item 表单项配置  
155 - * @returns {Any} 返回值  
156 - */  
157 - itemValue(item) {  
158 - if (this.itemKey) {  
159 - // 如果存在itemKey,即当前项位于嵌套分组内,查询分组名下对应key的值  
160 - const groupItem = this.model[this.itemKey] || {};  
161 - return groupItem[item.key];  
162 - } else {  
163 - // 否则即意味着不在分组内,直接查询model下对应key的值  
164 - return this.model[item.key];  
165 - }  
166 - },  
167 - /**  
168 - * @description 组件有值输入时的事件  
169 - * @param {Object} data { value => 组件值; item => 表单项配置 }  
170 - */  
171 - onInput({ value, item }) {  
172 - if (this.itemKey) {  
173 - this.$emit('item-change', { [this.itemKey]: { ...this.model[this.itemKey], [item.key]: value } });  
174 - } else {  
175 - this.$emit('item-change', { [item.key]: value });  
176 - }  
177 - this.$emit('form-item-change', { [item.fullKey]: value });  
178 - },  
179 - /**  
180 - * @description 当表单项有改动时的事件  
181 - * @param {Any} 表单项值  
182 - */  
183 - onItemChange(value) {  
184 - if (this.itemKey) {  
185 - this.$emit('item-change', { [this.itemKey]: { ...this.model[this.itemKey], ...value } });  
186 - } else {  
187 - this.$emit('item-change', value);  
188 - }  
189 - },  
190 - /**  
191 - * @description 当表单项校验值有改动时的事件  
192 - * @param {Any} 表单项校验值  
193 - */  
194 - onFormItemChange(value) {  
195 - this.$emit('form-item-change', value);  
196 - },  
197 - /**  
198 - * @description 当表单项有手动更新时的事件  
199 - * @param {Any} 表单项值  
200 - */  
201 - onItemUpdate(value) {  
202 - this.$emit('item-update', value);  
203 - },  
204 - /**  
205 - * @description 绑定表单项事件  
206 - * @param {Object} item 表单项配置  
207 - * @returns {Function} 事件函数  
208 - */  
209 - bindItemEvent(item) {  
210 - if (item.on) {  
211 - if (typeof item.on === 'function') {  
212 - return item.on({ model: this.value, update: e => this.$emit('item-update', e) });  
213 - } else {  
214 - return item.on;  
215 - }  
216 - } else {  
217 - return undefined;  
218 - }  
219 - },  
220 - /**  
221 - * @description 绑定表单项显示状态  
222 - * @param {Object} item 表单项配置  
223 - * @param {String} type Vue显示类型,可选值:visible、show  
224 - * @returns {Boolean} 显示状态  
225 - */  
226 - bindItemVisible(item, type) {  
227 - const visible = item[type];  
228 - if (typeof visible === 'function') {  
229 - return visible(this.model, this.params || {});  
230 - }  
231 - return item[type] !== false;  
232 - },  
233 - /**  
234 - * @description 格式化props属性  
235 - * @param {Object|Function} props 属性或属性对象  
236 - * @returns {Object} 格式化的属性  
237 - */  
238 - propsFormatter(props) {  
239 - if (typeof props === 'function') {  
240 - return props(this.model, this.params || {});  
241 - }  
242 - return props || {};  
243 - },  
244 - },  
245 -};  
246 -</script>