Commit 57f06f4f36d0623a9f6a86c7e803eef6824b12ab
1 parent
db24c087
Exists in
master
and in
2 other branches
feat: 修改SchemaPage组件
Showing
22 changed files
with
2165 additions
and
992 deletions
Show diff stats
examples/router/routes.js
| 1 | import DefaultLayout from '@/views/layout/default'; | 1 | import DefaultLayout from '@/views/layout/default'; |
| 2 | import ComponentLayout from '@/views/layout/component'; | 2 | import ComponentLayout from '@/views/layout/component'; |
| 3 | import DevelopLayout from '@/views/layout/develop'; | 3 | import DevelopLayout from '@/views/layout/develop'; |
| 4 | +import DesignLayout from '@/views/layout/design'; | ||
| 4 | 5 | ||
| 5 | // 开发指南的文档 | 6 | // 开发指南的文档 |
| 6 | 7 | ||
| @@ -89,10 +90,10 @@ const _components = [ | @@ -89,10 +90,10 @@ const _components = [ | ||
| 89 | component: () => import('@/views/docs/component/schema-table.md'), | 90 | component: () => import('@/views/docs/component/schema-table.md'), |
| 90 | }, | 91 | }, |
| 91 | { | 92 | { |
| 92 | - path: 'schema', | ||
| 93 | - name: 'schema', | ||
| 94 | - meta: { title: 'Schema 方案' }, | ||
| 95 | - component: () => import('@/views/docs/component/schema.md'), | 93 | + path: 'schema-page', |
| 94 | + name: 'schema-page', | ||
| 95 | + meta: { title: 'Schema Page 页面' }, | ||
| 96 | + component: () => import('@/views/docs/component/schema-page.md'), | ||
| 96 | }, | 97 | }, |
| 97 | ], | 98 | ], |
| 98 | }, | 99 | }, |
| @@ -139,24 +140,6 @@ const _develops = [ | @@ -139,24 +140,6 @@ const _develops = [ | ||
| 139 | meta: { title: 'Schema 介绍' }, | 140 | meta: { title: 'Schema 介绍' }, |
| 140 | component: () => import('@/views/docs/develop/schema/introduce.md'), | 141 | component: () => import('@/views/docs/develop/schema/introduce.md'), |
| 141 | }, | 142 | }, |
| 142 | - { | ||
| 143 | - path: 'schema-form', | ||
| 144 | - name: 'developSchemaForm', | ||
| 145 | - meta: { title: 'Schema Form 方案表单' }, | ||
| 146 | - component: () => import('@/views/docs/develop/schema/schema-form.md'), | ||
| 147 | - }, | ||
| 148 | - { | ||
| 149 | - path: 'schema-table', | ||
| 150 | - name: 'developSchemaTable', | ||
| 151 | - meta: { title: 'Schema Table 方案表格' }, | ||
| 152 | - component: () => import('@/views/docs/develop/schema/schema-table.md'), | ||
| 153 | - }, | ||
| 154 | - { | ||
| 155 | - path: 'schema-page', | ||
| 156 | - name: 'developSchemaPage', | ||
| 157 | - meta: { title: 'Schema Page 方案页面' }, | ||
| 158 | - component: () => import('@/views/docs/develop/schema/schema-page.md'), | ||
| 159 | - }, | ||
| 160 | ], | 143 | ], |
| 161 | }, | 144 | }, |
| 162 | ]; | 145 | ]; |
| @@ -166,6 +149,25 @@ _develops.forEach(data => { | @@ -166,6 +149,25 @@ _develops.forEach(data => { | ||
| 166 | _develops_children = [..._develops_children, ...data.children]; | 149 | _develops_children = [..._develops_children, ...data.children]; |
| 167 | }); | 150 | }); |
| 168 | 151 | ||
| 152 | +const _designs = [ | ||
| 153 | + { | ||
| 154 | + group: '表格', | ||
| 155 | + children: [ | ||
| 156 | + { | ||
| 157 | + path: 'table', | ||
| 158 | + name: 'designTable', | ||
| 159 | + meta: { title: '表格设计规范' }, | ||
| 160 | + component: () => import('@/views/docs/design/table.md'), | ||
| 161 | + }, | ||
| 162 | + ], | ||
| 163 | + }, | ||
| 164 | +]; | ||
| 165 | + | ||
| 166 | +let _designs_children = []; | ||
| 167 | +_designs.forEach(data => { | ||
| 168 | + _designs_children = [..._designs_children, ...data.children]; | ||
| 169 | +}); | ||
| 170 | + | ||
| 169 | // 用于导航的页面 | 171 | // 用于导航的页面 |
| 170 | const _pages = [ | 172 | const _pages = [ |
| 171 | { | 173 | { |
| @@ -190,6 +192,14 @@ const _pages = [ | @@ -190,6 +192,14 @@ const _pages = [ | ||
| 190 | children: [..._components_children, ..._guides, ..._others], | 192 | children: [..._components_children, ..._guides, ..._others], |
| 191 | }, | 193 | }, |
| 192 | { | 194 | { |
| 195 | + path: '/design', | ||
| 196 | + name: 'design', | ||
| 197 | + meta: { title: '设计', path: '/design' }, | ||
| 198 | + component: DesignLayout, | ||
| 199 | + redirect: `/design/${_designs[0].children[0].path}`, | ||
| 200 | + children: [..._designs_children], | ||
| 201 | + }, | ||
| 202 | + { | ||
| 193 | path: '/develop', | 203 | path: '/develop', |
| 194 | name: 'develop', | 204 | name: 'develop', |
| 195 | meta: { title: '开发', path: '/develop' }, | 205 | meta: { title: '开发', path: '/develop' }, |
| @@ -204,6 +214,7 @@ export const guides = _guides; | @@ -204,6 +214,7 @@ export const guides = _guides; | ||
| 204 | export const components = _components; | 214 | export const components = _components; |
| 205 | export const others = _others; | 215 | export const others = _others; |
| 206 | export const develops = _develops; | 216 | export const develops = _develops; |
| 217 | +export const designs = _designs; | ||
| 207 | 218 | ||
| 208 | export default [ | 219 | export default [ |
| 209 | { path: '*', redirect: '/404', hidden: true }, | 220 | { path: '*', redirect: '/404', hidden: true }, |
| @@ -0,0 +1,573 @@ | @@ -0,0 +1,573 @@ | ||
| 1 | +# Schema Page 方案页面 | ||
| 2 | + | ||
| 3 | +根据JSON Schema配置自动生成一个包含搜索、表格、表单、详情功能的页面 | ||
| 4 | + | ||
| 5 | +## 基础用法 | ||
| 6 | + | ||
| 7 | +`schema`设置配置项,其中包括`filter`、`table`、`form`三个基本schema配置,配置方式分别对应`z-schema-filter`、`z-schema-table`、`z-schema-form`,绑定值则分别对应`value-filter`、`value-table`、`value-form`,由于是多个双向绑定的值,所以使用`sync`修饰符来做双向绑定。 | ||
| 8 | + | ||
| 9 | +::: snippet 基础示例 | ||
| 10 | + | ||
| 11 | +```html | ||
| 12 | +<template> | ||
| 13 | + <z-schema-page :schema="schema" :value-filter.sync="filterModel" :value-table.sync="tableModel" :value-form.sync="formModel"></z-schema-page> | ||
| 14 | +</template> | ||
| 15 | + | ||
| 16 | +<script> | ||
| 17 | +export default { | ||
| 18 | + data() { | ||
| 19 | + return { | ||
| 20 | + schema: { | ||
| 21 | + filter: { | ||
| 22 | + items: [ | ||
| 23 | + { is: 'el-input', prop: 'name', label: '姓名' }, | ||
| 24 | + ] | ||
| 25 | + }, | ||
| 26 | + table: { | ||
| 27 | + props: { size: 'mini', border: true } , | ||
| 28 | + items: [ | ||
| 29 | + { prop: 'name', label: '姓名' }, | ||
| 30 | + { prop: 'age', label: '年龄' }, | ||
| 31 | + { prop: 'address', label: '地址' }, | ||
| 32 | + { prop: 'status', label: '状态' }, | ||
| 33 | + ] | ||
| 34 | + }, | ||
| 35 | + form: { | ||
| 36 | + props: { labelWidth: '70px', size: 'small', span: 12 }, | ||
| 37 | + items: [ | ||
| 38 | + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] }, | ||
| 39 | + { is: 'el-input-number', prop: 'age', label: '年龄', rules: [{ required: true, message: '请输入有效年龄' }] }, | ||
| 40 | + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 }, | ||
| 41 | + ] | ||
| 42 | + }, | ||
| 43 | + }, | ||
| 44 | + filterModel: { name: '', age: '', gender: '', address: '' }, | ||
| 45 | + formModel: { name: '', age: '', address: '' }, | ||
| 46 | + tableModel: [ | ||
| 47 | + { id: '0', name: '李饼', age: 32, address: '地址0', status: '正常' }, | ||
| 48 | + { id: '1', name: '陈拾', age: 20, address: '地址1', status: '正常' }, | ||
| 49 | + { id: '3', name: '王七', age: 26, address: '地址3', status: '正常' }, | ||
| 50 | + { id: '4', name: '崔倍', age: 27, address: '地址4', status: '正常' }, | ||
| 51 | + { id: '5', name: '孙豹', age: 38, address: '地址5', status: '正常' }, | ||
| 52 | + ], | ||
| 53 | + } | ||
| 54 | + }, | ||
| 55 | +} | ||
| 56 | +</script> | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +::: | ||
| 60 | + | ||
| 61 | +## 详情入口 | ||
| 62 | + | ||
| 63 | +可以通过自定义插槽的方式,在单元格内指定详情入口。由于三个子组件均支持自定义插槽,所以在本组件中使用自定义插槽时需要加上子组件**前缀**。 | ||
| 64 | + | ||
| 65 | +::: snippet 配置表格中的详情入口可以使用`table-cell-`插槽。 | ||
| 66 | + | ||
| 67 | +```html | ||
| 68 | +<template> | ||
| 69 | + <z-schema-page :schema="schema" :value-filter.sync="filterModel" :value-table.sync="tableModel" :value-form.sync="formModel"> | ||
| 70 | + <template #table-cell-name="{ value, row, openDetail }"> | ||
| 71 | + <el-link type="primary" @click="openDetail(row)">{{ value }}</el-link> | ||
| 72 | + </template> | ||
| 73 | + </z-schema-page> | ||
| 74 | +</template> | ||
| 75 | + | ||
| 76 | +<script> | ||
| 77 | +export default { | ||
| 78 | + data() { | ||
| 79 | + return { | ||
| 80 | + schema: { | ||
| 81 | + filter: { | ||
| 82 | + items: [ | ||
| 83 | + { is: 'el-input', prop: 'name', label: '姓名' }, | ||
| 84 | + ] | ||
| 85 | + }, | ||
| 86 | + table: { | ||
| 87 | + props: { size: 'mini', border: true } , | ||
| 88 | + items: [ | ||
| 89 | + { prop: 'name', label: '姓名' }, | ||
| 90 | + { prop: 'age', label: '年龄' }, | ||
| 91 | + { prop: 'address', label: '地址' }, | ||
| 92 | + { prop: 'status', label: '状态' }, | ||
| 93 | + ] | ||
| 94 | + }, | ||
| 95 | + form: { | ||
| 96 | + props: { labelWidth: '70px', size: 'small', span: 12 }, | ||
| 97 | + items: [ | ||
| 98 | + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] }, | ||
| 99 | + { is: 'el-input-number', prop: 'age', label: '年龄', rules: [{ required: true, message: '请输入有效年龄' }] }, | ||
| 100 | + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 }, | ||
| 101 | + ] | ||
| 102 | + }, | ||
| 103 | + }, | ||
| 104 | + filterModel: { name: '', age: '', gender: '', address: '' }, | ||
| 105 | + formModel: { name: '', age: '', address: '' }, | ||
| 106 | + tableModel: [ | ||
| 107 | + { id: '0', name: '李饼', age: 32, address: '地址0', status: '正常' }, | ||
| 108 | + { id: '1', name: '陈拾', age: 20, address: '地址1', status: '正常' }, | ||
| 109 | + { id: '3', name: '王七', age: 26, address: '地址3', status: '正常' }, | ||
| 110 | + { id: '4', name: '崔倍', age: 27, address: '地址4', status: '正常' }, | ||
| 111 | + { id: '5', name: '孙豹', age: 38, address: '地址5', status: '正常' }, | ||
| 112 | + ], | ||
| 113 | + } | ||
| 114 | + }, | ||
| 115 | +} | ||
| 116 | +</script> | ||
| 117 | +``` | ||
| 118 | + | ||
| 119 | +::: | ||
| 120 | + | ||
| 121 | +## 对接接口 | ||
| 122 | + | ||
| 123 | +本组件预置了增删改查逻辑,因此分别对应`api-search`、`api-new`、`api-edit`、`api-get`、`api-delete`五个基本接口。 | ||
| 124 | + | ||
| 125 | +::: snippet 接口格式为返回一个**Promise**对象的**Function**。其中,`api-search`的执行结果必须是{ list: [...], total: n }的格式,`api-get`的执行结果必须与`valur-form`相对应,`api-new`、`api-edit`、`api-delete`保持默认的**Promise**的resolve或reject逻辑即可,详情见示例。 | ||
| 126 | + | ||
| 127 | +```html | ||
| 128 | +<template> | ||
| 129 | + <z-schema-page | ||
| 130 | + :schema="schema" | ||
| 131 | + :value-filter.sync="filterModel" | ||
| 132 | + :value-form.sync="formModel" | ||
| 133 | + :api-search="searchAPI" | ||
| 134 | + :api-get="getAPI" | ||
| 135 | + :api-delete="deleteAPI" | ||
| 136 | + :api-new="newAPI" | ||
| 137 | + :api-edit="editAPI" | ||
| 138 | + > | ||
| 139 | + <template #table-cell-name="{ value, row, openDetail }"> | ||
| 140 | + <el-link type="primary" @click="openDetail(row)">{{ value }}</el-link> | ||
| 141 | + </template> | ||
| 142 | + </z-schema-page> | ||
| 143 | +</template> | ||
| 144 | + | ||
| 145 | +<script> | ||
| 146 | +export default { | ||
| 147 | + data() { | ||
| 148 | + return { | ||
| 149 | + schema: { | ||
| 150 | + auto: true, | ||
| 151 | + filter: { | ||
| 152 | + items: [ | ||
| 153 | + { is: 'el-input', prop: 'name', label: '姓名' }, | ||
| 154 | + ] | ||
| 155 | + }, | ||
| 156 | + table: { | ||
| 157 | + props: { size: 'mini', border: true } , | ||
| 158 | + items: [ | ||
| 159 | + { prop: 'name', label: '姓名' }, | ||
| 160 | + { prop: 'age', label: '年龄' }, | ||
| 161 | + { prop: 'address', label: '地址' }, | ||
| 162 | + { prop: 'status', label: '状态' }, | ||
| 163 | + ] | ||
| 164 | + }, | ||
| 165 | + form: { | ||
| 166 | + props: { labelWidth: '70px', size: 'small', span: 12 }, | ||
| 167 | + items: [ | ||
| 168 | + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] }, | ||
| 169 | + { is: 'el-input-number', prop: 'age', label: '年龄', rules: [{ required: true, message: '请输入有效年龄' }] }, | ||
| 170 | + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 }, | ||
| 171 | + ] | ||
| 172 | + }, | ||
| 173 | + }, | ||
| 174 | + filterModel: { name: '', age: '', gender: '', address: '' }, | ||
| 175 | + formModel: { name: '', age: '', address: '' }, | ||
| 176 | + } | ||
| 177 | + }, | ||
| 178 | + methods: { | ||
| 179 | + searchAPI(params) { | ||
| 180 | + console.log('search', params); | ||
| 181 | + return new Promise(resolve => { | ||
| 182 | + setTimeout(() => { | ||
| 183 | + const list = [ | ||
| 184 | + { id: '0', name: '李饼', age: 32, address: '地址0', status: '正常' }, | ||
| 185 | + { id: '1', name: '陈拾', age: 20, address: '地址1', status: '正常' }, | ||
| 186 | + { id: '3', name: '王七', age: 26, address: '地址3', status: '正常' }, | ||
| 187 | + { id: '4', name: '崔倍', age: 27, address: '地址4', status: '正常' }, | ||
| 188 | + { id: '5', name: '孙豹', age: 38, address: '地址5', status: '正常' }, | ||
| 189 | + ] | ||
| 190 | + resolve({ | ||
| 191 | + list, | ||
| 192 | + total: list.length | ||
| 193 | + }); | ||
| 194 | + }, 500); | ||
| 195 | + }); | ||
| 196 | + }, | ||
| 197 | + getAPI(row) { | ||
| 198 | + console.log('get', row); | ||
| 199 | + return new Promise((resolve, reject) => { | ||
| 200 | + setTimeout(() => { | ||
| 201 | + resolve({ ...row, name: `[${row.name}]` }); | ||
| 202 | + }, 1500); | ||
| 203 | + }); | ||
| 204 | + }, | ||
| 205 | + newAPI(model) { | ||
| 206 | + console.log('new', model); | ||
| 207 | + return new Promise((resolve, reject) => { | ||
| 208 | + setTimeout(() => { | ||
| 209 | + resolve(); | ||
| 210 | + }, 1500); | ||
| 211 | + }); | ||
| 212 | + }, | ||
| 213 | + editAPI(model) { | ||
| 214 | + console.log('edit', model); | ||
| 215 | + return new Promise((resolve, reject) => { | ||
| 216 | + setTimeout(() => { | ||
| 217 | + reject(); | ||
| 218 | + }, 1500); | ||
| 219 | + }); | ||
| 220 | + }, | ||
| 221 | + deleteAPI(selection) { | ||
| 222 | + console.log('delete', selection); | ||
| 223 | + return new Promise((resolve, reject) => { | ||
| 224 | + setTimeout(() => { | ||
| 225 | + resolve(); | ||
| 226 | + }, 1500); | ||
| 227 | + }); | ||
| 228 | + } | ||
| 229 | + } | ||
| 230 | +} | ||
| 231 | +</script> | ||
| 232 | +``` | ||
| 233 | + | ||
| 234 | +::: | ||
| 235 | + | ||
| 236 | +## 详情接口 | ||
| 237 | + | ||
| 238 | +由于可能出现详情与表单不同的情况,因此本组件提供了`value-detail`属性和`api-detail`接口可独立维护详情页面,若详情表单项也不同,则可以在`schema`中配置`detail`来设置。 | ||
| 239 | + | ||
| 240 | +::: snippet 若没有详情绑定值属性和接口,则默认以表单的值为准。 | ||
| 241 | + | ||
| 242 | +```html | ||
| 243 | +<template> | ||
| 244 | + <z-schema-page | ||
| 245 | + :schema="schema" | ||
| 246 | + :value-filter.sync="filterModel" | ||
| 247 | + :value-form.sync="formModel" | ||
| 248 | + :value-detail.sync="detailModel" | ||
| 249 | + :api-search="searchAPI" | ||
| 250 | + :api-get="getAPI" | ||
| 251 | + :api-detail="detailAPI" | ||
| 252 | + :api-delete="deleteAPI" | ||
| 253 | + :api-new="newAPI" | ||
| 254 | + :api-edit="editAPI" | ||
| 255 | + > | ||
| 256 | + <template #table-cell-name="{ value, row, openDetail }"> | ||
| 257 | + <el-link type="primary" @click="openDetail(row)">{{ value }}</el-link> | ||
| 258 | + </template> | ||
| 259 | + <template #detail-packages="{ value }"> | ||
| 260 | + <el-tag v-for="item in value" size="mini" type="info" style="margin-right: 10px">{{ item }}</el-tag> | ||
| 261 | + </template> | ||
| 262 | + </z-schema-page> | ||
| 263 | +</template> | ||
| 264 | + | ||
| 265 | +<script> | ||
| 266 | +export default { | ||
| 267 | + data() { | ||
| 268 | + return { | ||
| 269 | + schema: { | ||
| 270 | + auto: true, | ||
| 271 | + filter: { | ||
| 272 | + items: [ | ||
| 273 | + { is: 'el-input', prop: 'name', label: '姓名' }, | ||
| 274 | + ] | ||
| 275 | + }, | ||
| 276 | + table: { | ||
| 277 | + props: { size: 'mini', border: true } , | ||
| 278 | + items: [ | ||
| 279 | + { prop: 'name', label: '姓名' }, | ||
| 280 | + { prop: 'age', label: '年龄' }, | ||
| 281 | + { prop: 'address', label: '地址' }, | ||
| 282 | + { prop: 'status', label: '状态' }, | ||
| 283 | + ] | ||
| 284 | + }, | ||
| 285 | + form: { | ||
| 286 | + props: { labelWidth: '70px', size: 'small', span: 12 }, | ||
| 287 | + items: [ | ||
| 288 | + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] }, | ||
| 289 | + { is: 'el-input-number', prop: 'age', label: '年龄', rules: [{ required: true, message: '请输入有效年龄' }] }, | ||
| 290 | + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 }, | ||
| 291 | + ] | ||
| 292 | + }, | ||
| 293 | + detail: { | ||
| 294 | + props: { labelWidth: '70px', size: 'small', span: 12 }, | ||
| 295 | + items: [ | ||
| 296 | + { prop: 'name', label: '姓名' }, | ||
| 297 | + { prop: 'age', label: '年龄' }, | ||
| 298 | + { props: { type: 'textarea' }, prop: 'address', label: '住址' }, | ||
| 299 | + { prop: 'packages', label: '包裹', span: 24 } | ||
| 300 | + ] | ||
| 301 | + } | ||
| 302 | + }, | ||
| 303 | + filterModel: { name: '', age: '', gender: '', address: '' }, | ||
| 304 | + formModel: { name: '', age: '', address: '' }, | ||
| 305 | + detailModel: { name: '', age: '', address: '', packages: [] }, | ||
| 306 | + } | ||
| 307 | + }, | ||
| 308 | + methods: { | ||
| 309 | + searchAPI(params) { | ||
| 310 | + console.log('search', params); | ||
| 311 | + return new Promise(resolve => { | ||
| 312 | + setTimeout(() => { | ||
| 313 | + const list = [ | ||
| 314 | + { id: '0', name: '李饼', age: 32, address: '地址0', status: '正常' }, | ||
| 315 | + { id: '1', name: '陈拾', age: 20, address: '地址1', status: '正常' }, | ||
| 316 | + { id: '3', name: '王七', age: 26, address: '地址3', status: '正常' }, | ||
| 317 | + { id: '4', name: '崔倍', age: 27, address: '地址4', status: '正常' }, | ||
| 318 | + { id: '5', name: '孙豹', age: 38, address: '地址5', status: '正常' }, | ||
| 319 | + ] | ||
| 320 | + resolve({ | ||
| 321 | + list, | ||
| 322 | + total: list.length | ||
| 323 | + }); | ||
| 324 | + }, 500); | ||
| 325 | + }); | ||
| 326 | + }, | ||
| 327 | + getAPI(row) { | ||
| 328 | + console.log('get', row); | ||
| 329 | + return new Promise((resolve, reject) => { | ||
| 330 | + setTimeout(() => { | ||
| 331 | + resolve({ ...row, name: `[${row.name}]` }); | ||
| 332 | + }, 1500); | ||
| 333 | + }); | ||
| 334 | + }, | ||
| 335 | + detailAPI(row) { | ||
| 336 | + console.log('detail', row); | ||
| 337 | + return new Promise((resolve, reject) => { | ||
| 338 | + setTimeout(() => { | ||
| 339 | + resolve({ ...row, packages: ['手机', '电脑'], address: '上海市青浦区华新镇纪鹤公路1988号' }); | ||
| 340 | + }, 1500); | ||
| 341 | + }); | ||
| 342 | + }, | ||
| 343 | + newAPI(model) { | ||
| 344 | + console.log('new', model); | ||
| 345 | + return new Promise((resolve, reject) => { | ||
| 346 | + setTimeout(() => { | ||
| 347 | + resolve(); | ||
| 348 | + }, 1500); | ||
| 349 | + }); | ||
| 350 | + }, | ||
| 351 | + editAPI(model) { | ||
| 352 | + console.log('edit', model); | ||
| 353 | + return new Promise((resolve, reject) => { | ||
| 354 | + setTimeout(() => { | ||
| 355 | + reject(); | ||
| 356 | + }, 1500); | ||
| 357 | + }); | ||
| 358 | + }, | ||
| 359 | + deleteAPI(selection) { | ||
| 360 | + console.log('delete', selection); | ||
| 361 | + return new Promise((resolve, reject) => { | ||
| 362 | + setTimeout(() => { | ||
| 363 | + resolve(); | ||
| 364 | + }, 1500); | ||
| 365 | + }); | ||
| 366 | + } | ||
| 367 | + } | ||
| 368 | +} | ||
| 369 | +</script> | ||
| 370 | +``` | ||
| 371 | + | ||
| 372 | +::: | ||
| 373 | + | ||
| 374 | +## 弹窗类型 | ||
| 375 | + | ||
| 376 | +除了本组件内置的`new`、`edit`、`detail`三种弹出框模式之外,还可以通过任意插槽打开任意自定义弹出框。也支持重新定义原有的三种弹框,同时也需要重新自定义表单校验和提交等逻辑。 | ||
| 377 | + | ||
| 378 | +::: snippet 插槽提供`openDialog`打开弹出框方法,参数类型为(type: 弹出框类型, title: 弹出框标题, config: 弹出框配置),弹出框主体通过`dialog-`插槽定义。`closeDialog`为关闭弹出框。 | ||
| 379 | + | ||
| 380 | +```html | ||
| 381 | +<template> | ||
| 382 | + <z-schema-page :schema="schema" :value-table.sync="tableModel"> | ||
| 383 | + <template #action-button="{ size, openDialog }"> | ||
| 384 | + <el-button :size="size" @click="openDialog('bill', '账单', { width: '70%' })">账单</el-button> | ||
| 385 | + </template> | ||
| 386 | + <template #operation-button="{ openDialog, row }"> | ||
| 387 | + <el-button type="text" @click="openDialog('load', `配置-${row.name}`, { width: '400px' })">配载</el-button> | ||
| 388 | + </template> | ||
| 389 | + <template #table-cell-name="{ value, row, openDetail }"> | ||
| 390 | + <el-link type="primary" @click="openDetail(row)">{{ value }}</el-link> | ||
| 391 | + </template> | ||
| 392 | + <template #dialog-bill="{ closeDialog }"> | ||
| 393 | + <z-schema-table v-model="billData" :schema="billSchema"></z-schema-table> | ||
| 394 | + <div style="text-align: center; margin-top: 20px"> | ||
| 395 | + <el-button plain @click="closeDialog">关闭</el-button> | ||
| 396 | + </div> | ||
| 397 | + </template> | ||
| 398 | + <template #dialog-load="{ closeDialog }"> | ||
| 399 | + <el-alert title="这里是一段自定义信息" type="error" show-icon style="margin-bottom: 20px"></el-alert> | ||
| 400 | + <z-schema-form v-model="loadModel" :schema="loadSchema"></z-schema-form> | ||
| 401 | + <el-button type="primary" @click="closeDialog">关闭弹出框</el-button> | ||
| 402 | + </template> | ||
| 403 | + <template #dialog-new="{ closeDialog }"> | ||
| 404 | + <el-alert title="这里是自定义新增弹窗" type="success" show-icon style="margin-bottom: 20px"></el-alert> | ||
| 405 | + <el-button plain @click="closeDialog">关闭</el-button> | ||
| 406 | + </template> | ||
| 407 | + <template #dialog-edit="{ closeDialog }"> | ||
| 408 | + <el-alert title="这里是自定义编辑弹窗" type="warning" show-icon style="margin-bottom: 20px"></el-alert> | ||
| 409 | + <el-button plain @click="closeDialog">关闭</el-button> | ||
| 410 | + </template> | ||
| 411 | + <template #dialog-detail="{ closeDialog }"> | ||
| 412 | + <el-alert title="这里是自定义详情弹窗" type="info" show-icon style="margin-bottom: 20px"></el-alert> | ||
| 413 | + <el-button plain @click="closeDialog">关闭</el-button> | ||
| 414 | + </template> | ||
| 415 | + </z-schema-page> | ||
| 416 | +</template> | ||
| 417 | + | ||
| 418 | +<script> | ||
| 419 | +export default { | ||
| 420 | + data() { | ||
| 421 | + return { | ||
| 422 | + schema: { | ||
| 423 | + filter: { | ||
| 424 | + items: [ | ||
| 425 | + { is: 'el-input', prop: 'name', label: '姓名' }, | ||
| 426 | + ] | ||
| 427 | + }, | ||
| 428 | + table: { | ||
| 429 | + props: { size: 'mini', border: true } , | ||
| 430 | + items: [ | ||
| 431 | + { prop: 'name', label: '姓名' }, | ||
| 432 | + { prop: 'age', label: '年龄' }, | ||
| 433 | + { prop: 'address', label: '地址' }, | ||
| 434 | + { prop: 'status', label: '状态' }, | ||
| 435 | + ] | ||
| 436 | + }, | ||
| 437 | + form: { | ||
| 438 | + props: { labelWidth: '70px', size: 'small', span: 12 }, | ||
| 439 | + items: [ | ||
| 440 | + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] }, | ||
| 441 | + { is: 'el-input-number', prop: 'age', label: '年龄', rules: [{ required: true, message: '请输入有效年龄' }] }, | ||
| 442 | + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 }, | ||
| 443 | + ] | ||
| 444 | + }, | ||
| 445 | + operation: { width: 120 } | ||
| 446 | + }, | ||
| 447 | + tableModel: [ | ||
| 448 | + { id: '0', name: '李饼', age: 32, address: '地址0', status: '正常' }, | ||
| 449 | + { id: '1', name: '陈拾', age: 20, address: '地址1', status: '正常' }, | ||
| 450 | + { id: '3', name: '王七', age: 26, address: '地址3', status: '正常' }, | ||
| 451 | + { id: '4', name: '崔倍', age: 27, address: '地址4', status: '正常' }, | ||
| 452 | + { id: '5', name: '孙豹', age: 38, address: '地址5', status: '正常' }, | ||
| 453 | + ], | ||
| 454 | + loadModel: { | ||
| 455 | + name: '', | ||
| 456 | + count: 1, | ||
| 457 | + packages: [] | ||
| 458 | + }, | ||
| 459 | + loadSchema: { | ||
| 460 | + props: { 'label-width': '70px', size: 'small' }, | ||
| 461 | + items: [ | ||
| 462 | + { is: 'el-input', prop: 'name', label: '名称' }, | ||
| 463 | + { is: 'el-input-number', prop: 'count', label: '数量' }, | ||
| 464 | + { | ||
| 465 | + is: 'el-checkbox-group', | ||
| 466 | + prop: 'packages', | ||
| 467 | + label: '包裹', | ||
| 468 | + children: [ | ||
| 469 | + { is: 'el-checkbox', props: { label: '零食' } }, | ||
| 470 | + { is: 'el-checkbox', props: { label: '手机' } }, | ||
| 471 | + { is: 'el-checkbox', props: { label: '电脑' } }, | ||
| 472 | + ], | ||
| 473 | + }, | ||
| 474 | + ] | ||
| 475 | + }, | ||
| 476 | + billData: [ | ||
| 477 | + { billno: 'B20210401000001', amount: '18750' }, | ||
| 478 | + { billno: 'B20210401000002', amount: '637' }, | ||
| 479 | + ], | ||
| 480 | + billSchema: { | ||
| 481 | + items: [ | ||
| 482 | + { label: '单号', prop: 'billno' }, | ||
| 483 | + { label: '金额', prop: 'amount' }, | ||
| 484 | + ] | ||
| 485 | + } | ||
| 486 | + } | ||
| 487 | + }, | ||
| 488 | +} | ||
| 489 | +</script> | ||
| 490 | +``` | ||
| 491 | + | ||
| 492 | +::: | ||
| 493 | + | ||
| 494 | +## 按钮权限 | ||
| 495 | + | ||
| 496 | +本组件不包含自定义业务逻辑,因此配置项不包含权限判断,如果需要按钮的权限判断,可以通过`action`插槽和`operation`插槽将渲染逻辑暴露在视图模板中,然后进行自定义判断。 | ||
| 497 | + | ||
| 498 | +::: snippet 本示例项目中没有`v-permission`等自定义权限指令,使用时根据实际情况在对应的按钮加上判断即可 | ||
| 499 | + | ||
| 500 | +```html | ||
| 501 | +<template> | ||
| 502 | + <z-schema-page :schema="schema" :value-filter.sync="filterModel" :value-table.sync="tableModel" :value-form.sync="formModel"> | ||
| 503 | + <template #action="{ size, selection, openNew, onDeleteMultiple }"> | ||
| 504 | + <el-button :size="size" type="primary" @click="openNew">新增</el-button> | ||
| 505 | + <el-button :size="size" plain :disabled="selection.length === 0" @click="onDeleteMultiple(selection)">删除</el-button> | ||
| 506 | + </template> | ||
| 507 | + <template #operation="{ openEdit, onDelete }"> | ||
| 508 | + <el-table-column label="操作" width="90" align="center"> | ||
| 509 | + <template #default="{ row, column, $index }"> | ||
| 510 | + <div class="z-schema-page__table-operation"> | ||
| 511 | + <el-button type="text" icon="el-icon-edit" title="编辑" @click="openEdit(row)"></el-button> | ||
| 512 | + <el-popconfirm confirm-button-text="确定" cancel-button-text="取消" title="确定删除吗?" placement="top" @confirm="onDelete([row])"> | ||
| 513 | + <el-button slot="reference" type="text" icon="el-icon-delete" title="删除"></el-button> | ||
| 514 | + </el-popconfirm> | ||
| 515 | + </div> | ||
| 516 | + </template> | ||
| 517 | + </el-table-column> | ||
| 518 | + </template> | ||
| 519 | + </z-schema-page> | ||
| 520 | +</template> | ||
| 521 | + | ||
| 522 | +<script> | ||
| 523 | +export default { | ||
| 524 | + data() { | ||
| 525 | + return { | ||
| 526 | + schema: { | ||
| 527 | + filter: { | ||
| 528 | + items: [ | ||
| 529 | + { is: 'el-input', prop: 'name', label: '姓名' }, | ||
| 530 | + ] | ||
| 531 | + }, | ||
| 532 | + table: { | ||
| 533 | + props: { size: 'mini', border: true } , | ||
| 534 | + items: [ | ||
| 535 | + { prop: 'name', label: '姓名' }, | ||
| 536 | + { prop: 'age', label: '年龄' }, | ||
| 537 | + { prop: 'address', label: '地址' }, | ||
| 538 | + { prop: 'status', label: '状态' }, | ||
| 539 | + ] | ||
| 540 | + }, | ||
| 541 | + form: { | ||
| 542 | + props: { labelWidth: '70px', size: 'small', span: 12 }, | ||
| 543 | + items: [ | ||
| 544 | + { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] }, | ||
| 545 | + { is: 'el-input-number', prop: 'age', label: '年龄', rules: [{ required: true, message: '请输入有效年龄' }] }, | ||
| 546 | + { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 }, | ||
| 547 | + ] | ||
| 548 | + }, | ||
| 549 | + }, | ||
| 550 | + filterModel: { name: '', age: '', gender: '', address: '' }, | ||
| 551 | + formModel: { name: '', age: '', address: '' }, | ||
| 552 | + tableModel: [ | ||
| 553 | + { id: '0', name: '李饼', age: 32, address: '地址0', status: '正常' }, | ||
| 554 | + { id: '1', name: '陈拾', age: 20, address: '地址1', status: '正常' }, | ||
| 555 | + { id: '3', name: '王七', age: 26, address: '地址3', status: '正常' }, | ||
| 556 | + { id: '4', name: '崔倍', age: 27, address: '地址4', status: '正常' }, | ||
| 557 | + { id: '5', name: '孙豹', age: 38, address: '地址5', status: '正常' }, | ||
| 558 | + ], | ||
| 559 | + } | ||
| 560 | + }, | ||
| 561 | +} | ||
| 562 | +</script> | ||
| 563 | +``` | ||
| 564 | + | ||
| 565 | +::: | ||
| 566 | + | ||
| 567 | +## API | ||
| 568 | + | ||
| 569 | +## Attribute 属性 | ||
| 570 | + | ||
| 571 | +参数|说明|类型|可选值|默认值 | ||
| 572 | +-|-|-|-|- | ||
| 573 | +schema | JSON Schema配置项列表 | Array | - | [] |
examples/views/docs/component/schema.md
| @@ -1,155 +0,0 @@ | @@ -1,155 +0,0 @@ | ||
| 1 | -# Schema 方案 | ||
| 2 | - | ||
| 3 | -根据JSON Schema配置自动生成一个包含搜索条件、表格、编辑表单、详情表单的页面 | ||
| 4 | - | ||
| 5 | -## 基础用法 | ||
| 6 | - | ||
| 7 | -通过配置`JSON Schema`快速生成CURD逻辑 | ||
| 8 | - | ||
| 9 | -::: snippet 通过`list`配置项目 | ||
| 10 | - | ||
| 11 | -```html | ||
| 12 | -<template> | ||
| 13 | - <z-schema :list="list" :search-api="searchAPI" :get-api="getAPI" :submit-api="submitAPI" :delete-api="deleteAPI" auto real-selection> | ||
| 14 | - <el-table-column type="selection" align="center" width="40"></el-table-column> | ||
| 15 | - <template #header> | ||
| 16 | - <el-tabs v-model="activeName"> | ||
| 17 | - <el-tab-pane label="待执行" name="wait"></el-tab-pane> | ||
| 18 | - <el-tab-pane label="已执行" name="done"></el-tab-pane> | ||
| 19 | - </el-tabs> | ||
| 20 | - </template> | ||
| 21 | - <template #button="{ size, openDialog }"> | ||
| 22 | - <el-button :size="size" plain>删除</el-button> | ||
| 23 | - <el-button :size="size" plain @click="openDialog('other', '追加')">追加操作</el-button> | ||
| 24 | - </template> | ||
| 25 | - <template #dialog-other="{ model, closeDialog }"> | ||
| 26 | - <el-tag type="success">其它页面</el-tag> | ||
| 27 | - <el-input | ||
| 28 | - type="textarea" | ||
| 29 | - :rows="2" | ||
| 30 | - placeholder="请输入内容" | ||
| 31 | - v-model="model.name"> | ||
| 32 | - </el-input> | ||
| 33 | - <el-button @click="closeDialog">关闭弹出框</el-button> | ||
| 34 | - </template> | ||
| 35 | - </z-schema> | ||
| 36 | -</template> | ||
| 37 | - | ||
| 38 | -<script> | ||
| 39 | -export default { | ||
| 40 | - data() { | ||
| 41 | - return { | ||
| 42 | - activeName: 'wait', | ||
| 43 | - list: [ | ||
| 44 | - { type: 'el-input', label: '姓名', key: 'name', rules: [{ required: true, message: '请输入', trigger: 'change' }] }, | ||
| 45 | - { type: 'el-input', label: '年龄', key: 'age' }, | ||
| 46 | - ] | ||
| 47 | - } | ||
| 48 | - }, | ||
| 49 | - methods: { | ||
| 50 | - searchAPI(params) { | ||
| 51 | - console.log(params); | ||
| 52 | - return new Promise(resolve => { | ||
| 53 | - setTimeout(() => { | ||
| 54 | - const list = [ | ||
| 55 | - { id: '0', name: '李饼', age: 32, location: { lat: 0, lng: 0 } }, | ||
| 56 | - { id: '1', name: '陈拾', age: 20 }, | ||
| 57 | - { id: '2', name: '阿里巴巴', age: 24 }, | ||
| 58 | - { id: '3', name: '王七', age: 26 }, | ||
| 59 | - { id: '4', name: '崔倍', age: 27 }, | ||
| 60 | - { id: '5', name: '孙豹', age: 38 }, | ||
| 61 | - { id: '6', name: '庞柏', age: 42 }, | ||
| 62 | - { id: '7', name: '蔡疏', age: 60 }, | ||
| 63 | - { id: '8', name: '卢纳', age: 55 }, | ||
| 64 | - ] | ||
| 65 | - resolve({ | ||
| 66 | - result: list, | ||
| 67 | - totalCount: list.length | ||
| 68 | - }); | ||
| 69 | - }, 1500); | ||
| 70 | - }); | ||
| 71 | - }, | ||
| 72 | - getAPI(row) { | ||
| 73 | - return new Promise((resolve, reject) => { | ||
| 74 | - setTimeout(() => { | ||
| 75 | - resolve({ ...row, name: `[${row.name}]` }); | ||
| 76 | - }, 1500); | ||
| 77 | - }); | ||
| 78 | - }, | ||
| 79 | - submitAPI(model, config) { | ||
| 80 | - console.log(JSON.parse(JSON.stringify(model))); | ||
| 81 | - console.log(config); | ||
| 82 | - console.log('start'); | ||
| 83 | - return new Promise((resolve, reject) => { | ||
| 84 | - setTimeout(() => { | ||
| 85 | - console.log('done'); | ||
| 86 | - reject(); | ||
| 87 | - }, 1500); | ||
| 88 | - }); | ||
| 89 | - }, | ||
| 90 | - deleteAPI() { | ||
| 91 | - return new Promise((resolve, reject) => { | ||
| 92 | - setTimeout(() => { | ||
| 93 | - resolve(); | ||
| 94 | - }, 1500); | ||
| 95 | - }); | ||
| 96 | - } | ||
| 97 | - } | ||
| 98 | -} | ||
| 99 | -</script> | ||
| 100 | -``` | ||
| 101 | - | ||
| 102 | -::: | ||
| 103 | - | ||
| 104 | -## 内置接口逻辑 | ||
| 105 | - | ||
| 106 | -如果CURD的接口都是同一路径下,可以使用内置接口逻辑快速对接 | ||
| 107 | - | ||
| 108 | -::: snippet 通过`url`配置接口路径,`http`设置Promise形式的HTTP请求库 | ||
| 109 | - | ||
| 110 | -```html | ||
| 111 | -<template> | ||
| 112 | - <z-schema ref="schema" :list="list" url="/customer" :http="$http" :alias="{ getUrl: '/getCustomerByCode', getKey: 'code' }" auto real-selection> | ||
| 113 | - <el-table-column type="selection" align="center" width="40"></el-table-column> | ||
| 114 | - <template #render-code="{ value, row, openView }"> | ||
| 115 | - <el-link type="primary" @click="openView(row)">{{ value }}</el-link> | ||
| 116 | - </template> | ||
| 117 | - <template #view-name="{ value }"> | ||
| 118 | - <el-tag size="mini">{{ value }}</el-tag> | ||
| 119 | - </template> | ||
| 120 | - <template #cell-name="{ value }"> | ||
| 121 | - <el-tag size="mini" type="danger">{{ value }}</el-tag> | ||
| 122 | - </template> | ||
| 123 | - <template #form-id="{ value }"> | ||
| 124 | - <el-tag size="mini">{{ value }}</el-tag> | ||
| 125 | - </template> | ||
| 126 | - </z-schema> | ||
| 127 | -</template> | ||
| 128 | - | ||
| 129 | -<script> | ||
| 130 | -export default { | ||
| 131 | - data() { | ||
| 132 | - return { | ||
| 133 | - activeName: 'wait', | ||
| 134 | - list: [ | ||
| 135 | - { type: 'el-input', label: 'ID', key: 'id', props: { disabled: true }, include: 'form', visible: () => this.$refs.schema.dialogType === 'edit' }, | ||
| 136 | - { type: 'el-input', label: '编号', key: 'code' }, | ||
| 137 | - { type: 'el-input', label: '名称', key: 'name' }, | ||
| 138 | - ] | ||
| 139 | - } | ||
| 140 | - }, | ||
| 141 | - methods: { | ||
| 142 | - } | ||
| 143 | -} | ||
| 144 | -</script> | ||
| 145 | -``` | ||
| 146 | - | ||
| 147 | -::: | ||
| 148 | - | ||
| 149 | -## API | ||
| 150 | - | ||
| 151 | -## Attribute 属性 | ||
| 152 | - | ||
| 153 | -参数|说明|类型|可选值|默认值 | ||
| 154 | --|-|-|-|- | ||
| 155 | -list | JSON Schema配置项列表 | Array | - | [] |
No preview for this file type
examples/views/docs/develop/schema/schema-form.md
| @@ -1,75 +0,0 @@ | @@ -1,75 +0,0 @@ | ||
| 1 | -# Schema Form 方案表单 | ||
| 2 | - | ||
| 3 | -通过配置JSON Schema的方式快速生成一个表单 | ||
| 4 | - | ||
| 5 | -## 基础用法 | ||
| 6 | - | ||
| 7 | -::: snippet 预置了许多业务逻辑,避免重复维护相同的业务逻辑 | ||
| 8 | - | ||
| 9 | -```html | ||
| 10 | -<template> | ||
| 11 | - <div> | ||
| 12 | - <div>{{ form }}</div> | ||
| 13 | - <z-schema-form v-model="form" :schema="schema" @submit="onSubmit" @cancel="onCancel"> | ||
| 14 | - <template #error-name> | ||
| 15 | - <div><span>请输入</span><el-tag size="mini">格式正确</el-tag><span>的姓名</span></div> | ||
| 16 | - </template> | ||
| 17 | - <template #label-packages> | ||
| 18 | - <el-tooltip content="详情" placement="top" effect="light"> | ||
| 19 | - <i class="el-icon-question"></i> | ||
| 20 | - </el-tooltip> | ||
| 21 | - <span>包裹</span> | ||
| 22 | - </template> | ||
| 23 | - </z-schema-form> | ||
| 24 | - </div> | ||
| 25 | -</template> | ||
| 26 | - | ||
| 27 | -<script> | ||
| 28 | -export default { | ||
| 29 | - data() { | ||
| 30 | - return { | ||
| 31 | - form: { | ||
| 32 | - name: '张三', | ||
| 33 | - age: 27, | ||
| 34 | - address: '上海市青浦区华新镇纪鹤公路1988号', | ||
| 35 | - packages: ['零食'], | ||
| 36 | - info: { | ||
| 37 | - mobile: '18888888888', | ||
| 38 | - } | ||
| 39 | - }, | ||
| 40 | - schema: { | ||
| 41 | - props: { labelWidth: '70px', size: 'small', span: 12 }, | ||
| 42 | - items: [ | ||
| 43 | - { is: 'el-input', prop: 'name', label: '姓名', rules: [{ required: true, message: '请输入姓名' }] }, | ||
| 44 | - { is: 'el-input-number', prop: 'age', label: '年龄' }, | ||
| 45 | - { is: 'el-input', prop: 'info.mobile', label: '电话' }, | ||
| 46 | - { is: 'el-divider', props: { 'content-position': 'left' }, render: '收货信息', span: 24, labelWidth: '0px' }, | ||
| 47 | - { is: 'el-input', props: { type: 'textarea' }, prop: 'address', label: '住址', span: 24 }, | ||
| 48 | - { | ||
| 49 | - is: 'el-checkbox-group', | ||
| 50 | - prop: 'packages', | ||
| 51 | - label: '包裹', | ||
| 52 | - children: [ | ||
| 53 | - { is: 'el-checkbox', props: { label: '零食' } }, | ||
| 54 | - { is: 'el-checkbox', props: { label: '手机' } }, | ||
| 55 | - { is: 'el-checkbox', props: { label: '电脑' } }, | ||
| 56 | - ], | ||
| 57 | - span: 24 | ||
| 58 | - }, | ||
| 59 | - ] | ||
| 60 | - }, | ||
| 61 | - }; | ||
| 62 | - }, | ||
| 63 | - methods: { | ||
| 64 | - onSubmit(value) { | ||
| 65 | - console.log(value); | ||
| 66 | - }, | ||
| 67 | - onCancel() { | ||
| 68 | - console.log('cancal'); | ||
| 69 | - } | ||
| 70 | - } | ||
| 71 | -}; | ||
| 72 | -</script> | ||
| 73 | -``` | ||
| 74 | - | ||
| 75 | -::: | ||
| 76 | \ No newline at end of file | 0 | \ No newline at end of file |
examples/views/docs/develop/schema/schema-page.md
examples/views/docs/develop/schema/schema-table.md
| @@ -0,0 +1,36 @@ | @@ -0,0 +1,36 @@ | ||
| 1 | +<template> | ||
| 2 | + <el-container> | ||
| 3 | + <layout-header @menu-change="handleSelect"></layout-header> | ||
| 4 | + <el-container class="layout-container__component"> | ||
| 5 | + <el-aside class="layout-aside__component" width="300px"> | ||
| 6 | + <el-menu :default-active="activeMenu" class="layout-aside-menu__component" @select="handleSelect"> | ||
| 7 | + <h4 class="side-menu__group">设计指南</h4> | ||
| 8 | + <el-menu-item-group v-for="(design, idx) in designList" :key="idx"> | ||
| 9 | + <template slot="title">{{ design.group }}</template> | ||
| 10 | + <el-menu-item v-for="(data, index) in design.children" :key="index" :index="data.name">{{ data.meta.title }}</el-menu-item> | ||
| 11 | + </el-menu-item-group> | ||
| 12 | + </el-menu> | ||
| 13 | + </el-aside> | ||
| 14 | + <el-main class="layout-main__component"> | ||
| 15 | + <router-view></router-view> | ||
| 16 | + </el-main> | ||
| 17 | + <el-aside class="layout-aside__preview" width="200px"> | ||
| 18 | + <a class="anchor" :class="{ active: item.hash === currentAnchor }" v-for="(item, index) in anchorList" :key="index" @click="jumpAnchor(item)">{{ item.text }}</a> | ||
| 19 | + </el-aside> | ||
| 20 | + </el-container> | ||
| 21 | + </el-container> | ||
| 22 | +</template> | ||
| 23 | + | ||
| 24 | +<script> | ||
| 25 | +import ComponentLayout from './component'; | ||
| 26 | +import { designs } from '@/router/routes'; | ||
| 27 | + | ||
| 28 | +export default { | ||
| 29 | + extends: ComponentLayout, | ||
| 30 | + data() { | ||
| 31 | + return { | ||
| 32 | + designList: designs, | ||
| 33 | + }; | ||
| 34 | + }, | ||
| 35 | +}; | ||
| 36 | +</script> |
packages/form-item/index.vue
| @@ -4,7 +4,7 @@ export default { | @@ -4,7 +4,7 @@ export default { | ||
| 4 | props: { | 4 | props: { |
| 5 | label: String, | 5 | label: String, |
| 6 | labelWidth: String, | 6 | labelWidth: String, |
| 7 | - value: [Number, String], | 7 | + value: [Number, String, Array, Object], |
| 8 | prop: String, | 8 | prop: String, |
| 9 | span: { | 9 | span: { |
| 10 | type: [Number, String], | 10 | type: [Number, String], |
| @@ -0,0 +1,59 @@ | @@ -0,0 +1,59 @@ | ||
| 1 | +export default { | ||
| 2 | + data() { | ||
| 3 | + return { | ||
| 4 | + originData: {}, | ||
| 5 | + originProps: {}, | ||
| 6 | + }; | ||
| 7 | + }, | ||
| 8 | + created() { | ||
| 9 | + const { originData, originProps, ...other } = this._data; | ||
| 10 | + this.originData = this.cloneDeep(other); | ||
| 11 | + this.originProps = this.cloneDeep(this._props); | ||
| 12 | + }, | ||
| 13 | + methods: { | ||
| 14 | + // 深克隆对象 | ||
| 15 | + cloneDeep(obj) { | ||
| 16 | + if (typeof obj !== 'object') { | ||
| 17 | + return obj; | ||
| 18 | + } | ||
| 19 | + if (!obj) { | ||
| 20 | + return obj; | ||
| 21 | + } | ||
| 22 | + if (obj instanceof Date) { | ||
| 23 | + return new Date(obj); | ||
| 24 | + } | ||
| 25 | + if (obj instanceof RegExp) { | ||
| 26 | + return new RegExp(obj); | ||
| 27 | + } | ||
| 28 | + if (obj instanceof Function) { | ||
| 29 | + return obj; | ||
| 30 | + } | ||
| 31 | + let newObj; | ||
| 32 | + if (obj instanceof Array) { | ||
| 33 | + newObj = []; | ||
| 34 | + for (let i = 0, len = obj.length; i < len; i++) { | ||
| 35 | + newObj.push(this.cloneDeep(obj[i])); | ||
| 36 | + } | ||
| 37 | + return newObj; | ||
| 38 | + } | ||
| 39 | + newObj = {}; | ||
| 40 | + for (let key in obj) { | ||
| 41 | + if (Object.prototype.hasOwnProperty.call(obj, key)) { | ||
| 42 | + if (typeof obj[key] !== 'object') { | ||
| 43 | + newObj[key] = obj[key]; | ||
| 44 | + } else { | ||
| 45 | + newObj[key] = this.cloneDeep(obj[key]); | ||
| 46 | + } | ||
| 47 | + } | ||
| 48 | + } | ||
| 49 | + return newObj; | ||
| 50 | + }, | ||
| 51 | + // 获取初始值 | ||
| 52 | + getOriginData(key) { | ||
| 53 | + if (key) { | ||
| 54 | + return this.cloneDeep(this.originData)[key]; | ||
| 55 | + } | ||
| 56 | + return this.cloneDeep(this.originData); | ||
| 57 | + }, | ||
| 58 | + }, | ||
| 59 | +}; |
packages/schema-form/index.vue
| @@ -34,7 +34,8 @@ export default { | @@ -34,7 +34,8 @@ export default { | ||
| 34 | const item = props.item || {}; | 34 | const item = props.item || {}; |
| 35 | let content = []; | 35 | let content = []; |
| 36 | if (item.render && typeof item.render === 'function') { | 36 | if (item.render && typeof item.render === 'function') { |
| 37 | - content = context.props.render; | 37 | + console.log('render'); |
| 38 | + content = item.render(props.value, h); | ||
| 38 | } | 39 | } |
| 39 | if (item.children) { | 40 | if (item.children) { |
| 40 | if (Array.isArray(item.children)) { | 41 | if (Array.isArray(item.children)) { |
| @@ -0,0 +1,72 @@ | @@ -0,0 +1,72 @@ | ||
| 1 | +.z-schema { | ||
| 2 | + &__header { | ||
| 3 | + margin-bottom: 10px; | ||
| 4 | + } | ||
| 5 | + &__filter { | ||
| 6 | + border: 1px solid #ebeef5; | ||
| 7 | + padding-top: 10px; | ||
| 8 | + border-radius: 4px; | ||
| 9 | + margin-bottom: 10px; | ||
| 10 | + } | ||
| 11 | + &__action { | ||
| 12 | + display: flex; | ||
| 13 | + flex-wrap: wrap; | ||
| 14 | + align-items: center; | ||
| 15 | + justify-content: flex-start; | ||
| 16 | + line-height: 1; | ||
| 17 | + .el-button + .el-button { | ||
| 18 | + margin-left: 0; | ||
| 19 | + } | ||
| 20 | + .el-button { | ||
| 21 | + margin-right: 10px; | ||
| 22 | + margin-bottom: 10px; | ||
| 23 | + } | ||
| 24 | + } | ||
| 25 | + &__table { | ||
| 26 | + &-operation { | ||
| 27 | + display: flex; | ||
| 28 | + flex-wrap: wrap; | ||
| 29 | + align-items: center; | ||
| 30 | + justify-content: flex-start; | ||
| 31 | + .el-button + .el-button { | ||
| 32 | + margin-left: 0; | ||
| 33 | + } | ||
| 34 | + .el-button { | ||
| 35 | + margin-right: 10px; | ||
| 36 | + padding-top: 6px; | ||
| 37 | + padding-bottom: 6px; | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + &__dialog-button { | ||
| 42 | + display: flex; | ||
| 43 | + align-items: center; | ||
| 44 | + justify-content: center; | ||
| 45 | + padding-top: 10px; | ||
| 46 | + } | ||
| 47 | + &__footer { | ||
| 48 | + margin-top: 10px; | ||
| 49 | + text-align: right; | ||
| 50 | + display: flex; | ||
| 51 | + justify-content: space-between; | ||
| 52 | + align-items: center; | ||
| 53 | + .selection-info { | ||
| 54 | + word-break: break-all; | ||
| 55 | + white-space: nowrap; | ||
| 56 | + font-size: 12px; | ||
| 57 | + color: #606266; | ||
| 58 | + .num { | ||
| 59 | + color: #000; | ||
| 60 | + font-weight: bold; | ||
| 61 | + padding: 0 5px; | ||
| 62 | + font-size: 16px; | ||
| 63 | + } | ||
| 64 | + .el-button { | ||
| 65 | + margin-left: 5px; | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + .el-pagination { | ||
| 69 | + flex: auto; | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | +} | ||
| 0 | \ No newline at end of file | 73 | \ No newline at end of file |
| @@ -0,0 +1,649 @@ | @@ -0,0 +1,649 @@ | ||
| 1 | +<style lang="scss"> | ||
| 2 | +@import './index.scss'; | ||
| 3 | +</style> | ||
| 4 | + | ||
| 5 | +<template> | ||
| 6 | + <div class="z-schema"> | ||
| 7 | + <!-- 头部内容 --> | ||
| 8 | + <div v-if="$scopedSlots.header || $slots.header" class="z-schema__header"> | ||
| 9 | + <slot name="header" :filterModel="filterModel" v-bind="_slotScope"></slot> | ||
| 10 | + </div> | ||
| 11 | + <!-- 筛选组件 --> | ||
| 12 | + <div v-if="filter" class="z-schema__filter"> | ||
| 13 | + <z-schema-filter | ||
| 14 | + :value="_filterModel" | ||
| 15 | + :list="filterList || listMap.filter | noRulesFilter" | ||
| 16 | + :size="size" | ||
| 17 | + @input="onFilterInput" | ||
| 18 | + @search="onSearch" | ||
| 19 | + :loading="loading" | ||
| 20 | + v-bind="filterProps" | ||
| 21 | + :params="_slotScope" | ||
| 22 | + ></z-schema-filter> | ||
| 23 | + </div> | ||
| 24 | + <!-- 按钮区 --> | ||
| 25 | + <div v-if="action" class="z-schema__action"> | ||
| 26 | + <slot v-if="hadSlot('action')" name="action" v-bind="_slotScope"></slot> | ||
| 27 | + <template v-else> | ||
| 28 | + <el-button :size="size" type="primary" @click="openNew">新增</el-button> | ||
| 29 | + <el-button :size="size" plain :disabled="selection.length === 0" @click="handleDeleteMul(selection)">删除</el-button> | ||
| 30 | + <slot name="button" v-bind="_slotScope"></slot> | ||
| 31 | + </template> | ||
| 32 | + </div> | ||
| 33 | + <!-- 表格内容 --> | ||
| 34 | + <div class="z-schema__table"> | ||
| 35 | + <z-schema-table | ||
| 36 | + ref="table" | ||
| 37 | + v-model="tableData" | ||
| 38 | + v-loading="loading" | ||
| 39 | + :list="tableList || listMap.table" | ||
| 40 | + :tableProps="{ border: true, 'row-key': 'id', 'highlight-current-row': true, ...tableProps }" | ||
| 41 | + :size="size" | ||
| 42 | + @selection-change="onTableSelectionChange" | ||
| 43 | + @selection="onTableSelection" | ||
| 44 | + > | ||
| 45 | + <slot></slot> | ||
| 46 | + <!-- 表格列内容渲染 --> | ||
| 47 | + <template v-for="(item, index) in renderList"> | ||
| 48 | + <template v-if="$scopedSlots[`cell-${item.fullKey}`]"> | ||
| 49 | + <el-table-column :slot="item.fullKey" v-bind="item" :prop="item.agentKey || item.key" :key="`table-cell-${index}`"> | ||
| 50 | + <template slot-scope="{ row, column, $index }"> | ||
| 51 | + <slot :name="`cell-${item.fullKey}`" v-bind="{ ...item, ..._slotScope }" :row="row" :value="row[item.key]" :column="column" :index="$index"></slot> | ||
| 52 | + </template> | ||
| 53 | + </el-table-column> | ||
| 54 | + </template> | ||
| 55 | + <template v-else-if="$scopedSlots[`render-${item.fullKey}`]"> | ||
| 56 | + <el-table-column :slot="item.fullKey" v-bind="item" :prop="item.agentKey || item.key" :key="`table-render-${index}`"> | ||
| 57 | + <template slot-scope="{ row, column, $index }"> | ||
| 58 | + <slot :name="`render-${item.fullKey}`" v-bind="{ ...item, ..._slotScope }" :row="row" :value="row[item.key]" :column="column" :index="$index"></slot> | ||
| 59 | + </template> | ||
| 60 | + </el-table-column> | ||
| 61 | + </template> | ||
| 62 | + </template> | ||
| 63 | + <slot slot="column-append" name="column-append" v-bind="_slotScope"></slot> | ||
| 64 | + <!-- 表格尾追加操作列 --> | ||
| 65 | + <template #column-end> | ||
| 66 | + <slot slot="column-end" name="column-end" v-bind="_slotScope"></slot> | ||
| 67 | + <el-table-column v-if="operation" prop="$operation" label="操作" v-bind="{ width: 100, fixed: 'right', ...operationProps }"> | ||
| 68 | + <div class="z-schema__table-operation" slot-scope="slotScope"> | ||
| 69 | + <slot name="operation-button" v-bind="{ ..._slotScope, slotScope }"></slot> | ||
| 70 | + <el-button type="text" icon="el-icon-edit" title="编辑" @click="openEdit(slotScope.row)"></el-button> | ||
| 71 | + <el-popconfirm confirmButtonText="确定" cancelButtonText="取消" title="确定删除吗?" placement="top" @confirm="handleDelete([slotScope.row])"> | ||
| 72 | + <el-button slot="reference" type="text" icon="el-icon-delete" title="删除"></el-button> | ||
| 73 | + </el-popconfirm> | ||
| 74 | + <slot name="operation-button-append" v-bind="{ ..._slotScope, slotScope }"></slot> | ||
| 75 | + </div> | ||
| 76 | + </el-table-column> | ||
| 77 | + </template> | ||
| 78 | + </z-schema-table> | ||
| 79 | + </div> | ||
| 80 | + <!-- 底部区域 --> | ||
| 81 | + <div class="z-schema__footer"> | ||
| 82 | + <div v-if="selection.length > 0" class="selection-info"> | ||
| 83 | + <span>已选中</span> | ||
| 84 | + <span class="num">{{ selection.length }}</span> | ||
| 85 | + <span>项</span> | ||
| 86 | + <el-popconfirm confirmButtonText="确定" cancelButtonText="取消" title="确定清除吗?" placement="top" @confirm="clearSelection"> | ||
| 87 | + <el-button slot="reference" :size="size" type="text">清除</el-button> | ||
| 88 | + </el-popconfirm> | ||
| 89 | + </div> | ||
| 90 | + <!-- 分页器 --> | ||
| 91 | + <el-pagination | ||
| 92 | + v-if="pagination" | ||
| 93 | + @size-change="handleSizeChange" | ||
| 94 | + @current-change="handleCurrentChange" | ||
| 95 | + :current-page="currentPage" | ||
| 96 | + :page-sizes="pageSizes" | ||
| 97 | + :page-size="pageSize" | ||
| 98 | + layout="total, sizes, prev, pager, next, jumper" | ||
| 99 | + :total="total" | ||
| 100 | + > | ||
| 101 | + </el-pagination> | ||
| 102 | + </div> | ||
| 103 | + <!-- 弹出框 --> | ||
| 104 | + <el-dialog | ||
| 105 | + :visible.sync="dialogVisible" | ||
| 106 | + :title="dialogTitle" | ||
| 107 | + destroy-on-close | ||
| 108 | + append-to-body | ||
| 109 | + :lock-scroll="false" | ||
| 110 | + :close-on-click-modal="false" | ||
| 111 | + @closed="onDialogClosed" | ||
| 112 | + @close="onDialogClose" | ||
| 113 | + v-bind="_dialogProps" | ||
| 114 | + > | ||
| 115 | + <div v-loading="dialogLoading"> | ||
| 116 | + <!-- 自定义弹出框标题 --> | ||
| 117 | + <slot v-if="hadSlot('dialog-title')" slot="title" name="dialog-title" :dialogType="dialogType" v-bind="_slotScope"></slot> | ||
| 118 | + <template v-if="dialogRender"> | ||
| 119 | + <!-- 自定义弹出框内容 --> | ||
| 120 | + <slot v-if="hadSlot(`dialog-${dialogType}`)" :name="`dialog-${dialogType}`" :model="_formModel" v-bind="_slotScope"></slot> | ||
| 121 | + <template v-else> | ||
| 122 | + <!-- 内置弹出框新增修改表单 --> | ||
| 123 | + <template v-if="['new', 'edit'].includes(dialogType)"> | ||
| 124 | + <z-form | ||
| 125 | + ref="form" | ||
| 126 | + :value="_formModel" | ||
| 127 | + :list="formList || listMap.form" | ||
| 128 | + @input="onFormInput" | ||
| 129 | + @validate="onFormValidate" | ||
| 130 | + v-bind="{ span: 12, 'label-width': '110px', ...formProps }" | ||
| 131 | + :params="_slotScope" | ||
| 132 | + > | ||
| 133 | + <!-- 表单自定义插槽 --> | ||
| 134 | + <template v-for="item in renderList"> | ||
| 135 | + <template v-if="$scopedSlots[`form-${item.fullKey}`]"> | ||
| 136 | + <slot :slot="item.fullKey" :name="`form-${item.fullKey}`" :value="get(_formModel, item.fullKey)" :row="_formModel" :model="_formModel" v-bind="_slotScope"></slot> | ||
| 137 | + </template> | ||
| 138 | + </template> | ||
| 139 | + </z-form> | ||
| 140 | + </template> | ||
| 141 | + <!-- 内置弹出框详情表单 --> | ||
| 142 | + <template v-else> | ||
| 143 | + <z-form | ||
| 144 | + ref="form" | ||
| 145 | + class="z-schema__view" | ||
| 146 | + :value="_formModel" | ||
| 147 | + :list="viewList || listMap.form | viewTypeFilter | noRulesFilter" | ||
| 148 | + v-bind="{ span: 12, 'label-width': '110px', ...viewProps }" | ||
| 149 | + :params="_slotScope" | ||
| 150 | + > | ||
| 151 | + <!-- 详情自定义插槽渲染 --> | ||
| 152 | + <template v-for="item in renderList"> | ||
| 153 | + <template v-if="$scopedSlots[`view-${item.fullKey}`]"> | ||
| 154 | + <slot :slot="item.fullKey" :name="`view-${item.fullKey}`" :value="get(_formModel, item.fullKey)" :row="_formModel" :model="_formModel" v-bind="_slotScope"></slot> | ||
| 155 | + </template> | ||
| 156 | + <template v-else-if="$scopedSlots[`render-${item.fullKey}`]"> | ||
| 157 | + <slot :slot="item.fullKey" :name="`render-${item.fullKey}`" :value="get(_formModel, item.fullKey)" :row="_formModel" :model="_formModel" v-bind="_slotScope"></slot> | ||
| 158 | + </template> | ||
| 159 | + </template> | ||
| 160 | + </z-form> | ||
| 161 | + </template> | ||
| 162 | + </template> | ||
| 163 | + <!-- 内置弹出框新增修改按钮 --> | ||
| 164 | + <div class="z-schema__dialog-button" v-if="['new', 'edit'].includes(dialogType)"> | ||
| 165 | + <el-button :size="size" type="primary" @click="handleConfirm" :loading="submitting">确定</el-button> | ||
| 166 | + <el-button :size="size" plain @click="closeDialog">取消</el-button> | ||
| 167 | + </div> | ||
| 168 | + </template> | ||
| 169 | + </div> | ||
| 170 | + </el-dialog> | ||
| 171 | + </div> | ||
| 172 | +</template> | ||
| 173 | + | ||
| 174 | +<script> | ||
| 175 | +import { cloneDeep, get } from '../utils'; | ||
| 176 | +import { clear } from '../utils/param'; | ||
| 177 | + | ||
| 178 | +let propsMap = {}; | ||
| 179 | +const propsKeys = ['tableProps', 'filterProps', 'formProps', 'viewProps', 'dialogProps', 'operationProps']; | ||
| 180 | +propsKeys.forEach(key => { | ||
| 181 | + propsMap[key] = { | ||
| 182 | + type: Object, | ||
| 183 | + default: () => ({}), | ||
| 184 | + }; | ||
| 185 | +}); | ||
| 186 | +const apiKeys = ['searchApi', 'submitApi', 'addApi', 'modifyApi', 'getApi', 'viewApi', 'deleteApi']; | ||
| 187 | +apiKeys.forEach(key => { | ||
| 188 | + propsMap[key] = { | ||
| 189 | + type: Function, | ||
| 190 | + }; | ||
| 191 | +}); | ||
| 192 | +const blockKeys = ['filter', 'action', 'pagination', 'operation']; | ||
| 193 | +blockKeys.forEach(key => { | ||
| 194 | + propsMap[key] = { | ||
| 195 | + type: Boolean, | ||
| 196 | + default: true, | ||
| 197 | + }; | ||
| 198 | +}); | ||
| 199 | + | ||
| 200 | +export default { | ||
| 201 | + name: 'SchemaPage', | ||
| 202 | + props: { | ||
| 203 | + ...propsMap, | ||
| 204 | + list: Array, | ||
| 205 | + filterList: Array, | ||
| 206 | + tableList: Array, | ||
| 207 | + formList: Array, | ||
| 208 | + viewList: Array, | ||
| 209 | + size: { | ||
| 210 | + type: String, | ||
| 211 | + default: 'mini', | ||
| 212 | + }, | ||
| 213 | + formModel: Object, | ||
| 214 | + filterModel: Object, | ||
| 215 | + auto: Boolean, | ||
| 216 | + realSelection: Boolean, | ||
| 217 | + url: String, // 请求地址 | ||
| 218 | + http: Function, // http库 | ||
| 219 | + alias: Object, // 别名配置 | ||
| 220 | + }, | ||
| 221 | + data() { | ||
| 222 | + return { | ||
| 223 | + filterForm: {}, | ||
| 224 | + editForm: {}, | ||
| 225 | + dialogVisible: false, | ||
| 226 | + dialogRender: true, | ||
| 227 | + dialogType: 'none', | ||
| 228 | + dialogLoading: false, | ||
| 229 | + dialogTitle: '', | ||
| 230 | + dialogPropsHack: {}, | ||
| 231 | + currentPage: 1, | ||
| 232 | + pageSize: 10, | ||
| 233 | + total: 0, | ||
| 234 | + pageSizes: [10, 20, 50], | ||
| 235 | + tableData: [], | ||
| 236 | + submitting: false, | ||
| 237 | + loading: false, | ||
| 238 | + selection: [], | ||
| 239 | + }; | ||
| 240 | + }, | ||
| 241 | + created() { | ||
| 242 | + if (this.auto) { | ||
| 243 | + this.search(); | ||
| 244 | + } | ||
| 245 | + }, | ||
| 246 | + filters: { | ||
| 247 | + // 无规则过滤器,过滤掉筛选条件表单中的必填规则等 | ||
| 248 | + noRulesFilter(val = []) { | ||
| 249 | + let list = cloneDeep(val); | ||
| 250 | + const clearRules = list => { | ||
| 251 | + list.forEach(item => { | ||
| 252 | + if (item.list) { | ||
| 253 | + clearRules(item.list); | ||
| 254 | + } else { | ||
| 255 | + delete item.rules; | ||
| 256 | + } | ||
| 257 | + }); | ||
| 258 | + }; | ||
| 259 | + clearRules(list); | ||
| 260 | + return list; | ||
| 261 | + }, | ||
| 262 | + // 详情类型过滤器 | ||
| 263 | + viewTypeFilter(val = []) { | ||
| 264 | + let list = cloneDeep(val); | ||
| 265 | + const clearRules = list => { | ||
| 266 | + list.forEach(item => { | ||
| 267 | + item.type = (h, { model, config }) => h('span', config, model[item.key]); | ||
| 268 | + }); | ||
| 269 | + }; | ||
| 270 | + clearRules(list); | ||
| 271 | + return list; | ||
| 272 | + }, | ||
| 273 | + }, | ||
| 274 | + computed: { | ||
| 275 | + listMap() { | ||
| 276 | + // 默认作用域 | ||
| 277 | + const LIST_SPACE = ['filter', 'form', 'table']; | ||
| 278 | + const array = { | ||
| 279 | + filter: [], // 筛选 | ||
| 280 | + form: [], // 表单 | ||
| 281 | + table: [], // 表格 | ||
| 282 | + }; | ||
| 283 | + this.list.forEach(item => { | ||
| 284 | + // 可以在列表中通过include或exclude设置当前配置的作用域 | ||
| 285 | + const { include = LIST_SPACE, exclude = [] } = item; | ||
| 286 | + // 判断include | ||
| 287 | + let _inclue = []; | ||
| 288 | + if (include instanceof String || typeof include === 'string') { | ||
| 289 | + _inclue = [include]; | ||
| 290 | + } else if (include instanceof Array && typeof include === 'object') { | ||
| 291 | + _inclue = include; | ||
| 292 | + } | ||
| 293 | + // 判断exclude转换为include的情况 | ||
| 294 | + let _exclude_include = []; | ||
| 295 | + if (exclude instanceof String || typeof exclude === 'string') { | ||
| 296 | + _exclude_include = LIST_SPACE.filter(item => item !== exclude); | ||
| 297 | + } else if (exclude instanceof Array && typeof exclude === 'object') { | ||
| 298 | + _exclude_include = LIST_SPACE.filter(item => !exclude.includes(item)); | ||
| 299 | + } | ||
| 300 | + // 作用域交集 | ||
| 301 | + const _intersection = _inclue.filter(v => _exclude_include.includes(v)); | ||
| 302 | + // 返回改配置项的作用域 | ||
| 303 | + const _list_space = cloneDeep(_intersection); | ||
| 304 | + // 将配置项按需分配至各作用域下 | ||
| 305 | + _list_space.forEach(name => { | ||
| 306 | + array[name].push({ ...item, ...(item[name] || {}) }); | ||
| 307 | + }); | ||
| 308 | + }); | ||
| 309 | + return array; | ||
| 310 | + }, | ||
| 311 | + renderList() { | ||
| 312 | + // 深度克隆传入的列表,避免原始值被修改 | ||
| 313 | + const newList = cloneDeep(this.list); | ||
| 314 | + // 生成列表值的全路径key,即列表项为对象时,对象内的key与上一级的key合并作为全路径key | ||
| 315 | + const generateFullKey = (list, parentKey) => { | ||
| 316 | + list.forEach(item => { | ||
| 317 | + if (item.group && item.list) { | ||
| 318 | + if (item.group.key) { | ||
| 319 | + item.fullKey = `${parentKey ? `${parentKey}-${item.group.key}` : item.group.key}`; | ||
| 320 | + } else { | ||
| 321 | + item.fullKey = parentKey || item.key; | ||
| 322 | + } | ||
| 323 | + generateFullKey(item.list, item.fullKey); | ||
| 324 | + } else { | ||
| 325 | + item.fullKey = `${parentKey ? `${parentKey}-${item.key}` : item.key}`; | ||
| 326 | + } | ||
| 327 | + }); | ||
| 328 | + }; | ||
| 329 | + // 生成fullKey | ||
| 330 | + generateFullKey(newList); | ||
| 331 | + return newList; | ||
| 332 | + }, | ||
| 333 | + _filterModel() { | ||
| 334 | + return this.filterModel || this.filterForm || {}; | ||
| 335 | + }, | ||
| 336 | + _formModel() { | ||
| 337 | + return this.formModel || this.editForm || {}; | ||
| 338 | + }, | ||
| 339 | + _slotScope() { | ||
| 340 | + return { | ||
| 341 | + handleSearch: this.search, | ||
| 342 | + openDialog: this.openDialog, | ||
| 343 | + closeDialog: this.closeDialog, | ||
| 344 | + openView: this.openView, | ||
| 345 | + openEdit: this.openEdit, | ||
| 346 | + openNew: this.openNew, | ||
| 347 | + handleDelete: this.handleDelete, | ||
| 348 | + handleDeleteMul: this.handleDeleteMul, | ||
| 349 | + size: this.size, | ||
| 350 | + dialogType: this.dialogType, | ||
| 351 | + selection: this.selection, | ||
| 352 | + }; | ||
| 353 | + }, | ||
| 354 | + _alias() { | ||
| 355 | + const alias = this.alias; | ||
| 356 | + const zAlias = this.zAlias; | ||
| 357 | + if (alias && zAlias) { | ||
| 358 | + return { ...zAlias, ...alias }; | ||
| 359 | + } | ||
| 360 | + return this.alias || this.zAlias || {}; | ||
| 361 | + }, | ||
| 362 | + _dialogProps() { | ||
| 363 | + return { | ||
| 364 | + ...this.dialogProps, | ||
| 365 | + ...this.dialogPropsHack, | ||
| 366 | + }; | ||
| 367 | + }, | ||
| 368 | + }, | ||
| 369 | + methods: { | ||
| 370 | + get, | ||
| 371 | + // 空Promise | ||
| 372 | + emptyPromise() { | ||
| 373 | + return new Promise(resolve => resolve()); | ||
| 374 | + }, | ||
| 375 | + // 设置表格选中行 | ||
| 376 | + toggleRowSelection() { | ||
| 377 | + this.tableData.forEach(row => { | ||
| 378 | + if (this.selection.find(item => item.id === row.id)) { | ||
| 379 | + this.$refs.table && this.$refs.table.toggleRowSelection(row); | ||
| 380 | + } | ||
| 381 | + }); | ||
| 382 | + }, | ||
| 383 | + // 表格选中状态 | ||
| 384 | + onTableSelectionChange(selection, type) { | ||
| 385 | + if (this.realSelection) { | ||
| 386 | + if (type === 'check') { | ||
| 387 | + const result = this.selection || []; | ||
| 388 | + selection.forEach(item => { | ||
| 389 | + if (!result.find(i => i.id === item.id)) { | ||
| 390 | + result.push(item); | ||
| 391 | + } | ||
| 392 | + }); | ||
| 393 | + this.selection = result; | ||
| 394 | + } else if (type === 'uncheck') { | ||
| 395 | + selection.forEach(i => { | ||
| 396 | + this.selection = this.selection.filter(item => item.id !== i.id); | ||
| 397 | + }); | ||
| 398 | + } | ||
| 399 | + } | ||
| 400 | + }, | ||
| 401 | + // 表格选中 | ||
| 402 | + onTableSelection(selection) { | ||
| 403 | + if (!this.realSelection) { | ||
| 404 | + this.selection = selection; | ||
| 405 | + } | ||
| 406 | + }, | ||
| 407 | + // 清除表格选中 | ||
| 408 | + clearSelection() { | ||
| 409 | + this.$refs.table && this.$refs.table.clearSelection(); | ||
| 410 | + this.selection = []; | ||
| 411 | + }, | ||
| 412 | + // 内置搜索接口 | ||
| 413 | + _searchAPI(params) { | ||
| 414 | + if (this.url && (this.http || this.zHttp)) { | ||
| 415 | + const _http = this.http || this.zHttp; | ||
| 416 | + return _http({ url: `${clear(this.url)}/${this._alias.pageUrl || 'page'}`, params }); | ||
| 417 | + } | ||
| 418 | + return undefined; | ||
| 419 | + }, | ||
| 420 | + // 重置查询 | ||
| 421 | + onSearch() { | ||
| 422 | + this.currentPage = 1; | ||
| 423 | + this.search(); | ||
| 424 | + }, | ||
| 425 | + // 搜索 | ||
| 426 | + async search() { | ||
| 427 | + this.loading = true; | ||
| 428 | + const params = { | ||
| 429 | + ...this._filterModel, | ||
| 430 | + currentPage: this.currentPage, | ||
| 431 | + pageSize: this.pageSize, | ||
| 432 | + }; | ||
| 433 | + const searchAPI = this.searchApi || this._searchAPI || this.emptyPromise; | ||
| 434 | + await searchAPI(params) | ||
| 435 | + .then(res => { | ||
| 436 | + const response = res || {}; | ||
| 437 | + this.tableData = response[this._alias.list || 'list'] || []; | ||
| 438 | + this.total = response[this._alias.total || 'total'] || 0; | ||
| 439 | + this.$nextTick(this.toggleRowSelection); | ||
| 440 | + }) | ||
| 441 | + .catch(() => { | ||
| 442 | + this.$message.error('查询失败'); | ||
| 443 | + }); | ||
| 444 | + this.loading = false; | ||
| 445 | + }, | ||
| 446 | + // 更新筛选model | ||
| 447 | + onFilterInput(val) { | ||
| 448 | + this.filterForm = val || {}; | ||
| 449 | + this.$emit('update:filterModel', val || {}); | ||
| 450 | + }, | ||
| 451 | + // 更新表单model | ||
| 452 | + onFormInput(val) { | ||
| 453 | + this.editForm = val || {}; | ||
| 454 | + this.$emit('update:formModel', val || {}); | ||
| 455 | + }, | ||
| 456 | + // 内置新增保存接口 | ||
| 457 | + _addAPI(data) { | ||
| 458 | + if (this.url && (this.http || this.zHttp)) { | ||
| 459 | + const _http = this.http || this.zHttp; | ||
| 460 | + return _http({ url: `${clear(this.url)}/${this._alias.addUrl || 'add'}`, method: 'post', data }); | ||
| 461 | + } | ||
| 462 | + return undefined; | ||
| 463 | + }, | ||
| 464 | + // 内置修改保存接口 | ||
| 465 | + _modifyAPI(data) { | ||
| 466 | + if (this.url && (this.http || this.zHttp)) { | ||
| 467 | + const _http = this.http || this.zHttp; | ||
| 468 | + return _http({ url: `${clear(this.url)}/${this._alias.modifyUrl || 'modify'}`, method: 'post', data }); | ||
| 469 | + } | ||
| 470 | + return undefined; | ||
| 471 | + }, | ||
| 472 | + // 表单提交且通过校验 | ||
| 473 | + async onFormValidate(valid, model) { | ||
| 474 | + if (valid) { | ||
| 475 | + this.submitting = true; | ||
| 476 | + let submitAPI = this.submitApi || this.emptyPromise; | ||
| 477 | + if (this.dialogType === 'new') { | ||
| 478 | + submitAPI = this.addApi || this.submitApi || this._addAPI || this.emptyPromise; | ||
| 479 | + } else if (this.dialogType === 'edit') { | ||
| 480 | + submitAPI = this.modifyApi || this.submitApi || this._modifyAPI || this.emptyPromise; | ||
| 481 | + } | ||
| 482 | + submitAPI(model, { type: this.dialogType }) | ||
| 483 | + .then(() => { | ||
| 484 | + this.$message.success('保存成功'); | ||
| 485 | + this.closeDialog(); | ||
| 486 | + this.search(); | ||
| 487 | + }) | ||
| 488 | + .catch(() => { | ||
| 489 | + this.$message.error('保存失败'); | ||
| 490 | + }) | ||
| 491 | + .finally(() => { | ||
| 492 | + this.submitting = false; | ||
| 493 | + }); | ||
| 494 | + } | ||
| 495 | + }, | ||
| 496 | + // 表单按钮确定 | ||
| 497 | + handleConfirm() { | ||
| 498 | + this.$refs.form && this.$refs.form.validate(); | ||
| 499 | + }, | ||
| 500 | + // 表单按钮取消 | ||
| 501 | + handleCancel() { | ||
| 502 | + this.closeDialog(); | ||
| 503 | + }, | ||
| 504 | + // 查询是否有某个插槽 | ||
| 505 | + hadSlot(name) { | ||
| 506 | + return !!this.$slots[name] || !!this.$scopedSlots[name]; | ||
| 507 | + }, | ||
| 508 | + // 打开新增弹出框 | ||
| 509 | + openNew() { | ||
| 510 | + this.openDialog('new', '新增'); | ||
| 511 | + }, | ||
| 512 | + // 内置查询详情接口 | ||
| 513 | + _getAPI(row) { | ||
| 514 | + if (this.url && (this.http || this.zHttp)) { | ||
| 515 | + const _http = this.http || this.zHttp; | ||
| 516 | + const _getKey = this._alias.getKey || this._alias.primaryKey || 'id'; | ||
| 517 | + const _resultKey = this._alias.result || 'result'; | ||
| 518 | + return _http({ url: `${clear(this.url)}/${this._alias.getUrl || 'queryById'}`, params: { [_getKey]: row[_getKey] } }).then(response => response[_resultKey] || {}); | ||
| 519 | + } | ||
| 520 | + return undefined; | ||
| 521 | + }, | ||
| 522 | + // 打开编辑弹出框 | ||
| 523 | + openEdit(row) { | ||
| 524 | + this.dialogLoading = true; | ||
| 525 | + this.openDialog('edit', '编辑'); | ||
| 526 | + const getRow = () => | ||
| 527 | + new Promise(resolve => { | ||
| 528 | + resolve(row); | ||
| 529 | + }); | ||
| 530 | + const getAPI = this.getApi || this._getAPI || getRow; | ||
| 531 | + getAPI(row) | ||
| 532 | + .then(result => { | ||
| 533 | + this.editForm = result; | ||
| 534 | + this.$emit('update:formModel', result || {}); | ||
| 535 | + }) | ||
| 536 | + .finally(() => { | ||
| 537 | + this.dialogLoading = false; | ||
| 538 | + }); | ||
| 539 | + }, | ||
| 540 | + // 内置查询详情接口 | ||
| 541 | + _viewAPI(row) { | ||
| 542 | + if (this.url && (this.http || this.zHttp)) { | ||
| 543 | + const _http = this.http || this.zHttp; | ||
| 544 | + const _viewKey = this._alias.viewKey || this._alias.getKey || this._alias.primaryKey || 'id'; | ||
| 545 | + const _resultKey = this._alias.result || 'result'; | ||
| 546 | + return _http({ url: `${clear(this.url)}/${this._alias.getUrl || 'queryById'}`, params: { [_viewKey]: row[_viewKey] } }).then(response => response[_resultKey] || {}); | ||
| 547 | + } | ||
| 548 | + return undefined; | ||
| 549 | + }, | ||
| 550 | + // 打开详情弹出框 | ||
| 551 | + openView(row) { | ||
| 552 | + this.dialogLoading = true; | ||
| 553 | + this.openDialog('view', '详情'); | ||
| 554 | + const getRow = () => | ||
| 555 | + new Promise(resolve => { | ||
| 556 | + resolve(row); | ||
| 557 | + }); | ||
| 558 | + const viewAPI = this.viewApi || this.getApi || this._viewAPI || this._getAPI || getRow; | ||
| 559 | + viewAPI(row) | ||
| 560 | + .then(result => { | ||
| 561 | + this.editForm = result; | ||
| 562 | + this.$emit('update:formModel', result || {}); | ||
| 563 | + }) | ||
| 564 | + .finally(() => { | ||
| 565 | + this.dialogLoading = false; | ||
| 566 | + }); | ||
| 567 | + }, | ||
| 568 | + // 内置删除接口 | ||
| 569 | + _deleteAPI(keys) { | ||
| 570 | + if (this.url && (this.http || this.zHttp)) { | ||
| 571 | + const _http = this.http || this.zHttp; | ||
| 572 | + return _http({ url: `${clear(this.url)}/${this._alias.modifyUrl || 'delete'}`, method: 'post', data: keys }); | ||
| 573 | + } | ||
| 574 | + return undefined; | ||
| 575 | + }, | ||
| 576 | + // 删除 | ||
| 577 | + handleDelete(selection) { | ||
| 578 | + const loading = this.$loading({ | ||
| 579 | + text: '处理中', | ||
| 580 | + spinner: 'el-icon-loading', | ||
| 581 | + background: 'rgba(255, 255, 255, 0.5)', | ||
| 582 | + }); | ||
| 583 | + const deleteAPI = this.deleteApi || this._deleteAPI || this.emptyPromise; | ||
| 584 | + const _deleteKey = this._alias.deleteKey || this._alias.primaryKey || 'id'; | ||
| 585 | + const keys = selection.map(i => i[_deleteKey]); | ||
| 586 | + deleteAPI(keys) | ||
| 587 | + .then(() => { | ||
| 588 | + this.search(); | ||
| 589 | + this.$message.success('删除成功'); | ||
| 590 | + }) | ||
| 591 | + .finally(() => { | ||
| 592 | + loading.close(); | ||
| 593 | + }); | ||
| 594 | + }, | ||
| 595 | + // 批量删除 | ||
| 596 | + handleDeleteMul(selection) { | ||
| 597 | + this.$confirm(`是否删除这 [${selection.length}] 项?`, '提示', { | ||
| 598 | + confirmButtonText: '确定', | ||
| 599 | + cancelButtonText: '取消', | ||
| 600 | + type: 'warning', | ||
| 601 | + }) | ||
| 602 | + .then(() => { | ||
| 603 | + this.handleDelete(selection); | ||
| 604 | + }) | ||
| 605 | + .catch(() => {}); | ||
| 606 | + }, | ||
| 607 | + // 打开弹出框 | ||
| 608 | + openDialog(type, title, config) { | ||
| 609 | + this.dialogVisible = true; | ||
| 610 | + this.dialogRender = true; | ||
| 611 | + this.dialogType = type; | ||
| 612 | + this.dialogTitle = title; | ||
| 613 | + this.dialogPropsHack = config || {}; | ||
| 614 | + this.$emit('dialog-change', type); | ||
| 615 | + }, | ||
| 616 | + // 关闭弹出框 | ||
| 617 | + closeDialog() { | ||
| 618 | + this.dialogVisible = false; | ||
| 619 | + }, | ||
| 620 | + // 清空表单 | ||
| 621 | + clearEditForm() { | ||
| 622 | + this.editForm = {}; | ||
| 623 | + this.$emit('update:formModel', {}); | ||
| 624 | + }, | ||
| 625 | + // 弹出框关闭 | ||
| 626 | + onDialogClose() { | ||
| 627 | + this.dialogType = 'none'; | ||
| 628 | + this.dialogRender = false; | ||
| 629 | + this.$emit('dialog-change', 'none'); | ||
| 630 | + }, | ||
| 631 | + // 弹出框关闭动画结束 | ||
| 632 | + onDialogClosed() { | ||
| 633 | + this.clearEditForm(); | ||
| 634 | + this.dialogPropsHack = {}; | ||
| 635 | + }, | ||
| 636 | + // 分页-每页个数 | ||
| 637 | + handleSizeChange(val) { | ||
| 638 | + this.pageSize = val; | ||
| 639 | + this.currentPage = 1; | ||
| 640 | + this.$nextTick(this.search); | ||
| 641 | + }, | ||
| 642 | + // 分页-当前页数 | ||
| 643 | + handleCurrentChange(val) { | ||
| 644 | + this.currentPage = val; | ||
| 645 | + this.$nextTick(this.search); | ||
| 646 | + }, | ||
| 647 | + }, | ||
| 648 | +}; | ||
| 649 | +</script> |
| @@ -0,0 +1,105 @@ | @@ -0,0 +1,105 @@ | ||
| 1 | +.z-schema-page { | ||
| 2 | + &__header { | ||
| 3 | + margin-bottom: 10px; | ||
| 4 | + } | ||
| 5 | + &__filter { | ||
| 6 | + border: 1px solid #ebeef5; | ||
| 7 | + padding-top: 10px; | ||
| 8 | + padding-right: 10px; | ||
| 9 | + border-radius: 4px; | ||
| 10 | + margin-bottom: 10px; | ||
| 11 | + } | ||
| 12 | + &__action { | ||
| 13 | + display: flex; | ||
| 14 | + flex-wrap: wrap; | ||
| 15 | + align-items: center; | ||
| 16 | + justify-content: flex-start; | ||
| 17 | + line-height: 1; | ||
| 18 | + .el-button + .el-button { | ||
| 19 | + margin-left: 0; | ||
| 20 | + } | ||
| 21 | + .el-button { | ||
| 22 | + margin-right: 10px; | ||
| 23 | + margin-bottom: 10px; | ||
| 24 | + } | ||
| 25 | + } | ||
| 26 | + &__table { | ||
| 27 | + &-operation { | ||
| 28 | + display: flex; | ||
| 29 | + flex-wrap: wrap; | ||
| 30 | + align-items: center; | ||
| 31 | + justify-content: center; | ||
| 32 | + .el-button + .el-button { | ||
| 33 | + margin-left: 0; | ||
| 34 | + } | ||
| 35 | + .el-button { | ||
| 36 | + margin-right: 8px; | ||
| 37 | + padding-top: 6px; | ||
| 38 | + padding-bottom: 6px; | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | + &__dialog-button { | ||
| 43 | + display: flex; | ||
| 44 | + align-items: center; | ||
| 45 | + justify-content: center; | ||
| 46 | + padding-top: 10px; | ||
| 47 | + } | ||
| 48 | + &__footer { | ||
| 49 | + margin-top: 10px; | ||
| 50 | + text-align: right; | ||
| 51 | + display: flex; | ||
| 52 | + justify-content: space-between; | ||
| 53 | + align-items: center; | ||
| 54 | + .selection-info { | ||
| 55 | + word-break: break-all; | ||
| 56 | + white-space: nowrap; | ||
| 57 | + font-size: 12px; | ||
| 58 | + color: #606266; | ||
| 59 | + .num { | ||
| 60 | + color: #333; | ||
| 61 | + font-weight: bold; | ||
| 62 | + padding: 0 5px; | ||
| 63 | + font-size: 16px; | ||
| 64 | + } | ||
| 65 | + .el-button { | ||
| 66 | + margin-left: 5px; | ||
| 67 | + } | ||
| 68 | + } | ||
| 69 | + .el-pagination { | ||
| 70 | + flex: auto; | ||
| 71 | + } | ||
| 72 | + } | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +.z-loading-toast { | ||
| 76 | + $toast-color: #fff; | ||
| 77 | + $toast-bg-color: #000; | ||
| 78 | + background-color: rgba($toast-bg-color, 0); | ||
| 79 | + .el-loading-spinner { | ||
| 80 | + width: 200px; | ||
| 81 | + height: 120px; | ||
| 82 | + margin: auto; | ||
| 83 | + background-color: rgba($toast-bg-color, 0.7); | ||
| 84 | + color: $toast-color; | ||
| 85 | + left: 50%; | ||
| 86 | + transform: translateX(-50%); | ||
| 87 | + border-radius: 16px; | ||
| 88 | + display: flex; | ||
| 89 | + flex-direction: column; | ||
| 90 | + align-items: center; | ||
| 91 | + justify-content: center; | ||
| 92 | + .path { | ||
| 93 | + stroke: $toast-color; | ||
| 94 | + } | ||
| 95 | + .el-icon-loading { | ||
| 96 | + font-size: 32px; | ||
| 97 | + color: $toast-color; | ||
| 98 | + } | ||
| 99 | + .el-loading-text { | ||
| 100 | + font-size: 14px; | ||
| 101 | + color: $toast-color; | ||
| 102 | + margin-top: 10px; | ||
| 103 | + } | ||
| 104 | + } | ||
| 105 | +} | ||
| 0 | \ No newline at end of file | 106 | \ No newline at end of file |
| @@ -0,0 +1,436 @@ | @@ -0,0 +1,436 @@ | ||
| 1 | +<style lang="scss"> | ||
| 2 | +@import './index.scss'; | ||
| 3 | +</style> | ||
| 4 | + | ||
| 5 | +<template> | ||
| 6 | + <div class="z-schema-page"> | ||
| 7 | + <!-- 筛选组件 --> | ||
| 8 | + <div v-if="schema.filter" class="z-schema-page__filter"> | ||
| 9 | + <z-schema-filter :schema="schema.filter" :value="valueFilter" @input="e => $emit('update:value-filter', e)" :loading="loading" @search="onSearch"> | ||
| 10 | + <template v-for="item in getSlotKeys('filter-')" #[item.name]="slotScope"> | ||
| 11 | + <slot :name="item.slot" v-bind="{ ..._slotScope, ...slotScope }"></slot> | ||
| 12 | + </template> | ||
| 13 | + </z-schema-filter> | ||
| 14 | + </div> | ||
| 15 | + <!-- 按钮区 --> | ||
| 16 | + <div v-if="schema.action !== false" class="z-schema-page__action"> | ||
| 17 | + <slot name="action" v-bind="_slotScope"> | ||
| 18 | + <el-button :size="_size" type="primary" @click="openNew">新增</el-button> | ||
| 19 | + <el-button :size="_size" plain :disabled="selection.length === 0" @click="onDeleteMultiple(selection)">删除</el-button> | ||
| 20 | + <slot name="action-button" v-bind="_slotScope"></slot> | ||
| 21 | + </slot> | ||
| 22 | + </div> | ||
| 23 | + <!-- 表格内容 --> | ||
| 24 | + <div v-if="schema.table" class="z-schema-page__table"> | ||
| 25 | + <z-schema-table :schema="schema.table" v-model="tableData" v-loading="loading" @selection-change="onTableSelectionChange"> | ||
| 26 | + <template #left> | ||
| 27 | + <el-table-column v-if="schema.selectable !== false" type="selection" width="40" align="center"></el-table-column> | ||
| 28 | + </template> | ||
| 29 | + <template v-for="item in getSlotKeys('table-')" #[item.name]="slotScope"> | ||
| 30 | + <slot :name="item.slot" v-bind="{ ..._slotScope, ...slotScope }"></slot> | ||
| 31 | + </template> | ||
| 32 | + <slot v-if="schema.operation !== false" name="operation" v-bind="_slotScope"> | ||
| 33 | + <el-table-column v-bind="{ label: '操作', width: '90', align: 'center', ...(schema.operation || {}) }"> | ||
| 34 | + <template #default="{ row, column, $index }"> | ||
| 35 | + <div class="z-schema-page__table-operation"> | ||
| 36 | + <slot name="operation-left" v-bind="{ ..._slotScope, row, column, $index }"></slot> | ||
| 37 | + <slot name="operation-button" v-bind="{ ..._slotScope, row, column, $index }"></slot> | ||
| 38 | + <el-button type="text" icon="el-icon-edit" title="编辑" @click="openEdit(row)"></el-button> | ||
| 39 | + <el-popconfirm confirm-button-text="确定" cancel-button-text="取消" title="确定删除吗?" placement="top" @confirm="onDelete([row])"> | ||
| 40 | + <el-button slot="reference" type="text" icon="el-icon-delete" title="删除"></el-button> | ||
| 41 | + </el-popconfirm> | ||
| 42 | + <slot name="operation-right" v-bind="{ ..._slotScope, row, column, $index }"></slot> | ||
| 43 | + </div> | ||
| 44 | + </template> | ||
| 45 | + </el-table-column> | ||
| 46 | + </slot> | ||
| 47 | + </z-schema-table> | ||
| 48 | + </div> | ||
| 49 | + <!-- 底部区域 --> | ||
| 50 | + <div class="z-schema-page__footer"> | ||
| 51 | + <slot name="pagination" v-bind="_slotScope"> | ||
| 52 | + <div v-if="selection.length > 0" class="selection-info"> | ||
| 53 | + <span>已选中</span> | ||
| 54 | + <span class="num">{{ selection.length }}</span> | ||
| 55 | + <span>项</span> | ||
| 56 | + </div> | ||
| 57 | + <!-- 分页器 --> | ||
| 58 | + <el-pagination | ||
| 59 | + v-if="schema.pagination !== false" | ||
| 60 | + @size-change="onSizeChange" | ||
| 61 | + @current-change="onCurrentChange" | ||
| 62 | + :current-page="currentPage" | ||
| 63 | + :page-sizes="pageSizes" | ||
| 64 | + :page-size="pageSize" | ||
| 65 | + :layout="layout" | ||
| 66 | + :total="total" | ||
| 67 | + > | ||
| 68 | + </el-pagination> | ||
| 69 | + </slot> | ||
| 70 | + </div> | ||
| 71 | + <el-dialog :title="elDialogTitle" :visible.sync="visible" v-bind="_dialogProps" @update:visible="onVisibleUpdate" @close="onDialogClose" @closed="onDialogClosed"> | ||
| 72 | + <template v-if="elDialogRender"> | ||
| 73 | + <slot v-if="getSlot(`dialog-${elDialogType}`)" :name="`dialog-${elDialogType}`" v-bind="_slotScope"></slot> | ||
| 74 | + <div v-else v-loading="dialogLoading"> | ||
| 75 | + <template v-if="['new', 'edit'].includes(elDialogType)"> | ||
| 76 | + <z-schema-form | ||
| 77 | + :key="`form-${elDialogType}`" | ||
| 78 | + ref="form" | ||
| 79 | + :value="valueForm" | ||
| 80 | + @input="e => $emit('update:value-form', e)" | ||
| 81 | + :schema="schema.form" | ||
| 82 | + @submit="onFormSubmit" | ||
| 83 | + @cancel="closeDialog" | ||
| 84 | + > | ||
| 85 | + <template v-for="item in getSlotKeys('form-')" #[item.name]="slotScope"> | ||
| 86 | + <slot :name="item.slot" v-bind="{ ..._slotScope, ...slotScope }"></slot> | ||
| 87 | + </template> | ||
| 88 | + <template #footer="{ submit, cancel }"> | ||
| 89 | + <div style="text-align: center"> | ||
| 90 | + <el-button :size="_size" type="primary" @click="submit" :loading="submitting">确定</el-button> | ||
| 91 | + <el-button :size="_size" plain @click="cancel">取消</el-button> | ||
| 92 | + </div> | ||
| 93 | + </template> | ||
| 94 | + </z-schema-form> | ||
| 95 | + </template> | ||
| 96 | + <template v-else-if="elDialogType === 'detail'"> | ||
| 97 | + <z-schema-form key="form-detail" ref="form" v-model="detail" :schema="schema.detail || detailSchema"> | ||
| 98 | + <template v-for="item in getSlotKeys('detail-')" #[item.name]="slotScope"> | ||
| 99 | + <slot :name="item.slot" v-bind="{ ..._slotScope, ...slotScope }"></slot> | ||
| 100 | + </template> | ||
| 101 | + </z-schema-form> | ||
| 102 | + </template> | ||
| 103 | + </div> | ||
| 104 | + </template> | ||
| 105 | + </el-dialog> | ||
| 106 | + </div> | ||
| 107 | +</template> | ||
| 108 | + | ||
| 109 | +<script> | ||
| 110 | +import { cloneDeep, get } from '../utils'; | ||
| 111 | +import { filterout } from '../utils/schema'; | ||
| 112 | +import MIX_ORGIN from '../mixins/origin'; | ||
| 113 | + | ||
| 114 | +let propsMap = {}; | ||
| 115 | + | ||
| 116 | +const setKeysDefault = function(keys, value) { | ||
| 117 | + keys.reduce(function(result, current) { | ||
| 118 | + result[current] = value; | ||
| 119 | + return result; | ||
| 120 | + }, propsMap); | ||
| 121 | +}; | ||
| 122 | +setKeysDefault(['value-filter', 'value-form', 'value-detail'], { | ||
| 123 | + type: Object, | ||
| 124 | + default() { | ||
| 125 | + return {}; | ||
| 126 | + }, | ||
| 127 | +}); | ||
| 128 | +setKeysDefault(['value-table'], { | ||
| 129 | + type: Array, | ||
| 130 | + default() { | ||
| 131 | + return []; | ||
| 132 | + }, | ||
| 133 | +}); | ||
| 134 | +setKeysDefault(['size', 'dialogTitle', 'dialogType'], String); | ||
| 135 | +setKeysDefault(['dialogVisible', 'auto'], Boolean); | ||
| 136 | +setKeysDefault(['api-search', 'api-submit', 'api-new', 'api-edit', 'api-get', 'api-detail', 'api-delete'], Function); | ||
| 137 | + | ||
| 138 | +export default { | ||
| 139 | + name: 'SchemaPage', | ||
| 140 | + mixins: [MIX_ORGIN], | ||
| 141 | + props: { | ||
| 142 | + ...propsMap, | ||
| 143 | + schema: { | ||
| 144 | + type: Object, | ||
| 145 | + required: true, | ||
| 146 | + }, | ||
| 147 | + }, | ||
| 148 | + data() { | ||
| 149 | + return { | ||
| 150 | + selection: [], | ||
| 151 | + // 分页参数 | ||
| 152 | + currentPage: get(this.schema, 'pagination.currentPage') || 1, | ||
| 153 | + pageSizes: get(this.schema, 'pagination.pageSizes') || [10, 20, 50, 100], | ||
| 154 | + pageSize: get(this.schema, 'pagination.pageSize') || 10, | ||
| 155 | + layout: get(this.schema, 'pagination.layout') || 'total, sizes, prev, pager, next, jumper', | ||
| 156 | + total: get(this.schema, 'pagination.total') || 0, | ||
| 157 | + visible: this.dialogVisible, | ||
| 158 | + elDialogRender: true, | ||
| 159 | + elDialogType: this.dialogType || 'none', | ||
| 160 | + elDialogTitle: this.dialogTitle || '', | ||
| 161 | + elDialogProps: {}, | ||
| 162 | + detailSchema: filterout(cloneDeep(this.schema.form), ['is', 'rules']), | ||
| 163 | + detail: this.valueDetail || {}, | ||
| 164 | + tableData: this.valueTable || [], | ||
| 165 | + loading: false, | ||
| 166 | + submitting: false, | ||
| 167 | + dialogLoading: false, | ||
| 168 | + }; | ||
| 169 | + }, | ||
| 170 | + created() { | ||
| 171 | + if (this.auto || this.schema.auto) { | ||
| 172 | + this.onSearch(); | ||
| 173 | + } | ||
| 174 | + console.log(this); | ||
| 175 | + }, | ||
| 176 | + watch: { | ||
| 177 | + detailValue(val) { | ||
| 178 | + this.detail = val; | ||
| 179 | + }, | ||
| 180 | + detail(val) { | ||
| 181 | + this.$emit('update:value-detail', val); | ||
| 182 | + }, | ||
| 183 | + dialogVisible(val) { | ||
| 184 | + this.visible = val; | ||
| 185 | + }, | ||
| 186 | + visible(val) { | ||
| 187 | + this.$emit('update:dialog-visible', val); | ||
| 188 | + }, | ||
| 189 | + dialogType(val) { | ||
| 190 | + this.elDialogType = val; | ||
| 191 | + }, | ||
| 192 | + elDialogType(val) { | ||
| 193 | + this.$emit('update:dialog-type', val); | ||
| 194 | + }, | ||
| 195 | + dialogTitle(val) { | ||
| 196 | + this.elDialogTitle = val; | ||
| 197 | + }, | ||
| 198 | + elDialogTitle(val) { | ||
| 199 | + this.$emit('update:dialog-title', val); | ||
| 200 | + }, | ||
| 201 | + valueTable(val) { | ||
| 202 | + this.tableData = val; | ||
| 203 | + }, | ||
| 204 | + tableData(val) { | ||
| 205 | + this.$emit('update:value-table', val); | ||
| 206 | + }, | ||
| 207 | + }, | ||
| 208 | + computed: { | ||
| 209 | + slotKeys() { | ||
| 210 | + return Object.keys(this.$scopedSlots); | ||
| 211 | + }, | ||
| 212 | + _size() { | ||
| 213 | + return this.size || get(this.schema, 'props.size') || (this.$ELEMENT || {}).size || 'small'; | ||
| 214 | + }, | ||
| 215 | + _slotScope() { | ||
| 216 | + const properties = ['selection', 'currentPage', 'pageSizes', 'pageSize', 'layout', 'total', 'loading']; | ||
| 217 | + const methods = ['search', 'onSearch', 'onDelete', 'onDeleteMultiple', 'openNew', 'openEdit', 'openDetail', 'openDialog', 'closeDialog']; | ||
| 218 | + const defaultScope = { | ||
| 219 | + size: this._size, | ||
| 220 | + }; | ||
| 221 | + return [...properties, ...methods].reduce((result, current) => { | ||
| 222 | + result[current] = this[current]; | ||
| 223 | + return result; | ||
| 224 | + }, defaultScope); | ||
| 225 | + }, | ||
| 226 | + _dialogProps() { | ||
| 227 | + return { | ||
| 228 | + 'destroy-on-close': true, | ||
| 229 | + 'append-to-body': true, | ||
| 230 | + 'lock-scroll': false, | ||
| 231 | + 'close-on-click-modal': false, | ||
| 232 | + ...(this.schema.dialog || {}), | ||
| 233 | + ...this.elDialogProps, | ||
| 234 | + }; | ||
| 235 | + }, | ||
| 236 | + }, | ||
| 237 | + methods: { | ||
| 238 | + getSlot(name) { | ||
| 239 | + return this.$slots[name] || this.$scopedSlots[name]; | ||
| 240 | + }, | ||
| 241 | + getSlotKeys(prefix) { | ||
| 242 | + return this.slotKeys.reduce((result, current) => { | ||
| 243 | + if (current.indexOf(prefix) === 0) { | ||
| 244 | + result.push({ | ||
| 245 | + slot: current, | ||
| 246 | + name: current.substring(prefix.length), | ||
| 247 | + }); | ||
| 248 | + } | ||
| 249 | + return result; | ||
| 250 | + }, []); | ||
| 251 | + }, | ||
| 252 | + // 空Promise | ||
| 253 | + emptyPromise() { | ||
| 254 | + return new Promise(resolve => resolve()); | ||
| 255 | + }, | ||
| 256 | + // 查询 | ||
| 257 | + search() { | ||
| 258 | + if (!this.loading) { | ||
| 259 | + this.loading = true; | ||
| 260 | + const params = { | ||
| 261 | + ...this.valueFilter, | ||
| 262 | + currentPage: this.currentPage, | ||
| 263 | + pageSize: this.pageSize, | ||
| 264 | + }; | ||
| 265 | + const searchAPI = this.apiSearch || this.emptyPromise; | ||
| 266 | + searchAPI(params) | ||
| 267 | + .then(res => { | ||
| 268 | + const response = res || {}; | ||
| 269 | + this.tableData = response[this.schema.listKey || 'list'] || []; | ||
| 270 | + this.total = response[this.schema.totalKey || 'total'] || 0; | ||
| 271 | + }) | ||
| 272 | + .finally(() => { | ||
| 273 | + this.loading = false; | ||
| 274 | + }); | ||
| 275 | + } | ||
| 276 | + }, | ||
| 277 | + // 重置查询 | ||
| 278 | + onSearch() { | ||
| 279 | + this.currentPage = 1; | ||
| 280 | + this.search(); | ||
| 281 | + }, | ||
| 282 | + // 表单提交 | ||
| 283 | + onFormSubmit(value) { | ||
| 284 | + if (this.$listeners['form-submit']) { | ||
| 285 | + this.$emit('form-submit', value); | ||
| 286 | + } else { | ||
| 287 | + this.submitting = true; | ||
| 288 | + let submitAPI = this.apiSubmit || this.emptyPromise; | ||
| 289 | + if (this.elDialogType === 'new') { | ||
| 290 | + submitAPI = this.apiNew || this.apiSubmit || this.emptyPromise; | ||
| 291 | + } else if (this.elDialogType === 'edit') { | ||
| 292 | + submitAPI = this.apiEdit || this.apiSubmit || this.emptyPromise; | ||
| 293 | + } | ||
| 294 | + submitAPI(this.valueForm, { type: this.elDialogType }) | ||
| 295 | + .then(() => { | ||
| 296 | + if (this.$listeners['submit-success']) { | ||
| 297 | + this.$emit('submit-success'); | ||
| 298 | + } else { | ||
| 299 | + this.$message.success('保存成功'); | ||
| 300 | + } | ||
| 301 | + this.closeDialog(); | ||
| 302 | + this.search(); | ||
| 303 | + }) | ||
| 304 | + .finally(() => { | ||
| 305 | + this.submitting = false; | ||
| 306 | + }); | ||
| 307 | + } | ||
| 308 | + }, | ||
| 309 | + // 打开新增 | ||
| 310 | + openNew() { | ||
| 311 | + this.openDialog('new', '新增'); | ||
| 312 | + }, | ||
| 313 | + // 打开编辑 | ||
| 314 | + openEdit(row) { | ||
| 315 | + this.dialogLoading = true; | ||
| 316 | + this.openDialog('edit', '编辑'); | ||
| 317 | + const getRow = () => | ||
| 318 | + new Promise(resolve => { | ||
| 319 | + resolve(cloneDeep(row)); | ||
| 320 | + }); | ||
| 321 | + const getAPI = this.apiGet || getRow; | ||
| 322 | + getAPI(cloneDeep(row)) | ||
| 323 | + .then(result => { | ||
| 324 | + if (result) { | ||
| 325 | + this.$emit('update:value-form', result); | ||
| 326 | + } | ||
| 327 | + }) | ||
| 328 | + .finally(() => { | ||
| 329 | + this.dialogLoading = false; | ||
| 330 | + }); | ||
| 331 | + }, | ||
| 332 | + // 打开详情 | ||
| 333 | + openDetail(row) { | ||
| 334 | + this.dialogLoading = true; | ||
| 335 | + this.openDialog('detail', '详情'); | ||
| 336 | + const getRow = () => | ||
| 337 | + new Promise(resolve => { | ||
| 338 | + resolve(cloneDeep(row)); | ||
| 339 | + }); | ||
| 340 | + const getAPI = this.apiDetail || this.apiGet || getRow; | ||
| 341 | + getAPI(cloneDeep(row)) | ||
| 342 | + .then(result => { | ||
| 343 | + if (result) { | ||
| 344 | + this.detail = result; | ||
| 345 | + this.$emit('update:value-detail', result); | ||
| 346 | + } | ||
| 347 | + }) | ||
| 348 | + .finally(() => { | ||
| 349 | + this.dialogLoading = false; | ||
| 350 | + }); | ||
| 351 | + }, | ||
| 352 | + // 打开弹出框 | ||
| 353 | + openDialog(type, title, config) { | ||
| 354 | + this.elDialogRender = true; | ||
| 355 | + this.elDialogType = type; | ||
| 356 | + this.elDialogTitle = title; | ||
| 357 | + this.elDialogProps = config || {}; | ||
| 358 | + this.visible = true; | ||
| 359 | + this.$emit('dialog-change', type); | ||
| 360 | + }, | ||
| 361 | + // 关闭弹出框 | ||
| 362 | + closeDialog() { | ||
| 363 | + this.visible = false; | ||
| 364 | + }, | ||
| 365 | + // 弹出框显示状态更新 | ||
| 366 | + onVisibleUpdate(visible) { | ||
| 367 | + this.visible = visible; | ||
| 368 | + }, | ||
| 369 | + // 弹出框关闭 | ||
| 370 | + onDialogClose() { | ||
| 371 | + this.elDialogType = 'none'; | ||
| 372 | + this.$emit('dialog-change', 'none'); | ||
| 373 | + }, | ||
| 374 | + // 弹出框关闭动画结束 | ||
| 375 | + onDialogClosed() { | ||
| 376 | + if (this.$refs.form) { | ||
| 377 | + this.$refs.form.resetFields(); | ||
| 378 | + } | ||
| 379 | + this.elDialogRender = false; | ||
| 380 | + this.elDialogProps = {}; | ||
| 381 | + this.$emit('update:value-form', this.cloneDeep(this.originProps).valueForm); | ||
| 382 | + this.$emit('update:value-detail', this.cloneDeep(this.originProps).detailValue); | ||
| 383 | + }, | ||
| 384 | + // 表格选中状态 | ||
| 385 | + onTableSelectionChange(selection, type) { | ||
| 386 | + this.selection = selection; | ||
| 387 | + }, | ||
| 388 | + // 分页-每页个数 | ||
| 389 | + onSizeChange(val) { | ||
| 390 | + this.pageSize = val; | ||
| 391 | + this.currentPage = 1; | ||
| 392 | + this.$nextTick(this.search); | ||
| 393 | + }, | ||
| 394 | + // 分页-当前页数 | ||
| 395 | + onCurrentChange(val) { | ||
| 396 | + this.currentPage = val; | ||
| 397 | + this.$nextTick(this.search); | ||
| 398 | + }, | ||
| 399 | + // 删除数据 | ||
| 400 | + onDelete(selection) { | ||
| 401 | + const loading = this.$loading({ | ||
| 402 | + lock: true, | ||
| 403 | + text: '处理中', | ||
| 404 | + spinner: 'el-icon-loading', | ||
| 405 | + customClass: 'z-loading-toast', | ||
| 406 | + background: 'rgba(0, 0, 0, 0)', | ||
| 407 | + }); | ||
| 408 | + const deleteAPI = this.apiDelete || this.emptyPromise; | ||
| 409 | + deleteAPI(selection) | ||
| 410 | + .then(() => { | ||
| 411 | + this.search(); | ||
| 412 | + if (this.$listeners['delete-success']) { | ||
| 413 | + this.$emit('delete-success'); | ||
| 414 | + } else { | ||
| 415 | + this.$message.success('删除成功'); | ||
| 416 | + } | ||
| 417 | + }) | ||
| 418 | + .finally(() => { | ||
| 419 | + loading.close(); | ||
| 420 | + }); | ||
| 421 | + }, | ||
| 422 | + // 批量删除 | ||
| 423 | + onDeleteMultiple(selection) { | ||
| 424 | + this.$confirm(`是否删除这 [${selection.length}] 项?`, '提示', { | ||
| 425 | + confirmButtonText: '确定', | ||
| 426 | + cancelButtonText: '取消', | ||
| 427 | + type: 'warning', | ||
| 428 | + }) | ||
| 429 | + .then(() => { | ||
| 430 | + this.onDelete(selection); | ||
| 431 | + }) | ||
| 432 | + .catch(() => {}); | ||
| 433 | + }, | ||
| 434 | + }, | ||
| 435 | +}; | ||
| 436 | +</script> |
packages/schema-table/index.vue
| @@ -32,7 +32,7 @@ export default { | @@ -32,7 +32,7 @@ export default { | ||
| 32 | render(h) { | 32 | render(h) { |
| 33 | const schema = this.schema || {}; | 33 | const schema = this.schema || {}; |
| 34 | const _props = schema.props || {}; | 34 | const _props = schema.props || {}; |
| 35 | - const _on = schema.on || {}; | 35 | + const _on = schema.on || this.$listeners || {}; |
| 36 | return h('z-table', { props: { value: this.model, columns: schema.items, ..._props }, on: _on, scopedSlots: this.$scopedSlots }); | 36 | return h('z-table', { props: { value: this.model, columns: schema.items, ..._props }, on: _on, scopedSlots: this.$scopedSlots }); |
| 37 | }, | 37 | }, |
| 38 | }; | 38 | }; |
packages/schema/index.scss
| @@ -1,72 +0,0 @@ | @@ -1,72 +0,0 @@ | ||
| 1 | -.z-schema { | ||
| 2 | - &__header { | ||
| 3 | - margin-bottom: 10px; | ||
| 4 | - } | ||
| 5 | - &__filter { | ||
| 6 | - border: 1px solid #ebeef5; | ||
| 7 | - padding-top: 10px; | ||
| 8 | - border-radius: 4px; | ||
| 9 | - margin-bottom: 10px; | ||
| 10 | - } | ||
| 11 | - &__action { | ||
| 12 | - display: flex; | ||
| 13 | - flex-wrap: wrap; | ||
| 14 | - align-items: center; | ||
| 15 | - justify-content: flex-start; | ||
| 16 | - line-height: 1; | ||
| 17 | - .el-button + .el-button { | ||
| 18 | - margin-left: 0; | ||
| 19 | - } | ||
| 20 | - .el-button { | ||
| 21 | - margin-right: 10px; | ||
| 22 | - margin-bottom: 10px; | ||
| 23 | - } | ||
| 24 | - } | ||
| 25 | - &__table { | ||
| 26 | - &-operation { | ||
| 27 | - display: flex; | ||
| 28 | - flex-wrap: wrap; | ||
| 29 | - align-items: center; | ||
| 30 | - justify-content: flex-start; | ||
| 31 | - .el-button + .el-button { | ||
| 32 | - margin-left: 0; | ||
| 33 | - } | ||
| 34 | - .el-button { | ||
| 35 | - margin-right: 10px; | ||
| 36 | - padding-top: 6px; | ||
| 37 | - padding-bottom: 6px; | ||
| 38 | - } | ||
| 39 | - } | ||
| 40 | - } | ||
| 41 | - &__dialog-button { | ||
| 42 | - display: flex; | ||
| 43 | - align-items: center; | ||
| 44 | - justify-content: center; | ||
| 45 | - padding-top: 10px; | ||
| 46 | - } | ||
| 47 | - &__footer { | ||
| 48 | - margin-top: 10px; | ||
| 49 | - text-align: right; | ||
| 50 | - display: flex; | ||
| 51 | - justify-content: space-between; | ||
| 52 | - align-items: center; | ||
| 53 | - .selection-info { | ||
| 54 | - word-break: break-all; | ||
| 55 | - white-space: nowrap; | ||
| 56 | - font-size: 12px; | ||
| 57 | - color: #606266; | ||
| 58 | - .num { | ||
| 59 | - color: #000; | ||
| 60 | - font-weight: bold; | ||
| 61 | - padding: 0 5px; | ||
| 62 | - font-size: 16px; | ||
| 63 | - } | ||
| 64 | - .el-button { | ||
| 65 | - margin-left: 5px; | ||
| 66 | - } | ||
| 67 | - } | ||
| 68 | - .el-pagination { | ||
| 69 | - flex: auto; | ||
| 70 | - } | ||
| 71 | - } | ||
| 72 | -} | ||
| 73 | \ No newline at end of file | 0 | \ No newline at end of file |
packages/schema/index.vue
| @@ -1,649 +0,0 @@ | @@ -1,649 +0,0 @@ | ||
| 1 | -<style lang="scss"> | ||
| 2 | -@import './index.scss'; | ||
| 3 | -</style> | ||
| 4 | - | ||
| 5 | -<template> | ||
| 6 | - <div class="z-schema"> | ||
| 7 | - <!-- 头部内容 --> | ||
| 8 | - <div v-if="$scopedSlots.header || $slots.header" class="z-schema__header"> | ||
| 9 | - <slot name="header" :filterModel="filterModel" v-bind="_slotScope"></slot> | ||
| 10 | - </div> | ||
| 11 | - <!-- 筛选组件 --> | ||
| 12 | - <div v-if="filter" class="z-schema__filter"> | ||
| 13 | - <z-schema-filter | ||
| 14 | - :value="_filterModel" | ||
| 15 | - :list="filterList || listMap.filter | noRulesFilter" | ||
| 16 | - :size="size" | ||
| 17 | - @input="onFilterInput" | ||
| 18 | - @search="onSearch" | ||
| 19 | - :loading="loading" | ||
| 20 | - v-bind="filterProps" | ||
| 21 | - :params="_slotScope" | ||
| 22 | - ></z-schema-filter> | ||
| 23 | - </div> | ||
| 24 | - <!-- 按钮区 --> | ||
| 25 | - <div v-if="action" class="z-schema__action"> | ||
| 26 | - <slot v-if="hadSlot('action')" name="action" v-bind="_slotScope"></slot> | ||
| 27 | - <template v-else> | ||
| 28 | - <el-button :size="size" type="primary" @click="openNew">新增</el-button> | ||
| 29 | - <el-button :size="size" plain :disabled="selection.length === 0" @click="handleDeleteMul(selection)">删除</el-button> | ||
| 30 | - <slot name="button" v-bind="_slotScope"></slot> | ||
| 31 | - </template> | ||
| 32 | - </div> | ||
| 33 | - <!-- 表格内容 --> | ||
| 34 | - <div class="z-schema__table"> | ||
| 35 | - <z-schema-table | ||
| 36 | - ref="table" | ||
| 37 | - v-model="tableData" | ||
| 38 | - v-loading="loading" | ||
| 39 | - :list="tableList || listMap.table" | ||
| 40 | - :tableProps="{ border: true, 'row-key': 'id', 'highlight-current-row': true, ...tableProps }" | ||
| 41 | - :size="size" | ||
| 42 | - @selection-change="onTableSelectionChange" | ||
| 43 | - @selection="onTableSelection" | ||
| 44 | - > | ||
| 45 | - <slot></slot> | ||
| 46 | - <!-- 表格列内容渲染 --> | ||
| 47 | - <template v-for="(item, index) in renderList"> | ||
| 48 | - <template v-if="$scopedSlots[`cell-${item.fullKey}`]"> | ||
| 49 | - <el-table-column :slot="item.fullKey" v-bind="item" :prop="item.agentKey || item.key" :key="`table-cell-${index}`"> | ||
| 50 | - <template slot-scope="{ row, column, $index }"> | ||
| 51 | - <slot :name="`cell-${item.fullKey}`" v-bind="{ ...item, ..._slotScope }" :row="row" :value="row[item.key]" :column="column" :index="$index"></slot> | ||
| 52 | - </template> | ||
| 53 | - </el-table-column> | ||
| 54 | - </template> | ||
| 55 | - <template v-else-if="$scopedSlots[`render-${item.fullKey}`]"> | ||
| 56 | - <el-table-column :slot="item.fullKey" v-bind="item" :prop="item.agentKey || item.key" :key="`table-render-${index}`"> | ||
| 57 | - <template slot-scope="{ row, column, $index }"> | ||
| 58 | - <slot :name="`render-${item.fullKey}`" v-bind="{ ...item, ..._slotScope }" :row="row" :value="row[item.key]" :column="column" :index="$index"></slot> | ||
| 59 | - </template> | ||
| 60 | - </el-table-column> | ||
| 61 | - </template> | ||
| 62 | - </template> | ||
| 63 | - <slot slot="column-append" name="column-append" v-bind="_slotScope"></slot> | ||
| 64 | - <!-- 表格尾追加操作列 --> | ||
| 65 | - <template #column-end> | ||
| 66 | - <slot slot="column-end" name="column-end" v-bind="_slotScope"></slot> | ||
| 67 | - <el-table-column v-if="operation" prop="$operation" label="操作" v-bind="{ width: 100, fixed: 'right', ...operationProps }"> | ||
| 68 | - <div class="z-schema__table-operation" slot-scope="slotScope"> | ||
| 69 | - <slot name="operation-button" v-bind="{ ..._slotScope, slotScope }"></slot> | ||
| 70 | - <el-button type="text" icon="el-icon-edit" title="编辑" @click="openEdit(slotScope.row)"></el-button> | ||
| 71 | - <el-popconfirm confirmButtonText="确定" cancelButtonText="取消" title="确定删除吗?" placement="top" @confirm="handleDelete([slotScope.row])"> | ||
| 72 | - <el-button slot="reference" type="text" icon="el-icon-delete" title="删除"></el-button> | ||
| 73 | - </el-popconfirm> | ||
| 74 | - <slot name="operation-button-append" v-bind="{ ..._slotScope, slotScope }"></slot> | ||
| 75 | - </div> | ||
| 76 | - </el-table-column> | ||
| 77 | - </template> | ||
| 78 | - </z-schema-table> | ||
| 79 | - </div> | ||
| 80 | - <!-- 底部区域 --> | ||
| 81 | - <div class="z-schema__footer"> | ||
| 82 | - <div v-if="selection.length > 0" class="selection-info"> | ||
| 83 | - <span>已选中</span> | ||
| 84 | - <span class="num">{{ selection.length }}</span> | ||
| 85 | - <span>项</span> | ||
| 86 | - <el-popconfirm confirmButtonText="确定" cancelButtonText="取消" title="确定清除吗?" placement="top" @confirm="clearSelection"> | ||
| 87 | - <el-button slot="reference" :size="size" type="text">清除</el-button> | ||
| 88 | - </el-popconfirm> | ||
| 89 | - </div> | ||
| 90 | - <!-- 分页器 --> | ||
| 91 | - <el-pagination | ||
| 92 | - v-if="pagination" | ||
| 93 | - @size-change="handleSizeChange" | ||
| 94 | - @current-change="handleCurrentChange" | ||
| 95 | - :current-page="currentPage" | ||
| 96 | - :page-sizes="pageSizes" | ||
| 97 | - :page-size="pageSize" | ||
| 98 | - layout="total, sizes, prev, pager, next, jumper" | ||
| 99 | - :total="total" | ||
| 100 | - > | ||
| 101 | - </el-pagination> | ||
| 102 | - </div> | ||
| 103 | - <!-- 弹出框 --> | ||
| 104 | - <el-dialog | ||
| 105 | - :visible.sync="dialogVisible" | ||
| 106 | - :title="dialogTitle" | ||
| 107 | - destroy-on-close | ||
| 108 | - append-to-body | ||
| 109 | - :lock-scroll="false" | ||
| 110 | - :close-on-click-modal="false" | ||
| 111 | - @closed="onDialogClosed" | ||
| 112 | - @close="onDialogClose" | ||
| 113 | - v-bind="_dialogProps" | ||
| 114 | - > | ||
| 115 | - <div v-loading="dialogLoading"> | ||
| 116 | - <!-- 自定义弹出框标题 --> | ||
| 117 | - <slot v-if="hadSlot('dialog-title')" slot="title" name="dialog-title" :dialogType="dialogType" v-bind="_slotScope"></slot> | ||
| 118 | - <template v-if="dialogRender"> | ||
| 119 | - <!-- 自定义弹出框内容 --> | ||
| 120 | - <slot v-if="hadSlot(`dialog-${dialogType}`)" :name="`dialog-${dialogType}`" :model="_formModel" v-bind="_slotScope"></slot> | ||
| 121 | - <template v-else> | ||
| 122 | - <!-- 内置弹出框新增修改表单 --> | ||
| 123 | - <template v-if="['new', 'edit'].includes(dialogType)"> | ||
| 124 | - <z-form | ||
| 125 | - ref="form" | ||
| 126 | - :value="_formModel" | ||
| 127 | - :list="formList || listMap.form" | ||
| 128 | - @input="onFormInput" | ||
| 129 | - @validate="onFormValidate" | ||
| 130 | - v-bind="{ span: 12, 'label-width': '110px', ...formProps }" | ||
| 131 | - :params="_slotScope" | ||
| 132 | - > | ||
| 133 | - <!-- 表单自定义插槽 --> | ||
| 134 | - <template v-for="item in renderList"> | ||
| 135 | - <template v-if="$scopedSlots[`form-${item.fullKey}`]"> | ||
| 136 | - <slot :slot="item.fullKey" :name="`form-${item.fullKey}`" :value="get(_formModel, item.fullKey)" :row="_formModel" :model="_formModel" v-bind="_slotScope"></slot> | ||
| 137 | - </template> | ||
| 138 | - </template> | ||
| 139 | - </z-form> | ||
| 140 | - </template> | ||
| 141 | - <!-- 内置弹出框详情表单 --> | ||
| 142 | - <template v-else> | ||
| 143 | - <z-form | ||
| 144 | - ref="form" | ||
| 145 | - class="z-schema__view" | ||
| 146 | - :value="_formModel" | ||
| 147 | - :list="viewList || listMap.form | viewTypeFilter | noRulesFilter" | ||
| 148 | - v-bind="{ span: 12, 'label-width': '110px', ...viewProps }" | ||
| 149 | - :params="_slotScope" | ||
| 150 | - > | ||
| 151 | - <!-- 详情自定义插槽渲染 --> | ||
| 152 | - <template v-for="item in renderList"> | ||
| 153 | - <template v-if="$scopedSlots[`view-${item.fullKey}`]"> | ||
| 154 | - <slot :slot="item.fullKey" :name="`view-${item.fullKey}`" :value="get(_formModel, item.fullKey)" :row="_formModel" :model="_formModel" v-bind="_slotScope"></slot> | ||
| 155 | - </template> | ||
| 156 | - <template v-else-if="$scopedSlots[`render-${item.fullKey}`]"> | ||
| 157 | - <slot :slot="item.fullKey" :name="`render-${item.fullKey}`" :value="get(_formModel, item.fullKey)" :row="_formModel" :model="_formModel" v-bind="_slotScope"></slot> | ||
| 158 | - </template> | ||
| 159 | - </template> | ||
| 160 | - </z-form> | ||
| 161 | - </template> | ||
| 162 | - </template> | ||
| 163 | - <!-- 内置弹出框新增修改按钮 --> | ||
| 164 | - <div class="z-schema__dialog-button" v-if="['new', 'edit'].includes(dialogType)"> | ||
| 165 | - <el-button :size="size" type="primary" @click="handleConfirm" :loading="submitting">确定</el-button> | ||
| 166 | - <el-button :size="size" plain @click="closeDialog">取消</el-button> | ||
| 167 | - </div> | ||
| 168 | - </template> | ||
| 169 | - </div> | ||
| 170 | - </el-dialog> | ||
| 171 | - </div> | ||
| 172 | -</template> | ||
| 173 | - | ||
| 174 | -<script> | ||
| 175 | -import { cloneDeep, get } from '../utils'; | ||
| 176 | -import { clear } from '../utils/param'; | ||
| 177 | - | ||
| 178 | -let propsMap = {}; | ||
| 179 | -const propsKeys = ['tableProps', 'filterProps', 'formProps', 'viewProps', 'dialogProps', 'operationProps']; | ||
| 180 | -propsKeys.forEach(key => { | ||
| 181 | - propsMap[key] = { | ||
| 182 | - type: Object, | ||
| 183 | - default: () => ({}), | ||
| 184 | - }; | ||
| 185 | -}); | ||
| 186 | -const apiKeys = ['searchApi', 'submitApi', 'addApi', 'modifyApi', 'getApi', 'viewApi', 'deleteApi']; | ||
| 187 | -apiKeys.forEach(key => { | ||
| 188 | - propsMap[key] = { | ||
| 189 | - type: Function, | ||
| 190 | - }; | ||
| 191 | -}); | ||
| 192 | -const blockKeys = ['filter', 'action', 'pagination', 'operation']; | ||
| 193 | -blockKeys.forEach(key => { | ||
| 194 | - propsMap[key] = { | ||
| 195 | - type: Boolean, | ||
| 196 | - default: true, | ||
| 197 | - }; | ||
| 198 | -}); | ||
| 199 | - | ||
| 200 | -export default { | ||
| 201 | - name: 'Schema', | ||
| 202 | - props: { | ||
| 203 | - ...propsMap, | ||
| 204 | - list: Array, | ||
| 205 | - filterList: Array, | ||
| 206 | - tableList: Array, | ||
| 207 | - formList: Array, | ||
| 208 | - viewList: Array, | ||
| 209 | - size: { | ||
| 210 | - type: String, | ||
| 211 | - default: 'mini', | ||
| 212 | - }, | ||
| 213 | - formModel: Object, | ||
| 214 | - filterModel: Object, | ||
| 215 | - auto: Boolean, | ||
| 216 | - realSelection: Boolean, | ||
| 217 | - url: String, // 请求地址 | ||
| 218 | - http: Function, // http库 | ||
| 219 | - alias: Object, // 别名配置 | ||
| 220 | - }, | ||
| 221 | - data() { | ||
| 222 | - return { | ||
| 223 | - filterForm: {}, | ||
| 224 | - editForm: {}, | ||
| 225 | - dialogVisible: false, | ||
| 226 | - dialogRender: true, | ||
| 227 | - dialogType: 'none', | ||
| 228 | - dialogLoading: false, | ||
| 229 | - dialogTitle: '', | ||
| 230 | - dialogPropsHack: {}, | ||
| 231 | - currentPage: 1, | ||
| 232 | - pageSize: 10, | ||
| 233 | - total: 0, | ||
| 234 | - pageSizes: [10, 20, 50], | ||
| 235 | - tableData: [], | ||
| 236 | - submitting: false, | ||
| 237 | - loading: false, | ||
| 238 | - selection: [], | ||
| 239 | - }; | ||
| 240 | - }, | ||
| 241 | - created() { | ||
| 242 | - if (this.auto) { | ||
| 243 | - this.search(); | ||
| 244 | - } | ||
| 245 | - }, | ||
| 246 | - filters: { | ||
| 247 | - // 无规则过滤器,过滤掉筛选条件表单中的必填规则等 | ||
| 248 | - noRulesFilter(val = []) { | ||
| 249 | - let list = cloneDeep(val); | ||
| 250 | - const clearRules = list => { | ||
| 251 | - list.forEach(item => { | ||
| 252 | - if (item.list) { | ||
| 253 | - clearRules(item.list); | ||
| 254 | - } else { | ||
| 255 | - delete item.rules; | ||
| 256 | - } | ||
| 257 | - }); | ||
| 258 | - }; | ||
| 259 | - clearRules(list); | ||
| 260 | - return list; | ||
| 261 | - }, | ||
| 262 | - // 详情类型过滤器 | ||
| 263 | - viewTypeFilter(val = []) { | ||
| 264 | - let list = cloneDeep(val); | ||
| 265 | - const clearRules = list => { | ||
| 266 | - list.forEach(item => { | ||
| 267 | - item.type = (h, { model, config }) => h('span', config, model[item.key]); | ||
| 268 | - }); | ||
| 269 | - }; | ||
| 270 | - clearRules(list); | ||
| 271 | - return list; | ||
| 272 | - }, | ||
| 273 | - }, | ||
| 274 | - computed: { | ||
| 275 | - listMap() { | ||
| 276 | - // 默认作用域 | ||
| 277 | - const LIST_SPACE = ['filter', 'form', 'table']; | ||
| 278 | - const array = { | ||
| 279 | - filter: [], // 筛选 | ||
| 280 | - form: [], // 表单 | ||
| 281 | - table: [], // 表格 | ||
| 282 | - }; | ||
| 283 | - this.list.forEach(item => { | ||
| 284 | - // 可以在列表中通过include或exclude设置当前配置的作用域 | ||
| 285 | - const { include = LIST_SPACE, exclude = [] } = item; | ||
| 286 | - // 判断include | ||
| 287 | - let _inclue = []; | ||
| 288 | - if (include instanceof String || typeof include === 'string') { | ||
| 289 | - _inclue = [include]; | ||
| 290 | - } else if (include instanceof Array && typeof include === 'object') { | ||
| 291 | - _inclue = include; | ||
| 292 | - } | ||
| 293 | - // 判断exclude转换为include的情况 | ||
| 294 | - let _exclude_include = []; | ||
| 295 | - if (exclude instanceof String || typeof exclude === 'string') { | ||
| 296 | - _exclude_include = LIST_SPACE.filter(item => item !== exclude); | ||
| 297 | - } else if (exclude instanceof Array && typeof exclude === 'object') { | ||
| 298 | - _exclude_include = LIST_SPACE.filter(item => !exclude.includes(item)); | ||
| 299 | - } | ||
| 300 | - // 作用域交集 | ||
| 301 | - const _intersection = _inclue.filter(v => _exclude_include.includes(v)); | ||
| 302 | - // 返回改配置项的作用域 | ||
| 303 | - const _list_space = cloneDeep(_intersection); | ||
| 304 | - // 将配置项按需分配至各作用域下 | ||
| 305 | - _list_space.forEach(name => { | ||
| 306 | - array[name].push({ ...item, ...(item[name] || {}) }); | ||
| 307 | - }); | ||
| 308 | - }); | ||
| 309 | - return array; | ||
| 310 | - }, | ||
| 311 | - renderList() { | ||
| 312 | - // 深度克隆传入的列表,避免原始值被修改 | ||
| 313 | - const newList = cloneDeep(this.list); | ||
| 314 | - // 生成列表值的全路径key,即列表项为对象时,对象内的key与上一级的key合并作为全路径key | ||
| 315 | - const generateFullKey = (list, parentKey) => { | ||
| 316 | - list.forEach(item => { | ||
| 317 | - if (item.group && item.list) { | ||
| 318 | - if (item.group.key) { | ||
| 319 | - item.fullKey = `${parentKey ? `${parentKey}-${item.group.key}` : item.group.key}`; | ||
| 320 | - } else { | ||
| 321 | - item.fullKey = parentKey || item.key; | ||
| 322 | - } | ||
| 323 | - generateFullKey(item.list, item.fullKey); | ||
| 324 | - } else { | ||
| 325 | - item.fullKey = `${parentKey ? `${parentKey}-${item.key}` : item.key}`; | ||
| 326 | - } | ||
| 327 | - }); | ||
| 328 | - }; | ||
| 329 | - // 生成fullKey | ||
| 330 | - generateFullKey(newList); | ||
| 331 | - return newList; | ||
| 332 | - }, | ||
| 333 | - _filterModel() { | ||
| 334 | - return this.filterModel || this.filterForm || {}; | ||
| 335 | - }, | ||
| 336 | - _formModel() { | ||
| 337 | - return this.formModel || this.editForm || {}; | ||
| 338 | - }, | ||
| 339 | - _slotScope() { | ||
| 340 | - return { | ||
| 341 | - handleSearch: this.search, | ||
| 342 | - openDialog: this.openDialog, | ||
| 343 | - closeDialog: this.closeDialog, | ||
| 344 | - openView: this.openView, | ||
| 345 | - openEdit: this.openEdit, | ||
| 346 | - openNew: this.openNew, | ||
| 347 | - handleDelete: this.handleDelete, | ||
| 348 | - handleDeleteMul: this.handleDeleteMul, | ||
| 349 | - size: this.size, | ||
| 350 | - dialogType: this.dialogType, | ||
| 351 | - selection: this.selection, | ||
| 352 | - }; | ||
| 353 | - }, | ||
| 354 | - _alias() { | ||
| 355 | - const alias = this.alias; | ||
| 356 | - const zAlias = this.zAlias; | ||
| 357 | - if (alias && zAlias) { | ||
| 358 | - return { ...zAlias, ...alias }; | ||
| 359 | - } | ||
| 360 | - return this.alias || this.zAlias || {}; | ||
| 361 | - }, | ||
| 362 | - _dialogProps() { | ||
| 363 | - return { | ||
| 364 | - ...this.dialogProps, | ||
| 365 | - ...this.dialogPropsHack, | ||
| 366 | - }; | ||
| 367 | - }, | ||
| 368 | - }, | ||
| 369 | - methods: { | ||
| 370 | - get, | ||
| 371 | - // 空Promise | ||
| 372 | - emptyPromise() { | ||
| 373 | - return new Promise(resolve => resolve()); | ||
| 374 | - }, | ||
| 375 | - // 设置表格选中行 | ||
| 376 | - toggleRowSelection() { | ||
| 377 | - this.tableData.forEach(row => { | ||
| 378 | - if (this.selection.find(item => item.id === row.id)) { | ||
| 379 | - this.$refs.table && this.$refs.table.toggleRowSelection(row); | ||
| 380 | - } | ||
| 381 | - }); | ||
| 382 | - }, | ||
| 383 | - // 表格选中状态 | ||
| 384 | - onTableSelectionChange(selection, type) { | ||
| 385 | - if (this.realSelection) { | ||
| 386 | - if (type === 'check') { | ||
| 387 | - const result = this.selection || []; | ||
| 388 | - selection.forEach(item => { | ||
| 389 | - if (!result.find(i => i.id === item.id)) { | ||
| 390 | - result.push(item); | ||
| 391 | - } | ||
| 392 | - }); | ||
| 393 | - this.selection = result; | ||
| 394 | - } else if (type === 'uncheck') { | ||
| 395 | - selection.forEach(i => { | ||
| 396 | - this.selection = this.selection.filter(item => item.id !== i.id); | ||
| 397 | - }); | ||
| 398 | - } | ||
| 399 | - } | ||
| 400 | - }, | ||
| 401 | - // 表格选中 | ||
| 402 | - onTableSelection(selection) { | ||
| 403 | - if (!this.realSelection) { | ||
| 404 | - this.selection = selection; | ||
| 405 | - } | ||
| 406 | - }, | ||
| 407 | - // 清除表格选中 | ||
| 408 | - clearSelection() { | ||
| 409 | - this.$refs.table && this.$refs.table.clearSelection(); | ||
| 410 | - this.selection = []; | ||
| 411 | - }, | ||
| 412 | - // 内置搜索接口 | ||
| 413 | - _searchAPI(params) { | ||
| 414 | - if (this.url && (this.http || this.zHttp)) { | ||
| 415 | - const _http = this.http || this.zHttp; | ||
| 416 | - return _http({ url: `${clear(this.url)}/${this._alias.pageUrl || 'page'}`, params }); | ||
| 417 | - } | ||
| 418 | - return undefined; | ||
| 419 | - }, | ||
| 420 | - // 重置查询 | ||
| 421 | - onSearch() { | ||
| 422 | - this.currentPage = 1; | ||
| 423 | - this.search(); | ||
| 424 | - }, | ||
| 425 | - // 搜索 | ||
| 426 | - async search() { | ||
| 427 | - this.loading = true; | ||
| 428 | - const params = { | ||
| 429 | - ...this._filterModel, | ||
| 430 | - currentPage: this.currentPage, | ||
| 431 | - pageSize: this.pageSize, | ||
| 432 | - }; | ||
| 433 | - const searchAPI = this.searchApi || this._searchAPI || this.emptyPromise; | ||
| 434 | - await searchAPI(params) | ||
| 435 | - .then(res => { | ||
| 436 | - const response = res || {}; | ||
| 437 | - this.tableData = response[this._alias.list || 'list'] || []; | ||
| 438 | - this.total = response[this._alias.total || 'total'] || 0; | ||
| 439 | - this.$nextTick(this.toggleRowSelection); | ||
| 440 | - }) | ||
| 441 | - .catch(() => { | ||
| 442 | - this.$message.error('查询失败'); | ||
| 443 | - }); | ||
| 444 | - this.loading = false; | ||
| 445 | - }, | ||
| 446 | - // 更新筛选model | ||
| 447 | - onFilterInput(val) { | ||
| 448 | - this.filterForm = val || {}; | ||
| 449 | - this.$emit('update:filterModel', val || {}); | ||
| 450 | - }, | ||
| 451 | - // 更新表单model | ||
| 452 | - onFormInput(val) { | ||
| 453 | - this.editForm = val || {}; | ||
| 454 | - this.$emit('update:formModel', val || {}); | ||
| 455 | - }, | ||
| 456 | - // 内置新增保存接口 | ||
| 457 | - _addAPI(data) { | ||
| 458 | - if (this.url && (this.http || this.zHttp)) { | ||
| 459 | - const _http = this.http || this.zHttp; | ||
| 460 | - return _http({ url: `${clear(this.url)}/${this._alias.addUrl || 'add'}`, method: 'post', data }); | ||
| 461 | - } | ||
| 462 | - return undefined; | ||
| 463 | - }, | ||
| 464 | - // 内置修改保存接口 | ||
| 465 | - _modifyAPI(data) { | ||
| 466 | - if (this.url && (this.http || this.zHttp)) { | ||
| 467 | - const _http = this.http || this.zHttp; | ||
| 468 | - return _http({ url: `${clear(this.url)}/${this._alias.modifyUrl || 'modify'}`, method: 'post', data }); | ||
| 469 | - } | ||
| 470 | - return undefined; | ||
| 471 | - }, | ||
| 472 | - // 表单提交且通过校验 | ||
| 473 | - async onFormValidate(valid, model) { | ||
| 474 | - if (valid) { | ||
| 475 | - this.submitting = true; | ||
| 476 | - let submitAPI = this.submitApi || this.emptyPromise; | ||
| 477 | - if (this.dialogType === 'new') { | ||
| 478 | - submitAPI = this.addApi || this.submitApi || this._addAPI || this.emptyPromise; | ||
| 479 | - } else if (this.dialogType === 'edit') { | ||
| 480 | - submitAPI = this.modifyApi || this.submitApi || this._modifyAPI || this.emptyPromise; | ||
| 481 | - } | ||
| 482 | - submitAPI(model, { type: this.dialogType }) | ||
| 483 | - .then(() => { | ||
| 484 | - this.$message.success('保存成功'); | ||
| 485 | - this.closeDialog(); | ||
| 486 | - this.search(); | ||
| 487 | - }) | ||
| 488 | - .catch(() => { | ||
| 489 | - this.$message.error('保存失败'); | ||
| 490 | - }) | ||
| 491 | - .finally(() => { | ||
| 492 | - this.submitting = false; | ||
| 493 | - }); | ||
| 494 | - } | ||
| 495 | - }, | ||
| 496 | - // 表单按钮确定 | ||
| 497 | - handleConfirm() { | ||
| 498 | - this.$refs.form && this.$refs.form.validate(); | ||
| 499 | - }, | ||
| 500 | - // 表单按钮取消 | ||
| 501 | - handleCancel() { | ||
| 502 | - this.closeDialog(); | ||
| 503 | - }, | ||
| 504 | - // 查询是否有某个插槽 | ||
| 505 | - hadSlot(name) { | ||
| 506 | - return !!this.$slots[name] || !!this.$scopedSlots[name]; | ||
| 507 | - }, | ||
| 508 | - // 打开新增弹出框 | ||
| 509 | - openNew() { | ||
| 510 | - this.openDialog('new', '新增'); | ||
| 511 | - }, | ||
| 512 | - // 内置查询详情接口 | ||
| 513 | - _getAPI(row) { | ||
| 514 | - if (this.url && (this.http || this.zHttp)) { | ||
| 515 | - const _http = this.http || this.zHttp; | ||
| 516 | - const _getKey = this._alias.getKey || this._alias.primaryKey || 'id'; | ||
| 517 | - const _resultKey = this._alias.result || 'result'; | ||
| 518 | - return _http({ url: `${clear(this.url)}/${this._alias.getUrl || 'queryById'}`, params: { [_getKey]: row[_getKey] } }).then(response => response[_resultKey] || {}); | ||
| 519 | - } | ||
| 520 | - return undefined; | ||
| 521 | - }, | ||
| 522 | - // 打开编辑弹出框 | ||
| 523 | - openEdit(row) { | ||
| 524 | - this.dialogLoading = true; | ||
| 525 | - this.openDialog('edit', '编辑'); | ||
| 526 | - const getRow = () => | ||
| 527 | - new Promise(resolve => { | ||
| 528 | - resolve(row); | ||
| 529 | - }); | ||
| 530 | - const getAPI = this.getApi || this._getAPI || getRow; | ||
| 531 | - getAPI(row) | ||
| 532 | - .then(result => { | ||
| 533 | - this.editForm = result; | ||
| 534 | - this.$emit('update:formModel', result || {}); | ||
| 535 | - }) | ||
| 536 | - .finally(() => { | ||
| 537 | - this.dialogLoading = false; | ||
| 538 | - }); | ||
| 539 | - }, | ||
| 540 | - // 内置查询详情接口 | ||
| 541 | - _viewAPI(row) { | ||
| 542 | - if (this.url && (this.http || this.zHttp)) { | ||
| 543 | - const _http = this.http || this.zHttp; | ||
| 544 | - const _viewKey = this._alias.viewKey || this._alias.getKey || this._alias.primaryKey || 'id'; | ||
| 545 | - const _resultKey = this._alias.result || 'result'; | ||
| 546 | - return _http({ url: `${clear(this.url)}/${this._alias.getUrl || 'queryById'}`, params: { [_viewKey]: row[_viewKey] } }).then(response => response[_resultKey] || {}); | ||
| 547 | - } | ||
| 548 | - return undefined; | ||
| 549 | - }, | ||
| 550 | - // 打开详情弹出框 | ||
| 551 | - openView(row) { | ||
| 552 | - this.dialogLoading = true; | ||
| 553 | - this.openDialog('view', '详情'); | ||
| 554 | - const getRow = () => | ||
| 555 | - new Promise(resolve => { | ||
| 556 | - resolve(row); | ||
| 557 | - }); | ||
| 558 | - const viewAPI = this.viewApi || this.getApi || this._viewAPI || this._getAPI || getRow; | ||
| 559 | - viewAPI(row) | ||
| 560 | - .then(result => { | ||
| 561 | - this.editForm = result; | ||
| 562 | - this.$emit('update:formModel', result || {}); | ||
| 563 | - }) | ||
| 564 | - .finally(() => { | ||
| 565 | - this.dialogLoading = false; | ||
| 566 | - }); | ||
| 567 | - }, | ||
| 568 | - // 内置删除接口 | ||
| 569 | - _deleteAPI(keys) { | ||
| 570 | - if (this.url && (this.http || this.zHttp)) { | ||
| 571 | - const _http = this.http || this.zHttp; | ||
| 572 | - return _http({ url: `${clear(this.url)}/${this._alias.modifyUrl || 'delete'}`, method: 'post', data: keys }); | ||
| 573 | - } | ||
| 574 | - return undefined; | ||
| 575 | - }, | ||
| 576 | - // 删除 | ||
| 577 | - handleDelete(selection) { | ||
| 578 | - const loading = this.$loading({ | ||
| 579 | - text: '处理中', | ||
| 580 | - spinner: 'el-icon-loading', | ||
| 581 | - background: 'rgba(255, 255, 255, 0.5)', | ||
| 582 | - }); | ||
| 583 | - const deleteAPI = this.deleteApi || this._deleteAPI || this.emptyPromise; | ||
| 584 | - const _deleteKey = this._alias.deleteKey || this._alias.primaryKey || 'id'; | ||
| 585 | - const keys = selection.map(i => i[_deleteKey]); | ||
| 586 | - deleteAPI(keys) | ||
| 587 | - .then(() => { | ||
| 588 | - this.search(); | ||
| 589 | - this.$message.success('删除成功'); | ||
| 590 | - }) | ||
| 591 | - .finally(() => { | ||
| 592 | - loading.close(); | ||
| 593 | - }); | ||
| 594 | - }, | ||
| 595 | - // 批量删除 | ||
| 596 | - handleDeleteMul(selection) { | ||
| 597 | - this.$confirm(`是否删除这 [${selection.length}] 项?`, '提示', { | ||
| 598 | - confirmButtonText: '确定', | ||
| 599 | - cancelButtonText: '取消', | ||
| 600 | - type: 'warning', | ||
| 601 | - }) | ||
| 602 | - .then(() => { | ||
| 603 | - this.handleDelete(selection); | ||
| 604 | - }) | ||
| 605 | - .catch(() => {}); | ||
| 606 | - }, | ||
| 607 | - // 打开弹出框 | ||
| 608 | - openDialog(type, title, config) { | ||
| 609 | - this.dialogVisible = true; | ||
| 610 | - this.dialogRender = true; | ||
| 611 | - this.dialogType = type; | ||
| 612 | - this.dialogTitle = title; | ||
| 613 | - this.dialogPropsHack = config || {}; | ||
| 614 | - this.$emit('dialog-change', type); | ||
| 615 | - }, | ||
| 616 | - // 关闭弹出框 | ||
| 617 | - closeDialog() { | ||
| 618 | - this.dialogVisible = false; | ||
| 619 | - }, | ||
| 620 | - // 清空表单 | ||
| 621 | - clearEditForm() { | ||
| 622 | - this.editForm = {}; | ||
| 623 | - this.$emit('update:formModel', {}); | ||
| 624 | - }, | ||
| 625 | - // 弹出框关闭 | ||
| 626 | - onDialogClose() { | ||
| 627 | - this.dialogType = 'none'; | ||
| 628 | - this.dialogRender = false; | ||
| 629 | - this.$emit('dialog-change', 'none'); | ||
| 630 | - }, | ||
| 631 | - // 弹出框关闭动画结束 | ||
| 632 | - onDialogClosed() { | ||
| 633 | - this.clearEditForm(); | ||
| 634 | - this.dialogPropsHack = {}; | ||
| 635 | - }, | ||
| 636 | - // 分页-每页个数 | ||
| 637 | - handleSizeChange(val) { | ||
| 638 | - this.pageSize = val; | ||
| 639 | - this.currentPage = 1; | ||
| 640 | - this.$nextTick(this.search); | ||
| 641 | - }, | ||
| 642 | - // 分页-当前页数 | ||
| 643 | - handleCurrentChange(val) { | ||
| 644 | - this.currentPage = val; | ||
| 645 | - this.$nextTick(this.search); | ||
| 646 | - }, | ||
| 647 | - }, | ||
| 648 | -}; | ||
| 649 | -</script> |
packages/table/editable.vue
| @@ -27,7 +27,15 @@ | @@ -27,7 +27,15 @@ | ||
| 27 | </style> | 27 | </style> |
| 28 | 28 | ||
| 29 | <template> | 29 | <template> |
| 30 | - <el-table :data="tableData | tableDataFilter" :size="tableSize" v-bind="bindProps" @header-click="onHeaderClick" @cell-click="onCellClick" @cell-dblclick="onCellDblclick"> | 30 | + <el-table |
| 31 | + :data="tableData | tableDataFilter" | ||
| 32 | + :size="tableSize" | ||
| 33 | + v-bind="bindProps" | ||
| 34 | + v-on="$listeners" | ||
| 35 | + @header-click="onHeaderClick" | ||
| 36 | + @cell-click="onCellClick" | ||
| 37 | + @cell-dblclick="onCellDblclick" | ||
| 38 | + > | ||
| 31 | <slot name="left"></slot> | 39 | <slot name="left"></slot> |
| 32 | <template v-for="(item, index) in columns"> | 40 | <template v-for="(item, index) in columns"> |
| 33 | <el-table-column v-bind="item" :key="index"> | 41 | <el-table-column v-bind="item" :key="index"> |
| @@ -48,6 +56,9 @@ | @@ -48,6 +56,9 @@ | ||
| 48 | <template v-if="$scopedSlots[`cell-${item.prop}`]"> | 56 | <template v-if="$scopedSlots[`cell-${item.prop}`]"> |
| 49 | <slot :name="`cell-${item.prop}`" :value="row[column.property]" :row="row" :index="$index"></slot> | 57 | <slot :name="`cell-${item.prop}`" :value="row[column.property]" :row="row" :index="$index"></slot> |
| 50 | </template> | 58 | </template> |
| 59 | + <template v-else-if="item.render && typeof item.render === 'function'" #default="{ row, column, $index }"> | ||
| 60 | + <cell-render :item="item" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></cell-render> | ||
| 61 | + </template> | ||
| 51 | </cell-editor> | 62 | </cell-editor> |
| 52 | </template> | 63 | </template> |
| 53 | </el-table-column> | 64 | </el-table-column> |
packages/table/index.js
| @@ -22,8 +22,6 @@ export default { | @@ -22,8 +22,6 @@ export default { | ||
| 22 | ...tableProps, | 22 | ...tableProps, |
| 23 | }, | 23 | }, |
| 24 | render(h) { | 24 | render(h) { |
| 25 | - const scopedSlots = this.$scopedSlots; | ||
| 26 | - const listeners = this.$listeners; | ||
| 27 | - return h(`z-table-${this.editable ? 'editable' : 'normal'}`, { props: { ...this._props }, scopedSlots, on: listeners }); | 25 | + return h(`z-table-${this.editable ? 'editable' : 'normal'}`, { props: { ...this._props }, scopedSlots: this.$scopedSlots, on: this.$listeners }); |
| 28 | }, | 26 | }, |
| 29 | }; | 27 | }; |
packages/table/normal.vue
| 1 | <template> | 1 | <template> |
| 2 | - <el-table :data="tableData" :size="tableSize" v-bind="bindProps"> | 2 | + <el-table :data="tableData" :size="tableSize" v-bind="bindProps" v-on="$listeners"> |
| 3 | <slot name="left"></slot> | 3 | <slot name="left"></slot> |
| 4 | <template v-for="(item, index) in columns"> | 4 | <template v-for="(item, index) in columns"> |
| 5 | <el-table-column v-bind="item" :key="index"> | 5 | <el-table-column v-bind="item" :key="index"> |
| @@ -8,7 +8,7 @@ | @@ -8,7 +8,7 @@ | ||
| 8 | <slot :name="`cell-${item.prop}`" :value="row[column.property]" :row="row" :index="$index"></slot> | 8 | <slot :name="`cell-${item.prop}`" :value="row[column.property]" :row="row" :index="$index"></slot> |
| 9 | </template> | 9 | </template> |
| 10 | <template v-else-if="item.render && typeof item.render === 'function'" #default="{ row, column, $index }"> | 10 | <template v-else-if="item.render && typeof item.render === 'function'" #default="{ row, column, $index }"> |
| 11 | - <cell-render :item="item" :value="row[item.prop]" :row="row" :column="column" :index="$index"></cell-render> | 11 | + <cell-render :item="item" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></cell-render> |
| 12 | </template> | 12 | </template> |
| 13 | </el-table-column> | 13 | </el-table-column> |
| 14 | </template> | 14 | </template> |
| @@ -19,6 +19,7 @@ | @@ -19,6 +19,7 @@ | ||
| 19 | 19 | ||
| 20 | <script> | 20 | <script> |
| 21 | import tableProps from './props'; | 21 | import tableProps from './props'; |
| 22 | +import { get } from '../utils'; | ||
| 22 | 23 | ||
| 23 | export default { | 24 | export default { |
| 24 | name: 'TableNormal', | 25 | name: 'TableNormal', |
| @@ -87,5 +88,8 @@ export default { | @@ -87,5 +88,8 @@ export default { | ||
| 87 | return props; | 88 | return props; |
| 88 | }, | 89 | }, |
| 89 | }, | 90 | }, |
| 91 | + methods: { | ||
| 92 | + get, | ||
| 93 | + }, | ||
| 90 | }; | 94 | }; |
| 91 | </script> | 95 | </script> |
| @@ -0,0 +1,15 @@ | @@ -0,0 +1,15 @@ | ||
| 1 | +export const filterout = (schema, key) => { | ||
| 2 | + if (schema.items) { | ||
| 3 | + schema.items = schema.items.map(item => { | ||
| 4 | + if (Array.isArray(key)) { | ||
| 5 | + key.forEach(k => { | ||
| 6 | + delete item[k]; | ||
| 7 | + }); | ||
| 8 | + } else { | ||
| 9 | + delete item[key]; | ||
| 10 | + } | ||
| 11 | + return item; | ||
| 12 | + }); | ||
| 13 | + } | ||
| 14 | + return schema; | ||
| 15 | +}; |