Commit 57f06f4f36d0623a9f6a86c7e803eef6824b12ab

Authored by 刘汉宸
1 parent db24c087

feat: 修改SchemaPage组件

examples/router/routes.js
1 1 import DefaultLayout from '@/views/layout/default';
2 2 import ComponentLayout from '@/views/layout/component';
3 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 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 140 meta: { title: 'Schema 介绍' },
140 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 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 172 const _pages = [
171 173 {
... ... @@ -190,6 +192,14 @@ const _pages = [
190 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 203 path: '/develop',
194 204 name: 'develop',
195 205 meta: { title: '开发', path: '/develop' },
... ... @@ -204,6 +214,7 @@ export const guides = _guides;
204 214 export const components = _components;
205 215 export const others = _others;
206 216 export const develops = _develops;
  217 +export const designs = _designs;
207 218  
208 219 export default [
209 220 { path: '*', redirect: '/404', hidden: true },
... ...
examples/views/docs/component/schema-page.md 0 → 100644
... ... @@ -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   -# 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 | - | []
examples/views/docs/design/table.md 0 → 100644
No preview for this file type
examples/views/docs/develop/schema/schema-form.md
... ... @@ -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 0 \ No newline at end of file
examples/views/docs/develop/schema/schema-page.md
... ... @@ -1,5 +0,0 @@
1   -# Schema 方案开发
2   -
3   -通过配置JSON Schema的方式快速生成一些业务组件
4   -
5   -## 基础用法
6 0 \ No newline at end of file
examples/views/docs/develop/schema/schema-table.md
... ... @@ -1,5 +0,0 @@
1   -# Schema 方案开发
2   -
3   -通过配置JSON Schema的方式快速生成一些业务组件
4   -
5   -## 基础用法
6 0 \ No newline at end of file
examples/views/layout/design.vue 0 → 100644
... ... @@ -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 4 props: {
5 5 label: String,
6 6 labelWidth: String,
7   - value: [Number, String],
  7 + value: [Number, String, Array, Object],
8 8 prop: String,
9 9 span: {
10 10 type: [Number, String],
... ...
packages/mixins/origin.js 0 → 100644
... ... @@ -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 34 const item = props.item || {};
35 35 let content = [];
36 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 40 if (item.children) {
40 41 if (Array.isArray(item.children)) {
... ...
packages/schema-page/index copy.scss 0 → 100644
... ... @@ -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 73 \ No newline at end of file
... ...
packages/schema-page/index copy.vue 0 → 100644
... ... @@ -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>
... ...
packages/schema-page/index.scss 0 → 100644
... ... @@ -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 106 \ No newline at end of file
... ...
packages/schema-page/index.vue 0 → 100644
... ... @@ -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 32 render(h) {
33 33 const schema = this.schema || {};
34 34 const _props = schema.props || {};
35   - const _on = schema.on || {};
  35 + const _on = schema.on || this.$listeners || {};
36 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   -.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 0 \ No newline at end of file
packages/schema/index.vue
... ... @@ -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 27 </style>
28 28  
29 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 39 <slot name="left"></slot>
32 40 <template v-for="(item, index) in columns">
33 41 <el-table-column v-bind="item" :key="index">
... ... @@ -48,6 +56,9 @@
48 56 <template v-if="$scopedSlots[`cell-${item.prop}`]">
49 57 <slot :name="`cell-${item.prop}`" :value="row[column.property]" :row="row" :index="$index"></slot>
50 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 62 </cell-editor>
52 63 </template>
53 64 </el-table-column>
... ...
packages/table/index.js
... ... @@ -22,8 +22,6 @@ export default {
22 22 ...tableProps,
23 23 },
24 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 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 3 <slot name="left"></slot>
4 4 <template v-for="(item, index) in columns">
5 5 <el-table-column v-bind="item" :key="index">
... ... @@ -8,7 +8,7 @@
8 8 <slot :name="`cell-${item.prop}`" :value="row[column.property]" :row="row" :index="$index"></slot>
9 9 </template>
10 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 12 </template>
13 13 </el-table-column>
14 14 </template>
... ... @@ -19,6 +19,7 @@
19 19  
20 20 <script>
21 21 import tableProps from './props';
  22 +import { get } from '../utils';
22 23  
23 24 export default {
24 25 name: 'TableNormal',
... ... @@ -87,5 +88,8 @@ export default {
87 88 return props;
88 89 },
89 90 },
  91 + methods: {
  92 + get,
  93 + },
90 94 };
91 95 </script>
... ...
packages/utils/schema.js 0 → 100644
... ... @@ -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 +};
... ...