Commit 463409cd567cd67ef65f25748e693e9324339134

Authored by 刘汉宸
1 parent a9b1e408

feat: 修改SchemaForm组件,更新文档

examples/router/routes.js
... ... @@ -134,6 +134,12 @@ const _develops = [
134 134 group: '方案开发',
135 135 children: [
136 136 {
  137 + path: 'introduce',
  138 + name: 'developSchemaIntroduce',
  139 + meta: { title: '介绍' },
  140 + component: () => import('@/views/docs/develop/schema/introduce.md'),
  141 + },
  142 + {
137 143 path: 'schema-form',
138 144 name: 'developSchemaForm',
139 145 meta: { title: 'Schema Form 方案表单' },
... ...
examples/views/docs/component/schema-form.md
1   -# Form 表单
  1 +# Schema Form 方案表单
2 2  
3   -根据JSON Schema配置自动生成表单
  3 +通过配置JSON Schema的方式快速生成一个表单
4 4  
5 5 ## 基础用法
6 6  
7   -配置`list`属性设置JSON Schema配置列表
  7 +`schema`设置配置项,其中**props**参数与`z-form`组件参数相同,**items**则对应`z-form-item`组件,默认参数为`<component>`组件与`z-form-item`组件参数的并集。
8 8  
9   -<div class="code-snippet-box">
10   -
11   -::: snippet `v-model`值动态改变,支持初始化
  9 +::: snippet 基础示例
12 10  
13 11 ```html
14 12 <template>
15   - <div>
16   - <pre class="demo-model">{{ model }}</pre>
17   - <z-schema-form v-model="model" :list="list"></z-schema-form>
18   - </div>
  13 + <z-schema-form v-model="form" :schema="schema"></z-schema-form>
19 14 </template>
20 15  
21 16 <script>
22 17 export default {
23 18 data() {
24 19 return {
25   - model: { name: '', age: '' },
26   - list: [
27   - { type: 'el-input', label: '姓名', key: 'name' },
28   - { type: 'el-input-number', label: '年龄', key: 'age', props: { 'controls-position': 'right' } },
29   - ]
30   - }
31   - }
32   -}
  20 + form: {
  21 + name: '张三',
  22 + age: 27,
  23 + address: '上海市青浦区华新镇纪鹤公路1988号',
  24 + },
  25 + schema: {
  26 + props: { labelWidth: '70px', size: 'small', span: 12 },
  27 + items: [
  28 + { is: 'el-input', prop: 'name', label: '姓名' },
  29 + { is: 'el-input-number', prop: 'age', label: '年龄' },
  30 + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 },
  31 + ]
  32 + },
  33 + };
  34 + },
  35 +};
33 36 </script>
34 37 ```
35 38  
36 39 :::
37 40  
38   -::: snippet `span`设置行占位
  41 +## 表单校验
  42 +
  43 +配置下的`props`对应`z-form`的参数,可以在此统一配置`rules`;配置下的`items`的每一项都对应`z-form-item`,也可以直接配置`rules`设置该项的校验规则。
  44 +
  45 +<div class="code-snippet-box">
  46 +
  47 +::: snippet 设置**props**下的校验规则示例
39 48  
40 49 ```html
41 50 <template>
42   - <z-schema-form v-model="model" :list="list" :span="12"></z-schema-form>
  51 + <z-schema-form v-model="form" :schema="schema"></z-schema-form>
43 52 </template>
44 53  
45 54 <script>
46 55 export default {
47 56 data() {
48 57 return {
49   - model: { name: '', age: '' },
50   - list: [
51   - { type: 'el-input', label: '姓名', key: 'name' },
52   - { type: 'el-input-number', label: '年龄', key: 'age', props: { 'controls-position': 'right' } },
53   - ]
54   - }
55   - }
56   -}
  58 + form: {
  59 + name: '张三',
  60 + age: 27,
  61 + address: '上海市青浦区华新镇纪鹤公路1988号',
  62 + },
  63 + schema: {
  64 + props: {
  65 + labelWidth: '70px',
  66 + size: 'small',
  67 + span: 12,
  68 + rules: {
  69 + name: [{ required: true, message: '请输入姓名' }],
  70 + age: [{ required: true, message: '请输入有效年龄' }],
  71 + }
  72 + },
  73 + items: [
  74 + { is: 'el-input', prop: 'name', label: '姓名' },
  75 + { is: 'el-input-number', prop: 'age', label: '年龄' },
  76 + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 },
  77 + ]
  78 + },
  79 + };
  80 + },
  81 +};
57 82 </script>
58 83 ```
59 84  
60 85 :::
61 86  
62   -::: snippet `label-width`设置标题宽度
  87 +::: snippet 设置**items**下的校验规则示例
63 88  
64 89 ```html
65 90 <template>
66   - <z-schema-form v-model="model" :list="list" label-width="60px"></z-schema-form>
  91 + <z-schema-form v-model="form" :schema="schema"></z-schema-form>
67 92 </template>
68 93  
69 94 <script>
70 95 export default {
71 96 data() {
72 97 return {
73   - model: { name: '', age: '' },
74   - list: [
75   - { type: 'el-input', label: '姓名', key: 'name' },
76   - { type: 'el-input-number', label: '年龄', key: 'age', props: { 'controls-position': 'right' } },
77   - ]
78   - }
79   - }
80   -}
  98 + form: {
  99 + name: '张三',
  100 + age: 27,
  101 + address: '上海市青浦区华新镇纪鹤公路1988号',
  102 + },
  103 + schema: {
  104 + props: { labelWidth: '70px', size: 'small', span: 12 },
  105 + items: [
  106 + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] },
  107 + { is: 'el-input-number', prop: 'age', label: '年龄', rules: [{ required: true, message: '请输入有效年龄' }] },
  108 + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 },
  109 + ]
  110 + },
  111 + };
  112 + },
  113 +};
81 114 </script>
82 115 ```
83 116  
... ... @@ -85,196 +118,378 @@ export default {
85 118  
86 119 </div>
87 120  
88   -## 表单校验
89   -
90   -配置`list`的配置项中的`rules`
  121 +## 表单提交
91 122  
92   -<div class="code-snippet-box">
  123 +表单会默认提供一个`footer插槽`,并提供表单的提交、取消、重置等方法,且相关方法都有对应的事件监听。也可以不使用插槽提供的方法直接自定义。
93 124  
94   -::: snippet `v-model`值动态改变,支持初始化
  125 +::: snippet 配置`footer`插槽提供触发方法的示例
95 126  
96 127 ```html
97 128 <template>
98   - <div>
99   - <z-schema-form ref="form" v-model="model" :list="list" :span="12" @validate="onValidate" @reset="onReset"></z-schema-form>
100   - <el-button size="small" type="primary" @click="submit">提交</el-button>
101   - <el-button size="small" plain @click="reset">重置</el-button>
102   - </div>
  129 + <z-schema-form v-model="form" :schema="schema" @submit="onSubmit" @reset="onReset" @cancel="onCancel">
  130 + <template #footer="{ submit, reset, cancel }">
  131 + <z-form-item>
  132 + <el-button type="primary" @click="submit">确定</el-button>
  133 + <el-button plain @click="reset">重置</el-button>
  134 + <el-button plain @click="cancel">取消</el-button>
  135 + </z-form-item>
  136 + </template>
  137 + </z-schema-form>
103 138 </template>
104 139  
105 140 <script>
106 141 export default {
107 142 data() {
108 143 return {
109   - model: { name: '', age: '' },
110   - list: [
111   - { type: 'el-input', label: '姓名', key: 'name', rules: [{ required: true, message: '请输入', trigger: 'change' }] },
112   - { type: 'el-input-number', label: '年龄', key: 'age', props: { 'controls-position': 'right' } },
113   - ]
114   - }
  144 + form: {
  145 + name: '张三',
  146 + age: 27,
  147 + address: '上海市青浦区华新镇纪鹤公路1988号',
  148 + },
  149 + schema: {
  150 + props: { labelWidth: '70px', size: 'small', span: 12 },
  151 + items: [
  152 + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] },
  153 + { is: 'el-input-number', prop: 'age', label: '年龄', rules: [{ required: true, message: '请输入有效年龄' }] },
  154 + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 },
  155 + ]
  156 + },
  157 + };
115 158 },
116 159 methods: {
117   - onValidate(valid, model) {
118   - if (valid) {
119   - console.log(model);
120   - }
  160 + onSubmit(value) {
  161 + console.log(value);
121 162 },
122 163 onReset() {
123   - Object.keys(this.model).forEach(key => {
124   - this.model[key] = '';
  164 + console.log('reset');
  165 + },
  166 + onCancel() {
  167 + console.log('cancal');
  168 + }
  169 + }
  170 +};
  171 +</script>
  172 +```
  173 +
  174 +:::
  175 +
  176 +::: snippet 自定义提交的示例
  177 +
  178 +```html
  179 +<template>
  180 + <z-schema-form ref="form" v-model="form" :schema="schema" @submit="onSubmit" @reset="onReset" @cancel="onCancel">
  181 + <template #footer>
  182 + <z-form-item>
  183 + <el-button type="primary" @click="onSubmit">确定</el-button>
  184 + <el-button plain @click="onReset">重置</el-button>
  185 + <el-button plain @click="onCancel">取消</el-button>
  186 + </z-form-item>
  187 + </template>
  188 + </z-schema-form>
  189 +</template>
  190 +
  191 +<script>
  192 +export default {
  193 + data() {
  194 + return {
  195 + form: {
  196 + name: '张三',
  197 + age: 27,
  198 + address: '上海市青浦区华新镇纪鹤公路1988号',
  199 + },
  200 + schema: {
  201 + props: { labelWidth: '70px', size: 'small', span: 12 },
  202 + items: [
  203 + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] },
  204 + { is: 'el-input-number', prop: 'age', label: '年龄', rules: [{ required: true, message: '请输入有效年龄' }] },
  205 + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 },
  206 + ]
  207 + },
  208 + };
  209 + },
  210 + methods: {
  211 + onSubmit() {
  212 + this.$refs.form.validate(valid => {
  213 + if (valid) {
  214 + console.log(this.form);
  215 + }
125 216 });
126 217 },
127   - submit() {
128   - this.$refs.form.validate();
  218 + onReset() {
  219 + this.$refs.form.resetFields();
129 220 },
130   - reset() {
131   - this.$refs.form.reset();
  221 + onCancel() {
  222 + console.log('cancal');
132 223 }
133 224 }
134   -}
  225 +};
135 226 </script>
136 227 ```
137 228  
138 229 :::
139 230  
140   -</div>
  231 +## 配置复用
141 232  
142   -## 表单分组
  233 +因为与`z-form`、`z-form-item`用法相同,所以除了可以用作表单以外,也可用作详情展示,同时因为本身是schema配置项,所以可以做到表单和详情**配置复用**。
143 234  
144   -将表单内容进行分组显示
145   -
146   -::: snippet 表单配置项的`group`值设置分组
  235 +::: snippet 配置复用示例
147 236  
148 237 ```html
149 238 <template>
150   - <div>
151   - <pre class="demo-model">{{ model }}</pre>
152   - <z-schema-form v-model="model" :list="list" :span="8" label-width="80px"></z-schema-form>
153   - </div>
  239 + <el-row>
  240 + <el-col :span="12">
  241 + <div style="margin-bottom: 20px">表单</div>
  242 + <z-schema-form v-model="form" :schema="schema"></z-schema-form>
  243 + </el-col>
  244 + <el-col :span="12">
  245 + <div style="margin-bottom: 20px">详情</div>
  246 + <z-schema-form :value="detail" :schema="detailSchema"></z-schema-form>
  247 + </el-col>
  248 + </el-row>
154 249 </template>
155 250  
156 251 <script>
157 252 export default {
158 253 data() {
159 254 return {
160   - model: { name: '', age: '', company: '', address: '', time: '', salary: '' },
161   - list: [
162   - {
163   - group: { title: '基础信息' },
164   - list: [
165   - { type: 'el-input', label: '姓名', key: 'name', rules: [{ required: true, message: '请输入', trigger: 'change' }], span: 12 },
166   - { type: 'el-input-number', label: '年龄', key: 'age', props: { 'controls-position': 'right' }, span: 12 },
167   - ],
168   - },
169   - {
170   - group: { title: '工作信息' },
171   - list: [
172   - { type: 'el-input', label: '公司', key: 'company' },
173   - { type: 'el-input', label: '地址', key: 'address' },
174   - { type: 'el-input', label: '时限', key: 'time' },
175   - { type: 'el-input', label: '薪资', key: 'salary' },
176   - ],
177   - },
178   - ]
179   - }
  255 + form: {
  256 + name: '张三',
  257 + age: 27,
  258 + address: '上海市青浦区华新镇纪鹤公路1988号',
  259 + },
  260 + detail: {
  261 + name: '张三',
  262 + age: 27,
  263 + address: '上海市青浦区华新镇纪鹤公路1988号',
  264 + },
  265 + schema: {
  266 + props: { labelWidth: '70px', size: 'small', span: 12 },
  267 + items: [
  268 + { is: 'el-input', prop: 'name', label: '姓名' },
  269 + { is: 'el-input-number', prop: 'age', label: '年龄' },
  270 + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 },
  271 + ]
  272 + },
  273 + };
180 274 },
181   -}
  275 + computed: {
  276 + detailSchema() {
  277 + const schema = this.schema;
  278 + // 去掉所有配置项的is参数即默认以文本展示
  279 + return { ...schema, items: schema.items.map(({ is, ...other }) => ({ ...other })) }
  280 + }
  281 + }
  282 +};
182 283 </script>
183 284 ```
184 285  
185 286 :::
186 287  
187   -## 自定义渲染
  288 +## 自定义插槽
188 289  
189   -对于一些特殊渲染逻辑的表单项,可以进行自定义渲染
  290 +在某些特殊情况下,表单项可能是当前页面的临时组件,或者项目并没有全局引入组件,亦或者需要一些特殊的函数去处理,此时可以使用自定义插槽替换默认配置项。插槽默认提供了`value`和`onInput`用于维护当前值。
190 291  
191   -<div class="code-snippet-box">
192   -
193   -::: snippet 通过`render`方法渲染
  292 +::: snippet schema配置中items配置项的`is`设置为`slot`,即可在组件下通过插槽的方式替换配置项。默认支持`具名插槽`、`label`插槽、`error`插槽,与`el-form-item`的插槽类似。
194 293  
195 294 ```html
196 295 <template>
197   - <div>
198   - <pre class="demo-model">{{ model }}</pre>
199   - <z-schema-form v-model="model" :list="list" label-width="80px"></z-schema-form>
200   - </div>
  296 + <z-schema-form v-model="form" :schema="schema">
  297 + <!-- error插槽,作用:修改当前项的校验信息样式 -->
  298 + <template #error-name>
  299 + <div><span>请输入</span><el-tag size="mini">格式正确</el-tag><span>的姓名</span></div>
  300 + </template>
  301 + <!-- label插槽,作用:自定义当前项的label -->
  302 + <template #label-age>
  303 + <el-tooltip content="说明" placement="top" effect="light">
  304 + <i class="el-icon-question"></i>
  305 + </el-tooltip>
  306 + <span>年龄</span>
  307 + </template>
  308 + <!-- 具名插槽,作用:自定义当前项的值 -->
  309 + <template #address="{ value, onInput }">
  310 + <div>
  311 + <el-link size="mini" type="primary" :underline="false" @click="() => onInput('当前地址xxxxxx')">键入当前地址</el-link>
  312 + <el-input type="textarea" :value="value" @input="onInput" />
  313 + </div>
  314 + </template>
  315 + </z-schema-form>
201 316 </template>
202 317  
203 318 <script>
204 319 export default {
205 320 data() {
206 321 return {
207   - model: { name: '', age: '' },
208   - list: [
209   - { type: 'el-input', label: '姓名', key: 'name' },
210   - { type: 'el-input', label: '年龄', key: 'age', type: (h, { model, config }) => { return h('el-input-number', config, model.age) }, props: { 'controls-position': 'right' } },
211   - ]
212   - }
  322 + form: {
  323 + name: '张三',
  324 + age: 27,
  325 + address: '上海市青浦区华新镇纪鹤公路1988号',
  326 + },
  327 + schema: {
  328 + props: { labelWidth: '70px', size: 'small', span: 12 },
  329 + items: [
  330 + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] },
  331 + { is: 'el-input-number', prop: 'age', label: '年龄' },
  332 + { is: 'slot', prop: 'address', label: '住址', span: 24 },
  333 + ]
  334 + },
  335 + };
213 336 },
214   -}
  337 +};
215 338 </script>
216 339 ```
217 340  
218 341 :::
219 342  
220   -::: snippet 通过`slot`插槽渲染
  343 +## 配置子节点
  344 +
  345 +使用表单时,除了可能会出现单组件子节点以外,可能出现带有子节点的子组件。schema也支持对子节点进行配置。
  346 +
  347 +::: snippet schema配置中items配置项的`children`可以配置子节点,格式同`<component>`组件。
221 348  
222 349 ```html
223 350 <template>
224   - <div>
225   - <pre class="demo-model">{{ model }}</pre>
226   - <z-schema-form v-model="model" :list="list" label-width="80px">
227   - <template slot="name">
228   - <el-radio v-model="model.name" label="zs">张三</el-radio>
229   - <el-radio v-model="model.name" label="ls">李四</el-radio>
230   - </template>
231   - <template slot="age">
232   - <el-slider v-model="model.age"></el-slider>
233   - </template>
234   - </z-schema-form>
235   - </div>
  351 + <z-schema-form v-model="form" :schema="schema"></z-schema-form>
236 352 </template>
237 353  
238 354 <script>
239 355 export default {
240 356 data() {
241 357 return {
242   - model: { name: 'zs', age: 0 },
243   - list: [
244   - { type: 'el-input', label: '姓名', key: 'name' },
245   - { type: 'el-input', label: '年龄', key: 'age' },
246   - ]
247   - }
  358 + form: {
  359 + name: '张三',
  360 + age: 27,
  361 + address: '上海市青浦区华新镇纪鹤公路1988号',
  362 + packages: ['零食'],
  363 + },
  364 + schema: {
  365 + props: { labelWidth: '70px', size: 'small', span: 12 },
  366 + items: [
  367 + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] },
  368 + { is: 'el-input-number', prop: 'age', label: '年龄' },
  369 + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 },
  370 + {
  371 + is: 'el-checkbox-group',
  372 + prop: 'packages',
  373 + label: '包裹',
  374 + children: [
  375 + { is: 'el-checkbox', props: { label: '零食' } },
  376 + { is: 'el-checkbox', props: { label: '手机' } },
  377 + { is: 'el-checkbox', props: { label: '电脑' } },
  378 + ],
  379 + span: 24
  380 + },
  381 + ]
  382 + },
  383 + };
248 384 },
249   -}
  385 +};
250 386 </script>
251 387 ```
252 388  
253 389 :::
254 390  
255   -</div>
  391 +## 复杂表单
  392 +
  393 +一般情况下,复杂表单建议使用`z-form`、`z-form-item`等组件构建视图,`不建议`使用schema的方式配置。但是利用schema配置项的一些特性,也可以做到部分复杂表单使用schema配置完成。
  394 +
  395 +::: snippet 包含了**插槽**、**子节点**、**自定义子节点**的复杂示例
  396 +
  397 +```html
  398 +<template>
  399 + <z-schema-form v-model="form" :schema="schema" @submit="onSubmit" @cancel="onCancel">
  400 + <template #error-name>
  401 + <div><span>请输入</span><el-tag size="mini">格式正确</el-tag><span>的姓名</span></div>
  402 + </template>
  403 + <template #label-packages>
  404 + <el-tooltip content="详情" placement="top" effect="light">
  405 + <i class="el-icon-question"></i>
  406 + </el-tooltip>
  407 + <span>包裹</span>
  408 + </template>
  409 + </z-schema-form>
  410 +</template>
  411 +
  412 +<script>
  413 +export default {
  414 + data() {
  415 + return {
  416 + form: {
  417 + name: '张三',
  418 + age: 27,
  419 + address: '上海市青浦区华新镇纪鹤公路1988号',
  420 + packages: ['零食'],
  421 + info: {
  422 + mobile: '18888888888',
  423 + },
  424 + admin: 'false',
  425 + },
  426 + schema: {
  427 + props: { labelWidth: '70px', size: 'small', span: 12 },
  428 + items: [
  429 + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] },
  430 + { is: 'el-input-number', prop: 'age', label: '年龄' },
  431 + { is: 'el-input', prop: 'info.mobile', label: '电话' },
  432 + { is: 'el-divider', props: { 'content-position': 'left' }, children: '收货信息', span: 24, labelWidth: '0px' },
  433 + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 },
  434 + {
  435 + is: 'el-checkbox-group',
  436 + prop: 'packages',
  437 + label: '包裹',
  438 + children: [
  439 + { is: 'el-checkbox', props: { label: '零食' } },
  440 + { is: 'el-checkbox', props: { label: '手机' } },
  441 + { is: 'el-checkbox', props: { label: '电脑' } },
  442 + ],
  443 + },
  444 + {
  445 + is: 'el-select',
  446 + prop: 'admin',
  447 + label: '管理员',
  448 + children: [
  449 + {
  450 + is: 'el-option',
  451 + props: { label: '是', value: 'true' },
  452 + children: [
  453 + {
  454 + is: 'div',
  455 + attrs: { style: 'display: flex; align-items: center; justify-content: space-between' },
  456 + children: [
  457 + { is: 'span', children: '是' },
  458 + { is: 'el-tag', props: { size: 'mini', type: 'success' }, children: 'yes' },
  459 + ]
  460 + },
  461 + ]
  462 + },
  463 + {
  464 + is: 'el-option',
  465 + props: { label: '否', value: 'false' },
  466 + children: [
  467 + {
  468 + is: 'div',
  469 + attrs: { style: 'display: flex; align-items: center; justify-content: space-between' },
  470 + children: [
  471 + { is: 'span', children: '否' },
  472 + { is: 'el-tag', props: { size: 'mini', type: 'warning' }, children: 'no' },
  473 + ]
  474 + },
  475 + ]
  476 + },
  477 + ],
  478 + },
  479 + ]
  480 + },
  481 + };
  482 + },
  483 + methods: {
  484 + onSubmit(value) {
  485 + console.log(value);
  486 + },
  487 + onCancel() {
  488 + console.log('cancal');
  489 + }
  490 + }
  491 +};
  492 +</script>
  493 +```
256 494  
257   -## API
258   -
259   -## Attribute 属性
260   -
261   -参数|说明|类型|可选值|默认值
262   --|-|-|-|-
263   -value | 值 | Object | - | -
264   -list | JSON Schema配置项列表 | Array | - | []
265   -formClass | 表单class | String | - | -
266   -titleClass | 标题class | String | - | -
267   -contentClass | 内容class | String | - | -
268   -itemClass | 表单项class | String | - | -
269   -groupClass | 表单分组class | String | - | -
270   -labelWidth | 表单项标题宽度 | String | - | -
271   -labelPosition | 表单项标题位置 | String | - | top
272   -size | 大小 | String | - | small
273   -span | 占位 | Number | - | 24
274   -
275   -## Events 事件
276   -
277   -事件名称|说明|回调参数
278   --|-|-
279   -input | 表单值变化 | 表单model
280   -validate | 校验表单 | 是否通过,表单model
281 495 \ No newline at end of file
  496 +:::
282 497 \ No newline at end of file
... ...
examples/views/docs/develop/schema/introduce.md 0 → 100644
... ... @@ -0,0 +1,260 @@
  1 +# 介绍
  2 +
  3 +方案开发是一种使用类`JSON Schema`格式的预置数据,快速生成一个业务逻辑视图,并且能够被引用及复用。
  4 +
  5 +## 使用场景
  6 +
  7 +① 复杂后台管理项目中,某些相同的功能可能会多次出现在不同的使用场景中,并且**同时**具有**相似性**和**差异性**,而**组件化**不能更方便的解决类似的场景。
  8 +
  9 +② 逻辑并不复杂,但是表单项非常多,需要反复编写大量**重复**的**布局代码**,导致**代码量大**,业务逻辑代码**维护不便**的场景。
  10 +
  11 +③ 不同功能页面下,使用了相同的表单或表格等相同的功能模块,遇到**改动**时,需要重新在多个页面里多次**重复修改**代码的场景。
  12 +
  13 +## 示例
  14 +
  15 +某页面的详情功能,常规情形下,会出现大量重复的布局组件`el-row`、`el-col`及表单组件`el-form-item`等,进行取值展示时,也需要多次书写`form.xxx`。本是一个没有业务逻辑的展示型页面,但大量书写的重复的代码,导致行数过多,且如遇到增删某个展示项时,还需要重新调整布局代码。总体上来说,代码可读性低,可维护性差。
  16 +
  17 +代码:
  18 +
  19 +```html
  20 +<template>
  21 + <el-form :model="form" size="small" label-width="100px">
  22 + <el-row>
  23 + <el-col :span="12">
  24 + <el-form-item label="线路名称">
  25 + {{ form.dispatchLine.lineName }}
  26 + </el-form-item>
  27 + </el-col>
  28 + <el-col :span="12">
  29 + <el-form-item label="出发站点">
  30 + {{ form.dispatchLine.startStation ? form.dispatchLine.startStation.name : '' }}
  31 + </el-form-item>
  32 + </el-col>
  33 + </el-row>
  34 + <el-row>
  35 + <el-col :span="12">
  36 + <el-form-item label="目的站点">
  37 + {{ form.dispatchLine.endStation ? form.dispatchLine.endStation.name : '' }}
  38 + </el-form-item>
  39 + </el-col>
  40 + <el-col :span="12">
  41 + <el-form-item label="客户名称">
  42 + {{ form.customer ? form.customer.name : '' }}
  43 + </el-form-item>
  44 + </el-col>
  45 + </el-row>
  46 + <el-row>
  47 + <el-col :span="12">
  48 + <el-form-item label="线路类型">
  49 + <template>
  50 + <dictionary-select-name option-name="LINE_TYPE" :value="form.dispatchLine.lineType"></dictionary-select-name>
  51 + </template>
  52 + </el-form-item>
  53 + </el-col>
  54 + <el-col :span="12">
  55 + <el-form-item label="出发城市">
  56 + {{ form.dispatchLine.startStation.city.name }}
  57 + </el-form-item>
  58 + </el-col>
  59 + </el-row>
  60 + <el-row>
  61 + <el-col :span="12">
  62 + <el-form-item label="经停城市">
  63 + {{ form.dispatchLine.stopOverCityName }}
  64 + </el-form-item>
  65 + </el-col>
  66 + <el-col :span="12">
  67 + <el-form-item label="目的城市">
  68 + {{ form.dispatchLine.endStation.city.name }}
  69 + </el-form-item>
  70 + </el-col>
  71 + </el-row>
  72 + </el-form>
  73 +</template>
  74 +
  75 +<script>
  76 +export default {
  77 + data() {
  78 + return {
  79 + form: {
  80 + dispatchLine: {
  81 + startStation: {
  82 + city: {},
  83 + },
  84 + endStation: {
  85 + city: {},
  86 + },
  87 + },
  88 + customer: {},
  89 + },
  90 + };
  91 + },
  92 +}
  93 +</script>
  94 +```
  95 +
  96 +仅视图层就有53行代码,改用Schema开发后可将视图层代码极致精简,可以使后端开发员修改前端代码时不需要关心界面布局及样式,只关心业务逻辑。
  97 +
  98 +使用Schema开发后的代码,视图层1行,而配置项也仅需13行代码,并且无需关心布局代码和多层级数据取值容错:
  99 +
  100 +```html
  101 +<template>
  102 + <z-schema-form v-model="form" :schema="schema"></z-schema-form>
  103 +</template>
  104 +
  105 +<script>
  106 +export default {
  107 + data() {
  108 + return {
  109 + schema: {
  110 + props: { size: 'small', 'label-width': '100px', span: 12 },
  111 + items: [
  112 + { prop: 'dispatchLine.lineName', label: '线路名称' },
  113 + { prop: 'dispatchLine.startStation.name', label: '出发站点' },
  114 + { prop: 'dispatchLine.endStation.name', label: '目的站点' },
  115 + { prop: 'customer.name', label: '客户名称' },
  116 + { is: 'dictionary-select-name', props: { 'option-name': 'LINE_TYPE' }, prop: 'customer.name', label: '线路类型' },
  117 + { prop: 'dispatchLine.startStation.city.name', label: '出发城市' },
  118 + { prop: 'dispatchLine.stopOverCityName', label: '经停城市' },
  119 + { prop: 'dispatchLine.endStation.city.name', label: '经停城市' },
  120 + ]
  121 + },
  122 + form: {
  123 + dispatchLine: {
  124 + startStation: {
  125 + city: {},
  126 + },
  127 + endStation: {
  128 + city: {},
  129 + },
  130 + },
  131 + customer: {},
  132 + },
  133 + };
  134 + },
  135 +}
  136 +</script>
  137 +```
  138 +
  139 +也可以将Schema配置项单独保存在js文件中,需要的时候引入使用即可:
  140 +
  141 +```js
  142 +// xxx.schema.js
  143 +export default {
  144 + props: { size: 'small', 'label-width': '100px', span: 12 },
  145 + items: [
  146 + { prop: 'dispatchLine.lineName', label: '线路名称' },
  147 + { prop: 'dispatchLine.startStation.name', label: '出发站点' },
  148 + { prop: 'dispatchLine.endStation.name', label: '目的站点' },
  149 + { prop: 'customer.name', label: '客户名称' },
  150 + { is: 'dictionary-select-name', props: { 'option-name': 'LINE_TYPE' }, prop: 'customer.name', label: '线路类型' },
  151 + { prop: 'dispatchLine.startStation.city.name', label: '出发城市' },
  152 + { prop: 'dispatchLine.stopOverCityName', label: '经停城市' },
  153 + { prop: 'dispatchLine.endStation.city.name', label: '经停城市' },
  154 + ]
  155 +}
  156 +```
  157 +
  158 +这样可以将业务逻辑结构和视图抽离并可复用。
  159 +
  160 +```html
  161 +<template>
  162 + <z-schema-form v-model="form" :schema="schema"></z-schema-form>
  163 +</template>
  164 +
  165 +<script>
  166 +import xxxSchema from '@/schema/xxx.schema.js'l
  167 +
  168 +export default {
  169 + data() {
  170 + return {
  171 + schema: xxxSchema,
  172 + form: {
  173 + dispatchLine: {
  174 + startStation: {
  175 + city: {},
  176 + },
  177 + endStation: {
  178 + city: {},
  179 + },
  180 + },
  181 + customer: {},
  182 + },
  183 + };
  184 + },
  185 +}
  186 +</script>
  187 +```
  188 +
  189 +也可对原始配置项进行一个差异化的处理,比如配置项相同的情况下改变schema已配置的属性:
  190 +
  191 +```html
  192 +<template>
  193 + <z-schema-form v-model="form" :schema="schema"></z-schema-form>
  194 +</template>
  195 +
  196 +<script>
  197 +import xxxSchema from '@/schema/xxx.schema.js'l
  198 +
  199 +export default {
  200 + data() {
  201 + return {
  202 + schema: { ...xxxSchema, props: { size: 'mini', 'label-width': '60px', span: 24 } },
  203 + form: {
  204 + dispatchLine: {
  205 + startStation: {
  206 + city: {},
  207 + },
  208 + endStation: {
  209 + city: {},
  210 + },
  211 + },
  212 + customer: {},
  213 + },
  214 + };
  215 + },
  216 +}
  217 +</script>
  218 +```
  219 +
  220 +或者在配置项的基础上再追加几个配置项:
  221 +
  222 +```html
  223 +<template>
  224 + <z-schema-form v-model="form" :schema="schema"></z-schema-form>
  225 +</template>
  226 +
  227 +<script>
  228 +import xxxSchema from '@/schema/xxx.schema.js'l
  229 +
  230 +export default {
  231 + data() {
  232 + return {
  233 + schema: {
  234 + ...xxxSchema,
  235 + items: [
  236 + ...xxxSchema.items,
  237 + { prop: 'vehicleLicenseNum', label: '车牌号' },
  238 + { prop: 'remark', label: '备注', spam: 24 },
  239 + ]
  240 + },
  241 + form: {
  242 + dispatchLine: {
  243 + startStation: {
  244 + city: {},
  245 + },
  246 + endStation: {
  247 + city: {},
  248 + },
  249 + },
  250 + customer: {},
  251 + },
  252 + };
  253 + },
  254 +}
  255 +</script>
  256 +```
  257 +
  258 +## 更多
  259 +
  260 +更多具体示例,请查看`schema-form`、`schema-table`、`schema-page`文档说明。
0 261 \ No newline at end of file
... ...
packages/schema-form/index.vue
... ... @@ -5,47 +5,33 @@
5 5 <z-form-item v-bind="keywordFilter(item)" :key="index">
6 6 <slot
7 7 v-if="$scopedSlots[item.prop]"
8   - :name="`${item.prop}`"
  8 + :name="item.prop"
9 9 :value="get(model, item.prop)"
10 10 :onInput="value => onComponentInput({ value, item })"
11 11 :props="{ value: get(model, item.prop) }"
12 12 :listeners="{ input: value => onComponentInput({ value, item }) }"
  13 + v-bind="slotProps"
13 14 >
14 15 </slot>
15   - <component v-else :is="item.is" :value="get(model, item.prop)" @input="value => onComponentInput({ value, item })" v-bind="item.props">
16   - <template v-if="item.render">
17   - <dynamic-render
18   - v-if="typeof item.render === 'function'"
19   - :render="
20   - item.render($createElement, {
21   - model,
22   - value: get(model, item.prop),
23   - onInput: value => onComponentInput({ value, item }),
24   - })
25   - "
26   - ></dynamic-render>
27   - <static-render v-else :render="item.render"></static-render>
28   - </template>
29   - <template v-if="item.children">
30   - <childrens-render :value="item.children"></childrens-render>
31   - </template>
32   - </component>
33   - <slot :name="`label-${item.prop}`" slot="label"></slot>
34   - <slot :name="`error-${item.prop}`" slot="error"></slot>
  16 + <item-render v-else :item="item" :value="get(model, item.prop)" :onInput="value => onComponentInput({ value, item })"></item-render>
  17 + <slot :name="`label-${item.prop}`" slot="label" v-bind="slotProps"></slot>
  18 + <slot :name="`error-${item.prop}`" slot="error" v-bind="slotProps"></slot>
35 19 </z-form-item>
36 20 </template>
37 21 <template v-else>
38   - <z-form-item v-bind="keywordFilter(item)" :key="index">
39   - <slot :name="item.prop"></slot>
  22 + <z-form-item v-bind="keywordFilter(item)" :key="index" :value="get(model, item.prop)">
  23 + <slot
  24 + :name="item.prop"
  25 + :value="get(model, item.prop)"
  26 + :onInput="value => onComponentInput({ value, item })"
  27 + :props="{ value: get(model, item.prop) }"
  28 + :listeners="{ input: value => onComponentInput({ value, item }) }"
  29 + v-bind="slotProps"
  30 + ></slot>
40 31 </z-form-item>
41 32 </template>
42 33 </template>
43   - <slot v-if="schema.footer !== false" name="footer">
44   - <z-form-item>
45   - <el-button type="primary" @click="onSubmit">确定</el-button>
46   - <el-button plain @click="onCancel">取消</el-button>
47   - </z-form-item>
48   - </slot>
  34 + <slot name="footer" v-bind="slotProps"></slot>
49 35 </z-form>
50 36 </template>
51 37  
... ... @@ -55,28 +41,33 @@ import { cloneDeep, get, set } from &#39;../utils&#39;;
55 41 export default {
56 42 name: 'SchemaForm',
57 43 components: {
58   - DynamicRender: {
59   - functional: true,
60   - render(h, context) {
61   - return context.props.render;
62   - },
63   - },
64   - StaticRender: {
65   - functional: true,
66   - render(h, context) {
67   - return h('span', [context.props.render]);
68   - },
69   - },
70   - ChildrensRender: {
  44 + ItemRender: {
71 45 functional: true,
72 46 render(h, context) {
73   - return context.props.value.map(item => {
74   - if (Array.isArray(item.children) && item.children.length > 0) {
75   - return h('childrens-render', { props: { value: item.children } });
  47 + const props = context.props;
  48 + const item = props.item || {};
  49 + let content = [];
  50 + if (item.render && typeof item.render === 'function') {
  51 + content = context.props.render;
  52 + }
  53 + if (item.children) {
  54 + if (Array.isArray(item.children)) {
  55 + if (item.children.length > 0) {
  56 + content = item.children.map(i => h('item-render', { props: { item: i } }));
  57 + }
76 58 } else {
77   - return h(item.is, { ...item });
  59 + content = [item.children];
78 60 }
79   - });
  61 + }
  62 + let _props = item.props || {};
  63 + if (props.value) {
  64 + _props = { ..._props, value: props.value };
  65 + }
  66 + let _on = item.on || {};
  67 + if (props.onInput) {
  68 + _on = { ..._on, input: props.onInput };
  69 + }
  70 + return h(item.is, { attrs: item.attrs, props: _props, on: _on }, content);
80 71 },
81 72 },
82 73 },
... ... @@ -102,6 +93,13 @@ export default {
102 93 schemaProps() {
103 94 return this.schema.props;
104 95 },
  96 + slotProps() {
  97 + return {
  98 + submit: this.onSubmit,
  99 + cancel: this.onCancel,
  100 + reset: this.onReset,
  101 + };
  102 + },
105 103 },
106 104 created() {
107 105 const { originData, ...other } = this._data;
... ... @@ -120,6 +118,18 @@ export default {
120 118 },
121 119 methods: {
122 120 get,
  121 + validate(callback) {
  122 + return this.$refs.form.validate(callback);
  123 + },
  124 + validateField(props, callback) {
  125 + return this.$refs.form.validateField(props, callback);
  126 + },
  127 + resetFields() {
  128 + this.$refs.form.resetFields();
  129 + },
  130 + clearValidate(props) {
  131 + return this.$refs.form.clearValidate(props);
  132 + },
123 133 keywordFilter(val = {}) {
124 134 const { children, is, ...other } = val;
125 135 return other;
... ... @@ -135,9 +145,13 @@ export default {
135 145 });
136 146 },
137 147 onCancel() {
138   - this.model = cloneDeep(this.originData).model;
139 148 this.$emit('cancel');
140 149 },
  150 + onReset() {
  151 + this.model = cloneDeep(this.originData).model;
  152 + this.$refs.form.resetFields();
  153 + this.$emit('reset');
  154 + },
141 155 },
142 156 };
143 157 </script>
... ...