From 07a45b45306b43939ba3f6b25bd3d3673bfa3d5f Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sun, 29 Jan 2023 13:44:19 +0800 Subject: [PATCH] build: trident-sync prepare --- packages/server/certd-client | 1 - packages/server/certd-server | 1 - packages/ui/certd-client/.browserslistrc | 3 + packages/ui/certd-client/.env | 3 + packages/ui/certd-client/.env.debug | 2 + packages/ui/certd-client/.env.debugpm | 2 + packages/ui/certd-client/.env.pm | 2 + packages/ui/certd-client/.env.production | 3 + packages/ui/certd-client/.eslintignore | 2 + packages/ui/certd-client/.eslintrc.js | 76 ++ packages/ui/certd-client/.gitignore | 11 + packages/ui/certd-client/.npmignore | 2 + packages/ui/certd-client/.prettierrc | 5 + packages/ui/certd-client/CHANGELOG.md | 12 + packages/ui/certd-client/LICENSE | 661 ++++++++++++++++++ packages/ui/certd-client/README.md | 27 + packages/ui/certd-client/build/modify-vars.ts | 11 + .../ui/certd-client/build/theme-colors.ts | 72 ++ .../ui/certd-client/build/theme-plugin.ts | 68 ++ packages/ui/certd-client/index.html | 26 + packages/ui/certd-client/package.json | 120 ++++ packages/ui/certd-client/public/favicon.ico | Bin 0 -> 4286 bytes .../public/images/logo-certd/logo.svg | 7 + .../public/images/logo-certd/rect-black.svg | 44 ++ .../public/images/logo-certd/rect.svg | 44 ++ .../public/images/logo-certd/square.svg | 17 + .../certd-client/public/images/logo/logo.svg | 7 + .../public/images/logo/rect-black.svg | 108 +++ .../certd-client/public/images/logo/rect.svg | 44 ++ .../public/images/logo/square.svg | 106 +++ .../ui/certd-client/public/images/plugin.png | Bin 0 -> 7742 bytes packages/ui/certd-client/public/index.css | 12 + packages/ui/certd-client/public/index.html | 17 + packages/ui/certd-client/public/logo.svg | 106 +++ packages/ui/certd-client/src/App.vue | 56 ++ .../src/api/modules/api.user.mock.ts | 31 + .../certd-client/src/api/modules/api.user.ts | 51 ++ packages/ui/certd-client/src/api/service.ts | 138 ++++ packages/ui/certd-client/src/api/tools.ts | 66 ++ .../ui/certd-client/src/assets/background.svg | 69 ++ packages/ui/certd-client/src/assets/logo.png | Bin 0 -> 6849 bytes .../certd-client/src/components/container.vue | 54 ++ .../components/dns-provider-selector/api.ts | 10 + .../dns-provider-selector/index.vue | 97 +++ .../certd-client/src/components/editable.vue | 119 ++++ .../ui/certd-client/src/components/index.ts | 18 + packages/ui/certd-client/src/i18n/index.ts | 30 + .../ui/certd-client/src/i18n/locale/en.ts | 3 + .../ui/certd-client/src/i18n/locale/zh_CN.ts | 9 + .../components/contentmenuList/index.vue | 56 ++ .../layout/components/contextmenu/index.vue | 68 ++ .../src/layout/components/locale/index.vue | 76 ++ .../src/layout/components/menu/index.jsx | 224 ++++++ .../src/layout/components/menu/index.less | 11 + .../layout/components/source-link/index.vue | 54 ++ .../src/layout/components/tabs/index.vue | 295 ++++++++ .../layout/components/theme/color-picker.vue | 101 +++ .../src/layout/components/theme/index.vue | 51 ++ .../src/layout/components/user-info/index.vue | 40 ++ .../src/layout/layout-framework.vue | 230 ++++++ .../src/layout/layout-outside.vue | 154 ++++ .../certd-client/src/layout/layout-pass.vue | 9 + packages/ui/certd-client/src/main.ts | 23 + packages/ui/certd-client/src/mock/base.js | 280 ++++++++ .../src/mock/common/cascader-data.js | 268 +++++++ .../certd-client/src/mock/common/mock.dict.js | 123 ++++ .../src/mock/common/pca-data-little.js | 70 ++ .../certd-client/src/mock/common/pcas-data.js | 87 +++ packages/ui/certd-client/src/mock/index.js | 49 ++ .../src/plugin/fast-crud/index.ts | 225 ++++++ .../src/plugin/iconfont/iconfont.js | 1 + .../certd-client/src/plugin/iconfont/index.ts | 1 + .../certd-client/src/plugin/iconify/index.ts | 2 + packages/ui/certd-client/src/plugin/index.ts | 12 + .../certd-client/src/plugin/permission/api.ts | 10 + .../src/plugin/permission/directive/index.js | 9 + .../plugin/permission/directive/permission.js | 11 + .../src/plugin/permission/errors.ts | 5 + .../src/plugin/permission/hook.ts | 45 ++ .../src/plugin/permission/index.ts | 23 + .../src/plugin/permission/store.permission.ts | 89 +++ .../plugin/permission/use-crud-permission.ts | 60 ++ .../src/plugin/permission/util.permission.ts | 29 + packages/ui/certd-client/src/router/index.ts | 69 ++ .../ui/certd-client/src/router/resolve.ts | 155 ++++ .../src/router/source/framework.ts | 35 + .../certd-client/src/router/source/header.ts | 73 ++ .../src/router/source/modules/certd.ts | 41 ++ .../src/router/source/modules/crud.ts | 604 ++++++++++++++++ .../src/router/source/modules/sys.ts | 61 ++ .../certd-client/src/router/source/outside.ts | 22 + packages/ui/certd-client/src/shims-vue.d.ts | 5 + packages/ui/certd-client/src/store/index.ts | 9 + .../ui/certd-client/src/store/modules/page.ts | 436 ++++++++++++ .../src/store/modules/resource.ts | 127 ++++ .../src/store/modules/settings.ts | 63 ++ .../ui/certd-client/src/store/modules/user.ts | 106 +++ packages/ui/certd-client/src/style.css | 1 + .../ui/certd-client/src/style/common.less | 124 ++++ .../certd-client/src/style/fix-windicss.less | 3 + .../ui/certd-client/src/style/scroll.less | 28 + .../certd-client/src/style/theme/default.less | 27 + .../certd-client/src/style/theme/index.less | 4 + .../ui/certd-client/src/style/transition.less | 36 + .../ui/certd-client/src/types/global.d.ts | 99 +++ packages/ui/certd-client/src/utils/index.ts | 12 + .../ui/certd-client/src/utils/util.common.ts | 33 + .../ui/certd-client/src/utils/util.env.ts | 40 ++ .../ui/certd-client/src/utils/util.mitt.ts | 2 + .../ui/certd-client/src/utils/util.site.ts | 11 + .../ui/certd-client/src/utils/util.storage.ts | 113 +++ .../access/access-selector/access/crud.jsx | 112 +++ .../access/access-selector/access/index.vue | 64 ++ .../certd/access/access-selector/index.vue | 103 +++ .../src/views/certd/access/api.js | 49 ++ .../src/views/certd/access/common.tsx | 85 +++ .../src/views/certd/access/crud.tsx | 61 ++ .../src/views/certd/access/index.vue | 44 ++ .../src/views/certd/pipeline/api.history.ts | 35 + .../src/views/certd/pipeline/api.plugin.ts | 38 + .../src/views/certd/pipeline/api.ts | 66 ++ .../views/certd/pipeline/certd-form/crud.jsx | 129 ++++ .../views/certd/pipeline/certd-form/dicts.ts | 9 + .../views/certd/pipeline/certd-form/index.vue | 46 ++ .../src/views/certd/pipeline/crud.tsx | 225 ++++++ .../src/views/certd/pipeline/dash-roll.readme | 1 + .../src/views/certd/pipeline/detail.vue | 86 +++ .../src/views/certd/pipeline/index.vue | 52 ++ .../component/history-timeline-item.vue | 66 ++ .../component/output-selector/index.vue | 61 ++ .../pipeline/component/status-show.vue | 46 ++ .../pipeline/component/step-form/index.vue | 314 +++++++++ .../pipeline/component/task-form/index.vue | 265 +++++++ .../pipeline/component/task-view/index.vue | 116 +++ .../pipeline/component/trigger-form/index.vue | 208 ++++++ .../views/certd/pipeline/pipeline/index.vue | 652 +++++++++++++++++ .../certd/pipeline/pipeline/plugin/index.ts | 65 ++ .../views/certd/pipeline/pipeline/type.d.ts | 22 + .../pipeline/pipeline/utils/util.status.ts | 49 ++ .../src/views/crud/advanced/big-data/api.js | 42 ++ .../src/views/crud/advanced/big-data/crud.jsx | 170 +++++ .../views/crud/advanced/big-data/index.vue | 43 ++ .../src/views/crud/advanced/big-data/mock.js | 126 ++++ .../views/crud/advanced/from-backend/api.js | 48 ++ .../advanced/from-backend/crud-backend.js | 29 + .../views/crud/advanced/from-backend/crud.jsx | 27 + .../crud/advanced/from-backend/index.vue | 45 ++ .../views/crud/advanced/from-backend/mock.js | 35 + .../views/crud/advanced/in-dialog/crud/api.js | 42 ++ .../crud/advanced/in-dialog/crud/crud.jsx | 106 +++ .../crud/advanced/in-dialog/crud/index.vue | 40 ++ .../crud/advanced/in-dialog/crud/mock.js | 40 ++ .../views/crud/advanced/in-dialog/index.vue | 37 + .../src/views/crud/advanced/linkage/api.js | 42 ++ .../src/views/crud/advanced/linkage/crud.jsx | 118 ++++ .../src/views/crud/advanced/linkage/index.vue | 38 + .../src/views/crud/advanced/linkage/mock.js | 129 ++++ .../crud/advanced/local-pagination/api.js | 42 ++ .../crud/advanced/local-pagination/crud.jsx | 101 +++ .../crud/advanced/local-pagination/index.vue | 57 ++ .../crud/advanced/local-pagination/mock.js | 19 + .../src/views/crud/advanced/nest/api.js | 50 ++ .../crud/advanced/nest/aside-table/api.js | 50 ++ .../crud/advanced/nest/aside-table/crud.jsx | 56 ++ .../crud/advanced/nest/aside-table/index.vue | 43 ++ .../crud/advanced/nest/aside-table/mock.js | 26 + .../src/views/crud/advanced/nest/crud.jsx | 101 +++ .../src/views/crud/advanced/nest/index.vue | 59 ++ .../src/views/crud/advanced/nest/mock.js | 22 + .../views/crud/advanced/nest/sub-table/api.js | 50 ++ .../crud/advanced/nest/sub-table/crud.jsx | 62 ++ .../crud/advanced/nest/sub-table/index.vue | 61 ++ .../crud/advanced/nest/sub-table/mock.js | 19 + .../src/views/crud/basis/columns-set/api.js | 50 ++ .../src/views/crud/basis/columns-set/crud.jsx | 69 ++ .../views/crud/basis/columns-set/index.vue | 52 ++ .../src/views/crud/basis/columns-set/mock.js | 19 + .../src/views/crud/basis/compute-more/api.js | 42 ++ .../views/crud/basis/compute-more/crud.jsx | 79 +++ .../views/crud/basis/compute-more/index.vue | 43 ++ .../src/views/crud/basis/compute-more/mock.js | 32 + .../src/views/crud/basis/compute/api.js | 42 ++ .../src/views/crud/basis/compute/crud.jsx | 211 ++++++ .../src/views/crud/basis/compute/index.vue | 60 ++ .../src/views/crud/basis/compute/mock.js | 34 + .../src/views/crud/basis/first/index.vue | 109 +++ .../src/views/crud/basis/i18n/api.js | 42 ++ .../src/views/crud/basis/i18n/crud.jsx | 72 ++ .../src/views/crud/basis/i18n/index.vue | 53 ++ .../src/views/crud/basis/i18n/mock.js | 25 + .../src/views/crud/basis/layout-card/api.js | 42 ++ .../src/views/crud/basis/layout-card/crud.jsx | 73 ++ .../views/crud/basis/layout-card/index.vue | 46 ++ .../src/views/crud/basis/layout-card/mock.js | 25 + .../src/views/crud/basis/layout-custom/api.js | 42 ++ .../views/crud/basis/layout-custom/crud.jsx | 75 ++ .../basis/layout-custom/custom-layout.vue | 74 ++ .../views/crud/basis/layout-custom/index.vue | 56 ++ .../views/crud/basis/layout-custom/mock.js | 25 + .../src/views/crud/basis/value-change/api.js | 42 ++ .../views/crud/basis/value-change/crud.jsx | 95 +++ .../views/crud/basis/value-change/index.vue | 43 ++ .../src/views/crud/basis/value-change/mock.js | 32 + .../src/views/crud/component/button/api.js | 42 ++ .../src/views/crud/component/button/crud.jsx | 115 +++ .../src/views/crud/component/button/index.vue | 42 ++ .../src/views/crud/component/button/mock.js | 23 + .../src/views/crud/component/cascader/api.js | 42 ++ .../views/crud/component/cascader/crud.jsx | 137 ++++ .../views/crud/component/cascader/index.vue | 40 ++ .../src/views/crud/component/cascader/mock.js | 23 + .../src/views/crud/component/checkbox/api.js | 42 ++ .../views/crud/component/checkbox/crud.jsx | 50 ++ .../views/crud/component/checkbox/index.vue | 40 ++ .../src/views/crud/component/checkbox/mock.js | 19 + .../src/views/crud/component/date/api.js | 42 ++ .../src/views/crud/component/date/crud.jsx | 174 +++++ .../src/views/crud/component/date/index.vue | 47 ++ .../src/views/crud/component/date/mock.js | 44 ++ .../src/views/crud/component/editor/api.js | 42 ++ .../src/views/crud/component/editor/crud.jsx | 107 +++ .../src/views/crud/component/editor/index.vue | 44 ++ .../src/views/crud/component/editor/mock.js | 29 + .../src/views/crud/component/icon/api.js | 42 ++ .../src/views/crud/component/icon/crud.jsx | 75 ++ .../src/views/crud/component/icon/index.vue | 42 ++ .../src/views/crud/component/icon/mock.js | 22 + .../src/views/crud/component/json/api.js | 42 ++ .../src/views/crud/component/json/crud.jsx | 78 +++ .../src/views/crud/component/json/index.vue | 40 ++ .../src/views/crud/component/json/mock.js | 18 + .../src/views/crud/component/number/api.js | 42 ++ .../src/views/crud/component/number/crud.jsx | 71 ++ .../src/views/crud/component/number/index.vue | 40 ++ .../src/views/crud/component/number/mock.js | 25 + .../src/views/crud/component/radio/api.js | 42 ++ .../src/views/crud/component/radio/crud.jsx | 73 ++ .../src/views/crud/component/radio/index.vue | 42 ++ .../src/views/crud/component/radio/mock.js | 24 + .../src/views/crud/component/select/api.js | 42 ++ .../src/views/crud/component/select/crud.jsx | 311 ++++++++ .../src/views/crud/component/select/index.vue | 45 ++ .../src/views/crud/component/select/mock.js | 42 ++ .../src/views/crud/component/switch/api.js | 42 ++ .../src/views/crud/component/switch/crud.jsx | 141 ++++ .../src/views/crud/component/switch/index.vue | 40 ++ .../src/views/crud/component/switch/mock.js | 27 + .../src/views/crud/component/text/api.js | 42 ++ .../src/views/crud/component/text/crud.jsx | 130 ++++ .../src/views/crud/component/text/index.vue | 40 ++ .../src/views/crud/component/text/mock.js | 43 ++ .../src/views/crud/component/tree/api.js | 42 ++ .../src/views/crud/component/tree/crud.jsx | 81 +++ .../src/views/crud/component/tree/index.vue | 40 ++ .../src/views/crud/component/tree/mock.js | 16 + .../crud/component/uploader/alioss/api.js | 42 ++ .../crud/component/uploader/alioss/crud.jsx | 75 ++ .../crud/component/uploader/alioss/index.vue | 40 ++ .../crud/component/uploader/alioss/mock.js | 24 + .../views/crud/component/uploader/cos/api.js | 42 ++ .../crud/component/uploader/cos/crud.jsx | 75 ++ .../crud/component/uploader/cos/index.vue | 40 ++ .../views/crud/component/uploader/cos/mock.js | 22 + .../crud/component/uploader/cropper/api.js | 42 ++ .../crud/component/uploader/cropper/crud.jsx | 110 +++ .../crud/component/uploader/cropper/index.vue | 40 ++ .../crud/component/uploader/cropper/mock.js | 24 + .../views/crud/component/uploader/form/api.js | 42 ++ .../crud/component/uploader/form/crud.jsx | 223 ++++++ .../crud/component/uploader/form/index.vue | 43 ++ .../crud/component/uploader/form/mock.js | 24 + .../crud/component/uploader/qiniu/api.js | 42 ++ .../crud/component/uploader/qiniu/crud.jsx | 73 ++ .../crud/component/uploader/qiniu/index.vue | 40 ++ .../crud/component/uploader/qiniu/mock.js | 22 + .../src/views/crud/debug/select/api.js | 42 ++ .../src/views/crud/debug/select/crud.jsx | 106 +++ .../src/views/crud/debug/select/index.vue | 40 ++ .../src/views/crud/debug/select/mock.js | 9 + .../src/views/crud/dict/cloneable/api.js | 42 ++ .../src/views/crud/dict/cloneable/crud.jsx | 88 +++ .../src/views/crud/dict/cloneable/index.vue | 48 ++ .../src/views/crud/dict/cloneable/mock.js | 21 + .../src/views/crud/dict/prototype/api.js | 42 ++ .../src/views/crud/dict/prototype/crud.jsx | 128 ++++ .../src/views/crud/dict/prototype/index.vue | 48 ++ .../src/views/crud/dict/prototype/mock.js | 31 + .../src/views/crud/dict/single/api.js | 42 ++ .../src/views/crud/dict/single/crud.jsx | 111 +++ .../src/views/crud/dict/single/index.vue | 49 ++ .../src/views/crud/dict/single/mock.js | 21 + .../views/crud/feature/column-resize/api.js | 42 ++ .../views/crud/feature/column-resize/crud.jsx | 70 ++ .../crud/feature/column-resize/index.vue | 43 ++ .../views/crud/feature/column-resize/mock.js | 20 + .../src/views/crud/feature/column-sort/api.js | 50 ++ .../views/crud/feature/column-sort/crud.jsx | 94 +++ .../views/crud/feature/column-sort/index.vue | 47 ++ .../views/crud/feature/column-sort/mock.js | 31 + .../src/views/crud/feature/columns-set/api.js | 50 ++ .../views/crud/feature/columns-set/crud.jsx | 55 ++ .../views/crud/feature/columns-set/index.vue | 49 ++ .../views/crud/feature/columns-set/mock.js | 19 + .../views/crud/feature/editable-row/api.js | 50 ++ .../views/crud/feature/editable-row/crud.jsx | 88 +++ .../views/crud/feature/editable-row/index.vue | 40 ++ .../views/crud/feature/editable-row/mock.js | 19 + .../src/views/crud/feature/editable/api.js | 50 ++ .../src/views/crud/feature/editable/crud.jsx | 105 +++ .../src/views/crud/feature/editable/index.vue | 100 +++ .../src/views/crud/feature/editable/mock.js | 19 + .../src/views/crud/feature/expand/api.js | 50 ++ .../src/views/crud/feature/expand/crud.jsx | 63 ++ .../src/views/crud/feature/expand/index.vue | 65 ++ .../src/views/crud/feature/expand/mock.js | 19 + .../src/views/crud/feature/filter/api.js | 42 ++ .../src/views/crud/feature/filter/crud.jsx | 70 ++ .../src/views/crud/feature/filter/index.vue | 40 ++ .../src/views/crud/feature/filter/mock.js | 19 + .../src/views/crud/feature/fixed/api.js | 50 ++ .../src/views/crud/feature/fixed/crud.jsx | 90 +++ .../src/views/crud/feature/fixed/index.vue | 39 ++ .../src/views/crud/feature/fixed/mock.js | 14 + .../views/crud/feature/header-group/api.js | 42 ++ .../views/crud/feature/header-group/crud.jsx | 109 +++ .../views/crud/feature/header-group/index.vue | 40 ++ .../views/crud/feature/header-group/mock.js | 30 + .../src/views/crud/feature/height/api.js | 42 ++ .../src/views/crud/feature/height/crud.jsx | 57 ++ .../src/views/crud/feature/height/index.vue | 42 ++ .../src/views/crud/feature/height/mock.js | 24 + .../src/views/crud/feature/hide/api.js | 42 ++ .../src/views/crud/feature/hide/crud.jsx | 100 +++ .../src/views/crud/feature/hide/index.vue | 184 +++++ .../src/views/crud/feature/hide/mock.js | 19 + .../src/views/crud/feature/index/api.js | 50 ++ .../src/views/crud/feature/index/crud.jsx | 56 ++ .../src/views/crud/feature/index/index.vue | 39 ++ .../src/views/crud/feature/index/mock.js | 19 + .../views/crud/feature/local-v-model/crud.jsx | 32 + .../crud/feature/local-v-model/index.vue | 51 ++ .../crud/feature/local-v-model/local.vue | 67 ++ .../src/views/crud/feature/local/crud.jsx | 35 + .../src/views/crud/feature/local/index.vue | 89 +++ .../src/views/crud/feature/merge/api.js | 43 ++ .../src/views/crud/feature/merge/crud.jsx | 129 ++++ .../src/views/crud/feature/merge/index.vue | 39 ++ .../src/views/crud/feature/merge/mock.js | 36 + .../src/views/crud/feature/remove/api.js | 42 ++ .../src/views/crud/feature/remove/crud.jsx | 74 ++ .../src/views/crud/feature/remove/index.vue | 40 ++ .../src/views/crud/feature/remove/mock.js | 25 + .../views/crud/feature/search-multi/api.js | 42 ++ .../views/crud/feature/search-multi/crud.jsx | 143 ++++ .../views/crud/feature/search-multi/index.vue | 41 ++ .../views/crud/feature/search-multi/mock.js | 19 + .../src/views/crud/feature/search/api.js | 42 ++ .../src/views/crud/feature/search/crud.jsx | 88 +++ .../src/views/crud/feature/search/index.vue | 57 ++ .../src/views/crud/feature/search/mock.js | 19 + .../views/crud/feature/selection-radio/api.js | 50 ++ .../crud/feature/selection-radio/crud.jsx | 67 ++ .../crud/feature/selection-radio/index.vue | 45 ++ .../crud/feature/selection-radio/mock.js | 19 + .../src/views/crud/feature/selection/api.js | 50 ++ .../src/views/crud/feature/selection/crud.jsx | 67 ++ .../views/crud/feature/selection/index.vue | 68 ++ .../src/views/crud/feature/selection/mock.js | 19 + .../src/views/crud/feature/sortable/api.js | 50 ++ .../src/views/crud/feature/sortable/crud.jsx | 62 ++ .../src/views/crud/feature/sortable/index.vue | 39 ++ .../src/views/crud/feature/sortable/mock.js | 25 + .../src/views/crud/feature/tree/api.js | 50 ++ .../src/views/crud/feature/tree/crud.jsx | 91 +++ .../src/views/crud/feature/tree/index.vue | 65 ++ .../src/views/crud/feature/tree/mock.js | 85 +++ .../views/crud/feature/value-builder/api.js | 50 ++ .../views/crud/feature/value-builder/crud.jsx | 54 ++ .../crud/feature/value-builder/index.vue | 46 ++ .../views/crud/feature/value-builder/mock.js | 21 + .../src/views/crud/form/base/api.js | 42 ++ .../src/views/crud/form/base/crud.jsx | 33 + .../src/views/crud/form/base/index.vue | 32 + .../src/views/crud/form/base/mock.js | 310 ++++++++ .../src/views/crud/form/custom-form/api.js | 42 ++ .../src/views/crud/form/custom-form/crud.jsx | 40 ++ .../src/views/crud/form/custom-form/index.vue | 86 +++ .../src/views/crud/form/custom-form/mock.js | 303 ++++++++ .../src/views/crud/form/drawer/api.js | 42 ++ .../src/views/crud/form/drawer/crud.jsx | 44 ++ .../src/views/crud/form/drawer/index.vue | 40 ++ .../src/views/crud/form/drawer/mock.js | 40 ++ .../src/views/crud/form/group-tabs/api.js | 42 ++ .../src/views/crud/form/group-tabs/crud.jsx | 114 +++ .../src/views/crud/form/group-tabs/index.vue | 40 ++ .../src/views/crud/form/group-tabs/mock.js | 303 ++++++++ .../src/views/crud/form/group/api.js | 42 ++ .../src/views/crud/form/group/crud.jsx | 143 ++++ .../src/views/crud/form/group/index.vue | 46 ++ .../src/views/crud/form/group/mock.js | 303 ++++++++ .../src/views/crud/form/helper/api.js | 42 ++ .../src/views/crud/form/helper/crud.jsx | 72 ++ .../src/views/crud/form/helper/index.vue | 48 ++ .../src/views/crud/form/helper/mock.js | 310 ++++++++ .../src/views/crud/form/independent/crud.jsx | 59 ++ .../src/views/crud/form/independent/index.vue | 215 ++++++ .../src/views/crud/form/inner/api.js | 42 ++ .../src/views/crud/form/inner/area/api.js | 50 ++ .../src/views/crud/form/inner/area/crud.jsx | 39 ++ .../src/views/crud/form/inner/area/index.vue | 40 ++ .../src/views/crud/form/inner/area/mock.js | 24 + .../src/views/crud/form/inner/crud.jsx | 77 ++ .../src/views/crud/form/inner/index.vue | 40 ++ .../src/views/crud/form/inner/mock.js | 25 + .../src/views/crud/form/layout-flex/api.js | 42 ++ .../src/views/crud/form/layout-flex/crud.jsx | 63 ++ .../src/views/crud/form/layout-flex/index.vue | 43 ++ .../src/views/crud/form/layout-flex/mock.js | 35 + .../src/views/crud/form/layout-grid/api.js | 42 ++ .../src/views/crud/form/layout-grid/crud.jsx | 67 ++ .../src/views/crud/form/layout-grid/index.vue | 52 ++ .../src/views/crud/form/layout-grid/mock.js | 34 + .../src/views/crud/form/layout/api.js | 42 ++ .../src/views/crud/form/layout/crud.jsx | 101 +++ .../src/views/crud/form/layout/index.vue | 50 ++ .../src/views/crud/form/layout/mock.js | 41 ++ .../src/views/crud/form/nest/api.js | 42 ++ .../src/views/crud/form/nest/crud.jsx | 76 ++ .../src/views/crud/form/nest/index.vue | 48 ++ .../src/views/crud/form/nest/mock.js | 319 +++++++++ .../src/views/crud/form/new-page/api.js | 42 ++ .../src/views/crud/form/new-page/crud.jsx | 96 +++ .../src/views/crud/form/new-page/edit.vue | 81 +++ .../src/views/crud/form/new-page/index.vue | 43 ++ .../src/views/crud/form/new-page/mock.js | 22 + .../src/views/crud/form/reset/api.js | 42 ++ .../src/views/crud/form/reset/crud.jsx | 56 ++ .../src/views/crud/form/reset/index.vue | 40 ++ .../src/views/crud/form/reset/mock.js | 40 ++ .../src/views/crud/form/validation/api.js | 42 ++ .../src/views/crud/form/validation/crud.jsx | 135 ++++ .../src/views/crud/form/validation/index.vue | 40 ++ .../src/views/crud/form/validation/mock.js | 310 ++++++++ .../src/views/crud/home/index.vue | 15 + .../src/views/crud/home/page-cover/helper.js | 31 + .../views/crud/home/page-cover/image/crud.png | Bin 0 -> 202647 bytes .../home/page-cover/image/darkblue@2x.png | Bin 0 -> 9259 bytes .../views/crud/home/page-cover/image/logo.svg | 178 +++++ .../src/views/crud/home/page-cover/index.vue | 139 ++++ .../src/views/crud/row-handle/dropdown/api.js | 42 ++ .../views/crud/row-handle/dropdown/crud.jsx | 82 +++ .../views/crud/row-handle/dropdown/index.vue | 40 ++ .../views/crud/row-handle/dropdown/mock.js | 19 + .../src/views/crud/row-handle/tooltip/api.js | 50 ++ .../views/crud/row-handle/tooltip/crud.jsx | 84 +++ .../views/crud/row-handle/tooltip/index.vue | 51 ++ .../src/views/crud/row-handle/tooltip/mock.js | 19 + .../src/views/crud/slots/cell/api.js | 42 ++ .../src/views/crud/slots/cell/crud.jsx | 89 +++ .../src/views/crud/slots/cell/index.vue | 79 +++ .../src/views/crud/slots/cell/mock.js | 26 + .../src/views/crud/slots/form-item/api.js | 42 ++ .../src/views/crud/slots/form-item/crud.jsx | 51 ++ .../src/views/crud/slots/form-item/index.vue | 72 ++ .../src/views/crud/slots/form-item/mock.js | 13 + .../src/views/crud/slots/form/api.js | 42 ++ .../src/views/crud/slots/form/crud.jsx | 50 ++ .../src/views/crud/slots/form/index.vue | 69 ++ .../src/views/crud/slots/form/mock.js | 13 + .../src/views/crud/slots/layout/api.js | 42 ++ .../src/views/crud/slots/layout/crud.jsx | 49 ++ .../src/views/crud/slots/layout/index.vue | 77 ++ .../src/views/crud/slots/layout/mock.js | 19 + .../src/views/crud/slots/search/api.js | 42 ++ .../src/views/crud/slots/search/crud.jsx | 49 ++ .../src/views/crud/slots/search/index.vue | 54 ++ .../src/views/crud/slots/search/mock.js | 19 + .../src/views/framework/error/404.vue | 18 + .../src/views/framework/home/index.vue | 15 + .../views/framework/home/page-cover/helper.js | 31 + .../framework/home/page-cover/image/crud.png | Bin 0 -> 202647 bytes .../home/page-cover/image/darkblue@2x.png | Bin 0 -> 9259 bytes .../framework/home/page-cover/image/logo.svg | 178 +++++ .../views/framework/home/page-cover/index.vue | 137 ++++ .../src/views/framework/login/index.vue | 270 +++++++ .../src/views/framework/register/index.vue | 178 +++++ .../src/views/sys/authority/permission/api.js | 48 ++ .../views/sys/authority/permission/crud.jsx | 150 ++++ .../permission/fs-permission-tree.vue | 178 +++++ .../views/sys/authority/permission/index.vue | 90 +++ .../src/views/sys/authority/role/api.js | 70 ++ .../src/views/sys/authority/role/crud.jsx | 81 +++ .../src/views/sys/authority/role/index.vue | 133 ++++ .../src/views/sys/authority/user/api.js | 41 ++ .../src/views/sys/authority/user/crud.jsx | 146 ++++ .../src/views/sys/authority/user/index.vue | 45 ++ packages/ui/certd-client/tailwind.config.js | 11 + .../certd-client/tests/unit/example.spec.ts | 13 + packages/ui/certd-client/tsconfig.json | 45 ++ packages/ui/certd-client/vite.config.ts | 101 +++ packages/ui/certd-client/windi.config.js | 6 + packages/ui/certd-server/.dockerignore | 16 + packages/ui/certd-server/.editorconfig | 11 + packages/ui/certd-server/.eslintrc.json | 11 + packages/ui/certd-server/.gitignore | 18 + packages/ui/certd-server/.prettierrc.js | 3 + packages/ui/certd-server/Dockerfile | 15 + packages/ui/certd-server/LICENSE | 661 ++++++++++++++++++ packages/ui/certd-server/README.md | 31 + packages/ui/certd-server/README.zh-CN.md | 30 + packages/ui/certd-server/app.js | 23 + packages/ui/certd-server/bootstrap.js | 15 + .../db/migration/v00001__init.sql | 77 ++ .../db/migration/v00002__for_pre.sql | 4 + .../db/migration/v10000__certd.sql | 24 + packages/ui/certd-server/f.yaml | 11 + packages/ui/certd-server/jest.config.js | 6 + packages/ui/certd-server/package.json | 91 +++ .../certd-server/src/basic/base-controller.ts | 33 + .../ui/certd-server/src/basic/base-service.ts | 207 ++++++ .../ui/certd-server/src/basic/constants.ts | 28 + .../certd-server/src/basic/crud-controller.ts | 64 ++ .../ui/certd-server/src/basic/enum-item.ts | 11 + .../src/basic/exception/auth-exception.ts | 14 + .../src/basic/exception/base-exception.ts | 11 + .../src/basic/exception/common-exception.ts | 14 + .../basic/exception/permission-exception.ts | 14 + .../src/basic/exception/preview-exception.ts | 14 + .../basic/exception/validation-exception.ts | 14 + packages/ui/certd-server/src/basic/result.ts | 18 + .../certd-server/src/config/config.default.ts | 60 ++ .../certd-server/src/config/config.preview.ts | 10 + .../src/config/config.production.ts | 10 + .../certd-server/src/config/config.syncdb.ts | 11 + packages/ui/certd-server/src/configuration.ts | 60 ++ .../ui/certd-server/src/controller/home.ts | 10 + .../certd-server/src/middleware/authority.ts | 48 ++ .../src/middleware/global-exception.ts | 27 + .../ui/certd-server/src/middleware/preview.ts | 50 ++ .../ui/certd-server/src/middleware/report.ts | 28 + .../controller/permission-controller.ts | 62 ++ .../authority/controller/role-controller.ts | 95 +++ .../authority/controller/user-controller.ts | 118 ++++ .../modules/authority/entity/permission.ts | 40 ++ .../authority/entity/role-permission.ts | 12 + .../src/modules/authority/entity/role.ts | 43 ++ .../src/modules/authority/entity/user-role.ts | 12 + .../src/modules/authority/entity/user.ts | 63 ++ .../authority/enums/ResourceTypeEnum.ts | 17 + .../authority/service/permission-service.ts | 52 ++ .../service/role-permission-service.ts | 18 + .../modules/authority/service/role-service.ts | 101 +++ .../authority/service/user-role-service.ts | 18 + .../modules/authority/service/user-service.ts | 113 +++ .../basic/controller/basic-controller.ts | 55 ++ .../src/modules/basic/service/code-service.ts | 57 ++ .../login/controller/login-controller.ts | 30 + .../modules/login/service/login-service.ts | 52 ++ .../pipeline/auto/auto-register-cron.ts | 26 + .../pipeline/controller/access-controller.ts | 80 +++ .../controller/dns-provider-controller.ts | 40 ++ .../pipeline/controller/history-controller.ts | 106 +++ .../controller/pipeline-controller.ts | 77 ++ .../pipeline/controller/plugin-controller.ts | 27 + .../src/modules/pipeline/entity/access.ts | 33 + .../modules/pipeline/entity/history-log.ts | 41 ++ .../src/modules/pipeline/entity/history.ts | 38 + .../src/modules/pipeline/entity/pipeline.ts | 52 ++ .../src/modules/pipeline/entity/storage.ts | 35 + .../pipeline/entity/vo/history-detail.ts | 12 + .../pipeline/entity/vo/pipeline-detail.ts | 13 + .../pipeline/service/access-service.ts | 44 ++ .../modules/pipeline/service/db-storage.ts | 47 ++ .../pipeline/service/dns-provider-service.ts | 9 + .../pipeline/service/history-log-service.ts | 27 + .../pipeline/service/history-service.ts | 81 +++ .../pipeline/service/pipeline-service.ts | 241 +++++++ .../pipeline/service/plugin-service.ts | 15 + .../pipeline/service/storage-service.ts | 56 ++ .../src/plugins/cron/configuration.ts | 27 + .../ui/certd-server/src/plugins/cron/cron.ts | 38 + .../ui/certd-server/src/plugins/cron/index.ts | 5 + .../src/plugins/pipeline/index.ts | 6 + .../src/plugins/pipeline/test/echo-plugin.ts | 27 + packages/ui/certd-server/src/utils/logger.ts | 12 + packages/ui/certd-server/src/utils/random.ts | 43 ++ .../certd-server/test/controller/api.test.ts | 25 + .../certd-server/test/controller/home.test.ts | 26 + packages/ui/certd-server/tsconfig.json | 25 + 589 files changed, 36886 insertions(+), 2 deletions(-) delete mode 160000 packages/server/certd-client delete mode 160000 packages/server/certd-server create mode 100644 packages/ui/certd-client/.browserslistrc create mode 100644 packages/ui/certd-client/.env create mode 100644 packages/ui/certd-client/.env.debug create mode 100644 packages/ui/certd-client/.env.debugpm create mode 100644 packages/ui/certd-client/.env.pm create mode 100644 packages/ui/certd-client/.env.production create mode 100644 packages/ui/certd-client/.eslintignore create mode 100644 packages/ui/certd-client/.eslintrc.js create mode 100644 packages/ui/certd-client/.gitignore create mode 100644 packages/ui/certd-client/.npmignore create mode 100644 packages/ui/certd-client/.prettierrc create mode 100644 packages/ui/certd-client/CHANGELOG.md create mode 100644 packages/ui/certd-client/LICENSE create mode 100644 packages/ui/certd-client/README.md create mode 100644 packages/ui/certd-client/build/modify-vars.ts create mode 100644 packages/ui/certd-client/build/theme-colors.ts create mode 100644 packages/ui/certd-client/build/theme-plugin.ts create mode 100644 packages/ui/certd-client/index.html create mode 100644 packages/ui/certd-client/package.json create mode 100644 packages/ui/certd-client/public/favicon.ico create mode 100644 packages/ui/certd-client/public/images/logo-certd/logo.svg create mode 100644 packages/ui/certd-client/public/images/logo-certd/rect-black.svg create mode 100644 packages/ui/certd-client/public/images/logo-certd/rect.svg create mode 100644 packages/ui/certd-client/public/images/logo-certd/square.svg create mode 100644 packages/ui/certd-client/public/images/logo/logo.svg create mode 100644 packages/ui/certd-client/public/images/logo/rect-black.svg create mode 100644 packages/ui/certd-client/public/images/logo/rect.svg create mode 100644 packages/ui/certd-client/public/images/logo/square.svg create mode 100644 packages/ui/certd-client/public/images/plugin.png create mode 100644 packages/ui/certd-client/public/index.css create mode 100644 packages/ui/certd-client/public/index.html create mode 100644 packages/ui/certd-client/public/logo.svg create mode 100644 packages/ui/certd-client/src/App.vue create mode 100644 packages/ui/certd-client/src/api/modules/api.user.mock.ts create mode 100644 packages/ui/certd-client/src/api/modules/api.user.ts create mode 100644 packages/ui/certd-client/src/api/service.ts create mode 100644 packages/ui/certd-client/src/api/tools.ts create mode 100644 packages/ui/certd-client/src/assets/background.svg create mode 100644 packages/ui/certd-client/src/assets/logo.png create mode 100644 packages/ui/certd-client/src/components/container.vue create mode 100644 packages/ui/certd-client/src/components/dns-provider-selector/api.ts create mode 100644 packages/ui/certd-client/src/components/dns-provider-selector/index.vue create mode 100644 packages/ui/certd-client/src/components/editable.vue create mode 100644 packages/ui/certd-client/src/components/index.ts create mode 100644 packages/ui/certd-client/src/i18n/index.ts create mode 100644 packages/ui/certd-client/src/i18n/locale/en.ts create mode 100644 packages/ui/certd-client/src/i18n/locale/zh_CN.ts create mode 100644 packages/ui/certd-client/src/layout/components/contextmenu/components/contentmenuList/index.vue create mode 100644 packages/ui/certd-client/src/layout/components/contextmenu/index.vue create mode 100644 packages/ui/certd-client/src/layout/components/locale/index.vue create mode 100644 packages/ui/certd-client/src/layout/components/menu/index.jsx create mode 100644 packages/ui/certd-client/src/layout/components/menu/index.less create mode 100644 packages/ui/certd-client/src/layout/components/source-link/index.vue create mode 100644 packages/ui/certd-client/src/layout/components/tabs/index.vue create mode 100644 packages/ui/certd-client/src/layout/components/theme/color-picker.vue create mode 100644 packages/ui/certd-client/src/layout/components/theme/index.vue create mode 100644 packages/ui/certd-client/src/layout/components/user-info/index.vue create mode 100644 packages/ui/certd-client/src/layout/layout-framework.vue create mode 100644 packages/ui/certd-client/src/layout/layout-outside.vue create mode 100644 packages/ui/certd-client/src/layout/layout-pass.vue create mode 100644 packages/ui/certd-client/src/main.ts create mode 100644 packages/ui/certd-client/src/mock/base.js create mode 100644 packages/ui/certd-client/src/mock/common/cascader-data.js create mode 100644 packages/ui/certd-client/src/mock/common/mock.dict.js create mode 100644 packages/ui/certd-client/src/mock/common/pca-data-little.js create mode 100644 packages/ui/certd-client/src/mock/common/pcas-data.js create mode 100644 packages/ui/certd-client/src/mock/index.js create mode 100644 packages/ui/certd-client/src/plugin/fast-crud/index.ts create mode 100644 packages/ui/certd-client/src/plugin/iconfont/iconfont.js create mode 100644 packages/ui/certd-client/src/plugin/iconfont/index.ts create mode 100644 packages/ui/certd-client/src/plugin/iconify/index.ts create mode 100644 packages/ui/certd-client/src/plugin/index.ts create mode 100644 packages/ui/certd-client/src/plugin/permission/api.ts create mode 100644 packages/ui/certd-client/src/plugin/permission/directive/index.js create mode 100644 packages/ui/certd-client/src/plugin/permission/directive/permission.js create mode 100644 packages/ui/certd-client/src/plugin/permission/errors.ts create mode 100644 packages/ui/certd-client/src/plugin/permission/hook.ts create mode 100644 packages/ui/certd-client/src/plugin/permission/index.ts create mode 100644 packages/ui/certd-client/src/plugin/permission/store.permission.ts create mode 100644 packages/ui/certd-client/src/plugin/permission/use-crud-permission.ts create mode 100644 packages/ui/certd-client/src/plugin/permission/util.permission.ts create mode 100644 packages/ui/certd-client/src/router/index.ts create mode 100644 packages/ui/certd-client/src/router/resolve.ts create mode 100644 packages/ui/certd-client/src/router/source/framework.ts create mode 100644 packages/ui/certd-client/src/router/source/header.ts create mode 100644 packages/ui/certd-client/src/router/source/modules/certd.ts create mode 100644 packages/ui/certd-client/src/router/source/modules/crud.ts create mode 100644 packages/ui/certd-client/src/router/source/modules/sys.ts create mode 100644 packages/ui/certd-client/src/router/source/outside.ts create mode 100644 packages/ui/certd-client/src/shims-vue.d.ts create mode 100644 packages/ui/certd-client/src/store/index.ts create mode 100644 packages/ui/certd-client/src/store/modules/page.ts create mode 100644 packages/ui/certd-client/src/store/modules/resource.ts create mode 100644 packages/ui/certd-client/src/store/modules/settings.ts create mode 100644 packages/ui/certd-client/src/store/modules/user.ts create mode 100644 packages/ui/certd-client/src/style.css create mode 100644 packages/ui/certd-client/src/style/common.less create mode 100644 packages/ui/certd-client/src/style/fix-windicss.less create mode 100644 packages/ui/certd-client/src/style/scroll.less create mode 100644 packages/ui/certd-client/src/style/theme/default.less create mode 100644 packages/ui/certd-client/src/style/theme/index.less create mode 100644 packages/ui/certd-client/src/style/transition.less create mode 100644 packages/ui/certd-client/src/types/global.d.ts create mode 100644 packages/ui/certd-client/src/utils/index.ts create mode 100644 packages/ui/certd-client/src/utils/util.common.ts create mode 100644 packages/ui/certd-client/src/utils/util.env.ts create mode 100644 packages/ui/certd-client/src/utils/util.mitt.ts create mode 100644 packages/ui/certd-client/src/utils/util.site.ts create mode 100644 packages/ui/certd-client/src/utils/util.storage.ts create mode 100644 packages/ui/certd-client/src/views/certd/access/access-selector/access/crud.jsx create mode 100644 packages/ui/certd-client/src/views/certd/access/access-selector/access/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/access/access-selector/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/access/api.js create mode 100644 packages/ui/certd-client/src/views/certd/access/common.tsx create mode 100644 packages/ui/certd-client/src/views/certd/access/crud.tsx create mode 100644 packages/ui/certd-client/src/views/certd/access/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/api.history.ts create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/api.plugin.ts create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/api.ts create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/certd-form/crud.jsx create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/certd-form/dicts.ts create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/certd-form/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/crud.tsx create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/dash-roll.readme create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/detail.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/history-timeline-item.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/output-selector/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/status-show.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/step-form/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-form/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-view/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/trigger-form/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/plugin/index.ts create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/type.d.ts create mode 100644 packages/ui/certd-client/src/views/certd/pipeline/pipeline/utils/util.status.ts create mode 100644 packages/ui/certd-client/src/views/crud/advanced/big-data/api.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/big-data/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/advanced/big-data/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/advanced/big-data/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/from-backend/api.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/from-backend/crud-backend.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/from-backend/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/advanced/from-backend/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/advanced/from-backend/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/api.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/in-dialog/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/advanced/linkage/api.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/linkage/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/advanced/linkage/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/advanced/linkage/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/local-pagination/api.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/local-pagination/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/advanced/local-pagination/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/advanced/local-pagination/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/api.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/api.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/api.js create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/columns-set/api.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/columns-set/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/basis/columns-set/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/basis/columns-set/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/compute-more/api.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/compute-more/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/basis/compute-more/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/basis/compute-more/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/compute/api.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/compute/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/basis/compute/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/basis/compute/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/first/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/basis/i18n/api.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/i18n/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/basis/i18n/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/basis/i18n/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/layout-card/api.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/layout-card/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/basis/layout-card/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/basis/layout-card/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/layout-custom/api.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/layout-custom/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/basis/layout-custom/custom-layout.vue create mode 100644 packages/ui/certd-client/src/views/crud/basis/layout-custom/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/basis/layout-custom/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/value-change/api.js create mode 100644 packages/ui/certd-client/src/views/crud/basis/value-change/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/basis/value-change/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/basis/value-change/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/button/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/button/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/button/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/button/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/cascader/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/cascader/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/cascader/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/cascader/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/checkbox/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/checkbox/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/checkbox/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/checkbox/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/date/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/date/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/date/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/date/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/editor/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/editor/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/editor/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/editor/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/icon/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/icon/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/icon/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/icon/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/json/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/json/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/json/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/json/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/number/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/number/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/number/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/number/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/radio/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/radio/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/radio/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/radio/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/select/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/select/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/select/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/select/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/switch/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/switch/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/switch/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/switch/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/text/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/text/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/text/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/text/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/tree/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/tree/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/tree/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/tree/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/alioss/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/alioss/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/alioss/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/alioss/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/cos/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/cos/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/cos/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/cos/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/cropper/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/cropper/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/cropper/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/cropper/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/form/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/form/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/form/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/form/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/qiniu/api.js create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/qiniu/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/qiniu/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/component/uploader/qiniu/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/debug/select/api.js create mode 100644 packages/ui/certd-client/src/views/crud/debug/select/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/debug/select/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/debug/select/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/dict/cloneable/api.js create mode 100644 packages/ui/certd-client/src/views/crud/dict/cloneable/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/dict/cloneable/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/dict/cloneable/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/dict/prototype/api.js create mode 100644 packages/ui/certd-client/src/views/crud/dict/prototype/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/dict/prototype/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/dict/prototype/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/dict/single/api.js create mode 100644 packages/ui/certd-client/src/views/crud/dict/single/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/dict/single/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/dict/single/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/column-resize/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/column-resize/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/column-resize/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/column-resize/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/column-sort/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/column-sort/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/column-sort/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/column-sort/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/columns-set/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/columns-set/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/columns-set/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/columns-set/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/editable-row/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/editable-row/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/editable-row/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/editable-row/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/editable/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/editable/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/editable/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/editable/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/expand/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/expand/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/expand/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/expand/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/filter/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/filter/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/filter/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/filter/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/fixed/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/fixed/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/fixed/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/fixed/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/header-group/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/header-group/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/header-group/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/header-group/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/height/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/height/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/height/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/height/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/hide/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/hide/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/hide/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/hide/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/index/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/index/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/index/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/index/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/local-v-model/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/local-v-model/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/local-v-model/local.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/local/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/local/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/merge/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/merge/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/merge/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/merge/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/remove/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/remove/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/remove/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/remove/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/search-multi/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/search-multi/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/search-multi/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/search-multi/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/search/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/search/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/search/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/search/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/selection-radio/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/selection-radio/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/selection-radio/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/selection-radio/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/selection/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/selection/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/selection/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/selection/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/sortable/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/sortable/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/sortable/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/sortable/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/tree/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/tree/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/tree/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/tree/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/value-builder/api.js create mode 100644 packages/ui/certd-client/src/views/crud/feature/value-builder/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/feature/value-builder/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/feature/value-builder/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/base/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/base/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/base/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/base/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/custom-form/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/custom-form/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/custom-form/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/custom-form/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/drawer/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/drawer/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/drawer/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/drawer/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/group-tabs/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/group-tabs/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/group-tabs/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/group-tabs/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/group/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/group/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/group/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/group/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/helper/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/helper/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/helper/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/helper/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/independent/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/independent/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/inner/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/inner/area/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/inner/area/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/inner/area/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/inner/area/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/inner/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/inner/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/inner/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/layout-flex/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/layout-flex/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/layout-flex/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/layout-flex/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/layout-grid/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/layout-grid/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/layout-grid/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/layout-grid/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/layout/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/layout/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/layout/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/layout/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/nest/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/nest/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/nest/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/nest/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/new-page/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/new-page/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/new-page/edit.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/new-page/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/new-page/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/reset/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/reset/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/reset/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/reset/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/form/validation/api.js create mode 100644 packages/ui/certd-client/src/views/crud/form/validation/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/form/validation/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/form/validation/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/home/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/home/page-cover/helper.js create mode 100644 packages/ui/certd-client/src/views/crud/home/page-cover/image/crud.png create mode 100644 packages/ui/certd-client/src/views/crud/home/page-cover/image/darkblue@2x.png create mode 100644 packages/ui/certd-client/src/views/crud/home/page-cover/image/logo.svg create mode 100644 packages/ui/certd-client/src/views/crud/home/page-cover/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/row-handle/dropdown/api.js create mode 100644 packages/ui/certd-client/src/views/crud/row-handle/dropdown/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/row-handle/dropdown/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/row-handle/dropdown/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/row-handle/tooltip/api.js create mode 100644 packages/ui/certd-client/src/views/crud/row-handle/tooltip/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/row-handle/tooltip/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/row-handle/tooltip/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/cell/api.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/cell/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/slots/cell/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/slots/cell/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/form-item/api.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/form-item/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/slots/form-item/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/slots/form-item/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/form/api.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/form/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/slots/form/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/slots/form/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/layout/api.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/layout/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/slots/layout/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/slots/layout/mock.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/search/api.js create mode 100644 packages/ui/certd-client/src/views/crud/slots/search/crud.jsx create mode 100644 packages/ui/certd-client/src/views/crud/slots/search/index.vue create mode 100644 packages/ui/certd-client/src/views/crud/slots/search/mock.js create mode 100644 packages/ui/certd-client/src/views/framework/error/404.vue create mode 100644 packages/ui/certd-client/src/views/framework/home/index.vue create mode 100644 packages/ui/certd-client/src/views/framework/home/page-cover/helper.js create mode 100644 packages/ui/certd-client/src/views/framework/home/page-cover/image/crud.png create mode 100644 packages/ui/certd-client/src/views/framework/home/page-cover/image/darkblue@2x.png create mode 100644 packages/ui/certd-client/src/views/framework/home/page-cover/image/logo.svg create mode 100644 packages/ui/certd-client/src/views/framework/home/page-cover/index.vue create mode 100644 packages/ui/certd-client/src/views/framework/login/index.vue create mode 100644 packages/ui/certd-client/src/views/framework/register/index.vue create mode 100644 packages/ui/certd-client/src/views/sys/authority/permission/api.js create mode 100644 packages/ui/certd-client/src/views/sys/authority/permission/crud.jsx create mode 100644 packages/ui/certd-client/src/views/sys/authority/permission/fs-permission-tree.vue create mode 100644 packages/ui/certd-client/src/views/sys/authority/permission/index.vue create mode 100644 packages/ui/certd-client/src/views/sys/authority/role/api.js create mode 100644 packages/ui/certd-client/src/views/sys/authority/role/crud.jsx create mode 100644 packages/ui/certd-client/src/views/sys/authority/role/index.vue create mode 100644 packages/ui/certd-client/src/views/sys/authority/user/api.js create mode 100644 packages/ui/certd-client/src/views/sys/authority/user/crud.jsx create mode 100644 packages/ui/certd-client/src/views/sys/authority/user/index.vue create mode 100644 packages/ui/certd-client/tailwind.config.js create mode 100644 packages/ui/certd-client/tests/unit/example.spec.ts create mode 100644 packages/ui/certd-client/tsconfig.json create mode 100644 packages/ui/certd-client/vite.config.ts create mode 100644 packages/ui/certd-client/windi.config.js create mode 100644 packages/ui/certd-server/.dockerignore create mode 100644 packages/ui/certd-server/.editorconfig create mode 100644 packages/ui/certd-server/.eslintrc.json create mode 100644 packages/ui/certd-server/.gitignore create mode 100644 packages/ui/certd-server/.prettierrc.js create mode 100644 packages/ui/certd-server/Dockerfile create mode 100644 packages/ui/certd-server/LICENSE create mode 100644 packages/ui/certd-server/README.md create mode 100644 packages/ui/certd-server/README.zh-CN.md create mode 100644 packages/ui/certd-server/app.js create mode 100644 packages/ui/certd-server/bootstrap.js create mode 100644 packages/ui/certd-server/db/migration/v00001__init.sql create mode 100644 packages/ui/certd-server/db/migration/v00002__for_pre.sql create mode 100644 packages/ui/certd-server/db/migration/v10000__certd.sql create mode 100644 packages/ui/certd-server/f.yaml create mode 100644 packages/ui/certd-server/jest.config.js create mode 100644 packages/ui/certd-server/package.json create mode 100644 packages/ui/certd-server/src/basic/base-controller.ts create mode 100644 packages/ui/certd-server/src/basic/base-service.ts create mode 100644 packages/ui/certd-server/src/basic/constants.ts create mode 100644 packages/ui/certd-server/src/basic/crud-controller.ts create mode 100644 packages/ui/certd-server/src/basic/enum-item.ts create mode 100644 packages/ui/certd-server/src/basic/exception/auth-exception.ts create mode 100644 packages/ui/certd-server/src/basic/exception/base-exception.ts create mode 100644 packages/ui/certd-server/src/basic/exception/common-exception.ts create mode 100644 packages/ui/certd-server/src/basic/exception/permission-exception.ts create mode 100644 packages/ui/certd-server/src/basic/exception/preview-exception.ts create mode 100644 packages/ui/certd-server/src/basic/exception/validation-exception.ts create mode 100644 packages/ui/certd-server/src/basic/result.ts create mode 100644 packages/ui/certd-server/src/config/config.default.ts create mode 100644 packages/ui/certd-server/src/config/config.preview.ts create mode 100644 packages/ui/certd-server/src/config/config.production.ts create mode 100644 packages/ui/certd-server/src/config/config.syncdb.ts create mode 100644 packages/ui/certd-server/src/configuration.ts create mode 100644 packages/ui/certd-server/src/controller/home.ts create mode 100644 packages/ui/certd-server/src/middleware/authority.ts create mode 100644 packages/ui/certd-server/src/middleware/global-exception.ts create mode 100644 packages/ui/certd-server/src/middleware/preview.ts create mode 100644 packages/ui/certd-server/src/middleware/report.ts create mode 100644 packages/ui/certd-server/src/modules/authority/controller/permission-controller.ts create mode 100644 packages/ui/certd-server/src/modules/authority/controller/role-controller.ts create mode 100644 packages/ui/certd-server/src/modules/authority/controller/user-controller.ts create mode 100644 packages/ui/certd-server/src/modules/authority/entity/permission.ts create mode 100644 packages/ui/certd-server/src/modules/authority/entity/role-permission.ts create mode 100644 packages/ui/certd-server/src/modules/authority/entity/role.ts create mode 100644 packages/ui/certd-server/src/modules/authority/entity/user-role.ts create mode 100644 packages/ui/certd-server/src/modules/authority/entity/user.ts create mode 100644 packages/ui/certd-server/src/modules/authority/enums/ResourceTypeEnum.ts create mode 100644 packages/ui/certd-server/src/modules/authority/service/permission-service.ts create mode 100644 packages/ui/certd-server/src/modules/authority/service/role-permission-service.ts create mode 100644 packages/ui/certd-server/src/modules/authority/service/role-service.ts create mode 100644 packages/ui/certd-server/src/modules/authority/service/user-role-service.ts create mode 100644 packages/ui/certd-server/src/modules/authority/service/user-service.ts create mode 100644 packages/ui/certd-server/src/modules/basic/controller/basic-controller.ts create mode 100644 packages/ui/certd-server/src/modules/basic/service/code-service.ts create mode 100644 packages/ui/certd-server/src/modules/login/controller/login-controller.ts create mode 100644 packages/ui/certd-server/src/modules/login/service/login-service.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/auto/auto-register-cron.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/controller/access-controller.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/controller/dns-provider-controller.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/controller/history-controller.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/controller/pipeline-controller.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/controller/plugin-controller.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/entity/access.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/entity/history-log.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/entity/history.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/entity/pipeline.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/entity/storage.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/entity/vo/history-detail.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/entity/vo/pipeline-detail.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/service/access-service.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/service/db-storage.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/service/dns-provider-service.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/service/history-log-service.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/service/history-service.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/service/plugin-service.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/service/storage-service.ts create mode 100644 packages/ui/certd-server/src/plugins/cron/configuration.ts create mode 100644 packages/ui/certd-server/src/plugins/cron/cron.ts create mode 100644 packages/ui/certd-server/src/plugins/cron/index.ts create mode 100644 packages/ui/certd-server/src/plugins/pipeline/index.ts create mode 100644 packages/ui/certd-server/src/plugins/pipeline/test/echo-plugin.ts create mode 100644 packages/ui/certd-server/src/utils/logger.ts create mode 100644 packages/ui/certd-server/src/utils/random.ts create mode 100644 packages/ui/certd-server/test/controller/api.test.ts create mode 100644 packages/ui/certd-server/test/controller/home.test.ts create mode 100644 packages/ui/certd-server/tsconfig.json diff --git a/packages/server/certd-client b/packages/server/certd-client deleted file mode 160000 index 8c7b3853b..000000000 --- a/packages/server/certd-client +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8c7b3853be27df392dff765d39c6c53de09418bd diff --git a/packages/server/certd-server b/packages/server/certd-server deleted file mode 160000 index a5681f154..000000000 --- a/packages/server/certd-server +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a5681f154468fc5c4aac7e4a0ce090cfffc00e9e diff --git a/packages/ui/certd-client/.browserslistrc b/packages/ui/certd-client/.browserslistrc new file mode 100644 index 000000000..214388fe4 --- /dev/null +++ b/packages/ui/certd-client/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not dead diff --git a/packages/ui/certd-client/.env b/packages/ui/certd-client/.env new file mode 100644 index 000000000..4933b20fc --- /dev/null +++ b/packages/ui/certd-client/.env @@ -0,0 +1,3 @@ +VITE_APP_API=/api +#登录与权限关闭 +VITE_APP_PM_ENABLED=true diff --git a/packages/ui/certd-client/.env.debug b/packages/ui/certd-client/.env.debug new file mode 100644 index 000000000..6a532d749 --- /dev/null +++ b/packages/ui/certd-client/.env.debug @@ -0,0 +1,2 @@ +#登录与权限开启 +VITE_APP_PM_ENABLED=false diff --git a/packages/ui/certd-client/.env.debugpm b/packages/ui/certd-client/.env.debugpm new file mode 100644 index 000000000..c3b69a80f --- /dev/null +++ b/packages/ui/certd-client/.env.debugpm @@ -0,0 +1,2 @@ +#登录与权限开启 +VITE_APP_PM_ENABLED=true diff --git a/packages/ui/certd-client/.env.pm b/packages/ui/certd-client/.env.pm new file mode 100644 index 000000000..c3b69a80f --- /dev/null +++ b/packages/ui/certd-client/.env.pm @@ -0,0 +1,2 @@ +#登录与权限开启 +VITE_APP_PM_ENABLED=true diff --git a/packages/ui/certd-client/.env.production b/packages/ui/certd-client/.env.production new file mode 100644 index 000000000..bc3dbcb73 --- /dev/null +++ b/packages/ui/certd-client/.env.production @@ -0,0 +1,3 @@ +VITE_APP_API=http://www.docmirror.cn:7001/api +#登录与权限开启 +VITE_APP_PM_ENABLED=true diff --git a/packages/ui/certd-client/.eslintignore b/packages/ui/certd-client/.eslintignore new file mode 100644 index 000000000..eb79dd5fc --- /dev/null +++ b/packages/ui/certd-client/.eslintignore @@ -0,0 +1,2 @@ +node_modules +.idea diff --git a/packages/ui/certd-client/.eslintrc.js b/packages/ui/certd-client/.eslintrc.js new file mode 100644 index 000000000..2dcd3abe2 --- /dev/null +++ b/packages/ui/certd-client/.eslintrc.js @@ -0,0 +1,76 @@ +module.exports = { + root: true, + env: { + browser: true, + node: true, + es6: true + }, + parser: "vue-eslint-parser", + parserOptions: { + parser: "@typescript-eslint/parser", + ecmaVersion: 2020, + sourceType: "module", + jsxPragma: "React", + ecmaFeatures: { + jsx: true, + tsx: true + } + }, + extends: [ + "plugin:vue/vue3-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "prettier" + ], + rules: { + //"max-len": [0, 200, 2, { ignoreUrls: true }], + "@typescript-eslint/ban-ts-ignore": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/explicit-module-boundary-types": "off" + // "@typescript-eslint/no-unused-vars": [ + // "error", + // { + // argsIgnorePattern: "^h$", + // varsIgnorePattern: "^h$", + // }, + // ], + // "no-unused-vars": [ + // "error", + // { + // argsIgnorePattern: "^h$", + // varsIgnorePattern: "^h$", + // }, + // ], + // "vue/custom-event-name-casing": "off", + // "no-use-before-define": "off", + // "space-before-function-paren": "off", + + // "vue/attributes-order": "off", + // "vue/one-component-per-file": "off", + // "vue/html-closing-bracket-newline": "off", + // "vue/max-attributes-per-line": "off", + // "vue/multiline-html-element-content-newline": "off", + // "vue/singleline-html-element-content-newline": "off", + // "vue/attribute-hyphenation": "off", + // "vue/require-default-prop": "off", + // "vue/html-self-closing": [ + // "error", + // { + // html: { + // void: "always", + // normal: "never", + // component: "always", + // }, + // svg: "always", + // math: "always", + // }, + // ], + } +}; diff --git a/packages/ui/certd-client/.gitignore b/packages/ui/certd-client/.gitignore new file mode 100644 index 000000000..980be95df --- /dev/null +++ b/packages/ui/certd-client/.gitignore @@ -0,0 +1,11 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +/stats.html +yarn.lock +.idea +/.idea/ +yarn-error.log +vite-profile.cpuprofile diff --git a/packages/ui/certd-client/.npmignore b/packages/ui/certd-client/.npmignore new file mode 100644 index 000000000..b6c09bbd8 --- /dev/null +++ b/packages/ui/certd-client/.npmignore @@ -0,0 +1,2 @@ +node_modules +/stats.html diff --git a/packages/ui/certd-client/.prettierrc b/packages/ui/certd-client/.prettierrc new file mode 100644 index 000000000..61e99f7c5 --- /dev/null +++ b/packages/ui/certd-client/.prettierrc @@ -0,0 +1,5 @@ +{ + + "trailingComma": "none", + "printWidth": 220 +} diff --git a/packages/ui/certd-client/CHANGELOG.md b/packages/ui/certd-client/CHANGELOG.md new file mode 100644 index 000000000..ccaa88d14 --- /dev/null +++ b/packages/ui/certd-client/CHANGELOG.md @@ -0,0 +1,12 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.10.5](https://github.com/fast-crud/fast-crud/compare/v0.10.4...v0.10.5) (2021-07-01) + + +### Performance Improvements + +* fs-admin 与crud demo ([4e6b20f](https://github.com/fast-crud/fast-crud/commit/4e6b20fe19434460853841f371b9fd5f16e5e2d3)) +* fs-admin纳入子模块 ([2940d30](https://github.com/fast-crud/fast-crud/commit/2940d30f419bf4bde1e8e791f1fbdb9184818285)) diff --git a/packages/ui/certd-client/LICENSE b/packages/ui/certd-client/LICENSE new file mode 100644 index 000000000..0ad25db4b --- /dev/null +++ b/packages/ui/certd-client/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/ui/certd-client/README.md b/packages/ui/certd-client/README.md new file mode 100644 index 000000000..600173994 --- /dev/null +++ b/packages/ui/certd-client/README.md @@ -0,0 +1,27 @@ +# fs-admin-antdv + +基于vue3、antdv 的 admin管理后台脚手架 +更多信息请参考: [fast-crud](https://github.com/fast-crud/fast-crud) +# server端 + +## fs-server-js +https://github.com/fast-crud/fs-server-js + + +# 感谢 + +### 依赖 +* [vue](https://github.com/vuejs/vue-next) +* [vue-router](https://github.com/vuejs/vue-router-next) +* [antdv 2x](https://github.com/vueComponent/ant-design-vue) +* [vitejs](https://github.com/vitejs/vite) +* [pinia](https://github.com/posva/pinia) +* [purge-icons](https://github.com/antfu/purge-icons) + +### 参考如下项目 +* [d2-admin](https://github.com/d2-projects/d2-admin) +* [antdv-pro](https://github.com/vueComponent/ant-design-vue-pro) +* [vben-admin](https://github.com/anncwb/vue-vben-admin) + +感谢这些优秀的项目 + diff --git a/packages/ui/certd-client/build/modify-vars.ts b/packages/ui/certd-client/build/modify-vars.ts new file mode 100644 index 000000000..c6cf20a37 --- /dev/null +++ b/packages/ui/certd-client/build/modify-vars.ts @@ -0,0 +1,11 @@ +// import { getThemeVariables } from "ant-design-vue/dist/theme"; +// import path from "path"; +// const resolve = path.resolve; +export function generateModifyVars(dark = false) { + //const modifyVars = getThemeVariables({ dark }); + // const vars = `${resolve("src/style/theme/index.less")}`; + return { + //...modifyVars + // hack: `true; @import (reference) "${vars}";` + }; +} diff --git a/packages/ui/certd-client/build/theme-colors.ts b/packages/ui/certd-client/build/theme-colors.ts new file mode 100644 index 000000000..41396ef06 --- /dev/null +++ b/packages/ui/certd-client/build/theme-colors.ts @@ -0,0 +1,72 @@ +import { generate } from "@ant-design/colors"; + +export const primaryColor = "#1890ff"; + +export const darkMode = "light"; + +type Fn = (...arg: any) => any; + +export interface GenerateColorsParams { + mixLighten: Fn; + mixDarken: Fn; + tinycolor: any; + color?: string; +} + +export function generateAntColors(color: string) { + return generate(color, { + theme: "default" + }); +} + +export function getThemeColors(color?: string) { + const tc = color || primaryColor; + const colors = generateAntColors(tc); + const primary = colors[5]; + const modeColors = generateAntColors(primary); + + return [...colors, ...modeColors]; +} + +export function generateColors({ color = primaryColor, mixLighten, mixDarken, tinycolor }: GenerateColorsParams) { + const arr = new Array(19).fill(0); + const lightens = arr.map((_t, i) => { + return mixLighten(color, i / 5); + }); + + const darkens = arr.map((_t, i) => { + return mixDarken(color, i / 5); + }); + + const alphaColors = arr.map((_t, i) => { + return tinycolor(color) + .setAlpha(i / 20) + .toRgbString(); + }); + + const shortAlphaColors = alphaColors.map((item) => item.replace(/\s/g, "").replace(/0\./g, ".")); + + const tinycolorLightens = arr + .map((_t, i) => { + return tinycolor(color) + .lighten(i * 5) + .toHexString(); + }) + .filter((item) => item !== "#ffffff"); + + const tinycolorDarkens = arr + .map((_t, i) => { + return tinycolor(color) + .darken(i * 5) + .toHexString(); + }) + .filter((item) => item !== "#000000"); + return [ + ...lightens, + ...darkens, + ...alphaColors, + ...shortAlphaColors, + ...tinycolorDarkens, + ...tinycolorLightens + ].filter((item) => !item.includes("-")); +} diff --git a/packages/ui/certd-client/build/theme-plugin.ts b/packages/ui/certd-client/build/theme-plugin.ts new file mode 100644 index 000000000..d223642b4 --- /dev/null +++ b/packages/ui/certd-client/build/theme-plugin.ts @@ -0,0 +1,68 @@ +/** + * Vite plugin for website theme color switching + * https://github.com/anncwb/vite-plugin-theme + */ +import type { Plugin } from "vite"; +import path from "path"; +import { viteThemePlugin, mixLighten, mixDarken, tinycolor, antdDarkThemePlugin } from "vite-plugin-theme"; +import { getThemeColors, generateColors } from "./theme-colors"; +import { generateModifyVars } from "./modify-vars"; + +export function configThemePlugin(isBuild: boolean): Plugin[] { + const colors = generateColors({ + mixDarken, + mixLighten, + tinycolor + }); + const colorVariables = [...getThemeColors(), ...colors]; + const plugin = [ + viteThemePlugin({ + // resolveSelector: (s) => { + // s = s.trim(); + // switch (s) { + // case ".ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon": + // return ".ant-steps-item-icon > .ant-steps-icon"; + // case ".ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)": + // case ".ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover": + // case ".ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active": + // return s; + // case ".ant-steps-item-icon > .ant-steps-icon": + // return s; + // } + // return `[data-theme] ${s}`; + // }, + resolveSelector: (s) => { + s = s.trim(); + if (s === ".ant-btn:hover,.ant-btn:focus") { + // console.log("ssss", s); + return ".theme-discard-xxxxxxx"; + } + return s; + }, + colorVariables + }), + antdDarkThemePlugin({ + preloadFiles: [ + path.resolve(process.cwd(), "node_modules/ant-design-vue/dist/antd.less"), + path.resolve(process.cwd(), "src/style/theme/index.less") + ], + filter: (id) => (isBuild ? !id.endsWith("antd.less") : true), + // extractCss: false, + darkModifyVars: { + ...generateModifyVars(true), + "text-color": "#c9d1d9", + "text-color-base": "#c9d1d9", + "component-background": "#151515", + // black: '#0e1117', + // #8b949e + "text-color-secondary": "#8b949e", + "border-color-base": "#303030", + // 'border-color-split': '#30363d', + "item-active-bg": "#111b26", + "app-content-background": "rgb(255 255 255 / 4%)" + } + }) + ]; + + return (plugin as unknown) as Plugin[]; +} diff --git a/packages/ui/certd-client/index.html b/packages/ui/certd-client/index.html new file mode 100644 index 000000000..aac3b8bfe --- /dev/null +++ b/packages/ui/certd-client/index.html @@ -0,0 +1,26 @@ + + + + + + + antdv-fast-crud + + + + + + + diff --git a/packages/ui/certd-client/package.json b/packages/ui/certd-client/package.json new file mode 100644 index 000000000..644a7cac6 --- /dev/null +++ b/packages/ui/certd-client/package.json @@ -0,0 +1,120 @@ +{ + "name": "@fast-crud/fs-admin-antdv", + "version": "1.8.1", + "private": true, + "scripts": { + "dev": "vite", + "dev:pm": "vite --mode pm", + "dev:force": "vite --force", + "debug": "vite --mode debug", + "debug:pm": "vite --mode debugpm", + "debug:force": "vite --force --mode debug", + "build": "vite build ", + "serve": "vite preview", + "preview": "vite preview", + "pretty-quick": "pretty-quick", + "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/", + "upgrade": "yarn upgrade-interactive --latest" + }, + "author": "Greper", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^6.0.0", + "@ant-design/icons-vue": "^6.0.1", + "@fast-crud/fast-crud": "^1.8.1", + "@fast-crud/fast-extends": "^1.7.2", + "@fast-crud/ui-antdv": "^1.8.1", + "@iconify/iconify": "^3.0.1", + "@iconify/json": "^2.1.151", + "@purge-icons/generated": "^0.9.0", + "@soerenmartius/vue3-clipboard": "^0.1.2", + "ant-design-vue": "^3.2.15", + "axios": "^1.2.1", + "axios-mock-adapter": "^1.21.2", + "base64-js": "^1.5.1", + "better-scroll": "^2.5.0", + "china-division": "^2.4.0", + "core-js": "^3.26.1", + "cos-js-sdk-v5": "^1.4.15-beta.0", + "cropperjs": "^1.5.13", + "deepdash-es": "5.3.5", + "highlight.js": "^11.7.0", + "humanize-duration": "^3.27.3", + "lodash-es": "^4.17.15", + "mitt": "^3.0.0", + "nanoid": "^4.0.0", + "nprogress": "^0.2.0", + "object-assign": "^4.1.1", + "pinia": "2.0.28", + "qiniu-js": "^3.4.1", + "sortablejs": "^1.14.0", + "vue": "^3.2.45", + "vue-cropperjs": "^5.0.0", + "vue-i18n": "^9.2.2", + "vue-router": "^4.1.5", + "vuedraggable": "^4.0.1" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^23.0.4", + "@rollup/plugin-node-resolve": "^15.0.1", + "@types/chai": "^4.3.4", + "@types/lodash-es": "^4.17.6", + "@types/mocha": "^10.0.1", + "@types/node": "^18.11.15", + "@typescript-eslint/eslint-plugin": "^5.46.1", + "@typescript-eslint/parser": "^5.46.1", + "@vitejs/plugin-legacy": "^3.0.1", + "@vitejs/plugin-vue": "^4.0.0", + "@vitejs/plugin-vue-jsx": "^3.0.0", + "@vue/compiler-sfc": "^3.2.45", + "@vue/eslint-config-typescript": "^11.0.2", + "@vue/test-utils": "^2.2.6", + "autoprefixer": "^10.4.12", + "caller-path": "^4.0.0", + "chai": "^4.3.7", + "eslint": "8.29.0", + "eslint-config-prettier": "^8.1.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.0.1", + "eslint-plugin-vue": "^9.8.0", + "esno": "^0.16.3", + "husky": "^8.0.2", + "less": "^4.1.3", + "less-loader": "^11.0.0", + "lint-staged": "^13.1.0", + "postcss": "^8.4.20", + "prettier": "2.8.1", + "pretty-quick": "^3.1.3", + "rimraf": "^3.0.2", + "rollup": "^3.7.4", + "rollup-plugin-visualizer": "^5.8.2", + "stylelint": "^14.16.0", + "stylelint-config-prettier": "^9.0.4", + "stylelint-order": "^5.0.0", + "tailwindcss": "^3.2.4", + "ts-node": "^10.9.1", + "typescript": "4.9.4", + "vite": "^4.0.1", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-purge-icons": "^0.9.2", + "vite-plugin-theme": "^0.8.1", + "vite-plugin-windicss": "^1.8.10", + "vue-eslint-parser": "^9.1.0", + "windicss": "^3.5.6" + }, + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged" + } + }, + "gitHead": "9c2162697f3affea22c9a8cbc0ca74f4034ab27e", + "vite": { + "optimizeDeps": { + "include": [ + "@iconify/iconify" + ] + } + } +} diff --git a/packages/ui/certd-client/public/favicon.ico b/packages/ui/certd-client/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/packages/ui/certd-client/public/images/logo-certd/logo.svg b/packages/ui/certd-client/public/images/logo-certd/logo.svg new file mode 100644 index 000000000..edcdefbed --- /dev/null +++ b/packages/ui/certd-client/public/images/logo-certd/logo.svg @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui/certd-client/public/images/logo-certd/rect-black.svg b/packages/ui/certd-client/public/images/logo-certd/rect-black.svg new file mode 100644 index 000000000..19bf439f6 --- /dev/null +++ b/packages/ui/certd-client/public/images/logo-certd/rect-black.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 让你的证书永不过期 + + + diff --git a/packages/ui/certd-client/public/images/logo-certd/rect.svg b/packages/ui/certd-client/public/images/logo-certd/rect.svg new file mode 100644 index 000000000..acc2f41c7 --- /dev/null +++ b/packages/ui/certd-client/public/images/logo-certd/rect.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 让你的证书永不过期 + + + diff --git a/packages/ui/certd-client/public/images/logo-certd/square.svg b/packages/ui/certd-client/public/images/logo-certd/square.svg new file mode 100644 index 000000000..026dd978f --- /dev/null +++ b/packages/ui/certd-client/public/images/logo-certd/square.svg @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/ui/certd-client/public/images/logo/logo.svg b/packages/ui/certd-client/public/images/logo/logo.svg new file mode 100644 index 000000000..edcdefbed --- /dev/null +++ b/packages/ui/certd-client/public/images/logo/logo.svg @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui/certd-client/public/images/logo/rect-black.svg b/packages/ui/certd-client/public/images/logo/rect-black.svg new file mode 100644 index 000000000..a57352ec1 --- /dev/null +++ b/packages/ui/certd-client/public/images/logo/rect-black.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/certd-client/public/images/logo/rect.svg b/packages/ui/certd-client/public/images/logo/rect.svg new file mode 100644 index 000000000..acc2f41c7 --- /dev/null +++ b/packages/ui/certd-client/public/images/logo/rect.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 让你的证书永不过期 + + + diff --git a/packages/ui/certd-client/public/images/logo/square.svg b/packages/ui/certd-client/public/images/logo/square.svg new file mode 100644 index 000000000..1b1395a16 --- /dev/null +++ b/packages/ui/certd-client/public/images/logo/square.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/certd-client/public/images/plugin.png b/packages/ui/certd-client/public/images/plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..23fa727cdc423af592fb70789db57f602c17f5c5 GIT binary patch literal 7742 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 Kp-R;sh5Qd%W|YeS literal 0 HcmV?d00001 diff --git a/packages/ui/certd-client/public/index.css b/packages/ui/certd-client/public/index.css new file mode 100644 index 000000000..0950b0504 --- /dev/null +++ b/packages/ui/certd-client/public/index.css @@ -0,0 +1,12 @@ +html, body, #app { height: 100%; margin: 0; padding: 0; width: 100%;} +.fs-bootstrap { background-color: #474949; height: 100%; display: flex; flex-direction: column;position: fixed;width: 100% } +.fs-bootstrap__main {flex:1; user-select: none; width: 100%; flex-grow: 1; display: flex; justify-content: center; align-items: center; flex-direction: column; } +.fs-bootstrap__footer { width: 100%; flex-grow: 0; text-align: center; padding: 10px 0; } +.fs-bootstrap__footer > a { font-size: 12px; color: #ABABAB; text-decoration: none; } +.fs-bootstrap__loading {box-sizing: border-box; height: 50px; width: 50px; margin-bottom: 5px;border:5px solid #333333;border-bottom:#aaa 5px solid; + border-radius:1000px; animation:load 1.1s infinite linear;-webkit-animation:load 1.1s infinite linear;-moz-animation:load 1.1s infinite linear; -o-animation:load 1.1s infinite linear; +} +@keyframes load {from {transform:rotate(0deg);-ms-transform:rotate(0deg);}to { transform:rotate(360deg);-ms-transform:rotate(360deg); } +}@-webkit-keyframes load {from {-webkit-transform:rotate(0deg); }to { -webkit-transform:rotate(360deg);} + }@-moz-keyframes load { from { -moz-transform:rotate(0deg); } to { -moz-transform:rotate(360deg);} + }@-o-keyframes load { from { -o-transform:rotate(0deg);} to { -o-transform:rotate(360deg);}} diff --git a/packages/ui/certd-client/public/index.html b/packages/ui/certd-client/public/index.html new file mode 100644 index 000000000..3e5a13962 --- /dev/null +++ b/packages/ui/certd-client/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ + + diff --git a/packages/ui/certd-client/public/logo.svg b/packages/ui/certd-client/public/logo.svg new file mode 100644 index 000000000..1b1395a16 --- /dev/null +++ b/packages/ui/certd-client/public/logo.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/certd-client/src/App.vue b/packages/ui/certd-client/src/App.vue new file mode 100644 index 000000000..5059cfa5c --- /dev/null +++ b/packages/ui/certd-client/src/App.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/ui/certd-client/src/api/modules/api.user.mock.ts b/packages/ui/certd-client/src/api/modules/api.user.mock.ts new file mode 100644 index 000000000..53411901b --- /dev/null +++ b/packages/ui/certd-client/src/api/modules/api.user.mock.ts @@ -0,0 +1,31 @@ +export default [ + { + path: "/login", + method: "post", + handle() { + return { + code: 0, + msg: "success", + data: { + token: "faker token", + expire: 10000 + } + }; + } + }, + { + path: "/sys/authority/user/mine", + method: "get", + handle() { + return { + code: 0, + msg: "success", + data: { + id: 1, + username: "username", + nickName: "admin" + } + }; + } + } +]; diff --git a/packages/ui/certd-client/src/api/modules/api.user.ts b/packages/ui/certd-client/src/api/modules/api.user.ts new file mode 100644 index 000000000..ea711596f --- /dev/null +++ b/packages/ui/certd-client/src/api/modules/api.user.ts @@ -0,0 +1,51 @@ +import { request, requestForMock } from "../service"; +import { env } from "/@/utils/util.env"; +/** + * @description: Login interface parameters + */ +export interface LoginReq { + username: string; + password: string; +} + +export interface UserInfoRes { + id: string | number; + username: string; + nickName: string; +} + +export interface LoginRes { + token: string; + expire: number; +} + +export async function login(data: LoginReq): Promise { + if (env.PM_ENABLED === "false") { + //没有开启权限模块,模拟登录 + return await requestForMock({ + url: "/login", + method: "post", + data + }); + } + //如果开启了登录与权限模块,则真实登录 + return await request({ + url: "/login", + method: "post", + data + }); +} + +export async function mine(): Promise { + if (env.PM_ENABLED === "false") { + //没有开启权限模块,模拟登录 + return await requestForMock({ + url: "/sys/authority/user/mine", + method: "post" + }); + } + return await request({ + url: "/sys/authority/user/mine", + method: "post" + }); +} diff --git a/packages/ui/certd-client/src/api/service.ts b/packages/ui/certd-client/src/api/service.ts new file mode 100644 index 000000000..cd8da796f --- /dev/null +++ b/packages/ui/certd-client/src/api/service.ts @@ -0,0 +1,138 @@ +import axios from "axios"; +import { get } from "lodash-es"; +import Adapter from "axios-mock-adapter"; +import { errorLog, errorCreate } from "./tools"; +import { env } from "/src/utils/util.env"; +import { useUserStore } from "../store/modules/user"; +/** + * @description 创建请求实例 + */ +function createService() { + // 创建一个 axios 实例 + const service = axios.create(); + // 请求拦截 + service.interceptors.request.use( + (config) => config, + (error) => { + // 发送失败 + console.log(error); + return Promise.reject(error); + } + ); + // 响应拦截 + service.interceptors.response.use( + (response) => { + if (response.config.responseType === "blob") { + return response; + } + // dataAxios 是 axios 返回数据中的 data + const dataAxios = response.data; + // 这个状态码是和后端约定的 + const { code } = dataAxios; + // 根据 code 进行判断 + if (code === undefined) { + // 如果没有 code 代表这不是项目后端开发的接口 比如可能是 D2Admin 请求最新版本 + errorCreate(`非标准返回:${dataAxios}, ${response.config.url}`); + return dataAxios; + } else { + // 有 code 代表这是一个后端接口 可以进行进一步的判断 + switch (code) { + case 0: + // [ 示例 ] code === 0 代表没有错误 + // @ts-ignore + if (response.config.unpack === false) { + //如果不需要解包 + return dataAxios; + } + return dataAxios.data; + default: + // 不是正确的 code + errorCreate(`${dataAxios.msg}: ${response.config.url}`); + return dataAxios; + } + } + }, + (error) => { + const status = get(error, "response.status"); + switch (status) { + case 400: + error.message = "请求错误"; + break; + case 401: + error.message = "未授权,请登录"; + break; + case 403: + error.message = "拒绝访问"; + break; + case 404: + error.message = `请求地址出错: ${error.response.config.url}`; + break; + case 408: + error.message = "请求超时"; + break; + case 500: + error.message = "服务器内部错误"; + break; + case 501: + error.message = "服务未实现"; + break; + case 502: + error.message = "网关错误"; + break; + case 503: + error.message = "服务不可用"; + break; + case 504: + error.message = "网关超时"; + break; + case 505: + error.message = "HTTP版本不受支持"; + break; + default: + break; + } + errorLog(error); + if (status === 401) { + const userStore = useUserStore(); + userStore.logout(); + } + return Promise.reject(error); + } + ); + return service; +} + +/** + * @description 创建请求方法 + * @param {Object} service axios 实例 + */ +function createRequestFunction(service) { + return function (config) { + const configDefault = { + headers: { + "Content-Type": get(config, "headers.Content-Type", "application/json") + }, + timeout: 5000, + baseURL: env.API, + data: {} + }; + const userStore = useUserStore(); + const token = userStore.getToken; + if (token != null) { + // @ts-ignore + configDefault.headers.Authorization = token; + } + return service(Object.assign(configDefault, config)); + }; +} + +// 用于真实网络请求的实例和请求方法 +export const service = createService(); +export const request = createRequestFunction(service); + +// 用于模拟网络请求的实例和请求方法 +export const serviceForMock = createService(); +export const requestForMock = createRequestFunction(serviceForMock); + +// 网络请求数据模拟工具 +export const mock = new Adapter(serviceForMock, { delayResponse: 200 }); diff --git a/packages/ui/certd-client/src/api/tools.ts b/packages/ui/certd-client/src/api/tools.ts new file mode 100644 index 000000000..80fa784bf --- /dev/null +++ b/packages/ui/certd-client/src/api/tools.ts @@ -0,0 +1,66 @@ +/** + * @description 安全地解析 json 字符串 + * @param {String} jsonString 需要解析的 json 字符串 + * @param {String} defaultValue 默认值 + */ +import { uiContext } from "@fast-crud/fast-crud"; + +export function parse(jsonString = "{}", defaultValue = {}) { + let result = defaultValue; + try { + result = JSON.parse(jsonString); + } catch (error) { + console.log(error); + } + return result; +} + +/** + * @description 接口请求返回 + * @param {Any} data 返回值 + * @param {String} msg 状态信息 + * @param {Number} code 状态码 + */ +export function response(data = {}, msg = "", code = 0) { + return [200, { code, msg, data }]; +} + +/** + * @description 接口请求返回 正确返回 + * @param {Any} data 返回值 + * @param {String} msg 状态信息 + */ +export function responseSuccess(data = {}, msg = "成功") { + return response(data, msg); +} + +/** + * @description 接口请求返回 错误返回 + * @param {Any} data 返回值 + * @param {String} msg 状态信息 + * @param {Number} code 状态码 + */ +export function responseError(data = {}, msg = "请求失败", code = 500) { + return response(data, msg, code); +} + +/** + * @description 记录和显示错误 + * @param {Error} error 错误对象 + */ +export function errorLog(error) { + // 打印到控制台 + console.error(error); + // 显示提示 + uiContext.get().notification.error({ message: error.message }); +} + +/** + * @description 创建一个错误 + * @param {String} msg 错误信息 + */ +export function errorCreate(msg) { + const error = new Error(msg); + errorLog(error); + throw error; +} diff --git a/packages/ui/certd-client/src/assets/background.svg b/packages/ui/certd-client/src/assets/background.svg new file mode 100644 index 000000000..89c259764 --- /dev/null +++ b/packages/ui/certd-client/src/assets/background.svg @@ -0,0 +1,69 @@ + + + + Group 21 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/ui/certd-client/src/assets/logo.png b/packages/ui/certd-client/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 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 +
+
+
+
+ +
+
+ +
+ +
+
+
+ + + + + diff --git a/packages/ui/certd-client/src/components/dns-provider-selector/api.ts b/packages/ui/certd-client/src/components/dns-provider-selector/api.ts new file mode 100644 index 000000000..4f6f98e7a --- /dev/null +++ b/packages/ui/certd-client/src/components/dns-provider-selector/api.ts @@ -0,0 +1,10 @@ +import { request } from "/src/api/service"; + +const apiPrefix = "/pi/dnsProvider"; + +export async function GetList() { + return await request({ + url: apiPrefix + "/list", + method: "post" + }); +} diff --git a/packages/ui/certd-client/src/components/dns-provider-selector/index.vue b/packages/ui/certd-client/src/components/dns-provider-selector/index.vue new file mode 100644 index 000000000..855c53360 --- /dev/null +++ b/packages/ui/certd-client/src/components/dns-provider-selector/index.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/packages/ui/certd-client/src/components/editable.vue b/packages/ui/certd-client/src/components/editable.vue new file mode 100644 index 000000000..2c090322a --- /dev/null +++ b/packages/ui/certd-client/src/components/editable.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/packages/ui/certd-client/src/components/index.ts b/packages/ui/certd-client/src/components/index.ts new file mode 100644 index 000000000..7992c5e7e --- /dev/null +++ b/packages/ui/certd-client/src/components/index.ts @@ -0,0 +1,18 @@ +import PiContainer from "./container.vue"; +import PiAccessSelector from "../views/certd/access/access-selector/index.vue"; +import PiDnsProviderSelector from "./dns-provider-selector/index.vue"; +import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";import PiEditable from "./editable.vue"; +import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue"; +export default { + install(app) { + app.component("PiContainer", PiContainer); + app.component("PiAccessSelector", PiAccessSelector); + app.component("PiEditable", PiEditable); + app.component("PiOutputSelector", PiOutputSelector); + app.component("PiDnsProviderSelector", PiDnsProviderSelector); + + app.component("CheckCircleOutlined", CheckCircleOutlined); + app.component("InfoCircleOutlined", InfoCircleOutlined); + app.component("UndoOutlined", UndoOutlined); + } +}; diff --git a/packages/ui/certd-client/src/i18n/index.ts b/packages/ui/certd-client/src/i18n/index.ts new file mode 100644 index 000000000..2d432976d --- /dev/null +++ b/packages/ui/certd-client/src/i18n/index.ts @@ -0,0 +1,30 @@ +// @ts-ignore +import { createI18n } from "vue-i18n"; +// +// @ts-ignore +import enFsLocale from "@fast-crud/fast-crud/dist/locale/lang/en.js"; +// @ts-ignore +import zhFsLocale from "@fast-crud/fast-crud/dist/locale/lang/zh-cn.js"; +import en from "./locale/en"; +import zh from "./locale/zh_CN"; +const messages = { + en: { + label: "English", + // 定义您自己的字典,但是请不要和 `fs` 重复,这样会导致 fast-crud 内部组件的翻译失效. + fs: enFsLocale.fs, + ...en + }, + "zh-cn": { + label: "简体中文", + // 定义您自己的字典,但是请不要和 `fs` 重复,这样会导致 fast-crud 内部组件的翻译失效. + fs: zhFsLocale.fs, + ...zh + } +}; + +export default createI18n({ + legacy: false, + locale: "zh-cn", + fallbackLocale: "zh-cn", + messages +}); diff --git a/packages/ui/certd-client/src/i18n/locale/en.ts b/packages/ui/certd-client/src/i18n/locale/en.ts new file mode 100644 index 000000000..d59a415d1 --- /dev/null +++ b/packages/ui/certd-client/src/i18n/locale/en.ts @@ -0,0 +1,3 @@ +export default { + app: { crud: { i18n: { name: "name", city: "city", status: "status" } } } +}; diff --git a/packages/ui/certd-client/src/i18n/locale/zh_CN.ts b/packages/ui/certd-client/src/i18n/locale/zh_CN.ts new file mode 100644 index 000000000..cfb247c36 --- /dev/null +++ b/packages/ui/certd-client/src/i18n/locale/zh_CN.ts @@ -0,0 +1,9 @@ +export default { + app: { + crud: { i18n: { name: "姓名", city: "城市", status: "状态" } }, + login: { + logoutTip: "确认", + logoutMessage: "确定要注销登录吗?" + } + } +}; diff --git a/packages/ui/certd-client/src/layout/components/contextmenu/components/contentmenuList/index.vue b/packages/ui/certd-client/src/layout/components/contextmenu/components/contentmenuList/index.vue new file mode 100644 index 000000000..6e3866f7a --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/contextmenu/components/contentmenuList/index.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/packages/ui/certd-client/src/layout/components/contextmenu/index.vue b/packages/ui/certd-client/src/layout/components/contextmenu/index.vue new file mode 100644 index 000000000..664f41328 --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/contextmenu/index.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/packages/ui/certd-client/src/layout/components/locale/index.vue b/packages/ui/certd-client/src/layout/components/locale/index.vue new file mode 100644 index 000000000..87f8d92ac --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/locale/index.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/packages/ui/certd-client/src/layout/components/menu/index.jsx b/packages/ui/certd-client/src/layout/components/menu/index.jsx new file mode 100644 index 000000000..cb2cb58dd --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/menu/index.jsx @@ -0,0 +1,224 @@ +import { useRoute, useRouter } from "vue-router"; +import { ref, watch, onMounted, onUnmounted, resolveComponent, nextTick, defineComponent } from "vue"; +import getEachDeep from "deepdash-es/getEachDeep"; +import _ from "lodash-es"; +import BScroll from "better-scroll"; +import "./index.less"; +const eachDeep = getEachDeep(_); + +function useBetterScroll(enabled = true) { + let bsRef = ref(null); + let asideMenuRef = ref(); + + let onOpenChange = () => {}; + if (enabled) { + function bsInit() { + if (asideMenuRef.value == null) { + return; + } + bsRef.value = new BScroll(asideMenuRef.value, { + mouseWheel: true, + click: true, + momentum: false, + // 如果你愿意可以打开显示滚动条 + scrollbar: { + fade: true, + interactive: false + }, + bounce: false + }); + } + + function bsDestroy() { + if (bsRef.value != null && bsRef.value.destroy) { + try { + bsRef.value.destroy(); + } catch (e) { + // console.error(e); + } finally { + bsRef.value = null; + } + } + } + + onMounted(() => { + bsInit(); + }); + + onUnmounted(() => { + bsDestroy(); + }); + onOpenChange = async () => { + console.log("onOpenChange"); + setTimeout(() => { + bsRef.value?.refresh(); + }, 300); + }; + } + return { + onOpenChange, + asideMenuRef + }; +} +export default defineComponent({ + name: "FsMenu", + inheritAttrs: true, + props: { + menus: {}, + expandSelected: { + default: false + }, + scroll: {} + }, + setup(props, ctx) { + async function open(path) { + if (path == null) { + return; + } + if (path.startsWith("http://") || path.startsWith("https://")) { + window.open(path); + return; + } + try { + const navigationResult = await router.push(path); + if (navigationResult) { + // 导航被阻止 + } else { + // 导航成功 (包括重新导航的情况) + } + } catch (e) { + console.error("导航失败", e); + } + } + function onSelect(item) { + open(item.key); + } + + const FsIcon = resolveComponent("FsIcon"); + + const buildMenus = (children) => { + const slots = []; + if (children == null) { + return slots; + } + for (let sub of children) { + const title = () => { + if (sub?.meta?.icon) { + return ( +
+ + {sub.title} +
+ ); + } + return sub.title; + }; + if (sub.children && sub.children.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const subSlots = { + default: () => { + return buildMenus(sub.children); + }, + title + }; + function onTitleClick() { + if (sub.path && ctx.attrs.mode === "horizontal") { + open(sub.path); + } + } + slots.push(); + } else { + slots.push( + + {title} + + ); + } + } + return slots; + }; + const slots = { + default() { + return buildMenus(props.menus); + } + }; + const selectedKeys = ref([]); + const openKeys = ref([]); + const route = useRoute(); + const router = useRouter(); + + function openSelectedParents(fullPath) { + if (!props.expandSelected) { + return; + } + if (props.menus == null) { + return; + } + const keys = []; + let changed = false; + eachDeep(props.menus, (value, key, parent, context) => { + if (value == null) { + return; + } + if (value.path === fullPath) { + _.forEach(context.parents, (item) => { + if (item.value instanceof Array) { + return; + } + keys.push(item.value.index); + }); + } + }); + if (keys.length > 0) { + for (let key of keys) { + if (openKeys.value.indexOf(key) === -1) { + openKeys.value.push(key); + changed = true; + } + } + } + return changed; + } + + const { asideMenuRef, onOpenChange } = useBetterScroll(props.scroll); + + watch( + () => { + return route.fullPath; + }, + (path) => { + // path = route.fullPath; + selectedKeys.value = [path]; + const changed = openSelectedParents(path); + if (changed) { + onOpenChange(); + } + }, + { + immediate: true + } + ); + return () => { + const menu = ( + + ); + const classNames = { "fs-menu-wrapper": true, "fs-menu-better-scroll": props.scroll }; + return ( +
+ {menu} +
+ ); + }; + } +}); diff --git a/packages/ui/certd-client/src/layout/components/menu/index.less b/packages/ui/certd-client/src/layout/components/menu/index.less new file mode 100644 index 000000000..9521a4070 --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/menu/index.less @@ -0,0 +1,11 @@ +.fs-menu-wrapper{ + height: 100%; + overflow-y: auto; + &.fs-menu-better-scroll{ + overflow-y: hidden; + } + .menu-item-title{ + display: flex; + align-items: center; + } +} diff --git a/packages/ui/certd-client/src/layout/components/source-link/index.vue b/packages/ui/certd-client/src/layout/components/source-link/index.vue new file mode 100644 index 000000000..9d16a5021 --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/source-link/index.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/packages/ui/certd-client/src/layout/components/tabs/index.vue b/packages/ui/certd-client/src/layout/components/tabs/index.vue new file mode 100644 index 000000000..21ddddb8c --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/tabs/index.vue @@ -0,0 +1,295 @@ + + + + diff --git a/packages/ui/certd-client/src/layout/components/theme/color-picker.vue b/packages/ui/certd-client/src/layout/components/theme/color-picker.vue new file mode 100644 index 000000000..599baf34e --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/theme/color-picker.vue @@ -0,0 +1,101 @@ + + + + diff --git a/packages/ui/certd-client/src/layout/components/theme/index.vue b/packages/ui/certd-client/src/layout/components/theme/index.vue new file mode 100644 index 000000000..bc8a92c05 --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/theme/index.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/ui/certd-client/src/layout/components/user-info/index.vue b/packages/ui/certd-client/src/layout/components/user-info/index.vue new file mode 100644 index 000000000..24af19850 --- /dev/null +++ b/packages/ui/certd-client/src/layout/components/user-info/index.vue @@ -0,0 +1,40 @@ + + diff --git a/packages/ui/certd-client/src/layout/layout-framework.vue b/packages/ui/certd-client/src/layout/layout-framework.vue new file mode 100644 index 000000000..0e57eab86 --- /dev/null +++ b/packages/ui/certd-client/src/layout/layout-framework.vue @@ -0,0 +1,230 @@ + + + + diff --git a/packages/ui/certd-client/src/layout/layout-outside.vue b/packages/ui/certd-client/src/layout/layout-outside.vue new file mode 100644 index 000000000..a35b8c5e1 --- /dev/null +++ b/packages/ui/certd-client/src/layout/layout-outside.vue @@ -0,0 +1,154 @@ + + + + diff --git a/packages/ui/certd-client/src/layout/layout-pass.vue b/packages/ui/certd-client/src/layout/layout-pass.vue new file mode 100644 index 000000000..81c8aa3f3 --- /dev/null +++ b/packages/ui/certd-client/src/layout/layout-pass.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/packages/ui/certd-client/src/main.ts b/packages/ui/certd-client/src/main.ts new file mode 100644 index 000000000..776522909 --- /dev/null +++ b/packages/ui/certd-client/src/main.ts @@ -0,0 +1,23 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import router from "./router"; +import Antd from "ant-design-vue"; +import "ant-design-vue/dist/antd.less"; +// import "virtual:windi.css"; +import "./style/common.less"; +import "./mock"; +import i18n from "./i18n"; +import store from "./store"; + +import plugin from "./plugin/"; +import components from "./components"; +// @ts-ignore +const app = createApp(App); +// 尽量 +app.use(Antd); +app.use(router); +app.use(i18n); +app.use(store); +app.use(plugin, { i18n }); +app.use(components); +app.mount("#app"); diff --git a/packages/ui/certd-client/src/mock/base.js b/packages/ui/certd-client/src/mock/base.js new file mode 100644 index 000000000..1cacbac71 --- /dev/null +++ b/packages/ui/certd-client/src/mock/base.js @@ -0,0 +1,280 @@ +import _ from "lodash-es"; +function copyList(originList, newList, options, parentId) { + for (const item of originList) { + const newItem = { ...item, parentId }; + newItem.id = ++options.idGenerator; + newList.push(newItem); + if (item.children != null) { + newItem.children = []; + copyList(item.children, newItem.children, options, newItem.id); + } + } +} + +function delById(req, list) { + for (let i = 0; i < list.length; i++) { + const item = list[i]; + console.log("remove i", i, req, req.params.id, item.id); + if (item.id === parseInt(req.params.id)) { + console.log("remove i", i); + list.splice(i, 1); + break; + } + if (item.children != null && item.children.length > 0) { + delById(req, item.children); + } + } +} + +function findById(id, list) { + for (const item of list) { + if (item.id === id) { + return item; + } + if (item.children != null && item.children.length > 0) { + const sub = findById(id, item.children); + if (sub != null) { + return sub; + } + } + } +} +export default { + findById, + buildMock(options) { + const name = options.name; + if (options.copyTimes == null) { + options.copyTimes = 29; + } + const list = []; + for (let i = 0; i < options.copyTimes; i++) { + copyList(options.list, list, options); + } + options.list = list; + return [ + { + path: "/mock/" + name + "/page", + method: "get", + handle(req) { + let data = [...list]; + let limit = 20; + let offset = 0; + for (const item of list) { + if (item.children != null && item.children.length === 0) { + item.hasChildren = false; + item.lazy = false; + } + } + let orderProp, orderAsc; + if (req && req.body) { + const { page, query, sort } = req.body; + if (page.limit != null) { + limit = parseInt(page.limit); + } + if (page.offset != null) { + offset = parseInt(page.offset); + } + orderProp = sort.prop; + orderAsc = sort.asc; + + if (Object.keys(query).length > 0) { + data = list.filter((item) => { + let allFound = true; // 是否所有条件都符合 + for (const key in query) { + // 判定某一个条件 + const value = query[key]; + if (value == null || value === "") { + continue; + } + if (value instanceof Array) { + // 如果条件中的value是数组的话,只要查到一个就行 + if (value.length === 0) { + continue; + } + let found = false; + for (const i of value) { + if (item[key] instanceof Array) { + for (const j of item[key]) { + if (i === j) { + found = true; + break; + } + } + if (found) { + break; + } + } else if (item[key] === i || (typeof item[key] === "string" && item[key].indexOf(i + "") >= 0)) { + found = true; + break; + } + if (found) { + break; + } + } + if (!found) { + allFound = false; + } + } else if (value instanceof Object) { + for (const key2 in value) { + const v = value[key2]; + if (v && item[key] && v !== item[key][key2]) { + return false; + } + } + } else if (item[key] !== value) { + allFound = false; + } + } + return allFound; + }); + } + } + + const start = offset; + let end = offset + limit; + if (data.length < end) { + end = data.length; + } + + if (orderProp) { + // 排序 + data.sort((a, b) => { + let ret = 0; + if (a[orderProp] > b[orderProp]) { + ret = 1; + } else { + ret = -1; + } + return orderAsc ? ret : -ret; + }); + } + + const records = data.slice(start, end); + const lastOffset = data.length - (data.length % limit); + if (offset > lastOffset) { + offset = lastOffset; + } + return { + code: 0, + msg: "success", + data: { + records: records, + total: data.length, + limit, + offset + } + }; + } + }, + { + path: "/mock/" + name + "/get", + method: "get", + handle(req) { + let id = req.params.id; + id = parseInt(id); + let current = null; + for (const item of list) { + if (item.id === id) { + current = item; + break; + } + } + return { + code: 0, + msg: "success", + data: current + }; + } + }, + { + path: "/mock/" + name + "/add", + method: "post", + handle(req) { + req.body.id = ++options.idGenerator; + list.unshift(req.body); + return { + code: 0, + msg: "success", + data: req.body.id + }; + } + }, + { + path: "/mock/" + name + "/update", + method: "post", + handle(req) { + const item = findById(req.body.id, list); + if (item) { + _.mergeWith(item, req.body, (objValue, srcValue) => { + if (srcValue == null) { + return; + } + // 如果被合并对象为数组,则直接被覆盖对象覆盖,只要覆盖对象不为空 + if (_.isArray(objValue)) { + return srcValue; + } + }); + } + return { + code: 0, + msg: "success", + data: null + }; + } + }, + { + path: "/mock/" + name + "/delete", + method: "post", + handle(req) { + delById(req, list); + return { + code: 0, + msg: "success", + data: null + }; + } + }, + { + path: "/mock/" + name + "/batchDelete", + method: "post", + handle(req) { + const ids = req.body.ids; + for (let i = list.length - 1; i >= 0; i--) { + const item = list[i]; + if (ids.indexOf(item.id) >= 0) { + list.splice(i, 1); + } + } + return { + code: 0, + msg: "success", + data: null + }; + } + }, + { + path: "/mock/" + name + "/delete", + method: "post", + handle(req) { + delById(req, list); + return { + code: 0, + msg: "success", + data: null + }; + } + }, + { + path: "/mock/" + name + "/all", + method: "post", + handle(req) { + return { + code: 0, + msg: "success", + data: list + }; + } + } + ]; + } +}; diff --git a/packages/ui/certd-client/src/mock/common/cascader-data.js b/packages/ui/certd-client/src/mock/common/cascader-data.js new file mode 100644 index 000000000..2b22016ff --- /dev/null +++ b/packages/ui/certd-client/src/mock/common/cascader-data.js @@ -0,0 +1,268 @@ +export default [ + { + value: "zhinan", + label: "指南", + children: [ + { + value: "shejiyuanze", + label: "设计原则", + children: [ + { + value: "yizhi", + label: "一致" + }, + { + value: "fankui", + label: "反馈" + }, + { + value: "xiaolv", + label: "效率" + }, + { + value: "kekong", + label: "可控" + } + ] + }, + { + value: "daohang", + label: "导航", + children: [ + { + value: "cexiangdaohang", + label: "侧向导航" + }, + { + value: "dingbudaohang", + label: "顶部导航" + } + ] + } + ] + }, + { + value: "zujian", + label: "组件", + children: [ + { + value: "basic", + label: "Basic", + children: [ + { + value: "layout", + label: "Layout 布局" + }, + { + value: "color", + label: "Color 色彩" + }, + { + value: "typography", + label: "Typography 字体" + }, + { + value: "icon", + label: "Icon 图标" + }, + { + value: "button", + label: "Button 按钮" + } + ] + }, + { + value: "form", + label: "Form", + children: [ + { + value: "radio", + label: "Radio 单选框" + }, + { + value: "checkbox", + label: "Checkbox 多选框" + }, + { + value: "input", + label: "Input 输入框" + }, + { + value: "input-number", + label: "InputNumber 计数器" + }, + { + value: "select", + label: "Select 选择器" + }, + { + value: "cascader", + label: "Cascader 级联选择器" + }, + { + value: "switch", + label: "Switch 开关" + }, + { + value: "slider", + label: "Slider 滑块" + }, + { + value: "time-picker", + label: "TimePicker 时间选择器" + }, + { + value: "date-picker", + label: "DatePicker 日期选择器" + }, + { + value: "datetime-picker", + label: "DateTimePicker 日期时间选择器" + }, + { + value: "upload", + label: "Upload 上传" + }, + { + value: "rate", + label: "Rate 评分" + }, + { + value: "form1", + label: "Form 表单" + } + ] + }, + { + value: "data", + label: "Data", + children: [ + { + value: "table", + label: "Table 表格" + }, + { + value: "tag", + label: "Tag 标签" + }, + { + value: "progress", + label: "Progress 进度条" + }, + { + value: "tree", + label: "Tree 树形控件" + }, + { + value: "pagination", + label: "Pagination 分页" + }, + { + value: "badge", + label: "Badge 标记" + } + ] + }, + { + value: "notice", + label: "Notice", + children: [ + { + value: "alert", + label: "Alert 警告" + }, + { + value: "loading", + label: "Loading 加载" + }, + { + value: "message", + label: "Message 消息提示" + }, + { + value: "message-box", + label: "MessageBox 弹框" + }, + { + value: "notification", + label: "Notification 通知" + } + ] + }, + { + value: "navigation", + label: "Navigation", + children: [ + { + value: "menu", + label: "NavMenu 导航菜单" + }, + { + value: "tabs", + label: "Tabs 标签页" + }, + { + value: "breadcrumb", + label: "Breadcrumb 面包屑" + }, + { + value: "dropdown", + label: "Dropdown 下拉菜单" + }, + { + value: "steps", + label: "Steps 步骤条" + } + ] + }, + { + value: "others", + label: "Others", + children: [ + { + value: "dialog", + label: "Dialog 对话框" + }, + { + value: "tooltip", + label: "Tooltip 文字提示" + }, + { + value: "popover", + label: "Popover 弹出框" + }, + { + value: "card", + label: "Card 卡片" + }, + { + value: "carousel", + label: "Carousel 走马灯" + }, + { + value: "collapse", + label: "Collapse 折叠面板" + } + ] + } + ] + }, + { + value: "ziyuan", + label: "资源", + children: [ + { + value: "axure", + label: "Axure Components" + }, + { + value: "sketch", + label: "Sketch Templates" + }, + { + value: "jiaohu", + label: "组件交互文档" + } + ] + } +]; diff --git a/packages/ui/certd-client/src/mock/common/mock.dict.js b/packages/ui/certd-client/src/mock/common/mock.dict.js new file mode 100644 index 000000000..d758ddd4e --- /dev/null +++ b/packages/ui/certd-client/src/mock/common/mock.dict.js @@ -0,0 +1,123 @@ +import cascaderData from "./cascader-data"; +import pcaDataLittle from "./pca-data-little"; +import { TreeNodesLazyLoader, getPcaData } from "./pcas-data"; + +const openStatus = [ + { value: "1", label: "打开", color: "success",icon:"ion:radio-button-on" }, + { value: "2", label: "停止", color: "cyan" }, + { value: "0", label: "关闭", color: "red",icon:"ion:radio-button-off" } +]; + +const moreOpenStatus = [ + { value: "1", label: "打开(open)", color: "success" }, + { value: "2", label: "停止(stop)", color: "cyan" }, + { value: "0", label: "关闭(close)", color: "red" } +]; + +const textStatus = [ + { id: "1", text: "打开", color: "success" }, + { id: "2", text: "停止", color: "cyan" }, + { id: "0", text: "关闭", color: "red" } +]; + +export function GetTreeChildrenByParentId(parentId) { + return TreeNodesLazyLoader.getChildren(parentId); +} + +export function GetNodesByValues(values) { + return TreeNodesLazyLoader.getNodesByValues(values); +} + +export default [ + { + path: "/mock/dicts/OpenStatusEnum", + method: "get", + handle() { + return { + code: 0, + msg: "success", + data: openStatus + }; + } + }, + { + path: "/mock/dicts/_OpenStatusEnum2", + method: "get", + handle() { + return { + code: 0, + msg: "success", + data: textStatus + }; + } + }, + { + path: "/mock/dicts/moreOpenStatusEnum", + method: "get", + handle() { + return { + code: 0, + msg: "success", + data: moreOpenStatus + }; + } + }, + { + path: "/mock/dicts/cascaderData", + method: "get", + handle() { + return { + code: 0, + msg: "success", + data: cascaderData + }; + } + }, + { + path: "/mock/dicts/pca", + method: "get", + async handle() { + const data = await getPcaData(); + return { + code: 0, + msg: "success", + data: data + }; + } + }, + { + path: "/mock/dicts/littlePca", + method: "get", + async handle() { + return { + code: 0, + msg: "success", + data: pcaDataLittle + }; + } + }, + { + path: "/mock/tree/GetTreeChildrenByParentId", + method: "get", + async handle({ params }) { + const list = await GetTreeChildrenByParentId(params.parentId); + return { + code: 0, + msg: "success", + data: list + }; + } + }, + { + path: "/mock/tree/GetNodesByValues", + method: "get", + async handle({ params }) { + const list = await GetNodesByValues(params.values); + return { + code: 0, + msg: "success", + data: list + }; + } + } +]; diff --git a/packages/ui/certd-client/src/mock/common/pca-data-little.js b/packages/ui/certd-client/src/mock/common/pca-data-little.js new file mode 100644 index 000000000..ae5aa367a --- /dev/null +++ b/packages/ui/certd-client/src/mock/common/pca-data-little.js @@ -0,0 +1,70 @@ +export default [ + { + code: "1", + name: "北京", + children: [ + { + code: "2", + name: "北京市区", + children: [ + { + code: "3", + name: "海淀" + }, + { + code: "4", + name: "朝阳" + } + ] + }, + { + code: "5", + name: "北京郊区", + children: [ + { + code: "6", + name: "海淀郊区" + }, + { + code: "7", + name: "朝阳郊区" + } + ] + } + ] + }, + { + code: "11", + name: "深圳", + children: [ + { + code: "12", + name: "深圳市区", + children: [ + { + code: "13", + name: "南山" + }, + { + code: "14", + name: "福田" + } + ] + }, + { + code: "15", + name: "深圳郊区", + children: [ + { + code: "16", + name: "南山郊区" + }, + { + code: "17", + name: "福田郊区" + } + ] + } + ] + } +]; diff --git a/packages/ui/certd-client/src/mock/common/pcas-data.js b/packages/ui/certd-client/src/mock/common/pcas-data.js new file mode 100644 index 000000000..c1bbb64a7 --- /dev/null +++ b/packages/ui/certd-client/src/mock/common/pcas-data.js @@ -0,0 +1,87 @@ +import _ from "lodash-es"; +export async function getPcasData() { + const pcasData = () => import("china-division/dist/pcas-code.json"); + const ret = await pcasData(); + return ret.default; +} +export async function getPcaData() { + const pcaData = () => import("china-division/dist/pca-code.json"); + const ret = await pcaData(); + return ret.default; +} +export const TreeNodesLazyLoader = { + getNodesByValues(values) { + console.log("getNodesByValues", values); + if (!(values instanceof Array)) { + values = [values]; + } + return getPcasData().then((data) => { + const nodes = []; + for (const value of values) { + const found = this.getNode(data, value); + if (found) { + const target = _.cloneDeep(found); + delete target.children; + nodes.push(target); + } + } + return nodes; + }); + }, + getNode(list, value) { + for (const item of list) { + if (item.code === value) { + return item; + } + if (item.children && item.children.length > 0) { + const found = this.getNode(item.children, value); + if (found) { + return found; + } + } + } + }, + getChildren(parent) { + return getPcasData().then((data) => { + const list = this.getChildrenByParent(parent, data); + if (list == null) { + return []; + } + return this.cloneAndDeleteChildren(list); + }); + }, + getChildrenByParent(parentId, tree) { + if (!parentId) { + // 取第一级 + return tree; + } else { + for (const node of tree) { + if (node.code === parentId) { + return node.children; + } + if (node.children && node.children.length > 0) { + // 递归查找 + const list = this.getChildrenByParent(parentId, node.children); + if (list) { + return list; + } + } + } + } + }, + cloneAndDeleteChildren(list) { + const newList = []; + for (const node of list) { + const newNode = {}; + Object.assign(newNode, node); + if (newNode.children == null || newNode.children.length === 0) { + newNode.isLeaf = true; + newNode.leaf = true; + } + delete newNode.children; + newList.push(newNode); + } + console.log("found children:", newList); + return newList; + } +}; diff --git a/packages/ui/certd-client/src/mock/index.js b/packages/ui/certd-client/src/mock/index.js new file mode 100644 index 000000000..4494fc5f9 --- /dev/null +++ b/packages/ui/certd-client/src/mock/index.js @@ -0,0 +1,49 @@ +import { mock } from "../api/service"; +import * as tools from "../api/tools"; +import _ from "lodash-es"; +const commonMocks = import.meta.globEager("./common/mock.*.js"); +const apiMocks = import.meta.globEager("../api/modules/*.mock.ts"); +const viewMocks = import.meta.globEager("../views/**/mock.js"); + +const list = []; +_.forEach(commonMocks, (value) => { + list.push(value.default); +}); +_.forEach(apiMocks, (value) => { + list.push(value.default); +}); +_.forEach(viewMocks, (value) => { + list.push(value.default); +}); + +list.forEach((apiFile) => { + for (const item of apiFile) { + mock.onAny(new RegExp(item.path)).reply(async (config) => { + console.log("------------fake request start -------------"); + console.log("request:", config); + const data = config.data ? JSON.parse(config.data) : {}; + const query = config.url.indexOf("?") >= 0 ? config.url.substring(config.url.indexOf("?") + 1) : undefined; + const params = config.params || {}; + if (query) { + const arr = query.split("&"); + for (const item of arr) { + const kv = item.split("="); + params[kv[0]] = kv[1]; + } + } + + const req = { + body: data, + params: params + }; + const ret = await item.handle(req); + console.log("response:", ret); + console.log("------------fake request end-------------"); + if (ret.code === 0) { + return tools.responseSuccess(ret.data, ret.msg); + } else { + return tools.responseError(ret.data, ret.msg, ret.code); + } + }); + } +}); diff --git a/packages/ui/certd-client/src/plugin/fast-crud/index.ts b/packages/ui/certd-client/src/plugin/fast-crud/index.ts new file mode 100644 index 000000000..c89c25faf --- /dev/null +++ b/packages/ui/certd-client/src/plugin/fast-crud/index.ts @@ -0,0 +1,225 @@ +import { request, requestForMock } from "/src/api/service"; +import "/src/mock"; +import UiAntdv from "@fast-crud/ui-antdv"; +import { FastCrud, UseCrudProps, useTypes, setLogger } from "@fast-crud/fast-crud"; +import "@fast-crud/fast-crud/dist/style.css"; +import { FsExtendsUploader, FsExtendsEditor, FsExtendsJson, FsExtendsCopyable, FsExtendsTime } from "@fast-crud/fast-extends"; +import "@fast-crud/fast-extends/dist/style.css"; + +import { useCrudPermission } from "../permission"; + +function install(app, options: any = {}) { + app.use(UiAntdv); + //设置日志级别 + setLogger({ level: "debug" }); + app.use(FastCrud, { + i18n: options.i18n, + async dictRequest({ url }) { + if (url && url.startsWith("/mock")) { + //如果是crud开头的dict请求视为mock + return await requestForMock({ url, method: "post" }); + } + return await request({ url, method: "post" }); + }, + /** + * useCrud时会被执行 + * @param context,useCrud的参数 + */ + commonOptions(context: UseCrudProps) { + const crudBinding = context.expose?.crudBinding; + const opts = { + table: { + size: "small", + pagination: false, + onResizeColumn: (w, col) => { + crudBinding.value.table.columnsMap[col.key].width = w; + } + }, + rowHandle: { + buttons: { + view: { type: "link", text: null, icon: "ion:eye-outline" }, + edit: { type: "link", text: null, icon: "ion:create-outline" }, + remove: { type: "link", style: { color: "red" }, text: null, icon: "ion:trash-outline" } + }, + dropdown: { + more: { + type: "link" + } + } + }, + request: { + transformQuery: ({ page, form, sort }) => { + const limit = page.pageSize; + const currentPage = page.currentPage ?? 1; + const offset = limit * (currentPage - 1); + + sort = sort == null ? {} : sort; + + return { + page: { + limit, + offset + }, + query: form, + sort + }; + }, + transformRes: ({ res }) => { + const pageSize = res.limit; + let currentPage = res.offset / pageSize; + if (res.offset % pageSize === 0) { + currentPage++; + } + return { currentPage, pageSize, ...res }; + } + }, + form: { + display: "flex", + labelCol: { + //固定label宽度 + span: null, + style: { + width: "120px" + } + }, + wrapperCol: { + span: null + } + } + }; + + // 从 useCrud({permission}) 里获取permission参数,去设置各个按钮的权限 + const crudPermission = useCrudPermission({ permission: context.permission }); + return crudPermission.merge(opts); + } + }); + + // fast-extends里面的扩展组件均为异步组件,只有在使用时才会被加载,并不会影响首页加载速度 + //安装uploader 公共参数 + app.use(FsExtendsUploader, { + defaultType: "cos", + cos: { + domain: "https://d2p-demo-1251260344.cos.ap-guangzhou.myqcloud.com", + bucket: "d2p-demo-1251260344", + region: "ap-guangzhou", + secretId: "", // + secretKey: "", // 传了secretKey 和secretId 代表使用本地签名模式(不安全,生产环境不推荐) + getAuthorization(custom) { + // 不传secretKey代表使用临时签名模式,此时此参数必传(安全,生产环境推荐) + return request({ + url: "http://www.docmirror.cn:7070/api/upload/cos/getAuthorization", + method: "get" + }).then((ret) => { + // 返回结构如下 + // ret.data:{ + // TmpSecretId, + // TmpSecretKey, + // XCosSecurityToken, + // ExpiredTime, // SDK 在 ExpiredTime 时间前,不会再次调用 getAuthorization + // } + return ret; + }); + }, + successHandle(ret) { + // 上传完成后可以在此处处理结果,修改url什么的 + console.log("success handle:", ret); + return ret; + } + }, + alioss: { + domain: "https://d2p-demo.oss-cn-shenzhen.aliyuncs.com", + bucket: "d2p-demo", + region: "oss-cn-shenzhen", + accessKeyId: "", + accessKeySecret: "", + async getAuthorization(custom, context) { + // 不传accessKeySecret代表使用临时签名模式,此时此参数必传(安全,生产环境推荐) + const ret = await request({ + url: "http://www.docmirror.cn:7070/api/upload/alioss/getAuthorization", + method: "get" + }); + console.log("ret", ret); + return ret; + }, + sdkOpts: { + // sdk配置 + secure: true // 默认为非https上传,为了安全,设置为true + }, + successHandle(ret) { + // 上传完成后可以在此处处理结果,修改url什么的 + console.log("success handle:", ret); + return ret; + } + }, + qiniu: { + bucket: "d2p-demo", + async getToken(options) { + const ret = await request({ + url: "http://www.docmirror.cn:7070/api/upload/qiniu/getToken", + method: "get" + }); + return ret; // {token:xxx,expires:xxx} + }, + successHandle(ret) { + // 上传完成后可以在此处处理结果,修改url什么的 + console.log("success handle:", ret); + return ret; + }, + domain: "http://d2p.file.handsfree.work/" + }, + form: { + action: "http://www.docmirror.cn:7070/api/upload/form/upload", + name: "file", + withCredentials: false, + uploadRequest: async ({ action, file, onProgress }) => { + // @ts-ignore + const data = new FormData(); + data.append("file", file); + return await request({ + url: action, + method: "post", + headers: { + "Content-Type": "multipart/form-data" + }, + timeout: 60000, + data, + onUploadProgress: (p) => { + onProgress({ percent: Math.round((p.loaded / p.total) * 100) }); + } + }); + }, + successHandle(ret) { + // 上传完成后的结果处理, 此处应返回格式为{url:xxx} + return { + url: "http://www.docmirror.cn:7070" + ret, + key: ret.replace("/api/upload/form/download?key=", "") + }; + } + } + }); + + //安装editor + app.use(FsExtendsEditor, { + //编辑器的公共配置 + wangEditor: {} + }); + app.use(FsExtendsJson); + app.use(FsExtendsTime); + app.use(FsExtendsCopyable); + + const { addTypes } = useTypes(); + addTypes({ + time2: { + //如果与官方字段类型同名,将会覆盖官方的字段类型 + form: { component: { name: "a-date-picker" } }, + column: { component: { name: "fs-date-format", format: "YYYY-MM-DD" } }, + valueBuilder(context) { + console.log("time2,valueBuilder", context); + } + } + }); +} + +export default { + install +}; diff --git a/packages/ui/certd-client/src/plugin/iconfont/iconfont.js b/packages/ui/certd-client/src/plugin/iconfont/iconfont.js new file mode 100644 index 000000000..357dc3bdd --- /dev/null +++ b/packages/ui/certd-client/src/plugin/iconfont/iconfont.js @@ -0,0 +1 @@ +!function(t){var e,c,o,n,l,a='',i=(i=document.getElementsByTagName("script"))[i.length-1].getAttribute("data-injectcss"),h=function(t,e){e.parentNode.insertBefore(t,e)};if(i&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(t){console&&console.log(t)}}function s(){l||(l=!0,o())}function d(){try{n.documentElement.doScroll("left")}catch(t){return void setTimeout(d,50)}s()}e=function(){var t,e=document.createElement("div");e.innerHTML=a,a=null,(e=e.getElementsByTagName("svg")[0])&&(e.setAttribute("aria-hidden","true"),e.style.position="absolute",e.style.width=0,e.style.height=0,e.style.overflow="hidden",e=e,(t=document.body).firstChild?h(e,t.firstChild):t.appendChild(e))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(e,0):(c=function(){document.removeEventListener("DOMContentLoaded",c,!1),e()},document.addEventListener("DOMContentLoaded",c,!1)):document.attachEvent&&(o=e,n=t.document,l=!1,d(),n.onreadystatechange=function(){"complete"==n.readyState&&(n.onreadystatechange=null,s())})}(window); diff --git a/packages/ui/certd-client/src/plugin/iconfont/index.ts b/packages/ui/certd-client/src/plugin/iconfont/index.ts new file mode 100644 index 000000000..e1f5d7f5b --- /dev/null +++ b/packages/ui/certd-client/src/plugin/iconfont/index.ts @@ -0,0 +1 @@ +import "./iconfont.js" diff --git a/packages/ui/certd-client/src/plugin/iconify/index.ts b/packages/ui/certd-client/src/plugin/iconify/index.ts new file mode 100644 index 000000000..34aacf5ae --- /dev/null +++ b/packages/ui/certd-client/src/plugin/iconify/index.ts @@ -0,0 +1,2 @@ +// import "@iconify/iconify"; +import "@purge-icons/generated"; diff --git a/packages/ui/certd-client/src/plugin/index.ts b/packages/ui/certd-client/src/plugin/index.ts new file mode 100644 index 000000000..4a73280d1 --- /dev/null +++ b/packages/ui/certd-client/src/plugin/index.ts @@ -0,0 +1,12 @@ +import "./iconify"; +import "./iconfont"; +import FastCrud from "./fast-crud"; +import permission from "./permission"; +function install(app, options: any = {}) { + app.use(FastCrud, options); + app.use(permission); +} + +export default { + install +}; diff --git a/packages/ui/certd-client/src/plugin/permission/api.ts b/packages/ui/certd-client/src/plugin/permission/api.ts new file mode 100644 index 000000000..2b9c1ea4c --- /dev/null +++ b/packages/ui/certd-client/src/plugin/permission/api.ts @@ -0,0 +1,10 @@ +import { request } from "/src/api/service"; +export async function getPermissions() { + const ret = await request({ + url: "/sys/authority/user/permissions", + method: "post" + }); + // 如果使用你自己的后端,需要在此处将返回结果改造为本模块需要的结构 + // 结构详情,请参考示例中打印的日志 ”获取权限数据成功:{...}“ (实际上就是“资源管理”页面中列出来的数据) + return ret; +} diff --git a/packages/ui/certd-client/src/plugin/permission/directive/index.js b/packages/ui/certd-client/src/plugin/permission/directive/index.js new file mode 100644 index 000000000..e0e5181f7 --- /dev/null +++ b/packages/ui/certd-client/src/plugin/permission/directive/index.js @@ -0,0 +1,9 @@ +import permission from "./permission"; +import permissionUtil from "../util.permission"; +const install = function (app) { + app.directive("permission", permission); + app.config.globalProperties.$hasPermissions = permissionUtil.hasPermissions; +}; + +permission.install = install; +export default permission; diff --git a/packages/ui/certd-client/src/plugin/permission/directive/permission.js b/packages/ui/certd-client/src/plugin/permission/directive/permission.js new file mode 100644 index 000000000..0b2a95786 --- /dev/null +++ b/packages/ui/certd-client/src/plugin/permission/directive/permission.js @@ -0,0 +1,11 @@ +import permissionUtil from "../util.permission"; +export default { + mounted(el, binding, vnode) { + const { value } = binding; + const hasPermission = permissionUtil.hasPermissions(value); + + if (!hasPermission) { + el.parentNode && el.parentNode.removeChild(el); + } + } +}; diff --git a/packages/ui/certd-client/src/plugin/permission/errors.ts b/packages/ui/certd-client/src/plugin/permission/errors.ts new file mode 100644 index 000000000..6537f4544 --- /dev/null +++ b/packages/ui/certd-client/src/plugin/permission/errors.ts @@ -0,0 +1,5 @@ +export class NoPermissionError extends Error { + constructor(message?: string) { + super(message || "对不起,您没有权限执行此操作"); + } +} diff --git a/packages/ui/certd-client/src/plugin/permission/hook.ts b/packages/ui/certd-client/src/plugin/permission/hook.ts new file mode 100644 index 000000000..af4b0c76a --- /dev/null +++ b/packages/ui/certd-client/src/plugin/permission/hook.ts @@ -0,0 +1,45 @@ +import router from "/src/router"; +import { useUserStore } from "/@/store/modules/user"; +import { usePermissionStore } from "./store.permission"; +import util from "./util.permission"; +import { message } from "ant-design-vue"; +import NProgress from "nprogress"; +export function registerRouterHook() { + // 注册路由beforeEach钩子,在第一次加载路由页面时,加载权限 + router.beforeEach(async (to, from, next) => { + const permissionStore = usePermissionStore(); + if (permissionStore.isInited) { + if (to.meta.permission) { + //校验权限 + // @ts-ignore + if (!util.hasPermissions(to.meta.permission)) { + //没有权限 + message.warn("对不起,您没有权限"); + //throw new Error("对不起,您没有权限"); + NProgress.done(); + return false; + } + } + next(); + return; + } + + const userStore = useUserStore(); + const token = userStore.getToken; + if (!token || token === "undefined") { + next(); + return; + } + + // 初始化权限列表 + try { + console.log("permission is enabled"); + await permissionStore.loadFromRemote(); + console.log("PM load success"); + next({ ...to, replace: true }); + } catch (e) { + console.error("加载动态路由失败", e); + next(); + } + }); +} diff --git a/packages/ui/certd-client/src/plugin/permission/index.ts b/packages/ui/certd-client/src/plugin/permission/index.ts new file mode 100644 index 000000000..c300560a2 --- /dev/null +++ b/packages/ui/certd-client/src/plugin/permission/index.ts @@ -0,0 +1,23 @@ +import permissionDirective from "./directive/index"; +import { registerRouterHook } from "./hook"; +import util from "./util.permission"; +export * from "./use-crud-permission"; +export * from "./errors"; + +export function usePermission() { + return { + ...util + }; +} + +export default { + install(app) { + // 开启权限模块 + // 注册v-permission指令, 用于控制按钮权限 + app.use(permissionDirective); + // 注册路由钩子 + // 通过路由守卫,在登录成功后拦截路由,从后台加载权限数据 + // 然后将权限数据转化为菜单和路由,添加到系统中 + registerRouterHook(); + } +}; diff --git a/packages/ui/certd-client/src/plugin/permission/store.permission.ts b/packages/ui/certd-client/src/plugin/permission/store.permission.ts new file mode 100644 index 000000000..5cbb3305a --- /dev/null +++ b/packages/ui/certd-client/src/plugin/permission/store.permission.ts @@ -0,0 +1,89 @@ +import { defineStore } from "pinia"; +import { useResourceStore } from "/src/store/modules/resource"; +import { getPermissions } from "./api"; +import { mitter } from "/@/utils/util.mitt"; +import { env } from "/@/utils/util.env"; + +//监听注销事件 +mitter.on("app.logout", () => { + const permissionStore = usePermissionStore(); + permissionStore.clear(); +}); + +interface PermissionState { + permissions: []; + inited: boolean; +} + +/** + * 构建权限码列表 + * @param menuTree + * @param permissionList + * @returns {*} + */ +function formatPermissions(menuTree: Array, permissionList = []) { + if (menuTree == null) { + menuTree = []; + } + menuTree.forEach((item: any) => { + if (item.permission) { + // @ts-ignore + permissionList.push(item.permission); + } + if (item.children != null && item.children.length > 0) { + formatPermissions(item.children, permissionList); + } + }); + return permissionList; +} + +export const usePermissionStore = defineStore({ + id: "app.permission", + state: (): PermissionState => ({ + permissions: [], + inited: false + }), + getters: { + getPermissions() { + // @ts-ignore + return this.permissions; + }, + isInited() { + // @ts-ignore + return this.inited; + } + }, + actions: { + init({ permissions }) { + this.permissions = permissions; + this.inited = true; + }, + clear() { + this.permissions = []; + this.inited = false; + }, + resolve(resourceTree) { + const permissions = formatPermissions(resourceTree); + this.init({ permissions }); + + //过滤没有权限的菜单 + const resourceStore = useResourceStore(); + resourceStore.filterByPermission(permissions); + }, + async loadFromRemote() { + let permissionTree = []; + if (env.PM_ENABLED === "false") { + console.warn("当前权限模块未开启,权限列表为空"); + } else { + //开启了权限模块,向后台请求权限列表 + const data = await getPermissions(); + if (data != null) { + permissionTree = data; + } else { + console.warn("当前获取到的权限列表为空"); + } + } + this.resolve(permissionTree); + } + } +}); diff --git a/packages/ui/certd-client/src/plugin/permission/use-crud-permission.ts b/packages/ui/certd-client/src/plugin/permission/use-crud-permission.ts new file mode 100644 index 000000000..2c60f8e2c --- /dev/null +++ b/packages/ui/certd-client/src/plugin/permission/use-crud-permission.ts @@ -0,0 +1,60 @@ +import { usePermission } from "/@/plugin/permission"; +import _ from "lodash-es"; + +/** + * 设置按钮动作权限 + * @param permission {prefix,extra} + */ +export function useCrudPermission({ permission }) { + const { hasPermissions } = usePermission(); + + const prefix = permission instanceof Object ? permission.prefix : permission; + + //根据权限显示按钮 + function hasActionPermission(action) { + if (!prefix) { + return true; + } + return hasPermissions(prefix + ":" + action); + } + + function buildCrudPermission() { + if (permission == null) { + return {}; + } + + let extra = {}; + if (permission instanceof Object) { + extra = permission.extra; + if (permission.extra && permission.extra instanceof Function) { + extra = permission.extra({ hasActionPermission }); + } + } + + return _.merge( + { + actionbar: { + buttons: { + add: { show: hasActionPermission("add") } + } + }, + rowHandle: { + buttons: { + edit: { show: hasActionPermission("edit") }, + remove: { show: hasActionPermission("remove") }, + view: { show: hasActionPermission("view") } + } + } + }, + extra + ); + } + + function merge(userOptions) { + const permissionOptions = buildCrudPermission(); + _.merge(permissionOptions, userOptions); + return permissionOptions; + } + + return { merge, buildCrudPermission, hasActionPermission }; +} diff --git a/packages/ui/certd-client/src/plugin/permission/util.permission.ts b/packages/ui/certd-client/src/plugin/permission/util.permission.ts new file mode 100644 index 000000000..1c197335a --- /dev/null +++ b/packages/ui/certd-client/src/plugin/permission/util.permission.ts @@ -0,0 +1,29 @@ +import { usePermissionStore } from "./store.permission"; +import { NoPermissionError } from "./errors"; +import { message } from "ant-design-vue"; +const util = { + hasPermissions: (value: string | string[]): boolean => { + let need: string[] = []; + if (typeof value === "string") { + need.push(value); + } else if (value && value instanceof Array && value.length > 0) { + need = need.concat(value); + } + if (need.length === 0) { + throw new Error('need permissions! Like "sys:user:view" '); + } + const permissionStore = usePermissionStore(); + const userPermissionList = permissionStore.getPermissions; + return userPermissionList.some((permission) => { + return need.includes(permission); + }); + }, + requirePermissions: (value) => { + if (!util.hasPermissions(value)) { + message.error("对不起,您没有权限执行此操作"); + throw new NoPermissionError(); + } + } +}; + +export default util; diff --git a/packages/ui/certd-client/src/router/index.ts b/packages/ui/certd-client/src/router/index.ts new file mode 100644 index 000000000..77a592558 --- /dev/null +++ b/packages/ui/certd-client/src/router/index.ts @@ -0,0 +1,69 @@ +import { createRouter, createWebHashHistory } from "vue-router"; +// 进度条 +import NProgress from "nprogress"; +import "nprogress/nprogress.css"; +import { usePageStore } from "../store/modules/page"; +import { site } from "../utils/util.site"; +import { routes } from "./resolve"; +import { useResourceStore } from "../store/modules/resource"; +import { useUserStore } from "../store/modules/user"; + +const router = createRouter({ + history: createWebHashHistory(), + routes +}); + +/** + * 路由拦截 + */ +router.beforeEach(async (to, from, next) => { + // 进度条 + NProgress.start(); + + // 验证当前路由所有的匹配中是否需要有登录验证的 + if ( + to.matched.some((r) => { + return r.meta?.auth || r.meta?.permission; + }) + ) { + const userStore = useUserStore(); + // 这里暂时将cookie里是否存有token作为验证是否登录的条件 + // 请根据自身业务需要修改 + const token = userStore.getToken; + if (token) { + next(); + } else { + // 没有登录的时候跳转到登录界面 + // 携带上登陆成功之后需要跳转的页面完整路径 + next({ + name: "login", + query: { + redirect: to.fullPath + } + }); + // https://github.com/d2-projects/d2-admin/issues/138 + NProgress.done(); + } + } else { + // 不需要身份校验 直接通过 + next(); + } +}); + +router.afterEach((to) => { + // 进度条 + NProgress.done(); + // 多页控制 打开新的页面 + const pageStore = usePageStore(); + pageStore.open(to); + // 更改标题 + site.title(to.meta.title); + + //修改左侧边栏 + const matched = to.matched; + if (matched.length > 0) { + const resourceStore = useResourceStore(); + resourceStore.setAsideMenuByCurrentRoute(matched); + } +}); +export default router; diff --git a/packages/ui/certd-client/src/router/resolve.ts b/packages/ui/certd-client/src/router/resolve.ts new file mode 100644 index 000000000..39cbd9980 --- /dev/null +++ b/packages/ui/certd-client/src/router/resolve.ts @@ -0,0 +1,155 @@ +import LayoutPass from "/src/layout/layout-pass.vue"; +import _ from "lodash-es"; +import { outsideResource } from "./source/outside"; +import { headerResource } from "./source/header"; +import { frameworkResource } from "./source/framework"; +// @ts-ignore +const modules = import.meta.glob("/src/views/**/*.vue"); + +let index = 0; +function transformOneResource(resource) { + let menu: any = null; + if (resource.meta == null) { + resource.meta = {}; + } + const meta = resource.meta; + meta.title = meta.title ?? resource.title ?? "未命名"; + if (resource.title == null) { + resource.title = meta.title; + } + if (meta.isMenu === false) { + menu = null; + } else { + menu = _.cloneDeep(resource); + delete menu.component; + } + let route; + if (resource.type !== "menu") { + if (resource.path == null || resource.path.startsWith("https://") || resource.path.startsWith("http://")) { + //没有route + route = null; + } else { + route = _.cloneDeep(resource); + if (route.component && typeof route.component === "string") { + const path = "/src/views" + route.component; + route.component = modules[path]; + } + if (route.component == null) { + route.component = LayoutPass; + } + } + } + + return { + menu, + route + }; +} + +export const buildMenusAndRouters = (resources) => { + const routes: Array = []; + const menus: Array = []; + for (const item of resources) { + const { menu, route } = transformOneResource(item); + let menuChildren; + let routeChildren; + if (item.children) { + if (item.children.length > 0) { + const ret = buildMenusAndRouters(item.children); + menuChildren = ret.menus; + routeChildren = ret.routes; + } + } + + if (menu) { + menus.push(menu); + menu.children = menuChildren; + } + if (route) { + if (route?.meta?.cache !== false) { + if (route.meta == null) { + route.meta = {}; + } + route.meta.cache = true; + } + routes.push(route); + route.children = routeChildren; + } + } + + setIndex(menus); + return { + routes, + menus + }; +}; + +function setIndex(menus) { + for (const menu of menus) { + menu.index = "index_" + index; + index++; + if (menu.children && menu.children.length > 0) { + setIndex(menu.children); + } + } +} + +function findMenus(menus, condition) { + const list: any = []; + for (const menu of menus) { + if (condition(menu)) { + list.push(menu); + } + if (menu.children && menu.children.length > 0) { + const subList = findMenus(menu.children, condition); + for (const item of subList) { + list.push(item); + } + } + } + return list; +} + +function filterMenus(menus, condition) { + const list = menus.filter((item) => { + return condition(item); + }); + + for (const item of list) { + if (item.children && item.children.length > 0) { + item.children = filterMenus(item.children, condition); + } + } + return list; +} + +function flatChildren(list, children) { + for (const child of children) { + list.push(child); + if (child.children && child.children.length > 0) { + flatChildren(list, child.children); + } + child.children = null; + } +} +function flatSubRouters(routers) { + for (const router of routers) { + const children: Array = []; + if (router.children && router.children.length > 0) { + flatChildren(children, router.children); + } + router.children = children; + } + return routers; +} + +const frameworkRet = buildMenusAndRouters(frameworkResource); +const outsideRet = buildMenusAndRouters(outsideResource); +const headerRet = buildMenusAndRouters(headerResource); + +const outsideRoutes = outsideRet.routes; +const frameworkRoutes = flatSubRouters(frameworkRet.routes); +const routes = [...outsideRoutes, ...frameworkRoutes]; +const frameworkMenus = frameworkRet.menus; +const headerMenus = headerRet.menus; +export { routes, outsideRoutes, frameworkRoutes, frameworkMenus, headerMenus, findMenus, filterMenus }; diff --git a/packages/ui/certd-client/src/router/source/framework.ts b/packages/ui/certd-client/src/router/source/framework.ts new file mode 100644 index 000000000..800fcf99a --- /dev/null +++ b/packages/ui/certd-client/src/router/source/framework.ts @@ -0,0 +1,35 @@ +import LayoutFramework from "/src/layout/layout-framework.vue"; +//import { crudResources } from "/@/router/source/modules/crud"; +import { sysResources } from "/@/router/source/modules/sys"; +import { certdResources } from "/@/router/source/modules/certd"; + +export const frameworkResource = [ + { + title: "框架", + name: "framework", + path: "/", + redirect: "/index", + component: LayoutFramework, + meta: { + icon: "ion:accessibility", + auth: true + }, + children: [ + { + title: "首页", + name: "index", + path: "/index", + component: "/framework/home/index.vue", + meta: { + fixedAside: true, + showOnHeader: false, + icon: "ion:home-outline" + } + }, + //...crudResources, + ...certdResources, + ...sysResources + ] + } +]; +console.assert(frameworkResource.length === 1, "frameworkResource数组长度只能为1,你只能配置framework路由的子路由"); diff --git a/packages/ui/certd-client/src/router/source/header.ts b/packages/ui/certd-client/src/router/source/header.ts new file mode 100644 index 000000000..8c30304ae --- /dev/null +++ b/packages/ui/certd-client/src/router/source/header.ts @@ -0,0 +1,73 @@ +export const headerResource = [ + { + title: "文档", + path: "http://fast-crud.docmirror.cn/" + }, + { + title: "其他Demo", + name: "demo", + children: [ + { + title: "Element版", + path: "http://fast-crud.docmirror.cn/element/" + }, + { + title: "VbenAdmin", + path: "http://fast-crud.docmirror.cn/vben/" + }, + { + title: "cool-admin-vue", + path: "http://fast-crud.docmirror.cn/cool/" + } + ] + }, + + { + title: "源码", + name: "source", + key: "source", + meta: { + icon: "ion:git-branch-outline" + }, + children: [ + { + title: "fast-crud", + children: [ + { + title: "github", + path: "http://github.com/fast-crud/fast-crud", + meta: { + icon: "ion:logo-github" + } + }, + { + title: "gitee", + path: "http://gitee.com/fast-crud/fast-crud", + meta: { + icon: "ion:logo-octocat" + } + } + ] + }, + { + title: "fs-admin", + children: [ + { + title: "github", + path: "http://github.com/fast-crud/fs-admin-antdv", + meta: { + icon: "ion:logo-github" + } + }, + { + title: "gitee", + path: "http://gitee.com/fast-crud/fs-admin-antdv", + meta: { + icon: "ion:logo-octocat" + } + } + ] + } + ] + } +]; diff --git a/packages/ui/certd-client/src/router/source/modules/certd.ts b/packages/ui/certd-client/src/router/source/modules/certd.ts new file mode 100644 index 000000000..2ece20295 --- /dev/null +++ b/packages/ui/certd-client/src/router/source/modules/certd.ts @@ -0,0 +1,41 @@ +export const certdResources = [ + { + title: "证书自动化", + name: "certd", + path: "/certd", + redirect: "/certd/pipeline", + meta: { + icon: "ion:key-outline", + auth: true + }, + children: [ + { + title: "证书自动化流水线", + name: "pipeline", + path: "/certd/pipeline", + component: "/certd/pipeline/index.vue", + meta: { + icon: "ion:analytics-sharp" + } + }, + { + title: "编辑流水线", + name: "pipelineEdit", + path: "/certd/pipeline/detail", + component: "/certd/pipeline/detail.vue", + meta: { + isMenu: false + } + }, + { + title: "授权管理", + name: "access", + path: "/certd/access", + component: "/certd/access/index.vue", + meta: { + icon: "ion:disc-outline" + } + } + ] + } +]; diff --git a/packages/ui/certd-client/src/router/source/modules/crud.ts b/packages/ui/certd-client/src/router/source/modules/crud.ts new file mode 100644 index 000000000..f17952fc8 --- /dev/null +++ b/packages/ui/certd-client/src/router/source/modules/crud.ts @@ -0,0 +1,604 @@ +export const crudResources = [ + { + title: "CRUD示例", + name: "crud", + path: "/crud", + redirect: "/crud/basis", + meta: { + icon: "ion:apps-sharp" + }, + children: [ + { + title: "基本特性", + name: "basis", + path: "/crud/basis", + redirect: "/crud/basis/i18n", + meta: { + icon: "ion:disc-outline" + }, + children: [ + { + title: "HelloWorld", + name: "FsCrudFirst", + path: "/crud/basis/first", + component: "/crud/basis/first/index.vue" + }, + { + title: "动态计算", + name: "BasisCompute", + path: "/crud/basis/compute", + component: "/crud/basis/compute/index.vue" + }, + { + title: "动态计算-更多示例", + name: "BasisComputeMore", + path: "/crud/basis/compute-more", + component: "/crud/basis/compute-more/index.vue" + }, + { + title: "国际化", + name: "BasisI18n", + path: "/crud/basis/i18n", + component: "/crud/basis/i18n/index.vue" + }, + { + title: "ValueChange", + name: "BasisValueChange", + path: "/crud/basis/value-change", + component: "/crud/basis/value-change/index.vue" + }, + { + title: "Card布局", + name: "BasisLayoutCard", + path: "/crud/basis/layout-card", + component: "/crud/basis/layout-card/index.vue" + }, + { + title: "自定义布局", + name: "BasisLayoutCustom", + path: "/crud/basis/layout-custom", + component: "/crud/basis/layout-custom/index.vue" + }, + { + title: "列设置", + name: "BasisColumnsSet", + path: "/crud/basis/columns-set", + component: "/crud/basis/columns-set/index.vue" + } + ] + }, + { + title: "数据字典", + name: "dict", + path: "/crud/dict", + redirect: "/crud/dict/single", + meta: { + icon: "ion:book-outline" + }, + children: [ + { + title: "单例", + name: "DictSingle", + path: "/crud/dict/single", + component: "/crud/dict/single/index.vue" + }, + { + title: "分发复制", + name: "DictCloneable", + path: "/crud/dict/cloneable", + component: "/crud/dict/cloneable/index.vue" + }, + { + title: "原型复制", + name: "DictPrototype", + path: "/crud/dict/prototype", + component: "/crud/dict/prototype/index.vue" + } + ] + }, + { + title: "操作列", + name: "row-handle", + path: "/crud/row-handle", + redirect: "/crud/row-handle/tooltip", + meta: { + icon: "ion:build-outline" + }, + children: [ + { + title: "Tooltip", + name: "RowHandleTooltip", + path: "/crud/row-handle/tooltip", + component: "/crud/row-handle/tooltip/index.vue" + }, + { + title: "按钮折叠", + name: "RowHandleDropdown", + path: "/crud/row-handle/dropdown", + component: "/crud/row-handle/dropdown/index.vue" + } + ] + }, + { + title: "组件示例", + name: "component", + path: "/crud/component", + redirect: "/crud/component/text", + meta: { + icon: "ion:cube-outline" + }, + children: [ + { + title: "文本输入(input)", + name: "ComponentText", + path: "/crud/component/text", + component: "/crud/component/text/index.vue" + }, + { + title: "选择(select)", + name: "ComponentSelect", + path: "/crud/component/select", + component: "/crud/component/select/index.vue" + }, + { + title: "级联(cascader)", + name: "ComponentCascader", + path: "/crud/component/cascader", + component: "/crud/component/cascader/index.vue" + }, + { + title: "多选(checkbox)", + name: "ComponentCheckbox", + path: "/crud/component/checkbox", + component: "/crud/component/checkbox/index.vue" + }, + { + title: "单选(radio)", + name: "ComponentRadio", + path: "/crud/component/radio", + component: "/crud/component/radio/index.vue" + }, + { + title: "开关(switch)", + name: "ComponentSwitch", + path: "/crud/component/switch", + component: "/crud/component/switch/index.vue" + }, + { + title: "日期时间(date)", + name: "ComponentDate", + path: "/crud/component/date", + component: "/crud/component/date/index.vue" + }, + { + title: "按钮链接", + name: "ComponentButton", + path: "/crud/component/button", + component: "/crud/component/button/index.vue" + }, + { + title: "数字", + name: "ComponentNumber", + path: "/crud/component/number", + component: "/crud/component/number/index.vue" + }, + { + title: "树形选择", + name: "ComponentTree", + path: "/crud/component/tree", + component: "/crud/component/tree/index.vue" + }, + { + title: "图片裁剪上传", + name: "ComponentUploaderCropper", + path: "/crud/component/uploader/cropper", + component: "/crud/component/uploader/cropper/index.vue" + }, + { + title: "表单本地上传", + name: "ComponentUploaderForm", + path: "/crud/component/uploader/form", + component: "/crud/component/uploader/form/index.vue" + }, + { + title: "阿里云oss上传", + name: "ComponentUploaderAlioss", + path: "/crud/component/uploader/alioss", + component: "/crud/component/uploader/alioss/index.vue" + }, + { + title: "腾讯云cos上传", + name: "ComponentUploaderCos", + path: "/crud/component/uploader/cos", + component: "/crud/component/uploader/cos/index.vue" + }, + { + title: "七牛云上传", + name: "ComponentUploaderQiniu", + path: "/crud/component/uploader/qiniu", + component: "/crud/component/uploader/qiniu/index.vue" + }, + { + title: "富文本编辑器", + name: "ComponentEditor", + path: "/crud/component/editor", + component: "/crud/component/editor/index.vue" + }, + { + title: "图标", + name: "ComponentIcon", + path: "/crud/component/icon", + component: "/crud/component/icon/index.vue" + }, + { + title: "JsonEditor", + name: "ComponentJson", + path: "/crud/component/json", + component: "/crud/component/json/index.vue" + } + ] + }, + { + title: "Form表单", + name: "form", + path: "/crud/form", + redirect: "/crud/form/layout", + meta: { + icon: "ion:document-text-outline" + }, + children: [ + { + title: "基本表单", + name: "FormBase", + path: "/crud/form/base", + component: "/crud/form/base/index.vue" + }, + { + title: "表单Grid布局", + name: "FormLayoutGrid", + path: "/crud/form/layout-grid", + component: "/crud/form/layout-grid/index.vue" + }, + { + title: "表单Flex布局", + name: "FormLayoutFlex", + path: "/crud/form/layout-flex", + component: "/crud/form/layout-flex/index.vue" + }, + { + title: "表单动态布局", + name: "FormLayout", + path: "/crud/form/layout", + component: "/crud/form/layout/index.vue" + }, + { + title: "表单校验", + name: "FormValidation", + path: "/crud/form/validation", + component: "/crud/form/validation/index.vue" + }, + { + title: "抽屉表单", + name: "FormDrawer", + path: "/crud/form/drawer", + component: "/crud/form/drawer/index.vue" + }, + { + title: "表单分组", + name: "FormGroup", + path: "/crud/form/group", + component: "/crud/form/group/index.vue" + }, + { + title: "表单分组(tabs)", + name: "FormGroupTabs", + path: "/crud/form/group-tabs", + component: "/crud/form/group-tabs/index.vue" + }, + { + title: "自定义表单", + name: "FormCustomForm", + path: "/crud/form/custom-form", + component: "/crud/form/custom-form/index.vue" + }, + { + title: "字段帮助说明", + name: "FormHelper", + path: "/crud/form/helper", + component: "/crud/form/helper/index.vue" + }, + { + title: "页面内部弹出表单", + name: "FormInner", + path: "/crud/form/inner", + component: "/crud/form/inner/index.vue", + meta: { + cache: true + } + }, + { + title: "地区字典管理", + name: "FormInnerArea", + path: "/crud/form/inner/area", + component: "/crud/form/inner/area/index.vue", + meta: { + isMenu: false + } + }, + { + title: "新页面编辑", + name: "FormNewPage", + path: "/crud/form/new-page", + component: "/crud/form/new-page/index.vue", + meta: { + cache: false + } + }, + { + title: "新页面编辑表单", + name: "FormNewPageEdit", + path: "/crud/form/new-page/edit", + component: "/crud/form/new-page/edit.vue", + meta: { + isMenu: false + } + }, + { + title: "独立使用表单", + name: "FormIndependent", + path: "/crud/form/independent", + component: "/crud/form/independent/index.vue" + }, + { + title: "重置表单", + name: "FormReset", + path: "/crud/form/reset", + component: "/crud/form/reset/index.vue" + }, + { + title: "嵌套数据结构", + name: "FormNest", + path: "/crud/form/nest", + component: "/crud/form/nest/index.vue" + } + ] + }, + { + title: "表格特性", + path: "/crud/feature", + meta: { + icon: "ion:beer-outline" + }, + redirect: "/crud/feature/dropdown", + children: [ + { + title: "部件显隐", + name: "FeatureHide", + path: "/crud/feature/hide", + component: "/crud/feature/hide/index.vue" + }, + { + title: "多选&批量删除", + name: "FeatureSelection", + path: "/crud/feature/selection", + component: "/crud/feature/selection/index.vue" + }, + { + title: "单选", + name: "FeatureSelectionRadio", + path: "/crud/feature/selection-radio", + component: "/crud/feature/selection-radio/index.vue" + }, + { + title: "表头过滤", + name: "FeatureFilter", + path: "/crud/feature/filter", + component: "/crud/feature/filter/index.vue" + }, + { + title: "行展开", + name: "FeatureExpand", + path: "/crud/feature/expand", + component: "/crud/feature/expand/index.vue" + }, + { + title: "树形表格", + name: "FeatureTree", + path: "/crud/feature/tree", + component: "/crud/feature/tree/index.vue" + }, + { + title: "多级表头", + name: "FeatureHeaderGroup", + path: "/crud/feature/header-group", + component: "/crud/feature/header-group/index.vue" + }, + { + title: "合并单元格", + name: "FeatureMerge", + path: "/crud/feature/merge", + component: "/crud/feature/merge/index.vue" + }, + { + title: "序号", + name: "FeatureIndex", + path: "/crud/feature/index", + component: "/crud/feature/index/index.vue" + }, + { + title: "排序", + name: "FeatureSortable", + path: "/crud/feature/sortable", + component: "/crud/feature/sortable/index.vue" + }, + { + title: "固定列", + name: "FeatureFixed", + path: "/crud/feature/fixed", + component: "/crud/feature/fixed/index.vue" + }, + { + title: "不固定高度", + name: "FeatureHeight", + path: "/crud/feature/height", + component: "/crud/feature/height/index.vue" + }, + { + title: "可编辑", + name: "FeatureEditable", + path: "/crud/feature/editable", + component: "/crud/feature/editable/index.vue" + }, + { + title: "行编辑", + name: "FeatureEditableRow", + path: "/crud/feature/editable-row", + component: "/crud/feature/editable-row/index.vue" + }, + { + title: "查询框", + name: "FeatureSearch", + path: "/crud/feature/search", + component: "/crud/feature/search/index.vue" + }, + { + title: "查询框多行模式", + name: "FeatureSearchMulti", + path: "/crud/feature/search-multi", + component: "/crud/feature/search-multi/index.vue" + }, + { + title: "字段排序", + name: "FeatureColumnSort", + path: "/crud/feature/column-sort", + component: "/crud/feature/column-sort/index.vue" + }, + { + title: "ValueBuilder", + name: "FeatureValueBuilder", + path: "/crud/feature/value-builder", + component: "/crud/feature/value-builder/index.vue" + }, + { + title: "列设置", + name: "FeatureColumnsSet", + path: "/crud/feature/columns-set", + component: "/crud/feature/columns-set/index.vue" + }, + { + title: "本地化编辑", + name: "FeatureLocal", + path: "/crud/feature/local", + component: "/crud/feature/local/index.vue" + }, + { + title: "v-model", + name: "FeatureVModel", + path: "/crud/feature/v-model", + component: "/crud/feature/local-v-model/index.vue" + }, + { + title: "自定义删除", + name: "FeatureRemove", + path: "/crud/feature/remove", + component: "/crud/feature/remove/index.vue" + }, + { + title: "调整列宽", + name: "FeatureColumnResize", + path: "/crud/feature/column-resize", + component: "/crud/feature/column-resize/index.vue" + } + ] + }, + { + title: "插槽", + name: "Slots", + path: "/crud/slots", + redirect: "/crud/slots/layout", + meta: { + icon: "ion:extension-puzzle-outline" + }, + children: [ + { + title: "页面占位插槽", + name: "SlotsLayout", + path: "/crud/slots/layout", + component: "/crud/slots/layout/index.vue" + }, + { + title: "表单占位插槽", + name: "SlotsForm", + path: "/crud/slots/form", + component: "/crud/slots/form/index.vue" + }, + { + title: "查询字段插槽", + name: "SlotsSearch", + path: "/crud/slots/search", + component: "/crud/slots/search/index.vue" + }, + { + title: "单元格插槽", + name: "SlotsCell", + path: "/crud/slots/cell", + component: "/crud/slots/cell/index.vue" + }, + { + title: "表单字段插槽", + name: "SlotsFormItem", + path: "/crud/slots/form-item", + component: "/crud/slots/form-item/index.vue" + } + ] + }, + { + title: "复杂需求", + name: "Advanced", + path: "/crud/advanced", + redirect: "/crud/advanced/linkage", + meta: { + icon: "ion:flame-outline" + }, + children: [ + { + title: "选择联动", + name: "AdvancedLinkage", + path: "/crud/advanced/linkage", + component: "/crud/advanced/linkage/index.vue" + }, + { + title: "后台加载crud", + name: "AdvancedFormBackend", + path: "/crud/advanced/from-backend", + component: "/crud/advanced/from-backend/index.vue" + }, + { + title: "本地分页", + name: "AdvancedLocalPagination", + path: "/crud/advanced/local-pagination", + component: "/crud/advanced/local-pagination/index.vue" + }, + { + title: "嵌套子表格", + name: "AdvancedNest", + path: "/crud/advanced/nest", + component: "/crud/advanced/nest/index.vue" + }, + { + title: "对话框中显示crud", + name: "AdvancedInDialog", + path: "/crud/advanced/in-dialog", + component: "/crud/advanced/in-dialog/index.vue" + }, + { + title: "大量数据", + name: "AdvancedBigData", + path: "/crud/advanced/big-data", + component: "/crud/advanced/big-data/index.vue" + } + ] + } + ] + } +]; diff --git a/packages/ui/certd-client/src/router/source/modules/sys.ts b/packages/ui/certd-client/src/router/source/modules/sys.ts new file mode 100644 index 000000000..a2d751271 --- /dev/null +++ b/packages/ui/certd-client/src/router/source/modules/sys.ts @@ -0,0 +1,61 @@ +import LayoutPass from "/@/layout/layout-pass.vue"; + +export const sysResources = [ + { + title: "系统管理", + name: "sys", + path: "/sys", + redirect: "/sys/authority", + component: LayoutPass, + meta: { + icon: "ion:settings-outline", + permission: "sys" + }, + children: [ + { + title: "权限管理", + name: "authority", + path: "/sys/authority", + redirect: "/sys/authority/permission", + meta: { + icon: "ion:ribbon-outline", + //需要校验权限 + permission: "sys:auth" + }, + children: [ + { + title: "权限资源管理", + name: "permission", + meta: { + icon: "ion:list-outline", + //需要校验权限 + permission: "sys:auth:per:view" + }, + path: "/sys/authority/permission", + component: "/sys/authority/permission/index.vue" + }, + { + title: "角色管理", + name: "role", + meta: { + icon: "ion:people-outline", + permission: "sys:auth:role:view" + }, + path: "/sys/authority/role", + component: "/sys/authority/role/index.vue" + } + ] + }, + { + title: "用户管理", + name: "user", + meta: { + icon: "ion:person-outline", + permission: "sys:auth:user:view" + }, + path: "/sys/authority/user", + component: "/sys/authority/user/index.vue" + } + ] + } +]; diff --git a/packages/ui/certd-client/src/router/source/outside.ts b/packages/ui/certd-client/src/router/source/outside.ts new file mode 100644 index 000000000..177172c35 --- /dev/null +++ b/packages/ui/certd-client/src/router/source/outside.ts @@ -0,0 +1,22 @@ +import LayoutOutside from "/src/layout/layout-outside.vue"; +import Error404 from "/src/views/framework/error/404.vue"; +const errorPage = [{ path: "/:pathMatch(.*)*", name: "not-found", component: Error404 }]; +export const outsideResource = [ + { + title: "outside", + name: "outside", + path: "/outside", + component: LayoutOutside, + children: [ + { + meta: { + title: "登录" + }, + name: "login", + path: "/login", + component: "/framework/login/index.vue" + } + ] + }, + ...errorPage +]; diff --git a/packages/ui/certd-client/src/shims-vue.d.ts b/packages/ui/certd-client/src/shims-vue.d.ts new file mode 100644 index 000000000..daba9b9ec --- /dev/null +++ b/packages/ui/certd-client/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/packages/ui/certd-client/src/store/index.ts b/packages/ui/certd-client/src/store/index.ts new file mode 100644 index 000000000..52b9cf688 --- /dev/null +++ b/packages/ui/certd-client/src/store/index.ts @@ -0,0 +1,9 @@ +import { createPinia } from "pinia"; +const store = createPinia(); +export default { + install(app) { + app.use(store); + } +}; + +export { store }; diff --git a/packages/ui/certd-client/src/store/modules/page.ts b/packages/ui/certd-client/src/store/modules/page.ts new file mode 100644 index 000000000..8c2d7176d --- /dev/null +++ b/packages/ui/certd-client/src/store/modules/page.ts @@ -0,0 +1,436 @@ +import { defineStore } from "pinia"; +import { cloneDeep, get, uniq } from "lodash-es"; +import router from "/src/router"; +import { frameworkRoutes } from "/src/router/resolve"; +// @ts-ignore +import { LocalStorage } from "/src/utils/util.storage"; +import { useUserStore } from "/src/store/modules/user"; +const OPENED_CACHE_KEY = "TABS_OPENED"; + +interface PageState { + // 可以在多页 tab 模式下显示的页面 + pool: Array; + // 当前显示的多页面列表 + opened: Array; + // 已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201 + openedLoaded: boolean; + // 当前页面 + current: ""; + // 需要缓存的页面 name + keepAlive: Array; + inited: boolean; +} +// 判定是否需要缓存 +const isKeepAlive = (data) => get(data, "meta.cache", false); + +export const usePageStore = defineStore({ + id: "app.page", + state: (): PageState => ({ + // 可以在多页 tab 模式下显示的页面 + pool: [], + // 当前显示的多页面列表 + opened: [ + { + name: "index", + fullPath: "/index", + meta: { + title: "首页", + auth: false + } + } + ], + // 已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201 + openedLoaded: false, + // 当前页面 + current: "", + // 需要缓存的页面 name + keepAlive: [], + inited: false + }), + getters: { + getOpened() { + // @ts-ignore + return this.opened; + }, + getCurrent(): string { + return this.current; + } + }, + actions: { + /** + * @description 确认已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201 + * @param {Object} context + */ + async isLoaded() { + if (this.openedLoaded) { + return true; + } + return new Promise((resolve) => { + const timer = setInterval(() => { + if (this.openedLoaded) { + resolve(clearInterval(timer)); + } + }, 10); + }); + }, + /** + * @class opened + * @description 从持久化数据载入标签页列表 + * @param {Object} context + */ + async openedLoad() { + // store 赋值 + const value = LocalStorage.get(this.getStorageKey()); + if (value == null) { + return; + } + // 在处理函数中进行数据优化 过滤掉现在已经失效的页签或者已经改变了信息的页签 + // 以 fullPath 字段为准 + // 如果页面过多的话可能需要优化算法 + // valid 有效列表 1, 1, 0, 1 => 有效, 有效, 失效, 有效 + const valid: Array = []; + // 处理数据 + this.opened = value + .map((opened) => { + // 忽略首页 + if (opened.fullPath === "/index") { + valid.push(1); + return opened; + } + // 尝试在所有的支持多标签页的页面里找到 name 匹配的页面 + const find = this.pool.find((item) => item.name === opened.name); + // 记录有效或无效信息 + valid.push(find ? 1 : 0); + // 返回合并后的数据 新的覆盖旧的 + // 新的数据中一般不会携带 params 和 query, 所以旧的参数会留存 + return Object.assign({}, opened, find); + }) + .filter((opened, index) => valid[index] === 1); + // 标记已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201 + this.openedLoaded = true; + // 根据 opened 数据生成缓存设置 + this.keepAliveRefresh(); + }, + + getStorageKey() { + const userStore = useUserStore(); + const userId = userStore.getUserInfo?.id ?? "anonymous"; + return OPENED_CACHE_KEY + ":" + userId; + }, + /** + * 将 opened 属性赋值并持久化 在这之前请先确保已经更新了 state.opened + * @param {Object} context + */ + async opened2db() { + // 设置数据 + + LocalStorage.set(this.getStorageKey(), this.opened); + }, + /** + * @class opened + * @description 更新页面列表上的某一项 + * @param {Object} context + * @param {Object} payload { index, params, query, fullPath } 路由信息 + */ + async openedUpdate({ index, params, query, fullPath }) { + // 更新页面列表某一项 + const page = this.opened[index]; + page.params = params || page.params; + page.query = query || page.query; + page.fullPath = fullPath || page.fullPath; + this.opened.splice(index, 1, page); + // 持久化 + await this.opened2db(); + }, + /** + * @class opened + * @description 重排页面列表上的某一项 + * @param {Object} context + * @param {Object} payload { oldIndex, newIndex } 位置信息 + */ + async openedSort({ oldIndex, newIndex }) { + // 重排页面列表某一项 + const page = this.opened[oldIndex]; + this.opened.splice(oldIndex, 1); + this.opened.splice(newIndex, 0, page); + // 持久化 + await this.opened2db(); + }, + /** + * @class opened + * @description 新增一个 tag (打开一个页面) + * @param {Object} context + * @param {Object} payload new tag info + */ + async add({ tag, params, query, fullPath }) { + // 设置新的 tag 在新打开一个以前没打开过的页面时使用 + const newTag = tag; + newTag.params = params || newTag.params; + newTag.query = query || newTag.query; + newTag.fullPath = fullPath || newTag.fullPath; + // 添加进当前显示的页面数组 + this.opened.push(newTag); + // 如果这个页面需要缓存 将其添加到缓存设置 + if (isKeepAlive(newTag)) { + this.keepAlivePush(tag.name); + } + // 持久化 + await this.opened2db(); + }, + /** + * @class current + * @description 打开一个新的页面 + * @param {Object} context + * @param {Object} payload 从路由钩子的 to 对象上获取 { name, params, query, fullPath, meta } 路由信息 + */ + async open({ name, params, query, fullPath, meta }) { + // 已经打开的页面 + const opened = this.opened; + // 判断此页面是否已经打开 并且记录位置 + let pageOpendIndex = 0; + const pageOpend = opened.find((page, index) => { + const same = page.fullPath === fullPath; + pageOpendIndex = same ? index : pageOpendIndex; + return same; + }); + if (pageOpend) { + // 页面以前打开过 + await this.openedUpdate({ + index: pageOpendIndex, + params, + query, + fullPath + }); + } else { + // 页面以前没有打开过 + const page = this.pool.find((t) => t.name === name); + // 如果这里没有找到 page 代表这个路由虽然在框架内 但是不参与标签页显示 + if (page) { + this.add({ + tag: Object.assign({}, page), + params, + query, + fullPath + }); + } + } + // 如果这个页面需要缓存 将其添加到缓存设置 + if (isKeepAlive({ meta })) { + this.keepAlivePush(name); + } + // 设置当前的页面 + this.currentSet(fullPath); + }, + /** + * @class opened + * @description 关闭一个 tag (关闭一个页面) + * @param {Object} context + * @param {Object} payload { tagName: 要关闭的标签名字 } + */ + async close({ tagName }) { + // 预定下个新页面 + let newPage = {}; + const isCurrent = this.current === tagName; + // 如果关闭的页面就是当前显示的页面 + if (isCurrent) { + // 去找一个新的页面 + const len = this.opened.length; + for (let i = 0; i < len; i++) { + if (this.opened[i].fullPath === tagName) { + newPage = i < len - 1 ? this.opened[i + 1] : this.opened[i - 1]; + break; + } + } + } + // 找到这个页面在已经打开的数据里是第几个 + const index = this.opened.findIndex((page) => page.fullPath === tagName); + if (index >= 0) { + // 如果这个页面是缓存的页面 将其在缓存设置中删除 + this.keepAliveRemove(this.opened[index].name); + // 更新数据 删除关闭的页面 + this.opened.splice(index, 1); + } + // 持久化 + await this.opened2db(); + // 决定最后停留的页面 + if (isCurrent) { + // @ts-ignore + const { name = "index", params = {}, query = {} } = newPage; + const routerObj = { name, params, query }; + await router.push(routerObj); + } + }, + /** + * @class opened + * @description 关闭当前标签左边的标签 + * @param opts + */ + async closeLeft(opts = {}) { + await this.closeByCondition({ + condition({ i, currentIndex }) { + return i >= currentIndex; + }, + ...opts + }); + }, + /** + * @class opened + * @description 关闭当前标签右边的标签 + * @param opts + */ + async closeRight(opts = {}) { + await this.closeByCondition({ + condition({ i, currentIndex }) { + return currentIndex >= i; + }, + ...opts + }); + }, + /** + * @class opened + * @description 关闭当前标签右边的标签 + * @param opts + */ + async closeByCondition(opts = {}) { + // @ts-ignore + const { pageSelect, condition } = opts; + const pageAim = pageSelect || this.current; + let currentIndex = 0; + this.opened.forEach((page, index) => { + if (page.fullPath === pageAim) currentIndex = index; + }); + // 删除打开的页面 并在缓存设置中删除 + for (let i = this.opened.length - 1; i >= 0; i--) { + if (this.opened[i].name === "index" || condition({ i, currentIndex })) { + continue; + } + this.keepAliveRemove(this.opened[i].name); + this.opened.splice(i, 1); + } + // 持久化 + await this.opened2db(); + // 设置当前的页面 + this.current = pageAim; + // @ts-ignore + if (router.currentRoute.fullPath !== pageAim) await router.push(pageAim); + }, + /** + * @class opened + * @description 关闭当前激活之外的 tag + * @param opts + */ + async closeOther(opts = {}) { + await this.closeByCondition({ + condition({ i, currentIndex }) { + return currentIndex === i; + }, + ...opts + }); + }, + /** + * @class opened + * @description 关闭所有 tag + * @param {Object} context + */ + async closeAll() { + // 删除打开的页面 并在缓存设置中删除 + for (let i = this.opened.length - 1; i >= 0; i--) { + if (this.opened[i].name === "index") { + continue; + } + + this.keepAliveRemove(this.opened[i].name); + this.opened.splice(i, 1); + } + // 持久化 + await this.opened2db(); + // 关闭所有的标签页后需要判断一次现在是不是在首页 + // @ts-ignore + if (router.currentRoute.name !== "index") { + await router.push({ name: "index" }); + } + }, + /** + * @class keepAlive + * @description 从已经打开的页面记录中更新需要缓存的页面记录 + * @param {Object} state state + */ + keepAliveRefresh() { + this.keepAlive = this.opened.filter((item) => isKeepAlive(item)).map((e) => e.name); + console.log("keep alive:", this.keepAlive); + }, + /** + * @description 删除一个页面的缓存设置 + * @param {Object} state state + * @param {String} name name + */ + keepAliveRemove(name) { + const list = cloneDeep(this.keepAlive); + const index = list.findIndex((item) => item === name); + if (index !== -1) { + list.splice(index, 1); + this.keepAlive = list; + } + }, + /** + * @description 增加一个页面的缓存设置 + * @param {Object} state state + * @param {String} name name + */ + keepAlivePush(name) { + const keep = cloneDeep(this.keepAlive); + keep.push(name); + this.keepAlive = uniq(keep); + }, + /** + * @description 清空页面缓存设置 + * @param {Object} state state + */ + keepAliveClean() { + this.keepAlive = []; + }, + /** + * @class current + * @description 设置当前激活的页面 fullPath + * @param {Object} state state + * @param {String} fullPath new fullPath + */ + currentSet(fullPath) { + this.current = fullPath; + }, + /** + * @class pool + * @description 保存 pool (候选池) + * @param {Object} state state + * @param {Array} routes routes + */ + async init(routes) { + if (this.inited) { + return; + } + this.inited = true; + if (routes == null) { + //不能用全部的routes,只能是framework内的 + routes = frameworkRoutes; + } + + const pool = []; + const push = function (routes) { + routes.forEach((route) => { + if (route.children && route.children.length > 0) { + push(route.children); + } else { + if (!route.hidden) { + const { meta, name, path } = route; + // @ts-ignore + pool.push({ meta, name, path }); + } + } + }); + }; + push(routes); + this.pool = pool; + await this.openedLoad(); + } + } +}); diff --git a/packages/ui/certd-client/src/store/modules/resource.ts b/packages/ui/certd-client/src/store/modules/resource.ts new file mode 100644 index 000000000..33674c972 --- /dev/null +++ b/packages/ui/certd-client/src/store/modules/resource.ts @@ -0,0 +1,127 @@ +import { defineStore } from "pinia"; +// @ts-ignore +import { frameworkMenus, headerMenus, filterMenus, findMenus } from "/src/router/resolve"; +import _ from "lodash-es"; +import { mitter } from "/src/utils/util.mitt"; +//监听注销事件 +mitter.on("app.logout", () => { + const resourceStore = useResourceStore(); + resourceStore.clear(); +}); + +interface ResourceState { + frameworkMenus: Array; + headerMenus: Array; + asideMenus: Array; + fixedAsideMenus: Array; + inited: boolean; + currentAsidePath: string; +} + +export const useResourceStore = defineStore({ + id: "app.resource", + state: (): ResourceState => ({ + // user info + frameworkMenus: [], + headerMenus: [], + asideMenus: [], + fixedAsideMenus: [], + inited: false, + currentAsidePath: "" + }), + getters: { + getAsideMenus() { + return this.asideMenus; + }, + getHeaderMenus() { + return this.headerMenus; + }, + getFrameworkMenus() { + return this.frameworkMenus; + } + }, + actions: { + clear() { + this.inited = false; + }, + /** + * 初始化资源 + */ + init() { + if (this.inited) { + return; + } + this.inited = true; + + const showMenus = _.cloneDeep(frameworkMenus[0].children); + this.frameworkMenus = filterMenus(showMenus, (item) => { + return item?.meta?.showOnHeader !== false; + }); + + this.fixedAsideMenus = findMenus(showMenus, (item) => { + return item?.meta?.fixedAside === true; + }); + this.headerMenus = headerMenus; + this.setAsideMenu(); + }, + setAsideMenu(topMenu?) { + if (this.frameworkMenus.length === 0) { + return; + } + if (topMenu == null) { + topMenu = this.frameworkMenus[0]; + } + const asideMenus = topMenu?.children || []; + this.asideMenus = [...this.fixedAsideMenus, ...asideMenus]; + }, + setAsideMenuByCurrentRoute(matched) { + const menuHeader = this.frameworkMenus; + if (matched?.length <= 1) { + return; + } + + function findFromTree(tree, find) { + const results: Array = []; + for (const item of tree) { + if (find(item)) { + results.push(item); + return results; + } + if (item.children && item.children.length > 0) { + const found = findFromTree(item.children, find); + if (found) { + results.push(item); + return results.concat(found); + } + } + } + } + const matchedPath = matched[1].path; + const _side = findFromTree(menuHeader, (menu) => menu.path === matchedPath); + if (_side?.length > 0) { + if (this.currentAsidePath === _side[0]) { + return; + } + this.currentAsidePath = _side[0]; + this.setAsideMenu(_side[0]); + } + }, + filterByPermission(permissions) { + this.frameworkMenus = this.filterChildrenByPermission(this.frameworkMenus, permissions); + }, + filterChildrenByPermission(list, permissions) { + const menus = list.filter((item) => { + if (item?.meta?.permission) { + return permissions.includes(item.meta.permission); + } + return true; + }); + for (const menu of menus) { + if (menu.children && menu.children.length > 0) { + menu.children = this.filterChildrenByPermission(menu.children, permissions); + } + } + return menus; + } + } +}); diff --git a/packages/ui/certd-client/src/store/modules/settings.ts b/packages/ui/certd-client/src/store/modules/settings.ts new file mode 100644 index 000000000..a6a73dd58 --- /dev/null +++ b/packages/ui/certd-client/src/store/modules/settings.ts @@ -0,0 +1,63 @@ +import { defineStore } from "pinia"; +// @ts-ignore +import { LocalStorage } from "/src/utils/util.storage"; +// import { replaceStyleVariables } from "vite-plugin-theme/es/client"; + +// import { getThemeColors, generateColors } from "/src/../build/theme-colors"; +// +// import { mixLighten, mixDarken, tinycolor } from "vite-plugin-theme/es/colorUtils"; + +// export async function changeTheme(color?: string) { +// if (color == null) { +// return; +// } +// const colors = generateColors({ +// mixDarken, +// mixLighten, +// tinycolor, +// color +// }); +// +// return await replaceStyleVariables({ +// colorVariables: [...getThemeColors(color), ...colors] +// }); +// } + +interface SettingState { + theme: any; +} + +const SETTING_THEME_KEY = "SETTING_THEME"; +export const useSettingStore = defineStore({ + id: "app.setting", + state: (): SettingState => ({ + // user info + theme: null + }), + getters: { + getTheme(): any { + return this.theme || LocalStorage.get(SETTING_THEME_KEY) || {}; + } + }, + actions: { + persistTheme() { + LocalStorage.set(SETTING_THEME_KEY, this.getTheme); + }, + async setTheme(theme?: Object) { + if (theme == null) { + theme = this.getTheme; + } + this.theme = theme; + this.persistTheme(); + // await changeTheme(this.theme.primaryColor); + }, + async setPrimaryColor(color) { + const theme = this.theme; + theme.primaryColor = color; + await this.setTheme(); + }, + async init() { + await this.setTheme(this.getTheme); + } + } +}); diff --git a/packages/ui/certd-client/src/store/modules/user.ts b/packages/ui/certd-client/src/store/modules/user.ts new file mode 100644 index 000000000..45ed7b29b --- /dev/null +++ b/packages/ui/certd-client/src/store/modules/user.ts @@ -0,0 +1,106 @@ +import { defineStore } from "pinia"; +import { store } from "../index"; +import router from "../../router"; +// @ts-ignore +import { LocalStorage } from "/src/utils/util.storage"; +// @ts-ignore +import * as UserApi from "/src/api/modules/api.user"; +// @ts-ignore +import { LoginReq, UserInfoRes } from "/@/api/modules/api.user"; +import { Modal } from "ant-design-vue"; +import { useI18n } from "vue-i18n"; + +import { mitter } from "/src/utils/util.mitt"; + +interface UserState { + userInfo: Nullable; + token?: string; +} + +const USER_INFO_KEY = "USER_INFO"; +const TOKEN_KEY = "TOKEN"; +export const useUserStore = defineStore({ + id: "app.user", + state: (): UserState => ({ + // user info + userInfo: null, + // token + token: undefined + }), + getters: { + getUserInfo(): UserInfoRes { + return this.userInfo || LocalStorage.get(USER_INFO_KEY) || {}; + }, + getToken(): string { + return this.token || LocalStorage.get(TOKEN_KEY); + } + }, + actions: { + setToken(info: string, expire: number) { + this.token = info; + LocalStorage.set(TOKEN_KEY, this.token, expire); + }, + setUserInfo(info: UserInfoRes) { + this.userInfo = info; + LocalStorage.set(USER_INFO_KEY, info); + }, + resetState() { + this.userInfo = null; + this.token = ""; + LocalStorage.remove(TOKEN_KEY); + LocalStorage.remove(USER_INFO_KEY); + }, + /** + * @description: login + */ + async login(params: LoginReq): Promise { + try { + const data = await UserApi.login(params); + const { token, expire } = data; + + // save token + this.setToken(token, expire); + // get user info + const userInfo = await this.getUserInfoAction(); + await router.replace("/"); + mitter.emit("app.login", { userInfo, token: data }); + return userInfo; + } catch (error) { + return null; + } + }, + async getUserInfoAction(): Promise { + const userInfo = await UserApi.mine(); + this.setUserInfo(userInfo); + return userInfo; + }, + /** + * @description: logout + */ + logout(goLogin = true) { + this.resetState(); + goLogin && router.push("/login"); + mitter.emit("app.logout"); + }, + + /** + * @description: Confirm before logging out + */ + confirmLoginOut() { + const { t } = useI18n(); + Modal.config({ + iconType: "warning", + title: t("app.login.logoutTip"), + content: t("app.login.logoutMessage"), + onOk: async () => { + await this.logout(true); + } + }); + } + } +}); + +// Need to be used outside the setup +export function useUserStoreWidthOut() { + return useUserStore(store); +} diff --git a/packages/ui/certd-client/src/style.css b/packages/ui/certd-client/src/style.css new file mode 100644 index 000000000..211ee80a8 --- /dev/null +++ b/packages/ui/certd-client/src/style.css @@ -0,0 +1 @@ +/*占位,勿删*/ diff --git a/packages/ui/certd-client/src/style/common.less b/packages/ui/certd-client/src/style/common.less new file mode 100644 index 000000000..5edfd8dbf --- /dev/null +++ b/packages/ui/certd-client/src/style/common.less @@ -0,0 +1,124 @@ +@import './theme/index.less'; +@import './theme/default.less'; +@import './scroll.less'; +@import './transition.less'; +@import './fix-windicss.less'; +svg { vertical-align: baseline; } +html, body { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + box-sizing: border-box; +} + +div#app { + height: 100% +} + +h1, h2, h3, h4, h5, h6 { + margin-bottom: 0; +} + +.ant-btn-link { + height: 24px; +} + + +.ant-input-affix-wrapper { + padding: 4px 11px; +} + + +.anticon { + vertical-align: 0 !important; +} + + +.text-center{ + text-align: center; +} + +.red{ + color:red +} + +.font12{ + font-size: 12px; +} + +.bg-gray{ + background-color: #eee; +} + +.bg-white{ + background-color: #fff; +} + +.fs-page-header{ + background-color: #fff; +} + + +.ant-btn{ + .fs-iconify{ + font-size:16px; + margin-right:3px + } + + display: inline-flex; + justify-content: center; + align-items: center; + +} + +.ant-timeline{ + .fs-iconify{ + font-size: 16px; + } +} + +.mt-10{ + margin-top:10px; +} +.ml-5{ + margin-left:5px; +} +.ml-10{ + margin-left: 10px; +} +.mtb-5{ + margin: 5px 0 5px 0; +} + +.mb-10{ + margin-bottom: 10px; +} + +.mlr-5{ + margin: 0 5px 0 5px; +} + +.gray{ + color:gray; +} +.green{ + color:green; +} +.blue{ + color:blue; +} +.red{ + color:red +} +.yellow{ + color:yellow; +} + +.ml-2{ + margin-left: 2px; +} + +.font-20{ + font-size:20px +} \ No newline at end of file diff --git a/packages/ui/certd-client/src/style/fix-windicss.less b/packages/ui/certd-client/src/style/fix-windicss.less new file mode 100644 index 000000000..c4e284ecc --- /dev/null +++ b/packages/ui/certd-client/src/style/fix-windicss.less @@ -0,0 +1,3 @@ +img.ant-image-preview-img{ + display: initial; +} diff --git a/packages/ui/certd-client/src/style/scroll.less b/packages/ui/certd-client/src/style/scroll.less new file mode 100644 index 000000000..67b7a33ab --- /dev/null +++ b/packages/ui/certd-client/src/style/scroll.less @@ -0,0 +1,28 @@ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + width: 8px; + background: rgba(#101F1C, 0.1); + -webkit-border-radius: 2em; + -moz-border-radius: 2em; + border-radius: 2em; +} + +::-webkit-scrollbar-thumb { + // background-color: rgba(#101F1C, 0.5); + background-clip: padding-box; + min-height: 28px; + -webkit-border-radius: 2em; + -moz-border-radius: 2em; + border-radius: 2em; + background-color: #b3b3b3; + box-shadow: 0px 1px 1px #eee inset; +} + +::-webkit-scrollbar-thumb:hover { + + //background-color: rgba(#101F1C, 1); +} diff --git a/packages/ui/certd-client/src/style/theme/default.less b/packages/ui/certd-client/src/style/theme/default.less new file mode 100644 index 000000000..9b91b3456 --- /dev/null +++ b/packages/ui/certd-client/src/style/theme/default.less @@ -0,0 +1,27 @@ +.ant-layout{ + background-color: @bg-color; +} +.ant-layout-header { + background-color: @bg-color +} +.ant-layout-sider { + background-color:@bg-color +} +.ant-menu{ + background-color: @bg-color; + &.ant-menu-submenu-popup{ + background-color: transparent; + } +} +.aside-menu{ + .ant-menu-submenu > .ant-menu{ + background-color:@bg-color + } + + .ant-menu-item-active{ + background-color: @bg-menu-item-color; + } + .ant-menu-item-selected{ + background-color: @bg-menu-item-color !important; + } +} diff --git a/packages/ui/certd-client/src/style/theme/index.less b/packages/ui/certd-client/src/style/theme/index.less new file mode 100644 index 000000000..d83434fcf --- /dev/null +++ b/packages/ui/certd-client/src/style/theme/index.less @@ -0,0 +1,4 @@ +@primary-color: #1890ff; +// theme +@bg-color: #ebf1f6; +@bg-menu-item-color:hsla(0,0%,100%,.5); diff --git a/packages/ui/certd-client/src/style/transition.less b/packages/ui/certd-client/src/style/transition.less new file mode 100644 index 000000000..8ab0a2698 --- /dev/null +++ b/packages/ui/certd-client/src/style/transition.less @@ -0,0 +1,36 @@ +//.v-enter-from, +//.v-leave-to { +// opacity: 0; +//} +// +//.v-leave-from, +//.v-enter-to { +// opacity: 1; +//} +// 过渡动画 横向渐变 +.fade-transverse-leave-active, +.fade-transverse-enter-active { + transition: all .5s; +} +.fade-transverse-enter-from { + opacity: 0; + transform: translateX(-30px); +} +.fade-transverse-leave-to { + opacity: 0; + transform: translateX(30px); +} + +// 过渡动画 缩放渐变 +.fade-scale-leave-active, +.fade-scale-enter-active { + transition: all .3s; +} +.fade-scale-enter { + opacity: 0; + transform: scale(1.2); +} +.fade-scale-leave-to { + opacity: 0; + transform: scale(0.8); +} diff --git a/packages/ui/certd-client/src/types/global.d.ts b/packages/ui/certd-client/src/types/global.d.ts new file mode 100644 index 000000000..d40bbd681 --- /dev/null +++ b/packages/ui/certd-client/src/types/global.d.ts @@ -0,0 +1,99 @@ +import type { + ComponentRenderProxy, + VNode, + ComponentPublicInstance, + FunctionalComponent, + PropType as VuePropType, +} from 'vue'; + +declare global { + const __APP_INFO__: { + pkg: { + name: string; + version: string; + dependencies: Recordable; + devDependencies: Recordable; + }; + lastBuildTime: string; + }; + declare interface Window { + // Global vue app instance + __APP__: App; + } + + // vue + declare type PropType = VuePropType; + + export type Writable = { + -readonly [P in keyof T]: T[P]; + }; + + declare type Nullable = T | null; + declare type NonNullable = T extends null | undefined ? never : T; + declare type Recordable = Record; + declare type ReadonlyRecordable = { + readonly [key: string]: T; + }; + declare type Indexable = { + [key: string]: T; + }; + declare type DeepPartial = { + [P in keyof T]?: DeepPartial; + }; + declare type TimeoutHandle = ReturnType; + declare type IntervalHandle = ReturnType; + + declare interface ChangeEvent extends Event { + target: HTMLInputElement; + } + + declare interface WheelEvent { + path?: EventTarget[]; + } + interface ImportMetaEnv extends ViteEnv { + __: unknown; + } + + declare interface ViteEnv { + VITE_PORT: number; + VITE_USE_MOCK: boolean; + VITE_USE_PWA: boolean; + VITE_PUBLIC_PATH: string; + VITE_PROXY: [string, string][]; + VITE_GLOB_APP_TITLE: string; + VITE_GLOB_APP_SHORT_NAME: string; + VITE_USE_CDN: boolean; + VITE_DROP_CONSOLE: boolean; + VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none'; + VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean; + VITE_LEGACY: boolean; + VITE_USE_IMAGEMIN: boolean; + VITE_GENERATE_UI: string; + } + + declare function parseInt(s: string | number, radix?: number): number; + + declare function parseFloat(string: string | number): number; + + namespace JSX { + // tslint:disable no-empty-interface + type Element = VNode; + // tslint:disable no-empty-interface + type ElementClass = ComponentRenderProxy; + interface ElementAttributesProperty { + $props: any; + } + interface IntrinsicElements { + [elem: string]: any; + } + interface IntrinsicAttributes { + [elem: string]: any; + } + } +} + +declare module 'vue' { + export type JSXComponent = + | { new (): ComponentPublicInstance } + | FunctionalComponent; +} diff --git a/packages/ui/certd-client/src/utils/index.ts b/packages/ui/certd-client/src/utils/index.ts new file mode 100644 index 000000000..7e3f9afef --- /dev/null +++ b/packages/ui/certd-client/src/utils/index.ts @@ -0,0 +1,12 @@ +import * as envs from "./util.env"; +import * as sites from "./util.site"; +import * as storages from "./util.storage"; +import * as commons from "./util.common"; +import * as mitt from "./util.mitt"; +export const util = { + ...envs, + ...sites, + ...storages, + ...commons, + ...mitt +}; diff --git a/packages/ui/certd-client/src/utils/util.common.ts b/packages/ui/certd-client/src/utils/util.common.ts new file mode 100644 index 000000000..ca20b8c30 --- /dev/null +++ b/packages/ui/certd-client/src/utils/util.common.ts @@ -0,0 +1,33 @@ +import _ from "lodash-es"; +export default { + arrayToMap(array) { + if (!array) { + return {}; + } + if (!_.isArray(array)) { + return array; + } + const map = {}; + for (const item of array) { + if (item.key) { + map[item.key] = item; + } + } + return map; + }, + mapToArray(map) { + if (!map) { + return []; + } + if (_.isArray(map)) { + return map; + } + const array: any = []; + for (const key in map) { + const item = map[key]; + item.key = key; + array.push(item); + } + return array; + } +}; diff --git a/packages/ui/certd-client/src/utils/util.env.ts b/packages/ui/certd-client/src/utils/util.env.ts new file mode 100644 index 000000000..d87db9091 --- /dev/null +++ b/packages/ui/certd-client/src/utils/util.env.ts @@ -0,0 +1,40 @@ +import _ from "lodash-es"; +export function getEnvValue(key) { + // @ts-ignore + return import.meta.env["VITE_APP_" + key]; +} + +export class EnvConfig { + API; + MODE; + STORAGE; + TITLE; + PM_ENABLED; + constructor() { + this.init(); + } + + init() { + // @ts-ignore + _.forEach(import.meta.env, (value, key) => { + if (key.startsWith("VITE_APP")) { + key = key.replace("VITE_APP_", ""); + this[key] = value; + } + }); + // @ts-ignore + this.MODE = import.meta.env.MODE; + } + + get(key, defaultValue) { + return this[key] ?? defaultValue; + } + isDev() { + return this.MODE === "development" || this.MODE === "debug"; + } + isProd() { + return this.MODE === "production"; + } +} + +export const env = new EnvConfig(); diff --git a/packages/ui/certd-client/src/utils/util.mitt.ts b/packages/ui/certd-client/src/utils/util.mitt.ts new file mode 100644 index 000000000..be0f064f4 --- /dev/null +++ b/packages/ui/certd-client/src/utils/util.mitt.ts @@ -0,0 +1,2 @@ +import mitt from "mitt"; +export const mitter = mitt(); diff --git a/packages/ui/certd-client/src/utils/util.site.ts b/packages/ui/certd-client/src/utils/util.site.ts new file mode 100644 index 000000000..6305d396c --- /dev/null +++ b/packages/ui/certd-client/src/utils/util.site.ts @@ -0,0 +1,11 @@ +import { env } from "./util.env"; +export const site = { + /** + * @description 更新标题 + * @param {String} title 标题 + */ + title: function (titleText) { + const processTitle = env.TITLE || "FsAdmin"; + window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`; + } +}; diff --git a/packages/ui/certd-client/src/utils/util.storage.ts b/packages/ui/certd-client/src/utils/util.storage.ts new file mode 100644 index 000000000..b9342da67 --- /dev/null +++ b/packages/ui/certd-client/src/utils/util.storage.ts @@ -0,0 +1,113 @@ +import { env } from "./util.env"; +function isNullOrUnDef(value) { + return value == null; +} +const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7; +export interface CreateStorageParams { + prefixKey: string; + storage: Storage; + timeout?: number; +} + +/** + *Cache class + *Construction parameters can be passed into sessionStorage, localStorage, + * @class Cache + * @example + */ +export class WebStorage { + private storage: Storage; + private prefixKey?: string; + private timeout?: number; + /** + * + * @param option + */ + constructor(option: Partial) { + this.storage = option.storage ?? localStorage; + this.prefixKey = option.prefixKey ?? getStorageShortName(); + this.timeout = option.timeout ?? DEFAULT_CACHE_TIME; + } + + private getKey(key: string) { + return `${this.prefixKey}${key}`.toUpperCase(); + } + + /** + * + * Set cache + * @param {string} key + * @param {*} value + * @param expire + * @expire Expiration time in seconds + * @memberof Cache + */ + set(key: string, value: any, expire: number | undefined = this.timeout) { + const stringData = JSON.stringify({ + value, + time: Date.now(), + expire: expire != null ? new Date().getTime() + expire * 1000 : null + }); + this.storage.setItem(this.getKey(key), stringData); + } + + /** + *Read cache + * @param {string} key + * @param def + * @memberof Cache + */ + get(key: string, def: any = null): any { + const val = this.storage.getItem(this.getKey(key)); + if (!val) return def; + + try { + const data = JSON.parse(val); + const { value, expire } = data; + if (isNullOrUnDef(expire) || expire >= new Date().getTime()) { + return value; + } + this.remove(key); + } catch (e) { + return def; + } + } + + /** + * Delete cache based on key + * @param {string} key + * @memberof Cache + */ + remove(key: string) { + this.storage.removeItem(this.getKey(key)); + } + + /** + * Delete all caches of this instance + */ + clear(): void { + this.storage.clear(); + } +} +export const createStorage = (option: Partial = {}): WebStorage => { + return new WebStorage(option); +}; + +export type Options = Partial; + +function getStorageShortName() { + return env.MODE + "_" + env.get("STORAGE", "certd") + "_"; +} + +export const createSessionStorage = (options: Options = {}): WebStorage => { + return createStorage({ storage: sessionStorage, ...options }); +}; + +export const createLocalStorage = (options: Options = {}): WebStorage => { + return createStorage({ storage: localStorage, timeout: DEFAULT_CACHE_TIME, ...options }); +}; + +export const SessionStorage = createSessionStorage(); +export const LocalStorage = createLocalStorage(); + +export default WebStorage; diff --git a/packages/ui/certd-client/src/views/certd/access/access-selector/access/crud.jsx b/packages/ui/certd-client/src/views/certd/access/access-selector/access/crud.jsx new file mode 100644 index 000000000..c780d6446 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/access/access-selector/access/crud.jsx @@ -0,0 +1,112 @@ +import * as api from "/@/views/certd/access/api"; +import { ref } from "vue"; +import { getCommonColumnDefine } from "/@/views/certd/access/common"; + +export default function ({ expose, props, ctx }) { + const { crudBinding } = expose; + const lastResRef = ref(); + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + form.type = props.type; + const res = await api.UpdateObj(form); + lastResRef.value = res; + return res; + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + form.type = props.type; + const res = await api.AddObj(form); + lastResRef.value = res; + return res; + }; + + const selectedRowKey = ref([props.modelValue]); + // watch( + // () => { + // return props.modelValue; + // }, + // (value) => { + // selectedRowKey.value = [value]; + // }, + // { + // immediate: true + // } + // ); + const onSelectChange = (changed) => { + selectedRowKey.value = changed; + ctx.emit("update:modelValue", changed[0]); + }; + + const typeRef = ref("aliyun"); + const commonColumnsDefine = getCommonColumnDefine(crudBinding, typeRef); + commonColumnsDefine.type.form.component.disabled = true; + return { + typeRef, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + toolbar: { + show: false + }, + search: { + show: false + }, + form: { + wrapper: { + width: "1050px" + } + }, + rowHandle: { + width: "150px" + }, + table: { + rowSelection: { + type: "radio", + selectedRowKeys: selectedRowKey, + onChange: onSelectChange + }, + customRow: (record) => { + return { + onClick: () => { + onSelectChange([record.id]); + } // 点击行 + }; + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + name: { + title: "名称", + search: { + show: true + }, + type: ["text"], + form: { + rules: [{ required: true, message: "请填写名称" }] + } + }, + ...commonColumnsDefine + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/certd/access/access-selector/access/index.vue b/packages/ui/certd-client/src/views/certd/access/access-selector/access/index.vue new file mode 100644 index 000000000..d4b603413 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/access/access-selector/access/index.vue @@ -0,0 +1,64 @@ + + + + diff --git a/packages/ui/certd-client/src/views/certd/access/access-selector/index.vue b/packages/ui/certd-client/src/views/certd/access/access-selector/index.vue new file mode 100644 index 000000000..b5eec9381 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/access/access-selector/index.vue @@ -0,0 +1,103 @@ + + + + diff --git a/packages/ui/certd-client/src/views/certd/access/api.js b/packages/ui/certd-client/src/views/certd/access/api.js new file mode 100644 index 000000000..50ad40fbd --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/access/api.js @@ -0,0 +1,49 @@ +import { request } from "/src/api/service"; +const apiPrefix = "/pi/access"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "post", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "post", + params: { id } + }); +} + +export function GetProviderDefine(type) { + return request({ + url: apiPrefix + "/define", + method: "post", + params: { type } + }); +} diff --git a/packages/ui/certd-client/src/views/certd/access/common.tsx b/packages/ui/certd-client/src/views/certd/access/common.tsx new file mode 100644 index 000000000..d9a829a59 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/access/common.tsx @@ -0,0 +1,85 @@ +import { dict } from "@fast-crud/fast-crud"; +import * as api from "./api"; +import _ from "lodash-es"; + +export function getCommonColumnDefine(crudBinding, typeRef) { + const AccessTypeDictRef = dict({ + url: "/pi/access/accessTypeDict" + }); + const defaultPluginConfig = { + component: { + name: "a-input", + vModel: "value" + } + }; + + function buildDefineFields(define, mode) { + const columns = crudBinding.value[mode + "Form"].columns; + for (const key in columns) { + if (key.indexOf(".") >= 0) { + delete columns[key]; + } + } + console.log('crudBinding.value[mode + "Form"].columns', columns); + _.forEach(define.input, (value, mapKey) => { + const key = "access." + mapKey; + const field = { + ...value, + key + }; + columns[key] = _.merge({ title: key }, defaultPluginConfig, field); + console.log("form", crudBinding.value[mode + "Form"]); + }); + } + + return { + type: { + title: "类型", + type: "dict-select", + dict: AccessTypeDictRef, + search: { + show: false + }, + form: { + component: { + disabled: false + }, + rules: [{ required: true, message: "请选择类型" }], + valueChange: { + immediate: true, + async handle({ value, mode, form }) { + if (value == null) { + return; + } + const define = await api.GetProviderDefine(value); + console.log("define", define); + buildDefineFields(define, mode); + } + } + }, + addForm: { + value: typeRef + } + }, + setting: { + column: { show: false }, + form: { + show: false, + valueBuilder({ value, form }) { + form.access = {}; + if (!value) { + return; + } + const setting = JSON.parse(value); + for (const key in setting) { + form.access[key] = setting[key]; + } + }, + valueResolve({ form }) { + const setting = form.access; + form.setting = JSON.stringify(setting); + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/certd/access/crud.tsx b/packages/ui/certd-client/src/views/certd/access/crud.tsx new file mode 100644 index 000000000..ccd5d23df --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/access/crud.tsx @@ -0,0 +1,61 @@ +import * as api from "./api"; +import { useI18n } from "vue-i18n"; +import { ref } from "vue"; +import { getCommonColumnDefine } from "/@/views/certd/access/common"; + +export default function ({ expose }) { + const { t } = useI18n(); + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + const typeRef = ref(); + const { crudBinding } = expose; + const commonColumnsDefine = getCommonColumnDefine(crudBinding, typeRef); + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + labelCol: { + span: 6 + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + name: { + title: "名称", + type: "text", + form: { + rules: [{ required: true, message: "必填项" }] + } + }, + ...commonColumnsDefine + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/certd/access/index.vue b/packages/ui/certd-client/src/views/certd/access/index.vue new file mode 100644 index 000000000..379456ba3 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/access/index.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/api.history.ts b/packages/ui/certd-client/src/views/certd/pipeline/api.history.ts new file mode 100644 index 000000000..9cfc54d63 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/api.history.ts @@ -0,0 +1,35 @@ +import { request } from "/src/api/service"; +import { RunHistory } from "/@/views/certd/pipeline/pipeline/type"; + +const apiPrefix = "/pi/history"; + +export async function GetList(query) { + const list = await request({ + url: apiPrefix + "/list", + method: "post", + data: query + }); + for (const item of list) { + if (item.pipeline) { + item.pipeline = JSON.parse(item.pipeline); + } + } + console.log("history", list); + return list; +} + +export async function GetDetail(query): Promise { + const detail = await request({ + url: apiPrefix + "/detail", + method: "post", + params: query + }); + + const pipeline = JSON.parse(detail.history?.pipeline || "{}"); + const logs = JSON.parse(detail.log?.logs || "{}"); + return { + id: detail.history.id, + pipeline, + logs + } as RunHistory; +} diff --git a/packages/ui/certd-client/src/views/certd/pipeline/api.plugin.ts b/packages/ui/certd-client/src/views/certd/pipeline/api.plugin.ts new file mode 100644 index 000000000..42ad9842d --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/api.plugin.ts @@ -0,0 +1,38 @@ +import { request } from "/src/api/service"; +import _ from "lodash-es"; +const apiPrefix = "/pi/plugin"; + +const defaultInputDefine = { + component: { + name: "a-input", + vModel: "modelValue" + } +}; + +export async function GetList(query) { + const plugins = await request({ + url: apiPrefix + "/list", + method: "post", + params: query + }); + for (const plugin of plugins) { + for (const key in plugin.input) { + const field = _.merge({}, defaultInputDefine, plugin.input[key]); + if (field.component.name === "a-input" || field.component.name === "a-select") { + field.component.vModel = "value"; + } + //嵌套对象 + field.key = ["input", key]; + if (field.required) { + delete field.required; + if (field.rules == null) { + field.rules = []; + } + field.rules.push({ required: true, message: "此项必填" }); + } + plugin.input[key] = field; + } + } + console.log("plugins", plugins); + return plugins; +} diff --git a/packages/ui/certd-client/src/views/certd/pipeline/api.ts b/packages/ui/certd-client/src/views/certd/pipeline/api.ts new file mode 100644 index 000000000..4d564c4be --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/api.ts @@ -0,0 +1,66 @@ +import { request } from "/src/api/service"; +const apiPrefix = "/pi/pipeline"; + +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "post", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "post", + params: { id } + }); +} + +export function GetDetail(id) { + return request({ + url: apiPrefix + "/detail", + method: "post", + params: { id } + }); +} + +export function Save(pipelineEntity) { + return request({ + url: apiPrefix + "/save", + method: "post", + data: pipelineEntity + }); +} + +export function Trigger(id) { + return request({ + url: apiPrefix + "/trigger", + method: "post", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/certd/pipeline/certd-form/crud.jsx b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/crud.jsx new file mode 100644 index 000000000..4a2b35752 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/crud.jsx @@ -0,0 +1,129 @@ +import { compute } from "@fast-crud/fast-crud"; +import { Dicts } from "./dicts"; + +export default function () { + return { + crudOptions: { + form: { + wrapper: { + width: "1150px" + } + }, + columns: { + domains: { + title: "域名", + type: ["dict-select"], + search: { + show: true, + component: { + name: "a-input" + } + }, + form: { + col: { + span: 24 + }, + wrapperCol: { + span: null + }, + component: { + mode: "tags", + open: false + }, + helper: { + render: () => { + return ( +
+
支持通配符域名,例如: *.foo.com 、 *.test.handsfree.work
+
支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)
+
多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com)
+
输入一个回车之后,再输入下一个
+
+ ); + } + }, + valueResolve({ form }) { + if (form.domains instanceof String) { + form.domains = form.domains?.join(","); + } + }, + rules: [{ required: true, message: "请填写域名" }] + } + }, + email: { + title: "邮箱", + type: "text", + search: { show: false }, + form: { + rules: [{ required: true, type: "email", message: "请填写邮箱" }] + } + }, + dnsProviderType: { + title: "DNS提供商", + type: "dict-select", + dict: Dicts.dnsProviderTypeDict, + form: { + value: "aliyun", + rules: [{ required: true, message: "请选择DNS提供商" }], + valueChange({ form }) { + form.dnsProviderAccess = null; + } + } + }, + dnsProviderAccess: { + title: "DNS授权", + type: "text", + form: { + component: { + name: "PiAccessSelector", + type: compute(({ form }) => { + return form.dnsProviderType; + }), + vModel: "modelValue" + }, + rules: [{ required: true, message: "请选择DNS授权" }] + } + } + // country: { + // title: "国家", + // type: "text", + // form: { + // value: "China" + // } + // }, + // state: { + // title: "省份", + // type: "text", + // form: { + // value: "GuangDong" + // } + // }, + // locality: { + // title: "市区", + // type: "text", + // form: { + // value: "NanShan" + // } + // }, + // organization: { + // title: "单位", + // type: "text", + // form: { + // value: "CertD" + // } + // }, + // organizationUnit: { + // title: "部门", + // type: "text", + // form: { + // value: "IT Dept" + // } + // }, + // remark: { + // title: "备注", + // type: "text" + // } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/certd/pipeline/certd-form/dicts.ts b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/dicts.ts new file mode 100644 index 000000000..6a5d402df --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/dicts.ts @@ -0,0 +1,9 @@ +import { dict } from "@fast-crud/fast-crud"; + +export const Dicts = { + certIssuerDict: dict({ data: [{ value: "letencrypt", label: "LetEncrypt" }] }), + challengeTypeDict: dict({ data: [{ value: "dns", label: "DNS校验" }] }), + dnsProviderTypeDict: dict({ + url: "pi/dnsProvider/dnsProviderTypeDict" + }) +}; diff --git a/packages/ui/certd-client/src/views/certd/pipeline/certd-form/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/index.vue new file mode 100644 index 000000000..81cd96250 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/index.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/crud.tsx b/packages/ui/certd-client/src/views/certd/pipeline/crud.tsx new file mode 100644 index 000000000..19f6630fe --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/crud.tsx @@ -0,0 +1,225 @@ +import * as api from "./api"; +import { useI18n } from "vue-i18n"; +import { ref, shallowRef } from "vue"; +import { useRouter } from "vue-router"; +import { dict } from "@fast-crud/fast-crud"; +import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status"; +import { nanoid } from "nanoid"; +import { message } from "ant-design-vue"; +export default function ({ expose, certdFormRef }) { + const router = useRouter(); + const { t } = useI18n(); + const lastResRef = ref(); + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + const res = await api.UpdateObj(form); + lastResRef.value = res; + return res; + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + form.content = JSON.stringify({ + title: form.title + }); + const res = await api.AddObj(form); + lastResRef.value = res; + return res; + }; + function addCertdPipeline() { + certdFormRef.value.open(async ({ form }) => { + // 添加certd pipeline + const pipeline = { + title: form.domains[0] + "证书自动化", + stages: [ + { + id: nanoid(), + title: "证书申请阶段", + tasks: [ + { + id: nanoid(), + title: "证书申请任务", + steps: [ + { + id: nanoid(), + title: "申请证书", + input: { + renewDays: 20, + ...form + }, + strategy: { + runStrategy: 0 // 正常执行 + }, + type: "CertApply" + } + ] + } + ] + } + ] + }; + + const id = await api.Save({ + content: JSON.stringify(pipeline), + keepHistoryCount: 30 + }); + message.success("创建成功,请添加证书部署任务"); + router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } }); + }); + } + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + actionbar: { + buttons: { + add: { + order: 5, + text: "自定义流水线" + }, + addCertd: { + order: 1, + text: "添加证书流水线", + type: "primary", + click() { + addCertdPipeline(); + } + } + } + }, + form: { + afterSubmit({ form, res, mode }) { + if (mode === "add") { + router.push({ path: "/certd/pipeline/detail", query: { id: res.id, editMode: "true" } }); + } + } + }, + rowHandle: { + buttons: { + view: { + click({ row }) { + router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "false" } }); + } + }, + config: { + order: 1, + title: null, + type: "link", + icon: "ant-design:edit-outlined", + click({ row }) { + router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "true" } }); + } + }, + edit: { + order: 2, + icon: "ant-design:setting-outlined" + }, + remove: { + order: 5 + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + title: { + title: "流水线名称", + type: "text", + search: { + show: true, + component: { + name: "a-input" + } + }, + column: { + width: 300 + } + }, + lastHistoryTime: { + title: "最后运行", + type: "datetime", + form: { + show: false + } + }, + status: { + title: "状态", + type: "dict-select", + dict: dict({ + data: statusUtil.getOptions() + }), + form: { + show: false + } + }, + + disabled: { + title: "启用", + type: "dict-switch", + dict: dict({ + data: [ + { value: true, label: "禁用" }, + { value: false, label: "启用" } + ] + }), + form: { + value: false, + show: false + }, + column: { + component: { + name: "fs-dict-switch", + vModel: "checked" + }, + async valueChange({ row, key, value }) { + return await api.UpdateObj({ + id: row.id, + disabled: row[key] + }); + } + } + }, + keepHistoryCount: { + title: "历史记录保持数", + type: "number", + form: { + value: 30, + helper: "历史记录保持条数,多余的会被删除" + } + }, + createTime: { + title: "创建时间", + type: "datetime", + form: { + show: false + } + }, + updateTime: { + title: "更新时间", + type: "datetime", + form: { + show: false + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/certd/pipeline/dash-roll.readme b/packages/ui/certd-client/src/views/certd/pipeline/dash-roll.readme new file mode 100644 index 000000000..df3bbf8ba --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/dash-roll.readme @@ -0,0 +1 @@ +https://stackoverflow.com/questions/28365839/dashed-border-animation-in-css3-animation \ No newline at end of file diff --git a/packages/ui/certd-client/src/views/certd/pipeline/detail.vue b/packages/ui/certd-client/src/views/certd/pipeline/detail.vue new file mode 100644 index 000000000..31065ae47 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/detail.vue @@ -0,0 +1,86 @@ + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/index.vue new file mode 100644 index 000000000..be9f15943 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/index.vue @@ -0,0 +1,52 @@ + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/history-timeline-item.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/history-timeline-item.vue new file mode 100644 index 000000000..c27265f6e --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/history-timeline-item.vue @@ -0,0 +1,66 @@ + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/output-selector/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/output-selector/index.vue new file mode 100644 index 000000000..fbef2421f --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/output-selector/index.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/status-show.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/status-show.vue new file mode 100644 index 000000000..0f27d8d89 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/status-show.vue @@ -0,0 +1,46 @@ + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/step-form/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/step-form/index.vue new file mode 100644 index 000000000..433aaee18 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/step-form/index.vue @@ -0,0 +1,314 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-form/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-form/index.vue new file mode 100644 index 000000000..e5d847b93 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-form/index.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-view/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-view/index.vue new file mode 100644 index 000000000..5733cabdf --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-view/index.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/trigger-form/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/trigger-form/index.vue new file mode 100644 index 000000000..5d1c4800a --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/trigger-form/index.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue new file mode 100644 index 000000000..8d731d888 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue @@ -0,0 +1,652 @@ + + + + diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/plugin/index.ts b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/plugin/index.ts new file mode 100644 index 000000000..07efe368c --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/plugin/index.ts @@ -0,0 +1,65 @@ +export class PluginManager { + // @ts-ignore + map: { + [key: string]: any; + } = {}; + + /** + * 初始化plugins + * @param plugins + */ + init(plugins) { + const list = plugins; + const map = {}; + for (const plugin of list) { + map[plugin.key] = plugin; + } + this.map = map; + } + + get(name: string) { + return this.map[name]; + } + + getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }) { + const steps = this.collectionPreStepOutputs({ + pipeline, + currentStageIndex, + currentStepIndex, + currentTask + }); + const options: any[] = []; + for (const step of steps) { + const stepDefine = this.get(step.type); + for (const key in stepDefine?.output) { + options.push({ + value: `step.${step.id}.${key}`, + label: `${stepDefine.output[key].title}【from:${step.title}】`, + type: step.type + }); + } + } + return options; + } + + collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }) { + const steps: any[] = []; + // 开始放step + for (let i = 0; i < currentStageIndex; i++) { + const stage = pipeline.stages[i]; + for (const task of stage.tasks) { + for (const step of task.steps) { + steps.push(step); + } + } + } + //放当前任务下的step + for (let i = 0; i < currentStepIndex; i++) { + const step = currentTask.steps[i]; + steps.push(step); + } + return steps; + } +} + +export const pluginManager = new PluginManager(); diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/type.d.ts b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/type.d.ts new file mode 100644 index 000000000..952304a61 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/type.d.ts @@ -0,0 +1,22 @@ +import { PluginDefine, Pipeline } from "@certd/pipeline/src"; +export * from "@certd/pipeline/src"; +export type PipelineDetail = { + pipeline: Pipeline; +}; + +export type RunHistory = { + id: any; + pipeline: Pipeline; + logs?: { + [id: string]: string[]; + }; +}; + +export type PipelineOptions = { + doTrigger(options: { pipelineId }): Promise; + doSave(pipelineConfig: PipelineDefile): Promise; + getPipelineDetail(query: { pipelineId }): Promise; + getHistoryList(query: { pipelineId }): Promise; + getHistoryDetail(query: { historyId }): Promise; + getPlugins(): Promise; +}; diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/utils/util.status.ts b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/utils/util.status.ts new file mode 100644 index 000000000..429be4015 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/utils/util.status.ts @@ -0,0 +1,49 @@ +const StatusEnum = { + success: { + value: "success", + label: "成功", + color: "green", + icon: "ant-design:check-circle-outlined" + }, + error: { + value: "error", + label: "错误", + color: "red", + icon: "ant-design:info-circle-outlined" + }, + skip: { + value: "skip", + label: "跳过", + color: "blue", + icon: "fluent:arrow-step-over-20-filled" + }, + start: { + value: "start", + label: "运行中", + color: "blue", + spin: true, + icon: "ant-design:sync-outlined" + }, + none: { + value: "none", + label: "未运行", + color: "blue", + icon: "ant-design:minus-circle-twotone" + } +}; +export const statusUtil = { + getColor(status = "none") { + return StatusEnum[status].color; + }, + get(status = "none") { + return StatusEnum[status]; + }, + + getOptions() { + const options: any[] = []; + for (const key of Object.keys(StatusEnum)) { + options.push(StatusEnum[key]); + } + return options; + } +}; diff --git a/packages/ui/certd-client/src/views/crud/advanced/big-data/api.js b/packages/ui/certd-client/src/views/crud/advanced/big-data/api.js new file mode 100644 index 000000000..a2e187ae2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/big-data/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/AdvancedBigData"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/big-data/crud.jsx b/packages/ui/certd-client/src/views/crud/advanced/big-data/crud.jsx new file mode 100644 index 000000000..d66f65d81 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/big-data/crud.jsx @@ -0,0 +1,170 @@ +import * as api from "./api"; +import { message } from "ant-design-vue"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + output: {}, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + scroll: { + //启用横向滚动条,设置一个大于所有列宽之和的值,一般大于表格宽度 + x: 2400 + } + }, + pagination: { + pageSize: 100 + }, + rowHandle: { + fixed: "right" + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + text: { + title: "文本", + type: "text" + }, + dict1: { + title: "字典1", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict1" + }) + }, + dict2: { + title: "字典2", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict2" + }) + }, + dict3: { + title: "字典3", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict3" + }) + }, + dict4: { + title: "字典4", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict4" + }) + }, + dict5: { + title: "字典5", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict5" + }) + }, + dict6: { + title: "字典6", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict6" + }) + }, + dict7: { + title: "字典7", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict7" + }) + }, + dict8: { + title: "字典8", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict8" + }) + }, + dict9: { + title: "字典9", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict9" + }) + }, + dict10: { + title: "字典10", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?from=dict10" + }) + }, + text1: { + title: "文本", + type: "text" + }, + text2: { + title: "文本", + type: "text" + }, + text3: { + title: "文本", + type: "text" + }, + text4: { + title: "文本", + type: "text" + }, + text5: { + title: "文本", + type: "text" + }, + text6: { + title: "文本", + type: "text" + }, + text7: { + title: "文本", + type: "text" + }, + text8: { + title: "文本", + type: "text" + }, + text9: { + title: "文本", + type: "text" + }, + text10: { + title: "文本", + type: "text" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/big-data/index.vue b/packages/ui/certd-client/src/views/crud/advanced/big-data/index.vue new file mode 100644 index 000000000..91a4914c0 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/big-data/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/advanced/big-data/mock.js b/packages/ui/certd-client/src/views/crud/advanced/big-data/mock.js new file mode 100644 index 000000000..a166ac9d3 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/big-data/mock.js @@ -0,0 +1,126 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "AdvancedBigData", + idGenerator: 0, + copyTimes: 1000 +}; +const list = [ + { + text: "测试文本", + dict1: "1", + dict2: "1", + dict3: "2", + dict4: "1", + dict5: "2", + dict6: "1", + dict7: "1", + dict8: "1", + text1: "测试文本1", + text2: "测试文本2", + text3: "测试文本3", + text4: "测试文本4", + text5: "测试文本5", + text6: "测试文本6", + text7: "测试文本7", + text8: "测试文本8", + dict9: "2", + dict10: "1", + dict11: "2", + dict12: "1" + }, + { + text: "测试文本", + dict1: "1", + dict2: "1", + dict3: "2", + dict4: "1", + dict5: "2", + dict6: "1", + dict7: "1", + dict8: "1", + text1: "测试文本1", + text2: "测试文本2", + text3: "测试文本3", + text4: "测试文本4", + text5: "测试文本5", + text6: "测试文本6", + text7: "测试文本7", + text8: "测试文本8", + dict9: "2", + dict10: "1", + dict11: "2", + dict12: "1" + }, + { + text: "测试文本", + dict1: "1", + dict2: "1", + dict3: "2", + dict4: "1", + dict5: "2", + dict6: "1", + dict7: "1", + dict8: "1", + text1: "测试文本1", + text2: "测试文本2", + text3: "测试文本3", + text4: "测试文本4", + text5: "测试文本5", + text6: "测试文本6", + text7: "测试文本7", + text8: "测试文本8", + dict9: "2", + dict10: "1", + dict11: "2", + dict12: "1" + }, + { + text: "测试文本", + dict1: "1", + dict2: "1", + dict3: "2", + dict4: "1", + dict5: "2", + dict6: "1", + dict7: "1", + dict8: "1", + text1: "测试文本1", + text2: "测试文本2", + text3: "测试文本3", + text4: "测试文本4", + text5: "测试文本5", + text6: "测试文本6", + text7: "测试文本7", + text8: "测试文本8", + dict9: "2", + dict10: "1", + dict11: "2", + dict12: "1" + }, + { + text: "测试文本", + dict1: "1", + dict2: "1", + dict3: "2", + dict4: "1", + dict5: "2", + dict6: "1", + dict7: "1", + dict8: "1", + text1: "测试文本1", + text2: "测试文本2", + text3: "测试文本3", + text4: "测试文本4", + text5: "测试文本5", + text6: "测试文本6", + text7: "测试文本7", + text8: "测试文本8", + dict9: "2", + dict10: "1", + dict11: "2", + dict12: "1" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/advanced/from-backend/api.js b/packages/ui/certd-client/src/views/crud/advanced/from-backend/api.js new file mode 100644 index 000000000..ae38d2931 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/from-backend/api.js @@ -0,0 +1,48 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/AdvancedFromBackend"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} +export function GetCrud() { + return request({ + url: apiPrefix + "/crud", + method: "get" + }); +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/from-backend/crud-backend.js b/packages/ui/certd-client/src/views/crud/advanced/from-backend/crud-backend.js new file mode 100644 index 000000000..abcf8ef11 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/from-backend/crud-backend.js @@ -0,0 +1,29 @@ +export const crudOptions = ` + ({expose,dict}) => { + return { + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } +} + + +`; diff --git a/packages/ui/certd-client/src/views/crud/advanced/from-backend/crud.jsx b/packages/ui/certd-client/src/views/crud/advanced/from-backend/crud.jsx new file mode 100644 index 000000000..a577b88d8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/from-backend/crud.jsx @@ -0,0 +1,27 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/from-backend/index.vue b/packages/ui/certd-client/src/views/crud/advanced/from-backend/index.vue new file mode 100644 index 000000000..bec759306 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/from-backend/index.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/advanced/from-backend/mock.js b/packages/ui/certd-client/src/views/crud/advanced/from-backend/mock.js new file mode 100644 index 000000000..2f802d45d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/from-backend/mock.js @@ -0,0 +1,35 @@ +import mockUtil from "/src/mock/base"; +import { crudOptions } from "./crud-backend"; +const options = { + name: "AdvancedFromBackend", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); + +mock.push({ + path: "/AdvancedFromBackend/crud", + method: "get", + handle(req) { + return { + code: 0, + msg: "success", + data: crudOptions + }; + } +}); + +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/api.js b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/api.js new file mode 100644 index 000000000..236e922ac --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/AdvancedInDialog"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/crud.jsx b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/crud.jsx new file mode 100644 index 000000000..cde2c0c2f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/crud.jsx @@ -0,0 +1,106 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + name: { + title: "姓名", + type: "text", //虽然不写也能正确显示组件,但不建议省略它 + search: { show: true }, + form: { + component: { + maxlength: 20 + } + } + }, + search: { + title: "搜索", + type: "text", + form: { + component: { + addonAfter: "后置", + suffix: "suffix", + children: { + addonBefore() { + return ; + } + } + } + } + }, + password: { + title: "密码", + type: "password", + column: { + //一般密码不显示在列里面 + show: false + } + }, + intro: { + title: "简介", + type: "textarea", + form: { + component: { showWordLimit: true, maxlength: 200 } + }, + column: { + ellipsis: true + } + }, + render: { + title: "复杂输入(render)", + form: { + title: "复杂输入", + component: { + render(context) { + console.log("context scope", context); + return ( + + + + + ); + } + } + } + }, + render2: { + title: "我的值是由复杂输入列输入的", + column: { + width: "300px" + }, + form: { + show: false + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/index.vue b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/index.vue new file mode 100644 index 000000000..5b3591efe --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/mock.js b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/mock.js new file mode 100644 index 000000000..31a5c5618 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/crud/mock.js @@ -0,0 +1,40 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "AdvancedInDialog", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + date: "2016-05-02", + status: "0", + province: "1", + avatar: "https://alicdn.antdv.com/vue.png", + show: true, + city: "sz", + address: "123123", + zip: "518000", + intro: "王小虎是element-plus的table示例出现的名字" + }, + { + name: "张三", + date: "2016-05-04", + status: "1", + province: "2" + }, + { + name: "李四", + date: 2232433534511, + status: "1", + province: "0" + }, + { + name: "王五", + date: "2016-05-03", + status: "2", + province: "wh,gz" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/advanced/in-dialog/index.vue b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/index.vue new file mode 100644 index 000000000..64ac77137 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/in-dialog/index.vue @@ -0,0 +1,37 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/advanced/linkage/api.js b/packages/ui/certd-client/src/views/crud/advanced/linkage/api.js new file mode 100644 index 000000000..bbc69cc2f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/linkage/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormLinkage"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/linkage/crud.jsx b/packages/ui/certd-client/src/views/crud/advanced/linkage/crud.jsx new file mode 100644 index 000000000..6fe185407 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/linkage/crud.jsx @@ -0,0 +1,118 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + rowHandle: { + align: "center" + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + province: { + title: "省", + type: "dict-select", + search: { + show: true + }, + dict: dict({ + url: "/mock/linkage/province", + value: "id", + cache: true + }), + form: { + valueChange({ form, value, getComponentRef }) { + form.city = undefined; // 将“city”的值置空 + form.county = undefined; // 将“county”的值置空 + if (value) { + getComponentRef("city").reloadDict(); // 执行city的select组件的reloadDict()方法,触发“city”重新加载字典 + } + } + } + }, + city: { + title: "市", + type: "dict-select", + search: { + show: true + }, + dict: dict({ + cache: true, + prototype: true, + // url() 改成构建url,返回一个url + url({ form }) { + if (form && form.province != null) { + // 本数据字典的url是通过前一个select的选项决定的 + return `/mock/linkage/city?province=${form.province}`; + } + return undefined; // 返回undefined 将不加载字典 + }, + value: "id" + }), + form: { + // 注释同上 + valueChange({ value, form, getComponentRef }) { + if (value) { + form.county = undefined; // 将county的value置空 + const countySelect = getComponentRef("county"); + if (form && form.province && form.city) { + countySelect.reloadDict(); // 重新加载字典项 + } else { + countySelect.clearDict(); // 清空选项 + } + } + } + } + }, + county: { + title: "区", + type: "dict-select", + search: { + show: true + }, + dict: dict({ + value: "id", + cache: true, + prototype: true, + url({ form }) { + if (form && form.province != null && form.city != null) { + return `/mock/linkage/county?province=${form.province} &city=${form.city}`; + } + return undefined; + } + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/linkage/index.vue b/packages/ui/certd-client/src/views/crud/advanced/linkage/index.vue new file mode 100644 index 000000000..48faf05b2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/linkage/index.vue @@ -0,0 +1,38 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/advanced/linkage/mock.js b/packages/ui/certd-client/src/views/crud/advanced/linkage/mock.js new file mode 100644 index 000000000..8e42b2d52 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/linkage/mock.js @@ -0,0 +1,129 @@ +import mockUtil from "/src/mock/base"; +import _ from "lodash-es"; +const options = { + name: "FormLinkage", + idGenerator: 0 +}; +const list = [ + { + province: 10000, + city: 100003, + county: 100004 + }, + { + province: 10010, + city: 100113, + county: 100115 + } +]; +const tree = [ + { + id: 10000, + label: "北京市", + children: [ + { + id: 100003, + label: "北京市辖区", + children: [ + { id: 100004, label: "东城区" }, + { id: 100005, label: "西城区" } + ] + }, + { + id: 100103, + label: "北京郊区", + children: [ + { id: 100104, label: "东郊" }, + { id: 100105, label: "西郊" } + ] + } + ] + }, + { + id: 10010, + label: "天津市", + children: [ + { + id: 100013, + label: "天津市辖区", + children: [ + { id: 100014, label: "天津湾" }, + { id: 100015, label: "渤海湾" } + ] + }, + { + id: 100113, + label: "天津市郊区", + children: [ + { id: 100114, label: "天津湾郊区" }, + { id: 100115, label: "渤海湾郊区" } + ] + } + ] + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); + +function omitChildren(orignalListt) { + const list = []; + orignalListt.forEach((item) => { + list.push(_.omit(item, "children")); + }); + return list; +} +mock.push({ + path: "/mock/linkage/province", + method: "get", + handle() { + const list = omitChildren(tree); + return { + code: 0, + msg: "success", + data: list + }; + } +}); + +mock.push({ + path: "/mock/linkage/city", + method: "get", + handle(req) { + const province = parseInt(req.params.province); + const a = tree.filter((item) => { + return item.id === province; + }); + const list = omitChildren(a[0].children); + return { + code: 0, + msg: "success", + data: list + }; + } +}); + +mock.push({ + path: "/mock/linkage/county", + method: "get", + handle(req) { + const province = parseInt(req.params.province); + const a = tree.filter((item) => { + return item.id === province; + }); + const city = parseInt(req.params.city); + const b = a[0].children.filter((item) => { + return item.id === city; + }); + + const list = omitChildren(b[0].children); + return { + code: 0, + msg: "success", + data: list + }; + } +}); + +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/advanced/local-pagination/api.js b/packages/ui/certd-client/src/views/crud/advanced/local-pagination/api.js new file mode 100644 index 000000000..457dac6d8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/local-pagination/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/AdvancedLocalPagination"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/local-pagination/crud.jsx b/packages/ui/certd-client/src/views/crud/advanced/local-pagination/crud.jsx new file mode 100644 index 000000000..e4989bee1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/local-pagination/crud.jsx @@ -0,0 +1,101 @@ +import * as api from "./api"; +import _ from "lodash-es"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose, localDataRef }) { + const pageRequest = async ({ page, query }) => { + //总数据 + let data = localDataRef.value; + //获取请求参数 + const limit = page.limit; + let offset = page.offset; + data = data.filter((item) => { + // 根据你的业务,编写你的本地查询逻辑 + // text改成你的查询字段 + if (query.status && item.status !== query.status) { + return false; + } + return true; + }); + + // 本地分页 + const start = offset; + let end = offset + limit; + if (data.length < end) { + end = data.length; + } + const records = data.slice(start, end); + + // 构造返回结果 + return { + offset, + limit, + total: localDataRef.value.length, + records + }; + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + await api.UpdateObj(form); + //更新本地数据 + const tableData = localDataRef.value; + for (const item of tableData) { + if (item.id === form.id) { + _.merge(item, form); + } + } + }; + + const addRequest = async ({ form }) => { + const id = await api.AddObj(form); + //本地添加 + form.id = id; + localDataRef.value.unshift(form); + return id; + }; + + const delRequest = async ({ row }) => { + await api.DelObj(row.id); + //本地删除那一条记录 + const tableData = localDataRef.value; + let index = 0; + for (const item of tableData) { + if (item.id === row.id) { + localDataRef.value.splice(index, 1); + } + index++; + } + }; + + return { + output: {}, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + status: { + title: "状态", + search: { show: true }, + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/local-pagination/index.vue b/packages/ui/certd-client/src/views/crud/advanced/local-pagination/index.vue new file mode 100644 index 000000000..d48a680ee --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/local-pagination/index.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/advanced/local-pagination/mock.js b/packages/ui/certd-client/src/views/crud/advanced/local-pagination/mock.js new file mode 100644 index 000000000..120c2a634 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/local-pagination/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "AdvancedLocalPagination", + idGenerator: 0 +}; +const list = [ + { + status: "1" + }, + { + status: "2" + }, + { + status: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/api.js b/packages/ui/certd-client/src/views/crud/advanced/nest/api.js new file mode 100644 index 000000000..b18db901d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/AdvancedNest"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/api.js b/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/api.js new file mode 100644 index 000000000..3a8a4a1b2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/AdvancedAside"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/crud.jsx b/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/crud.jsx new file mode 100644 index 000000000..567715c65 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/crud.jsx @@ -0,0 +1,56 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + pagination: { + showSizeChanger: false, // antdv + showQuickJumper: false // antdv + }, + request: { + pageRequest: api.GetList, + addRequest, + editRequest, + delRequest + }, + toolbar: { + compact: false + }, + rowHandle: { + width: "230px" + }, + table: {}, + columns: { + gradeId: { + title: "年级Id", + search: { show: true }, + type: "number", + column: { + width: 80, + align: "center", + sortable: true + } + }, + class: { + title: "班级", + search: { show: false }, + type: "text", + column: { + sortable: true + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/index.vue b/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/index.vue new file mode 100644 index 000000000..9a198b9e4 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/mock.js b/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/mock.js new file mode 100644 index 000000000..2d70c579f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/aside-table/mock.js @@ -0,0 +1,26 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "AdvancedAside", + idGenerator: 0 +}; +const list = [ + { + class: "一班", + gradeId: 1 + }, + { + class: "二班", + gradeId: 1 + }, + { + class: "三班", + gradeId: 2 + }, + { + class: "四班", + gradeId: 2 + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/crud.jsx b/packages/ui/certd-client/src/views/crud/advanced/nest/crud.jsx new file mode 100644 index 000000000..b42ba5c82 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/crud.jsx @@ -0,0 +1,101 @@ +import * as api from "./api"; +import { ref, shallowRef } from "vue"; +import SubTable from "./sub-table/index.vue"; +import { compute } from "@fast-crud/fast-crud"; +export default function ({ expose, asideTableRef }) { + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + const currentRow = ref(); + + const onCurrentRowChange = (id) => { + currentRow.value = id; + asideTableRef.value.setSearchFormData({ form: { gradeId: id } }); + asideTableRef.value.doRefresh(); + }; + return { + crudOptions: { + table: { + customRow(record, index) { + const clazz = record.id === currentRow.value ? "fs-current-row" : ""; + return { + onClick() { + onCurrentRowChange(record.id); + }, + class: clazz + }; + } + }, + pagination: { + showSizeChanger: false, // antdv + showQuickJumper: false // antdv + }, + form: { + wrapper: { + is: "a-drawer" + } + }, + request: { + pageRequest: api.GetList, + addRequest, + editRequest, + delRequest + }, + rowHandle: { + width: "240px" + }, + toolbar: { + compact: false + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + grade: { + title: "年级", + search: { show: true }, + type: "text", + column: { + sortable: true + } + }, + nestId: { + title: "嵌套表格", + //复合字段类型 + type: ["number", "colspan"], + form: { + // 嵌套表格字段 + rules: [{ required: true, message: "请选择用户" }], + component: { + //局部引用子表格,要用shallowRef包裹 + name: shallowRef(SubTable), + vModel: "modelValue", + gradeId: compute(({ form }) => { + return form.id; + }) + } + // antdv 的跨列配置,需要配置如下三个, 可以通过colspan简化 + // col: { span: 24 }, + // labelCol: { span: 2 }, + // wrapperCol: { span: 21 } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/index.vue b/packages/ui/certd-client/src/views/crud/advanced/nest/index.vue new file mode 100644 index 000000000..5fa4b445e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/index.vue @@ -0,0 +1,59 @@ + + + + diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/mock.js b/packages/ui/certd-client/src/views/crud/advanced/nest/mock.js new file mode 100644 index 000000000..709699d22 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/mock.js @@ -0,0 +1,22 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "AdvancedNest", + idGenerator: 0 +}; +const list = [ + { + grade: "一年级", + nestId: 1 + }, + { + grade: "二年级", + nestId: 2 + }, + { + grade: "三年级", + nestId: 3 + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/api.js b/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/api.js new file mode 100644 index 000000000..167a31de3 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/AdvancedSubTable"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/crud.jsx b/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/crud.jsx new file mode 100644 index 000000000..ad5faa1e5 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/crud.jsx @@ -0,0 +1,62 @@ +import * as api from "./api"; +export default function ({ expose, props, ctx }) { + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + table: { + customRow(record, index) { + const clazz = record.id === props.modelValue ? "fs-current-row" : ""; + return { + onClick() { + ctx.emit("update:modelValue", record.id); + }, + class: clazz + }; + } + }, + request: { + pageRequest: api.GetList, + addRequest, + editRequest, + delRequest + }, + search: { show: false }, + form: { + wrapper: { + is: "a-drawer" + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + name: { + title: "用户姓名", + search: { show: true }, + type: "text", + column: { + sortable: true + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/index.vue b/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/index.vue new file mode 100644 index 000000000..fb9284386 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/index.vue @@ -0,0 +1,61 @@ + + + + diff --git a/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/mock.js b/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/mock.js new file mode 100644 index 000000000..726a628a8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/advanced/nest/sub-table/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "AdvancedSubTable", + idGenerator: 0 +}; +const list = [ + { + name: "张三" + }, + { + name: "李四" + }, + { + name: "王五" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/basis/columns-set/api.js b/packages/ui/certd-client/src/views/crud/basis/columns-set/api.js new file mode 100644 index 000000000..2d20318fa --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/columns-set/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/BasisColumnsSet"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/basis/columns-set/crud.jsx b/packages/ui/certd-client/src/views/crud/basis/columns-set/crud.jsx new file mode 100644 index 000000000..559e49e35 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/columns-set/crud.jsx @@ -0,0 +1,69 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + toolbar: { + columnsFilter: { + mode: "default" + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + }, + disabled: { + title: "列设置禁用", + type: "text", + column: { + columnSetDisabled: true + } + }, + hidden: { + title: "列设置隐藏", + type: "text", + column: { + columnSetShow: false + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/basis/columns-set/index.vue b/packages/ui/certd-client/src/views/crud/basis/columns-set/index.vue new file mode 100644 index 000000000..a54361075 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/columns-set/index.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/basis/columns-set/mock.js b/packages/ui/certd-client/src/views/crud/basis/columns-set/mock.js new file mode 100644 index 000000000..90154b73b --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/columns-set/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "BasisColumnsSet", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/basis/compute-more/api.js b/packages/ui/certd-client/src/views/crud/basis/compute-more/api.js new file mode 100644 index 000000000..39c56ba12 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/compute-more/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormComputeMore"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/basis/compute-more/crud.jsx b/packages/ui/certd-client/src/views/crud/basis/compute-more/crud.jsx new file mode 100644 index 000000000..92ccdee89 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/compute-more/crud.jsx @@ -0,0 +1,79 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { useCompute } from "@fast-crud/fast-crud"; +import { message } from "ant-design-vue"; +import { ref, computed } from "vue"; +const { asyncCompute, compute } = useCompute(); +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + //普通的ref引用,可以动态切换配置 + const defValueRef = ref("我是动态的默认值"); + const defValueComputed = computed(() => { + return defValueRef.value; + }); + return { + output: { + defValueRef, + defValueComputed + }, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + scroll: { + x: 1500 + } + }, + form: { + labelCol: { span: 8 }, + wrapperCol: { span: 14 } + }, + rowHandle: { + fixed: "right", + align:'center', + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + defValue: { + title: "默认值", + type: "text", + search: { show: true, value: null }, + form: { + // form.value不支持asyncCompute + // 假如你的默认值异步获取的,那么你自己必须保证先异步计算完成之后,才能打开对话框。 + // 因为在打开对话框时,默认值就必须得设置好。 + value: defValueRef + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/basis/compute-more/index.vue b/packages/ui/certd-client/src/views/crud/basis/compute-more/index.vue new file mode 100644 index 000000000..01d9da383 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/compute-more/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/basis/compute-more/mock.js b/packages/ui/certd-client/src/views/crud/basis/compute-more/mock.js new file mode 100644 index 000000000..ee602acfa --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/compute-more/mock.js @@ -0,0 +1,32 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FormComputeMore", + idGenerator: 0 +}; +const list = [ + { + ref: "根据showRef显示", + compute: true, + status: "1", + remote: "2", + shower: "---> 点右边编辑查看示例效果", + remote2: "2", + editable: true + }, + { + compute: false, + status: "2", + remote: "0", + remote2: "2", + editable: false + }, + { + compute: true, + status: "0", + remote2: "2", + editable: true + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/basis/compute/api.js b/packages/ui/certd-client/src/views/crud/basis/compute/api.js new file mode 100644 index 000000000..c8ec9fc8e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/compute/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormCompute"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/basis/compute/crud.jsx b/packages/ui/certd-client/src/views/crud/basis/compute/crud.jsx new file mode 100644 index 000000000..0f450f138 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/compute/crud.jsx @@ -0,0 +1,211 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { useCompute } from "@fast-crud/fast-crud"; +import { message } from "ant-design-vue"; +import { ref, computed } from "vue"; +const { asyncCompute, compute } = useCompute(); +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + //普通的ref引用,可以动态切换配置 + const showRef = ref(false); + const showTableRef = ref(true); + const showTableComputed = computed(() => { + return showTableRef.value; + }); + + const columnComponentShowRef = ref(true); + const columnComponentShowComputed = computed(() => { + return columnComponentShowRef.value; + }); + + return { + output: { + showRef, + showTableRef, + columnComponentShowRef + }, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + scroll: { + x: 1500 + }, + //通过switch动态显隐table + show: showTableComputed //不仅支持computed,直接传showTableRef也是可以的 + }, + form: { + labelCol: { span: 8 }, + wrapperCol: { span: 14 } + }, + rowHandle: { + fixed: "right", + buttons: { + edit: { + show: compute(({ row }) => { + return row.editable; + }) + }, + remove: { + show: compute(({ row }) => { + return row.editable; + }) + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50, + resizable: true + }, + form: { + show: false + } + }, + refSwitch: { + title: "ref引用切换", + type: "text", + form: { + helper: "点我切换右边的输入框显示" + } + }, + ref: { + title: "根据ref引用显示", + type: ["text"], + form: { + component: { + show: showRef + }, + helper: "我会根据showRef进行显隐" + } + }, + compute: { + title: "compute", + search: { show: false }, + type: "text", + column: { + show: columnComponentShowComputed, + columnSetDisabled: true, //这里采用自定义控制显隐,那么列设置里面就要禁用 + // columnSetShow: false, //直接不在列设置里面显示也行 + component: { + name: "a-switch", + vModel: "checked" + } + }, + form: { + component: { + name: "a-switch", + vModel: "checked" + }, + helper: "点我触发动态计算" + } + }, + shower: { + title: "根据compute显示", + search: { show: false }, + type: "button", + form: { + component: { + show: compute(({ form }) => { + return form.compute; + }) + } + }, + column: { + width: 250, + resizable: true, + component: { + show: compute(({ row }) => { + return row.compute; + }) + } + } + }, + remote: { + title: "asyncCompute", + search: { show: true }, + type: "text", + form: { + component: { + name: "a-select", + vModel: "value", + placeholder: "异步计算远程获取options", + options: asyncCompute({ + async asyncFn(watchValue, context) { + const url = "/mock/dicts/OpenStatusEnum?remote"; + return await requestForMock({ url }); + } + }) + }, + helper: "我的options是异步计算远程获取的,只会获取一次" + } + }, + remote2: { + title: "监听switch触发异步计算", + search: { show: false }, + type: "text", + form: { + component: { + name: "a-select", + vModel: "value", + placeholder: "异步计算远程获取options", + options: asyncCompute({ + watch({ form }) { + return form.compute; + }, + async asyncFn(watchValue) { + message.info("监听switch,触发远程获取options"); + const url = watchValue + ? "/mock/dicts/OpenStatusEnum?remote" + : "/mock/dicts/moreOpenStatusEnum?remote"; + return await requestForMock({ url }); + } + }) + }, + helper: "监听其他属性修改后,触发重新计算" + }, + column: { + width: 200 + } + }, + editable: { + title: "可编辑", + search: { show: false }, + type: "text", + column: { + fixed: "right", + component: { + name: "a-switch", + vModel: "checked" + } + }, + form: { + show: false + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/basis/compute/index.vue b/packages/ui/certd-client/src/views/crud/basis/compute/index.vue new file mode 100644 index 000000000..d7408935e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/compute/index.vue @@ -0,0 +1,60 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/basis/compute/mock.js b/packages/ui/certd-client/src/views/crud/basis/compute/mock.js new file mode 100644 index 000000000..b4c21d853 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/compute/mock.js @@ -0,0 +1,34 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FormCompute", + idGenerator: 0 +}; +const list = [ + { + ref: "根据showRef显示", + compute: true, + status: "1", + remote: "2", + shower: "---> 点右边编辑查看示例效果", + remote2: "2", + editable: true + }, + { + compute: false, + shower: "---> 点右边编辑查看示例效果", + status: "2", + remote: "0", + remote2: "2", + editable: false + }, + { + compute: true, + shower: "---> 点右边编辑查看示例效果", + status: "0", + remote2: "2", + editable: true + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/basis/first/index.vue b/packages/ui/certd-client/src/views/crud/basis/first/index.vue new file mode 100644 index 000000000..6edf3a6d1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/first/index.vue @@ -0,0 +1,109 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/basis/i18n/api.js b/packages/ui/certd-client/src/views/crud/basis/i18n/api.js new file mode 100644 index 000000000..d62b1b729 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/i18n/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/BasisI18n"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/basis/i18n/crud.jsx b/packages/ui/certd-client/src/views/crud/basis/i18n/crud.jsx new file mode 100644 index 000000000..e789ca3e4 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/i18n/crud.jsx @@ -0,0 +1,72 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { useI18n } from "vue-i18n"; +export default function ({ expose }) { + const { t } = useI18n(); + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + name: { + title: t("app.crud.i18n.name"), + type: "text", + search: { show: true } + }, + city: { + title: t("app.crud.i18n.city"), + type: "dict-select", + search: { show: true }, + dict: dict({ + value: "id", + label: "text", + data: [ + { id: "sz", text: "深圳", color: "success" }, + { id: "gz", text: "广州", color: "blue" }, + { id: "bj", text: "北京" }, + { id: "wh", text: "武汉" }, + { id: "sh", text: "上海" } + ] + }) + }, + radio: { + title: t("app.crud.i18n.status"), + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/basis/i18n/index.vue b/packages/ui/certd-client/src/views/crud/basis/i18n/index.vue new file mode 100644 index 000000000..3c50ea0a3 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/i18n/index.vue @@ -0,0 +1,53 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/basis/i18n/mock.js b/packages/ui/certd-client/src/views/crud/basis/i18n/mock.js new file mode 100644 index 000000000..4bbcc95b7 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/i18n/mock.js @@ -0,0 +1,25 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "BasisI18n", + idGenerator: 0 +}; +const list = [ + { + radio: "1", + name: "张三", + city: "sz" + }, + { + radio: "2", + name: "李四", + city: "gz" + }, + { + radio: "0", + name: "王五", + city: "sh" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/basis/layout-card/api.js b/packages/ui/certd-client/src/views/crud/basis/layout-card/api.js new file mode 100644 index 000000000..1ee8f29fa --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/layout-card/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/BasisLayoutCard"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/basis/layout-card/crud.jsx b/packages/ui/certd-client/src/views/crud/basis/layout-card/crud.jsx new file mode 100644 index 000000000..085f48522 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/layout-card/crud.jsx @@ -0,0 +1,73 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ crudExpose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + container: { + is: "fs-layout-card" + }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + name: { + title: "姓名", + type: "text", + search: { show: true } + }, + city: { + title: "城市", + type: "dict-select", + search: { show: true }, + dict: dict({ + value: "id", + label: "text", + data: [ + { id: "sz", text: "深圳", color: "success" }, + { id: "gz", text: "广州", color: "blue" }, + { id: "bj", text: "北京" }, + { id: "wh", text: "武汉" }, + { id: "sh", text: "上海" } + ] + }) + }, + radio: { + title: "单选", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/basis/layout-card/index.vue b/packages/ui/certd-client/src/views/crud/basis/layout-card/index.vue new file mode 100644 index 000000000..60597b7e4 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/layout-card/index.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/crud/basis/layout-card/mock.js b/packages/ui/certd-client/src/views/crud/basis/layout-card/mock.js new file mode 100644 index 000000000..417d9601f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/layout-card/mock.js @@ -0,0 +1,25 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "BasisLayoutCard", + idGenerator: 0 +}; +const list = [ + { + radio: "1", + name: "张三", + city: "sz" + }, + { + radio: "2", + name: "李四", + city: "gz" + }, + { + radio: "0", + name: "王五", + city: "sh" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/basis/layout-custom/api.js b/packages/ui/certd-client/src/views/crud/basis/layout-custom/api.js new file mode 100644 index 000000000..f243b4020 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/layout-custom/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/BasisLayoutCustom"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/basis/layout-custom/crud.jsx b/packages/ui/certd-client/src/views/crud/basis/layout-custom/crud.jsx new file mode 100644 index 000000000..851929944 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/layout-custom/crud.jsx @@ -0,0 +1,75 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import CustomLayout from "./custom-layout.vue"; +import { shallowRef } from "vue"; +export default function ({ crudExpose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + container: { + is: shallowRef(CustomLayout) //可以将自定义布局组件全局注册,这里只需要配置name即可 + }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + name: { + title: "姓名", + type: "text", + search: { show: true } + }, + city: { + title: "城市", + type: "dict-select", + search: { show: false }, + dict: dict({ + value: "id", + label: "text", + data: [ + { id: "sz", text: "深圳", color: "success" }, + { id: "gz", text: "广州", color: "blue" }, + { id: "bj", text: "北京" }, + { id: "wh", text: "武汉" }, + { id: "sh", text: "上海" } + ] + }) + }, + radio: { + title: "单选", + search: { show: false }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/basis/layout-custom/custom-layout.vue b/packages/ui/certd-client/src/views/crud/basis/layout-custom/custom-layout.vue new file mode 100644 index 000000000..0f65a9b21 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/layout-custom/custom-layout.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/crud/basis/layout-custom/index.vue b/packages/ui/certd-client/src/views/crud/basis/layout-custom/index.vue new file mode 100644 index 000000000..747c9dd9e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/layout-custom/index.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/crud/basis/layout-custom/mock.js b/packages/ui/certd-client/src/views/crud/basis/layout-custom/mock.js new file mode 100644 index 000000000..d877b043e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/layout-custom/mock.js @@ -0,0 +1,25 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "BasisLayoutCustom", + idGenerator: 0 +}; +const list = [ + { + radio: "1", + name: "张三", + city: "sz" + }, + { + radio: "2", + name: "李四", + city: "gz" + }, + { + radio: "0", + name: "王五", + city: "sh" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/basis/value-change/api.js b/packages/ui/certd-client/src/views/crud/basis/value-change/api.js new file mode 100644 index 000000000..35fdfa0c1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/value-change/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/BasisValueChange"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/basis/value-change/crud.jsx b/packages/ui/certd-client/src/views/crud/basis/value-change/crud.jsx new file mode 100644 index 000000000..15dde4a4b --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/value-change/crud.jsx @@ -0,0 +1,95 @@ +import * as api from "./api"; +import { message } from "ant-design-vue"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + output: {}, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + switch: { + title: "开关", + type: "dict-switch", + dict: dict({ + data: [ + { value: true, label: "开启" }, + { value: false, label: "关闭" } + ] + }), + column: { + component: { + name: "fs-dict-switch", + vModel: "checked" + }, + valueChange(context) { + console.log("column value changed:", context); + } + }, + form: { + valueChange({ value, key, form }) { + console.log("valueChanged,", key, value, form); + message.info(`valueChanged:${key}=${value}`); + } + } + }, + normal: { + title: "value-change", + type: "text", + form: { + valueChange({ value, key, form }) { + console.log("valueChanged,", key, value, form); + message.info(`valueChanged:${key}=${value}`); + } + } + }, + immediate: { + title: "immediate", + type: "text", + search: { + show: true + }, + form: { + valueChange: { + handle({ value, key, form, immediate }) { + console.log("valueChange,", key, value, "isImmediate=", immediate); + message.info(`valueChanged:${key}=${value},isImmediate=${immediate}`); + }, + immediate: true + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/basis/value-change/index.vue b/packages/ui/certd-client/src/views/crud/basis/value-change/index.vue new file mode 100644 index 000000000..0a6bc659c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/value-change/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/basis/value-change/mock.js b/packages/ui/certd-client/src/views/crud/basis/value-change/mock.js new file mode 100644 index 000000000..2d16ebcc6 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/basis/value-change/mock.js @@ -0,0 +1,32 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "BasisValueChange", + idGenerator: 0 +}; +const list = [ + { + ref: "根据showRef显示", + compute: true, + status: "1", + remote: "2", + shower: "---> 点右边编辑查看示例效果", + remote2: "2", + editable: true + }, + { + compute: false, + status: "2", + remote: "0", + remote2: "2", + editable: false + }, + { + compute: true, + status: "0", + remote2: "2", + editable: true + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/button/api.js b/packages/ui/certd-client/src/views/crud/component/button/api.js new file mode 100644 index 000000000..99ab67a8a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/button/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentButton"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/button/crud.jsx b/packages/ui/certd-client/src/views/crud/component/button/crud.jsx new file mode 100644 index 000000000..4e10ee199 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/button/crud.jsx @@ -0,0 +1,115 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { dict, compute } from "@fast-crud/fast-crud"; +import { message } from "ant-design-vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + //配置表单label的宽度 + labelCol: { span: 6 } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + button: { + title: "按钮", + search: { show: true }, + type: "button", + column: { + component: { + show: compute(({ value }) => { + //当value为null时,不显示 + return value != null; + }), + on: { + // 注意:必须要on前缀 + onClick({ row }) { + message.success("按钮点击:" + row.button); + } + } + } + } + }, + url: { + title: "url", + search: { show: true }, + type: "text", + column: { + show: false + } + }, + link: { + title: "链接", + search: { show: true }, + type: "link", + column: { + component: { + on: { + // 注意:必须要on前缀 + onClick({ row }) { + if (row.url) { + window.open(row.url); + } + } + } + } + }, + form: { + title: "按钮文字" + } + }, + link2: { + title: "手写link配置", + search: { show: true }, + type: "text", //form组件用input + column: { + component: { + name: "fs-button", //列展示组件为button + vModel: "text", // 将row.link2的值赋值给text属性 + type: "link", // 按钮展示为链接样式 + on: { + //注册点击事件 + // 注意:必须要on前缀 + onClick({ row }) { + if (row.url) { + window.open(row.url); + } + } + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/button/index.vue b/packages/ui/certd-client/src/views/crud/component/button/index.vue new file mode 100644 index 000000000..bf86cd95a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/button/index.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/crud/component/button/mock.js b/packages/ui/certd-client/src/views/crud/component/button/mock.js new file mode 100644 index 000000000..bddcce225 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/button/mock.js @@ -0,0 +1,23 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentButton", + idGenerator: 0 +}; +const list = [ + { + button: "张三", + link: "百度", + url: "https://www.baidu.com", + link2: "手写配置" + }, + { + button: "李四", + link: "百度", + url: "https://www.baidu.com", + link2: "手写配置" + }, + {} +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/cascader/api.js b/packages/ui/certd-client/src/views/crud/component/cascader/api.js new file mode 100644 index 000000000..8e851ae50 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/cascader/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentCascader"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/cascader/crud.jsx b/packages/ui/certd-client/src/views/crud/component/cascader/crud.jsx new file mode 100644 index 000000000..2b5206b7d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/cascader/crud.jsx @@ -0,0 +1,137 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ crudRef }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + // 单列布局 + col: { span: 24 }, + labelCol: { span: 4 }, + wrapperCol: { span: 18 } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + cascader: { + title: "级联", + search: { show: true }, + type: "dict-cascader", + dict: dict({ + cloneable: false, + isTree: true, + url: "/mock/dicts/cascaderData?single" + }) + }, + lazyLoad: { + title: "懒加载", + type: "dict-cascader", + dict: dict({ + url: "/mock/tree/GetTreeChildrenByParentId?lazyLoad", + value: "code", + label: "name", + isTree: true, + prototype: true, + getNodesByValues(values) { + //给cell展示组件调用,根据value值获取节点,每行都会请求一次 + if (values == null) { + return []; + } + return requestForMock({ + url: "/mock/tree/GetNodesByValues", + params: { values } + }); + } + }), + form: { + component: { + vModel: "value", + options: [ + { + code: "11", + name: "北京", + isLeaf: false + }, + { + code: "12", + name: "天津", + isLeaf: false + } + ], + loadData: async (selectedOptions) => { + console.log("lazyLoad", selectedOptions); + const targetOption = selectedOptions[selectedOptions.length - 1]; + targetOption.loading = true; + + const ret = await requestForMock({ + url: "/mock/tree/GetTreeChildrenByParentId", + params: { parentId: targetOption.code } + }); + targetOption.loading = false; + const list = []; + for (const item of ret) { + list.push({ + code: item.code, + name: item.name, + isLeaf: item.leaf === true + }); + } + targetOption.children = list; + //options.value = [...options.value]; + }, + changeOnSelect: true + } + } + }, + multiple: { + title: "可搜索,可只选父节点", + type: "dict-cascader", + dict: dict({ + isTree: true, + url: "/mock/dicts/cascaderData?multiple" + }), + form: { + component: { + showSearch: { + filter: (inputValue, path) => { + return path.some((option) => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1); + } + }, + "change-on-select": true + }, + helper: "antd cascader 不支持级联多选" + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/cascader/index.vue b/packages/ui/certd-client/src/views/crud/component/cascader/index.vue new file mode 100644 index 000000000..58428ac78 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/cascader/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/cascader/mock.js b/packages/ui/certd-client/src/views/crud/component/cascader/mock.js new file mode 100644 index 000000000..01bc0582f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/cascader/mock.js @@ -0,0 +1,23 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentCascader", + idGenerator: 0 +}; +const list = [ + { + cascader: ["zhinan", "shejiyuanze", "yizhi"], + lazyLoad: ["11", "1101", "110101", "110101001"], + multiple: ["antdv cascader不支持多选"] + }, + { + cascader: ["zhinan", "shejiyuanze", "yizhi"], + multiple: ["antdv cascader不支持多选"] + }, + { + cascader: ["zhinan", "shejiyuanze", "yizhi"], + multiple: ["antdv cascader不支持多选"] + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/checkbox/api.js b/packages/ui/certd-client/src/views/crud/component/checkbox/api.js new file mode 100644 index 000000000..f1d4edba9 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/checkbox/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentCheckbox"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/checkbox/crud.jsx b/packages/ui/certd-client/src/views/crud/component/checkbox/crud.jsx new file mode 100644 index 000000000..ba55af1bc --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/checkbox/crud.jsx @@ -0,0 +1,50 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ crudRef }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + checkbox: { + title: "状态", + search: { show: true }, + type: "dict-checkbox", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/checkbox/index.vue b/packages/ui/certd-client/src/views/crud/component/checkbox/index.vue new file mode 100644 index 000000000..a1dd4729c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/checkbox/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/checkbox/mock.js b/packages/ui/certd-client/src/views/crud/component/checkbox/mock.js new file mode 100644 index 000000000..1763e9bb6 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/checkbox/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentCheckbox", + idGenerator: 0 +}; +const list = [ + { + checkbox: ["1", "2"] + }, + { + checkbox: "2" + }, + { + checkbox: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/date/api.js b/packages/ui/certd-client/src/views/crud/component/date/api.js new file mode 100644 index 000000000..f6a0197f3 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/date/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentDate"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/date/crud.jsx b/packages/ui/certd-client/src/views/crud/component/date/crud.jsx new file mode 100644 index 000000000..ba405c562 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/date/crud.jsx @@ -0,0 +1,174 @@ +import * as api from "./api"; +import { utils } from "@fast-crud/fast-crud"; +import dayjs from "dayjs"; + +console.log("utils", utils); +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + scroll: { x: 2000 } + }, + rowHandle: { fixed: "right" }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + timestamp: { + title: "时间戳", + type: "datetime", + search: { + show: true, + width: 185, + component: {} + }, + valueBuilder({ value, row, key }) { + console.log("value builder:", key, value, row); + if (value != null) { + row[key] = dayjs(value); + } + }, + valueResolve({ value, row, key }) { + if (value != null) { + row[key] = value.unix(); + } + } + }, + humanize: { + type: ["datetime", "time-humanize"], + title: "人性化时间", + column: { + component: { + options: { + largest: 2 + } + } + } + }, + datetime: { + title: "日期时间", + type: "datetime", + form: { + component: { + valueFormat: "YYYY-MM-DD HH:mm:ss" //输入值的格式 + } + } + }, + format: { + title: "格式化", + type: "datetime", + form: { + component: { + format: "YYYY年MM月DD日 HH:mm", + valueFormat: "YYYY-MM-DD HH:mm:ss" //输入值的格式 + } + }, + column: { + width: 180, + component: { + // 行展示组件使用的dayjs, + format: "YYYY年MM月DD日 HH:mm" + } + } + }, + date: { + title: "仅日期", + type: "date", + form: { + component: { + valueFormat: "YYYY-MM-DD HH:mm:ss", //输入值的格式 + events: { + onChange(context) { + console.log("change", context); + } + } + } + } + }, + time: { + title: "仅时间", + type: "time", + form: { + component: { + valueFormat: "YYYY-MM-DD HH:mm:ss" //输入值的格式 + } + } + }, + disabledDate: { + title: "禁用日期", + type: "date", + form: { + component: { + valueFormat: "YYYY-MM-DD HH:mm:ss", //输入值的格式 + disabledDate(current) { + return current && current < dayjs().endOf("day"); + } + } + } + }, + daterange: { + title: "日期范围", + type: "daterange", + search: { show: true, width: 300 }, + valueBuilder({ row, key }) { + if (!utils.strings.hasEmpty(row.daterangeStart, row.daterangeEnd)) { + row[key] = [dayjs(row.daterangeStart), dayjs(row.daterangeEnd)]; + } + } + }, + datetimerange: { + title: "日期时间范围", + type: "datetimerange", + search: { show: true, width: 300 }, + valueBuilder({ row, key }) { + if (!utils.strings.hasEmpty(row.datetimerangeStart, row.datetimerangeEnd)) { + row[key] = [dayjs(row.datetimerangeStart), dayjs(row.datetimerangeEnd)]; + } + }, + valueResolve({ form, key }) { + const row = form; + if (row[key] != null && !utils.strings.hasEmpty(row[key])) { + row.datetimerangeStart = row[key][0]; + row.datetimerangeEnd = row[key][1]; + } else { + row.datetimerangeStart = null; + row.datetimerangeEnd = null; + } + } + }, + customType: { + title: "自定义字段类型", + type: "time2" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/date/index.vue b/packages/ui/certd-client/src/views/crud/component/date/index.vue new file mode 100644 index 000000000..8ec795284 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/date/index.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/date/mock.js b/packages/ui/certd-client/src/views/crud/component/date/mock.js new file mode 100644 index 000000000..895fddda9 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/date/mock.js @@ -0,0 +1,44 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "ComponentDate", + idGenerator: 0 +}; +const list = [ + { + timestamp: 123123123123, + humanize: new Date().getTime() - 11111111, + datetime: "2019-09-01 11:11:11", + date: "2019-09-02 11:11:11", + format: "2019-09-21 11:11:11", + time: "2019-09-22 12:11:11", + daterangeStart: "2019-09-23 11:11:11", + daterangeEnd: "2019-09-24 11:11:11", + datetimerangeStart: "2019-09-25 11:11:11", + datetimerangeEnd: "2019-09-26 11:11:11" + }, + { + // timestamp: 444444555, + datetime: "2017-09-20 11:11:11", + date: "2019-09-20 11:11:11", + humanize: new Date().getTime() - 22222222, + // time: 12313123334, + daterangeStart: "2019-09-20 11:11:11", + daterangeEnd: "2019-09-21 11:11:11", + datetimerangeStart: "2019-09-20 11:11:11", + datetimerangeEnd: "2019-09-21 11:11:11" + }, + { + // timestamp: 5555555555, + datetime: "2017-09-20 11:11:11", + date: "2019-09-20 11:11:11", + // time: 12313123334, + daterangeStart: "2019-09-20 11:11:11", + daterangeEnd: "2019-09-21 11:11:11", + datetimerangeStart: "2019-09-20 11:11:11", + datetimerangeEnd: "2019-09-21 11:11:11" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/editor/api.js b/packages/ui/certd-client/src/views/crud/component/editor/api.js new file mode 100644 index 000000000..523cfb370 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/editor/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentEditor"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/editor/crud.jsx b/packages/ui/certd-client/src/views/crud/component/editor/crud.jsx new file mode 100644 index 000000000..d32431b78 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/editor/crud.jsx @@ -0,0 +1,107 @@ +import * as api from "./api"; +import { utils, dict, compute } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + title: { + title: "标题", + type: "text", + column: { + width: 400 + }, + form:{ + col: { span: 24 }, + }, + }, + text: { + title: "摘要", + type: "textarea", + form:{ + col: { span: 24 }, + }, + viewForm: { + component: { + name: null, + render(h, scope) { + return
{scope.value}
; + } + } + } + }, + disabled: { + title: "禁用启用", + search: { show: false }, + type: "dict-switch", + dict: dict({ + data: [ + { value: true, label: "禁用" }, + { value: false, label: "启用" } + ] + }) + }, + content_wang: { + title: "内容", + column: { + width: 300, + show: false + }, + type: ["editor-wang5"], // 富文本图片上传依赖file-uploader,请先配置好file-uploader + form: { + helper:"示例已升级到wangEditor5版本,原来的editor-wang目前仍然可以使用,后续fs升级可能会将其删除,请尽快升级到editor-wang5版本", + col: { span: 24 }, + // 动态显隐字段 + // show: compute(({ form }) => { + // return form.change === "wang"; + // }), + rules: [{ required: true, message: "此项必填" }], + component: { + disabled: compute(({ form }) => { + return form.disabled; + }), + id: "1", // 当同一个页面有多个editor时,需要配置不同的id + config: {}, + uploader: { + type: "form", + buildUrl(res) { + return res.url; + } + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/editor/index.vue b/packages/ui/certd-client/src/views/crud/component/editor/index.vue new file mode 100644 index 000000000..61e5e2033 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/editor/index.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/editor/mock.js b/packages/ui/certd-client/src/views/crud/component/editor/mock.js new file mode 100644 index 000000000..8351e0756 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/editor/mock.js @@ -0,0 +1,29 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "ComponentEditor", + idGenerator: 0 +}; +const list = [ + { + title: "d2-crud-plus好用吗?", + text: "非常好用", + content_quill: '

非常好用哦

', + change: "quill" + }, + { + title: "d2-crud-plus有什么优势?", + text: "简单,方便", + content_quill: "简单方便", + change: "wang" + }, + { + title: "1111111", + text: "22222", + content_quill: "3333", + change: "wang" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/icon/api.js b/packages/ui/certd-client/src/views/crud/component/icon/api.js new file mode 100644 index 000000000..4db9ed9ba --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/icon/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentIcon"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/icon/crud.jsx b/packages/ui/certd-client/src/views/crud/component/icon/crud.jsx new file mode 100644 index 000000000..ad9c38bed --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/icon/crud.jsx @@ -0,0 +1,75 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + icon: { + title: "icon", + search: { show: true }, + type: "text", + column: { + component: { + name: "fs-icon", + vModel: "icon", + style: "font-size:18px" + } + }, + form: { + helper: { + render() { + return ( + + 点击此处选择图标名称 + + ); + } + } + } + }, + svg: { + title: "svg", + search: { show: true }, + type: "text", + column: { + component: { + name: "fs-icon", + vModel: "icon", + style: "font-size:18px" + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/icon/index.vue b/packages/ui/certd-client/src/views/crud/component/icon/index.vue new file mode 100644 index 000000000..f8f1e9293 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/icon/index.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/icon/mock.js b/packages/ui/certd-client/src/views/crud/component/icon/mock.js new file mode 100644 index 000000000..fe2de8ed2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/icon/mock.js @@ -0,0 +1,22 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentIcon", + idGenerator: 0 +}; +const list = [ + { + icon: "ri:24-hours-fill", + svg:"svg:icon-compass" + }, + { + icon: "ion:add-circle-outline", + svg:"svg:icon-left-circle" + }, + { + icon: "ion:american-football-sharp", + svg:"svg:icon-Dollar" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/json/api.js b/packages/ui/certd-client/src/views/crud/component/json/api.js new file mode 100644 index 000000000..a62736e62 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/json/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentJson"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/json/crud.jsx b/packages/ui/certd-client/src/views/crud/component/json/crud.jsx new file mode 100644 index 000000000..898fb5d30 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/json/crud.jsx @@ -0,0 +1,78 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + wrapper: { + async onOpened({ mode, formRef }) { + if (!formRef.form.async) { + setTimeout(() => { + formRef.form.async = { aaa: "11", bb: "111" }; + }, 2000); + } + } + } + }, + columns: { + json: { + title: "json", + type: "json", + form: { + valueBuilder({ form }) { + if (form.json == null) { + return; + } + form.json = JSON.parse(form.json); + }, + valueResolve({ form }) { + if (form.json == null) { + return; + } + form.json = JSON.stringify(form.json); + } + } + }, + async: { + title: "异步加载", + type: "json", + form: { + // 上面form.wrapper.onOpened里面配置了异步加载 + helper: "在onOpened里面配置异步加载json字符串", + valueBuilder({ form }) { + if (form.async == null) { + return; + } + form.async = JSON.parse(form.async); + }, + valueResolve({ form }) { + if (form.async == null) { + return; + } + form.async = JSON.stringify(form.async); + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/json/index.vue b/packages/ui/certd-client/src/views/crud/component/json/index.vue new file mode 100644 index 000000000..34868881f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/json/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/json/mock.js b/packages/ui/certd-client/src/views/crud/component/json/mock.js new file mode 100644 index 000000000..727a17889 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/json/mock.js @@ -0,0 +1,18 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentJson", + idGenerator: 0 +}; +const list = [ + { + json: '{"a":1,"b":2}', + async: null + }, + { + json: '{"a":3,"b":4}', + async: null + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/number/api.js b/packages/ui/certd-client/src/views/crud/component/number/api.js new file mode 100644 index 000000000..30c88fc3f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/number/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentNumber"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/number/crud.jsx b/packages/ui/certd-client/src/views/crud/component/number/crud.jsx new file mode 100644 index 000000000..057f7b18a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/number/crud.jsx @@ -0,0 +1,71 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + integer: { + title: "整数", + search: { show: true }, + type: "number" + }, + float: { + title: "小数", + type: "number", + form: { + component: { + step: "0.1" + } + } + }, + format: { + title: "格式化", + type: "number", + form: { + component: { + formatter: (value) => `${value}%`, + parser: (value) => value.replace("%", "") + } + }, + column: { + formatter({ value }) { + return value + "%"; + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/number/index.vue b/packages/ui/certd-client/src/views/crud/component/number/index.vue new file mode 100644 index 000000000..f6989e027 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/number/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/number/mock.js b/packages/ui/certd-client/src/views/crud/component/number/mock.js new file mode 100644 index 000000000..6b8854a1c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/number/mock.js @@ -0,0 +1,25 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentNumber", + idGenerator: 0 +}; +const list = [ + { + integer: 1, + float: 1.1, + format: 100 + }, + { + integer: 2, + float: 1.2, + format: 100 + }, + { + integer: 3, + float: 1.3, + format: 100 + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/radio/api.js b/packages/ui/certd-client/src/views/crud/component/radio/api.js new file mode 100644 index 000000000..761dae47f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/radio/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentRadio"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/radio/crud.jsx b/packages/ui/certd-client/src/views/crud/component/radio/crud.jsx new file mode 100644 index 000000000..cf7320196 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/radio/crud.jsx @@ -0,0 +1,73 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + }, + button: { + title: "按钮样式", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }), + form: { + component: { + optionName: "a-radio-button" + } + } + }, + bool: { + title: "布尔类型", + search: { show: true }, + type: "dict-radio", + dict: dict({ + data: [ + { value: true, label: "TRUE" }, + { value: false, label: "FALSE" } + ] + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/radio/index.vue b/packages/ui/certd-client/src/views/crud/component/radio/index.vue new file mode 100644 index 000000000..d851e5852 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/radio/index.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/radio/mock.js b/packages/ui/certd-client/src/views/crud/component/radio/mock.js new file mode 100644 index 000000000..647514850 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/radio/mock.js @@ -0,0 +1,24 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentRadio", + idGenerator: 0 +}; +const list = [ + { + radio: "1", + button: "1", + bool: true + }, + { + radio: "2", + button: "2", + bool: false + }, + { + radio: "0", + button: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/select/api.js b/packages/ui/certd-client/src/views/crud/component/select/api.js new file mode 100644 index 000000000..6c9e2511d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/select/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentSelect"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/select/crud.jsx b/packages/ui/certd-client/src/views/crud/component/select/crud.jsx new file mode 100644 index 000000000..a04fafa05 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/select/crud.jsx @@ -0,0 +1,311 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +import _ from "lodash-es"; +function useSearchRemote() { + let lastFetchId = 0; + + const state = { + data: ref([]), + fetching: ref(false) + }; + const fetchUser = _.debounce((value) => { + console.log("fetching user", value); + lastFetchId += 1; + + const fetchId = lastFetchId; + + state.data.value = []; + + state.fetching.value = true; + + fetch("https://randomuser.me/api/?results=5") + .then((response) => response.json()) + .then((body) => { + if (fetchId !== lastFetchId) { + // for fetch callback order + return; + } + const data = body.results.map((user) => ({ + text: `${user.name.first} ${user.name.last}`, + value: user.login.username + })); + state.data.value = data; + state.fetching.value = false; + }); + }, 800); + + return { + fetchUser, + searchState: state + }; +} +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + const dictRef = dict({ + value: "id", + label: "text", + data: [ + { id: "sz", text: "深圳", color: "success" }, + { id: "gz", text: "广州", color: "blue" }, + { id: "bj", text: "北京" }, + { id: "wh", text: "武汉" }, + { id: "sh", text: "上海" } + ] + }); + + function dynamicUpdateDictOptions() { + dictRef.data.push({ id: "xg", text: "香港" }); + //dictRef.toMap(); + } + + const { fetchUser, searchState } = useSearchRemote(); + return { + dynamicUpdateDictOptions, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + // 单列布局 + col: { span: 24 }, + labelCol: { span: 4 }, + wrapperCol: { span: 18 } + }, + rowHandle: { + fixed: "right", + align: "center" + }, + table: { + scroll: { + //启用横向滚动条,设置一个大于所有列宽之和的值,一般大于表格宽度 + x: 1400 + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + statusLocal: { + title: "单选本地", + type: "dict-select", + dict: dictRef + }, + statusRemote: { + title: "单选远程", + search: { + show: true, + rules: null, + component: { + style: { width: "100px" } + } + }, + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?simple" + }), + form: { + rules: [{ required: true, message: "请选择一个选项" }] + } + }, + filter: { + title: "本地过滤", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?simple" + }), + form: { + component: { + showSearch: true, + //默认的filterOption仅支持value的过滤,label并不会加入查询 + //所以需要自定义filterOption + filterOption(inputValue, option) { + return option.label.indexOf(inputValue) >= 0 || option.value.indexOf(inputValue) >= 0; + } + } + } + }, + search: { + title: "远程搜索", + search: { show: true, component: { style: { width: "240px" } } }, + form: { + component: { + name: "a-select", + vModel: "value", + filterOption: false, + //labelInValue: true, + mode: "multiple", + showSearch: true, + allowClear: true, + placeholder: "输入远程搜索,数据仅供演示", + options: searchState.data, + onSearch(value) { + fetchUser(value); + }, + children: { + notFoundContent() { + if (searchState.fetching.value) { + return ; + } + return "暂无记录"; + } + } + } + } + }, + customDictGetData: { + title: "自定义字典请求", + search: { show: false }, + type: "dict-select", + dict: dict({ + getData({ dict }) { + // 覆盖全局获取字典请求配置 + console.log(`我是从自定义的getData方法中加载的数据字典`, dict); + return requestForMock({ + url: "/mock/dicts/OpenStatusEnum?cache", + method: "get" + }); + } + }), + form: { + value: "2", //默认值, 注意search也会影响到,需要将search.value=null,取消search的默认值 + helper: "dict.getData可以覆盖全局配置的getRemoteDictFunc" + }, + column: { + width: 120, + component: { + type: "text" // 不使用tag,纯文本展示 + } + } + }, + disabledOptions: { + title: "禁用某个选项", + key: "disabledOptions", + type: "dict-select", + dict: dict({ + cloneable: true, + url: "/mock/dicts/OpenStatusEnum?disabledOptions" + }), + form: { + component: { + dict: { + // 此处dict配置会覆盖上面dict的属性 + prototype: true, // form表单的dict设置为原型复制,每次初始化时都会重新loadDict + onReady({ dict }) { + console.log("字典请求ready", dict); + dict.data[0].disabled = true; // 禁用某个选项, 还可以自己修改选项 + } + } + }, + helper: "禁用字典选项" + }, + column: { + width: 150 + } + }, + firstDefault: { + title: "默认值", + type: "dict-select", + dict: dict({ + cloneable: true, + url: "/mock/dicts/OpenStatusEnum?disabledOptions" + }), + form: { + component: { + //监听 dict-change事件 + onDictChange({ dict, form, key }) { + console.log("dict data changed", dict, key); + if (dict.data != null && form.firstDefault == null) { + form.firstDefault = dict.data[0].value; + } + } + // 下面的方法也可以,注意要配置dict.prototype:true + // dict: { + // // 此处dict配置会覆盖上面dict的属性 + // // form表单的dict设置为原型复制,每次初始化时都会重新loadDict + // prototype: true, + // + // onReady({ dict, form }) { + // console.log("字典请求ready", dict, form, getComponentRef); + // // prototype= true 才能获取到form表单数据 + // form.firstDefault = dict.data[0].value; + // } + // } + }, + helper: "默认选择第一个选项" + } + }, + multiple: { + title: "多选自动染色", + sortable: true, + type: "dict-select", + form: { + title: "多选本地", + component: { + mode: "multiple" + } + }, + dict: dict({ + data: [ + { value: "sz", label: "深圳", color: "success" }, + { value: "gz", label: "广州" }, + { value: "wh", label: "武汉" }, + { value: "sh", label: "上海" }, + { value: "hz", label: "杭州" }, + { value: "bj", label: "北京", color: "red" } + ] + }), + column: { + width: 290, + component: { + color: "auto", // 自动染色 + defaultLabel: "未知城市" //无数据字典时的默认文本 + } + } + }, + statusSimple: { + title: "普通选择", + form: { + component: { + name: "a-select", + vModel: "value", + options: [ + { value: "sz", label: "深圳", color: "success" }, + { value: "gz", label: "广州", color: "blue" }, + { value: "bj", label: "北京" }, + { value: "wh", label: "武汉" }, + { value: "sh", label: "上海" } + ] + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/select/index.vue b/packages/ui/certd-client/src/views/crud/component/select/index.vue new file mode 100644 index 000000000..8278b4564 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/select/index.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/select/mock.js b/packages/ui/certd-client/src/views/crud/component/select/mock.js new file mode 100644 index 000000000..d74488775 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/select/mock.js @@ -0,0 +1,42 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentSelect", + idGenerator: 0 +}; +const list = [ + { + statusLocal: "sz", + customDictUrl: "0", + statusValue: 1, + multiple: ["sz", "bj", "gz", "sh", "hz", "xz", "xg"], + checkbox: "0", + select_local: "sz", + statusRemote: "0", + status_custom_2: "0", + customDictGetData: "1", + checkbox_btn: "1" + }, + { + statusLocal: "xg", + customDictUrl: "1", + statusValue: 2, + statusRemote: "1", + status_custom_2: "2", + select_local: "gz", + multiple: ["sh", "sz"], + checkbox: "0" + }, + { + statusLocal: "gz", + customDictUrl: "1", + statusValue: 1, + disabledCache: "1", + disabledOptions: "2", + select_local: "gz", + multiple: ["sh", "gz"], + checkbox: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/switch/api.js b/packages/ui/certd-client/src/views/crud/component/switch/api.js new file mode 100644 index 000000000..f2e25bed5 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/switch/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentSwitch"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/switch/crud.jsx b/packages/ui/certd-client/src/views/crud/component/switch/crud.jsx new file mode 100644 index 000000000..e2d751a75 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/switch/crud.jsx @@ -0,0 +1,141 @@ +import * as api from "./api"; +import { dict, compute } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + switch: { + title: "状态", + search: { show: true }, + type: "dict-switch", + dict: dict({ + data: [ + { value: false, label: "开启" }, + { value: true, label: "关闭" } + ] + }) + }, + notBool: { + title: "自定义value", + search: { show: true }, + type: "dict-switch", + dict: dict({ + data: [ + { value: "1", label: "开启" }, + { value: "2", label: "关闭" } + ] + }) + }, + switchLabel: { + title: "切换字段label", + search: { show: true }, + type: "dict-switch", + dict: dict({ + data: [ + { value: true, label: "开启" }, + { value: false, label: "关闭" } + ] + }), + column: { + show: false + } + }, + labelTarget: { + title: "我将被切换", + type: "text", + column: { + show: false + }, + form: { + label: compute(({ form }) => { + return form.switchLabel ? "我将被切换" : "再切换一下"; + }) + } + }, + cellSwitch: { + title: "cell显示", + search: { show: true }, + type: "dict-switch", + form: { + component: {} + }, + column: { + component: { + name: "fs-dict-switch", + vModel: "checked", + onChange: (value) => { + console.log("onChange", value); + } + // onChange: compute((context) => { + // //动态onChange方法测试 + // return () => { + // console.log("onChange", context.row.cellSwitch); + // }; + // }) + } + }, + dict: dict({ + data: [ + { value: true, label: "开启" }, + { value: false, label: "关闭" } + ] + }) + }, + showTarget: { + title: "显隐目标", + type: "text", + column: { + component: { + name: "fs-values-format", + show: compute((context) => { + //根据cellSwitch字段显隐 + return context.row.cellSwitch === true; + }) + } + }, + search: { + show: false + }, + form: { + show: compute((context) => { + console.log("context", context); + //根据cellSwitch字段显隐 + return context.form.cellSwitch === true; + }) + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/switch/index.vue b/packages/ui/certd-client/src/views/crud/component/switch/index.vue new file mode 100644 index 000000000..beb30f7df --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/switch/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/switch/mock.js b/packages/ui/certd-client/src/views/crud/component/switch/mock.js new file mode 100644 index 000000000..42772b1f7 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/switch/mock.js @@ -0,0 +1,27 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentSwitch", + idGenerator: 0 +}; +const list = [ + { + switch: true, + cellSwitch: true, + notBool: "1", + showTarget: "点左边开关显示或隐藏" + }, + { + switch: false, + cellSwitch: true, + notBool: "2", + showTarget: "点左边开关显示或隐藏" + }, + { + switch: true, + cellSwitch: false, + showTarget: "点左边开关显示或隐藏" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/text/api.js b/packages/ui/certd-client/src/views/crud/component/text/api.js new file mode 100644 index 000000000..2111f7724 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/text/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentText"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/text/crud.jsx b/packages/ui/certd-client/src/views/crud/component/text/crud.jsx new file mode 100644 index 000000000..24850e9e5 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/text/crud.jsx @@ -0,0 +1,130 @@ +import * as api from "./api"; +import { compute } from "@fast-crud/fast-crud"; +import { resolveDirective, withDirectives } from "vue"; + +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + name: { + title: "姓名", + type: "text", //虽然不写也能正确显示组件,但不建议省略它 + search: { show: true }, + form: { + component: { + maxlength: 20 + } + } + }, + search: { + title: "搜索", + type: "text", + form: { + component: { + addonAfter: "后置", + suffix: "suffix", + children: { + addonBefore() { + return ; + } + } + } + } + }, + password: { + title: "密码", + type: "password", + column: { + //一般密码不显示在列里面 + show: false + } + }, + // copy: { + // title: "剪贴板", + // type: "text", + // column: { + // cellRender({ value, row }) { + // const content = ( + //
+ // {value} + // 复制 + //
+ // ); + // const clipboard = resolveDirective("clipboard"); + // return withDirectives(content, [[clipboard, value]]); + // } + // } + // }, + copy: { + title: "剪贴板", + type: ["text", "copyable"] + }, + intro: { + title: "简介", + type: "textarea", + form: { + component: { showWordLimit: true, maxlength: 200 } + }, + column: { + ellipsis: true, + showTitle: true + } + }, + render: { + title: "复杂输入(render)", + form: { + title: "复杂输入", + component: { + render(context) { + console.log("context scope", context); + return ( + + + + + ); + } + } + } + }, + render2: { + title: "我的值是由复杂输入列输入的", + column: { + width: "300px" + }, + form: { + show: false + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/text/index.vue b/packages/ui/certd-client/src/views/crud/component/text/index.vue new file mode 100644 index 000000000..a97ef5519 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/text/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/text/mock.js b/packages/ui/certd-client/src/views/crud/component/text/mock.js new file mode 100644 index 000000000..4bc9df77c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/text/mock.js @@ -0,0 +1,43 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentText", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + date: "2016-05-02", + status: "0", + province: "1", + avatar: "https://alicdn.antdv.com/vue.png", + show: true, + city: "sz", + address: "123123", + zip: "518000", + intro: "王小虎是element-plus的table示例出现的名字", + copy: "测试文本" + }, + { + name: "张三", + date: "2016-05-04", + status: "1", + province: "2", + copy: "测试文本" + }, + { + name: "李四", + date: 2232433534511, + status: "1", + province: "0", + copy: "测试文本" + }, + { + name: "王五", + date: "2016-05-03", + status: "2", + province: "wh,gz" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/tree/api.js b/packages/ui/certd-client/src/views/crud/component/tree/api.js new file mode 100644 index 000000000..b9db03f12 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/tree/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentTree"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/tree/crud.jsx b/packages/ui/certd-client/src/views/crud/component/tree/crud.jsx new file mode 100644 index 000000000..1d3ce5abf --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/tree/crud.jsx @@ -0,0 +1,81 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + tree: { + title: "树形选择", + search: { show: false }, + type: "dict-tree", + dict: dict({ + isTree: true, + url: "/mock/dicts/cascaderData?single" + }) + }, + multiple: { + title: "多选", + search: { show: false }, + type: "dict-tree", + dict: dict({ + cloneable: false, + isTree: true, + url: "/mock/dicts/cascaderData?single" + }), + form: { + component: { + "tree-checkable": true + } + } + }, + fieldReplace: { + title: "修改options的value字段名", + search: { show: false }, + type: "dict-tree", + dict: dict({ + isTree: true, + url: "/mock/dicts/littlePca", + value: "code", + label: "name" + }), + form: { + component: { + fieldNames: { label: "name", key: "code", value: "code" } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/tree/index.vue b/packages/ui/certd-client/src/views/crud/component/tree/index.vue new file mode 100644 index 000000000..b5c740739 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/tree/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/tree/mock.js b/packages/ui/certd-client/src/views/crud/component/tree/mock.js new file mode 100644 index 000000000..ed3a1d394 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/tree/mock.js @@ -0,0 +1,16 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentTree", + idGenerator: 0 +}; +const list = [ + { + tree: "zhinan", + multiple: ["zhinan", "yizhi"] + }, + { tree: "zhinan" }, + { tree: "zhinan" } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/alioss/api.js b/packages/ui/certd-client/src/views/crud/component/uploader/alioss/api.js new file mode 100644 index 000000000..c5871d5bf --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/alioss/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/AliossUploader"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/alioss/crud.jsx b/packages/ui/certd-client/src/views/crud/component/uploader/alioss/crud.jsx new file mode 100644 index 000000000..c5dd0c412 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/alioss/crud.jsx @@ -0,0 +1,75 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + file: { + title: "阿里云上传", + type: "file-uploader", + form: { + component: { + uploader: { + type: "alioss" + } + } + } + }, + pictureCard: { + title: "照片墙", + type: "image-uploader", + form: { + component: { + uploader: { + type: "alioss" + } + } + } + }, + cropper: { + title: "裁剪", + type: "cropper-uploader", + form: { + component: { + uploader: { + type: "alioss" + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/alioss/index.vue b/packages/ui/certd-client/src/views/crud/component/uploader/alioss/index.vue new file mode 100644 index 000000000..0a305710c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/alioss/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/alioss/mock.js b/packages/ui/certd-client/src/views/crud/component/uploader/alioss/mock.js new file mode 100644 index 000000000..c3de48577 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/alioss/mock.js @@ -0,0 +1,24 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "AliossUploader", + idGenerator: 0 +}; +const list = [ + { + avatar_error1: "http://greper.handsfree.work/extends/avatar1.jpg", + avatar_error2: "http://greper.handsfree.work/extends/avatar1.jpg", + avatar: "http://greper.handsfree.work/extends/avatar.jpg", + file: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + image: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + image2: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"] + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/cos/api.js b/packages/ui/certd-client/src/views/crud/component/uploader/cos/api.js new file mode 100644 index 000000000..536456ea9 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/cos/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/CosUploader"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/cos/crud.jsx b/packages/ui/certd-client/src/views/crud/component/uploader/cos/crud.jsx new file mode 100644 index 000000000..05e54e162 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/cos/crud.jsx @@ -0,0 +1,75 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + file: { + title: "腾讯云", + type: "file-uploader", + form: { + component: { + uploader: { + type: "cos" + } + } + } + }, + pictureCard: { + title: "照片墙", + type: "image-uploader", + form: { + component: { + uploader: { + type: "cos" + } + } + } + }, + cropper: { + title: "裁剪", + type: "cropper-uploader", + form: { + component: { + uploader: { + type: "cos" + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/cos/index.vue b/packages/ui/certd-client/src/views/crud/component/uploader/cos/index.vue new file mode 100644 index 000000000..8b4bc1ef5 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/cos/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/cos/mock.js b/packages/ui/certd-client/src/views/crud/component/uploader/cos/mock.js new file mode 100644 index 000000000..5c608212f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/cos/mock.js @@ -0,0 +1,22 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "CosUploader", + idGenerator: 0 +}; +const list = [ + { + avatar: "http://greper.handsfree.work/extends/avatar.jpg", + file: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + image: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + image2: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"] + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/cropper/api.js b/packages/ui/certd-client/src/views/crud/component/uploader/cropper/api.js new file mode 100644 index 000000000..5113d49c4 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/cropper/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/CropperUploader"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/cropper/crud.jsx b/packages/ui/certd-client/src/views/crud/component/uploader/cropper/crud.jsx new file mode 100644 index 000000000..4d8e168c9 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/cropper/crud.jsx @@ -0,0 +1,110 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + cropper: { + title: "头像裁剪上传", + type: "cropper-uploader" + }, + avatar: { + title: "数量限制", + type: "cropper-uploader", + form: { + component: { + limit: 5 //默认限制1个,即头像上传,0为不限制 + } + } + }, + aspect: { + title: "按比例裁剪", + type: "cropper-uploader", + form: { + component: { + cropper: { + aspectRatio: 2 + } + } + } + }, + alioss: { + title: "alioss", + type: "cropper-uploader", + form: { + component: { + uploader: { + type: "alioss" + } + } + } + }, + qiniu: { + title: "七牛", + type: "cropper-uploader", + form: { + component: { + uploader: { + type: "qiniu" + } + } + } + }, + cos: { + title: "腾讯cos", + type: "cropper-uploader", + form: { + component: { + uploader: { + type: "cos" + } + } + } + }, + form: { + title: "表单", + type: "cropper-uploader", + form: { + component: { + uploader: { + type: "form" + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/cropper/index.vue b/packages/ui/certd-client/src/views/crud/component/uploader/cropper/index.vue new file mode 100644 index 000000000..3e6a98bf8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/cropper/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/cropper/mock.js b/packages/ui/certd-client/src/views/crud/component/uploader/cropper/mock.js new file mode 100644 index 000000000..83108782a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/cropper/mock.js @@ -0,0 +1,24 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "CropperUploader", + idGenerator: 0 +}; +const list = [ + { + avatar_error1: "http://greper.handsfree.work/extends/avatar1.jpg", + avatar_error2: "http://greper.handsfree.work/extends/avatar1.jpg", + avatar: "http://greper.handsfree.work/extends/avatar.jpg", + file: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + image: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + image2: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"] + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/form/api.js b/packages/ui/certd-client/src/views/crud/component/uploader/form/api.js new file mode 100644 index 000000000..af8d42b4a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/form/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentUploader"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/form/crud.jsx b/packages/ui/certd-client/src/views/crud/component/uploader/form/crud.jsx new file mode 100644 index 000000000..7a9cb0646 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/form/crud.jsx @@ -0,0 +1,223 @@ +import * as api from "./api"; +import { AllUploadSuccessValidator } from "@fast-crud/fast-extends"; +import { dict } from "@fast-crud/fast-crud"; +import { nextTick } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + wrapper: { + async onOpened() { + // 异步组件实例的获取 + const componentRef = await expose.getFormComponentRef("file", true); + console.log("componentRef", componentRef); + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + file: { + title: "表单上传", + type: "file-uploader", + form: { + component: { + multiple: true, //可选择多个 + uploader: { + type: "form" + } + } + }, + column: { + component: { + // 如果你后台返回的值不是一个完整的url,那么展示时就无法显示和点击 + // 需要你本地根据value构建文件的url。 + // 支持异步 + async buildUrl(value) { + return value; + } + } + } + }, + pictureCard: { + title: "照片墙", + type: "image-uploader", + form: { + component: { + limit: 1, + uploader: { + type: "form" + } + }, + helper: "最大可上传1个文件" + }, + column: { + component: { + buildPreviewUrl({ url, index }) { + if (index === 0) { + return "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"; + } else { + return url + "?preview=600x600"; + } + } + } + } + }, + pictureCard2: { + title: "通过urls显示", + type: "image-uploader", + column: { + component: { + urls: [ + { + url: "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?1", + previewUrl: "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?preview1" + }, + { + url: "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?2", + previewUrl: "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?preview2" + } + ] + } + } + }, + avatar: { + title: "头像上传", + type: "avatar-uploader", + form: { + component: { + uploader: { + type: "form" + } + }, + helper: "就是照片墙limit=1的效果" + } + }, + cropper: { + title: "裁剪", + type: "cropper-uploader", + form: { + component: { + uploader: { + type: "form" + } + } + } + }, + keyValueType: { + title: "valueType为key", + type: "file-uploader", + form: { + component: { + uploader: { + type: "form" + }, + valueType: "key", + async buildUrl(value) { + return new Promise((resolve) => { + const url = "http://www.docmirror.cn:7070/api/upload/form/download?key=" + value; + resolve(url); + }); + } + } + }, + column: { + component: { + async buildUrl(value) { + return new Promise((resolve) => { + const url = "http://www.docmirror.cn:7070/api/upload/form/download?key=" + value; + resolve(url); + }); + } + } + } + }, + limit: { + title: "限制数量", + type: "file-uploader", + form: { + component: { + limit: 2, + uploader: { + type: "form" + } + }, + helper: "最大可上传2个文件" + } + }, + sizeLimit: { + title: "限制大小", + type: "file-uploader", + form: { + component: { + sizeLimit: 1024, + uploader: { + type: "form" + } + }, + helper: "大小不能超过1k" + } + }, + accept: { + title: "限制类型", + type: "file-uploader", + form: { + component: { + accept: "*.jpg,*.png" + }, + helper: "只能上传jpg或者png" + } + }, + validation: { + title: "校验", + type: "file-uploader", + form: { + rules: [ + { required: true, message: "此项必传" }, + { + validator: AllUploadSuccessValidator(), //如果要自定义校验规则则需要手动配置这个 + message: "还有文件正在上传,请稍候" + } + ], + helper: "大小不能超过50M,文件未上传完成之前,阻止提交", + component: { + uploader: { + type: "form", + sizeLimit: 1024 * 1024 * 50 + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/form/index.vue b/packages/ui/certd-client/src/views/crud/component/uploader/form/index.vue new file mode 100644 index 000000000..eaae1cac8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/form/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/form/mock.js b/packages/ui/certd-client/src/views/crud/component/uploader/form/mock.js new file mode 100644 index 000000000..e4fda8db3 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/form/mock.js @@ -0,0 +1,24 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentUploader", + idGenerator: 0 +}; +const list = [ + { + avatar: "http://greper.handsfree.work/extends/avatar.jpg", + file: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + pictureCard: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + limit: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + statusRemote: "0", + keyValueType: "/2022-12-20/qygzqdjd1g.yaml" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/api.js b/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/api.js new file mode 100644 index 000000000..2d79f7b54 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/QiniuUploader"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/crud.jsx b/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/crud.jsx new file mode 100644 index 000000000..89c787c3e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/crud.jsx @@ -0,0 +1,73 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + file: { + title: "七牛云上传", + type: "file-uploader", + form: { + component: { + uploader: { + type: "qiniu" + } + } + } + }, + pictureCard: { + title: "照片墙", + type: "image-uploader", + form: { + component: { + uploader: { + type: "qiniu" + } + } + } + }, + cropper: { + title: "裁剪", + type: "cropper-uploader", + form: { + component: { + uploader: { + type: "qiniu" + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/index.vue b/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/index.vue new file mode 100644 index 000000000..0a305710c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/mock.js b/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/mock.js new file mode 100644 index 000000000..45c796850 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/component/uploader/qiniu/mock.js @@ -0,0 +1,22 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "QiniuUploader", + idGenerator: 0 +}; +const list = [ + { + avatar: "http://greper.handsfree.work/extends/avatar.jpg", + file: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + image: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"], + image2: ["http://greper.handsfree.work/extends/avatar.jpg", "https://www.baidu.com/img/bd_logo1.png"] + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/debug/select/api.js b/packages/ui/certd-client/src/views/crud/debug/select/api.js new file mode 100644 index 000000000..06a6bd3e5 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/debug/select/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/DebugSelect"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/debug/select/crud.jsx b/packages/ui/certd-client/src/views/crud/debug/select/crud.jsx new file mode 100644 index 000000000..d3bee6123 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/debug/select/crud.jsx @@ -0,0 +1,106 @@ +import * as api from "./api"; +import { requestForMock } from "/src/api/service"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + // 单列布局 + col: { span: 24 }, + labelCol: { span: 4 }, + wrapperCol: { span: 18 } + }, + rowHandle: { + fixed: "right" + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + + statusRemote: { + title: "单选远程", + search: { + show: true, + value: [] + }, + type: "dict-select", + dict: dict({ + url: "/mock/dicts/_OpenStatusEnum2?simple", + value: "id", + label: "text" + }), + form: { + component: { mode: "multiple" }, + rules: [{ required: true, message: "请选择一个选项" }] + }, + column: { + width: 200 + } + }, + id2: { + title: "ID", + key: "id", + type: "number", + column: { + width: 300 + }, + form: { + show: false + } + }, + id3: { + title: "ID", + key: "id", + type: "number", + column: { + width: 300 + }, + form: { + show: false + } + }, + id4: { + title: "ID", + key: "id", + type: "number", + column: { + width: 300 + }, + form: { + show: false + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/debug/select/index.vue b/packages/ui/certd-client/src/views/crud/debug/select/index.vue new file mode 100644 index 000000000..e7105c522 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/debug/select/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/debug/select/mock.js b/packages/ui/certd-client/src/views/crud/debug/select/mock.js new file mode 100644 index 000000000..2cb1e1750 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/debug/select/mock.js @@ -0,0 +1,9 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "DebugSelect", + idGenerator: 0 +}; +const list = []; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/dict/cloneable/api.js b/packages/ui/certd-client/src/views/crud/dict/cloneable/api.js new file mode 100644 index 000000000..a6c63db5f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/cloneable/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/DictCloneable"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/dict/cloneable/crud.jsx b/packages/ui/certd-client/src/views/crud/dict/cloneable/crud.jsx new file mode 100644 index 000000000..4ad17c789 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/cloneable/crud.jsx @@ -0,0 +1,88 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + const remoteDict = dict({ + cloneable: true, + url: "/mock/dicts/OpenStatusEnum" + }); + + return { + crudOptions: { + remoteDict, + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + remote: { + title: "远程字典", + search: { show: true }, + dict: remoteDict, + type: "dict-select", + form: { + component: { dict: { cache: false } } + } + }, + modifyDict: { + title: "动态修改字典", + search: { show: false }, + type: "text", + column: { + component: { + name: "a-switch", + vModel: "checked" + }, + valueChange({ row, getComponentRef }) { + // 这里不能使用remoteDict,因为在分发时已经clone到form配置中了 + // 这里dict修改不会影响列里面的数据 + const targetDict = getComponentRef("remote").dict; + targetDict.url = row.modifyDict ? "/mock/dicts/moreOpenStatusEnum?remote" : "/mock/dicts/OpenStatusEnum?remote"; + targetDict.reloadDict(); + } + }, + form: { + component: { + name: "a-switch", + vModel: "checked" + }, + valueChange({ form, getComponentRef }) { + // 这里不能使用remoteDict,因为在分发时已经clone到form配置中了 + // 这里dict修改不会影响列里面的数据 + const targetDict = getComponentRef("remote").dict; + targetDict.url = form.modifyDict ? "/mock/dicts/moreOpenStatusEnum?remote" : "/mock/dicts/OpenStatusEnum?remote"; + targetDict.reloadDict(); + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/dict/cloneable/index.vue b/packages/ui/certd-client/src/views/crud/dict/cloneable/index.vue new file mode 100644 index 000000000..4850b6200 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/cloneable/index.vue @@ -0,0 +1,48 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/dict/cloneable/mock.js b/packages/ui/certd-client/src/views/crud/dict/cloneable/mock.js new file mode 100644 index 000000000..acb0f31b8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/cloneable/mock.js @@ -0,0 +1,21 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "DictCloneable", + idGenerator: 0 +}; +const list = [ + { + status: "1", + remote: "2" + }, + { + status: "2", + remote: "0" + }, + { + status: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/dict/prototype/api.js b/packages/ui/certd-client/src/views/crud/dict/prototype/api.js new file mode 100644 index 000000000..08ab81955 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/prototype/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/DictPrototype"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/dict/prototype/crud.jsx b/packages/ui/certd-client/src/views/crud/dict/prototype/crud.jsx new file mode 100644 index 000000000..c40a2b070 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/prototype/crud.jsx @@ -0,0 +1,128 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { requestForMock } from "../../../../api/service"; + +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + const remoteDict = dict({ + prototype: true, //这个dict只是一个原型,引用它的dict组件初始化时都会把此dict对象clone一份 + url: "/mock/dicts/OpenStatusEnum" + }); + + const dynamicUrlDict = dict({ + cache: true, + prototype: true, //这个dict只是一个原型,引用它的dict组件初始化时都会把此dict对象clone一份 + url({ row }) { + return row.switch ? "/mock/dicts/moreOpenStatusEnum" : "/mock/dicts/OpenStatusEnum"; + } + }); + const dynamicDict = dict({ + cache: true, + prototype: true, //这个dict只是一个原型,引用它的dict组件初始化时都会把此dict对象clone一份 + url({ row }) { + return row.switch ? "/mock/dicts/moreOpenStatusEnum" : "/mock/dicts/OpenStatusEnum"; + }, + async getData({ url }) { + return await requestForMock({ url }); + } + }); + + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + remote: { + title: "远程字典", + search: { show: true }, + dict: remoteDict, + type: "dict-select" + }, + modifyDict: { + title: "动态修改字典", + search: { show: true }, + type: "text", + form: { + helper: "此处可以动态切换左边select的options", + component: { + name: "a-switch", + vModel: "checked" + }, + valueChange({ form, value, getComponentRef }) { + console.log("form", value); + const targetDict = getComponentRef("remote").getDict(); + targetDict.url = form.modifyDict + ? "/mock/dicts/moreOpenStatusEnum?remote" + : "/mock/dicts/OpenStatusEnum?remote"; + targetDict.reloadDict(); + } + }, + column: { + component: { + name: "a-switch", + vModel: "checked" + }, + valueChange({ value, getComponentRef }) { + console.log("value", value); + const targetDict = getComponentRef("remote").getDict(); + targetDict.url = value ? "/mock/dicts/moreOpenStatusEnum?remote" : "/mock/dicts/OpenStatusEnum?remote"; + targetDict.reloadDict(); + } + } + }, + switch: { + title: "switch", + type: "dict-switch", + dict: dict({ + data: [ + { value: true, label: "开启" }, + { value: false, label: "关闭" } + ] + }), + form: { + helper: "动态getData和动态Url根据此字段的值获取不同的dictData,此处无法动态切换,仅在打开对话框时生效" + } + }, + dynamicGetData: { + title: "动态getData", + dict: dynamicDict, + type: "dict-select" + }, + dynamicUrl: { + title: "动态Url", + dict: dynamicUrlDict, + type: "dict-select" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/dict/prototype/index.vue b/packages/ui/certd-client/src/views/crud/dict/prototype/index.vue new file mode 100644 index 000000000..1adcdfaa0 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/prototype/index.vue @@ -0,0 +1,48 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/dict/prototype/mock.js b/packages/ui/certd-client/src/views/crud/dict/prototype/mock.js new file mode 100644 index 000000000..adc12b8a0 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/prototype/mock.js @@ -0,0 +1,31 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "DictPrototype", + idGenerator: 0 +}; +const list = [ + { + status: "1", + remote: "2", + modifyDict: false, + switch: true, + dynamicGetData: "1", + dynamicUrl: "2" + }, + { + status: "2", + remote: "0", + modifyDict: false, + dynamicGetData: "1", + dynamicUrl: "2" + }, + { + status: "0", + modifyDict: false, + dynamicGetData: "1", + dynamicUrl: "2" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/dict/single/api.js b/packages/ui/certd-client/src/views/crud/dict/single/api.js new file mode 100644 index 000000000..971936543 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/single/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/DictSingle"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/dict/single/crud.jsx b/packages/ui/certd-client/src/views/crud/dict/single/crud.jsx new file mode 100644 index 000000000..aa161d64b --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/single/crud.jsx @@ -0,0 +1,111 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + const statusDict = dict({ + cloneable: false, // 关闭cloneable,任何情况下,都使用同一个dict + data: [ + { value: "1", label: "开启", color: "success" }, + { value: "2", label: "停止", color: "blue" }, + { value: "0", label: "关闭", color: "blue" } + ] + }); + + const remoteDict = dict({ + cloneable: false, // 关闭cloneable,任何情况下,都使用同一个dict + url: "/mock/dicts/OpenStatusEnum", + immediate: false + }); + // remoteDict.loadDict(); + + return { + remoteDict, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + status: { + title: "本地字典", + search: { show: false }, + dict: statusDict, + type: "dict-select" + }, + remote: { + title: "远程字典", + search: { show: true }, + dict: remoteDict, + type: "dict-select", + column: { + component: { + onDictChange(opts) { + console.log("字典变化:", opts); + } + } + } + }, + modifyDict: { + title: "动态修改字典", + search: { show: false }, + type: "text", + form: { + component: { + name: "a-switch", + vModel: "checked" + }, + valueChange({ form }) { + console.log("changed", form.modifyDict); + remoteDict.url = form.modifyDict + ? "/mock/dicts/moreOpenStatusEnum?remote" + : "/mock/dicts/OpenStatusEnum?remote"; + // 由于remoteDict.cloneable =false,所以全局公用一个实例,修改会影响全部地方 + remoteDict.reloadDict(); + } + }, + column: { + component: { + name: "a-switch", + vModel: "checked", + on: { + // 注意:必须要on前缀 + onChange({ $event }) { + remoteDict.url = $event + ? "/mock/dicts/moreOpenStatusEnum?remote" + : "/mock/dicts/OpenStatusEnum?remote"; + remoteDict.reloadDict(); + } + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/dict/single/index.vue b/packages/ui/certd-client/src/views/crud/dict/single/index.vue new file mode 100644 index 000000000..a0f6a2d01 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/single/index.vue @@ -0,0 +1,49 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/dict/single/mock.js b/packages/ui/certd-client/src/views/crud/dict/single/mock.js new file mode 100644 index 000000000..cad375c93 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/dict/single/mock.js @@ -0,0 +1,21 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "DictSingle", + idGenerator: 0 +}; +const list = [ + { + status: "1", + remote: "2" + }, + { + status: "2", + remote: "0" + }, + { + status: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/column-resize/api.js b/packages/ui/certd-client/src/views/crud/feature/column-resize/api.js new file mode 100644 index 000000000..279509cc9 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/column-resize/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureSearch"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/column-resize/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/column-resize/crud.jsx new file mode 100644 index 000000000..913909dbd --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/column-resize/crud.jsx @@ -0,0 +1,70 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + const crudBinding = expose.crudBinding; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + onResizeColumn: (w, col) => { + //触发resize事件后,修改column宽度,width只能配置为number类型 + //可以将此方法写在app.use()中的commonOptions里面 + crudBinding.value.table.columnsMap[col.key].width = w; + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50, //宽度必须number类型 + resizable: true //配置true,表示可以调整宽度 + }, + form: { + show: false + } + }, + text: { + title: "说明", + type: "text", + column: { + ellipsis: true, + showTitle: true, + resizable: true, //配置true,表示可以调整宽度 + width: 400 //宽度必须number类型 + } + }, + //必须留一个自动宽度 + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/column-resize/index.vue b/packages/ui/certd-client/src/views/crud/feature/column-resize/index.vue new file mode 100644 index 000000000..8565e9e60 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/column-resize/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/column-resize/mock.js b/packages/ui/certd-client/src/views/crud/feature/column-resize/mock.js new file mode 100644 index 000000000..68cfb67ad --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/column-resize/mock.js @@ -0,0 +1,20 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureSearch", + idGenerator: 0 +}; +const list = [ + { + text: "这一列可以调整宽度,另外必须留一列自动宽度", + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/column-sort/api.js b/packages/ui/certd-client/src/views/crud/feature/column-sort/api.js new file mode 100644 index 000000000..37c27d576 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/column-sort/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureColumnSort"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/column-sort/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/column-sort/crud.jsx new file mode 100644 index 000000000..fbadd388f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/column-sort/crud.jsx @@ -0,0 +1,94 @@ +import * as api from "./api"; +export default function ({ expose }) { + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest: api.GetList, + addRequest, + editRequest, + delRequest + }, + toolbar: { + //工具按钮排序 + buttons: { + search: { order: 1 } // 查询按钮排到前面 + } + }, + form: { + //表单跨列 + col: { span: 24 }, + labelCol: { span: 6 } + }, + columns: { + col1: { + title: "col.1", + search: { show: true }, + type: "text" + }, + col2: { + title: "col.2,我排最后一个", + search: { + //控制查询字段顺序 + show: true, + //字段默认order为100,比100大的放最后面 + order: 101 + }, + type: "text", + column: { + //控制列字段顺序 + //字段默认order为100,比100大的放最后面 + order: 101 + }, + form: { + //控制表单字段顺序 + //字段默认order为100,比100大的放最后面 + order: 101 + } + }, + col3: { + title: "col.3,我排第一个", + search: { + show: true, + order: 0 + }, + type: "text", + column: { + order: 0 + }, + form: { + order: 0 + } + }, + col4: { + title: "col.4,我在col3后面", + type: "text", + search: { + show: true, + order: 0 + }, + column: { + order: 0 + }, + form: { + order: 0 + } + }, + col5: { + title: "col.5", + type: "text" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/column-sort/index.vue b/packages/ui/certd-client/src/views/crud/feature/column-sort/index.vue new file mode 100644 index 000000000..4f86ebeab --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/column-sort/index.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/column-sort/mock.js b/packages/ui/certd-client/src/views/crud/feature/column-sort/mock.js new file mode 100644 index 000000000..66315f552 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/column-sort/mock.js @@ -0,0 +1,31 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureColumnSort", + idGenerator: 0 +}; +const list = [ + { + col1: "1", + col2: "2", + col3: "3", + col4: "4", + col5: "5" + }, + { + col1: "1", + col2: "2", + col3: "3", + col4: "4", + col5: "5" + }, + { + col1: "1", + col2: "2", + col3: "3", + col4: "4", + col5: "5" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/columns-set/api.js b/packages/ui/certd-client/src/views/crud/feature/columns-set/api.js new file mode 100644 index 000000000..b3a572570 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/columns-set/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureColumnsSet"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/columns-set/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/columns-set/crud.jsx new file mode 100644 index 000000000..3099f8b95 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/columns-set/crud.jsx @@ -0,0 +1,55 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + toolbar: { + columnsFilter: { + mode: "simple" + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/columns-set/index.vue b/packages/ui/certd-client/src/views/crud/feature/columns-set/index.vue new file mode 100644 index 000000000..5c898b199 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/columns-set/index.vue @@ -0,0 +1,49 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/columns-set/mock.js b/packages/ui/certd-client/src/views/crud/feature/columns-set/mock.js new file mode 100644 index 000000000..79043c91e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/columns-set/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureColumnsSet", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/editable-row/api.js b/packages/ui/certd-client/src/views/crud/feature/editable-row/api.js new file mode 100644 index 000000000..acb0aab5c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/editable-row/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureEditableRow"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/editable-row/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/editable-row/crud.jsx new file mode 100644 index 000000000..4b4c8784b --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/editable-row/crud.jsx @@ -0,0 +1,88 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + const id = await api.AddObj(form); + return { id }; + }; + + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + actionbar: { buttons: { add: { show: false }, addRow: { show: true } } }, + table: { + editable: { + enabled: true, + mode: "row", + activeTrigger: false + } + }, + columns: { + id: { + title: "ID", + type: "number", + form: { + show: false + }, + column: { width: 80, align: "center" } + }, + disable: { + title: "禁止编辑", + type: "text", + column: { + editable: { + disabled: true //也可以配置为方法,根据条件禁用或启用编辑 + // disabled: ({ column, index, row }) => { + // return index % 2 === 0; + // } + } + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + }, + name: { + title: "姓名", + type: "text" + }, + province: { + title: "省份", + search: { show: true }, + type: "dict-select", + dict: dict({ + value: "id", + label: "text", + data: [ + { id: "sz", text: "深圳", color: "success" }, + { id: "gz", text: "广州", color: "primary" }, + { id: "bj", text: "北京" }, + { id: "wh", text: "武汉" }, + { id: "sh", text: "上海" } + ] + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/editable-row/index.vue b/packages/ui/certd-client/src/views/crud/feature/editable-row/index.vue new file mode 100644 index 000000000..54f2c6973 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/editable-row/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/editable-row/mock.js b/packages/ui/certd-client/src/views/crud/feature/editable-row/mock.js new file mode 100644 index 000000000..9a11dbb27 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/editable-row/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureEditableRow", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/editable/api.js b/packages/ui/certd-client/src/views/crud/feature/editable/api.js new file mode 100644 index 000000000..e3cf94ecc --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/editable/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureEditable"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/editable/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/editable/crud.jsx new file mode 100644 index 000000000..72d8d6084 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/editable/crud.jsx @@ -0,0 +1,105 @@ +import * as api from "./api"; +import { dict, compute } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const { crudBinding } = expose; + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + actionbar: { + buttons: { + add: { + show: compute(() => { + if (crudBinding.value) { + return !crudBinding.value?.table.editable.enabled; + } + return false; + }) + }, + addRow: { + show: compute(() => { + if (crudBinding.value) { + return crudBinding.value?.table.editable.enabled; + } + return false; + }) + } + } + }, + table: { + editable: { + mode: "free" + } + }, + columns: { + id: { + title: "ID", + type: "number", + form: { + show: false + }, + column: { width: 80, align: "center" } + }, + disable: { + title: "禁止编辑", + type: "text", + column: { + editable: { + disabled: true //也可以配置为方法,根据条件禁用或启用编辑 + // disabled: ({ column, index, row }) => { + // return index % 2 === 0; + // } + } + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + }, + name: { + title: "姓名", + type: "text" + }, + province: { + title: "省份", + search: { show: true }, + type: "dict-select", + dict: dict({ + value: "id", + label: "text", + data: [ + { id: "sz", text: "深圳", color: "success" }, + { id: "gz", text: "广州", color: "primary" }, + { id: "bj", text: "北京" }, + { id: "wh", text: "武汉" }, + { id: "sh", text: "上海" } + ] + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/editable/index.vue b/packages/ui/certd-client/src/views/crud/feature/editable/index.vue new file mode 100644 index 000000000..e7280e8a0 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/editable/index.vue @@ -0,0 +1,100 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/editable/mock.js b/packages/ui/certd-client/src/views/crud/feature/editable/mock.js new file mode 100644 index 000000000..b1d6b08d6 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/editable/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureEditable", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/expand/api.js b/packages/ui/certd-client/src/views/crud/feature/expand/api.js new file mode 100644 index 000000000..c9ad003be --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/expand/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureExpand"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/expand/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/expand/crud.jsx new file mode 100644 index 000000000..5a3dee3f6 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/expand/crud.jsx @@ -0,0 +1,63 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + //默认展开第一行 + defaultExpandedRowKeys:[1], + slots: { + expandedRowRender: (scope) => { + return ( +
+ index: {scope.index} ; row: {JSON.stringify(scope.record)} +
+ ); + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/expand/index.vue b/packages/ui/certd-client/src/views/crud/feature/expand/index.vue new file mode 100644 index 000000000..fe8b3d2c4 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/expand/index.vue @@ -0,0 +1,65 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/expand/mock.js b/packages/ui/certd-client/src/views/crud/feature/expand/mock.js new file mode 100644 index 000000000..3b6d69446 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/expand/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureExpand", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/filter/api.js b/packages/ui/certd-client/src/views/crud/feature/filter/api.js new file mode 100644 index 000000000..6970dae3d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/filter/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureFilter"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/filter/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/filter/crud.jsx new file mode 100644 index 000000000..672d89113 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/filter/crud.jsx @@ -0,0 +1,70 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + // 表头过滤改变事件 + onFilterChange(e) { + console.log("onFilterChange", e); + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }), + column: { + filters: [ + { text: "开", value: "1" }, + { text: "关", value: "0" }, + { text: "停", value: "2" } + ], + // specify the condition of filtering result + // here is that finding the name started with `value` + onFilter: (value, record) => { + return record.radio === value; + }, + sorter: (a, b) => a.radio - b.radio, + sortDirections: ["descend"] + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/filter/index.vue b/packages/ui/certd-client/src/views/crud/feature/filter/index.vue new file mode 100644 index 000000000..7205a9473 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/filter/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/filter/mock.js b/packages/ui/certd-client/src/views/crud/feature/filter/mock.js new file mode 100644 index 000000000..fd405168b --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/filter/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureFilter", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/fixed/api.js b/packages/ui/certd-client/src/views/crud/feature/fixed/api.js new file mode 100644 index 000000000..ac40ccaf6 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/fixed/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureFixed"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/fixed/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/fixed/crud.jsx new file mode 100644 index 000000000..9b15c1ec1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/fixed/crud.jsx @@ -0,0 +1,90 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest: api.GetList, + addRequest, + editRequest, + delRequest + }, + rowHandle: { + //固定右侧 + fixed: "right" + }, + table: { + scroll: { + //当你表格宽度大到需要使用固定列时,需要设置此值,并且是大于等于列宽度之和的值 + //否则可能会出现将自动宽度列挤变形,或者拖动滚动条表头不动等问题。 + x: 1400 + } + }, + columns: { + text1: { + title: "text1", + type: "text", + column: { + // 固定左侧 + // 注意被固定在左侧的列要放在最前面,否则会出现某些列错位不显示的问题 + fixed: "left", + width: 260 + } + }, + id: { + title: "id", + type: "text", + column: { + width: 100 + } + }, + text2: { + title: "text2", + type: "text", + column: { + width: 260 + } + }, + text3: { + title: "text3", + type: "text", + column: { + width: 260 + } + }, + text4: { + title: "text4", + type: "text", + column: { + width: 260 + } + }, + text5: { + title: "text5", + type: "text", + column: { + width: 260 + } + }, + last: { + title: "last", + type: "text", + column: { + width: 260 + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/fixed/index.vue b/packages/ui/certd-client/src/views/crud/feature/fixed/index.vue new file mode 100644 index 000000000..2e1d2ddfd --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/fixed/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/fixed/mock.js b/packages/ui/certd-client/src/views/crud/feature/fixed/mock.js new file mode 100644 index 000000000..22bd02c46 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/fixed/mock.js @@ -0,0 +1,14 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureFixed", + idGenerator: 0 +}; +const list = [ + { + text1: "我会被固定在左侧", + last: "操作列被固定在右侧" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/header-group/api.js b/packages/ui/certd-client/src/views/crud/feature/header-group/api.js new file mode 100644 index 000000000..4af20b765 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/header-group/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureHeaderGroup"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/header-group/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/header-group/crud.jsx new file mode 100644 index 000000000..09819296d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/header-group/crud.jsx @@ -0,0 +1,109 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { size: "small" }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + user: { + title: "用户信息", + children: { + name: { + title: "姓名", + type: "text" + }, + age: { + title: "年龄", + type: "number" + }, + switch: { + title: "开关", + type: "dict-switch", + dict: dict({ + data: [ + { value: true, label: "开启" }, + { value: false, label: "关闭" } + ] + }), + column: { + component: { + name: "fs-dict-switch", + vModel: "checked" + }, + valueChange(context) { + console.log("column value changed:", context); + } + } + } + } + }, + address: { + title: "地址", + children: { + area: { + title: "地区", + children: { + province: { + title: "省", + search: { show: true }, + type: "dict-select", + dict: dict({ + data: [ + { value: "广东省", label: "广东省" }, + { value: "浙江省", label: "浙江省" } + ] + }) + }, + city: { + title: "市", + search: { show: true }, + type: "text" + }, + county: { + title: "区", + search: { show: true }, + type: "text" + } + } + }, + street: { + title: "街道", + type: "text" + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/header-group/index.vue b/packages/ui/certd-client/src/views/crud/feature/header-group/index.vue new file mode 100644 index 000000000..c5b595f1c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/header-group/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/header-group/mock.js b/packages/ui/certd-client/src/views/crud/feature/header-group/mock.js new file mode 100644 index 000000000..05760a739 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/header-group/mock.js @@ -0,0 +1,30 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureHeaderGroup", + idGenerator: 0 +}; +const list = [ + { + name: "张三", + age: 18, + province: "广东省", + city: "深圳市", + county: "南山区", + street: "粤海街道" + }, + { + name: "李四", + age: 26, + province: "浙江省", + city: "杭州市", + county: "西湖区", + street: "西湖街道" + }, + { + name: "王五", + age: 24 + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/height/api.js b/packages/ui/certd-client/src/views/crud/feature/height/api.js new file mode 100644 index 000000000..4f5c7d118 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/height/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/ComponentHeight"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/height/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/height/crud.jsx new file mode 100644 index 000000000..ecd5350d9 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/height/crud.jsx @@ -0,0 +1,57 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + container: { + fixedHeight: false + }, + table: { + scroll: { + y: null + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/height/index.vue b/packages/ui/certd-client/src/views/crud/feature/height/index.vue new file mode 100644 index 000000000..34e52d62d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/height/index.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/height/mock.js b/packages/ui/certd-client/src/views/crud/feature/height/mock.js new file mode 100644 index 000000000..594f66396 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/height/mock.js @@ -0,0 +1,24 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "ComponentHeight", + idGenerator: 0 +}; +const list = [ + { + radio: "1", + button: "1", + bool: true + }, + { + radio: "2", + button: "2", + bool: false + }, + { + radio: "0", + button: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/hide/api.js b/packages/ui/certd-client/src/views/crud/feature/hide/api.js new file mode 100644 index 000000000..dd73bbf3c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/hide/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureHide"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/hide/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/hide/crud.jsx new file mode 100644 index 000000000..27d53eda0 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/hide/crud.jsx @@ -0,0 +1,100 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + search: { + show: true + }, + pagination: { + show: true + }, + table: { + show: true + }, + actionbar: { + show: true, + buttons: { + add: { + show: true + }, + test: { + text: "自定义按钮", + show: true, + click() { + console.log("click"); + } + } + } + }, + toolbar: { + show: true, + buttons: { + search: { show: true }, + refresh: { show: true }, + compact: { show: true }, + export: { show: true }, + columns: { show: true } + } + }, + rowHandle: { + show: true, + width: 330, + buttons: { + view: { show: true }, + edit: { show: true }, + remove: { show: true }, + custom: { + text: "自定义", + order: 4, + show: true, + click(context) { + console.log("click", context); + } + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/hide/index.vue b/packages/ui/certd-client/src/views/crud/feature/hide/index.vue new file mode 100644 index 000000000..454fa5a3e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/hide/index.vue @@ -0,0 +1,184 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/hide/mock.js b/packages/ui/certd-client/src/views/crud/feature/hide/mock.js new file mode 100644 index 000000000..6876a94c7 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/hide/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureHide", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/index/api.js b/packages/ui/certd-client/src/views/crud/feature/index/api.js new file mode 100644 index 000000000..4cf33ea3a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/index/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureIndex"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/index/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/index/crud.jsx new file mode 100644 index 000000000..5c3f31050 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/index/crud.jsx @@ -0,0 +1,56 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: {}, + columns: { + _index: { + title: "序号", + form: { show: false }, + column: { + // type: "index", + align: "center", + width: "55px", + columnSetDisabled: true, //禁止在列设置中选择 + formatter: (context) => { + //计算序号,你可以自定义计算规则,此处为翻页累加 + let index = context.index ?? 1; + let pagination = expose.crudBinding.value.pagination; + return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1; + } + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/index/index.vue b/packages/ui/certd-client/src/views/crud/feature/index/index.vue new file mode 100644 index 000000000..06c33b2b8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/index/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/index/mock.js b/packages/ui/certd-client/src/views/crud/feature/index/mock.js new file mode 100644 index 000000000..6e10439f4 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/index/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureIndex", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/local-v-model/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/local-v-model/crud.jsx new file mode 100644 index 000000000..1dbe46ff5 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/local-v-model/crud.jsx @@ -0,0 +1,32 @@ +import { uiContext } from "@fast-crud/fast-crud"; + +export default function ({ expose }) { + return { + crudOptions: { + mode:{ + name:'local', + isMergeWhenUpdate:true, + isAppendWhenAdd:true + }, + search: { + show: false + }, + toolbar:{ + show:false, + }, + pagination:{ + show:false + }, + columns: { + name: { + type: "text", + title: "联系人姓名" + }, + mobile: { + type: "text", + title: "联系人手机号码" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/local-v-model/index.vue b/packages/ui/certd-client/src/views/crud/feature/local-v-model/index.vue new file mode 100644 index 000000000..01e615f59 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/local-v-model/index.vue @@ -0,0 +1,51 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/local-v-model/local.vue b/packages/ui/certd-client/src/views/crud/feature/local-v-model/local.vue new file mode 100644 index 000000000..0110ae279 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/local-v-model/local.vue @@ -0,0 +1,67 @@ + + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/local/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/local/crud.jsx new file mode 100644 index 000000000..c0f2f6af8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/local/crud.jsx @@ -0,0 +1,35 @@ +import { uiContext } from "@fast-crud/fast-crud"; + +export default function ({ expose }) { + return { + crudOptions: { + mode: { + name: "local", + isMergeWhenUpdate: true, + isAppendWhenAdd: true + }, + actionbar: { buttons: { add: { show: true }, addRow: { show: false } } }, + editable: { + enabled: false, + mode: "row", + activeTrigger: false + }, + search: { + show: false + }, + pagination: { + show: false + }, + columns: { + name: { + type: "text", + title: "联系人姓名" + }, + mobile: { + type: "text", + title: "联系人手机号码" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/local/index.vue b/packages/ui/certd-client/src/views/crud/feature/local/index.vue new file mode 100644 index 000000000..743588f1a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/local/index.vue @@ -0,0 +1,89 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/merge/api.js b/packages/ui/certd-client/src/views/crud/feature/merge/api.js new file mode 100644 index 000000000..65dafffca --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/merge/api.js @@ -0,0 +1,43 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureMerge"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + diff --git a/packages/ui/certd-client/src/views/crud/feature/merge/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/merge/crud.jsx new file mode 100644 index 000000000..36d418971 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/merge/crud.jsx @@ -0,0 +1,129 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + /** + * 列合并render + */ + function colMergeRender({ index }) { + return { + props: { + colSpan: 5 + } + }; + } + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + slots: { + summary() { + return
总结栏
; + } + } + }, + columns: { + id: { + title: "id", + type: "text" + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + }, + cellMerge: { + title: "上下合并", + column: { + customRender: ({ text, index }, cellRender) => { + const obj = { + props: {} + }; + if (index === 2) { + obj.children = text + "(我合并了)"; + obj.props.rowSpan = 2; + } else if (index === 3) { + obj.props.rowSpan = 0; + } else { + obj.children = cellRender(); + } + return obj; + } + } + }, + colMerge1: { + title: "左右合并", + column: { + align: "center", + customRender({ text, index, record, dataIndex }, cellRender) { + if (index !== 4) { + return { + children: cellRender() + }; + } + return { + children: text + "(我合并了)", + props: { + colSpan: 2 + } + }; + } + } + }, + colMerge2: { + title: "左右合并", + column: { + customRender({ text, index, record, dataIndex }, cellRender) { + if (index !== 4) { + return { + children: cellRender() + }; + } + return { + props: { + colSpan: 0 + } + }; + } + } + }, + header1: { + title: "表头合并(我合并了)", + type: "text", + column: { + colSpan: 2 + } + }, + header2: { + title: "表头合并", + type: "text", + column: { + colSpan: 0 + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/merge/index.vue b/packages/ui/certd-client/src/views/crud/feature/merge/index.vue new file mode 100644 index 000000000..49d0ce99c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/merge/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/merge/mock.js b/packages/ui/certd-client/src/views/crud/feature/merge/mock.js new file mode 100644 index 000000000..e99a6af50 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/merge/mock.js @@ -0,0 +1,36 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureMerge", + idGenerator: 0 +}; +const list = [ + { + radio: "1", + cellMerge: "test", + colMerge1: "111", + colMerge2: "222", + + header1: "aaa", + header2: "bbb" + }, + { + radio: "2", + cellMerge: "test", + colMerge1: "111", + colMerge2: "222", + + header1: "aaa", + header2: "bbb" + }, + { + radio: "0", + cellMerge: "test", + colMerge1: "111", + colMerge2: "222", + header1: "aaa", + header2: "bbb" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/remove/api.js b/packages/ui/certd-client/src/views/crud/feature/remove/api.js new file mode 100644 index 000000000..1d251eb79 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/remove/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureRemove"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/remove/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/remove/crud.jsx new file mode 100644 index 000000000..52a476cd3 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/remove/crud.jsx @@ -0,0 +1,74 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { Modal } from "ant-design-vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + remove: { + async confirmFn(context) { + await new Promise((resolve, reject) => { + Modal.confirm({ + content: `确定删除记录(${context.row.id})吗?`, + onOk() { + resolve(); + }, + onCancel() { + reject(); + } + }); + }); + }, + confirmTitle: "请确认", // confirmFn配置为空时生效 + confirmMessage: "确定删除此记录吗", // confirmFn配置为空时生效 + showSuccessMessage: true, //是否显示删除成功记录 + refreshTable: true, //删除后刷新表格 + onCanceled({ row }) { + console.log(`记录${row.id}取消删除`); + }, + onRemoved({ row }) { + console.log(`记录${row.id}已删除`); + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + text: { + title: "文本", + search: { show: true }, + type: "text" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/remove/index.vue b/packages/ui/certd-client/src/views/crud/feature/remove/index.vue new file mode 100644 index 000000000..804e85bb1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/remove/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/remove/mock.js b/packages/ui/certd-client/src/views/crud/feature/remove/mock.js new file mode 100644 index 000000000..efb116af7 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/remove/mock.js @@ -0,0 +1,25 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureRemove", + idGenerator: 0 +}; +const list = [ + { + text: "测试文本1" + }, + { + text: "测试文本2" + }, + { + text: "测试文本3" + }, + { + text: "测试文本4" + }, + { + text: "测试文本5" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/search-multi/api.js b/packages/ui/certd-client/src/views/crud/feature/search-multi/api.js new file mode 100644 index 000000000..2f83742af --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/search-multi/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureSearchMulti"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/search-multi/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/search-multi/crud.jsx new file mode 100644 index 000000000..6f9fdbd2f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/search-multi/crud.jsx @@ -0,0 +1,143 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + search: { + layout: "multi-line", + col: { + span: 4 + }, + options: { + labelCol: { + style: { + width: "100px" + } + } + } + }, + actionbar: { + buttons: { + change: { + text: "切换模式", + click() { + if (expose.crudBinding.value.search.layout === "multi-line") { + expose.crudBinding.value.search.layout = ""; + } else { + expose.crudBinding.value.search.layout = "multi-line"; + } + } + }, + search: { + text: "查询", + click() { + expose.getSearchRef().doSearch(); + } + }, + reset: { + text: "重置查询", + click() { + expose.getSearchRef().doReset(); + } + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }), + column: { + filters: [ + { text: "开", value: "1" }, + { text: "关", value: "0" }, + { text: "停", value: "2" } + ], + // specify the condition of filtering result + // here is that finding the name started with `value` + onFilter: (value, record) => { + return record.radio === value; + }, + sorter: (a, b) => a.radio - b.radio, + sortDirections: ["descend"] + } + }, + text1: { + type: "text", + title: "text1", + search: { show: true } + }, + text2: { + type: "text", + title: "text2", + search: { show: true } + }, + text3: { + type: "text", + title: "text3", + search: { show: true } + }, + text4: { + type: "text", + title: "text4", + search: { show: true } + }, + text5: { + type: "text", + title: "text5", + search: { show: true } + }, + text6: { + type: "text", + title: "text6", + search: { show: true } + }, + text7: { + type: "text", + title: "text7", + search: { show: true } + }, + text8: { + type: "text", + title: "text8", + search: { show: true } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/search-multi/index.vue b/packages/ui/certd-client/src/views/crud/feature/search-multi/index.vue new file mode 100644 index 000000000..6e6d96052 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/search-multi/index.vue @@ -0,0 +1,41 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/search-multi/mock.js b/packages/ui/certd-client/src/views/crud/feature/search-multi/mock.js new file mode 100644 index 000000000..0a2531527 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/search-multi/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureSearchMulti", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/search/api.js b/packages/ui/certd-client/src/views/crud/feature/search/api.js new file mode 100644 index 000000000..279509cc9 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/search/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureSearch"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/search/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/search/crud.jsx new file mode 100644 index 000000000..0fe249de4 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/search/crud.jsx @@ -0,0 +1,88 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + // 表头过滤改变事件 + onFilterChange(e) { + console.log("onFilterChange", e); + } + }, + search:{ + buttons:{ + custom:{ + text:"自定义", + show:true, + order:3, + icon:{ + icon:"ant-design:search", + style:{ + "font-size":'16px' + } + }, + click(){ + console.log("点击了自定义按钮") + } + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }), + column: { + filters: [ + { text: "开", value: "1" }, + { text: "关", value: "0" }, + { text: "停", value: "2" } + ], + // specify the condition of filtering result + // here is that finding the name started with `value` + onFilter: (value, record) => { + return record.radio === value; + }, + sorter: (a, b) => a.radio - b.radio, + sortDirections: ["descend"] + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/search/index.vue b/packages/ui/certd-client/src/views/crud/feature/search/index.vue new file mode 100644 index 000000000..e36b8dff1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/search/index.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/search/mock.js b/packages/ui/certd-client/src/views/crud/feature/search/mock.js new file mode 100644 index 000000000..a6741fa06 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/search/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureSearch", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/selection-radio/api.js b/packages/ui/certd-client/src/views/crud/feature/selection-radio/api.js new file mode 100644 index 000000000..1d86caa21 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/selection-radio/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureSelection"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/selection-radio/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/selection-radio/crud.jsx new file mode 100644 index 000000000..18efb25f8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/selection-radio/crud.jsx @@ -0,0 +1,67 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + const selectedRowKey = ref(); + const onSelectChange = (changed) => { + console.log("selection", changed); + selectedRowKey.value = changed; + }; + return { + selectedRowKey, //返回给index.vue去使用 + crudOptions: { + table: { + rowKey: "id", + rowSelection: { + type: "radio", + selectedRowKeys: selectedRowKey, + onChange: onSelectChange, + getCheckboxProps: (record) => ({ + disabled: record.id === 1 // 此处演示第一行禁用 + }) + } + }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/selection-radio/index.vue b/packages/ui/certd-client/src/views/crud/feature/selection-radio/index.vue new file mode 100644 index 000000000..57386c5a6 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/selection-radio/index.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/selection-radio/mock.js b/packages/ui/certd-client/src/views/crud/feature/selection-radio/mock.js new file mode 100644 index 000000000..b9b0bade6 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/selection-radio/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureSelection", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/selection/api.js b/packages/ui/certd-client/src/views/crud/feature/selection/api.js new file mode 100644 index 000000000..1d86caa21 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/selection/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureSelection"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/selection/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/selection/crud.jsx new file mode 100644 index 000000000..9ac917e3b --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/selection/crud.jsx @@ -0,0 +1,67 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + const selectedRowKeys = ref([]); + + const onSelectChange = (changed) => { + console.log("selection", changed); + selectedRowKeys.value = changed; + }; + return { + selectedRowKeys, //返回给index.vue去使用 + crudOptions: { + table: { + rowKey: "id", + rowSelection: { + selectedRowKeys: selectedRowKeys, + onChange: onSelectChange, + getCheckboxProps: (record) => ({ + disabled: record.id === 1 // 此处演示第一行禁用 + }) + } + }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/selection/index.vue b/packages/ui/certd-client/src/views/crud/feature/selection/index.vue new file mode 100644 index 000000000..7a84d6c29 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/selection/index.vue @@ -0,0 +1,68 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/selection/mock.js b/packages/ui/certd-client/src/views/crud/feature/selection/mock.js new file mode 100644 index 000000000..b9b0bade6 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/selection/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureSelection", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/sortable/api.js b/packages/ui/certd-client/src/views/crud/feature/sortable/api.js new file mode 100644 index 000000000..17fe338f5 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/sortable/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureSortable"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/sortable/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/sortable/crud.jsx new file mode 100644 index 000000000..0b00b387a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/sortable/crud.jsx @@ -0,0 +1,62 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest: api.GetList, + addRequest, + editRequest, + delRequest + }, + columns: { + radio: { + title: "本地排序", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }), + column: { + sorter(a, b) { + return a.radio < b.radio ? 1 : -1; + } + } + }, + radio1: { + title: "服务端排序1", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }), + column: { + sorter: true + } + }, + radio2: { + title: "服务端排序2", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }), + column: { + sorter: true + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/sortable/index.vue b/packages/ui/certd-client/src/views/crud/feature/sortable/index.vue new file mode 100644 index 000000000..1d3e59455 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/sortable/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/sortable/mock.js b/packages/ui/certd-client/src/views/crud/feature/sortable/mock.js new file mode 100644 index 000000000..5bb6d0e69 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/sortable/mock.js @@ -0,0 +1,25 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureSortable", + idGenerator: 0 +}; +const list = [ + { + radio: "1", + radio1: "1", + radio2: "2" + }, + { + radio: "2", + radio1: "2", + radio2: "0" + }, + { + radio: "0", + radio1: "0", + radio2: "1" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/tree/api.js b/packages/ui/certd-client/src/views/crud/feature/tree/api.js new file mode 100644 index 000000000..771633983 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/tree/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureTree"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/tree/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/tree/crud.jsx new file mode 100644 index 000000000..b47c3fa96 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/tree/crud.jsx @@ -0,0 +1,91 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; + +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + const selectedRowKeys = ref([]); + + const onSelectChange = (changed) => { + console.log("selection", changed); + selectedRowKeys.value = changed; + }; + return { + selectedRowKeys, //返回给index.vue去使用 + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + table: { + rowSelection: { selectedRowKeys: selectedRowKeys, onChange: onSelectChange } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 100 + }, + form: { + show: false + } + }, + time: { + title: "时间", + type: "datetime", + column: { + width: 180 + } + }, + province: { + title: "地区", + type: "dict-select", + search: { show: true }, + form: { + component: { filterable: true, multiple: true } + }, + dict: dict({ + data: [ + { value: "sz", label: "深圳" }, + { value: "gz", label: "广州" }, + { value: "wh", label: "武汉" }, + { value: "sh", label: "上海" } + ] + }), + column: { + width: 300 + } + }, + amount: { + title: "金额(元)", + key: "amount" + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/tree/index.vue b/packages/ui/certd-client/src/views/crud/feature/tree/index.vue new file mode 100644 index 000000000..b9240eac1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/tree/index.vue @@ -0,0 +1,65 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/tree/mock.js b/packages/ui/certd-client/src/views/crud/feature/tree/mock.js new file mode 100644 index 000000000..f1df3badc --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/tree/mock.js @@ -0,0 +1,85 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureTree", + idGenerator: 0 +}; +const list = [ + { + radio: "1", + data: "我会懒加载", + time: "2020-01-01 11:11:11", + province: "wh", + amount: 100, + hasChildren: true, + loaded: false, + children: [ + { + data: "懒加载的子数据", + province: ["sh", "gz"], + time: "2020-01-01 11:11:11", + amount: 100 + }, + { + data: "懒加载的子数据2", + province: ["sh", "sz"], + time: "2020-01-01 11:11:11", + amount: 100 + } + ] + }, + { + data: "data2", + province: "sh", + time: "2020-01-01 11:11:11", + amount: 100, + children: [ + { + id: 999, + data: "data1_1", + time: "2020-01-01 11:11:11", + province: ["gz", "sz"], // 可以逗号分隔的字符串 'gz,sz' + amount: 100, + children: [ + { + id: 1000, + data: "data1_1_1", + time: "2020-01-01 11:11:11", + province: ["sz", "gz"], // 可以逗号分隔的字符串 'gz,sz' + amount: 100 + } + ] + }, + { + id: 888, + data: "data1_2", + time: "2020-01-01 11:11:11", + province: "sh", + amount: 100, + children: [ + { + id: 889, + data: "data1_2_1", + time: "2020-01-01 11:11:11", + province: "gz", + amount: 100 + } + ] + } + ] + }, + { + data: "data3", + province: ["sh", "gz"], + time: "2020-01-01 11:11:11", + amount: 100 + }, + { + data: "data4", + province: ["sh", "sz"], + time: "2020-01-01 11:11:11", + amount: 100 + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/feature/value-builder/api.js b/packages/ui/certd-client/src/views/crud/feature/value-builder/api.js new file mode 100644 index 000000000..09e547c6a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/value-builder/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FeatureValueBuilder"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/feature/value-builder/crud.jsx b/packages/ui/certd-client/src/views/crud/feature/value-builder/crud.jsx new file mode 100644 index 000000000..b44f24830 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/value-builder/crud.jsx @@ -0,0 +1,54 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest: api.GetList, + addRequest, + editRequest, + delRequest + }, + columns: { + name: { + title: "姓名", + type: "text" + }, + roles: { + title: "角色", + search: { show: true }, + type: "dict-select", + dict: dict({ + value: "id", + label: "name", + data: [ + { id: 1, name: "管理员" }, + { id: 2, name: "普通用户" } + ] + }), + form: { + component: { + mode: "multiple" + }, + valueBuilder({ form }) { + if (form.roles) { + form.roles = form.roles.map((item) => item.id); + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/feature/value-builder/index.vue b/packages/ui/certd-client/src/views/crud/feature/value-builder/index.vue new file mode 100644 index 000000000..156b4e5bf --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/value-builder/index.vue @@ -0,0 +1,46 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/feature/value-builder/mock.js b/packages/ui/certd-client/src/views/crud/feature/value-builder/mock.js new file mode 100644 index 000000000..8c86a37e1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/feature/value-builder/mock.js @@ -0,0 +1,21 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "FeatureValueBuilder", + idGenerator: 0 +}; +const list = [ + { + name: "user1", + roles: [ + { id: 1, name: "管理员" }, + { id: 2, name: "普通用户" } + ] + }, + { + name: "user2", + roles: [{ id: 1, name: "管理员" }] + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/base/api.js b/packages/ui/certd-client/src/views/crud/form/base/api.js new file mode 100644 index 000000000..cff9ab911 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/base/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormBase"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/base/crud.jsx b/packages/ui/certd-client/src/views/crud/form/base/crud.jsx new file mode 100644 index 000000000..453a68296 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/base/crud.jsx @@ -0,0 +1,33 @@ +import * as api from "./api"; +export default function ({ crudExpose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + name: { + title: "姓名", + type: "text" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/base/index.vue b/packages/ui/certd-client/src/views/crud/form/base/index.vue new file mode 100644 index 000000000..97184909d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/base/index.vue @@ -0,0 +1,32 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/base/mock.js b/packages/ui/certd-client/src/views/crud/form/base/mock.js new file mode 100644 index 000000000..bb0fe7b99 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/base/mock.js @@ -0,0 +1,310 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormBase", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + age: 15, + password: "", + status: "2", + url: "https://baidu.com" + }, + { + name: "张三", + age: 18, + password: "", + url: "https://baidu.com" + }, + { + status: "2" + } +]; + +const dictData = [ + { + value: "zhinan", + label: "指南", + children: [ + { + value: "shejiyuanze", + label: "设计原则", + children: [ + { + value: "yizhi", + label: "一致" + }, + { + value: "fankui", + label: "反馈" + }, + { + value: "xiaolv", + label: "效率" + }, + { + value: "kekong", + label: "可控" + } + ] + }, + { + value: "daohang", + label: "导航", + children: [ + { + value: "cexiangdaohang", + label: "侧向导航" + }, + { + value: "dingbudaohang", + label: "顶部导航" + } + ] + } + ] + }, + { + value: "zujian", + label: "组件", + children: [ + { + value: "basic", + label: "Basic", + children: [ + { + value: "layout", + label: "Layout 布局" + }, + { + value: "color", + label: "Color 色彩" + }, + { + value: "typography", + label: "Typography 字体" + }, + { + value: "icon", + label: "Icon 图标" + }, + { + value: "button", + label: "Button 按钮" + } + ] + }, + { + value: "form", + label: "Form", + children: [ + { + value: "radio", + label: "Radio 单选框" + }, + { + value: "checkbox", + label: "Checkbox 多选框" + }, + { + value: "input", + label: "Input 输入框" + }, + { + value: "input-number", + label: "InputNumber 计数器" + }, + { + value: "select", + label: "Select 选择器" + }, + { + value: "cascader", + label: "Cascader 级联选择器" + }, + { + value: "switch", + label: "Switch 开关" + }, + { + value: "slider", + label: "Slider 滑块" + }, + { + value: "time-picker", + label: "TimePicker 时间选择器" + }, + { + value: "date-picker", + label: "DatePicker 日期选择器" + }, + { + value: "datetime-picker", + label: "DateTimePicker 日期时间选择器" + }, + { + value: "upload", + label: "Upload 上传" + }, + { + value: "rate", + label: "Rate 评分" + }, + { + value: "form", + label: "Form 表单" + } + ] + }, + { + value: "data", + label: "Data", + children: [ + { + value: "table", + label: "Table 表格" + }, + { + value: "tag", + label: "Tag 标签" + }, + { + value: "progress", + label: "Progress 进度条" + }, + { + value: "tree", + label: "Tree 树形控件" + }, + { + value: "pagination", + label: "Pagination 分页" + }, + { + value: "badge", + label: "Badge 标记" + } + ] + }, + { + value: "notice", + label: "Notice", + children: [ + { + value: "alert", + label: "Alert 警告" + }, + { + value: "loading", + label: "Loading 加载" + }, + { + value: "message", + label: "Message 消息提示" + }, + { + value: "message-box", + label: "MessageBox 弹框" + }, + { + value: "notification", + label: "Notification 通知" + } + ] + }, + { + value: "navigation", + label: "Navigation", + children: [ + { + value: "menu", + label: "NavMenu 导航菜单" + }, + { + value: "tabs", + label: "Tabs 标签页" + }, + { + value: "breadcrumb", + label: "Breadcrumb 面包屑" + }, + { + value: "dropdown", + label: "Dropdown 下拉菜单" + }, + { + value: "steps", + label: "Steps 步骤条" + } + ] + }, + { + value: "others", + label: "Others", + children: [ + { + value: "dialog", + label: "Dialog 对话框" + }, + { + value: "tooltip", + label: "Tooltip 文字提示" + }, + { + value: "popover", + label: "Popover 弹出框" + }, + { + value: "card", + label: "Card 卡片" + }, + { + value: "carousel", + label: "Carousel 走马灯" + }, + { + value: "collapse", + label: "Collapse 折叠面板" + } + ] + } + ] + }, + { + value: "ziyuan", + label: "资源", + children: [ + { + value: "axure", + label: "Axure Components" + }, + { + value: "sketch", + label: "Sketch Templates" + }, + { + value: "jiaohu", + label: "组件交互文档" + } + ] + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); +mock.push({ + path: "/select/cascadeData", + method: "get", + handle(req) { + return { + code: 0, + msg: "success", + data: dictData + }; + } +}); + +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/custom-form/api.js b/packages/ui/certd-client/src/views/crud/form/custom-form/api.js new file mode 100644 index 000000000..1cab36020 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/custom-form/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormCustomForm"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/custom-form/crud.jsx b/packages/ui/certd-client/src/views/crud/form/custom-form/crud.jsx new file mode 100644 index 000000000..875a80636 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/custom-form/crud.jsx @@ -0,0 +1,40 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const { getFormRef, getFormData } = expose; + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + title: { + title: "商品标题", + type: "text" + }, + code: { + title: "商品代码", + search: { show: true }, + type: "text" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/custom-form/index.vue b/packages/ui/certd-client/src/views/crud/form/custom-form/index.vue new file mode 100644 index 000000000..faa3c2be1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/custom-form/index.vue @@ -0,0 +1,86 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/custom-form/mock.js b/packages/ui/certd-client/src/views/crud/form/custom-form/mock.js new file mode 100644 index 000000000..479b89b43 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/custom-form/mock.js @@ -0,0 +1,303 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormCustomForm", + idGenerator: 0 +}; +const list = [ + { + title: "无线充电宝", + code: "100001", + images: "https://img0.bdstatic.com/static/searchdetail/img/logo-2X_0c4ef02.png", + price: 100, + stock: 99, + intro: "30000毫安超大容量移动电源充电宝官方原装正品专用便携", + content: "" + } +]; + +const dictData = [ + { + value: "zhinan", + label: "指南", + children: [ + { + value: "shejiyuanze", + label: "设计原则", + children: [ + { + value: "yizhi", + label: "一致" + }, + { + value: "fankui", + label: "反馈" + }, + { + value: "xiaolv", + label: "效率" + }, + { + value: "kekong", + label: "可控" + } + ] + }, + { + value: "daohang", + label: "导航", + children: [ + { + value: "cexiangdaohang", + label: "侧向导航" + }, + { + value: "dingbudaohang", + label: "顶部导航" + } + ] + } + ] + }, + { + value: "zujian", + label: "组件", + children: [ + { + value: "basic", + label: "Basic", + children: [ + { + value: "layout", + label: "Layout 布局" + }, + { + value: "color", + label: "Color 色彩" + }, + { + value: "typography", + label: "Typography 字体" + }, + { + value: "icon", + label: "Icon 图标" + }, + { + value: "button", + label: "Button 按钮" + } + ] + }, + { + value: "form", + label: "Form", + children: [ + { + value: "radio", + label: "Radio 单选框" + }, + { + value: "checkbox", + label: "Checkbox 多选框" + }, + { + value: "input", + label: "Input 输入框" + }, + { + value: "input-number", + label: "InputNumber 计数器" + }, + { + value: "select", + label: "Select 选择器" + }, + { + value: "cascader", + label: "Cascader 级联选择器" + }, + { + value: "switch", + label: "Switch 开关" + }, + { + value: "slider", + label: "Slider 滑块" + }, + { + value: "time-picker", + label: "TimePicker 时间选择器" + }, + { + value: "date-picker", + label: "DatePicker 日期选择器" + }, + { + value: "datetime-picker", + label: "DateTimePicker 日期时间选择器" + }, + { + value: "upload", + label: "Upload 上传" + }, + { + value: "rate", + label: "Rate 评分" + }, + { + value: "form", + label: "Form 表单" + } + ] + }, + { + value: "data", + label: "Data", + children: [ + { + value: "table", + label: "Table 表格" + }, + { + value: "tag", + label: "Tag 标签" + }, + { + value: "progress", + label: "Progress 进度条" + }, + { + value: "tree", + label: "Tree 树形控件" + }, + { + value: "pagination", + label: "Pagination 分页" + }, + { + value: "badge", + label: "Badge 标记" + } + ] + }, + { + value: "notice", + label: "Notice", + children: [ + { + value: "alert", + label: "Alert 警告" + }, + { + value: "loading", + label: "Loading 加载" + }, + { + value: "message", + label: "Message 消息提示" + }, + { + value: "message-box", + label: "MessageBox 弹框" + }, + { + value: "notification", + label: "Notification 通知" + } + ] + }, + { + value: "navigation", + label: "Navigation", + children: [ + { + value: "menu", + label: "NavMenu 导航菜单" + }, + { + value: "tabs", + label: "Tabs 标签页" + }, + { + value: "breadcrumb", + label: "Breadcrumb 面包屑" + }, + { + value: "dropdown", + label: "Dropdown 下拉菜单" + }, + { + value: "steps", + label: "Steps 步骤条" + } + ] + }, + { + value: "others", + label: "Others", + children: [ + { + value: "dialog", + label: "Dialog 对话框" + }, + { + value: "tooltip", + label: "Tooltip 文字提示" + }, + { + value: "popover", + label: "Popover 弹出框" + }, + { + value: "card", + label: "Card 卡片" + }, + { + value: "carousel", + label: "Carousel 走马灯" + }, + { + value: "collapse", + label: "Collapse 折叠面板" + } + ] + } + ] + }, + { + value: "ziyuan", + label: "资源", + children: [ + { + value: "axure", + label: "Axure Components" + }, + { + value: "sketch", + label: "Sketch Templates" + }, + { + value: "jiaohu", + label: "组件交互文档" + } + ] + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); +mock.push({ + path: "/select/cascadeData", + method: "get", + handle(req) { + return { + code: 0, + msg: "success", + data: dictData + }; + } +}); + +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/drawer/api.js b/packages/ui/certd-client/src/views/crud/form/drawer/api.js new file mode 100644 index 000000000..b9e50dc64 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/drawer/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/formText"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/drawer/crud.jsx b/packages/ui/certd-client/src/views/crud/form/drawer/crud.jsx new file mode 100644 index 000000000..bf6e0c5fa --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/drawer/crud.jsx @@ -0,0 +1,44 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + wrapper: { + is: "a-drawer" + } + }, + columns: { + name: { + title: "姓名", + type: "text", //虽然不写也能正确显示组件,但不建议省略它 + search: { show: true }, + form: { + component: { + maxlength: 20 + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/drawer/index.vue b/packages/ui/certd-client/src/views/crud/form/drawer/index.vue new file mode 100644 index 000000000..5143f074e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/drawer/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/drawer/mock.js b/packages/ui/certd-client/src/views/crud/form/drawer/mock.js new file mode 100644 index 000000000..fcf01621e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/drawer/mock.js @@ -0,0 +1,40 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "formText", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + date: "2016-05-02", + status: "0", + province: "1", + avatar: "https://alicdn.antdv.com/vue.png", + show: true, + city: "sz", + address: "123123", + zip: "518000", + intro: "王小虎是element-plus的table示例出现的名字" + }, + { + name: "张三", + date: "2016-05-04", + status: "1", + province: "2" + }, + { + name: "李四", + date: 2232433534511, + status: "1", + province: "0" + }, + { + name: "王五", + date: "2016-05-03", + status: "2", + province: "wh,gz" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/group-tabs/api.js b/packages/ui/certd-client/src/views/crud/form/group-tabs/api.js new file mode 100644 index 000000000..a059b9001 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/group-tabs/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormGroupTabs"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/group-tabs/crud.jsx b/packages/ui/certd-client/src/views/crud/form/group-tabs/crud.jsx new file mode 100644 index 000000000..74dbfb0b3 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/group-tabs/crud.jsx @@ -0,0 +1,114 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const { getFormRef, getFormData } = expose; + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + title: { + title: "商品标题", + type: "text" + }, + code: { + title: "商品代码", + search: { show: true }, + type: "text", + form: { + rules: [{ required: true, message: "此项必填" }] + } + }, + images: { + title: "图片", + type: "image-uploader" + }, + price: { + title: "价格", + sortable: true + }, + store: { + title: "库存", + type: "number" + }, + intro: { + title: "简介", + type: "textarea", + column: { + ellipsis: true, + showTitle: true + } + }, + content: { + title: "详情", + type: "editor-ueditor", + form: { + labelWidth: "0px" + } + } + }, + form: { + group: { + groupType: "tabs", //collapse, tabs + accordion: false, + groups: { + base: { + slots: { + tab: (scope) => { + return ( + + + 商品基础 + + ); + } + }, + icon: "el-icon-goods", + columns: ["code", "title", "images"] + }, + price: { + tab: "库存价格", + icon: "el-icon-price-tag", + columns: ["store", "price"] + }, + info: { + tab: "详情", + collapsed: true, //默认折叠 + icon: "el-icon-warning-outline", + columns: ["intro", "content"] + } + // custom: { + // title: "自定义", + // collapsed: false, + // show(context) { + // console.log("custom context", context); + // return context.mode === "view"; + // }, + // disabled: false, + // icon: "el-icon-warning-outline", + // columns: ["custom", "custom2"] + // } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/group-tabs/index.vue b/packages/ui/certd-client/src/views/crud/form/group-tabs/index.vue new file mode 100644 index 000000000..3d65edece --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/group-tabs/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/group-tabs/mock.js b/packages/ui/certd-client/src/views/crud/form/group-tabs/mock.js new file mode 100644 index 000000000..bc980fa87 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/group-tabs/mock.js @@ -0,0 +1,303 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormGroupTabs", + idGenerator: 0 +}; +const list = [ + { + title: "无线充电宝", + code: "100001", + images: "https://img0.bdstatic.com/static/searchdetail/img/logo-2X_0c4ef02.png", + price: 100, + stock: 99, + intro: "30000毫安超大容量移动电源充电宝官方原装正品专用便携", + content: "" + } +]; + +const dictData = [ + { + value: "zhinan", + label: "指南", + children: [ + { + value: "shejiyuanze", + label: "设计原则", + children: [ + { + value: "yizhi", + label: "一致" + }, + { + value: "fankui", + label: "反馈" + }, + { + value: "xiaolv", + label: "效率" + }, + { + value: "kekong", + label: "可控" + } + ] + }, + { + value: "daohang", + label: "导航", + children: [ + { + value: "cexiangdaohang", + label: "侧向导航" + }, + { + value: "dingbudaohang", + label: "顶部导航" + } + ] + } + ] + }, + { + value: "zujian", + label: "组件", + children: [ + { + value: "basic", + label: "Basic", + children: [ + { + value: "layout", + label: "Layout 布局" + }, + { + value: "color", + label: "Color 色彩" + }, + { + value: "typography", + label: "Typography 字体" + }, + { + value: "icon", + label: "Icon 图标" + }, + { + value: "button", + label: "Button 按钮" + } + ] + }, + { + value: "form", + label: "Form", + children: [ + { + value: "radio", + label: "Radio 单选框" + }, + { + value: "checkbox", + label: "Checkbox 多选框" + }, + { + value: "input", + label: "Input 输入框" + }, + { + value: "input-number", + label: "InputNumber 计数器" + }, + { + value: "select", + label: "Select 选择器" + }, + { + value: "cascader", + label: "Cascader 级联选择器" + }, + { + value: "switch", + label: "Switch 开关" + }, + { + value: "slider", + label: "Slider 滑块" + }, + { + value: "time-picker", + label: "TimePicker 时间选择器" + }, + { + value: "date-picker", + label: "DatePicker 日期选择器" + }, + { + value: "datetime-picker", + label: "DateTimePicker 日期时间选择器" + }, + { + value: "upload", + label: "Upload 上传" + }, + { + value: "rate", + label: "Rate 评分" + }, + { + value: "form", + label: "Form 表单" + } + ] + }, + { + value: "data", + label: "Data", + children: [ + { + value: "table", + label: "Table 表格" + }, + { + value: "tag", + label: "Tag 标签" + }, + { + value: "progress", + label: "Progress 进度条" + }, + { + value: "tree", + label: "Tree 树形控件" + }, + { + value: "pagination", + label: "Pagination 分页" + }, + { + value: "badge", + label: "Badge 标记" + } + ] + }, + { + value: "notice", + label: "Notice", + children: [ + { + value: "alert", + label: "Alert 警告" + }, + { + value: "loading", + label: "Loading 加载" + }, + { + value: "message", + label: "Message 消息提示" + }, + { + value: "message-box", + label: "MessageBox 弹框" + }, + { + value: "notification", + label: "Notification 通知" + } + ] + }, + { + value: "navigation", + label: "Navigation", + children: [ + { + value: "menu", + label: "NavMenu 导航菜单" + }, + { + value: "tabs", + label: "Tabs 标签页" + }, + { + value: "breadcrumb", + label: "Breadcrumb 面包屑" + }, + { + value: "dropdown", + label: "Dropdown 下拉菜单" + }, + { + value: "steps", + label: "Steps 步骤条" + } + ] + }, + { + value: "others", + label: "Others", + children: [ + { + value: "dialog", + label: "Dialog 对话框" + }, + { + value: "tooltip", + label: "Tooltip 文字提示" + }, + { + value: "popover", + label: "Popover 弹出框" + }, + { + value: "card", + label: "Card 卡片" + }, + { + value: "carousel", + label: "Carousel 走马灯" + }, + { + value: "collapse", + label: "Collapse 折叠面板" + } + ] + } + ] + }, + { + value: "ziyuan", + label: "资源", + children: [ + { + value: "axure", + label: "Axure Components" + }, + { + value: "sketch", + label: "Sketch Templates" + }, + { + value: "jiaohu", + label: "组件交互文档" + } + ] + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); +mock.push({ + path: "/select/cascadeData", + method: "get", + handle(req) { + return { + code: 0, + msg: "success", + data: dictData + }; + } +}); + +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/group/api.js b/packages/ui/certd-client/src/views/crud/form/group/api.js new file mode 100644 index 000000000..b1fcf8560 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/group/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormGroup"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/group/crud.jsx b/packages/ui/certd-client/src/views/crud/form/group/crud.jsx new file mode 100644 index 000000000..8016d35e4 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/group/crud.jsx @@ -0,0 +1,143 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + title: { + title: "商品标题", + type: "text" + }, + code: { + title: "商品代码", + search: { show: true }, + type: "text" + }, + images: { + title: "图片", + type: "image-uploader" + }, + price: { + title: "价格", + sortable: true + }, + store: { + title: "库存", + type: "number" + }, + intro: { + title: "简介", + type: "textarea", + column: { + ellipsis: true, + showTitle: true + } + }, + content: { + title: "详情", + type: "editor-ueditor", + form: { + labelWidth: "0px" + } + }, + slotField: { + title: "插槽示例", + type: "text" + }, + product: { + title: "未分组字段", + type: "text", + form: { + col: { span: 24 }, + helper: "未分组的字段会显示在这里,一般来说你应该把所有字段都编入分组内" + } + }, + hidden1: { + title: "隐藏1", + type: "text", + form: { + show: false + }, + column: { + show: false + } + }, + hidden2: { + title: "隐藏2", + type: "text", + form: { + show: false + }, + column: { + show: false + } + } + }, + form: { + group: { + type: "collapse", // tab + accordion: true, //手风琴模式 + groups: { + base: { + slots: { + //自定义header + header: () => { + return ( + + 商品基础 + + + ); + } + }, + columns: ["code", "title", "images", "invalidKey"] + }, + price: { + header: "库存价格", + columns: ["store", "price"] + }, + info: { + header: "详情", + collapsed: true, //默认折叠 + columns: ["intro", "content", "slotField"] + }, + hiddenTest: { + header: "分组隐藏测试", //如果组里面的所有的组件都配置了隐藏,则本分组隐藏 + columns: ["hidden1", "hidden2"] + } + // custom: { + // title: "自定义", + // collapsed: false, + // show(context) { + // console.log("custom context", context); + // return context.mode === "view"; + // }, + // disabled: false, + // icon: "el-icon-warning-outline", + // columns: ["custom", "custom2"] + // } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/group/index.vue b/packages/ui/certd-client/src/views/crud/form/group/index.vue new file mode 100644 index 000000000..14e290c72 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/group/index.vue @@ -0,0 +1,46 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/group/mock.js b/packages/ui/certd-client/src/views/crud/form/group/mock.js new file mode 100644 index 000000000..70d8f4d4d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/group/mock.js @@ -0,0 +1,303 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormGroup", + idGenerator: 0 +}; +const list = [ + { + title: "无线充电宝", + code: "100001", + images: "https://img0.bdstatic.com/static/searchdetail/img/logo-2X_0c4ef02.png", + price: 100, + stock: 99, + intro: "30000毫安超大容量移动电源充电宝官方原装正品专用便携", + content: "" + } +]; + +const dictData = [ + { + value: "zhinan", + label: "指南", + children: [ + { + value: "shejiyuanze", + label: "设计原则", + children: [ + { + value: "yizhi", + label: "一致" + }, + { + value: "fankui", + label: "反馈" + }, + { + value: "xiaolv", + label: "效率" + }, + { + value: "kekong", + label: "可控" + } + ] + }, + { + value: "daohang", + label: "导航", + children: [ + { + value: "cexiangdaohang", + label: "侧向导航" + }, + { + value: "dingbudaohang", + label: "顶部导航" + } + ] + } + ] + }, + { + value: "zujian", + label: "组件", + children: [ + { + value: "basic", + label: "Basic", + children: [ + { + value: "layout", + label: "Layout 布局" + }, + { + value: "color", + label: "Color 色彩" + }, + { + value: "typography", + label: "Typography 字体" + }, + { + value: "icon", + label: "Icon 图标" + }, + { + value: "button", + label: "Button 按钮" + } + ] + }, + { + value: "form", + label: "Form", + children: [ + { + value: "radio", + label: "Radio 单选框" + }, + { + value: "checkbox", + label: "Checkbox 多选框" + }, + { + value: "input", + label: "Input 输入框" + }, + { + value: "input-number", + label: "InputNumber 计数器" + }, + { + value: "select", + label: "Select 选择器" + }, + { + value: "cascader", + label: "Cascader 级联选择器" + }, + { + value: "switch", + label: "Switch 开关" + }, + { + value: "slider", + label: "Slider 滑块" + }, + { + value: "time-picker", + label: "TimePicker 时间选择器" + }, + { + value: "date-picker", + label: "DatePicker 日期选择器" + }, + { + value: "datetime-picker", + label: "DateTimePicker 日期时间选择器" + }, + { + value: "upload", + label: "Upload 上传" + }, + { + value: "rate", + label: "Rate 评分" + }, + { + value: "form", + label: "Form 表单" + } + ] + }, + { + value: "data", + label: "Data", + children: [ + { + value: "table", + label: "Table 表格" + }, + { + value: "tag", + label: "Tag 标签" + }, + { + value: "progress", + label: "Progress 进度条" + }, + { + value: "tree", + label: "Tree 树形控件" + }, + { + value: "pagination", + label: "Pagination 分页" + }, + { + value: "badge", + label: "Badge 标记" + } + ] + }, + { + value: "notice", + label: "Notice", + children: [ + { + value: "alert", + label: "Alert 警告" + }, + { + value: "loading", + label: "Loading 加载" + }, + { + value: "message", + label: "Message 消息提示" + }, + { + value: "message-box", + label: "MessageBox 弹框" + }, + { + value: "notification", + label: "Notification 通知" + } + ] + }, + { + value: "navigation", + label: "Navigation", + children: [ + { + value: "menu", + label: "NavMenu 导航菜单" + }, + { + value: "tabs", + label: "Tabs 标签页" + }, + { + value: "breadcrumb", + label: "Breadcrumb 面包屑" + }, + { + value: "dropdown", + label: "Dropdown 下拉菜单" + }, + { + value: "steps", + label: "Steps 步骤条" + } + ] + }, + { + value: "others", + label: "Others", + children: [ + { + value: "dialog", + label: "Dialog 对话框" + }, + { + value: "tooltip", + label: "Tooltip 文字提示" + }, + { + value: "popover", + label: "Popover 弹出框" + }, + { + value: "card", + label: "Card 卡片" + }, + { + value: "carousel", + label: "Carousel 走马灯" + }, + { + value: "collapse", + label: "Collapse 折叠面板" + } + ] + } + ] + }, + { + value: "ziyuan", + label: "资源", + children: [ + { + value: "axure", + label: "Axure Components" + }, + { + value: "sketch", + label: "Sketch Templates" + }, + { + value: "jiaohu", + label: "组件交互文档" + } + ] + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); +mock.push({ + path: "/select/cascadeData", + method: "get", + handle(req) { + return { + code: 0, + msg: "success", + data: dictData + }; + } +}); + +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/helper/api.js b/packages/ui/certd-client/src/views/crud/form/helper/api.js new file mode 100644 index 000000000..aa8f08902 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/helper/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormHelper"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/helper/crud.jsx b/packages/ui/certd-client/src/views/crud/form/helper/crud.jsx new file mode 100644 index 000000000..e9946e84f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/helper/crud.jsx @@ -0,0 +1,72 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + labelCol: { span: 6 }, + wrapperCol: { span: 16 }, + helper: { + // position: "label" // helper的展示位置全局配置 + // tooltip:{} + } + }, + columns: { + name: { + title: "最简单", + type: "text", + form: { + helper: "最简单的helper\n换行测试" + } + }, + age: { + title: "jsx", + type: "text", + form: { + helper: { + render() { + return
jsx自定义render
; + } + } + } + }, + status: { + title: "显示在label", + type: "text", + form: { + rules: [{ required: true, message: "此项必填" }], + helper: { + position: "label", + tooltip: { + placement: "topLeft" + }, + text: "在label通过tooltip方式显示的helper\n换行测试" + // render() { + // return
在label通过tooltip方式显示的helper
; + // } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/helper/index.vue b/packages/ui/certd-client/src/views/crud/form/helper/index.vue new file mode 100644 index 000000000..440574488 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/helper/index.vue @@ -0,0 +1,48 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/helper/mock.js b/packages/ui/certd-client/src/views/crud/form/helper/mock.js new file mode 100644 index 000000000..92b3dacaa --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/helper/mock.js @@ -0,0 +1,310 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormHelper", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + age: 15, + password: "", + status: "2", + url: "https://baidu.com" + }, + { + name: "张三", + age: 18, + password: "", + url: "https://baidu.com" + }, + { + status: "2" + } +]; + +const dictData = [ + { + value: "zhinan", + label: "指南", + children: [ + { + value: "shejiyuanze", + label: "设计原则", + children: [ + { + value: "yizhi", + label: "一致" + }, + { + value: "fankui", + label: "反馈" + }, + { + value: "xiaolv", + label: "效率" + }, + { + value: "kekong", + label: "可控" + } + ] + }, + { + value: "daohang", + label: "导航", + children: [ + { + value: "cexiangdaohang", + label: "侧向导航" + }, + { + value: "dingbudaohang", + label: "顶部导航" + } + ] + } + ] + }, + { + value: "zujian", + label: "组件", + children: [ + { + value: "basic", + label: "Basic", + children: [ + { + value: "layout", + label: "Layout 布局" + }, + { + value: "color", + label: "Color 色彩" + }, + { + value: "typography", + label: "Typography 字体" + }, + { + value: "icon", + label: "Icon 图标" + }, + { + value: "button", + label: "Button 按钮" + } + ] + }, + { + value: "form", + label: "Form", + children: [ + { + value: "radio", + label: "Radio 单选框" + }, + { + value: "checkbox", + label: "Checkbox 多选框" + }, + { + value: "input", + label: "Input 输入框" + }, + { + value: "input-number", + label: "InputNumber 计数器" + }, + { + value: "select", + label: "Select 选择器" + }, + { + value: "cascader", + label: "Cascader 级联选择器" + }, + { + value: "switch", + label: "Switch 开关" + }, + { + value: "slider", + label: "Slider 滑块" + }, + { + value: "time-picker", + label: "TimePicker 时间选择器" + }, + { + value: "date-picker", + label: "DatePicker 日期选择器" + }, + { + value: "datetime-picker", + label: "DateTimePicker 日期时间选择器" + }, + { + value: "upload", + label: "Upload 上传" + }, + { + value: "rate", + label: "Rate 评分" + }, + { + value: "form", + label: "Form 表单" + } + ] + }, + { + value: "data", + label: "Data", + children: [ + { + value: "table", + label: "Table 表格" + }, + { + value: "tag", + label: "Tag 标签" + }, + { + value: "progress", + label: "Progress 进度条" + }, + { + value: "tree", + label: "Tree 树形控件" + }, + { + value: "pagination", + label: "Pagination 分页" + }, + { + value: "badge", + label: "Badge 标记" + } + ] + }, + { + value: "notice", + label: "Notice", + children: [ + { + value: "alert", + label: "Alert 警告" + }, + { + value: "loading", + label: "Loading 加载" + }, + { + value: "message", + label: "Message 消息提示" + }, + { + value: "message-box", + label: "MessageBox 弹框" + }, + { + value: "notification", + label: "Notification 通知" + } + ] + }, + { + value: "navigation", + label: "Navigation", + children: [ + { + value: "menu", + label: "NavMenu 导航菜单" + }, + { + value: "tabs", + label: "Tabs 标签页" + }, + { + value: "breadcrumb", + label: "Breadcrumb 面包屑" + }, + { + value: "dropdown", + label: "Dropdown 下拉菜单" + }, + { + value: "steps", + label: "Steps 步骤条" + } + ] + }, + { + value: "others", + label: "Others", + children: [ + { + value: "dialog", + label: "Dialog 对话框" + }, + { + value: "tooltip", + label: "Tooltip 文字提示" + }, + { + value: "popover", + label: "Popover 弹出框" + }, + { + value: "card", + label: "Card 卡片" + }, + { + value: "carousel", + label: "Carousel 走马灯" + }, + { + value: "collapse", + label: "Collapse 折叠面板" + } + ] + } + ] + }, + { + value: "ziyuan", + label: "资源", + children: [ + { + value: "axure", + label: "Axure Components" + }, + { + value: "sketch", + label: "Sketch Templates" + }, + { + value: "jiaohu", + label: "组件交互文档" + } + ] + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); +mock.push({ + path: "/select/cascadeData", + method: "get", + handle(req) { + return { + code: 0, + msg: "success", + data: dictData + }; + } +}); + +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/independent/crud.jsx b/packages/ui/certd-client/src/views/crud/form/independent/crud.jsx new file mode 100644 index 000000000..7e492d640 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/independent/crud.jsx @@ -0,0 +1,59 @@ +export default function ({ expose }) { + return { + crudOptions: { + form: { + wrapper: { + onClosed(e) { + console.log("onClosed", e); + }, + onOpened(e) { + console.log("onOpened", e); + } + }, + labelCol: { span: 6 }, + wrapperCol: { span: 16 }, + helper: { + // position: "label" // helper的展示位置全局配置 + // tooltip:{} + } + }, + columns: { + name: { + title: "最简单", + type: "text", + form: { + helper: "最简单的helper" + } + }, + age: { + title: "jsx", + type: "text", + form: { + helper: { + render() { + return
jsx自定义render
; + } + } + } + }, + status: { + title: "显示在label", + type: "text", + form: { + rules: [{ required: true, message: "此项必填" }], + helper: { + position: "label", + tooltip: { + placement: "topLeft" + }, + text: "在label通过tooltip方式显示的helper" + // render() { + // return
在label通过tooltip方式显示的helper
; + // } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/independent/index.vue b/packages/ui/certd-client/src/views/crud/form/independent/index.vue new file mode 100644 index 000000000..633904440 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/independent/index.vue @@ -0,0 +1,215 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/inner/api.js b/packages/ui/certd-client/src/views/crud/form/inner/api.js new file mode 100644 index 000000000..d8c3543c8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/inner/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormInner"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/inner/area/api.js b/packages/ui/certd-client/src/views/crud/form/inner/area/api.js new file mode 100644 index 000000000..b9142c890 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/inner/area/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormInnerArea"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function GetAll(query) { + return request({ + url: apiPrefix + "/all", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/inner/area/crud.jsx b/packages/ui/certd-client/src/views/crud/form/inner/area/crud.jsx new file mode 100644 index 000000000..dbd13b803 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/inner/area/crud.jsx @@ -0,0 +1,39 @@ +import * as api from "./api"; +import { dict, useExpose } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + wrapper: { + inner: true + } + }, + columns: { + area: { + title: "地区", + type: "text" + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/inner/area/index.vue b/packages/ui/certd-client/src/views/crud/form/inner/area/index.vue new file mode 100644 index 000000000..ab81c09b0 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/inner/area/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/inner/area/mock.js b/packages/ui/certd-client/src/views/crud/form/inner/area/mock.js new file mode 100644 index 000000000..d7c076ef2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/inner/area/mock.js @@ -0,0 +1,24 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormInnerArea", + idGenerator: 0 +}; +const list = [ + { + area: "深圳" + }, + { + area: "北京" + }, + { + area: "上海" + }, + { + area: "广州" + } +]; +options.list = list; +options.copyTimes = 1; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/inner/crud.jsx b/packages/ui/certd-client/src/views/crud/form/inner/crud.jsx new file mode 100644 index 000000000..c7009bf7f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/inner/crud.jsx @@ -0,0 +1,77 @@ +import * as api from "./api"; +import { dict, useExpose } from "@fast-crud/fast-crud"; +import { useRouter } from "vue-router"; +import { message } from "ant-design-vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + const router = useRouter(); + const areaDict = dict({ + value: "id", + label: "area", + url: "/mock/FormInnerArea/all" + }); + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + wrapper: { + inner: true + } + }, + columns: { + name: { + title: "姓名", + type: "text" + }, + age: { + title: "年龄", + type: "text" + }, + area: { + title: "地区", + type: "dict-select", + dict: areaDict, + form: { + suffixRender() { + function refresh() { + message.info("刷新dict"); + areaDict.reloadDict(); + } + function gotoAddArea() { + message.info("调用 router.push 打开地区管理页面"); + router.push({ path: "/crud/form/inner/area" }); + } + return ( + + + + + 添加地区 + + ); + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/inner/index.vue b/packages/ui/certd-client/src/views/crud/form/inner/index.vue new file mode 100644 index 000000000..ab81c09b0 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/inner/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/inner/mock.js b/packages/ui/certd-client/src/views/crud/form/inner/mock.js new file mode 100644 index 000000000..a9f809c81 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/inner/mock.js @@ -0,0 +1,25 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormInner", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + age: 15, + password: "", + status: "2", + url: "https://baidu.com" + }, + { + name: "张三", + age: 18, + password: "", + url: "https://baidu.com" + } +]; +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/layout-flex/api.js b/packages/ui/certd-client/src/views/crud/form/layout-flex/api.js new file mode 100644 index 000000000..0ba39afe2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout-flex/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/formLayoutFlex"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/layout-flex/crud.jsx b/packages/ui/certd-client/src/views/crud/form/layout-flex/crud.jsx new file mode 100644 index 000000000..1e7c5c383 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout-flex/crud.jsx @@ -0,0 +1,63 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + display: "flex", + labelCol: { + //固定label宽度 + span: null, + style: { + width: "120px" + } + }, + wrapperCol: { + span: null + } + }, + columns: { + name: { + title: "姓名", + type: "text", + search: { show: true } + }, + order: { + title: "字段排序", + type: "text", + form: { + order: 0 + } + }, + intro: { + title: "跨列", + search: { show: true }, + type: ["textarea"], + form: { + col: { span: 24 } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/layout-flex/index.vue b/packages/ui/certd-client/src/views/crud/form/layout-flex/index.vue new file mode 100644 index 000000000..e43bc8cf1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout-flex/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/layout-flex/mock.js b/packages/ui/certd-client/src/views/crud/form/layout-flex/mock.js new file mode 100644 index 000000000..e48c1f0fd --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout-flex/mock.js @@ -0,0 +1,35 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "formLayoutFlex", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + date: "2016-05-02", + status: "0", + province: "1", + avatar: "https://alicdn.antdv.com/vue.png", + show: true, + city: "sz", + address: "123123", + zip: "518000", + order: "我在编辑的时候会排到第一个", + intro: "王小虎是element-plus的table示例出现的名字" + }, + { + name: "张三", + date: "2016-05-04", + status: "1", + province: "2" + }, + { + name: "李四", + date: 2232433534511, + status: "1", + province: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/layout-grid/api.js b/packages/ui/certd-client/src/views/crud/form/layout-grid/api.js new file mode 100644 index 000000000..c00a57873 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout-grid/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/formLayoutGrid"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/layout-grid/crud.jsx b/packages/ui/certd-client/src/views/crud/form/layout-grid/crud.jsx new file mode 100644 index 000000000..284f7b9f8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout-grid/crud.jsx @@ -0,0 +1,67 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + // 具体可配置请参考 grid 布局: http://www.ruanyifeng.com/blog/2019/03/grid-layout-tutorial.html + display: "grid" + }, + columns: { + avatar: { + title: "头像上传", + type: "avatar-uploader", + form: { + order: 0, + col: { + style: { gridRow: "span 3" } + }, + helper: "通过grid布局,可以实现比flex更加规整的排列" + } + }, + name: { + title: "姓名", + type: "text", + search: { show: true } + }, + order: { + title: "占位演示", + type: "text" + }, + place: { + title: "占位演示", + type: "text" + }, + intro: { + title: "跨列", + type: "textarea", + form: { + col: { + style: { gridColumn: "span 2" } // grid 模式控制跨列 + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/layout-grid/index.vue b/packages/ui/certd-client/src/views/crud/form/layout-grid/index.vue new file mode 100644 index 000000000..499e5fe7d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout-grid/index.vue @@ -0,0 +1,52 @@ + + + + diff --git a/packages/ui/certd-client/src/views/crud/form/layout-grid/mock.js b/packages/ui/certd-client/src/views/crud/form/layout-grid/mock.js new file mode 100644 index 000000000..41ec78880 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout-grid/mock.js @@ -0,0 +1,34 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "formLayoutGrid", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + date: "2016-05-02", + status: "0", + province: "1", + avatar: "https://alicdn.antdv.com/vue.png", + show: true, + city: "sz", + address: "123123", + zip: "518000", + order: "我在编辑的时候会排到第一个", + intro: "王小虎是element-plus的table示例出现的名字" + }, + { + name: "张三", + date: "2016-05-04", + status: "1" + }, + { + name: "李四", + date: 2232433534511, + status: "1", + province: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/layout/api.js b/packages/ui/certd-client/src/views/crud/form/layout/api.js new file mode 100644 index 000000000..d9a789418 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/formLayout"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/layout/crud.jsx b/packages/ui/certd-client/src/views/crud/form/layout/crud.jsx new file mode 100644 index 000000000..fabaec8ee --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout/crud.jsx @@ -0,0 +1,101 @@ +import * as api from "./api"; +import { dict, compute } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + + const { getFormData, getFormWrapperRef } = expose; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + /** + * flex模式,通过 + * grid模式 + */ + display: "flex", + wrapper: { + customClass: "page-layout", + onOpened(context) { + getFormWrapperRef().formOptions.display = context.options.initial?.display; + console.log("form opened", context, getFormData()); + } + } + }, + columns: { + display: { + title: "布局", + type: "dict-radio", + dict: dict({ + data: [ + { value: "flex", label: "flex", color: "blue" }, + { value: "grid", label: "grid", color: "green" } + ] + }), + search: { show: true, valueChange: null }, + form: { + valueChange(context) { + const { value } = context; + getFormWrapperRef().formOptions.display = value; + console.log("valueChange", value, context); + } + } + }, + name: { + title: "姓名", + type: "text", + search: { show: true } + }, + zip: { + title: "邮编", + type: "text" + }, + blank: { + title: "表单占位栏", + type: "text", + form: { + blank: true + } + }, + gridSpan: { + title: "grid跨列", + type: "textarea", + form: { + col: { + style: { gridColumn: "span 2" } // grid 模式 + } + } + }, + flexSpan: { + title: "flex跨列", + type: "textarea", + search: { show: false }, + form: { + show: compute((context) => { + // grid跨列模式下使用flex模式的设置会显示异常,为了演示效果,在grid模式下隐藏 + return context.form.display !== "grid"; + }), + col: { span: 24 } // flex模式跨列配置 + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/layout/index.vue b/packages/ui/certd-client/src/views/crud/form/layout/index.vue new file mode 100644 index 000000000..1a969b414 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout/index.vue @@ -0,0 +1,50 @@ + + + + diff --git a/packages/ui/certd-client/src/views/crud/form/layout/mock.js b/packages/ui/certd-client/src/views/crud/form/layout/mock.js new file mode 100644 index 000000000..f63208a5a --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/layout/mock.js @@ -0,0 +1,41 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "formLayout", + idGenerator: 0 +}; +const list = [ + { + display: "flex", + name: "aa", + date: "2016-05-02", + status: "0", + province: "1", + avatar: "https://alicdn.antdv.com/vue.png", + show: true, + city: "sz", + address: "123123", + zip: "518000" + }, + { + display: "grid", + name: "bb", + date: "2016-05-04", + status: "1", + province: "2" + }, + { + name: "cc", + date: 2232433534511, + status: "1", + province: "0" + }, + { + name: "dd", + date: "2016-05-03", + status: "2", + province: "wh,gz" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/nest/api.js b/packages/ui/certd-client/src/views/crud/form/nest/api.js new file mode 100644 index 000000000..f7d01faa7 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/nest/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormNestObject"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/nest/crud.jsx b/packages/ui/certd-client/src/views/crud/form/nest/crud.jsx new file mode 100644 index 000000000..1bf0a55af --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/nest/crud.jsx @@ -0,0 +1,76 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + labelCol: { span: 6 }, + wrapperCol: { span: 16 }, + helper: { + // position: "label" // helper的展示位置全局配置 + // tooltip:{} + } + }, + columns: { + username: { + title: "用户名", + type: "text" + }, + "profile.name": { + title: "profile.name", + type: "text", + search:{show:true}, + form: { + key: ["profile", "name"], + rules: [{ required: true, message: "姓名必填" }] + } + }, + "profile.age": { + title: "profile.age", + type: "number", + form: { + key: ["profile", "age"] + } + }, + "profile.status": { + title: "profile.status", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }), + form: { + key: ["profile", "status"] + } + }, + "profile.count": { + title: "不提交的字段", + type: "text", + form: { + submit: false, + key: ["profile", "count"] + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/nest/index.vue b/packages/ui/certd-client/src/views/crud/form/nest/index.vue new file mode 100644 index 000000000..5e4c48c99 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/nest/index.vue @@ -0,0 +1,48 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/nest/mock.js b/packages/ui/certd-client/src/views/crud/form/nest/mock.js new file mode 100644 index 000000000..1658d74a1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/nest/mock.js @@ -0,0 +1,319 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormNestObject", + idGenerator: 0 +}; +const list = [ + { + username: "wangxiaohu", + profile: { + name: "王小虎", + age: 15, + status: "1", + count: "配置submit=false将不会提交给后台" + } + }, + { + username: "zhangsan", + profile: { + name: "张三", + age: 18, + status: "2" + } + }, + { + username: "lisi", + profile: { + name: "李四", + age: 18, + status: "2" + } + } +]; + +const dictData = [ + { + value: "zhinan", + label: "指南", + children: [ + { + value: "shejiyuanze", + label: "设计原则", + children: [ + { + value: "yizhi", + label: "一致" + }, + { + value: "fankui", + label: "反馈" + }, + { + value: "xiaolv", + label: "效率" + }, + { + value: "kekong", + label: "可控" + } + ] + }, + { + value: "daohang", + label: "导航", + children: [ + { + value: "cexiangdaohang", + label: "侧向导航" + }, + { + value: "dingbudaohang", + label: "顶部导航" + } + ] + } + ] + }, + { + value: "zujian", + label: "组件", + children: [ + { + value: "basic", + label: "Basic", + children: [ + { + value: "layout", + label: "Layout 布局" + }, + { + value: "color", + label: "Color 色彩" + }, + { + value: "typography", + label: "Typography 字体" + }, + { + value: "icon", + label: "Icon 图标" + }, + { + value: "button", + label: "Button 按钮" + } + ] + }, + { + value: "form", + label: "Form", + children: [ + { + value: "radio", + label: "Radio 单选框" + }, + { + value: "checkbox", + label: "Checkbox 多选框" + }, + { + value: "input", + label: "Input 输入框" + }, + { + value: "input-number", + label: "InputNumber 计数器" + }, + { + value: "select", + label: "Select 选择器" + }, + { + value: "cascader", + label: "Cascader 级联选择器" + }, + { + value: "switch", + label: "Switch 开关" + }, + { + value: "slider", + label: "Slider 滑块" + }, + { + value: "time-picker", + label: "TimePicker 时间选择器" + }, + { + value: "date-picker", + label: "DatePicker 日期选择器" + }, + { + value: "datetime-picker", + label: "DateTimePicker 日期时间选择器" + }, + { + value: "upload", + label: "Upload 上传" + }, + { + value: "rate", + label: "Rate 评分" + }, + { + value: "form", + label: "Form 表单" + } + ] + }, + { + value: "data", + label: "Data", + children: [ + { + value: "table", + label: "Table 表格" + }, + { + value: "tag", + label: "Tag 标签" + }, + { + value: "progress", + label: "Progress 进度条" + }, + { + value: "tree", + label: "Tree 树形控件" + }, + { + value: "pagination", + label: "Pagination 分页" + }, + { + value: "badge", + label: "Badge 标记" + } + ] + }, + { + value: "notice", + label: "Notice", + children: [ + { + value: "alert", + label: "Alert 警告" + }, + { + value: "loading", + label: "Loading 加载" + }, + { + value: "message", + label: "Message 消息提示" + }, + { + value: "message-box", + label: "MessageBox 弹框" + }, + { + value: "notification", + label: "Notification 通知" + } + ] + }, + { + value: "navigation", + label: "Navigation", + children: [ + { + value: "menu", + label: "NavMenu 导航菜单" + }, + { + value: "tabs", + label: "Tabs 标签页" + }, + { + value: "breadcrumb", + label: "Breadcrumb 面包屑" + }, + { + value: "dropdown", + label: "Dropdown 下拉菜单" + }, + { + value: "steps", + label: "Steps 步骤条" + } + ] + }, + { + value: "others", + label: "Others", + children: [ + { + value: "dialog", + label: "Dialog 对话框" + }, + { + value: "tooltip", + label: "Tooltip 文字提示" + }, + { + value: "popover", + label: "Popover 弹出框" + }, + { + value: "card", + label: "Card 卡片" + }, + { + value: "carousel", + label: "Carousel 走马灯" + }, + { + value: "collapse", + label: "Collapse 折叠面板" + } + ] + } + ] + }, + { + value: "ziyuan", + label: "资源", + children: [ + { + value: "axure", + label: "Axure Components" + }, + { + value: "sketch", + label: "Sketch Templates" + }, + { + value: "jiaohu", + label: "组件交互文档" + } + ] + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); +mock.push({ + path: "/select/cascadeData", + method: "get", + handle(req) { + return { + code: 0, + msg: "success", + data: dictData + }; + } +}); + +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/new-page/api.js b/packages/ui/certd-client/src/views/crud/form/new-page/api.js new file mode 100644 index 000000000..0dc2616a0 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/new-page/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormNewPage"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/get", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/new-page/crud.jsx b/packages/ui/certd-client/src/views/crud/form/new-page/crud.jsx new file mode 100644 index 000000000..c68282819 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/new-page/crud.jsx @@ -0,0 +1,96 @@ +import * as api from "./api"; +import { useRouter } from "vue-router"; +export default function ({ expose }) { + const router = useRouter(); + + const { getFormRef, getFormData } = expose; + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + if (row.id) { + form.id = row.id; + } + + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + actionbar: { + buttons: { + add: { + click() { + router.push("/crud/form/new-page/edit"); + } + } + } + }, + rowHandle: { + buttons: { + edit: { + click(context) { + router.push("/crud/form/new-page/edit?id=" + context.row.id); + } + } + } + }, + columns: { + title: { + title: "商品标题", + type: "text" + }, + code: { + title: "商品代码", + search: { show: true }, + type: "text" + }, + images: { + title: "图片", + type: "image-uploader" + }, + price: { + title: "价格", + sortable: true + }, + store: { + title: "库存", + type: "number" + }, + intro: { + title: "简介", + type: "textarea", + column: { + ellipsis: true + } + }, + content: { + title: "详情", + type: ["editor-wang", "colspan"], + form: { + itemProps: { labelWidth: "0px" } + } + }, + product: { + title: "未分组字段", + type: ["text", "colspan"], + form: { + helper: "未分组的字段会显示在这里,一般来说你应该把所有字段都编入分组内" + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/new-page/edit.vue b/packages/ui/certd-client/src/views/crud/form/new-page/edit.vue new file mode 100644 index 000000000..c93ef0bdd --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/new-page/edit.vue @@ -0,0 +1,81 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/new-page/index.vue b/packages/ui/certd-client/src/views/crud/form/new-page/index.vue new file mode 100644 index 000000000..0702f08c9 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/new-page/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/new-page/mock.js b/packages/ui/certd-client/src/views/crud/form/new-page/mock.js new file mode 100644 index 000000000..a182214c8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/new-page/mock.js @@ -0,0 +1,22 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormNewPage", + idGenerator: 0 +}; +const list = [ + { + title: "无线充电宝", + code: "100001", + images: "https://img0.bdstatic.com/static/searchdetail/img/logo-2X_0c4ef02.png", + price: 100, + stock: 99, + intro: "30000毫安超大容量移动电源充电宝官方原装正品专用便携", + content: "" + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/reset/api.js b/packages/ui/certd-client/src/views/crud/form/reset/api.js new file mode 100644 index 000000000..52a5ff5b0 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/reset/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/formReset"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/reset/crud.jsx b/packages/ui/certd-client/src/views/crud/form/reset/crud.jsx new file mode 100644 index 000000000..1a4f1df2f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/reset/crud.jsx @@ -0,0 +1,56 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + async doReset() { + console.log("reset之后可以执行其他操作"); + }, + wrapper: { + buttons: { + reset: { + text: "重置", + order: -1, + click(value) { + console.log("on reset", value); + expose.getFormRef().reset(); + } + } + } + } + }, + columns: { + name: { + title: "姓名", + type: "text", //虽然不写也能正确显示组件,但不建议省略它 + search: { show: true }, + form: { + component: { + maxlength: 20 + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/reset/index.vue b/packages/ui/certd-client/src/views/crud/form/reset/index.vue new file mode 100644 index 000000000..8c2e77e6d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/reset/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/reset/mock.js b/packages/ui/certd-client/src/views/crud/form/reset/mock.js new file mode 100644 index 000000000..f586e9c46 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/reset/mock.js @@ -0,0 +1,40 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "formReset", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + date: "2016-05-02", + status: "0", + province: "1", + avatar: "https://alicdn.antdv.com/vue.png", + show: true, + city: "sz", + address: "123123", + zip: "518000", + intro: "王小虎是element-plus的table示例出现的名字" + }, + { + name: "张三", + date: "2016-05-04", + status: "1", + province: "2" + }, + { + name: "李四", + date: 2232433534511, + status: "1", + province: "0" + }, + { + name: "王五", + date: "2016-05-03", + status: "2", + province: "wh,gz" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/form/validation/api.js b/packages/ui/certd-client/src/views/crud/form/validation/api.js new file mode 100644 index 000000000..f62ba48b1 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/validation/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/FormValidation"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/form/validation/crud.jsx b/packages/ui/certd-client/src/views/crud/form/validation/crud.jsx new file mode 100644 index 000000000..856ca779b --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/validation/crud.jsx @@ -0,0 +1,135 @@ +import * as api from "./api"; +import { dict, useExpose } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const { getFormRef, getFormData } = expose; + const validatePass1 = async (rule, value) => { + if (value === "") { + throw new Error("请输入密码"); + } + if (getFormData().password2 !== "") { + getFormRef().getFormRef().validateFields(["password2"]); + } + }; + const validatePass2 = async (rule, value) => { + if (value === "") { + throw new Error("请再次输入密码"); + } else if (value !== getFormData().password) { + throw new Error("两次输入密码不一致!"); + } + }; + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + row:{ + gutter:20 + }, + beforeSubmit(context) { + console.log("beforeSubmit", context); + }, + afterSubmit(context) { + console.log("afterSubmit", context); + } + }, + columns: { + name: { + title: "姓名", + type: "text", + form: { + helper: "添加和编辑时必填,编辑时额外需要校验长度", + rules: [{ required: true, message: "请输入姓名" }], + component: { + maxlength: 5, // 原生属性要写在这里 + props: { + type: "text", + showWordLimit: true + } + } + }, + editForm: { + rules: [{ min: 2, max: 5, message: "姓名长度为2-5" }] + } + }, + age: { + title: "年龄", + type: "text", + form: { + rules: [{ pattern: /^\d+$/, message: "必须为整数" }], + helper: "正则表达式" + } + }, + password: { + title: "密码", + type: "password", + column: { + component: { + cellRender() { + return ******; + } + } + }, + form: { + rules: [ + { required: true, message: "请输入密码" }, + { validator: validatePass1, trigger: "blur" } + ] + } + }, + password2: { + title: "确认密码", + type: "password", + column: { show: false }, + form: { + rules: [ + { required: true, message: "请输入确认密码" }, + { validator: validatePass2, trigger: "blur" } + ] + } + }, + status: { + title: "必选", + type: "dict-select", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum" + }), + form: { + rules: [{ required: true, message: "请选择一个选项" }] + } + }, + email: { + title: "邮箱", + type: "text", + form: { + rules: [{ type: "email", message: "请填写正确的邮箱" }] + } + }, + url: { + title: "URL", + type: "text", + form: { + rules: [{ type: "url", message: "请填写正确的url" }] + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/form/validation/index.vue b/packages/ui/certd-client/src/views/crud/form/validation/index.vue new file mode 100644 index 000000000..49bd373a8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/validation/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/form/validation/mock.js b/packages/ui/certd-client/src/views/crud/form/validation/mock.js new file mode 100644 index 000000000..0ca7bc117 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/form/validation/mock.js @@ -0,0 +1,310 @@ +import mockUtil from "/src/mock/base"; + +const options = { + name: "FormValidation", + idGenerator: 0 +}; +const list = [ + { + name: "王小虎", + age: 15, + password: "", + status: "2", + url: "https://baidu.com" + }, + { + name: "张三", + age: 18, + password: "", + url: "https://baidu.com" + }, + { + status: "2" + } +]; + +const dictData = [ + { + value: "zhinan", + label: "指南", + children: [ + { + value: "shejiyuanze", + label: "设计原则", + children: [ + { + value: "yizhi", + label: "一致" + }, + { + value: "fankui", + label: "反馈" + }, + { + value: "xiaolv", + label: "效率" + }, + { + value: "kekong", + label: "可控" + } + ] + }, + { + value: "daohang", + label: "导航", + children: [ + { + value: "cexiangdaohang", + label: "侧向导航" + }, + { + value: "dingbudaohang", + label: "顶部导航" + } + ] + } + ] + }, + { + value: "zujian", + label: "组件", + children: [ + { + value: "basic", + label: "Basic", + children: [ + { + value: "layout", + label: "Layout 布局" + }, + { + value: "color", + label: "Color 色彩" + }, + { + value: "typography", + label: "Typography 字体" + }, + { + value: "icon", + label: "Icon 图标" + }, + { + value: "button", + label: "Button 按钮" + } + ] + }, + { + value: "form", + label: "Form", + children: [ + { + value: "radio", + label: "Radio 单选框" + }, + { + value: "checkbox", + label: "Checkbox 多选框" + }, + { + value: "input", + label: "Input 输入框" + }, + { + value: "input-number", + label: "InputNumber 计数器" + }, + { + value: "select", + label: "Select 选择器" + }, + { + value: "cascader", + label: "Cascader 级联选择器" + }, + { + value: "switch", + label: "Switch 开关" + }, + { + value: "slider", + label: "Slider 滑块" + }, + { + value: "time-picker", + label: "TimePicker 时间选择器" + }, + { + value: "date-picker", + label: "DatePicker 日期选择器" + }, + { + value: "datetime-picker", + label: "DateTimePicker 日期时间选择器" + }, + { + value: "upload", + label: "Upload 上传" + }, + { + value: "rate", + label: "Rate 评分" + }, + { + value: "form", + label: "Form 表单" + } + ] + }, + { + value: "data", + label: "Data", + children: [ + { + value: "table", + label: "Table 表格" + }, + { + value: "tag", + label: "Tag 标签" + }, + { + value: "progress", + label: "Progress 进度条" + }, + { + value: "tree", + label: "Tree 树形控件" + }, + { + value: "pagination", + label: "Pagination 分页" + }, + { + value: "badge", + label: "Badge 标记" + } + ] + }, + { + value: "notice", + label: "Notice", + children: [ + { + value: "alert", + label: "Alert 警告" + }, + { + value: "loading", + label: "Loading 加载" + }, + { + value: "message", + label: "Message 消息提示" + }, + { + value: "message-box", + label: "MessageBox 弹框" + }, + { + value: "notification", + label: "Notification 通知" + } + ] + }, + { + value: "navigation", + label: "Navigation", + children: [ + { + value: "menu", + label: "NavMenu 导航菜单" + }, + { + value: "tabs", + label: "Tabs 标签页" + }, + { + value: "breadcrumb", + label: "Breadcrumb 面包屑" + }, + { + value: "dropdown", + label: "Dropdown 下拉菜单" + }, + { + value: "steps", + label: "Steps 步骤条" + } + ] + }, + { + value: "others", + label: "Others", + children: [ + { + value: "dialog", + label: "Dialog 对话框" + }, + { + value: "tooltip", + label: "Tooltip 文字提示" + }, + { + value: "popover", + label: "Popover 弹出框" + }, + { + value: "card", + label: "Card 卡片" + }, + { + value: "carousel", + label: "Carousel 走马灯" + }, + { + value: "collapse", + label: "Collapse 折叠面板" + } + ] + } + ] + }, + { + value: "ziyuan", + label: "资源", + children: [ + { + value: "axure", + label: "Axure Components" + }, + { + value: "sketch", + label: "Sketch Templates" + }, + { + value: "jiaohu", + label: "组件交互文档" + } + ] + } +]; + +options.list = list; +options.copyTimes = 1000; +const mock = mockUtil.buildMock(options); +mock.push({ + path: "/select/cascadeData", + method: "get", + handle(req) { + return { + code: 0, + msg: "success", + data: dictData + }; + } +}); + +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/home/index.vue b/packages/ui/certd-client/src/views/crud/home/index.vue new file mode 100644 index 000000000..274457799 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/home/index.vue @@ -0,0 +1,15 @@ + + + + diff --git a/packages/ui/certd-client/src/views/crud/home/page-cover/helper.js b/packages/ui/certd-client/src/views/crud/home/page-cover/helper.js new file mode 100644 index 000000000..5c955ba6e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/home/page-cover/helper.js @@ -0,0 +1,31 @@ +export default { + crud: ` columns: [ + date:{ + title: '日期', //字段名称 + type: 'date', //字段类型,添加、修改、查询将自动生成相应表单组件 + }, + status: { + title: '状态', + type: 'dict-select', //选择框,默认单选 + dict: dict({ url: '/dicts/OpenStatusEnum' })//远程数据字典 + }, + province: { + title: '地区', + type: 'dict-select', //选择框 + form: { //表单组件自定义配置,此处配置选择框为多选 + component: { //支持任何v-model组件 + filterable: true, multiple: true, clearable: true + } + }, + dict: dict({ + data: [ //本地数据字典 + { value: 'sz', label: '深圳' }, + { value: 'gz', label: '广州' }, + { value: 'wh', label: '武汉' }, + { value: 'sh', label: '上海' } + ] + }) + } + ] + ` +}; diff --git a/packages/ui/certd-client/src/views/crud/home/page-cover/image/crud.png b/packages/ui/certd-client/src/views/crud/home/page-cover/image/crud.png new file mode 100644 index 0000000000000000000000000000000000000000..3dfb047dd4cec6a1bd79e999ae8e7370af54c9c2 GIT binary patch literal 202647 zcmd?Qbx>U0@-B=MGz16)m*DO$gWKSiK|=xzZUap45Q0UJV8IFQ?ixZ8f)4HyoMCVc zGH{3YJ?Grtt-61_=Z{O-*Xn+r?j5bCqeg^JjgN+gMx>#xY=DM_ zM~a4q{onx(>YK&$&L}iALKjCRB|Qx#B}P3jcY8+{J2W))=p<9zXNIOUa!?i~CcLL4 z=u|fR3g?NBzP)2)H1v4%3GgjSkSVHp)Bqbnp`YPktUM}(#>tgC1tM?qA#W%)a?*dF zTYSTgS%1y|yW5bwXulkWF1as2_B(LVGJ~^>(qeZo^cXo4SyjVp*z&>E>XvAs@|e#F zKKKQ;Bxc5u5abY^g`|j!f8kAZ=F_YUiMcZ!rF^gvh!&?o0>)dv!H6DbBk5N0ksIrEsKG+Z)F;6`QISFa-an)Em>g)h$`wVI(?R~?$-~2et2e@1?l+*G8yTx%0h6r) z(h83ni!@mJWk~X7h?YXx{*C_16eoe=)_Go=&vi zy`wBQ{4@lq(*;?Xbr#}20^^H6EaR*ye_`PQ4#hsU8(=0AJ>S4S{t{SI!7M1Q6ZVke z-KV_Cvu>ReztTT8d^3-~yv-E){xnfHimqRUkuX+oD@K+~)8Lbc407ySwep!o2J?{a z$jq9_w;PUvCHxose)^y6`LfI&P!^>zz>+E0SjyJEYkB#-B%3(;%?;rB{o3Oz2L4R# zSCKe^?|ZTzWe>CcSx66G6Rx8%d5k(l*yoq1vK*#iX?|rO!3zz1W%b#~=)t?3+qG(w zARqME(oyEg4>E^Xs~9Ah$15S>PRX`2=3IY>oW9>34u^ewxHc52EqQxzrZfkm7Mu9) zB=U|amM4=*>jjoz#uM!iEK;j%uLS6$dcQeyxX?ETm!JL$4*QzBL>WV`^JsaJnRW$l(zku+KlJX-I2o}MvoyIpv0?2)>O+?Ue$b; z+)W8pda367G;jUUx?XM1v@l%9`@n|6uA-|vR%)tz#UB!()_D%Q%)P>x=?!#^=KV=* z1FlJ!@hiu6#^TwxaOX<9p*5nNwRE*FOlCFY&zfT(7dI!g41(~z&Y|Zf zr=a8#rw6Bb7XmY$a@p3b%q#p_)%;VL_l(|+ejklvS#+m&+PQFt?u`I{;8YR?I?1|( zgv87-h!XMe>k3;lvP~XA`@1gI4gG7&kHGM9^@^S(N{m3w?XQ{kgoB}?9gL&JW0hPbKonX01XeCT@FS$nsYZ!HMR?GTeqACiO3rY57HYfzBk;z zaheHCbtzvF`*+g_yk>lW_l1e@`;!`g%Omi2%p;{Y@ef!iG?d5Ufh@T3jQF-w~|(_aj9`hgBqfF%DAG)4Aa)K zmRIsMtu93z_)|&(G%md_rO$F5{T=;}EK+5S!gT`*y;6>|MB&7M#Ob-YIg`2DxwVEL zb0-aD^$#7g$AgQJF{Qdbxn9Ky#rQiyJA6*uXT)csA64t}!pDRh!t3qo&E`1gFwNvM z6`%zWIL4KXzi3BjaMG<}Wckaoo~hJa!(77>i27EoPAng2S|=rMU_7(TyduEpBH1mQ zUI}K&Z&d|wFIl4RIPr&_YFznVJ-Wg`s$9*V>B8mhpA$OcM0SIDY zRIOc<+L7FeMTARy&fU9byF+k5;+q8QRe@*TQ4ra}_deV9KEi|{HZag%mHGoAZ655vU`%FnKf?f50_)FB#M1+=SDlz~s7O-`ubqSzcE${j3ReG52id zLtUz?A^7Ay@9Y%J#I|ktynN4GI-pIY&3%u0FKd=&7P6P*3Kuc!cs3!`$xfQAzmlG1JL;u-)YO`pz?r{fVyk$hwKkK)f#$zbko53TQV>< zSc=uKm15P9RZ|Dm(pr1hncKya%SA<5$*-6(zW&>vVzb$;TwNd-FZPP!OEK7BcgEQP2wfv zed0Uxm-Op$&GPU@1xLB_+&zby1KoI=pydZMk}q|8^LswBa{*>Jfqyr@DGks;v!Ed!5oTzl*XT$?KY!^DS7svxqbY?YZaqJ04d zv-#->d+e0Da`HcHHE$DN8u}C-7t;5$*cRID_uYJbzcqPyRoe`rsq??2y_LF!C=@^c zm}CBg#ey|3jV`f+>l&n^H@uVKrXN*oF!6LVN<7MXQ+3O&FDRWdyNTOfK%O@wB-BK~ zzFR-KlSS9dKkc%9)sfBs?3h{)@&9_-FdkqhSexmkEC*ULazK;c;pe0!U*@2vz@{{& zJcFrcdKN#cdS(#lSU5h&Dx96HoE#`mko_ZjQf%Jq{LSpct?Dh8^!I~9Y4c(*|D{|% z%agp#11kV1uIcxdLDQIVK?pCY3sI|9hghG=2>ok)2vv#y4fF>)Mg5=-~ye! zI%}RJa(?FAKYi2SyEAu^WR+w-r9Z_v0M6{v_)b#=xNt8<@UfNT7;V!b+Y`ZqE z{YKkVJkqUl`9)*Fi-GHk)rzJj6<6otnzE7wodv6UWM9k#9k}b_y3@C)Z}?p%160G; zXJ2O_sCBBT(Ki;Jwc+ECnOhlfkl$f?9)93l3N2Y~%x>vgMU%*pIJ|zCD!(A}1X8^h zxBTU*r!i8T-iDUb9YAd_b}IIcS~`6!T`DFg&S`_Dd4SGlM&N>2!`^-gUQuN>eMl=3IG75NSGo1x;kn+8hA3>A4dZ6W`*$BG zuBl|dCrWAOr@NfJ-g>$xw?~u`#4m1n)HSvsSh)6QGOmohD%|=SE_HGBgT_-_%B^sr zJ;-^iVtD?|JZyh`+3!+olphniwfuB}yQ?ao>o^o1mS#;CvbYQ%_uIAk)1wvpiGDF? zFy!V&{L-DFl|uH5Y)PQgQT=&pZ(Z{KuYK+=-J1?X%jqRRcb36F#3gX$aP=_zONiA@ z=E(e+!MWGr79$n$$TBoA}oN#{Uxp;Uy zAOHQkmrh_t8m(T(G+QA_@hv!_!b;OS2_(Z1$hva^r<4p{#I(aIg@+*SusdKe8C{vw zA}ILbV)snLURxWD3w8Yf4GWzb4I6cZj{3-<)BNXJ6`cbO^RMqQ(9j|s(Xjp{qk}r% z|HYv`_rLl3jFE%!ZwWlo9L#@TV?VeT^`f)nL!IzE)Ikt5Gz!-H54wf{%OM&X08K+# z!RQtG?jr6h^U2w-N7K?e8Sve`T{_ZF0vIAL{XBZxuVaNYsq82@5<4^ z_(unT0YN^S&jDQKAIhWJeTI$^EkQy8Ktsp+M+d+ImQ}`oERA;GI?g|8_^%H9zis{N zgZ$iotV@(`NYuXz{ulDLEUQoc3(Z7`t^GIRPrr`X)8hOyJGi$O0OO~=eWWw{1BaW^BiLC}M@a(p8pgKLm zf}!mxw^Unawx4ar$K7;7_9|g_0?RKxxP{YeBcBkyFsDo@`jWG%IBv)sKfk=Zoyxph zt2Z5BZy(_eU$7Dy>w8M#ejRgu-#$tQS{PWQKgN~>TxXm8+7c5J=S&(zW@9{3R5EL0we_{Q%E=sj^8d#5+3+8mrK9yYXR*HXFQ@i1rn;R6z^PC;1&no86Xr z;u*LvZuSxNLo8BQMJZAM`KlbSaY!VG zBV3{@$48Gu`dJEJmq5c zkV=UArquD2uLIkC)qL{PG-WloP#w?%wQ#2=6N6P1as)Ak1=nbmKF%ilJo>kDQhNznJ9lzlM*F^~vNlAot1a*~UhtQClh%TyTszEfl4FMiO51t&SzXgV-sA zp8FbhM$qbR-p}c;6o2*ai23ga9gXeVIuzCEW&|Y?1{KDl?g47?B@WJp143%0M%?c; z{IBPr^qGwHfaP6kM>idgU>&!^gZpRLpU4c`6NDfhbdYeMOE zqeE%_FWusM$i<*ys{2v?)q}qd@sCiGCloXB*DCuz?)-0=|Kkc^K|rETKHV1k^Ixmt zf4262NdD&r2^A`KVqR0{{lDM-zdZGO16ruEXUDa}KM()@e?5?VwX!p8Z0t{6HhJ~# z(>ngA+w`XcE_3ame6xJl`NmI~i_F`5JLWa@^+r>bmh5z1(lh7zg{1Pv*^~o+85pTj zPHwJZe}6xVtgNMwuyELZqFa;aI9s34y!ZYBsI;ui?r4>DtRnyl33em%mhI%;kqkcD z5v_Hec9UE^R0^UzSy0OD`o29wrshiY*137$e8=YD*ds3?&@mh@#4j@*1VXj-RXAxqbm8_ zkFJ~j5)9%e#J@NGvtRs=<&T_ObNlh z>Lc@$O{<2w^6blZ0^^llqu5&mZ}HZ4%rou&Hf5H#U$?A*2`N^=#&fB^7p@Pk14e?< zj)tJer@}Ceq8Pf3gNp^$yMSAj%NIM|RH9yQE&P5IC`_-lt*S8uF&h>IIf5TDh#9r| zAa>gDVa`~m7$<`aTLVjDLh#ADvh_3gY|c>Drw#+7_JK-D7skY}pn;c%I$f zDO+GXu>0H;EPKB{+hfrOuY0DRt?3H6Y?HKX?r>u^BGR1iUWAE-K&MhS+m5b-1Uk^O$zZ1Z&Le!E)LU@R!C z*_2x@P6R?8YJu#GN(&AChw4e9asU$;(PD1imV{$KoZENy(j(GaXT-p})X0n~-rGjv z;3e_R7sX`C6IIsq3d-H%HEj2L7U0c;#I$ndFNydGf(iqIY7CB;(VN5R?))!^X(bpF zch>)`1@9BHT;04c@%rf=LT4b`R~;X$%EimwWNFzHoUpslVy^n}iAy%WWv7{#`>Yjz zPl~YprKMbswL5&OA;Ge+H=OKMzR&(rhuT&8Mpi+4`!}3N@b(4bJsCdz(~sMgK8`=VIL;}G~A=Gil5>jE=sL9BU|9G`gUBE_`Ev<37%H5zVKl175d4h?zh zJ^=w?9a=*lO#?3WMPm!BV%vNV#JaxhCNe{M3_Lw+kmFlFq@lBCx3^7gyF&-Jzm8AD z|D-Q&`+cPPk~f-E_n%;YL>WV$fMu@U@BN-)Q{`t@9h!U~Dqk(B5?MJ;s8naqPflmg9?*@qaeO^(sc6im3o)jc(A~=96tHoBebUE=>qn)<(b=@pBHF`3$g$%CL*b1bC@?1y z0mha-$Z0p~E2c6|FIP@}8$!+L0pBo;Pxjd7qD?fWE zTKsdW@9N*a7hp)B%p2!$-RRoLe}!4pRXeX7;XKN)nX6x@lgDS zg280!6C|Vqe{G+nCIEger%~zl46hwd_JL{P&ZHPRi1t^t)M*k9>L)jFu(d=P&RKDH z$5v*Dz8+#nQ(k_PLlRkU7-l8^$YcVLjr{qs7G1dw*vLqy;}S#l&c$2Xo_Sf$iDdXD zd|-E1i48~mgEIjcR{h-{eW8;g>~ptHZSzJAO{aH+yUV>e+^!3|S29IOPWCLzxuMFP z1t@ppe{pa0`SmWG=Y~-86d)&_mwXAkP6yP@y%wcw)s74pA1n0X?sJ?Ro6S(W z-}Kn9mjVtj{g6XVdy*$Bk@ODUByM&?e$T?7X9xWQ{P76)W)*yV{Pj__?6_dRAeAt0 z*#QFn4rZ;C)?w_d6HIOAoN@Vs0LGCQRhLCs#{}t?BhL1uGAsAfi$@G;jY7lF=J1%v z5s`cu*qUf+GO(q^ZTF**3)IqKhOZrw)xKZ^&rWB4+EBQk#=KeXidd(-1j)<$<9 zS-M5!djf|-Zg~#U0Xy^iz;RV%S(Sx+!C>Pe2(0AUK$UE)e4xEEA*DipQ-H_jKfmy# zZhG?8`a@|&m5<501+V&S4z41hlRrOdxl-xD-A437y3MGZQJtK;^?AC$(z&)gWcbRl zaWK0H;CnFZ>VD-o-&jG&AO}>9rFM)w`Y58v*S_g^8m1#O8x6Yx*`r#Tn!6%* z{~;!#8TGE=vTr&@8JV`!U;wIU-@Q3I>l(YVR%m+UIQd8Qkkt~fOCx4cmgm~fG=2fm zCTR)bAbQb`jc+RN_bialHSRGBYV!24JFlD~WG-&rqqh|)$eEpHTtyP_dsdm+33c-b z%9C6AUuLgQrMp+pl6Ze9Oo;KtyNIcCXyP|JLxiu@ToWcRZ1IR(zl=qNNk2O$rd+Xo zYB3!*nTs3%G47*GL-$TqdY)LgEH%0_TT-`wu#;a9UD_;}*LsbZ4N_|)N#nf9%uDTE znEFmeOEu!6ci=JibiWSSg94RE-SOO>#-XgOtA(Wb&dk#tc>i0dB6@BEO>UBku35fb7m77c$Xj;Nbd3`yV=& ztshZ6HZUT4zES_GE-*M4^q@_ z+dbhg?2DDZWeLM2N-WC0%pS}XU&a}{k7wo@U0)W7N^goJ3|+0o$~SZ?-$6Iscd$Am z$DELvnjryF*HEx#G}SQA_`<8Cq$EyWvw9MuD+-Nx25r6ZahMFaOn$AYD>)Ps7b}nx z`B9^3+9nv8v=$s(PYYdu?d6tLwOBPC%rL3Z`@Ox{@t!7ft7^ZB2>dhJD`s9KJQo2W z3+=l49Qkhu@;&eMMN*bPz~S~w-L(2`g5ByTI>xbbxA~UmD5xIn=S+4jJ~+75)ncqTnGOImP1^SQ63MkYte*LNfGQl5qp) zRIKEcI#tubGjNhWgSguq3xC&fnx&fj!}YU0^Mk8l#i6%}gLb>yHU@gnc8OW$I`c;C z_wj6VK|z69Wveb-p~|gUR{8YcH_;|lLn-J;|Mxii4!+F1yo0gMWI%fsm=F??dJdP% zixBxt<$yTHkakJkI=`ANYo2**RH>k(!>B*7OOcDNn+7lb#gi=>5PG^dsYJmCTA?4V z2(jY74$B4y0qGB+MG6`g zaE`o^x8A#~A3lwt!}i+CzI%Zge|!v7eLCKmS{E!LzLeiDf6&oug;iSjNO%E-Q6(AL z9^$aw<5N|9T1TNrjwGYPY&|2m@147JaZ5K5lg70khAj&YwdWRsoJzMcvjLy00rS{t z?w>mQi?nt+-C(|)Lit)qblEcPpZObXkWcQjn^&4!q5i98t=8t_fZA`d(r`4;mHuTI~nCR>62Jxj@&ZW%Do(ST2 z2n?Bdt>X{w?CPb|zPd0jGnrS3q?OUfNiFur{~PE>^W~tonp&LqN-bJ+_loIyEZGwJP6S26GVnNXFr_W6+)I38|l4IgW9Ghw~JN8vjF@PWgW5bSq~2leB= z)^Cn##q&Eq@H%GgP;pc)$Rlhh*xa!eO^TIwNV;u5UX7mbWp7MP&qpZ)*7cvmr?D+{ zxOcY;b*!?oi#Vk4i{sv`SDKZVAiXKXW5Hxl*KK2NF+!zNqVR4+_yM{hrtKcZI&!#IyD@6B=-# zVz#K_Z{>%ZyZDkppD7vJ+=H%+sNAbpSFUzbY!*!M^!BrZ$Ve}+f7 zb8iBcA|->oz1EIvyE}h4%9AF(bO?q|2%a7s>qO`*Pro~b4596qXAn40ZyzkU6e;|H z%guhsI1?Fvg#B;MNF57(>qJ}m%Imm~J?(jxB8{t+Mfjk>%;gZuQb z1#PJZF4Ey^tZ-v_<)2GR!@Voy)rQ8hT+nX)mVB0iveaEXDjYfp@mPFZ1v|^#b`J?^ z{rI-)mS22+`X2DOdbe~v%*YOsGnNyDMmqHkAh^@cKmE-r0b~d)ULDz4DYR2(1PuF8 z&hTYIhYuDk$4_CmZ@_03JBe~Z{?|yGU!dWaw%DiNjxsK_c8p3>{UZo>z!6y73B2aM z6QD|A?`DCbSHzIb!THX-U$#Fuyuf#!o7VvoOe1FLgE8~K&399x=<>Hy1olg4WKY%; zCG91IheQ&SQdj632SJ-M`$9e0aIng9ZuZ*wK5t#9dMD*%(DVi>*cZuuje`+<-QxmZ z;55Y{+NLp4O6IfXHE(oFLG~WJ#IR})bNp7?j3+L4_!YU)a@X-Z3|bg+*+2{374)$} zhErN6Yp06^3|$biq{(c^CRAL|RBa3=oD2>L?+rqCpZBw82XA(QwyKrbTf$!JO58pR zh`hMRj4NskDFnmvF#A%1!eM30$dej8z0JUk;z)6%Ch2{mpdk;I73U^&Vl=-pNIhSh z#6~kB4dLEM3>sTsyiBSs%xSGLZk~n-L`TNZX0WM!?%HP~i|4Lavn|4V*18eq1s|PC zeOhY=ZlL?L8}m}w#h!_X&Hu0-IIH)J*P5p+@gb$LW{WC2HV5k__o8KX;(&r*iI-m2 z+y^z?wC9BY1!Xg1Cs#7yR61Ica2p6K<6tFi<#@PlzeYU3I%sl3Xpqo#v#MUa=d-oz zn~4LFJpFAw2PtLuVikEM-Hca&j`X|39v;8FGx0sCkUz*4q36%<{HNU~YtQYuhj{8e z*VtOtV<~YK?BQc}`{nA944$7q&4kg>Gv9A5cn?hw+GrWo~-m zamhx{L;1#7Y8LPBE0J?>D4TALf^6m>Zn579#f;w-j5VbF7uH?9hddM`npn;u7kD+> zy?+_X7JNC7V3(}%qjCca(zI1$e`7{BZ_@kS2P1MR^`>(0^HGIG@Fhcm+Qq)_q7Z~> zzpp6E2bYP6PH2WWz3|b=Ut~Xu;Q-})!`jE;GGfn*cg^C$kgf zNlV6siE>73AzN;s2K%7QjZWbm^Ij@dm&^W!S}@G2O8ZO^SKt%zd>XRnX!w?!xp|U%ag^seM!@8Mv7F{Zvuj<5 zr|dK-WImThXu8-2P4>=qq3b+5p+#jVbO3j-ox36J^rqQ^mr@olI8>JN>WPPLNvjl; z=iHIOEmc&vhVH?m>CbMkNryz2W1Cz&?GjJG1(c$vFzea2Cb8?Pt_>;Ox<8#qo6^Ti z*9VCG-Cbb3?ZKmhFbj6sUmK4?bVNx=_yS!d=fU|PXxEtY$d1Q z;MIGYIy0!uQ?RG4pdSUD3VvbjzBMT1DUM6fW*j}NEMbc)7IU_#_VIB`Vh)kwQM&RL z=vJZ+m6}V71p-FKms>i+{BTGe7tMW*2g0k8`&4iX2AD9%Tz>06Bzx_>`NuomhI1K9 zbn(oyW&6;X;gJf7f6Xb;SCs6OM_T*ojGN{{kdT-3VX_uGi@ma z-QT76mFWo_o=opXa8s*8+VL>PZ2Uy8eTqRPXD4O$B4fL%sQszpfOSu3sGh2rw)!sk zIPwH`W_ANFvW_CQq;R?Xv!<(Zx5M2_8(T5XcY5fv;AM!qNawiFS?%nejIY&M?=9#E~je-VXd(lybc>vugCscMM|qmLb)P zt*+^GT^AQynXdBHw18$f+SZw&zx6rxp40rz!@W@XO$|v&A=b~O#4OUj z8^f8g^1NhiM4{qX87Y$v3c7Kt-lhv4>bNhCrJ1=~2j_e9TKf7mL&Z6u$NOy;^un-^ z^DuB0GCSZGc~J}?w);jMQX(n8<-1K%{O)afc6_|s)%>umh_i-9$?n+`X0cvE-Kn21 z@7`io`yhxpFA^?$OCYgSF@wTSBZQYv^!e+rxNn8PC8cjNB%b<>TLb9wJlAOQ^2qL7%g; z53-6LeHK3b_C5h{-ztfX!RP1RYv>OR;wV$W*avuN>3vhbX2 zI36(6Z4}3m6yJVTnY}%p5o;NM!ezM(@KgR#YzcIzo%eFs?_@Y_GoN96wp#vk$g-g+ARY7x}fey*r@cZ+nfOWPmgOv4yQ zu{palK^e0)vgiAY0|)163M+%c!B4NXpYUJEALeY2@af82ewc2h4h_CNfYp>pbWj{( z{S~C3@`q1kUd6dyUY%W@>(?Z18X`-nuP5v2Hh3`{sFiA>VA9^E1(a z(eu4yx=p>of7lXVbB8=}z|UQ)U&;Ri@}nE>)DbMEJ$h zonA5z$P6qzg!2lq2c2DE%}Dn=4GbI&-x~NDim4oFHV!${;_u=@S$uqkSHFgk&5Z;! z;+xcCw%>%qy&17&ht-8z8XTw`u$b@F>zGQIkd!h=0!33fQ7!g7?JsxD7nYN)c5Nx9+GIoy@K$z{=r;Vd^d z0b8LcR^mngJXtK~D*}3tV#ag|b*{&dQc^qvl3N+9d4qAB|MK*oGywbpI9xhYp&1z_ zV(Gt=dCgr(#$xNXX0(5%u7KWfg0n8oyOa{0?BCAs|0%0j<)5}(6R&8%d1Q%-dAr+z zW0iG2=Ak?wlK^oG0g`LoC%mbfxco(@@X5v0L?KA`pZNpJ>zUNq3TJlkn4A-1d~&6w zl%{S1r#hy;NP3)=%OoKCXJQ9i)OK5HrT2PTrq@m9Q>0sG?~Qs9lwc>W-t4qN+jl0^k8SSL#yNC@N)HRWm#x!t7|TydGUee!`o}QuI{=} zz*IR1VU<6N-sHZ_*V59m@7GDDb2#!)e(+PO{kD6@uN9Ct$GZd_8LOpW@Vgj9UE|*R% zl4O9$>1?iLclJi2Dqc31b$OkFgQf>%R}xMiGYY`J8KLH$7 z8f0wVd^&}3IuDY7Q#wEaWc}~KcBud_a0!+DB+#Gj(`ZgHETwJEYuAH_rPNYDAA_{y8t!mZR>0 zgdz4{d8`|}?KopI?^h#T%aD}JV8mXZkY9hmJ|r)zH|4;LGhNJ;bck6bWGS$135)Er zRx~d(|=*#ty*V2q$hwuLS6A>VpGgzxB=iFeLIjFMFaq0WoI=bw^Q zv`nU`D5`S5ovqbc6)$#xtLjpNc5Q1Zec-fza$BeSWfagt?H~P8*Gyba)5JDE`R@5V40o}{CUCI4% ze9T=HjYB7?ROq(Ys_iC*Fx)`}eU;ANXi>W*5eoGTwJ}URZpDZ?_A<2VE*ydEp2Fjp z)1l#%AJ%nOFB!OOi!MxgL(2M8Y_J$EO(g2I-rWSMr)4g-hF^?%PrFFh2^}w<1q)_j z^Lq5znf9R&v_PKxjnuZQjOoOl;^I^tJu~q{MX>10Ca;`EH#>9pmxJ~R{)$T z?Q`e^ryz4E9;mGqvP%S+Hfc=cLUsAgt)RtNuv;gzDq)!t;gIu;mNQhu{d_ZCjn-Zm z4WU9$muB;tN^{71v|LFF2oHNG%Y08K#L_oV?Z`||ujsgUow4;TS1ZU}Z5swCjU2#a z=OK>^ov|bs$YusYaIjujqN=4%_P+SAna3vIws5k=an$=de98)DBZ0SxR?_e)mi@Rh z!IUb{^dnQ-B}621U2Rp_z8b77Xa4jnTOTrr7%QlAS!!>Qw3<6`-XYqhv_8eHvhxqS z#$^b0*`ffgRa2&*@{#7_i9kJ^mLq4R*bbkif|$= z#$i}nX*S(wv8&dvPijIaH<@1pTu`NjZdpiuBQsuUU(`jp(JorJ~I`Q6uzQA7y{Dvy>t#zCgIWtb1yoQIr)tyd*1o_TJGeqoqws zZ?s&wn}Ucm7<}AT`^}c2{VbUVX@5KU`?GP`bktI)l{K1Sbyey%!c$J?R2FzgqD~lU zE6*T4Z58NeQeY9N-U<{^khZRJTR60bv(b%S;ua40C(JtR9FJhc?;xPjEk(tfoxiV4 z5=@pZd?wo5ln9nH3^}WYeJyF4FYXtLtPUwNvQ%8%#Lg!1M>frgA>r}BFHTmgLZ#K@tW^cAVuEo@6 z>}jhYJH4`H-(Kr5v)H76d3ChVSENx6jPb!w+7ZL%8@?yqCC#sj?o;|rKYn+IEr8y6 zZ}RI%DV$;!yI&%fZ*9PIs9;=Zc!3oTLit_9rO|23Ko=63?Q11n2NY)gAQTc$8+8X*%dw##cMppM0MY_-s>Xd>Z6n~f9ktASHlbjw8RLw%RB5sHpKU%i=dNvu? z(KPno5eOH*x@%IRs}PhiwL?Xq0^aa(w3@EC1xjgnr(J0 zE4SF||5Ge~OSFA}FF;_lyf4e@8^1;OZn4!Hzh3I(5rDA|OC-nR`(daM^wt1W?a;Cp zq^XGtss~6L+ApW1g*Fy1TrWf*?m9X#+a%!IcT0(RRMGXov{PYeo~Gblykzgw%xL(SlS`lkKbw8P5gS~`$wX}aDc|W({;K| z*XefD*CKdBR4icZJLW~oayB$(Yc)R`MQxr&qTY7--TqG-3LUz0bRtL@*SWtz{)P}F>TKw-v77Pm zMq+v2Ue~5ynNXy5qucyurFR$J+Tys>7X~rJ^;cB*J}tF2<8S;t%fVyJF2#bUg`tXdqbx(GBrA5WqdR*YMlUEbQ=(KNodf^G_x@;k+NzyCrj<|<|$TylB&S##S1Y#cv1Gd^;`m2ima$Y zR_n)Lwvk*A-a~npPl4x`MyXP+t$2D;W+LfThP8uwMORY7otGw^Sw|Hauf83f1&2Tz zwIbB+acaVfL{V%se;jaY?b&R)ZlF2SMaE}Nar#WAhe5`u;`3>S_Po?6k*MwO@s6LY zpYhh6C|$cv>)kP2YoA7xI60OE|DFI!^UPBYIE@p~2}&5B?``-my17~|b7%TC79;yP zu}CGkYK~s*&)%^^KRe4^+@4q)EDz21L=YuKGWY|gwzdXB7=nSmyByW25j}f9NFhT- zueNlAF;w>sn6gl{Vs-P#ddSu=`t*FWXTo5nw6*F-r$0?t9r3cCb3-rx1tU@JeiTba zE={e{`3e)h*R;rDY8jMy9jVz-=7oYBZioAQKO0{RK1m4ut1#f6adAV}ekzOTofX@w z4=7CCw%PwV#w3$DR;24L4Kgy~85kO2Gdu02;Js3Up{NsIvpRMO1vTo=LW_hgUcb7@ zT$8cd6yM&>Hng4hWGFBT zB({Wj;fz!qLL#~a7>YW}-IB3F_ z-8)5ReIbLNgIucf<=~5LDY6d{)k`p$Yk4Zk7tH~w@?`8Dj(-kq&a%oxCDbva>)Q`k zxxIeah18Gx6OX)JUB0>Uz8uD4HBN)vaaVQnrC52zTV@U3p@4+gmdkK&-%ZHtPWEI# zEvgRMu4;qQctHCgsqb-u(E9M2&_-rUl8~K3`0mlAW}b~`m{h?WkDSzc3eKZ5hZJV; zN8OaI;HWBc*-Z0k9K4TkkTjO_{4Tj84I`BuqI}NPH)T?G>wEt#@Tc$k=4OAAsRi4` z4brM*rO(G0VNVx*1cWTb(EZ+wwu>Epww`Edctpus+$X&yFpMb zPqZybkX17P8AqLj2l@YZ(#|Bb7>ks16NhYeZ(*+(wl$c_tt)p{t`QyFC|B?=HV#0J zCFVNEGrjClXFB9B{+XDwv>PxSmqRUZdPBfJu=+y_I1934YW$6c7`G!TrJ`v6kE0@`K zqPeaU?`bWOgz?G0&>zI0$39>w1qj{TEusRV#h|A^0BnOzz39t(jssxubkIIBwFR%Y zPNK)(*(7(`Dn$7A2nmgtTYc(#Z}Sd-f%PcOYhw9bJu={FII^?Te#mT;agD~kReMZ$ zY(uo=*TuFx_vHxeYo5B<2}+DqsN`EcyXjtWvb-)IDw9V7 zF4|3CV^8lI`Y~Di!n}bzeP#H{TOIY%ONm$*?COov=*pl7Sv<}oI`M_GOdis1Wd>f< z>pO+KUlJpPI5!%B@%3|`nXXCaQZ5EZ>@lSZ(+)o0TKgdQOarcb$}PVXv($@SgZOE!KoF?4) zN~SPet%jCITza#$K4s40OtiTk^M`SL=c@$%n0+1NWj~HW6ttwO26qwl^%PcBi1rxby26l7l{(sjtIO~i;pcs`Yy()a zKJZ<6(aU$n&f1Le#N7#G9&|b&|NwECz1G)h`Zs#Yx@8 zdGO|oK@T#lU6A)P_aWu{?#Ark^Ke@45#><=$N?$?*#KRsBZ*6!O`znT zoUnx+`E4*75wlfxmjbc;jh^4hUf=nPk@u*2!D%ZbZvnls56({QK3_6YrDaqDU*4|Z&bc^?q%(1 zyIhg)=j!L?*LAm!LXJ7QZ*{9_0?NgI2Hr^gFZSL#tjeYPA65jEk`x4_yM;|igM@TQ zH_{E#jTm%yZo0drQIzg(Q0a~ho94YSIL~>`bB^cF_qx8nf4FY;>^=9)tTk(`S@9Vq zO+jhUxN@f(QscDky2(zWT$8#Q#Z~-v-QHrqLPN!dS=Hq7`dcFsTE?On`L?gG8m6|! z<8F~lY$DjN4rnvhL-f=vJlo@W-9mbFO68hPU442@Cd&;a2VWg~NIUilE*r5jG+!Q? z4plsJ{;(<;%UF@3gv#fvClK~IFt+ZFKV=Y+@tZ`LVhe1OWEp&3-*+5P{o3xxQ2tGD zvGv!61Fd*&2aO!Yi$>9_KMr3F*BrQ-6{}OF28Xqzl5;hr*)gMI)o{X2!#GC+I7#Ah zziy(fjTM5#pXAh2T!I2tOmD`S^cBmonM1}JsEt9+wuti)Q&^_gwlN-c; zx!t&<E!z;~yy-~6;2Z-a@;G*}NI`3mi4eFAjkh18Y3ejR7JU5I(&}>NOuln0*>$kyY1TVZ`8~1!R7PAwrV>3c{W<(9m z%^;^;`AP*nvnxd#g%RKw!ja>uJ|9&6)AyiGvLESD;v|p9W=@tgSQ{Z&Aon=zUbBJf zBDz4SojHar9uzFsRUoO4iI5No<=Rd$$(D6<#V-)|YC`(Y2uu49iHcfvYFDO^YuiA~-dp#z--v*( z&xDB&{CK*Mca8aYpUl~1#(Zc$?~2ntkli@(rQ53cn^o<%>O`qA9hHUMGfKBq1sT2* za^vT$tw{<^=j-NnD~Go_t-`l@Z27St;KaWep0nlO(Y8D2CotF4`Gp05PDwYYvFz4l zM4j}a9GOa=7=1+_`r-rZcmFjA@oj{EhpCk}E@fe%qp-QLqiQI(*^b7`@d{_jX!;pd zijs2!7P|N~pRRFA{cvmILBh!QtKlVNGZQ{|ANS+1O{TMhu@cdlNY&JZPvCQC?J92F zSU-No-hS{Hjc0RruTQ8&zM`5sj-8*kxoHkb7x}kqB_Sct2ymPZ6W9*xq_6hP2l?mI zg-UqP4J5?pEnPys`|yAgI-aYqlH2tKeaBlR4G-r)D?8)!?$f+G*RF)CGUgbhXC^x!hp~JQh=mF&ChH=yUG{BJ zE$Jh&`AxAZ4OiJiHg83)O7$AX&8K@(IA(TXN~+mnZPhaskZK z3toREqHn3At!Zpq(sFloCFSr}$Q`VRb2~Sbs6^OSoHyONYU<9N)=_aVUTH z$JPO=Bt~E^OZeVR1UPJrPcyJhtkm)ASTbU{%v%-%xkJw(8iIZlqsFQsKKZp9AsShh z^;7mcC`Bcc*pR!*ztR)Q2kKRr;yDkVaV{?S4;AD%Ep)67W>^r1Yg0FDMg$s6luq5k zU;{p?*R%lw@Xe_FM&b}sJ*d?zhCkKzDUjYD5;SY9+TsY^p4`10B&re(-c3>Ks?pO@ z0pYxRUf!$ad2xD#XsEDe^*ZEQ0|}5KMQu9Fc9pb-gWdYn1RV~qW7P?!Hp_)XTLb!i zqSqP0opapJyCixo&-s=zF+*dGQ`|^$Nk|~ssTF-(tpz>^$Qug)wUFms9hsL^Zwnru_x4?bwq8oDM$9UFwmcbP=RMDFhmtJjx2eUGJu} zCc^2To+xt|p`P0Jsigx32wjmW1h%DE*w~ZA+9<0i)tdHSPJRSw?q%hSdlrpLJ-zHQ z!PK>%Z}h4V%H5i)XrHHb1bfDAkK|>jyn5aet@b~PIIqts2sB}OK3T*nCdivp(FGh6 z%8-p|UU+pO%KV0*N{M;67 zU#9T&8)ShD?bXTWpnv_7mt;UC!ptTu-gTArO?JdTJqbXrOLxHxZ2!J$mgBlEzPC6% z?VtMn)BFFgPo!rvA8R^deSJO76dmPz!H8doz?NI%Mk7C0$|_(Ox`D@nT#A?mP9%7=BcXm(5Nsnk9d)7T0Xd zsR7yqY!QA8!}gG`+HK={7@r-@O$n~~QsBvtaS+L9HBDzn!cN4#T?xiA8PlhJha4O{ zqgAL)w$KMrwnM)C<`*BmB?UN}+Dv|el8S12OkJ^3Qlh&nV;51;|N0wfNPiUctD4|g z+8{hRdwanA?Aj^_oA_SNe`Wh$<@_`Ccfy6kr&K!CW$g%sfcp3t4_R^3^8Mi*G`J-F zHcp?Xg+9GMMI~`39I#8m@1%+-XIGlec2yf4#K64v*E4ATfhQ0Fb4r%i)NVcqb@$lG z`4j`>5==}HRl)<*^78@yG07FJmg#E`Kg?vm4zOXv0r(2;y4vdWj>+tw!v%o%a~5fG z5aM8MV>$W6KPk`1>m^HV4p?+T=U-L4bf=QfNuab4ma|{l+qa-#Ulvyz>X1b?wNeo6Y-#!#6{6(h!LLVu^tTYysq ziP;Wmr>k+q_xfg~FUlJTzq0}6Jrm0GkXp;cg=lAg8z3*um#*XJnm;zLlPzaGzD)x| zYa->juGhqPmP*stuHmTMeWB2eKq-;bnl0{{E-0)b(-qcNl<4FP`1=tPC)Mu=QQw25 zOZ7I^;;M-avLD}d{$SPa`g1I=UgZFT0pl^VHvSnNMZGN<#9ZlSa;^FY?s;Q%c8Eo8 zVyi_&M)ukm1&0N`Q{p6Q|EQgcay>e7ARPScX<6<1g8Pi-e#7-l(M-LUxrt=sb!b4! zN?lU~qt8F|7lzN3A;|00`Qxt7l8D$pslXKdG*;^TPNd!<+q~D=vW-*no;4Txi`$^| zcOe#Ot?^YLGA+4Z%GuinU^zsUu&rMwi(fncMtT4G=C1~^TizqEt9P{i_6vV$@P2&# z4$QFLfBz0x05SpN1=G;opHYav^!aNvHw_=L03-(~0rsD;9Diy5?`Z(2N`Oj|c-tsO z^p~bzcFTV^+*1G~)Z^Qe-{aSR_obT=0l)SZMgvBLenjf=pP&BY*odWosegnu_!WQo z{SPq&fl&!h1VV2AW5@q7-v39C|NjwWsC8y;XIWucr7K7&y4>+um7?OA*1Py#@-Q&z zP`yyVqqtZ~svt4*Y~|hZUt;E$*7|P~3@xaoT5Gi2U8AaSQk&ug@Yn(s5LhvOn~Vi9 z`w}WBxXa}_1wB}%u?$1C4gnC8NYX|-1w-9-thG3OMzrTMFwS)}q%7XX!VhPo7uK@< zI$*(R%-M>G7*p8}HA9hOfM_&aI`UIegNnpqGJF37K013rsu%qn{g*(#(d+*;y0;Xi z&I-})&D9diiz=O`>AS^^M;3{s8E%-f-YXQs7(?c1{aR4L z40aI1<0S(MpT}is$oz5#>r-7g26WTwWjie9M0eMJvn1rTEnt@L{usrsk)`MtcTwTe zM^e+p?+h*OelQ(k`M`w!6?c{To+TeRtrl(yv0s2Ct+yS-S|gw#LLVmP3v%-`5=jMW zFp-gxI2n^)^yxWY4Y2;BX!8E_fSUQXaO8?oM?Df_co4TgMnc57)y}m# zaE%g6Ocveci<6r4aXnm+-kI zcj<4EYyVq0+z4tt%?1A9F2k;m6DXv%$CbfTW2#I5B~ZWo$H?*Xg9Xxk-m3oa*Zkc+ zzTQG_No?kEmi9;U^B z|B_mN{0ocbJcfLC%>4z&ZM+9<7JuKP6wsm3zv%Gz!SMhx9J`l7^t|C7kHT4|hxz(z zV}s9opFj_RS>MgTI_K(L!gWV#9499Wuesa*x19H@!8;6AqDxpidDcDN?WvhP8T(L^ zDN$nn0u;(3y#Xz%g%H!=zzT$X z@j&q#>HCNy`7^vNVD|5T*<%5-_f5xkPR!m%o_W0x0|0L&VVeK&-~3azx8e9Sp3@jc z-^QWpf!WG9Vw-~N??C+KZ%hqy?u9DCu*)vJg!>-u1A{oKRn@WDM$gN&h-8xeV~#4C zLv_Odh}rmYTpmGvfnngufl3=hEZY1&2a`e90l#9$Szx+XIb{4k5Tf|Uo$*TG07*&< z%rlNuRYDac6-eu<;F2Au4Y7zN1!A8PNOp(rS}&>!>2hPzpI?ppXAG_h??WK+{av)- zwg=-;6!N@@Ljwm@yO5E4UUf7)t(JyQ;V_abbm|^~MjSq_rzFyHH0l`5N8QJdqcje& zc2~i%E{@RPqlTIneh@chD-*|iFZ?@U;Jt{Y+K?wPhQp?{AZ|xzu2a=7RL9O2FZ9^b z8nM5Rlg9Swtv&8sFD%+g?OcI4l{Q$3W>{I2Uw|H+#{+5(Wv8({V8(TP*f7mcAN#RU z8hS;B&g7dSsGF+KwU_X|=411t1PdG5c<`eKm9GM_!5}`T{*NabwR;^d7Tx zx0g;$(9^x_vK)PG>1Yzbd2gR9yTePiJ8C2Q7!@|adwB@k})Q1toxCW&RsAZ-mEr|2V2b#LK8 z$OzvfHfrg<$Ntn^bOlQXS8sF?;ZTdB%h%}u zzq^>u>g)DAZl_0|5lGGESwyC4u<+$ZMu2S5#kFIzh3Vzw(S1QsPz;te7*7~dgULcM z+Q82^uTG&99Hc+IB%QCq?L1;5?}fk5Fr~?eBPA8<;hNp09Vsbz$0tB)K{79X zdzeJzKIj8;`+eC+qH(28h2_%~n76^l{iNisl=Ubj90|B!Hm{h=Ml=x)^sU0&bI&5 zF4)uhotRYtR;}&xx*0tdOg1Hdqm&FaC*l-4DYQXm%K8w61gHRn-(F{XR^JOYQcnOZI3$8=?7QsU=yhj9*AW=#ydY~2S5;XattK6F~uM)m567{ zkWt#5Na)Gky4QYPJ(ridFKyOVMH4C=x1^KR;hkRcuG@ognGV7{xs)_KRW)dYq*!gP zWU4ks8H$3bsSxOW0$j;P*lDws%EC+*7 zOs|qHP9}MtDp?sc`78t%F@TE3J1d9>CSAJe&Icn;kb8v}afMmi3|<2GQR+>sJ8ArE zXt(mG%?KP#SnHJ*Fa2YTGu7j4yoC`nsCp%Jm?Ddp7gh~qG@Z_8xaq>~dn^apO}0lO!~*wczVP3# z9~zCfp*-@~%c-~*#U40`J)Bnw>LlilisE@GJO~e#T^G421N&m>3Koy_L5owZpv5+5 zIyYAUJgo><7_~Y$xz|CTpic^f4u>hcIDzvJ`e#%Fi2s7Ea!`vtkIUHjY=4XkQEEab zO~PanOcez>Pcm^j%>k_?1}4fwdRj~H^)eOicd*%sHM5>%;O5)N#&4L!PcSJFCTNok ztGE&sG_^+<6Et;y^dLm(x0znF*de(1+7mq7P9_Drc5oF-ah723xwM?Bs-ntpb|euk zNC^sCnQ!TizM!3uy>L3ZVg;Fi;Pp?R9&71R!?ks-Yi;(&+59wc) zOdtww%nJHmo#5Sger}$&uW#MA_sA||ny3VZqR$Z&9pk?AZJ)o_WKH`BcUP37Xhxtf=SoqN%7_x=1~lg)fDT(lR$zv%5M^-CPUBiX` z<4z2F2y?Rh+Mc!BAkyC;(gkIOB?lgZo~?CjM8*CI4T1JLJthx`cpQ08ALd26eEJ~N zWF*hAwbx;$KUP^Hyz?=ZM0|PbM`Zk1N<4$o?9@wo%CwP^Y2l2W!CcVDCEKCn;Hn$z zI%MTJm0yQG1tzNT8~V$HqTeQf+0cdu75?q@%%{74igHX6iHR@=`)Hdd6n8ElrX(`dv71sFk5sXRe zhznmGJpW}H{^daMvw^q_t~xF?*#x`M>f}Ru+udS!z2Y}u7A~!jhY^(B%^`h$G~Z{m zIm!50n@qc`=*ZPtA38~BN1#>_XCg3kMzR^ItIdSx5h|K2qr;U*uja)vK1tPI5LTkX z(RVbiKV&FegH^jSj5xU#KGT}VjS>1|g|^H+1cm$RdeNn@f}5UY$h3DHyqxo)#hs^8 zlEM3e_{eOWMWhMTzUiUR>$Pb_D?JL8s~czKjDKpXc5iy-%%!%0x&Da!H1Xw{?dq*@ z?}3kRrDmOMeYpKhig119-}qIUh^EmTXbm$+@eF!U83#12jjBLeY`dywi}P4frTgi{ z`doQ_yU+9xuwp(Ld3E(H^+!|MWXB?;`z!m4&S%K40L7z7wKv71LvXTeBO7Zq=R87O z%b9V=(!oy2&1Dj&%h_GQ&y2-3dRF5^zo`a@CWG4-cNz0+drDd3^;p6uzx43noJw(j z5XRfFSw4oq>pya5w}uRxKzQ*SPB$io=av2LCmw5H3Z;8-6&)m0uqw?9t?7R>Ah+eZ z0uKLqh50eV?b+Q&v!nY}o!G1$Ne?gG4SW_2YR70gq-L0Yhe16T`XYMSpHs&f8+C|>b_)Wv@{80*+ zL8r7~NiK*~=zQndB1g9(2un#^h&@dh5P_ea##V03)hMhR0+;(VyVU z0s9bUTuU?DT%uFP(hqY4M<}I8MCsTpE<_}azQO_q4v%1Q* zt3n>W>#@wd`e4QULHO=cR@b|`hGIVcnNl2Hmoo6PQ*~y!k5_P4~e zB!5WZma7UUqTQEDDqq_}>nPN$%9lu?b2hE-VXR)EWG4_3zpd=0Ol(R#(sAT4jNJ5c zx|)=ZF~E$tMG__VhIw>iV&eUmxxkqvE4J=+vNf;%$ob(O6VZflBT-Ps{1RC|9W?sP zgErKupBv||%j>7^T@kyUbbmXSxAWnXvPc!5IXt8C`AyF0I?OTMH)+{dF5mVZi-uHf zkB~MgMUxy4a&Gk2;cc``$0=C*AC^4}=#BylF(AGV}41sSzfIlln4^bZUbbzxADS)viXv z(086kcQmj_O~D-m_A_?+pRQ!2p@|tt?tTwH{=KdfI7c+GeQgtsNqNi2q+eJZr61*k=N%YWVZ{nnqq=?5=~zFtuIpTu#v;;DdiM(f4u&7MbTE!$kF%Caf90RDftXi z5zyRU26mqgnQCGC56xLx7YB!Ie5?J8mAYnFGV6@g{x0A@DrBGmT?o5eXW_wHcwCw6b#k{YF^#v$UdNS z>Dj2q^DMq=FB{c);W2JEO->&9Wf!^AKR)@B7O{MP5A51())&+`aRxP7E$3}b!66xz{JLX z8NNv`{X3%wZ60QJNF;#v<8T|9(L|1x5o`=M6?rH2FjHQu@CjW4^GXFMvdB}WUM7XH zYJa3H`y%=1wA#~bT1e~TQthc&Q4hhOZY9)0T22UOmYY)zKCY0PA0%YMc68sjZi%#P z5%+Y=*E4i)xt5kGY2Jh3ti60?niB26`Nc3$10^dF;ac%z^ljv-dT+53Ms|yw^%omw zSE7ungK&f_v#rT0y?VFach%hzm}vz|@R(WVIq#Mj`CHcb*#O|~bE=_;-~Tw8H1D|?!8Hs$ z4&}%a3$?#$pK{5TA@r!0AoMB_MU={Z$iQVQ`9$)`6k&+R6nyji0){ltUMP}+fl0NO zWPOtnsjH1d`HZJhL=tUr>{d>~5!n6`wXc3knu$625&HK>Z}mbxO}f)jZHh;h zsO@|nC!3raR7{pmY$vW?$zr2(mpPz0omUAZmy}_vRRrP8A&S!;E|rz^JziYuT<)vm z{SRl*&NIC5VztC~pbMkHw*{FjE%9E~+dCCKFw8c#^F1Nk9gHSwk^+626fSwmf)i{d zb=P@SGx>qDK@BNy)|XHeX5s34zgw73H3HkwgU>&Xb|FzxudYyH%B;vB>|e!Xb`gA& z!hj=cY~ZS#ocSWYe+ScMq*J(ML^zd+=lhJd%yPzXaL0CFOHMi>y;;ONCT$`L5Z67@*4 zVZ#^7kqjOD$WtN|=}@;Hc==xZAZPO3HjB4l@-QBZI|S#P6EqX=l*jCy@r-G#dq5k3 zrO-J2d_BXgwi6cFwAR`-V)OADOx3D3YgM!s81&7g4u@N>aO@fJI@DSUom*1=`K|fV z?K4BXzbc_WtNe5YUAbzjpHr#O1QM6a%R{r1%^-u5ChNVj(83 zX}-Q^yjJqib^?NdA=5z-AOk!XB(~x zfmqcX<(cn?)KgNbU2No0=5G0NsM$U?5Zp(^K#9%KB(#HaaU^YeGb&80JeqR}W@hHc1ffyVb`qSE(W*T64e0(&@o zMAKq`l9DD{t#X5SF&&1_Mw$%=M#w`59++8On~sAMRkm1@N{dQjrS+z;=O+7 z{wFqq^MOJ(osd<87!xkLujY@D=;qcD;lm;ELt4eB-y7)^+1}^#;uM(@xf*Nem zg%s6;2J@%oY?aCyCi{UH(!}x(8jfF_rd9XdyMweHW=qLWKF{VU4`kg(#g!6gov*_a zRy6j!z1s9q{^J+i`~!=ia)eRBX;c0@N*}Q|o|UUalH9W+6_j|2WOfI=OgVGOn2USw zOqCZcyQ1SVz6SVUYQHR1CQP=1@$cE>!HhrJ3+JkxguS~)q%k|Mv>fW|nK8N4tXx|t zCV@h{&sef!aT;v@0hS%?UXJc#ySvytH7w}MpzhF6-%;HnrQ!dmp1zwo48wG5;n zYk#jS1}IkvT^=(}tlCOr42SSovD_zM%8#yrvTe;DjxwT94iS^;kc}MY6ZMok?^aH` zO&Pp78LUV7AiR_vs+kigU7aHqqY61c!vBJB3-O&>q09PXpLw?mb?ijvkfx%Cp3m=u zelI>+YkCSODqw+v5(Uz&C>7+={6F@xuVo#9P0urqoBSI_%Qh=l!}P{t6X)tOP`t40 zAF>kMFKWovzAT-iNLXM0T*Nr|qQBC_C*2H_Jl^aR-{Z3pvAJ%_gD^K@`NosWL6RZ& zi!=Z*b(oNJ%PNv{t%>HP(EP1W&A(Y3dpU3aT7Lk>p;R}@nPiCFw?MeU_z4Soo_>Ri z+2cVD?Sksp#hZLk8;|DWDeDH8Q!{@Q}=q~6YmCkPZooPTV&uzDfI94|=}e?k-2z zY!oTv>0OS&{8Y>PDjz_V4hjH5-*Ph>HhzdZEg0>?c4u0vK(h&At z8U)sI0aslO2y6x3;@`0xG_SLK&znfdd`$7>Y>V)`w)8vZ%->l=-)(B3EZxpG@r68> zn7^peUlk^`cE<2?+3_pM1#zL z87}to+w7jQ#82t2akg$g@8V2a&RKNAt0b{!_v1tyOwTCh|Xeq?bHbUscT*3pb& z{=W{$fBMWj6&5=_t+@^A-)yC78ClIR1T1C%u$aU!=M8t$v7D2S_L0w#dClAAbsqO0 zG2Z}R|E+saSaD&{v@-&2kjw|FJB4kj;PwDm%1|nBFT(D@T$0T0#?&4=2b3n;sWjz*wIrDHq_FYg9^aDeWQ`@qnG#sapEo?6^LZm|OQR^aHNGvA8mn ze+(d8bL&81%jt)F)apmQUs^Vd9pF1|-+#Ol0vEDbj=J|k8d zj{H7xB3)oXVCVep{1re&kfCZ-+(x%ERDzzXpxaoEov|?J4kGqYB^cJN!rGMka(Nod={6XEtHd*rNkU0KRAJ~c#rXGnHII;}>iO$AJ z=Q{IB54`?r{JNdnvQEL(vfm~hTLq5L$_3JW!Fd=el^0b=!mqd@EVdEBZczfHO%MLW z^?$W5{oc0te{^**ij;IrDGL1bMV+VZn9qNIwbB50La5bWuVevt%G`$voc7?}*4*(W za#2o6<2(AW)iZ15?r<1Ve|M5+XXR;=2>XC_nQ_6&$^V=AeU(_4ZLHcKa*T~qs*$Z= zk;eJr5g7r*W{_*K{m{VP)NtB&`&)`&@_?Nv;XVEVWK*DP?gPBAceZMOrMFl&RQ7@z z&Dq(ke1@H``Z<>9ZTy_ScVc{}ra&SbEfDS-(qdFIdL58&KHX6N$}!BW4&IvWgHVl5 zH2vDk80%a8ubSXsir*TyDBf231R~CUMfU~wv69>aA#AbCXB2eua!2y=@fO1JFL9qB z{No(G?=#9-!2~Ej%8dpLxdR)DGPH;Yo!Pk2Y8()HoUb@#VK+ zr^rjMERwISPi~pfD$ZQ7H@=k1lwFnH{w#H}dWn5>${cN=N|pPvfN(;aYTUb+bnFLS zI+>HvnpDKCPDh2f@%aH*SM5yB9iAr4LeB^ONGwcEDB@usy%=}?Z!{^8OdJ@yu{7}6 zl~Z1hDu;12kkW~|7>%Z&I5vS!C@m{4t&2qgGgG5ZRUFo~p1dP|062_wY5%VWfoBR8 zpr?jT{Xu8dLjph9b5#{)X2rg6L)!eup=DL<`v1%VX^o%%|GE9ELTZs$f2SB$$~;cx zZzDCorUlrbKoLVgVAnrz!k?Mt({&NZo{N_0gMTfi1QCE7H!Pg!-yObgP78jrz~=sX zP&Y*Tuea)J3_!3^Gt&M>JNz*hw3O>gycLI^*ZTgO`NL8GWmIySilR3I{SWg0ug{42 zt|ejrhD5=y_5Q2%^Iu)ulK_NN>igWF8`=2BkpJhCfIA|vx+&w)%Ku*4MqL*by_CoO z?Pz|fjpJ)Y!&gFA^zW4bH9*4GU(;FsHJ9ws0ITb+t@6D-^ly(i|JfiQ2api%vxwop z=8~`1r7p(}#s60~$Tndj_b)7f|I3?YG(%~0bo9BVCTUfb17UY7uY!sS6=N*jjPHx9 zUy?we01k4QTU}i}IvpFO8Bt!ouWa4#bJNO-7!{)(12XOtcX1J61rx5k{k3CZ$x|;B zr=BfqAtDC4GeV2od4B8x0wv zICiCMsrZS_qh}$6k%rcd#-s84P%ciV%h#LJUUEM73FvF+>&hlA8&!(c88D~o7b(#A<$Zl z8n{=?e*FO!mPA@@iD};IL0nJ}#Hhe@ej=f2!|=E?&30>ZEh0MFuu<;lV_->~XDOGs zq-5mTPQ4_6zcRJysrn;mEbt7II4t*$<{kiXloLH$=lC+);VB4ybmTob4UPPB1%)Dq zIw|hY`R>A|ZWc2=Jx2ufg~X&q=^Xk123#4T-pTZ*i{s1n(wm|;8=L`-uFXx~Xr<`w z0`(-vczWRkaj_kReJOzm#|DR;xuVZon~3_-#t8(u9;ZEO8$1z_wN=#~$HG4(zxM#b z$T)|QJ0l%58KJBT7JtnnIOfjXgF;>NWF}hLa1kUNSPvE)M(iiki;%vT+3`1m|0#)_ zn%Y`WdYXsg_EIBMrR($SHaB#r!}c(ewK{Qz=zS-h5rfmnUX7y5Vj&k_3f@Klkj}1X zbE0;-)_Q0~0v4U^h5paWR4|6-LXdD61DbG)vtm01Kzhe==5@f8iU78u)upON1>sd@ zCb?t@n3sYoMr!7f%9YKA=pc?$q}%H{zRY zkRG$1SS(v>-(U~$JfQ<_6o{7PKY!IcN`F(CI7OeThOw1SV9pU&vvIXMQZaTX3st!| z!fmH%Sle>8CMr6b^Pq{+-m8uXrN#52q&u@frKK9mp3LPCu~Cw^)EMXTi?dVxUl}~*hQvoJAp&fgd! z*E%w}_7zxIK#QK`a%Y1tn1AY@N|BavUOzrxgv>|Wo*!L4XhA_9Q=?->8r9o2>_qJL zexTm(C@8=tW<`9<8fLQ7bVr%IWV^|FeolsX&;?}i7CyF8u@Ypo==Y*r_cAFAwzAT` zA*6iH^EiV;0`KLPwgi)oqtP^;aAx@Qwzqg+$R89YB~MignA2z)TH~?f-^`K>{xF&# zy=rht1A;Yw?Q*BjK#$dX$L(l4_ND$ajYJd2&+3s?)C0E5o1B9s%K>xd=wc&DXxDeC zUn^{Z9YgSWYf9l6TBFlIY!eJkLRAN&NAIq&LJ62WvBshXdu@Q^&RqzOhreWC)8Wn~ zPlx(M4}Yx2z2c)-ioAHO@*C^WM4p(9O{59(x!-kU*Z_W~p?*d`zB&GI17F8^JZ%`b z3v6kzQ-Sh*?#D@C&;tU#q22PviUIRw#?FM_jNQN!1W~#T#Tdj1ow8%=Dnnui;-KZvzyj(_^Ti+wnh{1x;A{?V$1E|EzK=0?ZAr}y-#lT@~+~|7W+t?+S=#T*Ao>X8C$4J>LEH- zBfEngs;DvjP%JarxHriPTta};*glxv`hLn=*ne3Bq+<;==RJ!LlX1iGntpHW0h#l; z;m;PWg(a2wQXgL?W?YP|a>p%K%v+7p*-{C>ZVfKQ2|Tj9IO@zvc1gCWHRD;n=v{3L zJ(DXM?*qS3nl0VK43&n=J=1w?UcCC);wO?MSAxK&JN}Tg*7SEAs(lX23_D*VB*$8V z6mt6gc-`O5Jvw(-)1dqaMfc%0Uve_g(kfb&v)dN2_M3@)@Bk<93}t{XQogIv#AC-$ ztKPr`7K4Dtegf|_qq1@j#cQ@bjO2~Mm*4{tqE{!h=KR`B+d!VwyMP}Tc>x>`OPUI$ zc5TjW^_a+NgB37WPOHX(_LaHC%vcf1u%(2XjBoR%L!YX%7Az}$!}mqrb0F;Ov?O{u zHtwnR1E2%I_@H>~ux_AMW?MB%-5c>xj5vIb$R)OZ`@Pbfr&H~rdv>4;SU+}ilW>2u z#C;Hacs#Nx!tJY=qv`}azZJneKU6hiU2yI^5@ArozG zM)Q|%WYEHk6S-H^H{h7vj>D>mtiu?!>N-l(-ZXL;68^IE@5Q&UjZ3{Or)94h=JN#4 zx>p({W8rWZ@eB!=Y}sj^E_3&7mzS3p%F7=myx6W&8|z)Ol=QO9P`)MY_a zQbWzKznnQilBsmCYHniLN+@tBYq)G<*-)70s=pXZEHXVyA+#D#_p;*0e8i2f!ML?94m zPRQxK^#WgJ!I^DxUx%0zwvwi}q{sOuad8kX*4Z&G%VP9+j6`$_))uP`WtdIz(FV&Q2fSLsHWDk`juuRP1g(~{Rgk6PnyP}V`6G| zKI*IcR(qW#FA>IP9KK_6>ln{a0bi`vHXI(gu^4s<*E%Ys@17eDBsoa6wYX zy=P|LFWO^po33uH&1HGc*iW3bR2h8SyOX#{!pS-5YI&YuTtxCAh2f3`=@bhGLcb&yF9-tp zeuK#e0Lj+rLmqg#B*kKG%0@p6DU^u&OJU;U7Yz?QijX$#LlSt+T3%`9gp~N>TgRCX zX;O8O+s$rnKu-Gk^sHd`ZDD+55?^dw9z!a!waXP16`$0{v$(vLYIH3J_5V_!#Atp3 z!2R3Ec&sfA$;$LcyZOl=5JkN)Y9wxMK}BH2N*{OY>(!@QVZ&$my`E}ngO8god<(`p zC@|47O^b=%Tl-fO@utz#RC!8@aBsRhOifNJMRCmCne6^$Y~6-EN4UmXb}65;ANR-!HTkY`xG3{D!j782 zZj4a~9<`F{&FP4*0@8B--UQ`n!>HFmI=;b-|6sZOB==Y=y~i+R12p9+n&x zUY|eg#n5y_DiD_l_-MJEwqu5k`?%us8#mT%h-G;Xl4hLxlgC_UT-x;}OJ7#FoLC1c zDW!0lVy}mexzm$9L*`(L)1u(5ir8{WW_MY@joWf_=)k4GTyHz-cZy02xKdo_SxvtxvA9CSTu!2!)h7cWOO-3y z&T89r-;_Ihx9C9Gve9WmcY$51^5nzx>DRc~y`$Q8!ZK|=J4ZlY{5-&-y=pn@R*5s_ za7b6fsaoajaKwbTU%G$u!{4qSbWmD`W8O(;>hUUfDX3CCYhXP~$M)FU%wXFnBql>? z+n%ZltV;6wIqsHbfz|Y9=R{@%`Xw$R%Nwbz?c!IP7`PZN>{IdA{e z{XpqCJK`grC}WmeMHY@sRK0S<+akV9(=}JAK_CFyv#YAYSWNz9nH2bmAjBoyX>cL^ z8Ig;?9CLgi_S}>M1bR9HP;G&=KYl;tV4w=-YBygyhrZG_Q=CUB+Dfv ziWY!Hf34{?OOMNM71Zuxa(6*KGMcL!mb4eJHru&wq{3|-n1sW26|_Nz@xCa3%r*dE zGbEnx10iAJx*dUsxc0aM$p5apVH6`1Q*2>j-2RS`VR;m*9QOj3u_>p&}Q`wWw=4&aP>ZaF=7E>yN$ZTuai z46uobQ(>m7cL0Lc1UjZpQcNT3XF~-Y#W|o9uFYSy?Zvb_62Uho5aGaAj&Y59%Nh|W zUUi=prmV_uPa00KDJ)kRh0l59k+PvmbvBOSgYr>kwB~{P# zrTTV*N7S-jsZ}>+Z>mS@^R9vJs@GV?};Zg8(8;i2(QZ9!NfXm#y z4oW4sHxuZ#S19yIqi=HI1#ZEd!fOZ$>K~C1L?}tdyC6vi!V7PBba=mU8Ed%Au+Ca* z4CH&CUtC-rBb&lLdOG5wMcDOqgP~q-JH_K?s6gNxAu3j-Bhbr5i7_^sGkDlS08p8B z%UcKp*`xQ_huo3|GoDFUAAF1hE`mC-ImB~0R)9{9^75f3xRHCTNpUYW=sX@i%O??n zR}&OHoe>2+uw=yzG3tBYHr4*>vUaTT|6}hh!>VkzwoxfT6c7-QM!Hc_dMY*P?o#P) zkPelQ?hffjS{g;VyF7AEoCtj9TB%k@6*W1BNV`HcBJo%b$rNczp)MuV4Gi5;bz+;HFn-10u&)KCSH z%^_iBWO?Vvq=p0M*~rKw%GRxuu03EwM9MmbpL$gFdS9A^cOXdULl-=|<+d@9thhwh z14D2xu_j9Gs`>U4=K~>xKU{K|4UN%ayA6nydE0KnC(8h`PA(sK*&9=$wgMzYM4wBnh90`b34yf-?XoRZQu zrZBtJu7k|2P_B*x98#V@(F70)TITLxL*4rKVhz;0fx>%1p#(2wUEDUq z3>|@r?sX0r$BoW#-+?etsNK17FHar8FBhg9{z_+2LR>uN9amp4!C;(pY5xw#c5S*L1x zJ>QLBCvv25!1KJ_q{W`mdIA4IH36?;+qUI`dra~rPswMY=xF71?{OE>5nh^h7ZUw( zi|!mlFCqbR<4A2v>T`5`Q-$!qN;9H-jlT)VjP$!dMAc1+q}HJt*XA7_r;kB;ltygs z`GiaBWPrFf&#`_IBXF!}{=pb19YeJw`T-lChTY0ytIw)RIXcYOBTo~W-$bBxZ?@a1 zWGmeE=egH0sMnrcTRrax9A{S6o3J;^HeGwI)$|S9sKIf-WFT}dwwOKMe|xCJiqy4C zIW+;TS)h0q#5o5o}XL>tw&`~>usagt|s*E_}ET2z27;8*|q)2V45lMmr-Wm z9C$XA4fhcA%6y?L_w#UMVZV>#WhYB$j>`Dvy9M=u{7Qw)O~6S>y@AFiW8KDpm9@_Y zbme<==ce{n~9)UZQwy&Xa32!oBLqwEKqz*&AgyqYY9d0kPMJ$!m9JTEA2KQEc zh++QNd7z~R8N%)5DP@i0$zeWTf~Oxn3#tq#{#vPv@@Z$Zp(vGViuk6t)3-@x4Z5Pl z$Bf>h6Oa2j1KzWWe$_14UD319nRh$hNH+`s;59B?!hH{x*J*B0C`;VRGJObyd zM`uo>W1YUb$CyAh3K=Cl&(Th7(-AjAT*utETC~J%@yM67lD00#QJT23v}-+tr&jf6 zA_NKjroj-O)Unl~=771&d^^}E>L5Y#+o<E%9k>1g zd9RWcC0W3|Esx7})!wW&@HV-20QQhfeUYY<|EDG|E35##*r=z|$344`RD($wNK2-T z(a;L|!`Rs}(K#exzj|?pi1kvGr|_clVps-;xIs$y{kW8n;~;2*5VwZ|PN?#Pe2a~Z zO=y?%l$$5BzIYgKP1jQ|{^qPU3v}v(r}v?F$l4nqTJ_d=RISH44T2X+E@$Uovlt2l z!N;=1c7CG}OQyI-$SKS|92tX5`bi8pebL(HZY85$NhLpA} z?IFq5hHL!|eer#~ZX*=_VBY#|h_ZXR#Z5?Rc8=N~{3c-LZt_bYz3k`SJjnHKR7^}l z&v0$V(+ziOV_lft3Ru9(s2T*7i@f0;Koq5`rn5`S_A^Th2xJbKAr!vl0t*S*Y`2h>RFftxerq?bDF0I*9cRQ3`6mV+~=IV)5 zz;;Vc_3X;;AE-qq#D@B6S?~$Y^1R)S+Y?NR)UsiK5rFUy&t*sMoSPYSZgP-n3Qe2c z505F3=KMaGA+xiLE$*jE* z4`af`Wu#14ynWJHw+1F>*;;H-c~mY(3TOheU4w*gyysE*oX?_cMq0y*)5b{eM9S7A z$OZ>Kyvg#kYyu}yxBrZ>d844bg$T6Q-Qlq>$ZkJr+%tJ?)-*?O-r`D_%)&^|YBeW0 z(6VKEYp4;yuTVP#pW}=D=F0gLfA>!plnfn-+&MMllxu5@>)uj=h3|vmg=Au0Ctj<~ z>aPVXHo-(s3bRZLVS81K-qv$#Q3->rg)dd4S3kh5a!ZJcx&&?#a#@t7oEnqarJb@v zPKfUli%NHIPqMiQ9CE)%*&uovXEa)KzA3I^NNMK#)BW{F@UFVzrwPkVZn`t&0ylkj z#Qk%>7lmv=)eAz@#9j#P*#qfQ|Gs1k>R(#ymFNwH=MCvNy`k|_ zd8lTCu4ibJ0BTlX;mP|bH_Sm$R_;F&#DAhA5Yb(LGt!P1oBijUkz9Z?;w>Q$98383 z4f)k1z*T&T3vfn4SeT{%nIa9HMLB>o0w10J&n4QROc)zRfHOj*tCawAWnuUa3PEl> z5$Nr}H&EIQXM|pa{|~zXoWiIEjPBFMPwZ%a+;tE@`$1KD4=bGeTO0oS&#$Gx=-Rm1 zm=OGDYybOBSYNzvXg|3Rj{j^nfTsHcd&B{YKNg4B|G4Y$IKULj(QW?uDrvtVRqeNC znf%9~|1aG@L8J$!FiqhPi1g4^5^_VTS_+gL{^PFy{{r&=eE|V|Y5mfDY%?S9f6)~` ztd8B3$^4Uu@LXkca~MGfCPRO-NI`CQD62W)=^7tSm2e=57>s?cLEmvgdkW>Uy%qC< zBz^x>rutKmj6LCY?WV2?tZ)B(;e8U|A77TbbpEoH{+V8vr!$vvIWoy*hKdRbluw?> zZ*CFZW|0Jg!)j}JfJj8?*xKra^v3X=Xek@?wv(Dq$`Qz0%dA5KX{70|sBad2SE zVBNtV21W?9cek}ITV7kk20wtrCgc87k72*IKqn_B_ridbo{1^Ct4o66P|m`lNK6#S zx*d`dxnXJmVL12zzB}=MuGFiL?4PprJ_k;3r>b!~D^pTd4!DiEA!pdgiUoi^b%%#j zj8!GPMo^RZH**4Op_iARmm%2@S$aeS7L7j`!GD{Kyw3Lda~~Y*%x7ZfB4puAEq{JY zgbi65zW+^<<%1dwqt#h<-cKD@Qb)zjFcbc7rsG!={^Q%%Vz1j+%Brfa_YB6g%-Dg# zy8IMjx!hD^f37w4;5j%rdJV}BMv2g<$-_Eg??HjhKO294dAOUhVE0U{<+o=(pUtqSFEp0P?AA2HeJZHeoGcmm)( zVomH51=B5|61I+xu1TovLC?IKJ%-y~v^@Ps*0H|~9IrnuK(MNH>W&CHVi$jmZ%sQ$ zA!h|2(8Z9Snfqx3m3GK%tVy@TOD2pg-779VJL2|(4GzepMIKx-_A_C)CnC~RKpMH( z*x}av`vkj1;dD*1C$+Mn<^QC8frGFIfRihSgKuN5ob1rW<0qq#zyET#&|BNZ;)r0Q za>y)uyf`bpB|bbLmnv&rvt(SVL|anCkBtIbUQ%MPTBjslhZhyz`(_UOu^j&Ak^iNe zZh|jTF}S_L!no;SVZIJDF2P3{CZ#3XcF0;z6)P{IfNcS7$Q?x69s4*&33F8IomYpo zmF?=-;1QBdBJBTlQQz`Lr19O;} zs_yiCl!v2e-$bKQN5|^E zDr+(+Q@cjq))^fe1v|PFm*hiBi+j`F2b|yUfz9qR3o0YO3w3iWD81wz^xm7kk;Nb$ zp}C`xOGtqudPbNHCyMX+_$SnO5xmF}HO(?K&GChXeEkIgwB=JrJ+p?mCSvis+OG$6 z;sNeU7y}AKmW`NSqCYwqDI?oS=|$^bkV9-?nk~YHoks=T&rQDpcw@SEckMK++JDc9 zC)NapB*gvFgdb`y?jOPf^Ql4z;GCVu>4-Eh8Y-=}D<)z-?|4r^2bmcTV#RjKU4*?V zkmF!!{#*LpKq=75&57~UF_G`=MZys-ST zddiN;&Ku?CBH6SY@~=hluW#NVH~z6$<9WqjTNc1Sfp7(|=n8Q3DNHY0D|XDg$Myo4 zpdp*}8$*T!SrMdan@|b@8an_26&z$8r)UCl#oBF_xbsG?$glMPwy}; zeWM?`eKQMy5?ZjbTQ8=L`B1Je`%41;^=+T&Cfh?-_pkGl{uyNfo;&EqbN?z#_4^F} zzGh+ea`9%Sel#{#K*i{9@6@166Utg zS0nf;k5eh>o`^Ks_clR_+J6R$fa8ZU?h^4jYI*!vO4Ych3#nu2Uhc$hd9DDYF|@W) zDX6K@z7r9#-GAl1yp|dfd3I*IJL9Zg@m*BicAWSnx1-p^Mma`~xQTMZg@aL1y=U5( zQRH4?6r&_HwKzcWPpmUr=}*)a*m7neN=POXY~WC)mp)z-wCRK(^Pi@Mle5+vb%%>b zbC4smj5+V6oEaiUxSoc8JvyT1$PXos9=Spgj^G!1v?;+7P>_};e%UB^R-;_KouO>I zv12fHp2@6DnJ4BLSv%%4rI9D#oZeY$c{<0E_vW@{x8%I5D!t>5v4S5G%GdW^YqGM1 zNgQkVVa*txM~d9e2buk~^Ve5`PoD`H^`%62$98AdPF=h^kZ135*+{hN1cIosfR7SN zP+MG|{zXsS%xH+$A>!-TnCq}A$7^#~h^&Iry6PydzG6-AYrPS5`T0C^`R%z~^Mlc(H)6_++W7p%yUILNHXmg_Q=;B!<~)THVTj1kFX=P_(4aVcZB zn2b{eR~AbYL%SviWG7+Pv!g(v_c;M1P69V&UDTs;aq*!sN5|y&5KyI4Q7nf?U|(paOS@d%>&74Qu;Me-P8s~zyK_*pc1_Np;z6rfWf@6&*}X)fESt>36ncFa z$`@1VxavB8Rux3RQC68)4kI5772epA@3OP?cgsNXqJBChbfOledF zoP5UeNdYfnWW27gC>$g7Qlz-6MgH1T!LJAn((IE6TdQk~<%(;_kgSIXK3b zeR_PVqs5}xUueBL&H6z-5{1$jfZC}9yM8}edoGY{UymJp4it{R)Wzf z{DkFk_8SpYexXFAI_sy^!6hp|;z~4dj-`a9rQk+tBEw~mk&23uuCn@=PqW1mq0gI& zsk@p45HZi%r^>aQ?x+1znvRA(d^VwXnD^;REK8!JsTVpLf;ML#sx__X4;%RKQE7oYl$sZisY`rz^`evG(>iCc8))Bey|+q)FVlPy<|V?x{A z_G|KE>epdDpu#(EfiuJr)I5#_o5F!TUNsU5u@E5BhPaW<<6z0t(sso2a-&In?i+=4 z3`Fze#Ty(IfOnkAyT+3`Iy5xo#*E?kQZ(DBD&s|U4$Pk#P=RyLxQf9{jeR*w)2V5= z{-pQE;qvNUcMG=X!uI+3v(t`>8IivFrhsdmYXhKM>q&LO3QP({?&xRgi~F4Q*BMud z)_p1&|DZP8y->WAnZ8<9o||1Pae!K_d8p?i;}laTyZ*5(*6e+jodg&DfJ0Qdsg&%s z)iqBvbPp3KPC%9>j!7<0;=cTn+hsd0Gog^C(=a#d!C`dLD*;6X{n1Sh+vN`VE%wIa z-jeu1VPAH9VoI#Fhb8UnV|34K>o{iSV^qevQ|yAh1rS^*!4i5w^4LBp$%%7#r%h!n zMHdich2E1p5r(XolPjKkr`v3^Rv|gMj%WM493*RL-PiNUd41ed$3O1Lq|xt%DeX)$ zY43h2==6;he61VdavF0~K;TCuwL2<_C3WpsrcMH`sdtI~^d2>9D_xWFbI?b{Oa>; znL%Rx(LsT&H@o%|>$60`RvZS^&O?@ZedSu`L1|XcxfyE0qrFsyYsi)^r+Obe)Xb8_ zAc-C)(en>MI_!9_NA1mVI;+@7IZgUb$_s>f8x1i&39%~Hb!hql>SZ|I5t7?vp%D9X z7ag$Lu6GN$ukw{@yNZT$tvL`O#$E#lE&UqZM*@bs3$81*&moiXXB;Xy_guH7++D)8!NLS(JncT_49AW6v-fc#(wH5YiP4W^s+T^MH_3n9zQ77v& ziMgyuO}X&mEd!tMlT@?9-EJ|n)F`+ z`Zb|>Rk1ruB_Vrc-o~=p(u6d#-hvN^A|0K$Wm=-p4z*( z>dSgoo@YTu*^i2Bvoq)J1;>!#t47$zD2+(gsM;PNtPz%DZg94^a-hG-K8%{cT<4n( znz38qwwgS57`pr*D|w#TDTBcNDffHcyyl_9ih=Dn)oUv5`Ez834q7?}N!vzbczL?{}>@9NAza zIhn?((*E_hO4p_>0KM zEw;aW3)RpCmam`C(dI@ct`o9S$Z2SL`*f4lSJ3I*o3n9^TTegb{Zy(u0 zeyQEz3Fnxo^8vyKO5NkEKvxp58)cT4we(!Ez z;YLJ+CM+H4+YF~PsvvkG2@aFX&rsjxSU2MUiow+eG?x8H73yI zj!{06;OthAwsP(V4oT2IYug$<_#sWl+NdjJJy0@UE9|GhKwA>4ZA5_J-*zMu#Y97} zc1gT?M0Kqmeqqsa&~YbIHhIkYG23&+)tXGv9!`Y`nlg*YHE5(Dl(c>rA3g~zivC3k)5j8@)hZ7!worrSE<6)nNb;3P2 z!j`*%Qzb;NTO-0F3`!PZ-W!nly+<_spyYIWV( zon^YnWwuZO@_2lF71G%`cgT*ms~d@c?MS?=j6{r&&vT=>`L6)o94sTlQgawGeWCpF z499z>_|C!3S@5$13OlUj@zZRHq3E}uek+?!Aw|O4PO(H#Dosx<=)F^Je@Bupu}-}f zOKN-!qkFFn2V}6NpYvYU@hlSEP?P41XA%fK-%W;*v$~e7M!_~&r{VD2yqj+&$x0Pz z#CIo$r`(S_u@&53G+~1?g_5Y^ZhcwaHgEDPVVms3>fJ*u(rm2x_71%%THPBIh4Pj$ zEI0BDP6Ke1PF|?y65SP?soAruKifzY?$HD zU<4?Q;tn6{M(Xlt!!^}eEP9t;`xQBrGM>Rz7&4O&b}OXFfrrxnHs-A06>mgB6m!Cd z+~UTuOd*+=5B$_>aNydVRkl4znblkPiEP72>Hg&;%lov}urf=fDPoJb}|9e;rJ`dk0%#SV3AY9Dk@IDrv4ENf@ZPyU3EN$jFq|e>r8&iTS@a?jKDI>WjD*fz3S}j z1WLytQE1Q7Fg#P3i4dApe`BAF{_Lc$QNwvTp~@jrmK8sgJuy7yxP4Q9(~Ch~0xhu6 zlfx^|PUD5Q@QI;`1uv5;mz%E~nO+;Ck(I&Lm_%z#Zv?A-T$I%;d8c|Zm0cg7nfr6o z1RSMIwqxelk#Qx-*%emqYf`~jOAJ0rWylFwTRpy<JF#)Bkcnbi-z3!x<&c#q_u!&H?lJRSAuHUc$hJ!7e)D_e1|V~mX<6G*wYe0cH^ zz$a1@vPJeX8`PmvZdw4K(UA5uonBHsj{LGfM z2XAyG_nDIcGPo>oXrLAv(04$u+7d?52mgyOkAkAb*c}_SEHgGxT*N(7Y-g7ANNZ>3 z_^OlHEVRiIpo69~xb-CTUte#}BD)c_PX5?mOQ_Yyk*r#If0wiBGiRLP7E@<+BGeK2Wh zllE5jYx&R>j8fQMHZTc-D~;NbbrzW3z~n%P-n{!n6a^KH{(Kld_Swr2nTnT9VppYK ztBV*i+>bvK#md|frEe&0BYqOL9>}l5T_~0Lxc&4xfCuNR5;m(^jb)46wZBMF{vB7- zhRuY8L4TjFWELD<*W@Q%p?%@pj!6^Uw~<(5XRP&Ja665eb2P-UbaY3AKYFLygSH*$ z@W}4sEoiJNnKyW)OHcr7Lgo6(waxb1imF84QAY4ee$$1#1{d}hjLnaAvTvs=mO+-VxYP>EiJ`TDoJBNYx9`MI4Z8x5y| zQK14MZg8UlR*>V298#*aWLFtTZy_wE*FbGO)DbvpETqor32D*?#aK;D2_chEjjvv4 zp_WOEtP6eq5I#E>!bQ-RY8{ziq#dlK-{(!-9*?+rIB)+xgI4`#r0$mYC!{e|H~PEP zP4v-Bbt{?Su^mr@`h)7wwO@QY84n`c=tE_hI~Fyn3)*pUnLKf}-9Z4hhVt8FexmyI zkmj!~)Ez)wh+OF1Ut+tSEnd629zRZb6&_))tQX&*%GIYK}Pk8OffoONyu z@8)R-CWZb;Zn*s6M>eg6F}!4j!F3#dgNc&;2wv`N0yDWETiK6-t&WH)>y{b&1g-dvt25Ix*4Xs z)jztnX1MWVnk!5tt7=!m&xrTf&I?uwl0jo%#l4wukLS}GD%d|3BLw#7EB03*h|gN} z#ye(yLjNHegkiq->VxwrYA5f@?C^r`5#5=K;qwwPsLVqY_upj?Z1ma_7S=d_sB5ip zPyA$N_NA)S2b7Qc^+fTBvZicZI;xV23IU{u{^>eML_}>4p2M#|QABm?C9aSnnD z0+U5>TMP#T5A1L_=jbZpyBr+z2q}Zte9v4cqr7h-8hl^D$e|1trA5JcIiR++P`+Qw!~Ls^rJpW5hkTQm_)RlnAxIwYFC)>H%; z4UO>r6|)bNfQX;D#8GCULoKKjGhgpz!Ri zNX~mWR)l~mRagBlWY=J>or8o*14LvdR$DE>7UpxI%|@^B4JB{B##>L^`&9&6s&d2I z?1zTmt8aT4U7Bk)easW2ch-3B`IIdLU7C>4!r|FOo-KMN@g7e}XTS(}2`@~Vx z>4gTT)f{>C<$|m2#M}LYvjd!iIyMp{n%tVFlfcXmw)=KkNfD{-+96xx~okjrnEkEA?_H5oRhgb2%dPtIOY=}>>&tyd{J6EdWX?A4Bt>J@mZ$V~%5sXAj$T%p zPCX%3%CzIEc)y%4sn?)Yl1elc_!GU-IcEh4{39gm$rzxcK)GMa~#)ufof~OVw zA^0@hVx$y0935?QsWXoZsxN7GIY;tc-Zvemiq>3|H{m)tqmqSL0wMh@&Cu4txo2;> zGPAO%3WY^PvPMS62&Q|uHG1#4XnST$CzHGAs1R#am<$`@^Ew8_XASQ~EWR2f#e$ml zfmlQxHOR;9ym>}l-lN;x_2${Nm~)!dWr<9xlxV5qny-|C0v6MdFA3esqO z3~|TN5m*%YD6Y9l$Ag2;VQcdFuIB<*nWtyFNz@UFP-$k<)edQ?K7}lz_g>PASoI|z zA^44r?w%KE!H_rJ$A*Kvx>k{{eGXuc7?EAiCTS1*+7^_1=Np<9w&U^WbV|=eKbZ6i zgsz9ix5cU(bleL3G?FdIh$178O8`EaSz}gD%NAo(tU~2GkM^A6up^u=y{op3Dn}*j zpse;tPoLb=$aQn8<%FtssBBl#fo#;tlsYXa+z;HV7#djg@o{18c18$pyPR&=i=}j= zMN6)C8r|m(H;8~N8rX`GSee@}oL<#&5b-0J^H}2tvVPYPnrM0TE4^P=L_{oJ=9 zL-&4kmJ$47?HdxdzQV@6C7zW*9H%yUp;zio<7dTY5_Wp7O9G`Ca4sAM`W~xdX715aOQ+ z^9;~*W0E|G>hYZzW%_)VdRpJMX7u6NdUwO;<`0v8Q~H|&3;aUI;Fzd1uO|-iO^Vv| z#?Tb!WOT|=u>-EKw{arfB(I;T5nO`PJ1KGOrZ1u`M_vKuA7Ta2qB_h^_ zd;q8U1np8=aBxhrJJj@mQn6oKbz`@22Qw#-bBA0spY51UQ1gU!yzC4cZtIB_i-nMU zSuuFf8TE{&`@Q?&@~m`dcT(y6y{LdHx#2S1tA=lTWyP=Zh!pHUDHS**`{D(Kss#Od zy%xM(D9UYgWsaTT0(0{CL~XR80TsOm#G33(;q@H_r;zXahAQYWYqo=il== z6fmn58ty+-5D<3UodHM_m#3#z(HMdkwD-aGU%y4x73vzP9k*`A9fap)G>=J?1kqK3 z3ncZ$D#cWG4!SOwEGLz>qUW_2U1jof9(OA$#%&yZZ^eIg*q%P)9z${ewf!{`zPEv{ z3})hcp@gRs^m-#oCM|)ZVBs@dIz`fCd-HCbVfWC5K)ZS@0&r%II$Mqg+twoQ6FAMz zX3#U$R=CnwG}J=P!@Ka^Bsdq5LHhE7*o^dK1K`@oQKM$6SP1}iwB@X>ETDYk33X_` zg@3e@806MEu0;dcYiL)E@x%mU-+;x;jEGUG*l8}BoDjE=nA_-#;gPZlbfoSV0eoj8 zYzM=2Z@3cEqxDE(S_==6KJlcZueKyeF`i>{sF{v~$86<{1LNVAQ7-6F9mhq2X$(QM z@$nVXLF<*QzS`%f@k*Q&a-8b1 zTN0>u80YqV2NM`IL+Gh z2sVDg&m+$m0N{Wq@-=5`oy1QgdV>kLR%AjS5~|EAP_Lfk5Kia82T|vGktO$#kv9}B z3HLmxHMb;Gd=<^R_I4hR{WO05JlA#8r+sW1W5hL!Gn3!3Ktwd}By17S`X0JW>b8QS z|GV2*8<=l;mQ5^K@vyI$A)hf?x?HrG2Qx@kq7XlMS3-qC(n3>%2K}M`I%xihbFr6a5gdbvJ&^e zrxujwWdZ!wT5P2Au&Jm;Sx?*UKdCokchU;DUUjKuPjQ5$vx{Rr)X-s$)pZpAIyL9E zwc|fM8OP`SzU{^R{y~aD+s+4dm=%iSL^F+!C8|hRXCY@L(U#n<5z?%q!-r1eH3nl( z6Q7Q38&gWx z-G?lVlt!r>Vpm(MdmSxJ?!p2;tWzF5SnfVsb1!mt9E15m0#tId^|Dpt#cX{WQSz~b z^Q2a;@-e$E-D)IHUifIS?n9DO4&Qo<1rCog?wMcZFisw4E3xlH+@+q$wB*w#@P2-d z>(i1zwCi2=6Y~Y01c*ri_MYNxj1;4(digSabGD} zW)1YS7Aq+2`i_*~CK#V}f>a)BZ3BZq3D@XOIJZ#}u3~)kyfaI-oBk%P^~QrlBHk|Q z6$HkY8E81UWF*P%~wCQC*1VIAamIbDO&NQlAgR&O+!G?mHZ zt(VEF=`Vaxe@vFNSxu;Ydy*~b`s2$z{Pd6&d5D1OGvOu4BHLBr#30&xF?BZC+LnBs z%%7RsJ_LLAV1|#7iDW)RWEBg2Vp~&Cz_Z-a}77CwWV?!}H?)hHd07#P~}B)2V=8KiazlDIy%7f1#Q(J`xP zdE{w1tc(Y5^y+x5aNu}F(Ui-^8*E=+VR%q7F)_8-mI0xrz8J_J#Goucg9HXGEWJ-T zA)*xYeFTL*Zlc{&mx1ulg#BWBqqt2a%8Q3E-^gNBk2AUGXGo7S7^u#Q;&U#@#b?1s zcG>z7cJFMdsg_EaYPgTTqzQW3bZPl(5oO+CuiIS8FDPz3eWjuLEFnll0Yj zzv|}^?vOrR!>xRQ6PO>*PxsN|QnhS*xWCrjpTlhY_GTQPbA(i~t?-M6awOF{OeLP~ z_LHwjeS%*sgJ|`~2C{FT4JrmkLl9gWXSTxIRP3yd_Q0k5{RNmo=~VoSKoYZ02IfY6 zc(yp(R<1Gy71PvWcqrGqP)GgUHD1~mgpWam+>Mw%3FC!N9Mzs1E|tcf`#`2nwmDBW zE|RIlYEV%QoPt!vyhXs$466QH$)1F+jE;57$u2Hrs=a~zl7Irn4WJkj-C6kZ&JEXz*Od-IvxrUPuazY|B9I=!*XxQ)vIkn<)`APtZ$ypyXaN% z>z0{wN5slMDCHxBsfS|3BlaRID>tcaPQQ`r^SP>RsU#cGj4+(fk^&ggkO zaMT)YP@JySjAoN^%EcnyX0PRZ@9C0n3WeVT>0Nd~UMO+Mdk$Bv^x&@Z^&4k#3ttf= z%CA`9sO=@{W6E=|0a}p`ol*vdg#yLxhWum` z-jmcbQ_NgC&avPg4nNQ#GxDHJp@pRToV07DlOIe;vGOy#q zL=7&h2+rf6xh?bPuBJqoKWE(T`R&%zKY)ysuZ08r1KZn?s9`8_Xl zG#kXSdE8{FEdCNE+lJl=F$fVads(wd2o?ESX^kq?R@(GP-q(Z-1o8Z!jj1Sbe3S6K zha!H=oZIsvnE1UEBd^Sn-!J!@J`EvaYg*}+d}7IMcZ}8b4|e1#IeR%=wWcj6lV6$J z(e~WBAd5+#UzPu`qeQ3qId*2VI2&#if^`lL3M#^i37!X)n+6jinKO3^*IXsStpVK8( zh_5M)++$MKIXl-I^yBN==P%yLyv^xEJ@D~G<1&$JJ?i}~oV78gbP_jYc%P){s`qx? zhMl}CN*rjD@SVIihG_(P5^R;LGZlk);0IX3%O>Uz_;J)3^Bk6LPr9BGi;_j8O!X8P zelN{^rqYx7#;@L@5#Rf$3IV8Fv%5&>U4aRZ%`Ix_OMU0u$B7jbH5pv&(=l88#Ix~( zH%ctOFFZS#)wGH!)oTYAd!l>-iw$9p@T6XGDy|6uAcPye=q-oTmSt7Y)T@?ww8c&-V_CcNpnhg<;a0|lowgy|_3{fV;W zgU)Jko0Auv6_9Lv<5ZfV^Ue(Ba3F}n>>Qsg%XL4GXc{JyyQoPvZ6=#Ykl|WQ>7JFG zn}C4?8~^=g5xf*q#hS7*)|*(iI5tKgonX8Vyl!5&|5Q%DlgskSc(N(G^}K_{n_-6T zDpw^?eJ;dRr{-$%#rJj&*>fMSKAs-xR&Lxg>o=N?khrTKGh-=y*$1mPpu@M*iod?epfBc-*}MSbI|PjBo%A%E3i0TJA_PL9+`Zg-Gz zImRpcS2Sc@+xv|}a=G)C<&v;iDE;0n_UgLLov%|?vDXymU_Kk-s2*hPg-NsYru9#d zsy*T(PMhZCJ5lqo6YG#pGdK?7dvJ%RXSJ0KBllDs+M3z+iN*vVMzKJ>}}3!?2dQk!#CttPxx(rF;J?2!0o^-*QAOtwKpem23+ zpm(SJI_v}8~FErS+UH#8pRkpVkcb%3#*?H4f<04Y&V!6uGnsI3mf=2B< zlE(SQL-T}70v2ojYgmq~fXO>|y0$xk4ihL9YXI&E_X)g-?WlM`fx5Ka7`0AGwQRDU zlWg0o(%kR_q6m|I&ce99!n*XRzp~pG7Er8{hb!5}xwQ$yf6q_yE(THsHYNNop}%ko z-h?U*K`&?_uBXD3GZ+>Gt$nb-lOsva`rj*z3#o9HQPu|5cc^;=V_ie_GW5ba0w9sc z-ZIM%e!ueDBVa-}j^kPxDe+WJe%x&a&v_BEn%VJ&!LpAmB1o?`cmspQLlr6@wdT0) z<5ZaV7r<-xJ(2#YAy19r@zHh3o-5jIJ>#WD-c0eDR5hFzDXk^yGjHzZ7+S{BZCyMEFYt*{6l*|X*5U&w&lT_1!kLXC}xkA;qvM<;a!$#XH|LP z-SLXP;QLLa?-=T$-u*h{q^_8q^&Nkp!WjNYo1OUN-A(p({%2@L>3stZ?dKh$`;yHg2dRQ2Mxqt-cgEeb zA;kqzTw4!1A9B_gm~YEIftxj|-2V+Gw(?!nYqUrO(dJj>fqEVH zfm@zn$#>azKQr?`gSgF3;oK1FEvtEp^?*cLJkNV)Q^ouSw@|KIRdL7_qWpzyRdPSF5z-Y zS_CU$@lT7Bj1G_ws(0ZM|8yhFEP`+J&rsd9F-#jn72Ypx@!L1?8=y%Sqy($u25azd zH$wjexd4?XqUJ8epk+j0zYbe?zqc3s9bpvxaOD}o%DLxqQVi2o&Pzjyg(PrQ$AN@z4dLVsli zLB|o+j^8>K_5hf}h%aEOf4UL&TxK;eyc@Hg6~8j1{$WnMGl8_gs0X6-F!spbv>(<_ zvWA-=?cPSNI^8oLMq&xo{%=^{1#<^`G z<)PCyBn;>z_6^#EzfQ`y1#bgDdu%Rzq`#&K%nhJ&=X5kZ;dJ2@TG&?R-wi8t#z3S1 z=*`7&pw9VoVNbtNLG)tWe=H*BVK-PjmyH|ej~~IhsZk=&-Sr!o`upw-_}yS2q2$iI zzrW|V#TIr`{3f7F{vY$mpn~8nKnch?>86wnG{6A80+R{oY(X!03*X3yQiv^d!2eFf zVQo8lbb}Y=jBM%u_z}vRVk_ykQNL36U`i9(j{j_68U}E7*N7iaq~f0oyFAb=tmq7) zKVRQqK-L9D;*(&Nm0^ASyD16)@KaPS(aQha>fgr}=+n9ouojO$^erp>@gv}yvP%Ci z6cMimjO4rjEQKbNa`OX>!O~1aW2-7fC)x7d5FtgTMe7MlE*i;$!!m51Z(QRp2K$^P z<|Vi`gAI4^Pm~_jiRcT5ci2!c*`5ejMu1Gumnm$DHRy^P0H`!}1C>Mos1)G-{eI`u zSMQg5__&o5M0f#(14{pQh4VM-5u}T0T{EK~ubcz#ujRfHb-v*II`}Ft&*HI9!);tV z|5?kx4ZhRyzzfSpF;3OisAQ=IvNVJ``)l*!10>g3w{+#M13feUx_pJrR1bwI$DN>} zTQ%slUg+CHml=XQ6Aahy3%;so*$zy9h8w;)>Fa2lO9Pgm`9Ciu&=hLiGhuvubUON; z*raW#QZ+0>94ZS;h5BzgC8`Mtnpw5kI!S$;6yMFP;|ab{`wHKezXzs^5JI$mK@OIeh$wGHV)!rEt_%6f!XCF5pyo;&PvAifM)a)KUN&=Tl$62PfbI zE`agZ<9~em2EYxwXc>UP>v|fG?3rP6-NXA+mB~fdwB@y zB4etLj#>cxr7~hfOpz^l{>pgHA(bhqX%itRH7hd(7?u6wJHUr?_b3P;Fxkb30piK;3Dqjh-UhWjeED}AkB{Je_tGeFnkScVnp1w`-Kl>DXs zZP@;$fsSs1d^4i5Q9QX3WM7-9o8&9zKror@a96z}%G)9J($~Tz(j8Uo(_&FHqId zS&4BPY4`-*D9dxKF@7)2xOp;TRxo?;H3vvR3qSC+B-DNt?%Mc&*n7*cs@8UGSP&FY zK$PxATDn6?LAtv;rJ0md6p(JDJ0>aJVbBeePRU72H@t&q32X25JZrE0eZSrx?;nrp zjKLUpT=x~{d0w$(jCAKp-P(J7YCjKGxkcO19yiaTG1@UzSA6nRNoR_D&GKmcAu6_& zi;Mq7Uy&B2(YQT^huiZHxlOX#F)>7Y=%;gfcdSBn1BA4zHFKFRY`}$bWX+{-X#UJvaS?~z&Ihx7-^1x`)STQt&#s}PgLhXKCQU}DyMn~b zPr&qorDK_%5ka&d8D`RfK_))}emd;Erjb%rRefKn$e;^GU-dH9{wzwL>l@$%y7omM z2@oZd%bUb<;ARcJdNgx!+)j+HvvtG@7M-Eax+MlnzHhncCAM&MzW6>@SobF6RYlPx zeSwR|lGkCZ!m64)d2KxQ;8%%HS$$gJk~YzI@q;Hkfbf4GaDdhIP|tm-cKIjJ(@1;8 zx#JO?{h8R|r)VbLZ`8SRlD6RrS&LupG&f<$a75#dGd{Dpjw8*NGduM?9&;r-5#am2 znV)ZPK{>sN+37DEv9KlR2ww;TfP;>K^kqP#L!MS`?)23CEmT-lX@0u%>&8f~&?DWk zSw~sF$1v{CAqs_*V}=dmW}^n)%{AW`N%G!h4zPR6|7w~uqf>A8LJ2WpDK71OWaHB| zaJki8Hp>RzI_4?O+8!De>#Qer_$-Xd<%!joIW`)Z*oNWN4`Z1}=ockBU4j6%TMZFC z@`)rDm>8Mz%<~lGbSg9KwN@g&Z*S@iEWKOs@{O+77V2r3RY`1SXp-snLj6O;6z`SO z&sILW*>RaGmH8xYyiXVu6CB$t42CnjxOa?<>MCfJ(BjgUm&GGc2{^m_LK1_6QfMUz ziSxwu@)|GuJiyM_cb0P&bHEPP&g)cqRe$a7e4k9SOI2;UJ^0O@C+Q$shv1!kdIU#= zR5Yx7c;sY~skYgxg9$UP#@9}`J0Y@)-f=}hc%O1l*?AikTTj%futM`_TsTG|MZ7lA z4!7((8o~rIE+4Dp?)dCrED z#xlr7U-!hEv+l~^s_tMCzy~Nr#iyL`f0#>E5f>Xs#iq1r>A?W`nd!ceUmOW% zmcocFpdWZP@pH0z?0J5!8h!2Aq%cRwBOJ+C6oxX0zOa>S@#BUyY@T0J{=ux^Hw>iV z58z*;Q%(Y6&lkILJ{+=*H8qk>?>bw+_f4*^?(YFy8EG%C#*4LnpQzNUB{YgiPSoI` zx=(RBpi*`Y4rgxv!;NOv_aG%VOGi|G{E%OxkL{!BIYz`(6+y-1?Ous0b01Ycc+jPaxinHR7W=za%ItH z4lr~U zeXls)*mzyg8kQr=!f`8(9%|#wj1CL<}9Pl)=|* za^X`u7ZB*^Xct?5BkA1rwoiQh$u=2ySXO|l(3FV%V5nIa83AMFXl^d%oY-v~iMsB* zcxo8{HAQk*jdtCzry%8KkW`Pa1{2@t4Da$hu!rzmeH8>;IFVlYHkMD`ncMMGoxA-> z8Tbs(e40y3-&ck3KEP$vZP{lI**(F~bbINXo5N;CyI#1gXMj^)Y&y;3LE>n8#b@LZ?pW z_NnD)5t}kCo$JZVFyXNR3}5eE+XJhgW+liZ7Lp|Haz$z~A*K<8*TPGcy}A|-e$^J2 zSM%ngpnmnkA#z#)#68*1klCCsxFL4h74m__b(&?l56tw3zKW^%9HUk(SXf5izdrsF z?RCvb(a{l`%4{OkAY!Qt(oMi-mXf=tIkSV1aotKn)83a{pOx_q=e6tMH2wGY0ZuBQ zfF{Sj6bd@d0E{rRhSY8At5%NmF+~cK8l*_KN zmOs};>Lmd1<*;wz3YA@7u6H-TMaHD1B3bAE>N4Z=LT77>J+pnkp){scpI|YR)<M-;dM)pBpJtrP9-E&Tt!2iYit+g=nQM5wq1>-z%(BFJP-K=x{5r<^c<7y? zO0~7Lazkrd)JNf@%Vh;2A}L1%ZGQa2R?T6Dq42|gnjzS>3Xi2oWd4Q+t{5Z zCMEz17elftU|r9?w@)Xl4X{Zc+p7H#{ z=Bq!lH(+)Qb93_-4+Md3i2d0`jNJGkG6IW91>(f4_dNjO zc?c|#>eV!T0xKI1O`;p~+$V@kjH1|Au^*ab9uvcysBo9oub(lE>*~sH0hvA8X*h2J z1b({^S5v1nu&d4y{m{O2`1GUovALK7M0_6$zwwLe>6Eh`Dm9s7onu*fuwYo8S1IMS zO@r{LNVg}}Iu1e!PaBg6^z9)`!ja@yCj0Qa5)o_Riz*NI?;{5m?V&y4^|R(t_*e~c zQbcfoD989iG>Wo?ly4++( zlIQRw(_(C~RC0a5Xe5G#!$Sa&f>-||X@+ckWZB|Qm_RXkdK}(x>>3K2%7?`m%YfF! zFd@!DZ=&bWycK_TrPworywwlM0v@sWUtzB&3YkoAnm8|Ju6i~eCD?t|;Jb9G_2kO= zj9;Wg?s6ND31OYAzuOUi*OZfZ$%8FT3osd?3uL-Pa+jHPpKpwEF7rvL&6IF^k=N(eaDx zY--iuw)Xr)QdwAt;9L>$^uq%TBF7%5+F$SYv*ZQ7Lz&k%q9fwd-v^rFcboW1-Jd$F z0(if)*Pv4sjW714rFsvsybebmbSgM+RDgOlHOeYB(!dFmnnwBTnesT-!0ZC8oj*p* z3UJSnhlWePc7&2P%E`Sv8jahw4Z9Ffndu(R#9ON{FObM7p!J%fA!><^!_y*-(BO>H z@}U;%@?0h4q8Ny6`?#NdS}xbh0t#mLx{4p)-={oYQVPC|4SqwBCt2b)Qtn`vsKk#N zQg(f%_xk5pN#rQ=SOCpZyl0vsz;%8J{tUyiy43>6x_UnJ{b)_j>~%lh*|rA>ZhdX4DGDWFL=XKXWM#e1gOrG_L&`$A=CuREM!*D7tzql{um+lXUKVUp1 zB1--K!y$j^F4i!7I-nG;<8$fLp?9^H*Awqp2a8v}!JKQ<9SqBM_n*U}0?uXm-}(gu z(y#{sFU?Y&#q~))?k=LMlz;pX#$I8tm?KPt<}VUMfT12{lHSn3A8UOn7?#qf76xBT z(>6$f@bLjwe5O-E<};h|od_R>Pl67)$aN*N+D~hpq+Telg>Y;KfJOB-ce@Nsp5hub z$Re_gus_@wu)$p?Da}*iFbGSKs)+^Y?7|AMfhCQ7xXTh|orHX{nSwbHgVEMJoN ztf~@pdyd;bQc;MYlzb)_$Juzba)JylwBd8b?~d{Dr2JBJb$J}T{H?nrI%!RUpj*Y~ zG_%4M?dn)}AG!#>0GwJh_}8eEfiB;89geH7j{{hEL(xoYmogq@n3(B3MPO<^cnUOF zdNPDV7ZVe+x5&30T0e?m=>*~1If(#Z?H@KLE4BR4w$9+QD@QEeWagZ^&$smXzYVHq z!Mw{7qP>52l{xcfMN4c2#M-0=QSf(4&?XinS1<_eK=xM`lQTN%qKmr_M)k6$7C%VD+t{d0fmndsEIjW5`%u}W=3+&f%txQl|h3^ zbNft!3qA!KjEvJV9|l% zB{16W-d-up*!o&#}pVCfDw7j7@vIBB6EdX0ioU#syl$@gxpLtQSt|Ry5%p zZx!`}#4fqanHf;``_K6Ad-Ybt_*1?v;fxW5W_F#BHfM zGW&hd0g`AstrLiC=7E*1F3k3=?Hu*?WWpDlLelVj_|i+|P16n6omq-zg_oiuBTd&Y zn%oan0C~vqUQOEB7-3o2Dh$!A{WXi;&?L^v0`j4}6?5(8s{&y`0nvSDEud$z_5`am@vn$mC#kZ2KV?^Z)! z=!*xO*H4_Pplrn%7?KQVZVtPCY0KHR78c;^fJXvyi~XIaBMm`3kn{&(rtcIy#O-H7 z_uQAFky-2Xyyo9~4og0*a$6v{@}s)~n7cD&LG7{Hg;-sd{* zN3}Tc8z6(-$m}*6Ya<%^Kj`eT*1S^Nhjf@& z2zyRcifWJHF<-$}+ki62jV^|tkxb+jo>tveuEpT1PC>)`@x6U2BPdzMeZRosQ`KL% z82KH1Ub6m0s5^}m&UVcs27ClhB*nnQ*RKydFA#62Y6Ppr5_zX`9B=d);20qRF?}{& z-Q7`kbM`v9xdTvWp`W&=+`-3S+0*rhKV$To--Pm7YUY}dZw>6%G#!+vh3E|D$bO0z zv$V9Nur;H&Eq;Wfz{@n9BS$`)lRjnPxLlNJ;tFjmNPK)IrzQaZt|>b`?oht%J~Sj~ zH(OQ1r-g$4k^(0lOOC`Kh9>qCK1&NZn?g9L4wK6%AOu&D@#dhK~ZJJ>HRGHlO5A;|!^AIUcaX&U%3 z61zCCd(*}=j#`be(2Ff!>v{$Uc7%0*7G4gBl>KhT3s2A*j(Lv2kq&w zLUGWxV|5Zcr+WX}OzX&OrdsEgAqfJn<9lj$M$et6VF)WJefqgsKZ1XEX}GPz z5hOSo#LTZ}+MhxN@*a0nRB^7HzHa&6#$N`&(}{?P68uhc#m)iO(PTNF86L*~N7vrg zobz@^+ym=kN)_whI~lO18$6TRhzo(#1g`3@n~5;cg9)#`yC$l5895lEnPJxkFYPf0281(K1zkJg zIRp~o4=}$s@5$Ah+Ai|Bzy-}5a|P$t(G`ZXutxB}D|mU0H;Jlgj(S1-*c82TAINs% zx(7h{4{OFJdt0Ktpo0sz>yMsVYgY(QZ{^5aX*4qpepf;R`!WqsfJFgyvb`X^S%S|% zEBbhvt{64#oU8ghffqb5vrf276d0t3fBj)9@k>~uT0fQmm1cv*aE{F00H8<*&CFG( zU*qF)g6)W)6VbF2J6159AFx?iS+NP8aTgW!xuHvV-x|cUc7&tRv-ch-BLtG2vS+5> zxI)o%-}|gJPrT4XM2)wQ2a7$3r{8KiOq9P1Pk_qDtoF0V`rG+esOP?Rx~?&Ccd%Sw zl|3eAf1VS!<+?RHvjzdwUc174JmYGd=fP5EVpj&={kmcqwL+ZXKRos$zBF6;tp6lJ zFdR=b>%LbRG+<63W^?`R31XBDYwfgUu;;<@tp5Cf^_eJu$ z;!6+Smi9NQ97~t_)*X@FR&4A-XbZqg5VpT zN||ru2gR?!_Wk8Gvy_dp05tNU*I~rj5-5SOE4ru7Ji9;eh{}Qqd0tV=Cg~%DdF_lQ zW(!~5vq^O#ov6NAOl+fZO6^{b#nA)tI6dpWE1i`fZyurBXyf`=J5Srpy0siX*?w`@ z_7wTRR6;9rNDs zlikO}!RZd)BfC*~p?C*tZuTkgJozaB(DfN5bwQIp3Z*NQHIdcaRL!y5* zggfh#ZxO4wOblm(dWdLN4l>ie99Chs4$I6jQWJN3fjA{PkiN1Q?SYK;)Sbw_X;Ec; zAwQtDN{0|HxU7`-Wyj^g{T7*EuzX-Y&;j=at6}@>D+xJcKFyKsX~; zc|H(N^gu}HBqf-Lk9q|4%=&!OC6VQoJU@;311x9N9DuI3z*LXVyA5c^1lNRx+zmW0 zvb6#io)b#4Z6VEMbLJ$yisiO*GBxU2ZFL$bV+(qVJ@b;#4+K%vy%89|t){;Ki3%yZ z+xgl>lV6HEn-3lL9* z^uN&3^GY9Q@d7^d zs|fr@ZA+BW0bz0h!|-_iUYQlcZ*wL>CkE;oCEZjYXl9ay#A%gB<51a!BY>*r5AV+u zf8|hMG2*1PnVlW!IK4Ym;ijN3^av@dPFV4JdalVRmqF(Y^0p7eTdt5P9w>e357j7} ziUOVVp$*$rZ$zXS;-RkFg$YsvEl;kP5LL@@)CY9r{(Gqv6Awu=amf-AW`x<8lE(rgb5?xS*L5{$BfKz z-OTlr3!j=#N2f5V%URsvvDQJOZzRILbWtg7iVw&pU$+W2+HL8Upiu5DpN&aXCl!g$ zK)>$o6gv`t_tBj3E)weeUC-PS6VQ4KWsH?f?4!vVNBAR~5@D+K+t+C_s+$J^eH=#0 z_L%*vvGvP~y|%Q9$M6!^1!>fWcq$7Xy58T+6JcxPDSF$HIBKWc1nYILBM{4SWcRn^ zU-I*Qp6Waa(B>{MT?eLP7|_bcy>_W4nKVutsIKBbk5EUDb$`%}dX3Cewmcn%0D zxm=W8RgN%qrTqTTzM?~Kb+F9+zDD7MSO(Q=GnVVQrwLIe2#Ru~kO=;bF&`}M?_omm z%xBLSFOPnn8{y->&?MTFMg;?5t8@m~VDNpf;^L+`;%%lC2TV2P>666=ojYFiu&*oC zbpCF5NIZw?zmK;1RomzE8fW!(h2L(8d+-q27ngYyCLQ`dqOX%=2-))(`ks;l4k%*| zt6lTH?wivNsKWdZEhmplv`m-EfG5mc1n~;z?2ZH8qOJPF1(ywISlB7b1wOT0b(6yo%okw8d$PJzNU= zZxryaE{QhEzL*h)PdW$%+B%kti`T>XjFz4oCFZ_#mj3_+7`x)ta8Y*oa^Vp2s`szCtm%DR=xu(KoleR*F*9hY8Mbc_vBR+6wH zkm}-3%{q8y?wntmUmx%7ymNo15WH|ZGQvBL7$(;rZ4Wvo9V-uU(Gn-Dvd$`ZsSTIP zt$#H-g!0HU-;TQ%oPeSc{~AP2$ANT-N?RZ!Lbk;FXDl!oW9nNQ=Nsg4+}6oLw6wig zwGJRfJH0A9_>EgL9)@^u4;d({6;8ohjTWOqS={$1<9U|@ZgOc2#&hv+_Nr2SKIrIJ zJ=m~P+Cg83w02#B1psGC(kM7gl0v6cutfYtVC+GF2pbx5b32Trh1!tia^YtNBk`%5 z(vsC`z{j&S@u_!khP}HXPoNE^A?f;^ooMFc(q3P^Y&={Drkt+&A~UrS5l;Eq3Nl;B zvvOkSe;oEThX`PEEGo`IavwU3G@LXT%Op|G=1_Oyvao)c(av)}vYus|JS5|F%pI%xka`OtO|EG_1{Q}>*rtvh3Ry=Vhu*50N9c}RZtjMk=bV2WaA_|!E& zZG%2ng0AyU_9yKe9#Ci}+wpl(a*p*8s;>ZtTnd1H&uhqJ^aq&-2G(O_XyD3f;9>?M-0|gv@8=E#OE((HA^=0 zb~}zNOQ4)V7l%^Lg9SrfI>e{vL*tXsEA>ko0kg9~u#&AtlCU!S8O;em*;t$jw-;SdvaCfq5GuD0^v*JK8bZ!ukJryUl1tJ@Ub7vAlPJ;bDSstg8a=fdlnRDQ}f z8dGHU%%SU?D|gGjLzep#js2K~%GKW?lh5{!t?g-~?|z7mw5eie;hz)L*5=Z@x&U#3 zY*6YyuV`|M?X{p;pcT=*HT%<8b zYsi_;Fx*9~50yx@E*nqx9a)V#RjctWN>@WY%M%{zG{ZS>b`<-@*edSlNJ#PCv*<%e z3}#sW>Lv4JUcnpu4LptFlssT85t?=fW5T#WP!+ z+%i>AEz^AVs!j>z0!|cFwuE778$)2KShwl_B0}uv}DrBzRoJyeQ-0;fU@yA4|-(6f`YVa#Nru(kW&Ag|S$%O!Tk{Y6! zf0xMrJ+Nv0H@%0<>SLeXF&NJLg>C(q!}GFx3EUPNe3IN9`V^b9OKyOkQA%u;w}a%0 z*fZCwy-G>l=`3q#8~tY~;$L$Iu!aG4z|Pg`=IzviD{uzv;u6s(@RJdqwGOAB{Y>B- zeoe5KTSw+W-l9a$1+;X@s3?>UUVzCz8_OA?+|_4y_R+K5Po%wxnWuM!6MIPR%e;(Is1oI-5!Sjyh{dXZumaWW;$N~UJHNslW-<@OIgL323_$O z**M}yM=>Qk8I@DB&avjL6JtR)DDzg+jaF<2*f!FD>&)PWo(TX$)Vu7R_y<9QfdY6m zf95CYklISMsjpv+a9*5uZPuGpNnnaibvG$v2W0PT`ub<>(GFBS2iU=wgf1Te(hX+Y z*MHwse|ds~EMZ&OSOqU<8G+2Fhm=l{>!W~-P}5K+aCsFHu1K;^8K+r$P3mV%ivhI% zzhL*Y;i++qX#P3?17e~w_Z5DkCeriu*S`=pM&U&3?Ea)DI30-w1v72a;hZbU2p`99 z*B2}H^+dI@IH{{mjHepH-*A*8o&<=s5KB|<{pBhC!+$%U;;L(tVyQTbzw2tcRO!r* z1%V`glZYX5tR3MY&g>BWIeTp}RmnC=zO*3*M--}aR78~CG)_l2Kjm$FWfqSZzTP+8D-ce8E@ zz>{`Q-!qq>0`@4_++bG@{rSMmPgOxpC%zCHv`uB_R_k9`OFsM~|`tDrne|jL&k2euV zdZ+R8|9lVsVYGkT5RVHTlK%H}a}>Y_ihdQIi~4&CZ>KE=p00U>MW(gGAf?uu zI7s4t1iM$&E1<~msXy}yP3h?$IxqvIuZ2tljr%{xzqrhx=p+N?&!Oj8<6lX4ckAwt zWkza(plw@C$gNJx$XFbWr__Ziis-U-3X#|29WSWJ5+*!?3F-=}Z$v#@sWGeXqvD~fdmgyJ#2lATLlPG|Q=gG16Vvo$0O;E7eGrFs)YW`M zZ^%ry9#m}KONkkGwPOz&ovTr@G$`K7~?N zz2hjYm)QBIok<#laNuCdI~;;Wj?bE>UdB3%OfJ=eJPJII?E&Fh?T};6YH>kE+)dYU zHK|Xo+Wqk<^~Hg@eZg-;b!e$(-wK$4wt+5N6u=CyOF{o>1A%LhU?ycwgYf|!uP||j zh;2msBU0#fdTKW|-#@AXh|NP2r-U1PVDlAO<|wh(vT!R*JgY^_AQ@5xy*x*mf7oqj z5aFP1n_z8bGx67GaE`^mWedmn=slB(R@C7iiL$vCd!0XZLvSi$gIg(PBMLnAaP#hH;}5^A;quC+|A@0)aaCIQ8hdqomty6s8qL#qD@{X?G^rv5g0 z*%!9=fCa*kAOGJg;`u!La$F6^c9#{t2ZmysE295^a-3!jEFPZ)JfY=hf(zm?$=VMyo#y`yd+!$~3AD;$W1q*3d9~ z%PWGu|CdApunqwgF(Y55PkDcu#&8CB6K6pPfJTd!MZgT}&TPI=t);u+JbS^!e@`&@ z1Gc@IYG8E}Q~LS%ak7$~(=^lWIPoC({B?Gb=~^V{ zk$G91{v^ecg5Z)rN#2m9>!%1~3?!JR#GWX$dryiJSkT+#Y*momdK^#Xgw4oV-v>a^ zZLCaVORaHUiW2olRSilfTF_sLO8~U$fAGV5-npxPu2T+5>j4bXV|_8!I01DaT@GSvXK7wzu6>VtW)?qy2zY6KW2Pywn3TTU#DMwS6lzwH6(Hhjt%C{xCWhb@+MqpAdrddQ{j zbL@=mhi8Aa7%($n-VV!=&2F2~zZ1?!X0N*di=t`ehZG*S83AdJTD{^QAE?KjW6*lBjF7xw4Q_`6r1fnUTw_55E4?P(-8 zB=|6z)bzX32NGqgAr`~%N{VG>8$6=Y(ZE*4nMXpp;^_p8%x*7dcY zE2~OrXw+`bHc9|o6UK*Vgwc*>$&7K{YYW7+El9p`m)Z>aDE)aBRNZ_^i3WnFld}8k8@+03@cI2PAo|g?XAYinL_h|fH6;HC? zRp)40&boK!ZK=PIW6e#)-|bnEA^4%CMlTpAZS8)FvGHN6LbG+%MY?y&F2Lx(ytI1u zotlpj;wxVpo_sF3KvQ?G^z zia41nJl%>-`R7z+Xh$F#DeIv8`!yEjJ+V zUb?x|C~fZR;kwENK{w(2tNm!qxjn{3?f!I;j8wGHoD-(5Sbwb9?Vh z840JxTye^)+$^@zbh~`? ziN#Mk8LrtMV;X&a;$~lOeViox6ICy&qI5CK;*uBQxwkvcgn3Y}FsTqldA_NUzLmGw zI(Jn}c(U9d;?M93eGdiv0|yo%oN?2=P}hAV^@v{hi%CJp(+IVZtBG>vIG}YQn|(k? z%CnkN0A|xxuRF{l-qH5mPmr8%RH;qW;JtD#FdV5n0O|s z!ysO!rOp?zJ%vwCQi@YtGVWZMQY2$pJ~4c?lBdV%zKWOAK3TBXsaGkJ&Rer43|=|+ z16XrXo?KEvFR7A8n>R~*Fv2ubGLL+ZDJ5pq`}r*rxWjPgEck4ldlOvewkyyc-c_*N zkYN2CpiIx@IxCZ@qpbyH4Y^HA2$la?86VX3@ZDun*lO2k(UtJV&dl5TV1)u-(KG&H zuCw^Su@1TFj&RTGkg7@Q)Iy#CY#3X^0C{K?ZswPtIA;EHX#|s#vBek11xf3kL0O11 ziRV+B72z(U1siM3G1T-wPFVZ_1~!v4gDxSl2BR~OrZdpPjvDfdzQlb%SW1tw(p;ZC>`!uijB?Tn&)_b z^?a=c7URTvY~I@wxiDoLC*lKY;ii%Qgo+R;Ou7vgCGlb9dLqhhJuc5^JnoR<#? zIPn27Hbg{+T^#0MH$lF;0llKuS3;-tGD^@%(fS!d#mDc^7s2W71lH;z@XWB2ySUKy zk6!Vc%Y)g;2Hz?$gI)?FUMxZ0rSAD(TTkPT;e9v=u`h=<>?V{#l+Fm&u6pkid6t~@ z57R9UsM0pJ7+eo^p3SE>Dnfx+tu+sK*vaJXGC=r!U#J1Rd^X`DHwzI?k>^? zYQ4RtQOtUdD&yGzsh6Il_mtAw=T~Ty9It|SL0aljw%2HyOd@ADft?E>Wut^nHv@Ci zNnkDB?zu^v4tZ2)0`4v@AH^eWo@_=%m_Q7bwopS{jv$epedWlQbRbdT7I)s8xoiT> zC*CDY5{R^w%v&TU1ZnRhMoG*IoO_iMq3o51uSsYX(1{h%pk6>vWxVsLBpoGnwAGyLtYMp>gl&q0MAP6Q2@BizC+KQsOi#KRB(0?Qp)2 z0l1{E*Jhf-_xy=5T{m{b{D*P+#L}t}7J%+eYy%e?yEGw9sczqP9lCqDJ_auavzc~; zZP#gajYLWET48`i$@poHUp4a#nM{wf&~kA;xOE+o2#en}MWi>+s%U&t-P{$5kIuCf zW|*8kidIw``^-Dt^-Nla>@DGM4-y_8RQpB5=S*bY`8*`B{d%IRdjmCV-cG$j7(b2~ zm9Tt#Q}JSE=hGFj_Cc?6>J+1(b1@TMyDV0Fhk_xqwbrc2&haRh9f@RC zn9`jKGy7zqz$wXjrKcr(odAF7bXAYN3}G#KVnV~=(pfLo|N1OIVYSYxgfo&BmWBKX zhmlkDwB{_IJ_H07pR|GI&HDl8NyQYwsfpU*(T)({pc~hr9m|qzp-;Yrs2{EhId;p? zOqr};a876~Kl9iKG$v4POVew{!PLM^uQanm>SfO=*ep1LWDhb#PSn9`I%mq^Yrc}3 zwSRFSI5!Sw)1CHlbV()Sk+GawCkM0{uMWzrO^T@3q{~VzhwP;F5J!do$aS%@I$am- z@-$3wuaPf=ltN23H2UHfjpE^gPpdXBuRS?{Ma>h2*`51ObUJTvB52m^`fb2Zt@z1~ zxn7-?FMXm(RS;r#O{hOj$$$s2)o+cIoIX6h^~#^XQUHeMuc1}q+I98%&vMThXV;q3 z!?SI9l_8kjc12-Vv=`)u%2DwtwH^ZLw_)~yG@SF|)Y#DWFzT~c57C^R-i#m%t~?v@ z(@o_#Ft9Ao4&T}G4S0pN_h;K{Be)U}dWGePWjvo9xKR5v9u&0LEXQVe^5S&cwV6>R zMg>fay@b6(PC7Nus<+6HA)%{1lp|C886&vgO zjW;ieV?J~gAMuRjF1Ko2-HVV-c*J)w6Q{Yf?MTRK@kNLm_S4;VP#UsUxL9y>dMz|W zBlEnnt8Db##~=h@Yg}vit!h@9rS(ySus+1BvQT5RbGVhoSW;4s!D3Bu&zHo1b2wg| zadcFY?XX_`%0w+tg%OeyCzS~R@!;R0Qt~RRQi}mTjOBskYb76K@|fV@V9EN4xcBg9 zGSN)Nlu}gZI?=aLj31-$^>0Op_%zIo3(SXJD}Z06@0h&bkSK?0*n3soS!dCwc?AVgxvB25 zu}oS_Oln%3K(#sprWxbj@puy%jdJL+Ujd*^dyb(mCA-dXmEo+lWSVrV;p&}D)3(dT z5;Qp^8=IJR@na=bxXhdde4j0I*3xg*F>z;LEHE zrcXA=^CBG_Q$Aa2hU7*a@NNoubW>F)n{vSiIB71UbDptbpfY~x5{t4fbMseoDZuRX z7#U;C9bblx<|aCA(5t*tun7ti&XD|d8wG%65gGc}7il*oV;a`)bCj#ImBT~Md%b2( zXUl8~lm!oWWaS3C(}+$+`tLiV6gTgFDaJDoK)sFX(;mSkpXeu}>>;C(^Zxt**~^x0 zdVcEeK9m*vVWc&FMfs&zF8&l%_I>#C!=}YtVtVGri~JtFFCE;)zJZ*!)0FW=ud!51 zH0Xnbws_6c^6Mtm>3Yqxf`p``qyT8hH%SuWJmOhLgw`L48omcC$t)K|&cwCbC*RLM zdzN*%WtHQtmD$&;<)B%GDDLgBUP0mBOjryeK8?@aolm2U;GScXOTdP`xqRyH7;M0Bx@n1Fr_)H-aIM?fyxu+3U>l;jQY$!s4VcNY zmYFT!Uzl!XQPVID|#9+(E0sd;(o^?J^@jKn&N(e_@u9p_wxn>$b98Z+y zdvFv_!24}-EtoLm{MhG0T2QA9T4?EcoX^A6%~(BaYwPDVX4kfTzE$%5sQj)KfTR<6 zE%Tcl8p3hy7aeReuQz5yb*r!Zr_YL4bhC9k|;kVdY9E*DpFMD z8K)vRwSkq4f67qleZCxZ=_-5>B~xc_^mV5t^)`Scfs?Zd84QoGk<6am*)n{~EjnkP zd*|p1Fs8+ko}Z01(}fA1$;ESQSXVA91j3j}Naaz9Bv8_@4g&jJOg!KN^RGKVwj^CLC zYADs&h0SHTTTRU!=jWy(LCt_z@(FBEm^?XVh0k>wZy_y?zkYcoaY^#*83s92r!@2st#Re9rd2xHxJ07@e{o z{%C5ojf*6?WX}rKrTec=YReB~iLU!THT^ZvM7zdk`&7dtW~pW=EZiifhedtj zmos@%CGANE-?qy}P=p0!(15W74H;Y#~Ea2?Q+$Ry-_Pgb0Jo) z`h1Cx?V=m#V&M6xs&aN*shur3wx!H6sM=$R?C=E{6)*VUlLR8&I#trv{RarT03Ex0 zTv;(lSRukBUMUm{2-CnigVyZcO^Ri?sg}yU%@IPTHZ;U<`*P49 z9OuU-^ZiiL!SY0zLZU1bcDkl(8}}**-0tq}Rd`9g-NLG7TiFLo7x7od%wv8ML146> zgq*_{hr~UC`Y|!a19my3jP+)%zVK4@=%`Ir_LQya5oO{JN z_n-h|m=qwfwZ9_0c%IRk8h90$)5E*U0CcVaqvSiL9i}(%_5XUf3wtcq>^^4##T`vR zlx*(k{nAnTJ;Ou2L~N#v?fxehRnT(B(C8bkmt!M|rdDUf`u3-1?Da6_uAh%iG&Pk; z!-Rz2*q59om|eCeX9mfRB*=apMr|o3ZJ}?t8)R#V13A#P zXIo_@IgI<&6K^UtUEVyzTAOt4CD&MAlxZv?LFiG%`=PZc-?P3q?GTp4SyhxAB|b^1 z^GEye5#RT9o=s!)_ccpMxFbSpkT%ad(M`gotPS3BXL}F6vY05-Kq+pVHTRg)aqXHv zztRWVhYKDDXNBO=#J8uC3RZsjd3AmHDSG-e)Al%Bvr_4#(hhfZImySMx_=b`AyOty z;{K=iEKV*N!$1y&P}ZM6W0XLr&Z(A=jy?*mwe{=BxGWs_@*!FtdU->}mFxwtrwSLf_f?g2g7VRV)`)}Y_Yoxp4=8Gg1T6oZ1VFhlA8u=mzcd1c$T zUesZ`_y$!e~Km;717_;d(gU%m7_-F6|7Yor=a z$73vq>j%~)R=WdtgRrMV}g0|(oYt#t`x7V0eJG!cs}!V6-#=CV~axm(IR3Ucl&(J zl0<#P`lIhwUejjs%#VcmxkcFjTw6ktPu{4k+g?v7m}!;wa~rY{(W+Jp3-y(xj5e8N zuBN)U%FRKvn6c8qx|vMPO+q(Lx(eZDF5`&56hFOKo|RRxTT+FIH_ukO*kmnqwj3YZ z*YA`xhQb}3qFf(d4}X4bI9IdRY4;~2du9B&)k|T>D`T`s6tIK~c%Gl5v7ywZhcZbT z-;#rsqM(s)7d~2*KoDjS#kMP_E-cE)OY3up< z&wc$Z>+47BwjC%4q6U4H3*#8Z@YMF+`#**XOjIpFb1}-s)N&|RzCIUR4nvd2^@cqo z1uCO}L!CyYbBg6c!vLAQx>t=_?+i4_4<@LM5G9eE=b!Z$QJqgLi0dfl# zRqD2TYVC2+43Nh!?sK64V!YAEQPsG@j!=5?;Dp zbXm;|7eIk|N_72Ef+&^1^V^9^;d|YR1pw8#CVqwck)?fo%J?nUp90{07%lc-J?3!T)MMf)U$(nG(b)o)(s9|$)9 za6R+4TQx@6<`?G6FccA{TNPOL6T1vle0&T=Yi=hvkIzC0xH!uOM7QBB7k5!9d3`Tr zQY4Hhe~ulR8VE=51a3->s6_F5!E`zi){Rg`2IDO<+=)IwD^A--QCfP`jZSrA5XyY3 zaP5fk(AnHtk8~X>m0#S*W#V;xc?nAY9AXe2-MQA6rI@`t^+;&&J4J&NUCPHivpsyD zM*3!-o87{WL_F`(GR;s$y!)IhK+0mXc$U)E5+nkPH*bF;!wMM4`sHJ(wkU?pUEvW- zz`!d6X>Fi7Xf3POek(}4I$v-s_N=y?F~zzvVCJgPgvBMgqlJ1-vOhM*E8EWab#A@o zxrpk;R?%eoQwNjA14zN8`D{rsDoMeqgp<6_u4-_I^7Us&U7_23ee7C|SB%^d;wDI} z!L{XI=R+<$dTy_tt!kysSs8i^B-GRY0}eY?+2bis+A) zjq?++vU>^&vLua2|05V8!en-~o@66c+3?8j_JAZ}uGK3E;Gl!?Yq1^CcUw;|37x{A zA*54JASx{<<_-+ay5p(V&#@w9qVydqOf{Z-wi%)3{eB5&UBJ|x^@-ZeH!7bvGk$ex$RKkmL> zrh0_aoRuKP)bqI#K}F!AZZ*~Cv!S#Ar)^UqvW-Ee0@Y>A7^Oug-d;h{f$8kt`h&_A zP#pOBF>2h5nhvp@uuaRUZ2R?YNSctk*PO@T%0!!IJc$99ud_M++*#?uff7jg#-b&Q z_=@kAHYqD#ZfHsKAQErIa#YPwZ5Bx)(Sf{J#Vq&`PvhILaSmRkOqSzOLC20b))b2brw(86an(vy#LI^!P7A7g{4_QODI&=l zQgGEOcEil)ydQP2bU{I(t(${@J6kyUq`2_p7IritsKu?1>nVcoV7r)JyyQrpl&GlR z>SGa)&Ri#rhYF-9-7o#3>=|FkMd(w?nRQMt4HA6-gah< zLu-m$%a7?J_HbEOhEusafUbFDj1#Z_W1{al0QY>n66YEY} zp9-yGJPPRNSn=HWm-7oT3fW{5CyP&Yr@redX7qLbs;U8(eHjNxTVFzJQb;zq6_1Wa zS*Mb*?j%=c@NNL1uJ8il*xzAGxI3w7!m4_{y^s>G`K*Q2o*9g?@u|?)ojGNCZLxr$PH zoNuvp>Di}(HuB?)1*RX$YceH);hI9ZA@qN@<+=jPBMU|HuroYCJ+A7j^VXoqxUf_38dBBQcglpaVO z1?OgHS$-7hR2-_`&?W_v>BRZ;muTYLcf-0hlH$006cw3DE%)LgBt}gc*TUyHnGnkdC_-cD&1xEtSJ*W-Gi z?1Fz7zkVj8LJ^?03U?k4VQ2p_*1ff>^7S+*2w@FSG~PLUf$}S+L3cRFd&|2;q@Af+ zoKzCrGIBU1@>#9{7moZYvQUv)chUWIM#Wc}9n@`;NMUAyS)b@vHo7iN+Fo15C4KKn z8jThn-3|os8Fyaqc}@|fKO};S-_^Fr;!liKoSq0gc~NZvkU;sCdJc%!FL~=>=PDtT zG``#h>I$5;`>H+SW3IOzN${CR8OjNA7+$G)M^<`@ zm;&2FJ6S_hd(HRbULO`Mp!5!4Zqzesg4fZoUajwKR7MXt!oA z;}${rYS47%0^s==zaiD2%Z1ud3f^dovjh`3bCu>pU5PZ;m>=UNOObK!OWe=!=pgI0 zArK(#LKQ(v+9jHS2=1mq?aCvGfD~3#NS+-mu^`jO9(@62vzApUTh`Q|N3dQ^?p6xHf-~$fgZ@)gXmAzSz_M9p+44uvvD1NYLdpRj^9++w)f2)$OOVXb)xNf zFmq<0b1v5`PEVuqvc7FyI>80J1(s9Zu?ReIu%d;=@;rO6G$V3zB$_jNwZRN?sU^=U znp&BeB82PY>Z7`~6MGk468_}<^F3p5kd9BR>IeQlHWtQstbuOvPnoA88hV=M4Lt=}fa|vQK zo%dq=>PZOZ2Igw*grY0U*t>92ozCwWP){?SPzIelt?&k5niZAI&|J`8 z4r9kY-&Co7F|$mhb1}nshtLnC8sVpOJ+DXUu^7-F-wh))eHv&vMpjxLq*B=+UA!$7 z>;C01LZiu~wO6OAu$O_4!Ar4N75e6quR95xH&%+>bMrP=Dew@2dQ96F>C-BA2b#SKyF z(n)o}IN3HZgFKgv39Lr;S}X#Fhv`BdYWYsu78VxJr7Pu;*>TO859`^BDt!!Dtw48k zMcsaEPNPrtkudMo#{82J*hhOnZIgdeTbebmMY80;3-Sie*^)u*pM@J>6HwJ#iVz`k zqW;YH{-Y|q-oYBijI+zi`B%X}{Rx=$H>ZOZs?nulhc#1`;dqrdN;Rq z0k>!vria4O+feT7?k5?o-~qdh;kRT~Mk(%z=z4~Id#+pWA}};`0Y5$auuzxFx_u|t z--#6c!4KXv133)xNP*RCV(Fs8(E%q*r+ICZMVoNKNJ>&NtG-@6ERmb60HSP;F`dVX zUd__y;<46MS{pEkT@pO_TZ^y}QEh&_`{mA14Xn@nVT>UBX44Z+I>4(FOL|dJo3j!k z>`?1!Skz#nHa2dTa&|q^S6sZfbFh)I_x;*RH*L!M5mBAPw?$nkNF7B8k;;%D1Ij6% z$cf+SXp!#+M>B=i(mGV4r~)-zPH#AKox+NOg5OBB@d3sgpPB(B;G6^qh})H&hIazI zBZ<)#O~;cTX(oq(qr2kGkQnrgnl>OFZpA!hsrro%N`hAJC_n1MwQK@&e67H-j^FYalDXxKXuxmfKca zz4&FHrR9h?H@BwKw8HC~Qd8+`k3Tn_EZ;w#)ZkV6mBW4Yo3h>xmqCE@qVa9viS&!> z$BYtikX_^y!H&A~LB1$QLq|y8Hg6=p0 zGBEne?vwlf_RPOu*bD;-xc>sF0wB=KPu@H&m6DjFV2pO&-sHa9Q-lf-4%>LV0qE!z zquB$U*`EysM4BW~ zcGHadG?oI({ae40$9LtjqFLbqa!grt8%cuAB z%V(aJRz4Y~pg@e&Kp5;7Hk6RVu@ZkcZQQ>TvetfNo}3xm{))6-=uYitE|mk(ULd+ZYgM6ty^iE-4NMjiIX zd#ZHseI>i%L-mp7t;Ih+3`U@@bqU-JRe*G>V0Bj{>giQ|%;Xk|s zl#1Cs*!wK8w5T9fC?_ekiPVZD?p-Pczgoj5hbi^eEsDx*a61-qu~irUntTo zQl30>N!hYxd;=^kJ>-Wni~rbqF<3xTvSZ6pTq;=VY?mP#;ztb?cy$ffjoYb!Kq)oltoO(bOLhpeTNW9BzMizu zYTd|tj1~V!cQ4xX_W|H!1+)OwFB#+#7gKy1#l(4PI_fr68p;t9(8Xhv_p@Ty1%R7w z@p+Vl{G1;A3|}sjQ%|#XAtl8QEs(`o4FvVE|m)VRz0jo$BoP&qe|vjI<~ zDJ%L%F4E&!^Ki!L^~e+~0Xw$l$N-3Cnl@D^zK$t+bFw;nLIAl_mGz+5D0l|JZf}l8 zARNLcErI(ecz`+V<$GZOghZY3At6p@C*b15^LjQ`QAvq{I6PN#iaH`sQ|qYIQkE1X zd<2jw-M2~;fYE}B1=a3J4N@__dC3mVjNU8IgNp%JEo3sS@K28l4HdAyZbcyBNmoZ- z&i^p2{$VbV*58??{fo@=`}O?KtLQIR+wYW~>z<9m-*@qUwzfZ?)pG|)u^%F7{;xq1 zq|Ctb?)Vrf`d@#ExHSO6jPkhI{O`rk?e4_TnM2s`4a{G@@n5%)`wK$$cVg)CCt?Tp zUjIMz=)dk$0e32&c5k!LA9lz8>0hGm#L!n$1>OI9F?6jvGlNJG`p>QYhXIY42fCN) z{A%PuC-Glf{SW)o^4+eeg=_ZTi=nUHiJ{kTg#XG5-S^`^wCImHgoXo*;qvd|6#vtW z4>OMPPVljZ-v|8Pi=o#6V(6GSqWJ%2t^d=&{J*qB^ymunPN>e&{x>Et{3ocLsgjt) zMEoPy7waW?Ezyy*^wji!$3i{y*5Btx#4Oa=yrqeG__MEKYD!c3;0QPI!@!K6=K#`S zD_{?DObWS2fssnUxTZx@Vu*14oc`%^cY7-*9?hQ*j9k1n`4IcT;)G~dSbX?$O%`<_%r>bZZ3M*H_N3032U zRYQ|@_HSHM5W^9awA~_1z)^aRcQ!8J9Nc{f6&Wr1X7ws?TLdZl?S9wj@rB9qVym1d z@wLmRC|W4&B$-`kxWJQ51Im-}F^p_YFonb&YeiIGM$-SVn6iT2&Fw{21mR-|t;)ud zWq;%L?7RZ_2JpV>bbPGWubs=l-}tH#+1h{7I~kN$pp=ga4*%S?h$<+c zkYH>p!uN`>r(YZ+n5w3TRw6j!8`1fNCjg{lj8&-~9bIOHYJD7+hsmjc=^KzCB$-an zn=U>ybDLrkTA&7lF47V6EY|TE-~eCES<8JA7>R7h21o>a+lir@AK-fb!9fC1q7U~a zY)K3A2AFoI>l5eH*zxu@rG;V8IzCp35sWxwZAjuc?DIB$ z%OKK*%XD-W=z_?SCf1Mj87f36y{nfRblVBm0Yu7iwzVFT8~@;V03QhQrvGN|781F$ z)=%3*_UAF{)ZN6$M2sh{8)~FK#pAX`m>9}yNBCX+4vhb6GQhj}XnXho8TjX?K7w&q z{gPJhXDQ5INH{b(w_dPg`Fg*ku)Wfv3QG)N9T?m=f`jTjs$idxg6h|1d71mg2$+x{ z3B!kCr2pi|JXl-+!g6g(@Xhlt-rrM+FKbqD)N(0xo-)fv^&J~4=y~5RM&!b07@h6j zj8ZMcF`+QfwZHe8{*x#I;N){n8@t#&6v%ZT3Hy664J{OKfJSH)9xBI#NaB z^JRis&Qxfe^kU~Wp4QeO6(VVdt#SuJ3i!0{Be)ruz?ou799I& z@R!dnjsT*cdSrIQj_)N?nH{{a&dz#3DYD^FAI+;2JvT-^kV;MQsXO!cpkt!!=6OWs zd+wMKghK%u4Z7#7ipK&p?kwhi|At{5z~en5Z~8@yAlOu+XY zCNzr3?zSEBd~>ZI3?~}1|HPEdDmU0{rWyMoAj40kcVoHkC2RS6(@@|*XyxA)?Vqyc z9yfRoXO{>t6tiyh^T&+?31EY*!bhI~K2P1#_d7qkPAKo)Mdi$uI+1&f+6esJMfMH( zUsK>uXC8FJ2#lals#oU&HEGtncU>EtqAxi!rfJ3iD87MBAlrR#CKYhrj89CQUd-*Y zSS0|D^ugm=`P;_}{X5kS=mQ#nM}IOBuDl5om;Wp{hlJJD^>cdKTp>OXIPb--KH^>8*xZ$-3RYI*H_)aQZZH`nS^D4k$0 z9UkG@tdn0*5C>$u^Ye;gDk|(}nmxjIXPY%q2)SuD26;7^uSYxh?e}hmH!-GiB&;gs zbZ+J;4y+G_36Eo=_w2WxbC$mm)&VGZXyO|vCM{YW93w^c>xb!N3Ls-r^T+$0!)I(n zQjY;Bmz z)4c5=%y&C+TB&2=QUgi?-a3U65r@clL8&ON#)Aja zJU&|?vcS{a)YUn{RUZlkFM+Ep3{%GK_+7wyxRsd;xTp3eDB3=aVRaxcTC4WFPfh zNQ)eSkqTfWkXPzHt5W=+eGQAmQTO|jR2thNwVK=>gyO;&-~hOe9vIP%(eM)*8K@0n zqM?yCD1{I<{W#-m|MV$6Q{dFaYDvxqv#P7r%#Js-;r|x=o;x%rJ6mL(qfri=f!!R>5Y5tlq`uOQ^_c zGB|zLBf+K1FhaZbI^4n42X~cJ0E%j+&FJ@c7%E?e$<@m)Rc4eL)tOUV8>bna&Cb|^ z)UKzRmqVqtcU0ikd&TC`Vp(F6$tyb-hntz@3GVwpK=E73z-^og+=i&Srt;$s_KQIY zRM_oOCQaJDb1yVkFKz-}i>DxAe~Rw@KN7O(>9oucw9!qPqq^WF_f~4Rb09kCQVk+~VDLM4;=WUBr z4|$g9TrT>xYrx&}KppfuK?}8+L6^iB#7XgDb){UJk(NjFAE5UgxxR38@RUhHrLa%= z&q`?wXO6M_hCV$v0034WZp&1qh4k}D;%hsBw3%%x zElb{*HKodX>g)L&`st|F460Dggn8n{aS7O(zOw6*yM0VweqYrJPw4kf$0?JmSFNN` zwZ>|RSvK~xy(CVbAHpP&%hKL!cxT~WlHi)h;jSG zs&SS?CjZhktd4^^$gK||!k|-uo@iTQ{V*M*Io@>`A}BRZafs{ZzOH&k zK=kB#1I~w}CXMJ}OJqe1VM(R`hzGrvKjc#^FkKLXbt!;AFxY$TAnbF<;Wgb6wbmcY z#mP_aeEN14r^|<}i1`;kObhm>#uZpIdoJ7sQ^(8qUXJgEJv39Foo$^zXA{*S%MGhz zFDjfEFgo63y?y%IQrem6w4HY7QAIuTe2c``_ox_}&+r!hoZUQfFraRGLpu^;M*v5X+jqsd)2ZLM3VmKW}xm zQOuJ*Q@wM1CHot~sg1aWb4E_JMHVD`Bl|3+HVfS4LI6mge{RbH@9To}ASEKfTL{C@87(CK}==a{0G2pm6Aw6wY1p)2|3} zU*LqMe;%bkG1$ZBmV>+w#(E*A$jHZ6CoK5tzTm;#UB>#G29~}yAFuX22Y~ygkD;`M znS-Oc0;s`VNgbL{(>5DNWFNK86ouuoTq@68SnOtLEU5Ll1tP!XV@`E05L2JwFsWkE zvnvzp`Ypr8{ujSfdlY(ZgM@C){;xpsiCL8mBQ(f+QPDQs({Z8VX>86ybF8Ki4k|Yo z0QE(`&C)#b9r6(|>s#NHROc!k=+Wl4jG(R?>!vf#@jd`&q#)Vil8bI=*w+&oMRFrt zpmSBq&ZvRQ?T-xeRQ(RNk_hOef#7cSLvEvjS)%SZVVV|)eV$?90R?L7_VufPP8u>^ zXN%Vjy9F8FdmYQ!CXt)t;iWqW@3hB}2u}CKMjCn^}h_S|ssNQMvB(mL`GH7Zt#XVt6>9DbU{V=;(1b7Dg+}t#)BtWU&?%OYvo} z=jpWW$Cpggrv=Jfv3QpR3pcrjal2lcoN+8{X9I*Be9QLaZ;{I%3fDGszKoMqW_7F# ziI1npk<7`rw41)|{ZfNEWI5>^F7I-Jul%qVN685JSl9mrX78#Y*hTo<^ONU!<5FA! zAbA~uaE!RBc6-itF_uOWH=tS5>-#a-@Mu}i=y=t@5K|xY@F0}=rh9=`$inI zG%6OEOK>zRy9}oj@j8NXz6%ok>Fw1$GuSX-t zc>`z}7^JLk&x%>iR|Bk5x0Nm=hjy47ZCz;=+ZBmabN01%Ty=V1rg5b?niH+zuRswI zI`CO&QSl#sBO?p_0P_mrNk`7qFml~{yRK%O`aGAdS-wga4-sC8+_4^BiPoMCPfcTE zlh)T+6Z@5H25S}NOA$C+*Dq`0i{n1>ZS?JaSNj;Nn=cAwdiVpXW`O9DzP49j#=JSiE%vxQbmp z%Qw1Zx~`{)ch}=GECuA$IKWIrl?^(L!(Mbd_oAci#^HB`&UXTV)UhuX@xGE?jKE@F zoWx4#pP<5n!IJS>f&=HDJc|&(ZpwSX)EvmCc-r3z%j#wsxKd2xA8psUgEQUaE^YwHTwGjWQ%b67*yb*$1Sva>kT3W=~`j!QX*;Y zTw|hw>V9z4U{6n6VM!DD?kXZ27*&%?&AN5(cJ-8NF$$1+6OK|5rhF(I+LVFflD^4r zI_x+w)Lp~Au$^*H9Fb=f}KKCZ`y(^q`p+rdb$=1ApQZR%=q`D5D>dc zp^ivQq$f~`kTXRa38ayY?qhC#Q&CwVJk*e(q-Y<7K(DBTpRYX3B3Pzv!}oSp*==i0 z4VvY2Cp`lIgh?hAXy%?0v_z~i%%b4amgYUnkz_-tK9YO+@)1bb3U_PanvH209*?k> zkVE`nOevzddW4B9dAPi5HF_dI%$=PgC2s$?yXxx=E!7hBiY}m`9!aPTXs9#R8w{`( ze&-8=G8;32F)viF5Z_kyrUsCE*=HxQ`a}H@fJQjinPyZ}XgYcGvQvHgjwPm?Qz)VL zk4HT%>+WXLbuV9VaDW=#03!*@yCya=sNHCEl@HH1I)X#1MDcWs9Uuka z@iw;&>zvfcpvXVZ81~b zYn88rYO)BT0E+)B5rE>?j#TT!3NXyQxJk-Wekr|1_C9up6A2vtW;{JnUirJq)Od&} zrn0pg-TAs^sDig+*s6hOU~{@Isl(!)3ryUYN!sTeoD*0#^{6Py%UL&&*9bFZI37Xw z_I&No8)moNkuNm`=SysHm8C7Rqd6&hjPU|DpV)WdZ*H5fwsN5^)P>Ud;M@5l@>G!r zP8*kQd$?Wr5m1y2oECf@uXji+wM6Yk(W1ceRo11*vUY#dR)xX1q%dj-dwp`tD@tMj|HU@r~B4@vscT2QJE4 zrn34UZ0Z#oDs=gRq`4?53^XV&Y8RX~p`^~5jA0~dm0f3UxHM*sELF*`vF^D5rpEd> z2coTq$RhyR`k#cU?}Wr)I>QMV!mrNAGK21v*4GOZ$B{??VE^2Y532T%T_q8pe(?9- z!>QOK#8IK1Iq#Y}xD+nE_D6~YG;C}B5L#by|Ev&X(Ug&k1d?+J z<>46UZ4S<0pcXst_S}*U_iTDSpq-=m*4Y*&qg5*_E7PK`dvlpDrMi3E zTWfr|PbE;>%OvFt>D`$(lZg9LW!~pfzcwN=aKHvBTq_I|>izKq#Dsl%`_jYPTTdGf zJE12>6c>@wsg=VD5I!V23dJRvUhI6eFgp?T+#xQK!DC-V@oJNVQUn%f3q9bK(k9rr z0eKP}GQ2QaXHr|o;K_#YaZe8x1Ot#V=j!v}-)wXd$2T)If;O!(n{QaG38xvq|{wlFyg^$&v#QbYQAA z4sOEFi0ZdIcxk|hhU|G{80P-CfuG34z$<0t;BjxR?3}LneyC4Z$Tw|R+WjsBoi%+Z zz<5q`R(|Y_*=`UOe110X%tO%j0|H_Gf=Q8k2ScfqTmPGyJTVd;x^1T|+_Ca&*~qAd zjIvRqQ@AzT0iE)K+t6M<0n4=dic$g@wUCl3+JDz$G(x~;Dj z$2`ZPt5wgw`g@Pf?w5JpdH{Ot);A|_=i-=j$siaqi4KG6Ky4p1R@8=k^+CdVipdXO zS1`OttNW1ID()@`tavF{qPlzht1+&;I4n9gfeI1;s;J8Cr!cs@5Nj}tnkr(V<2H?q zT&clx9wAZef^7;7rH|;sALFbZyO{Z`z!=j6(fc4x`PnuSXqV)RSig)`2aq!9s7cGnWw znNFwkY3}YtEKBYVEEz}5=Yp8mPh{9kk{MkVF0>8CptoO{Yz%52FWC6B z;TqdbY;zn>>!w9~Z}ab25d5KIT8+)h;A^;{=9W2+ za+=92cN z2o|T0>ns^{m|s_P^}=S_1b^~>Q1|^Zf;nB7!(5T{5}xQqnG`Jh)n$He(!lm}*TeKY z^Toy~oZpC5j9ED_>h=s9!2^dMPn*hSsVI&a?N20}=?W|bY+rp${DUfBegc_%YkTw! zi&}nuCu`J%vn~h7p#OHBDVG?d$OQ5%DJ$<26;T;@gmWqHwFRmx<&w{p&`a!Pa(VDd zqv4lp)S!+?N{+rb$(1v!D7FL9%>4|KMY@lu*OTzX^hviNl;MO_W;DsPIvgLre@(l2 zgRtM!xL}nhf4aJOY?P_Bet}cd_yQJf(;CVSk#e@dDe1XO-s2yGwXMuO=J3xIrx3sl z-SH3zq)ap>CPoqMM93?7TZoMBvJZ4$kLhku5`8g(pmSURaP5ue z+Yd=vUYT#%@w%Z>3jh=6+V`a^9TXC4fzT-8x3r1i_N7}q8O~#2v>)5jjs3(}gipog zbd4WFTk#Ph6>$aMtUBe+`$WEGRbo6oyp*lKwDd>E1y|N4+YbQ{AJvzlpUv9H!Z*Vc zIx*iE3B`$MnUCpW!{yQ%e4zD|0uK%hv3Q7U2@UGco;Ym<uL(9Ja@a6nK*(SqYZ&^Enefr^UixQ%3y!kWl6J<1#8Gn@|Ul)7?H z`Hif2xZZ5C`%cJMpmMZ)hdUmjToYA-x(a?FsS#?}ap4Cf>UUvs60+jW{INfYBX zndA}oMCoHb%V9ooDUgEQ+xm@!O`VOkU&QVbZW`Wl^NVj|#V9;+#pxD>)6OJ*CfSxk zM8UYD(IE6?hYmL_0K9K?kFD$NrQB$6Q*8OBZYKgyyDOwb8zhvA*v=F|Ta^umN{paw zRim|@W?wji!s*!Xj`)=VjX(Z+akV4BJcbJrc2~}@nycx1-W6iW#CLh0d;<~F94-#t z;%=~$n?wQo9rT7vo_p=45V=2<#2`M8T&mWZ2Yh^F3`H~~g}M-xDaNH%kjf$KLNQ(1 z0&(rsf6YhpmMkeqt@|+9AX~b4hrMp-(_~QjgWQK{{FkN`U(jpdJUOSr8Q1SrVxI3D zM<5d3lkijuE9Rjb^hSXU#%77_gM%#mgzV)e!)hG6FgBcG3IH|$gA z$;xtXDc*&>O@j)Gpkr$(<1P8H49qgNIQw}} z-|Fv(LpG;X2da^^ybIoQOst;m&kPcL!b*gmMdY293;{4x_1u2;bxW_adf4M=k|i%k zg5Hsb5oE@s&1Z`8iq#Ck?k9FON~huo#j4P2vk5tM0PyJx+qWwtX$Cd;v8LDGJ1X`M zX;|}yXcOe0QOI*Teit7QFms&3OA?qr)zVW7;@6h6@Up2y9tzy|Q%rw`_+Sg6o-oc` z`i&%FY9@kZSftbAMjo;<=}w?h?>)_8lK}lvfMdXO?M%~XB!kL+ZN4vbM(9=W)<|H6 z1mIz0V)6{x;P7^=VbhhKKV~&7H0u-1`$po>Q0_(l#k2?kezXUPe6$as7?A$lw_X$J zrh3tne0b@*E^wkrQk}=@YT0}(9;-(GYSy4u!|;-R6g zF10cg6{#W5e-`7HOtNpXX`Es>Efk2)difsu;)OXSWo@FWOl?PM8jIPOT->aY6!fLK z5a|f4MmV*Z*v{DKsIZTQlzT5Df3v!^PRZ)}YE@jUR|=)Pwzk1|a$@4_Iy!UnD!F=- zVgBw+qg<-Daf_CV)k*&7Y|#g#hc4s7boJTr301{04vR@vUYcWU?B(q4OS++CE>!}b z*f!gb8EfPB0Q@z1I=ZmsL!lFV9+w3LY* zF`uoOpFl|H_yOjSfEQUgmoVVC4o8TM%N zOX_e8yXOZRmA&8?dR-3@SYoVz9r>L~#snj$XHgs+xNr)Uu92%-=si~o#?|xz-AX$i zk8R>cmeyF9Bbt(|pK}^%JLZow>*m}T1kUFS+cQ!`z~A{7k{_Z!BU>SE@~AuY_9Mv- zjZ}>{oD!_w@!mLWBr~~qp7tH*ijhPEhvTAN!)_6)5Z;L@=E?iwZ_qxqysC@%nX)Bo zq8FsBF!bPOM8JpGLpPy8z1g!gLZ1J*c$3Qp1#6>p8IwDZ-{=^w-STHc{6BQv9JseqvDe5 zkY4O%1LR3B+g;Jw#qByRYK7y9pK0*`A zTL<=vumpQXb@zFW)a8Pa1gj8H-%lC)Rn4{eBTl?1B1V@k#>RQO0&QQ-kkqH-s?~D~d*JDBA|S(e#Y83c83F#-B}7dOBqCl7 zFh1<0+~RpKIyV|7yE&7pT_7S-8ePF0O-@RVMtlbCt-rc(7!qGpYqc)QkxdaLjeOGP z@;f5ogiuD;&6WQLxGf1}$3pmSG4ETN@-nU05EH+?8-I$0!kTQkhNXV>tg^Q*se&H`pT2kVk=_LJ#X*B^0av3J z*HSKOlaSwrwB>WbUn$lS>^IHEDNVOuf3M?gyJrBfTXBENpH;E_xYV$4#J{Ex{k@LL zSTxhpUZlqcH{b%vdEpBXsY>N~-JKAQ!}Z6%1py=g`MZ-8#0J=fkDWfzPsAN-l;Rg_ zX>+N$8rjr~oyE9izv9RCT5h1s{E>J~e9;xOIg7dF+H9sdTZI9YtJ&&YjZ}A_rY|9T zW~HR$uRiAGv%!r@9CIUhYec%)y>oW4Ua-}vNS)WJ0%kWL8__;Z@_h*cL9?q5kLAT) z?Xg68oD`4NxrC3GY8ns%l*|RSb_Y5JN^N0#%P(w&nnIBkkI!2_8W{jd7i{hVkZ>T< z2*zBFm^VE=FRhBo*V=wRBw|XgMcZa>!j`}naLC8VjX6WQsB^B9>bZRZ-`kSxHhHvA zi3G^Ew#&|+N=GZyK@k;b_ud$5(|2%GO#>2ncC?IGr6c~7upak**q zogm)sp@rm@X)?R4UWXsw&b?BzAA`2ES~3tp)mQgI)#4eE)YTCcxQ@k#M7BjJ79s7AKN8yliGqR%rC3skF!YU{!u zo%(9biUXLvgvBL7#f19uj|VB)I`!+i#i(vE>mFKjuaGURCDPrG$R+r;46#UKm_r+# zgWL~^;Ni{Zo2Bqw_9H^ZhLp!5nBqc|jA09&=ddeTgwEAoKn1=cmi90=7nT#jmNB2M zQ9)U}G@w9}kh5AZF2w=R^yzsh#K*i|Y*1UFV#F6tdL)SOjyR8k6DNDGc&4>y=QC&3 z)%PyRMrqn`tnd$_8JHNNgMuVSGBJ=6*^u-~HDk~RL0%}*ST z(5qaV@X9;S&q(in4WYrKZ|?38p2+*}xZnkQMZH(5O}x~t|D4YU9ygxpxfm66c%$64pwuF9rfMB`bTLLnQ?09Pq|v9mMUs$CO4YDZtBUjU zV6(XewQ$NSCRbaq?_7vRUWjRkgx<(;9F6G3q&y~%w<+qyLMsQ0kl%O$vWmnGDsUFJ zHusUJ$rYI;veR@jNKocd78kWw%?)koc9Io#|0(f{rtr;Fdt-OOr=9yI;O$5Bxe*Mm z{n_fdPm(gIFt!&)BQh2M->i-J>(}q4aRp^N3+2>ui5gLGWpH0SYn{mHzqA2A;wn}h zf5PXVPuB&>`z@ndgcu8+w>VVx)O%o+v{F>Wn||_fygu7Z{m`+)=2Ghf>>KLACGH&TQA@#StF0y?71o?Xxgx~S*lqa z#cEzP4#+&@n|~k|dnsrp--k>uE}k1xz9I~Vl0A@2b=K4}df4GU@<@nRU1eBq_6ZGs zA0(~Q>oVk9=9kCS5k=dwX-@+>bnJskRE84U;mu_O z$j5lU62}F?S@IXX<{`S=%~LX2(pX8{-oAELih zV94h`{o{Ec6!^o&dT3XfApIGb4=VRo@@ECH4%suiW#OK|!7%zy;f!vSBFK}b?Yl3N;gVNgM`uy0;4oY zcPkAN(hVXi-6f55cQ;BSJ#QLWAQ_X=m>a8921Umz$nblUK1&V0bS%95a!_ ztRhw{Th&@hTA^%+DXn%nrFYkt1+x^?A+(`=R+2>#0nsw~zK_sbJOyqsHcYL)RMO)+ zaU<6^+yz#gnRYQb4Ct-~r~;S@E=x|37xjizX8YZGz#@{S7XMAgq=eYpi!>?uZujRO zW9{|q?ON>pUU>|l>sHCofZ5OK-2&L38Foi}22dL7KFw_+^ib{2AV<4W&f0?(Rs+ra za^W}~VW}4F!u#ZGT=cXbaRvZCG;H@r9|?)B`)#`jN;f69{9P}5BLL!q>AgY{s9Ged z-CJ(B|4ye$A+k^TryzUD0)3?U+y>1v0d3|9lCln?=(B$ncxYSyK!;s^0>IV}KIP2| z{1V20e7B4WgPR=uLUfwwv}3sCrPj%5UTMVmcz3_5#i7k-Fsq9};b>LC2f`|Hn0*c5 zeMcn~7;i@V-Mo9$)zZ*|OPd8Hxzma5~Obpp6v zXFKQM}KIT6wuQZBU+&Yy#)nK!j&|ZK#@rzj9!DpmDZPnU*wlb8o%RGKbmyCvOQ z<{fYuRuqe|iRQe6YW#@c49GTc6t&5X3Db9XB)MIH_>a>^stMy_btYnAl!}pPx%|ec zCMz{w60`T2KvhqYgce^jotx@>Fp;%siLOSp6p({}*Oy&S)2UXTXHUywa~jVSW7p>M zTU4@s7oq2*s<*{;bdov7zRmmAgY|5ZWsDO1QjV1*p~o;_JVe0md^je=|0zfy1wi~H z7g8;M_H+5UdtOC%W>y}HP^|@Ma`l^o$>@hklJC8^Y>2^KpStOzgT2V)NzGlqhwD|> z7Nx&v5DQX&$3?!MB^^-q_8alm8p8e~_f#oPfVR{(mlA%0A-Rwf82Ui@FTb z{6dOw90w~oIpzoBf}3W!ZeN7Y~>ncgS+XD(a-A3w6$^z#{cu$No4*tSslu?c2^aojo3;Gx+C@n0o|pfR|KgQ6la=D?kp0`P#)R$oX$gA4y;4gP-3 zT^UG-+Lx^DuUZPZ)qWx8*-@bf%cZb)? zdo8uC2ukkBqIAe3#wre~uibRI>$}C7zsH_^P1kp!-?QHVmC7EaOUJ`T7^!V(LN#!*=@I zDWr0Oc!I&|l$5fU32vfub+t0{RN_i^|N7M_&`i;ii|D$8gGL{Az$glQ(wXQqij*JK zAJPrH1#2UIdL|$$irU`Z9aaL-EzH1pBjQay1MvPzZBS$tHTB@IS9l5JKanWF-Xrtz zp?$@?@|JAUpuUQRHj5oY02@c{i*A1-;1!>`u2tU~pu!Isff;HtBhLMkh`Ps9B&M?r zkWVg?Cb`3&iaDwxJ`=b%(g-RL|GQVeZ>{eq0Ev*D-57RBJIl*WNoUcaNGc1%G_T)e znY5#TOS(?RgipEqCu0CshuddW0j&gk-Ty$tm=dSzeZ6%6MY#-*-Hu#0;l7_*y1Tvx zayLAclnNDGjR*EvN9*NzhHUNv8lStL{2>mroC@PF8ELWT2x5^vLVtYLv0go-TAL0T zA2U0oECBXyOZ0SNGc1Wlx%YtW&q@K4JYc@1Q6m1(EI82$zNQY0B}BfezGy#0y!^;* zx>T{;mosU|9@9|^Xx8Qo<~G);fT6m;?NdfFBm(m5EJe3JNc_hp1&B^K0a)zr8*V>3 zIMDzJl8vXGwN@*;wC^*J+#Sx5_=S6dY*soSD+NR!609AJeMxrr=oQLemed^%7YMHT zcYg<^qmbJEE^xhNw*pbd*vSx}3oe)ggC_4#EESQAtduO#KGy7FN991KQNV=-BFBOG z^Jf$#z~U`qjRw2`kn7(pw)+rZYK32q@vuzAyz zRc3IsHK|OkoL|Eu9?OBZyl_~8P1@u??sAV5@EG>vu3r@Ae^+7BkXwO@WSIp&9`d>e z*Kq=leV{&ug2tqN7J%(5(SXA!o%k>&L}2vDG;O2g;SFL8Qws1+4iD}Pb8YOAaX_E^ zr^_I5-Fvz(An0{)IcA34iO^u5zoZn9vw$ZqoutHwC2l|L%~;3{2i}_;=;pdhBk*Dy z_FG?73Oni3sVtxFfGr5f>Zu^`@aIzQRsHs)I%+L; z#IUj(NGu@xYb5`AS9_gH>|f(Y8}O^KbpV7XcRWEXGVZOBfvT+vKj+Ux3U}Y%`&1v( zH9h!>Tg6+f0mu-LOea9~yW8Mc2rR?36?WX{KV?(vE9&fzp95IF> zsG8Io&L>SS^8)NHYCrQ{ryWIE`o{J9-mw?sHgUPNKw#0YC(vM;AqdJ?0|M!)Z}p^> z1-t*sERaZjZ@ZeywbmQRvzmD`Q8)Hk_&NlaTpbzuC#waOL{bp2cqdtr-2lY?WPSig zQuW%AoUD!g<$Hn+?#d53>Dkfl=~#bcu=)Mj0H48|V&=e6&`qS4pOj;e3^hLSD4=_h z9s`^xc-zU>X{7Hp+-Bv`m8gbk+|%O*rY8O3gXq0Q0jL-#!$oWzw(&(&`z7)5^*p>_ zW+;@$qS?FDFE@EQA1)qH+CCGzygK%#ms~3MO=rn9Y9nf`{1(*>NC&;N0VtMRRNVB4 zBi=Cd3H)~{u@2+f-MuTW)MpyI=ZF0iq7pt;V;Rhs$2RE|JY;-Pb3uOd89qVK0ba&* z*QmDb?AfZG<~-!6clCS`#hfWO_I<}brKaY&n7DXqNzMx&U-4@|R*~H_FPHUlK`i%@ zN#%KwIA~#{_H%#wBBi*3hmxk|T7(^>wnAhjtKGKJIYfef^>6~fj_fA%I3Hb+Euc)Y zll71tX?>zNpgS8#sG5&s_I-lSRW4gJ zxG9Z#Ut&1f)4Wg5k_A@Ru3fa_W>}%A=s-L%04_jqX6kb4#lVrTqN(FC&fxEK#5x}7 zvM?BOjYgbyWr>^WzV+*MI|Tbwi}K!mMj<} z#qn1OQp)lqSJ&233qLp7OAe1^)*hb504VLG(l@71*hOW_w|3WYxolEO zo54weWt9tvS4d7QAJr5{W!`s?nSfk_2JiBUDct5S?Or5Tic8K*&5s*8yx=Ela+`C* zCnI#1V7NCw>FPPc%o4M~`)0{5Ws9Re`4V#XO6_a*Wxe8lXm!j&IYlN1wY=tQN5_|1 zi?MqY8%-_}a3mJ{%Zt5S@s_jotKYNFpM(sEWS&Za$=uDD-elV==rG;puLjVB<%ErmaHzPfRrCc9}!GP@E={gsG+kDdJne4@^T{W^JS`| zA1~W79C!tQ%-}FfGa)<3S0`J=GD<}=CwOXIujTl*qwiow!FO{X#TFR#OsL??TpAx@@S)y0(H|H3%sm9sJDYz`$av|piP|a4YXa>zOcK3v4QjxE3XrJmVbX&a4Ev9Bo>Wny$0a6be&h?Zy;j zEIhV0MU{rm=CZ8=BrKCh5iB_wFsawMUH|%oTONJ`Q8y>F89XtW+O-D!PKftR$e;3F%VH*5$qT&g|rjGDA zTwd)WU1n~ssp4m1E-{3IIXj~>6*X0T^TN6<@XJq8 zN7uSd#A#j7an!qBV^P)s$!ek3ks%hVXPZ1P6tn+6JXEb0h~K!gE6V<$H$8XayPgVk zr4IP`7D#hqabYve$_;UcMVSBkfrMk6xa#}-3bmkf#E>;4F?W^njAQCt%doFt;bK); z-*Kl|>07hcw350-=8M?JytBOQLbw&iVvl8)CnbnwEcillhz*879w!H3+MXA~t&O>j zlrEIqH0dV?CTWPW#?i9TPsO^F8bxAI;jJ_{x~Rer8rfHP9OtcMTjBP@^ft4yyu$5` zjhnL4FxN%0F03mn9(7QTlVr?4Ezqhl7AS@oQHykkuNrt=-LbsDBS@JASpKUUIc$-u2^4m@u|scZx0BqWizej7NxclUDwSM! zB-;kCS*(zz2L?-WIpQFvrp;1E_VaH_l>$gifYk|JnEmHQ06dJSEN{K9Vh~SkDB$n- zO8Xk!5YW6YDGsp(tW@owhFNPL;D&9Q?Km5C?7$F+JgBmB@yeK{$lxm3wu}XN`H+Iy~gw*A6LMHj2Aa0W zv&-q)-K)*BaG*dcjDwOtsdd+&QU@e6^d1c&O(8T~WUW3k&cyEH%D=*4ysXWFlZ{~7 zlWo+W_sQh-;m0ehgzBc;JE#@k-&bhX#dt2>kO&>U8xFPe0g9#5CyN)Z+g%1crP)zb z`>#*Fx7V~uWvc#K^OfO=;ox?6n-D+cx@;*QVN$LcrbeyPJib^vNNlRQj(GfU?tqHU{X4YbDd^BpbQD69-8|LcgE&yzf z+bb7&VN)1g{y97>bJN0Yt|Y=7uP!N1X=+RWTjtUG_2)mrOzvi60E(PJijPH$SM9{% zWf0t?bxd@oPCULt8}j-+6pH>cmcceMzSHk$^QH$A5`miYiX8Teu5}&9%SCdK&Jzxo zl{=zR;-P|coGZTmG1i6pM%Vm#Q`;B~F)`Ar5Besy6dOLP4wdC_qpY|EW;NLdzjXty5nsNyV9~AzG z`aoM?AuTPfNt#=5?9n#h+|hPIdVNmGgwl0!zGrx9KxFzp)3PxrWM#jGiIIh%@$<;f zsTdm>4QW9kqe%o6rXoEmOG11hLJ+U-5WUeD)6@z*h_#)8DbL6 z+qID~-&)Zv6RRTZKFs^L?raXL#y_96J}5gLl>0ip99XAs3Ed~=A}t$40d)I7t^uEJ zms#(D`66*9+>8ENR~fDJlz9pqdDc#4#^p$ok-YM_$EI1=#b%4_+nIIhU5U%}Iu%9t z_^7YfZk4*EjYJ@9il~#tnjFG@NlQZm1U6B~YpdPT1PLEThAd`0b4-YTx1Q_2i5hs$ zqcYh!ko;h!#5z(qEtA$IhU%JXKT&6p+CFjmx?^Nv?V4vlCS9x>49PuzK}aqnwz2f6 zIX>#)N_w2xdN~Cp!81X}=Ir=zr#SyvnVuOmzVo-bT2{5JCKIIb(K9S3;8c3jptk@u zQ$~IfjY3RPEQIfJg=zOBYp_dF1)+FiC!C4k>lxd4`^O5TW#5RJ3H8eP5TsPMdROCP z)@+RX*p7+jE_-V&t9g@7&p8M>Ii{_X2|?D%@ea0igtUwieaS*>omfWvJHgsIR^J=G zh*Xb4kskZ1?*4S7BIyq+>tc{>7^h)txpYj64&S!kzqHRrWXH#0o@{Gw9rFVxEdZ5) zZCa01L6U?&;{w{)z2+kI)s+ju??WK`5&JZnUnCEfR-CYfM66leP_gDl<&bYe2!@P_ z>7>;gWmlp9#H^1Wt=Ic5oFFh^*FnpoJJS0(Nf;S)Rb$nZl=isa=-a10+0_JwCi)5m zlrJx-JimVKKzX8+e9A~aU#Z3_?FiE1L9EzB_itmd*w08~RFova~xSyvZ8q zK*PxusLn-uKXbq3j$IQt$wpA~ffA`ZM$01wbUBAOl^EjN;<|}$O-XkK-;=S0zkyH2 zfOl6_>8L$X)CnVrqdfT}qs_F8*A{1YM&!P*4^Y8+S`Y5{`n+nZv( zbhN<|Ax`YXT9CVXapbqKo}C|BVnltvGc}Np3WfXKl^c`WX(ree*sP&m7vf|VrsxkY z26>6}3_7eAVo_?L9F#is5IbzuqWNhx1)g69wh$)4-eX>YTnXh>AJRo}?dd+?u=T^n ziv)UdkuljiVAXKBK9$fGMjLt^Phd8bK6VZari0+AIpVYNyieYAx`{86_Qt{OlWgcS zPt!3E&+XJI`h!vinx0QfP+AEjt+NJ4ea%fuf8wNw>X! zL$623e|DDXWTx~YU^DonKaSXDs~ay->l2Zm^Ht}dsP<5XM?`!r?Wo~5H$Izo9}P`@ zA`!EjTmUCSCex*dR@oCpy6O2f-9c%aJ5d8!4ATn{!gwDhJ|XiF4MpEg@k>A-(U!R) zDOp-V7_xa89A#qsz7Q-MtY;*b=CjuJy&=sIcXcQ)!Y``WFK5&$KV(+;ij)Kv&Q$B@3bZW_J|@8Y|V$0Y+`R@q(Zth@rW@Lshf|Zz;(Bs zx_#6jftK&Gy~pomQfq>J|6S>Ox-TDY!WbFn2?FJRGM;fkm!i9#b6TW zWe(HDCoJb%8+HqoCp|IQEk{KQj{wSxC{VS`7o236_DI&s2jRqkcSlv6A8?<|E%2yY)X{lWYY0r+zy9#_SgFuWkrY5XH z_nD1l*V+W{R-F)D1V|NXB5u9F;Evi(JS@ z6g;BGVO9eSjXDAuWOM6WU1f^(M*;G67mCFjCSah*+U#lQYFHGDAI58D7G~X8pIOuB zeQdRBA3y&1jBoI+pp*YwGM>Q)nS1ce@(Iox0pn^ z!SlS07;gSRvC^57NWVEkK0V8(x$sCOfr)OR8~2J71J2mx2#X@ug=s-}a6hp8Y>E>< z*^GIaPvhY3t<#8)nnU~#3^EDubar!#PP~MNj)fVSAH}mb(OsR$L%eR|W*$j->Tk~7 zIL_#X;xl-7Wv&*bV_UX_Xq&8y5WhOW!XKe&WR z$jf>@nLhAL5M*cL5q+w}^JS=0!8ofB`2Z%74^_~Bt^d4d(sK1lqhlIcv8Yeg{LpQ2 z64Sj&Saqic@{`NCn1$njKyY8+R>$BT zVtOg-O2r&ZwXY8_2Q-9x8JKKLq__5|@I|)ul3_DwT*dyY7`AW3} z1@yXP>vYu~2Rmh;#iK3;rPBEQEfx*C8i_0dQ|6+4LEN0fN$cJ->wq(+7tChb^kdy} z+ylPmHxaqOhMw-%JiRpgG*_zMLsF|Ni0`qzx{2yI1~iB>)#FEoa>=UGqg^c?SsuUH zGYZ?k%il-A!wE~v?w;RW(h0C_4dNV6nOW`|j(=YqF5+{Tq&>>cv|}0MY4n`7>zFj) z_G?H?s9gwi>Ne%2-1^8*+(*LVPg~G&i_O1)*yWtb*CYAu$2*u^R}+-JgZqa1{c^~B zGYPg`6D@Z8Fu2uQ=J%ykjFcbh=#se^IAvvhIc%?-n_T8>AEE_Y^-Fkn(2W z=FX{t%t@iQg0+HPcJ(*UnlXP+y;ziZb)xbNI0u`>7s2lGPD@XRFA5Kfz!bs0o)`&Z zIu^j_EoooW$CzXDP^vnG#t$5D)Stx31QP45#!Of6Ul1Pfkg}}dbL*GHKE0q&DV&wF z0L8TkZ)N8zy{f%|`+`ZI!J#T1zjG=YBoNt=Jk|e#LJh~%$Z=}U?kD|>Uw?JDm8`K{ zuPe9C{DKPmSA-wgor41o_S1Nl+uHY;m|@ceyWL`egU7FJ3XbAZl+rKd22Vd~$TPih~rxDuerD8nQT^kTy z9m-`B;_pu;RmfcFh($Q`t$ia6__wYHbQa;^qTM!oDb9_>vp(q3hGoI#aF{Zr$Jpek zxz$^dXl+2^;Q8q6rH=IhNq^K?ld2K{Bt`0Ye{U{T{sMe{X;;snqT&+0(C03wRkv~FC$%ielQVan z-C(lmraTjAuO1A-IX{6xx~(f#8PN{UdxG>SWise?LE~ZKH007gMp)BSm^9dINf66~ z*#>?1u|}T(;Y~JtkkT*Sds|oe#FTbCKn-{x%?Sn&^MKyC0EaOlfX@x&0I;4Y#8RUt zsM6V}I{2JTU(Wwh$y-TiQ7z>e3TjFDH}cmI=1z~N3zs!7RQ~5gmgt&1j z&0)}MzVY8M-${>+)7V&!W5<%;xy=UCuNu4)goncywJSQ+?W&cPZIOD+yTIhhw^pXF zCt42_NN$UC8pyRg{(Lsqhr9}&$@t53nQO+cj}*$`X5ESykc>;cyc&2KJwS*mEj)!v zJWnvTmJw&l#2qwgh9m$Ic3Rb!mJvUBy|zlWY00_18)xoD4g>LYaqDcx(;z)@ap`;O2#LbebXEZ}0=@dAZn^Wp z+iYNCoMSC4Dv8fDwVvlPgzm4JU@_D>#p<`H6#$6bG z7G&Kv0a-0a6)H+ggWrYFYf?Bk3NX8cspKM(?Lvm&4r7pR2 zrYLxQL~JplbJo*7I`8T90wob08qYv*U#NC*OW}r0AL)QBstOTpzG}M!Z+-mQ+`9Xc z5miI@1_2HYR|54+zrhRyB+$X0o{@`C z<7kRBe3xecJEZmMc>?Ovu>kymI`62R`KAYh8P{t+1$ayq-5u0G3`jDpH*S^;$iTlO z3!F3~?rElQ?0~avRiUb@R@nN9alGIPe+*B(;W1C*!-P8gQoo+ zMp;2QVpi;L97HCI4bx68gkL$Km)d1XjY6bvvL~O7>JzleVT#?CKqI%2B-O-g?^Pl0 z<1h0;mRbV06+E4xty!){C*L5Q0x5V^TOlsq6cIxmkwURzH3ctwvbYvpRD|OG9?Oei zH#vNuV!hut6)d>jrL|onuc#bN^6#F@7iB*=Fr{p{22MVQ@n{;f3xTKkWPRzdZZ5~-2r~ZM2 z%6XAC@d`%)@;FV6r3hfMhLt7OoU}8=3Cki9!sgR`)j1e8%b8->J#HWu08lB0!#r4u zPi^#j@$_~xo;!#t>lq$!z{XfG)&DNO{R5;DCX&!VX*jx3hjZ}E!_m0#x!cR077yY~ z?_CU|lt)AG#&F3)A;cDjE!*2pe!*#?ENi_?#Eg?G!GYz%b++t#JGp6}g zn#32}U#4Blv&$RjmVj7QXQrAGVgfzB0><633k1l*2cmqMlsmqNB3@KMG*hi8Mlimv z?!R%hS7r{ORBesN!#qnMicUTs8ISCR*TZSsYJv8CDmnO&S6T9E9j|MJW-b~L^Vb=U z+Txk>RIzl#^joi}$2o`^9IzfN}Q zI*C7b{AyI50RL!{jh$_Mgs~eIp=o@Cl+x`=O_^O0T}0Pi@*YJ$b&i`1F7`Y@g20_+ zf79&TOjFxe^KUqH&yNeASUK+z*m-r~Vh)W5L|a1+Ov~MJi74BAZo;fhT8T2X0%1vc z0S6>USO@|qP4pl2!;YsR^~zn9yhY|IM^W#>S8Z9d4&R^i5KOfH3i;fFK^RmU`@90t zNvfWYK^W6 z>u4L+vzc;|3SEOt;n;_;=QU-G3_7POsW0FU)@gRc6fKWyxp1y9L>7}!g_sAf}w848jHPU?#tED~CEEbRR;M+H3E=815LnU(T zQVyQOCBFN&1q$zAtjw*N*+XxbRMf2cIsVqGW`A zEF;RM=ycR2V+ntHQZaJQ!osESGBi7- zX`-0}W>T?XRv~k|tPX*-Cgt0an4_lT`hd^Dtvix`0oQ;-9>>InzN+rg@Ab4n$(M=L z&5ViDP5nj!TEs9dc_O0M8)5e;G5{ZVv}%-RRi)(Cr~22mJRDBrVJ?6xP7Tslel;Bb zSlB$WA%Ny=W7VyU7gMSlRctYtaeG4I(&&pS=dL?<{lFJub~^<3FIs&4MA4!I+%&y? z(@Re&mtP`C-$b}?VjwqB?7u_c|GMTLPYPhvp|HE(kkJ45(H#{aOhBH2eSQaz{>`np z(f%Jt*1cHQ9DsYZZCubJ?3-ouUw`S?N?wX5@U)0pd&jYH5Y$*#YhWhyacgfP_kf0NBt8nh*avn!VKkbai#| zqGuF3Q0}ee42b%$4tTNU7Q1vi+=T7+ zUsXTe&aSG>AFhQ>gCBnxZNyFl^Vv?$(*6A{yyupo1R-x8-i}4TD_Y?`k91ER;O7C~ zBH!2XL1}@XaGIV{I^Z@aNe)+370n*p6dSwxUOr^F_$`X1r*W!pIqv8;nLswZT}O9+ zZ0>KpR(U9`Vu(?W=wl@J?1{vNYrBmXCjc47Vkfb2KEQizmanBphG^1lr7A!GE$(%3 zO%R{_1Q0d3nT(;&|5NOGH2`quCRq*CZlbq|xCb}mW}4v|$-4`Hz%8+PkI1F;Z*k|c zU|dc>B z5c5;zqwMZtDe7cQG%oqHw5P%iJ}ci&Q`)I%FxKaNxtLPRC|Mg&f zzI3~_bi>{HH_F=pMpTB0O1W@dOd%0DtRsBHsTik&m(9TK{Y7Ud<)BQUrCVF4Ke>ds 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 bie&B7m4}>?q8BS}13m`lOwJaaxf=Q}FggYg literal 0 HcmV?d00001 diff --git a/packages/ui/certd-client/src/views/crud/home/page-cover/image/darkblue@2x.png b/packages/ui/certd-client/src/views/crud/home/page-cover/image/darkblue@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..efe1577f417b8aea9215372f053115d21a4b5d7a GIT binary patch literal 9259 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 O($To1UZHv~{Qm*6e(k;h literal 0 HcmV?d00001 diff --git a/packages/ui/certd-client/src/views/crud/home/page-cover/image/logo.svg b/packages/ui/certd-client/src/views/crud/home/page-cover/image/logo.svg new file mode 100644 index 000000000..7a33425e3 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/home/page-cover/image/logo.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/certd-client/src/views/crud/home/page-cover/index.vue b/packages/ui/certd-client/src/views/crud/home/page-cover/index.vue new file mode 100644 index 000000000..7c51dea3d --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/home/page-cover/index.vue @@ -0,0 +1,139 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/row-handle/dropdown/api.js b/packages/ui/certd-client/src/views/crud/row-handle/dropdown/api.js new file mode 100644 index 000000000..3cfd81822 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/row-handle/dropdown/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/RowHandleDropdown"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/row-handle/dropdown/crud.jsx b/packages/ui/certd-client/src/views/crud/row-handle/dropdown/crud.jsx new file mode 100644 index 000000000..2947b5377 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/row-handle/dropdown/crud.jsx @@ -0,0 +1,82 @@ +import * as api from "./api"; +import { dict, compute } from "@fast-crud/fast-crud"; +import { message } from "ant-design-vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + rowHandle: { + width: 290, + buttons: { + remove: { + // 根据row的值判断按钮是否显示 + show: compute(({ row }) => { + return row.radio !== "0"; + }), + dropdown: true //---------》给想要折叠的按钮配置dropdown为true,就会放入dropdown中《--------------- + }, + orderExample: { + text: "我排前面", + title: "按钮排序示例", + type: "link", + order: 0, //数字越小,越靠前,默认排序号为1 + click(opts) { + console.log("自定义操作列按钮点击", opts); + message.success("自定义操作列按钮点击"); + } + } + }, + dropdown: { + // 操作列折叠 + // 至少几个以上的按钮才会被折叠 + // atLeast: 2, //TODO 注意 [atLeast]参数即将废弃,请给button配置dropdown即可放入折叠 + more: { + text: "更多", + icon: null, + iconRight: "ion:caret-down-outline" + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/row-handle/dropdown/index.vue b/packages/ui/certd-client/src/views/crud/row-handle/dropdown/index.vue new file mode 100644 index 000000000..45d6937e2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/row-handle/dropdown/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/row-handle/dropdown/mock.js b/packages/ui/certd-client/src/views/crud/row-handle/dropdown/mock.js new file mode 100644 index 000000000..ed703ceb8 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/row-handle/dropdown/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "RowHandleDropdown", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/row-handle/tooltip/api.js b/packages/ui/certd-client/src/views/crud/row-handle/tooltip/api.js new file mode 100644 index 000000000..dd8bf7d8c --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/row-handle/tooltip/api.js @@ -0,0 +1,50 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/RowHandleTooltip"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} + +export function BatchDelete(ids) { + return request({ + url: apiPrefix + "/batchDelete", + method: "post", + data: { ids } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/row-handle/tooltip/crud.jsx b/packages/ui/certd-client/src/views/crud/row-handle/tooltip/crud.jsx new file mode 100644 index 000000000..b8cc0141e --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/row-handle/tooltip/crud.jsx @@ -0,0 +1,84 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import { ref } from "vue"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + rowHandle: { + width: 400, + buttons: { + edit: { + tooltip: { + title: "编辑" + } + }, + view: { + tooltip: { + title: "查看" + } + }, + remove: { + tooltip: { + title: "删除" + } + }, + custom: { + text: "tooltip title render", + tooltip: { + slots: { + title() { + return ( +
+ 我是自定义render +
+ ); + } + } + } + } + } + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/row-handle/tooltip/index.vue b/packages/ui/certd-client/src/views/crud/row-handle/tooltip/index.vue new file mode 100644 index 000000000..40dde985f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/row-handle/tooltip/index.vue @@ -0,0 +1,51 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/row-handle/tooltip/mock.js b/packages/ui/certd-client/src/views/crud/row-handle/tooltip/mock.js new file mode 100644 index 000000000..7efa34b73 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/row-handle/tooltip/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "RowHandleTooltip", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/slots/cell/api.js b/packages/ui/certd-client/src/views/crud/slots/cell/api.js new file mode 100644 index 000000000..69094b0dd --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/cell/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/SlotsCell"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/slots/cell/crud.jsx b/packages/ui/certd-client/src/views/crud/slots/cell/crud.jsx new file mode 100644 index 000000000..ad836c9b2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/cell/crud.jsx @@ -0,0 +1,89 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +import dayjs from "dayjs"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + const radioDict = dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }); + return { + radioDict, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + rowHandle: { + buttons: { + edit: { dropdown: true }, + remove: { dropdown: true } + }, + width: 630 + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + like: { + title: "like", + type: "number", + search: { show: true } + }, + switch: { + title: "switch", + type: "dict-switch", + dict: dict({ + data: [ + { value: true, label: "开启" }, + { value: false, label: "关闭" } + ] + }) + }, + createDate: { + title: "时间", + type: "datetime", + column: { + align: "left", + width: 300 + }, + valueBuilder({ key, row }) { + row[key] = dayjs(row[key]); + } + }, + updateDate: { + title: "修改时间", + type: "datetime", + column: { + show: false + }, + valueBuilder({ key, row }) { + row[key] = dayjs(row[key]); + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/slots/cell/index.vue b/packages/ui/certd-client/src/views/crud/slots/cell/index.vue new file mode 100644 index 000000000..9b8b43494 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/cell/index.vue @@ -0,0 +1,79 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/slots/cell/mock.js b/packages/ui/certd-client/src/views/crud/slots/cell/mock.js new file mode 100644 index 000000000..aa2397b79 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/cell/mock.js @@ -0,0 +1,26 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "SlotsCell", + idGenerator: 0 +}; +const list = [ + { + like: 10000, + switch: true, + createDate: new Date().getTime(), + updateDate: new Date().getTime() + }, + { + like: 10000, + switch: false, + createDate: new Date().getTime(), + updateDate: new Date().getTime() + }, + { + like: 10000, + switch: true + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/slots/form-item/api.js b/packages/ui/certd-client/src/views/crud/slots/form-item/api.js new file mode 100644 index 000000000..924d6570b --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/form-item/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/SlotsFormItem"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/slots/form-item/crud.jsx b/packages/ui/certd-client/src/views/crud/slots/form-item/crud.jsx new file mode 100644 index 000000000..7af63d446 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/form-item/crud.jsx @@ -0,0 +1,51 @@ +import * as api from "./api"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + topics: { + title: "多行输入", + type: "text", + search: { show: true }, + form: { + rules: [{ required: true, message: "请输入" }] + }, + column: { + component: { name: "fs-values-format" } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/slots/form-item/index.vue b/packages/ui/certd-client/src/views/crud/slots/form-item/index.vue new file mode 100644 index 000000000..97869b958 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/form-item/index.vue @@ -0,0 +1,72 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/slots/form-item/mock.js b/packages/ui/certd-client/src/views/crud/slots/form-item/mock.js new file mode 100644 index 000000000..a9f3d7569 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/form-item/mock.js @@ -0,0 +1,13 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "SlotsFormItem", + idGenerator: 0 +}; +const list = [ + { + topics: ["fast-crud 666", "fast-crud真好用"] + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/slots/form/api.js b/packages/ui/certd-client/src/views/crud/slots/form/api.js new file mode 100644 index 000000000..b15ec4e05 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/form/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/SlotsForm"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/slots/form/crud.jsx b/packages/ui/certd-client/src/views/crud/slots/form/crud.jsx new file mode 100644 index 000000000..b5d8c1df5 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/form/crud.jsx @@ -0,0 +1,50 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + const radioDict = dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }); + return { + radioDict, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + text: { + title: "文本", + type: "text", + search: { show: true } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/slots/form/index.vue b/packages/ui/certd-client/src/views/crud/slots/form/index.vue new file mode 100644 index 000000000..74aa9ec35 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/form/index.vue @@ -0,0 +1,69 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/slots/form/mock.js b/packages/ui/certd-client/src/views/crud/slots/form/mock.js new file mode 100644 index 000000000..22d45cf72 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/form/mock.js @@ -0,0 +1,13 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "SlotsForm", + idGenerator: 0 +}; +const list = [ + { + text: "文本输入" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/slots/layout/api.js b/packages/ui/certd-client/src/views/crud/slots/layout/api.js new file mode 100644 index 000000000..56f3de45f --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/layout/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/SlotsLayout"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/slots/layout/crud.jsx b/packages/ui/certd-client/src/views/crud/slots/layout/crud.jsx new file mode 100644 index 000000000..af8ad45e2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/layout/crud.jsx @@ -0,0 +1,49 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async (id) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/slots/layout/index.vue b/packages/ui/certd-client/src/views/crud/slots/layout/index.vue new file mode 100644 index 000000000..ce35fe282 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/layout/index.vue @@ -0,0 +1,77 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/slots/layout/mock.js b/packages/ui/certd-client/src/views/crud/slots/layout/mock.js new file mode 100644 index 000000000..31e3a7d78 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/layout/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "SlotsLayout", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/crud/slots/search/api.js b/packages/ui/certd-client/src/views/crud/slots/search/api.js new file mode 100644 index 000000000..192ff43ea --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/search/api.js @@ -0,0 +1,42 @@ +import { requestForMock } from "/src/api/service"; +const request = requestForMock; +const apiPrefix = "/mock/SlotsSearch"; +export function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "get", + data: query + }); +} + +export function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "get", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/crud/slots/search/crud.jsx b/packages/ui/certd-client/src/views/crud/slots/search/crud.jsx new file mode 100644 index 000000000..af8ad45e2 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/search/crud.jsx @@ -0,0 +1,49 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async (id) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 50 + }, + form: { + show: false + } + }, + radio: { + title: "状态", + search: { show: true }, + type: "dict-radio", + dict: dict({ + url: "/mock/dicts/OpenStatusEnum?single" + }) + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/crud/slots/search/index.vue b/packages/ui/certd-client/src/views/crud/slots/search/index.vue new file mode 100644 index 000000000..bbf30cffb --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/search/index.vue @@ -0,0 +1,54 @@ + + + diff --git a/packages/ui/certd-client/src/views/crud/slots/search/mock.js b/packages/ui/certd-client/src/views/crud/slots/search/mock.js new file mode 100644 index 000000000..4312767d5 --- /dev/null +++ b/packages/ui/certd-client/src/views/crud/slots/search/mock.js @@ -0,0 +1,19 @@ +import mockUtil from "/src/mock/base"; +const options = { + name: "SlotsSearch", + idGenerator: 0 +}; +const list = [ + { + radio: "1" + }, + { + radio: "2" + }, + { + radio: "0" + } +]; +options.list = list; +const mock = mockUtil.buildMock(options); +export default mock; diff --git a/packages/ui/certd-client/src/views/framework/error/404.vue b/packages/ui/certd-client/src/views/framework/error/404.vue new file mode 100644 index 000000000..2dacd3630 --- /dev/null +++ b/packages/ui/certd-client/src/views/framework/error/404.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/ui/certd-client/src/views/framework/home/index.vue b/packages/ui/certd-client/src/views/framework/home/index.vue new file mode 100644 index 000000000..274457799 --- /dev/null +++ b/packages/ui/certd-client/src/views/framework/home/index.vue @@ -0,0 +1,15 @@ + + + + diff --git a/packages/ui/certd-client/src/views/framework/home/page-cover/helper.js b/packages/ui/certd-client/src/views/framework/home/page-cover/helper.js new file mode 100644 index 000000000..38334ac1d --- /dev/null +++ b/packages/ui/certd-client/src/views/framework/home/page-cover/helper.js @@ -0,0 +1,31 @@ +export default { + crud: ` columns: { + date:{ + title: '姓名', //字段名称 + type: 'text', //字段类型,添加、修改、查询将自动生成相应表单组件 + }, + province: { + title: '城市', + type: 'dict-select', //选择框 + form: { //表单组件自定义配置,此处配置选择框为多选 + component: { //支持任何v-model组件 + filterable: true, multiple: true, clearable: true + } + }, + dict: dict({ + data: [ //本地数据字典 + { value: 'sz', label: '深圳' }, + { value: 'gz', label: '广州' }, + { value: 'wh', label: '武汉' }, + { value: 'sh', label: '上海' } + ] + }) + }, + status: { + title: '状态', + type: 'dict-select', //选择框,默认单选 + dict: dict({ url: '/dicts/OpenStatusEnum' })//远程数据字典 + }, + } + ` +}; diff --git a/packages/ui/certd-client/src/views/framework/home/page-cover/image/crud.png b/packages/ui/certd-client/src/views/framework/home/page-cover/image/crud.png new file mode 100644 index 0000000000000000000000000000000000000000..3dfb047dd4cec6a1bd79e999ae8e7370af54c9c2 GIT binary patch literal 202647 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%0A-Rwf82Ui@FTb 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%0DtRsBHsTik&m(9TK{Y7Ud<)BQUrCVF4Ke>ds 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 bie&B7m4}>?q8BS}13m`lOwJaaxf=Q}FggYg literal 0 HcmV?d00001 diff --git a/packages/ui/certd-client/src/views/framework/home/page-cover/image/darkblue@2x.png b/packages/ui/certd-client/src/views/framework/home/page-cover/image/darkblue@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..efe1577f417b8aea9215372f053115d21a4b5d7a GIT binary patch literal 9259 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 O($To1UZHv~{Qm*6e(k;h literal 0 HcmV?d00001 diff --git a/packages/ui/certd-client/src/views/framework/home/page-cover/image/logo.svg b/packages/ui/certd-client/src/views/framework/home/page-cover/image/logo.svg new file mode 100644 index 000000000..7a33425e3 --- /dev/null +++ b/packages/ui/certd-client/src/views/framework/home/page-cover/image/logo.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/certd-client/src/views/framework/home/page-cover/index.vue b/packages/ui/certd-client/src/views/framework/home/page-cover/index.vue new file mode 100644 index 000000000..5c9241fa6 --- /dev/null +++ b/packages/ui/certd-client/src/views/framework/home/page-cover/index.vue @@ -0,0 +1,137 @@ + + + diff --git a/packages/ui/certd-client/src/views/framework/login/index.vue b/packages/ui/certd-client/src/views/framework/login/index.vue new file mode 100644 index 000000000..a4b8728b7 --- /dev/null +++ b/packages/ui/certd-client/src/views/framework/login/index.vue @@ -0,0 +1,270 @@ + + + + diff --git a/packages/ui/certd-client/src/views/framework/register/index.vue b/packages/ui/certd-client/src/views/framework/register/index.vue new file mode 100644 index 000000000..d63e8e102 --- /dev/null +++ b/packages/ui/certd-client/src/views/framework/register/index.vue @@ -0,0 +1,178 @@ + + + + diff --git a/packages/ui/certd-client/src/views/sys/authority/permission/api.js b/packages/ui/certd-client/src/views/sys/authority/permission/api.js new file mode 100644 index 000000000..64627ea60 --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/permission/api.js @@ -0,0 +1,48 @@ +import { request } from "/src/api/service"; +const apiPrefix = "/sys/authority/permission"; +export async function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "post", + data: query + }); +} + +export async function GetTree() { + return request({ + url: apiPrefix + "/tree", + method: "post" + }); +} + +export async function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export async function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export async function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export async function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "post", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/sys/authority/permission/crud.jsx b/packages/ui/certd-client/src/views/sys/authority/permission/crud.jsx new file mode 100644 index 000000000..c8b918534 --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/permission/crud.jsx @@ -0,0 +1,150 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; + +export default function ({ expose }) { + const pageRequest = async (query) => { + const list = await api.GetTree(query); + + return { + current: 1, + records: list, + total: 10000, + pageSize: 10000 + }; + }; + + async function afterChange() { + await permissionTreeDict.reloadDict(); + } + const editRequest = async ({ form, row }) => { + form.id = row.id; + const ret = await api.UpdateObj(form); + await afterChange(); + return ret; + }; + const delRequest = async ({ row }) => { + const ret = await api.DelObj(row.id); + await afterChange(); + return ret; + }; + + const addRequest = async ({ form }) => { + const ret = await api.AddObj(form); + await afterChange(); + return ret; + }; + let permissionTreeDict = dict({ + url: "/sys/authority/permission/tree", + isTree: true, + value: "id", + label: "title", + async onReady({ dict }) { + dict.setData([{ id: -1, title: "根节点", children: dict.data }]); + } + }); + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + actionbar: { + show: false + }, + toolbar: { + show: false + }, + table: { + show: false + // scroll: { fixed: true } + }, + rowHandle: { + fixed: "right" + }, + search: { + show: false + }, + pagination: { + show: false, + pageSize: 100000 + }, + columns: { + id: { + title: "id", + type: "number", + form: { show: false }, // 表单配置 + column: { + width: 120, + sortable: "custom" + } + }, + title: { + title: "权限名称", + type: "text", + form: { + rules: [ + { required: true, message: "请输入权限名称" }, + { max: 50, message: "最大50个字符" } + ], + component: { + placeholder: "权限名称" + } + }, + column: { + width: 200 + } + }, + + permission: { + title: "权限代码", + type: "text", + column: { + width: 170 + }, + form: { + rules: [ + { required: true, message: "请输入权限代码" }, + { max: 100, message: "最大100个字符" } + ], + component: { + placeholder: "例如:sys:user:view" + } + } + }, + sort: { + title: "排序", + type: "number", + column: { + width: 100 + }, + form: { + value: 100, + rules: [{ required: true, type: "number", message: "排序号必填" }] + } + }, + parentId: { + title: "父节点", + type: "dict-tree", + column: { + width: 100 + }, + dict: permissionTreeDict, + form: { + value: -1, + component: { + multiple: false, + defaultExpandAll: true, + dict: { cache: false }, + fieldNames: { + value: "id", + label: "title" + } + } + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/sys/authority/permission/fs-permission-tree.vue b/packages/ui/certd-client/src/views/sys/authority/permission/fs-permission-tree.vue new file mode 100644 index 000000000..692ca1ddb --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/permission/fs-permission-tree.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/sys/authority/permission/index.vue b/packages/ui/certd-client/src/views/sys/authority/permission/index.vue new file mode 100644 index 000000000..0005298e3 --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/permission/index.vue @@ -0,0 +1,90 @@ + + + + diff --git a/packages/ui/certd-client/src/views/sys/authority/role/api.js b/packages/ui/certd-client/src/views/sys/authority/role/api.js new file mode 100644 index 000000000..6793b6fa3 --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/role/api.js @@ -0,0 +1,70 @@ +import { request } from "/src/api/service"; +const apiPrefix = "/sys/authority/role"; +export async function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "post", + data: query + }); +} + +export async function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export async function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export async function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export async function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "post", + params: { id } + }); +} + +/** + * 获取角色权限资源 + * @param roleId + * @returns {*} + * @constructor + */ +export function getPermissionIds(roleId) { + return request({ + url: apiPrefix + "/getPermissionIds", + method: "post", + params: { id: roleId } + }); +} + +/** + * 授权 + * @param roleId + * @param permissionIds + * @returns {*} + * @constructor + */ +export function DoAuthz(roleId, permissionIds) { + return request({ + url: apiPrefix + "/authz", + method: "post", + data: { roleId, permissionIds } + }); +} diff --git a/packages/ui/certd-client/src/views/sys/authority/role/crud.jsx b/packages/ui/certd-client/src/views/sys/authority/role/crud.jsx new file mode 100644 index 000000000..a4c7290ef --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/role/crud.jsx @@ -0,0 +1,81 @@ +import * as api from "./api"; +export default function ({ expose, authz }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + rowHandle: { + width: 300, + buttons: { + authz: { + type: "link", + text: "授权", + async click(context) { + await authz.authzOpen(context.record.id); + } + } + } + }, + columns: { + id: { + title: "id", + form: { show: false }, // 表单配置 + column: { + width: 70, + sorter: true + } + }, + name: { + title: "角色名称", + type: "text", + search: { show: true }, + form: { + rules: [ + { required: true, message: "请输入角色名称" }, + { max: 50, message: "最大50个字符" } + ] + }, // 表单配置 + column: { + sorter: true + } + }, + createTime: { + title: "创建时间", + type: "datetime", + column: { + sorter: true + }, + form: { + show: false + } + }, + updateTime: { + title: "更新时间", + type: "datetime", + column: { + sorter: true + }, + form: { show: false } // 表单配置 + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/sys/authority/role/index.vue b/packages/ui/certd-client/src/views/sys/authority/role/index.vue new file mode 100644 index 000000000..089cf1cdd --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/role/index.vue @@ -0,0 +1,133 @@ + + + diff --git a/packages/ui/certd-client/src/views/sys/authority/user/api.js b/packages/ui/certd-client/src/views/sys/authority/user/api.js new file mode 100644 index 000000000..55474d504 --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/user/api.js @@ -0,0 +1,41 @@ +import { request } from "/src/api/service"; +const apiPrefix = "/sys/authority/user"; +export async function GetList(query) { + return request({ + url: apiPrefix + "/page", + method: "post", + data: query + }); +} + +export async function AddObj(obj) { + return request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); +} + +export async function UpdateObj(obj) { + return request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); +} + +export async function DelObj(id) { + return request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); +} + +export async function GetObj(id) { + return request({ + url: apiPrefix + "/info", + method: "post", + params: { id } + }); +} diff --git a/packages/ui/certd-client/src/views/sys/authority/user/crud.jsx b/packages/ui/certd-client/src/views/sys/authority/user/crud.jsx new file mode 100644 index 000000000..291e31f35 --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/user/crud.jsx @@ -0,0 +1,146 @@ +import * as api from "./api"; +import { dict } from "@fast-crud/fast-crud"; +export default function ({ expose }) { + const pageRequest = async (query) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }) => { + return await api.DelObj(row.id); + }; + + const addRequest = async ({ form }) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + rowHandle: { + fixed: "right" + }, + table: { + scroll: { + //使用固定列时需要设置此值,并且大于等于列宽度之和的值 + x: 1400 + } + }, + columns: { + id: { + title: "id", + form: { show: false }, // 表单配置 + column: { + width: 70, + sorter: true + } + }, + createTime: { + title: "创建时间", + type: "datetime", + form: { show: false }, // 表单配置 + column: { + width: 180, + sorter: true + } + }, + // updateTime: { + // title: "修改时间", + // type: "datetime", + // form: { show: false }, // 表单配置 + // column: { + // sortable: "update_time", + // width: 180 + // } + // }, + username: { + title: "用户名", + type: "text", + search: { show: true }, // 开启查询 + form: { + rules: [ + { required: true, message: "请输入用户名" }, + { max: 50, message: "最大50个字符" } + ] + }, + editForm: { component: { disabled: true } }, + column: { + sorter: true + } + }, + password: { + title: "密码", + type: "text", + key: "password", + column: { + show: false + }, + form: { + rules: [{ max: 50, message: "最大50个字符" }], + component: { + showPassword: true + }, + helper: "填写则修改密码" + } + }, + nickName: { + title: "昵称", + type: "text", + search: { show: true }, // 开启查询 + form: { + rules: [{ max: 50, message: "最大50个字符" }] + }, + column: { + sorter: true + } + }, + avatar: { + title: "头像", + type: "cropper-uploader", + column: { + width: 100, + component: { + //设置高度,修复操作列错位的问题 + style: { + height: "30px", + width: "auto" + } + } + } + }, + remark: { + title: "备注", + type: "text", + column: { + sorter: true + }, + form: { + rules: [{ max: 100, message: "最大100个字符" }] + } + }, + roles: { + title: "角色", + type: "dict-select", + dict: dict({ + url: "/sys/authority/role/list", + value: "id", + label: "name" + }), // 数据字典 + form: { + component: { mode: "multiple" } + }, + column: { + width: 250, + sortable: true + } + } + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/sys/authority/user/index.vue b/packages/ui/certd-client/src/views/sys/authority/user/index.vue new file mode 100644 index 000000000..33f0c7a55 --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/authority/user/index.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/ui/certd-client/tailwind.config.js b/packages/ui/certd-client/tailwind.config.js new file mode 100644 index 000000000..ec39f7e53 --- /dev/null +++ b/packages/ui/certd-client/tailwind.config.js @@ -0,0 +1,11 @@ +module.exports = { + purge: [], + darkMode: false, // or 'media' or 'class' + theme: { + extend: {} + }, + variants: { + extend: {} + }, + plugins: [] +}; diff --git a/packages/ui/certd-client/tests/unit/example.spec.ts b/packages/ui/certd-client/tests/unit/example.spec.ts new file mode 100644 index 000000000..bc9939b65 --- /dev/null +++ b/packages/ui/certd-client/tests/unit/example.spec.ts @@ -0,0 +1,13 @@ +import { expect } from 'chai' +import { shallowMount } from '@vue/test-utils' +import HelloWorld from '@/components/HelloWorld.vue' + +describe('HelloWorld.vue', () => { + it('renders props.msg when passed', () => { + const msg = 'new message' + const wrapper = shallowMount(HelloWorld, { + props: { msg } + }) + expect(wrapper.text()).to.include(msg) + }) +}) diff --git a/packages/ui/certd-client/tsconfig.json b/packages/ui/certd-client/tsconfig.json new file mode 100644 index 000000000..bdb44e0fe --- /dev/null +++ b/packages/ui/certd-client/tsconfig.json @@ -0,0 +1,45 @@ +{ + "compilerOptions": { + // 这样就可以对 `this` 上的数据属性进行更严格的推断` + "noImplicitAny": false, + "target": "esnext", + "module": "esnext", + "strict": true, + "jsx": "preserve", + "importHelpers": true, + "moduleResolution": "node", + "experimentalDecorators": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "baseUrl": ".", + "outDir": "./dist/ts", + "types": [ + "mocha", + "chai", + "node" + ], + "paths": { + "/@/*": ["src/*"], + "/src/*": ["src/*"], + "/#/*": ["types/*"] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "tests/**/*.ts", + "tests/**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/ui/certd-client/vite.config.ts b/packages/ui/certd-client/vite.config.ts new file mode 100644 index 000000000..de89cd4be --- /dev/null +++ b/packages/ui/certd-client/vite.config.ts @@ -0,0 +1,101 @@ +import vue from "@vitejs/plugin-vue"; +import vueJsx from "@vitejs/plugin-vue-jsx"; +import visualizer from "rollup-plugin-visualizer"; +import viteCompression from "vite-plugin-compression"; +import PurgeIcons from "vite-plugin-purge-icons"; +import * as path from "path"; +// import WindiCSS from "vite-plugin-windicss"; +// import { generateModifyVars } from "./build/modify-vars"; +// import { configThemePlugin } from "./build/theme-plugin"; +// import OptimizationPersist from "vite-plugin-optimize-persist"; +// import PkgConfig from "vite-plugin-package-config"; +// https://vitejs.dev/config/ +// 增加环境变量 _ +process.env.VITE_APP_VERSION = require("./package.json").version; +process.env.VITE_APP_BUILD_TIME = require("dayjs")().format("YYYY-M-D HH:mm:ss"); + +export default ({ command, mode }) => { + console.log("args", command, mode); + + let devServerFs: any = {}; + let devAlias: any[] = []; + if (mode.startsWith("debug")) { + devAlias = [ + { find: /@fast-crud\/fast-crud\/dist/, replacement: path.resolve("../../fast-crud/src/") }, + { find: /@fast-crud\/fast-crud$/, replacement: path.resolve("../../fast-crud/src/") }, + { find: /@fast-crud\/fast-extends\/dist/, replacement: path.resolve("../../fast-extends/src/") }, + { find: /@fast-crud\/fast-extends$/, replacement: path.resolve("../../fast-extends/src/") }, + { find: /@fast-crud\/ui-antdv$/, replacement: path.resolve("../../ui/ui-antdv/src/") } + ]; + devServerFs = { + // 如果是你自己的项目,这项可以删掉 + // 这里配置dev启动时读取的项目根目录 + allow: ["../../"] + }; + console.log("devAlias", devAlias); + } + + return { + base: "/", + plugins: [ + vueJsx(), + vue(), + // 压缩build后的代码 + viteCompression(), + PurgeIcons({ + // iconSource: "local" + // remoteDataAPI: "https://gitee.com/fast-crud/collections-json/raw/master/json", + // includedCollections: ["ion"] + }) + //主题色替换 + //...configThemePlugin(true), + // viteThemePlugin({ + // // Match the color to be modified + // colorVariables: ["#1890ff", "#40a9ff"] + // }), + // windicss tailwindcss + // WindiCSS() + ], + esbuild: { + // pure: ["console.log", "debugger"], + jsxFactory: "h", + jsxFragment: "Fragment" + }, + resolve: { + alias: [ + ...devAlias, + { find: "/@", replacement: path.resolve("./src") }, + { find: "/#", replacement: path.resolve("./types") } + ], + dedupe: ["vue"] + }, + optimizeDeps: { + include: ["ant-design-vue"] + }, + build: { + rollupOptions: { + plugins: [visualizer()] + } + }, + css: { + preprocessorOptions: { + less: { + // 修改默认主题颜色,配置less变量 + // modifyVars: generateModifyVars(), + javascriptEnabled: true + } + } + }, + server: { + port: 3002, + fs: devServerFs, + proxy: { + // with options + "/api": { + //配套后端 https://github.com/fast-crud/fs-server-js + target: "http://127.0.0.1:7001" + } + } + } + }; +}; diff --git a/packages/ui/certd-client/windi.config.js b/packages/ui/certd-client/windi.config.js new file mode 100644 index 000000000..6d82c4951 --- /dev/null +++ b/packages/ui/certd-client/windi.config.js @@ -0,0 +1,6 @@ +// windi.config.ts +export default { + attributify: { + prefix: "w:" + } +}; diff --git a/packages/ui/certd-server/.dockerignore b/packages/ui/certd-server/.dockerignore new file mode 100644 index 000000000..6eb6e80d1 --- /dev/null +++ b/packages/ui/certd-server/.dockerignore @@ -0,0 +1,16 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +!dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* +/data/db.sqlite diff --git a/packages/ui/certd-server/.editorconfig b/packages/ui/certd-server/.editorconfig new file mode 100644 index 000000000..4c7f8a8ef --- /dev/null +++ b/packages/ui/certd-server/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/packages/ui/certd-server/.eslintrc.json b/packages/ui/certd-server/.eslintrc.json new file mode 100644 index 000000000..7828b2c51 --- /dev/null +++ b/packages/ui/certd-server/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/ban-ts-ignore": "off", + "@typescript-eslint/ban-ts-comment": "off" + } +} diff --git a/packages/ui/certd-server/.gitignore b/packages/ui/certd-server/.gitignore new file mode 100644 index 000000000..41f712631 --- /dev/null +++ b/packages/ui/certd-server/.gitignore @@ -0,0 +1,18 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* +/data/db.sqlite +/pnpm-lock.yaml +.serverless diff --git a/packages/ui/certd-server/.prettierrc.js b/packages/ui/certd-server/.prettierrc.js new file mode 100644 index 000000000..b964930f3 --- /dev/null +++ b/packages/ui/certd-server/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/packages/ui/certd-server/Dockerfile b/packages/ui/certd-server/Dockerfile new file mode 100644 index 000000000..521407343 --- /dev/null +++ b/packages/ui/certd-server/Dockerfile @@ -0,0 +1,15 @@ +FROM registry.cn-shenzhen.aliyuncs.com/greper/node:15.8.0-alpine + +WORKDIR /home + +COPY . . +# 如果各公司有自己的私有源,可以替换registry地址 +#RUN npm install --registry=https://registry.npmmirror.com +RUN npm install -g cnpm +RUN cnpm install +RUN npm run build:preview + +# 如果端口更换,这边可以更新一下 +EXPOSE 7001 + +CMD ["npm", "run", "online:preview"] diff --git a/packages/ui/certd-server/LICENSE b/packages/ui/certd-server/LICENSE new file mode 100644 index 000000000..0ad25db4b --- /dev/null +++ b/packages/ui/certd-server/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/ui/certd-server/README.md b/packages/ui/certd-server/README.md new file mode 100644 index 000000000..d0aca2df6 --- /dev/null +++ b/packages/ui/certd-server/README.md @@ -0,0 +1,31 @@ +# fast-server-js + +base on midway + +## QuickStart + + + +see [midway docs][midway] for more detail. + +### Development + +```bash +$ npm i +$ npm run dev +$ open http://localhost:7001/ +``` + +### Deploy + +```bash +$ npm start +``` + +### npm scripts + +- Use `npm run lint` to check code style. +- Use `npm test` to run unit test. + + +[midway]: https://midwayjs.org diff --git a/packages/ui/certd-server/README.zh-CN.md b/packages/ui/certd-server/README.zh-CN.md new file mode 100644 index 000000000..54b99c0e4 --- /dev/null +++ b/packages/ui/certd-server/README.zh-CN.md @@ -0,0 +1,30 @@ +# fast-server-js + +## 快速入门 + + + +如需进一步了解,参见 [midway 文档][midway]。 + +### 本地开发 + +```bash +$ npm i +$ npm run dev +$ open http://localhost:7001/ +``` + +### 部署 + +```bash +$ npm start +``` + +### 内置指令 + +- 使用 `npm run lint` 来做代码风格检查。 +- 使用 `npm test` 来执行单元测试。 + + +[midway]: https://midwayjs.org + diff --git a/packages/ui/certd-server/app.js b/packages/ui/certd-server/app.js new file mode 100644 index 000000000..7f1d93539 --- /dev/null +++ b/packages/ui/certd-server/app.js @@ -0,0 +1,23 @@ +// 获取框架 +const WebFramework = require('@midwayjs/koa').Framework; +const { Bootstrap } = require('@midwayjs/bootstrap'); + +const DirectoryFileDetector = require( "@midwayjs/core").DirectoryFileDetector; + +const baseDir = process.cwd() +const pipelineDir = baseDir +"./node_modules/@certd/pipeline" +const customFileDetector = new DirectoryFileDetector({loadDir:[baseDir,pipelineDir]}) + + +module.exports = async () => { + // 加载框架并执行 + await Bootstrap.configure({ + moduleDetector:customFileDetector + }).run(); + // 获取依赖注入容器 + const container = Bootstrap.getApplicationContext(); + // 获取 koa framework + const framework = container.get(WebFramework); + // 返回 app 对象 + return framework.getApplication(); +}; diff --git a/packages/ui/certd-server/bootstrap.js b/packages/ui/certd-server/bootstrap.js new file mode 100644 index 000000000..1b0bbb73a --- /dev/null +++ b/packages/ui/certd-server/bootstrap.js @@ -0,0 +1,15 @@ +const WebFramework = require('@midwayjs/koa').Framework; +const web = new WebFramework().configure({ + port: 7001, +}); + +const DirectoryFileDetector = require( "@midwayjs/core").DirectoryFileDetector; + +const baseDir = process.cwd() +const pipelineDir = baseDir +"./node_modules/@certd/pipeline" +const customFileDetector = new DirectoryFileDetector({loadDir:[baseDir,pipelineDir]}) + +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap.load(web).configure({ + moduleDetector:customFileDetector +}).run(); diff --git a/packages/ui/certd-server/db/migration/v00001__init.sql b/packages/ui/certd-server/db/migration/v00001__init.sql new file mode 100644 index 000000000..b8f169c35 --- /dev/null +++ b/packages/ui/certd-server/db/migration/v00001__init.sql @@ -0,0 +1,77 @@ +-- 表:sys_permission +CREATE TABLE "sys_permission" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(100) NOT NULL, "permission" varchar(100), "parent_id" integer NOT NULL DEFAULT (-1), "sort" integer NOT NULL, "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (1, '系统管理', 'sys', -1, 1, 1, 1624085863636); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (2, '权限管理', 'sys:auth', 1, 1, 1, 1); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (3, '用户管理', 'sys:auth:user', 2, 1, 1, 1); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (4, '查看', 'sys:auth:user:view', 3, 100, 1, 1624189112333); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (5, '权限管理', 'sys:auth:per', 2, 1, 1, 1); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (6, '查看', 'sys:auth:per:view', 5, 100, 1, 1624189161317); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (7, '角色管理', 'sys:auth:role', 2, 1, 1, 1); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (8, '查看', 'sys:auth:role:view', 7, 1, 1, 1); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (9, '修改', 'sys:auth:user:edit', 3, 300, 1, 1624189127688); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (10, '删除', 'sys:auth:user:remove', 3, 400, 1, 1624189133184); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (11, '添加', 'sys:auth:user:add', 3, 200, 1, 1624189142576); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (12, '修改', 'sys:auth:role:edit', 7, 1, 1, 1); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (13, '删除', 'sys:auth:role:remove', 7, 1, 1, 1); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (14, '添加', 'sys:auth:role:add', 7, 1, 1, 1); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (15, '修改', 'sys:auth:per:edit', 5, 300, 1, 1624189308837); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (16, '删除', 'sys:auth:per:remove', 5, 400, 1, 1624189256926); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (17, '添加', 'sys:auth:per:add', 5, 200, 1, 1624189283766); +INSERT INTO sys_permission (id, title, permission, parent_id, sort, create_time, update_time) VALUES (18,'授权','sys:auth:role:authz',7,100,1,1624335712144); + + + +-- 表:sys_role +CREATE TABLE "sys_role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(100) NOT NULL, "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); +INSERT INTO sys_role (id, name, create_time, update_time) VALUES (1, '管理员', 1, 1623749138537); +INSERT INTO sys_role (id, name, create_time, update_time) VALUES (2, '只读角色', 1, 1623749138537); + +-- 表:sys_role_permission +CREATE TABLE "sys_role_permission" ("role_id" integer NOT NULL, "permission_id" integer NOT NULL, PRIMARY KEY ("role_id", "permission_id")); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 1); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 2); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 3); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 4); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 5); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 6); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 7); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 8); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 9); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 10); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 11); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 12); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 13); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 14); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 15); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 16); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 17); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 18); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, -1); + +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (2, 4); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (2, 6); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (2, 8); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (2, 1); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (2, 2); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (2, 3); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (2, 5); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (2, 7); +INSERT INTO sys_role_permission (role_id, permission_id) VALUES (2, -1); + +-- 表:sys_user +CREATE TABLE "sys_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "username" varchar(100) NOT NULL, "password" varchar(50) NOT NULL, "nick_name" varchar(50), "avatar" varchar(255), "phone_code" varchar(20), "mobile" varchar(20), "email" varchar(100),"remark" varchar(100), "status" integer NOT NULL DEFAULT (1), "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); +INSERT INTO sys_user (id, username, password, nick_name, avatar, phone_code, mobile, email, status, create_time, update_time,remark) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 'admin', NULL, NULL, NULL, NULL, 1, 2011123132, 123132,NULL); +INSERT INTO sys_user (id, username, password, nick_name, avatar, phone_code, mobile, email, status, create_time, update_time,remark) VALUES (2, 'readonly', 'e10adc3949ba59abbe56e057f20f883e', '只读用户', NULL, NULL, NULL, NULL, 1, 2011123132, 123132,'密码:123456'); + +-- 表:sys_user_role +CREATE TABLE "sys_user_role" ("role_id" integer NOT NULL, "user_id" integer NOT NULL, PRIMARY KEY ("role_id", "user_id")); +INSERT INTO sys_user_role (role_id, user_id) VALUES (1, 1); +INSERT INTO sys_user_role (role_id, user_id) VALUES (2, 2); + +-- 索引:IDX_223de54d6badbe43a5490450c3 +CREATE UNIQUE INDEX "IDX_223de54d6badbe43a5490450c3" ON "sys_role" ("name"); + +-- 索引:IDX_9e7164b2f1ea1348bc0eb0a7da +CREATE UNIQUE INDEX "IDX_9e7164b2f1ea1348bc0eb0a7da" ON "sys_user" ("username"); + + diff --git a/packages/ui/certd-server/db/migration/v00002__for_pre.sql b/packages/ui/certd-server/db/migration/v00002__for_pre.sql new file mode 100644 index 000000000..708d7aed5 --- /dev/null +++ b/packages/ui/certd-server/db/migration/v00002__for_pre.sql @@ -0,0 +1,4 @@ +-- for preview 限制演示环境的数据修改 +update sqlite_sequence set seq = 1000 where name = 'sys_user' ; +update sqlite_sequence set seq = 1000 where name = 'sys_permission' ; +update sqlite_sequence set seq = 1000 where name = 'sys_role' ; diff --git a/packages/ui/certd-server/db/migration/v10000__certd.sql b/packages/ui/certd-server/db/migration/v10000__certd.sql new file mode 100644 index 000000000..c3867d6b0 --- /dev/null +++ b/packages/ui/certd-server/db/migration/v10000__certd.sql @@ -0,0 +1,24 @@ +-- +-- 由SQLiteStudio v3.3.3 产生的文件 周六 7月 3 00:38:02 2021 +-- +-- 文本编码:UTF-8 +-- + +-- 表:cd_access +CREATE TABLE "cd_access" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer NOT NULL, "name" varchar(100) NOT NULL, "type" varchar(100) NOT NULL, "setting" varchar(1024), "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); + +-- 表:cd_cert +CREATE TABLE "cd_cert" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer NOT NULL, "domains" varchar(2048) NOT NULL, "email" varchar(100) NOT NULL, "cert_issuer_id" integer, "challenge_type" varchar(100), "challenge_dns_type" varchar(100),"challenge_access_id" integer, "country" varchar(100), "state" varchar(100), "locality" varchar(100), "organization" varchar(100), "organization_unit" varchar(100), "remark" varchar(100), "last_history_id" integer, "last_success_id" integer, "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); + +-- 表:cd_cert_apply_history +CREATE TABLE "cd_cert_apply_history" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer NOT NULL, "cert_id" integer NOT NULL, "success" boolean, "result" varchar(1024), "cert_crt" varchar(1024), "cert_key" varchar(1024), "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); + +-- 表:cd_cert_issuer +CREATE TABLE "cd_cert_issuer" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer NOT NULL, "type" varchar(20) NOT NULL, "account" varchar(100) NOT NULL, "private_key" varchar(1024), "setting" varchar, "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); + +-- 表:cd_task +CREATE TABLE "cd_task" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer NOT NULL, "name" varchar(100), "type" varchar(100), "setting" varchar(2048), "cert_id" integer NOT NULL, "last_history_id" integer, "last_success_id" integer, "remark" varchar(100), "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); + +-- 表:cd_task_history +CREATE TABLE "cd_task_history" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer NOT NULL, "task_id" integer NOT NULL, "cert_id" integer NOT NULL, "cert_apply_history_id" integer NOT NULL, "success" boolean, "result" varchar(2048), "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); + diff --git a/packages/ui/certd-server/f.yaml b/packages/ui/certd-server/f.yaml new file mode 100644 index 000000000..a63871126 --- /dev/null +++ b/packages/ui/certd-server/f.yaml @@ -0,0 +1,11 @@ +service: certd ## 应用发布到云平台的名字,一般指应用名 + +provider: + name: aliyun ## 发布的云平台,aliyun,tencent 等 + +deployType: + type: koa ## 部署的应用类型 + version: 3.0.0 +custom: + customDomain: + domainName: auto diff --git a/packages/ui/certd-server/jest.config.js b/packages/ui/certd-server/jest.config.js new file mode 100644 index 000000000..c5bd388a7 --- /dev/null +++ b/packages/ui/certd-server/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], +}; \ No newline at end of file diff --git a/packages/ui/certd-server/package.json b/packages/ui/certd-server/package.json new file mode 100644 index 000000000..bd8a55d53 --- /dev/null +++ b/packages/ui/certd-server/package.json @@ -0,0 +1,91 @@ +{ + "name": "@fast-crud/fs-server-js", + "version": "0.2.0", + "description": "fast-server base midway", + "private": true, + "midway-integration": { + "lifecycle": { + "before:package:cleanup": "npm run build" + } + }, + "scripts": { + "deploy": "midway-bin deploy", + "start": "NODE_ENV=production node ./bootstrap.js", + "online": "NODE_ENV=production node ./bootstrap.js", + "online:preview": "NODE_ENV=preview node ./bootstrap.js", + "dev": "cross-env NODE_ENV=local midway-bin dev --ts --watchFile='../../core/pipeline/src,../../plugins/'", + "dev:preview": "cross-env NODE_ENV=preview midway-bin dev --ts", + "dev:syncdb": "cross-env NODE_ENV=syncdb midway-bin dev --ts --watchFile='../../core/pipeline/src'", + "test": "midway-bin test --ts", + "cov": "midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix", + "ci": "npm run cov", + "build": "midway-bin build -c", + "build:preview": "cross-env NODE_ENV=preview midway-bin build -c", + "check": "luckyeye", + "mig": "typeorm migration:create -n name" + }, + "dependencies": { + "@alicloud/pop-core": "^1.7.12", + "@certd/pipeline": "^0.3.0", + "@certd/plugin-all": "^0.3.0", + "@koa/cors": "^3.4.3", + "@midwayjs/bootstrap": "^3.9.1", + "@midwayjs/cache": "^3.9.0", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/koa": "^3.9.0", + "@midwayjs/logger": "^2.17.0", + "@midwayjs/typeorm": "^3.9.5", + "@midwayjs/validate": "^3.9.0", + "@midwayjs/i18n": "^3.9.0", + "@types/cache-manager": "^3.4.3", + "cache-manager": "^3.6.3", + "dayjs": "^1.11.7", + "glob": "^7.2.3", + "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.21", + "log4js": "^6.7.1", + "md5": "^2.3.0", + "midway-flyway-js": "^3.0.0", + "node-cron": "^3.0.2", + "sqlite3": "^5.1.4", + "svg-captcha": "^1.4.0", + "typeorm": "^0.3.11" + }, + "devDependencies": { + "@midwayjs/cli": "^1.3.21", + "@midwayjs/luckyeye": "^1.1.0", + "@midwayjs/mock": "^3.9.0", + "@midwayjs/mwcc": "^0.8.0", + "@types/jest": "^26.0.24", + "@types/koa": "2.13.4", + "@types/node": "^14.18.35", + "cross-env": "^6.0.3", + "jest": "^26.6.3", + "mwts": "^1.3.0", + "ts-jest": "^26.5.6", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.1.1", + "typescript": "^4.9.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "midway-bin-clean": [ + ".vscode/.tsbuildinfo", + "dist" + ], + "midway-luckyeye": { + "packages": [ + "midway_v2" + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/fast-crud/fast-server-js" + }, + "author": "Greper", + "license": "MIT" +} diff --git a/packages/ui/certd-server/src/basic/base-controller.ts b/packages/ui/certd-server/src/basic/base-controller.ts new file mode 100644 index 000000000..38a5d76b2 --- /dev/null +++ b/packages/ui/certd-server/src/basic/base-controller.ts @@ -0,0 +1,33 @@ +import { Inject } from '@midwayjs/decorator'; +import { Context } from '@midwayjs/koa'; +import { Constants } from './constants'; + +export abstract class BaseController { + @Inject() + ctx: Context; + + /** + * 成功返回 + * @param data 返回数据 + */ + ok(data) { + const res = { + ...Constants.res.success, + data: undefined, + }; + if (data) { + res.data = data; + } + return res; + } + /** + * 失败返回 + * @param message + */ + fail(msg, code) { + return { + code: code ? code : Constants.res.error.code, + msg: msg ? msg : Constants.res.error.code, + }; + } +} diff --git a/packages/ui/certd-server/src/basic/base-service.ts b/packages/ui/certd-server/src/basic/base-service.ts new file mode 100644 index 000000000..c6b055c90 --- /dev/null +++ b/packages/ui/certd-server/src/basic/base-service.ts @@ -0,0 +1,207 @@ +import { Inject } from '@midwayjs/decorator'; +import { ValidateException } from './exception/validation-exception'; +import * as _ from 'lodash'; +import { Context } from '@midwayjs/koa'; +import { PermissionException } from './exception/permission-exception'; +import { Repository } from 'typeorm'; + +/** + * 服务基类 + */ +export abstract class BaseService { + @Inject() + ctx: Context; + + abstract getRepository(): Repository; + + /** + * 获得单个ID + * @param id ID + * @param infoIgnoreProperty 忽略返回属性 + */ + async info(id, infoIgnoreProperty?): Promise { + if (!id) { + throw new ValidateException('id不能为空'); + } + // @ts-ignore + const info = await this.getRepository().findOne({ where: { id } }); + if (info && infoIgnoreProperty) { + for (const property of infoIgnoreProperty) { + delete info[property]; + } + } + return info; + } + + /** + * 非分页查询 + * @param option 查询配置 + */ + async find(options) { + return await this.getRepository().find(options); + } + + /** + * 删除 + * @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3 + */ + async delete(ids) { + if (ids instanceof Array) { + await this.getRepository().delete(ids); + } else if (typeof ids === 'string') { + await this.getRepository().delete(ids.split(',')); + } else { + //ids是一个condition + await this.getRepository().delete(ids); + } + await this.modifyAfter(ids); + } + + /** + * 新增|修改 + * @param param 数据 + */ + async addOrUpdate(param) { + await this.getRepository().save(param); + } + + /** + * 新增 + * @param param 数据 + */ + async add(param) { + const now = new Date().getTime(); + param.createTime = now; + param.updateTime = now; + await this.addOrUpdate(param); + await this.modifyAfter(param); + return { + id: param.id, + }; + } + + /** + * 修改 + * @param param 数据 + */ + async update(param) { + if (!param.id) throw new ValidateException('no id'); + param.updateTime = new Date().getTime(); + await this.addOrUpdate(param); + await this.modifyAfter(param); + } + + /** + * 新增|修改|删除 之后的操作 + * @param data 对应数据 + */ + async modifyAfter(data) {} + + /** + * 分页查询 + * @param query 查询条件 bean + * @param page + * @param order + * @param buildQuery + */ + async page(query, page = { offset: 0, limit: 20 }, order, buildQuery) { + if (page.offset == null) { + page.offset = 0; + } + if (page.limit == null) { + page.limit = 20; + } + const qb = this.getRepository().createQueryBuilder('main'); + if (order && order.prop) { + qb.orderBy('main.' + order.prop, order.asc ? 'ASC' : 'DESC'); + } else { + qb.orderBy('id', 'DESC'); + } + qb.offset(page.offset).limit(page.limit); + //根据bean query + if (query) { + let whereSql = ''; + let index = 0; + _.forEach(query, (value, key) => { + if (!value) { + return; + } + if (index !== 0) { + whereSql += ' and '; + } + whereSql += ` main.${key} = :${key} `; + index++; + }); + if (index > 0) { + qb.where(whereSql, query); + } + } + //自定义query + if (buildQuery) { + buildQuery(qb); + } + const list = await qb.getMany(); + const total = await qb.getCount(); + return { + records: list, + total, + offset: page.offset, + limit: page.limit, + }; + } + + /** + * 分页查询 + * @param query 查询条件 bean + * @param order + * @param buildQuery + */ + async list(query, order, buildQuery) { + const qb = this.getRepository().createQueryBuilder('main'); + if (order && order.prop) { + qb.orderBy('main.' + order.prop, order.asc ? 'ASC' : 'DESC'); + } else { + qb.orderBy('id', 'DESC'); + } + //根据bean query + if (query) { + let whereSql = ''; + let index = 0; + _.forEach(query, (value, key) => { + if (!value) { + return; + } + if (index !== 0) { + whereSql += ' and '; + } + whereSql += ` main.${key} = :${key} `; + index++; + }); + if (index > 0) { + qb.where(whereSql, query); + } + } + //自定义query + if (buildQuery) { + buildQuery(qb); + } + return await qb.getMany(); + } + + async checkUserId(id = 0, userId, userKey = 'userId') { + // @ts-ignore + const res = await this.getRepository().findOne({ + // @ts-ignore + select: { [userKey]: true }, + where: { + // @ts-ignore + id, + }, + }); + // @ts-ignore + if (!res || res.userId === userId) { + return; + } + throw new PermissionException('权限不足'); + } +} diff --git a/packages/ui/certd-server/src/basic/constants.ts b/packages/ui/certd-server/src/basic/constants.ts new file mode 100644 index 000000000..992f13af0 --- /dev/null +++ b/packages/ui/certd-server/src/basic/constants.ts @@ -0,0 +1,28 @@ +export const Constants = { + res: { + error: { + code: 1, + message: 'error', + }, + success: { + code: 0, + message: 'success', + }, + validation: { + code: 10, + message: '参数错误', + }, + auth: { + code: 401, + message: '您还未登录或token已过期', + }, + permission: { + code: 402, + message: '您没有权限', + }, + preview: { + code: 10001, + message: '对不起,预览环境不允许修改此数据', + }, + }, +}; diff --git a/packages/ui/certd-server/src/basic/crud-controller.ts b/packages/ui/certd-server/src/basic/crud-controller.ts new file mode 100644 index 000000000..131cba5c7 --- /dev/null +++ b/packages/ui/certd-server/src/basic/crud-controller.ts @@ -0,0 +1,64 @@ +import { ALL, Body, Post, Query } from '@midwayjs/decorator'; +import { BaseController } from './base-controller'; + +export abstract class CrudController extends BaseController { + abstract getService(); + + @Post('/page') + async page( + @Body(ALL) + body + ) { + const pageRet = await this.getService().page( + body?.query, + body?.page, + body?.sort, + null + ); + return this.ok(pageRet); + } + + @Post('/list') + async list( + @Body(ALL) + body + ) { + const listRet = await this.getService().list(body, null, null); + return this.ok(listRet); + } + + @Post('/add') + async add( + @Body(ALL) + bean + ) { + const id = await this.getService().add(bean); + return this.ok(id); + } + + @Post('/info') + async info( + @Query('id') + id + ) { + const bean = await this.getService().info(id); + return this.ok(bean); + } + + @Post('/update') + async update( + @Body(ALL) + bean + ) { + await this.getService().update(bean); + return this.ok(null); + } + @Post('/delete') + async delete( + @Query('id') + id + ) { + await this.getService().delete([id]); + return this.ok(null); + } +} diff --git a/packages/ui/certd-server/src/basic/enum-item.ts b/packages/ui/certd-server/src/basic/enum-item.ts new file mode 100644 index 000000000..a7e4a4a88 --- /dev/null +++ b/packages/ui/certd-server/src/basic/enum-item.ts @@ -0,0 +1,11 @@ +export class EnumItem { + value: string; + label: string; + color: string; + + constructor(value, label, color) { + this.value = value; + this.label = label; + this.color = color; + } +} diff --git a/packages/ui/certd-server/src/basic/exception/auth-exception.ts b/packages/ui/certd-server/src/basic/exception/auth-exception.ts new file mode 100644 index 000000000..92e82a4d7 --- /dev/null +++ b/packages/ui/certd-server/src/basic/exception/auth-exception.ts @@ -0,0 +1,14 @@ +import { Constants } from '../constants'; +import { BaseException } from './base-exception'; +/** + * 授权异常 + */ +export class AuthException extends BaseException { + constructor(message) { + super( + 'AuthException', + Constants.res.auth.code, + message ? message : Constants.res.auth.message + ); + } +} diff --git a/packages/ui/certd-server/src/basic/exception/base-exception.ts b/packages/ui/certd-server/src/basic/exception/base-exception.ts new file mode 100644 index 000000000..a4e89604b --- /dev/null +++ b/packages/ui/certd-server/src/basic/exception/base-exception.ts @@ -0,0 +1,11 @@ +/** + * 异常基类 + */ +export class BaseException extends Error { + status: number; + constructor(name, code, message) { + super(message); + this.name = name; + this.status = code; + } +} diff --git a/packages/ui/certd-server/src/basic/exception/common-exception.ts b/packages/ui/certd-server/src/basic/exception/common-exception.ts new file mode 100644 index 000000000..f4afa91e6 --- /dev/null +++ b/packages/ui/certd-server/src/basic/exception/common-exception.ts @@ -0,0 +1,14 @@ +import { Constants } from '../constants'; +import { BaseException } from './base-exception'; +/** + * 通用异常 + */ +export class CommonException extends BaseException { + constructor(message) { + super( + 'CommonException', + Constants.res.error.code, + message ? message : Constants.res.error.message + ); + } +} diff --git a/packages/ui/certd-server/src/basic/exception/permission-exception.ts b/packages/ui/certd-server/src/basic/exception/permission-exception.ts new file mode 100644 index 000000000..edeb2b7e2 --- /dev/null +++ b/packages/ui/certd-server/src/basic/exception/permission-exception.ts @@ -0,0 +1,14 @@ +import { Constants } from '../constants'; +import { BaseException } from './base-exception'; +/** + * 授权异常 + */ +export class PermissionException extends BaseException { + constructor(message) { + super( + 'PermissionException', + Constants.res.permission.code, + message ? message : Constants.res.permission.message + ); + } +} diff --git a/packages/ui/certd-server/src/basic/exception/preview-exception.ts b/packages/ui/certd-server/src/basic/exception/preview-exception.ts new file mode 100644 index 000000000..2abfc48e9 --- /dev/null +++ b/packages/ui/certd-server/src/basic/exception/preview-exception.ts @@ -0,0 +1,14 @@ +import { Constants } from '../constants'; +import { BaseException } from './base-exception'; +/** + * 预览模式 + */ +export class PreviewException extends BaseException { + constructor(message) { + super( + 'PreviewException', + Constants.res.preview.code, + message ? message : Constants.res.preview.message + ); + } +} diff --git a/packages/ui/certd-server/src/basic/exception/validation-exception.ts b/packages/ui/certd-server/src/basic/exception/validation-exception.ts new file mode 100644 index 000000000..d595ff71b --- /dev/null +++ b/packages/ui/certd-server/src/basic/exception/validation-exception.ts @@ -0,0 +1,14 @@ +import { Constants } from '../constants'; +import { BaseException } from './base-exception'; +/** + * 校验异常 + */ +export class ValidateException extends BaseException { + constructor(message) { + super( + 'ValidateException', + Constants.res.validation.code, + message ? message : Constants.res.validation.message + ); + } +} diff --git a/packages/ui/certd-server/src/basic/result.ts b/packages/ui/certd-server/src/basic/result.ts new file mode 100644 index 000000000..ea1022444 --- /dev/null +++ b/packages/ui/certd-server/src/basic/result.ts @@ -0,0 +1,18 @@ +export class Result { + code: number; + msg: string; + data: T; + constructor(code, msg, data?) { + this.code = code; + this.msg = msg; + this.data = data; + } + + static error(code = 1, msg) { + return new Result(code, msg, null); + } + + static success(msg, data?) { + return new Result(0, msg, data); + } +} diff --git a/packages/ui/certd-server/src/config/config.default.ts b/packages/ui/certd-server/src/config/config.default.ts new file mode 100644 index 000000000..ed2d93628 --- /dev/null +++ b/packages/ui/certd-server/src/config/config.default.ts @@ -0,0 +1,60 @@ +import { join } from 'path'; +import { FlywayHistory } from 'midway-flyway-js/dist/entity'; + +import { MidwayConfig } from '@midwayjs/core'; +import { UserEntity } from '../modules/authority/entity/user'; + +export default { + // use for cookie sign key, should change to your own and keep security + keys: 'certd666', + koa: { + port: 7001, + }, + cron: {}, + /** + * 演示环境 + */ + preview: { + enabled: false, + }, + + /** + * 数据库 + */ + typeorm: { + dataSource: { + default: { + /** + * 单数据库实例 + */ + type: 'sqlite', + database: join(__dirname, '../../data/db.sqlite'), + synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true + logging: true, + + // 配置实体模型 或者 entities: '/entity', + entities: [ + '**/modules/*/entity/*.ts', + FlywayHistory, + UserEntity, + ], + }, + }, + }, + /** + * 自动升级数据库脚本 + */ + flyway: { + scriptDir: join(__dirname, '../../db/migration'), + }, + + biz: { + jwt: { + secret: 'greper-is-666', + expire: 7 * 24 * 60, //单位秒 + }, + auth: { + ignoreUrls: ['/', '/api/login', '/api/register'], + }, + }, +} as MidwayConfig; diff --git a/packages/ui/certd-server/src/config/config.preview.ts b/packages/ui/certd-server/src/config/config.preview.ts new file mode 100644 index 000000000..aa4bc6aac --- /dev/null +++ b/packages/ui/certd-server/src/config/config.preview.ts @@ -0,0 +1,10 @@ +import { MidwayConfig } from '@midwayjs/core'; + +export default { + /** + * 演示环境 + */ + preview: { + enabled: true, + } +} as MidwayConfig; diff --git a/packages/ui/certd-server/src/config/config.production.ts b/packages/ui/certd-server/src/config/config.production.ts new file mode 100644 index 000000000..aa4bc6aac --- /dev/null +++ b/packages/ui/certd-server/src/config/config.production.ts @@ -0,0 +1,10 @@ +import { MidwayConfig } from '@midwayjs/core'; + +export default { + /** + * 演示环境 + */ + preview: { + enabled: true, + } +} as MidwayConfig; diff --git a/packages/ui/certd-server/src/config/config.syncdb.ts b/packages/ui/certd-server/src/config/config.syncdb.ts new file mode 100644 index 000000000..478af50b6 --- /dev/null +++ b/packages/ui/certd-server/src/config/config.syncdb.ts @@ -0,0 +1,11 @@ +import { MidwayConfig } from '@midwayjs/core'; + +export default { + typeorm: { + dataSource: { + default: { + synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true + }, + }, + }, +} as MidwayConfig; diff --git a/packages/ui/certd-server/src/configuration.ts b/packages/ui/certd-server/src/configuration.ts new file mode 100644 index 000000000..97970bd52 --- /dev/null +++ b/packages/ui/certd-server/src/configuration.ts @@ -0,0 +1,60 @@ +import * as validateComp from '@midwayjs/validate'; +import * as productionConfig from './config/config.production'; +import * as previewConfig from './config/config.preview'; +import * as defaultConfig from './config/config.default'; +import { Configuration, App } from '@midwayjs/decorator'; +import * as koa from '@midwayjs/koa'; +import * as orm from '@midwayjs/typeorm'; +import * as cache from '@midwayjs/cache'; +import cors from '@koa/cors'; +import { join } from 'path'; +import * as flyway from 'midway-flyway-js'; +import { ReportMiddleware } from './middleware/report'; +import { GlobalExceptionMiddleware } from './middleware/global-exception'; +import { PreviewMiddleware } from './middleware/preview'; +import { AuthorityMiddleware } from './middleware/authority'; +import * as pipeline from './plugins/pipeline'; +import * as cron from './plugins/cron'; +@Configuration({ + imports: [koa, orm, cache, flyway, validateComp,pipeline, cron], + importConfigs: [ + { + default: defaultConfig, + preview: previewConfig, + production: productionConfig, + }, + ], +}) +export class ContainerConfiguration {} +@Configuration({ + conflictCheck: true, + importConfigs: [join(__dirname, './config')], +}) +export class ContainerLifeCycle { + @App() + app: koa.Application; + + async onReady() { + //跨域 + this.app.use( + cors({ + origin: '*', + }) + ); + // bodyparser options see https://github.com/koajs/bodyparser + //this.app.use(bodyParser()); + //请求日志打印 + + this.app.useMiddleware([ + ReportMiddleware, + //统一异常处理 + GlobalExceptionMiddleware, + //预览模式限制修改id<1000的数据 + PreviewMiddleware, + //授权处理 + AuthorityMiddleware, + ]); + + //加载插件 + } +} diff --git a/packages/ui/certd-server/src/controller/home.ts b/packages/ui/certd-server/src/controller/home.ts new file mode 100644 index 000000000..6aa5a9a74 --- /dev/null +++ b/packages/ui/certd-server/src/controller/home.ts @@ -0,0 +1,10 @@ +import { Controller, Get, Provide } from '@midwayjs/decorator'; + +@Provide() +@Controller('/') +export class HomeController { + @Get('/') + async home(): Promise { + return 'Hello Midwayjs!'; + } +} diff --git a/packages/ui/certd-server/src/middleware/authority.ts b/packages/ui/certd-server/src/middleware/authority.ts new file mode 100644 index 000000000..431d2b692 --- /dev/null +++ b/packages/ui/certd-server/src/middleware/authority.ts @@ -0,0 +1,48 @@ +import { Config, Provide } from '@midwayjs/decorator'; +import { + IWebMiddleware, + IMidwayKoaContext, + IMidwayKoaNext, +} from '@midwayjs/koa'; +import * as _ from 'lodash'; +import * as jwt from 'jsonwebtoken'; +import { Constants } from '../basic/constants'; + +/** + * 权限校验 + */ +@Provide() +export class AuthorityMiddleware implements IWebMiddleware { + @Config('biz.jwt.secret') + private secret: string; + @Config('biz.auth.ignoreUrls') + private ignoreUrls: string[]; + + resolve() { + return async (ctx: IMidwayKoaContext, next: IMidwayKoaNext) => { + const { url } = ctx; + const token = ctx.get('Authorization'); + // 路由地址为 admin前缀的 需要权限校验 + // console.log('ctx', ctx); + const queryIndex = url.indexOf('?'); + let uri = url; + if (queryIndex >= 0) { + uri = url.substring(0, queryIndex); + } + const yes = this.ignoreUrls.includes(uri); + if (yes) { + await next(); + return; + } + + try { + ctx.user = jwt.verify(token, this.secret); + } catch (err) { + ctx.status = 401; + ctx.body = Constants.res.auth; + return; + } + await next(); + }; + } +} diff --git a/packages/ui/certd-server/src/middleware/global-exception.ts b/packages/ui/certd-server/src/middleware/global-exception.ts new file mode 100644 index 000000000..b67faf118 --- /dev/null +++ b/packages/ui/certd-server/src/middleware/global-exception.ts @@ -0,0 +1,27 @@ +import { Provide } from '@midwayjs/decorator'; +import { + IWebMiddleware, + IMidwayKoaContext, + IMidwayKoaNext, +} from '@midwayjs/koa'; +import { logger } from '../utils/logger'; +import { Result } from '../basic/result'; + +@Provide() +export class GlobalExceptionMiddleware implements IWebMiddleware { + resolve() { + return async (ctx: IMidwayKoaContext, next: IMidwayKoaNext) => { + const { url } = ctx; + const startTime = Date.now(); + logger.info('请求开始:', url); + try { + await next(); + logger.info('请求完成', url, Date.now() - startTime + 'ms'); + } catch (err) { + logger.error('请求异常:', url, Date.now() - startTime + 'ms', err); + ctx.status = 200; + ctx.body = Result.error(err.code != null ? err.code : 1, err.message); + } + }; + } +} diff --git a/packages/ui/certd-server/src/middleware/preview.ts b/packages/ui/certd-server/src/middleware/preview.ts new file mode 100644 index 000000000..c93ecfa7b --- /dev/null +++ b/packages/ui/certd-server/src/middleware/preview.ts @@ -0,0 +1,50 @@ +import { Config, Provide } from '@midwayjs/decorator'; +import { + IMidwayKoaContext, + IMidwayKoaNext, + IWebMiddleware, +} from '@midwayjs/koa'; +import { PreviewException } from '../basic/exception/preview-exception'; + +/** + * 预览模式 + */ +@Provide() +export class PreviewMiddleware implements IWebMiddleware { + @Config('preview.enabled') + private preview: boolean; + + resolve() { + return async (ctx: IMidwayKoaContext, next: IMidwayKoaNext) => { + if (!this.preview) { + await next(); + return; + } + let { url, request } = ctx; + const body: any = request.body; + let id = body.id || request.query.id; + const roleId = body.roleId; + if (id == null && roleId != null) { + id = roleId; + } + if (id != null && typeof id === 'string') { + id = parseInt(id); + } + if (url.indexOf('?') !== -1) { + url = url.substring(0, url.indexOf('?')); + } + const isModify = + url.endsWith('update') || + url.endsWith('delete') || + url.endsWith('authz'); + const isPreviewId = id < 1000; + if (this.preview && isModify && isPreviewId) { + throw new PreviewException( + '对不起,预览环境不允许修改此数据,如需体验请添加新数据' + ); + } + await next(); + return; + }; + } +} diff --git a/packages/ui/certd-server/src/middleware/report.ts b/packages/ui/certd-server/src/middleware/report.ts new file mode 100644 index 000000000..d81eff85d --- /dev/null +++ b/packages/ui/certd-server/src/middleware/report.ts @@ -0,0 +1,28 @@ +import { Provide } from '@midwayjs/decorator'; +import { + IWebMiddleware, + IMidwayKoaContext, + IMidwayKoaNext, +} from '@midwayjs/koa'; +import { logger } from '../utils/logger'; + +@Provide() +export class ReportMiddleware implements IWebMiddleware { + resolve() { + return async (ctx: IMidwayKoaContext, next: IMidwayKoaNext) => { + const { url } = ctx; + logger.info('请求开始:', url); + const startTime = Date.now(); + await next(); + if (ctx.status !== 200) { + logger.error( + '请求失败:', + url, + ctx.status, + Date.now() - startTime + 'ms' + ); + } + logger.info('请求完成:', url, ctx.status, Date.now() - startTime + 'ms'); + }; + } +} diff --git a/packages/ui/certd-server/src/modules/authority/controller/permission-controller.ts b/packages/ui/certd-server/src/modules/authority/controller/permission-controller.ts new file mode 100644 index 000000000..6e0f0e176 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/controller/permission-controller.ts @@ -0,0 +1,62 @@ +import { + ALL, + Body, + Controller, + Inject, + Post, + Provide, + Query, +} from '@midwayjs/decorator'; +import { CrudController } from '../../../basic/crud-controller'; +import { PermissionService } from '../service/permission-service'; + +/** + * 权限资源 + */ +@Provide() +@Controller('/api/sys/authority/permission') +export class PermissionController extends CrudController { + @Inject() + service: PermissionService; + + getService() { + return this.service; + } + + @Post('/page') + async page( + @Body(ALL) + body + ) { + return await super.page(body); + } + + @Post('/add') + async add( + @Body(ALL) + bean + ) { + return await super.add(bean); + } + + @Post('/update') + async update( + @Body(ALL) + bean + ) { + return await super.update(bean); + } + @Post('/delete') + async delete( + @Query('id') + id + ) { + return await super.delete(id); + } + + @Post('/tree') + async tree() { + const tree = await this.service.tree({}); + return this.ok(tree); + } +} diff --git a/packages/ui/certd-server/src/modules/authority/controller/role-controller.ts b/packages/ui/certd-server/src/modules/authority/controller/role-controller.ts new file mode 100644 index 000000000..3c3a99199 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/controller/role-controller.ts @@ -0,0 +1,95 @@ +import { + ALL, + Body, + Controller, + Inject, + Post, + Provide, + Query, +} from '@midwayjs/decorator'; +import { CrudController } from '../../../basic/crud-controller'; +import { RoleService } from '../service/role-service'; + +/** + * 系统用户 + */ +@Provide() +@Controller('/api/sys/authority/role') +export class RoleController extends CrudController { + @Inject() + service: RoleService; + + getService() { + return this.service; + } + + @Post('/page') + async page( + @Body(ALL) + body + ) { + return await super.page(body); + } + + @Post('/list') + async list() { + const ret = await this.service.find({}); + return this.ok(ret); + } + + @Post('/add') + async add( + @Body(ALL) + bean + ) { + return await super.add(bean); + } + + @Post('/update') + async update( + @Body(ALL) + bean + ) { + return await super.update(bean); + } + @Post('/delete') + async delete( + @Query('id') + id + ) { + return await super.delete(id); + } + + @Post('/getPermissionTree') + async getPermissionTree( + @Query('id') + id + ) { + const ret = await this.service.getPermissionTreeByRoleId(id); + return this.ok(ret); + } + + @Post('/getPermissionIds') + async getPermissionIds( + @Query('id') + id + ) { + const ret = await this.service.getPermissionIdsByRoleId(id); + return this.ok(ret); + } + + /** + * 给角色授予权限 + * @param id + */ + @Post('/authz') + async authz( + @Body('roleId') + roleId, + @Body('permissionIds') + permissionIds + ) { + await this.service.authz(roleId, permissionIds); + return this.ok(null); + } +} diff --git a/packages/ui/certd-server/src/modules/authority/controller/user-controller.ts b/packages/ui/certd-server/src/modules/authority/controller/user-controller.ts new file mode 100644 index 000000000..1b8096240 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/controller/user-controller.ts @@ -0,0 +1,118 @@ +import { + Provide, + Controller, + Post, + Inject, + Body, + Query, + ALL, +} from '@midwayjs/decorator'; +import { UserService } from '../service/user-service'; +import { CrudController } from '../../../basic/crud-controller'; +import { RoleService } from '../service/role-service'; +import { PermissionService } from '../service/permission-service'; + +/** + * 系统用户 + */ +@Provide() +@Controller('/api/sys/authority/user') +export class UserController extends CrudController { + @Inject() + service: UserService; + + @Inject() + roleService: RoleService; + @Inject() + permissionService: PermissionService; + + getService() { + return this.service; + } + + @Post('/page') + async page( + @Body(ALL) + body + ) { + const ret = await super.page(body); + + const users = ret.data.records; + + //获取roles + const userIds = users.map(item => item.id); + const userRoles = await this.roleService.getByUserIds(userIds); + const userRolesMap = new Map(); + for (const ur of userRoles) { + let roles = userRolesMap.get(ur.userId); + if (roles == null) { + roles = []; + userRolesMap.set(ur.userId, roles); + } + roles.push(ur.roleId); + } + + for (const record of users) { + //withRoles + record.roles = userRolesMap.get(record.id); + //删除密码字段 + delete record.password; + } + + return ret; + } + + @Post('/add') + async add( + @Body(ALL) + bean + ) { + return await super.add(bean); + } + + @Post('/update') + async update( + @Body(ALL) + bean + ) { + return await super.update(bean); + } + @Post('/delete') + async delete( + @Query('id') + id + ) { + return await super.delete(id); + } + + /** + * 当前登录用户的个人信息 + */ + @Post('/mine') + public async mine() { + const id = this.ctx.user.id; + const info = await this.service.info(id, ['password']); + return this.ok(info); + } + + /** + * 当前登录用户的权限列表 + */ + @Post('/permissions') + public async permissions() { + const id = this.ctx.user.id; + const permissions = await this.service.getUserPermissions(id); + return this.ok(permissions); + } + + /** + * 当前登录用户的权限树形列表 + */ + @Post('/permissionTree') + public async permissionTree() { + const id = this.ctx.user.id; + const permissions = await this.service.getUserPermissions(id); + const tree = this.permissionService.buildTree(permissions); + return this.ok(tree); + } +} diff --git a/packages/ui/certd-server/src/modules/authority/entity/permission.ts b/packages/ui/certd-server/src/modules/authority/entity/permission.ts new file mode 100644 index 000000000..6fa91e18d --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/entity/permission.ts @@ -0,0 +1,40 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +/** + * 权限 + */ +@Entity('sys_permission') +export class PermissionEntity { + @PrimaryGeneratedColumn() + id: number; + @Column({ comment: '标题', length: 100 }) + title: string; + /** + * 权限代码 + * 示例:sys:user:read + */ + @Column({ comment: '权限代码', length: 100, nullable: true }) + permission: string; + + @Column({ name: 'parent_id', comment: '父节点ID', default: -1 }) + parentId: number; + + @Column({ comment: '排序号' }) + sort: number; + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; + + // @ManyToMany(type => RoleEntity, res => res.permissions) + // roles: RoleEntity[]; +} diff --git a/packages/ui/certd-server/src/modules/authority/entity/role-permission.ts b/packages/ui/certd-server/src/modules/authority/entity/role-permission.ts new file mode 100644 index 000000000..85ac0c734 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/entity/role-permission.ts @@ -0,0 +1,12 @@ +import { Entity, PrimaryColumn } from 'typeorm'; + +/** + * 角色权限多对多 + */ +@Entity('sys_role_permission') +export class RolePermissionEntity { + @PrimaryColumn({ name: 'role_id' }) + roleId: number; + @PrimaryColumn({ name: 'permission_id' }) + permissionId: number; +} diff --git a/packages/ui/certd-server/src/modules/authority/entity/role.ts b/packages/ui/certd-server/src/modules/authority/entity/role.ts new file mode 100644 index 000000000..2aecfb743 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/entity/role.ts @@ -0,0 +1,43 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; + +/** + * 角色 + */ +@Entity('sys_role') +export class RoleEntity { + @PrimaryGeneratedColumn() + id: number; + @Index({ unique: true }) + @Column({ comment: '角色名称', length: 100 }) + name: string; + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; + + // @ManyToMany(type => PermissionEntity, res => res.roles) + // @JoinTable({ + // name: 'sys_role_resources', + // joinColumn: { + // name: 'roleId', + // referencedColumnName: 'id', + // }, + // inverseJoinColumn: { + // name: 'resourceId', + // referencedColumnName: 'id', + // }, + // }) + // resources: PermissionEntity[]; + + // @ManyToMany(type => UserEntity, res => res.roles) + // users: UserEntity[]; +} diff --git a/packages/ui/certd-server/src/modules/authority/entity/user-role.ts b/packages/ui/certd-server/src/modules/authority/entity/user-role.ts new file mode 100644 index 000000000..533bf6441 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/entity/user-role.ts @@ -0,0 +1,12 @@ +import { Entity, PrimaryColumn } from 'typeorm'; + +/** + * 用户角色多对多 + */ +@Entity('sys_user_role') +export class UserRoleEntity { + @PrimaryColumn({ name: 'role_id' }) + roleId: number; + @PrimaryColumn({ name: 'user_id' }) + userId: number; +} diff --git a/packages/ui/certd-server/src/modules/authority/entity/user.ts b/packages/ui/certd-server/src/modules/authority/entity/user.ts new file mode 100644 index 000000000..598245836 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/entity/user.ts @@ -0,0 +1,63 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; + +/** + * 系统用户 + */ +@Entity('sys_user') +export class UserEntity { + @PrimaryGeneratedColumn() + id: number; + @Index({ unique: true }) + @Column({ comment: '用户名', length: 100 }) + username: string; + + @Column({ comment: '密码', length: 100 }) + password: string; + + @Column({ name: 'nick_name', comment: '昵称', length: 100, nullable: true }) + nickName: string; + + @Column({ comment: '头像', length: 255, nullable: true }) + avatar: string; + + @Column({ name: 'phone_code', comment: '区号', length: 20, nullable: true }) + phoneCode: string; + + @Column({ comment: '手机', length: 20, nullable: true }) + mobile: string; + + @Column({ comment: '邮箱', length: 50, nullable: true }) + email: string; + + @Column({ comment: '备注', length: 100, nullable: true }) + remark: string; + + @Column({ comment: '状态 0:禁用 1:启用', default: 1, type: 'int' }) + status: number; + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; + + // @ManyToMany(type => RoleEntity, res => res.users) + // @JoinTable({ + // name: 'sys_user_roles', + // joinColumn: { + // name: 'userId', + // referencedColumnName: 'id', + // }, + // inverseJoinColumn: { + // name: 'roleId', + // referencedColumnName: 'id', + // }, + // }) + // roles: RoleEntity[]; +} diff --git a/packages/ui/certd-server/src/modules/authority/enums/ResourceTypeEnum.ts b/packages/ui/certd-server/src/modules/authority/enums/ResourceTypeEnum.ts new file mode 100644 index 000000000..cf3411ece --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/enums/ResourceTypeEnum.ts @@ -0,0 +1,17 @@ +import { EnumItem } from '../../../basic/enum-item'; +import * as _ from 'lodash'; +class ResourceTypes { + MENU = new EnumItem('menu', '菜单', 'blue'); + BTN = new EnumItem('btn', '按钮', 'green'); + ROUTE = new EnumItem('route', '路由', 'red'); + + names() { + const list = []; + _.forEach(this, (item, key) => { + list.push(item); + }); + return list; + } +} + +export const ResourceTypeEnum = new ResourceTypes(); diff --git a/packages/ui/certd-server/src/modules/authority/service/permission-service.ts b/packages/ui/certd-server/src/modules/authority/service/permission-service.ts new file mode 100644 index 000000000..950a04973 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/service/permission-service.ts @@ -0,0 +1,52 @@ +import { Provide } from '@midwayjs/decorator'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseService } from '../../../basic/base-service'; +import { PermissionEntity } from '../entity/permission'; + +/** + * 权限资源 + */ +@Provide() +export class PermissionService extends BaseService { + @InjectEntityModel(PermissionEntity) + repository: Repository; + + getRepository() { + return this.repository; + } + + async tree(options: any = {}) { + if (options.order == null) { + options.order = { + sort: 'ASC', + }; + } + const list = await this.find(options); + return this.buildTree(list); + } + + buildTree(list: any) { + const idMap = {}; + const root = []; + for (const item of list) { + idMap[item.id] = item; + if (item.parentId == null || item.parentId <= 0) { + root.push(item); + } + } + + for (const item of list) { + if (item.parentId > 0) { + const parent = idMap[item.parentId]; + if (parent) { + if (parent.children == null) { + parent.children = []; + } + parent.children.push(item); + } + } + } + return root; + } +} diff --git a/packages/ui/certd-server/src/modules/authority/service/role-permission-service.ts b/packages/ui/certd-server/src/modules/authority/service/role-permission-service.ts new file mode 100644 index 000000000..e2b2b3d4c --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/service/role-permission-service.ts @@ -0,0 +1,18 @@ +import { Provide } from '@midwayjs/decorator'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseService } from '../../../basic/base-service'; +import { RolePermissionEntity } from '../entity/role-permission'; + +/** + * 角色->权限 + */ +@Provide() +export class RolePermissionService extends BaseService { + @InjectEntityModel(RolePermissionEntity) + repository: Repository; + + getRepository() { + return this.repository; + } +} diff --git a/packages/ui/certd-server/src/modules/authority/service/role-service.ts b/packages/ui/certd-server/src/modules/authority/service/role-service.ts new file mode 100644 index 000000000..330bc360a --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/service/role-service.ts @@ -0,0 +1,101 @@ +import { Inject, Provide } from '@midwayjs/decorator'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { BaseService } from '../../../basic/base-service'; +import { RoleEntity } from '../entity/role'; +import { UserRoleService } from './user-role-service'; +import { RolePermissionEntity } from '../entity/role-permission'; +import { PermissionService } from './permission-service'; +import * as _ from 'lodash'; +import { RolePermissionService } from './role-permission-service'; +/** + * 角色 + */ +@Provide() +export class RoleService extends BaseService { + @InjectEntityModel(RoleEntity) + repository: Repository; + @Inject() + userRoleService: UserRoleService; + @Inject() + permissionService: PermissionService; + @Inject() + rolePermissionService: RolePermissionService; + + getRepository() { + return this.repository; + } + + async getRoleIdsByUserId(id: any) { + const userRoles = await this.userRoleService.find({ + where: { userId: id }, + }); + return userRoles.map(item => item.roleId); + } + async getByUserIds(ids: any) { + return await this.userRoleService.find({ + where: { + userId: In(ids), + }, + }); + } + + async getPermissionByRoleIds(roleIds: any) { + return await this.permissionService.repository + .createQueryBuilder('permission') + .innerJoinAndSelect( + RolePermissionEntity, + 'rp', + 'rp.permissionId = permission.id and rp.roleId in (:...roleIds)', + { roleIds } + ) + .getMany(); + } + + async addRoles(userId: number, roles) { + if (roles == null || roles.length === 0) { + return; + } + for (const roleId of roles) { + await this.userRoleService.add({ + userId, + roleId, + }); + } + } + + async updateRoles(userId, roles) { + if (roles == null) { + return; + } + const oldRoleIds = await this.getRoleIdsByUserId(userId); + if (_.xor(roles, oldRoleIds).length === 0) { + //如果两个数组相等,则不修改 + return; + } + //先删除所有 + await this.userRoleService.delete({ userId }); + //再添加 + await this.addRoles(userId, roles); + } + + async getPermissionTreeByRoleId(id: any) { + const list = await this.getPermissionByRoleIds([id]); + return this.permissionService.buildTree(list); + } + + async getPermissionIdsByRoleId(id: any) { + const list = await this.getPermissionByRoleIds([id]); + return list.map(item => item.id); + } + + async authz(roleId: any, permissionIds: any) { + await this.rolePermissionService.delete({ roleId }); + for (const permissionId of permissionIds) { + await this.rolePermissionService.add({ + roleId, + permissionId, + }); + } + } +} diff --git a/packages/ui/certd-server/src/modules/authority/service/user-role-service.ts b/packages/ui/certd-server/src/modules/authority/service/user-role-service.ts new file mode 100644 index 000000000..7c528cff4 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/service/user-role-service.ts @@ -0,0 +1,18 @@ +import { Provide } from '@midwayjs/decorator'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseService } from '../../../basic/base-service'; +import { UserRoleEntity } from '../entity/user-role'; + +/** + * 用户->角色 + */ +@Provide() +export class UserRoleService extends BaseService { + @InjectEntityModel(UserRoleEntity) + repository: Repository; + + getRepository() { + return this.repository; + } +} diff --git a/packages/ui/certd-server/src/modules/authority/service/user-service.ts b/packages/ui/certd-server/src/modules/authority/service/user-service.ts new file mode 100644 index 000000000..0fc0c5f32 --- /dev/null +++ b/packages/ui/certd-server/src/modules/authority/service/user-service.ts @@ -0,0 +1,113 @@ +import { Inject, Provide } from '@midwayjs/decorator'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserEntity } from '../entity/user'; +import _ from 'lodash'; +import md5 from 'md5'; +import { CommonException } from '../../../basic/exception/common-exception'; +import { BaseService } from '../../../basic/base-service'; +import { logger } from '../../../utils/logger'; +import { RoleService } from './role-service'; +import { PermissionService } from './permission-service'; +import { UserRoleService } from './user-role-service'; + +/** + * 系统用户 + */ +@Provide() +export class UserService extends BaseService { + @InjectEntityModel(UserEntity) + repository: Repository; + @Inject() + roleService: RoleService; + @Inject() + permissionService: PermissionService; + @Inject() + userRoleService: UserRoleService; + + getRepository() { + return this.repository; + } + + /** + * 获得个人信息 + */ + async mine() { + const info = await this.repository.findOne({ + where: { + id: this.ctx.user.id, + }, + }); + delete info.password; + return info; + } + + /** + * 新增 + * @param param + */ + async add(param) { + const exists = await this.repository.findOne({ + where: { + username: param.username, + }, + }); + if (!_.isEmpty(exists)) { + throw new CommonException('用户名已经存在'); + } + const password = param.password ?? '123456'; + param.password = md5(password); // 默认密码 建议未改密码不能登陆 + await super.add(param); + //添加角色 + if (param.roles && param.roles.length > 0) { + await this.roleService.addRoles(param.id, param.roles); + } + return param.id; + } + + /** + * 修改 + * @param param 数据 + */ + async update(param) { + if (param.id == null) { + throw new CommonException('id不能为空'); + } + const userInfo = await this.repository.findOne({ + where: { id: param.id }, + }); + if (!userInfo) { + throw new CommonException('用户不存在'); + } + + delete param.username; + if (!_.isEmpty(param.password)) { + param.password = md5(param.password); + } else { + delete param.password; + } + await super.update(param); + await this.roleService.updateRoles(param.id, param.roles); + } + + async findOne(param) { + return this.repository.findOne({ + where: param, + }); + } + + checkPassword(rawPassword: any, md5Password: any) { + logger.info('md5', md5('123456')); + return md5(rawPassword) === md5Password; + } + + /** + * 获取用户的菜单资源列表 + * @param id + */ + async getUserPermissions(id: any) { + const roleIds = await this.roleService.getRoleIdsByUserId(id); + + return await this.roleService.getPermissionByRoleIds(roleIds); + } +} diff --git a/packages/ui/certd-server/src/modules/basic/controller/basic-controller.ts b/packages/ui/certd-server/src/modules/basic/controller/basic-controller.ts new file mode 100644 index 000000000..e0d505998 --- /dev/null +++ b/packages/ui/certd-server/src/modules/basic/controller/basic-controller.ts @@ -0,0 +1,55 @@ +import { Rule, RuleType } from '@midwayjs/validate'; +import { ALL, Inject } from '@midwayjs/decorator'; +import { Body } from '@midwayjs/decorator'; +import { Controller, Post, Provide } from '@midwayjs/decorator'; +import { BaseController } from '../../../basic/base-controller'; +import { CodeService } from '../service/code-service'; +export class SmsCodeReq { + @Rule(RuleType.number().required()) + phoneCode: number; + + @Rule(RuleType.string().required()) + mobile: string; + + @Rule(RuleType.string().required().max(10)) + randomStr: string; + + @Rule(RuleType.number().required().max(4)) + imgCode: string; +} + +// const enumsMap = {}; +// glob('src/modules/**/enums/*.ts', {}, (err, matches) => { +// console.log('matched', matches); +// for (const filePath of matches) { +// const module = require('/' + filePath); +// console.log('modules', module); +// } +// }); + +/** + */ +@Provide() +@Controller('/api/basic') +export class BasicController extends BaseController { + @Inject() + codeService: CodeService; + @Post('/sendSmsCode') + public sendSmsCode( + @Body(ALL) + body: SmsCodeReq + ) { + // 设置缓存内容 + return this.ok(null); + } + + @Post('/captcha') + public async getCaptcha( + @Body() + randomStr + ) { + console.assert(randomStr < 10, 'randomStr 过长'); + const captcha = await this.codeService.generateCaptcha(randomStr); + return this.ok(captcha.data); + } +} diff --git a/packages/ui/certd-server/src/modules/basic/service/code-service.ts b/packages/ui/certd-server/src/modules/basic/service/code-service.ts new file mode 100644 index 000000000..cb306af67 --- /dev/null +++ b/packages/ui/certd-server/src/modules/basic/service/code-service.ts @@ -0,0 +1,57 @@ +import { Inject, Provide } from '@midwayjs/decorator'; +import { CacheManager } from '@midwayjs/cache'; +const svgCaptcha = require('svg-captcha'); + +// {data: '', text: 'abcd'} +/** + */ +@Provide() +export class CodeService { + @Inject() + cache: CacheManager; // 依赖注入CacheManager + + /** + */ + async generateCaptcha(randomStr) { + console.assert(randomStr < 10, 'randomStr 过长'); + const c = svgCaptcha.create(); + //{data: '', text: 'abcd'} + const imgCode = c.text; // = RandomUtil.randomStr(4, true); + await this.cache.set('imgCode:' + randomStr, imgCode, { + ttl: 2 * 60 * 1000, //过期时间 2分钟 + }); + return c; + } + + async getCaptchaText(randomStr) { + return await this.cache.get('imgCode:' + randomStr); + } + + async removeCaptcha(randomStr) { + await this.cache.del('imgCode:' + randomStr); + } + + async checkCaptcha(randomStr, userCaptcha) { + const code = await this.getCaptchaText(randomStr); + if (code == null) { + throw new Error('验证码已过期'); + } + if (code !== userCaptcha) { + throw new Error('验证码不正确'); + } + return true; + } + /** + */ + async sendSms(phoneCode, mobile, smsCode) { + console.assert(phoneCode != null && mobile != null, '手机号不能为空'); + console.assert(smsCode != null, '验证码不能为空'); + } + + /** + * loginBySmsCode + */ + async loginBySmsCode(user, smsCode) { + console.assert(user.mobile != null, '手机号不能为空'); + } +} diff --git a/packages/ui/certd-server/src/modules/login/controller/login-controller.ts b/packages/ui/certd-server/src/modules/login/controller/login-controller.ts new file mode 100644 index 000000000..efe0212bc --- /dev/null +++ b/packages/ui/certd-server/src/modules/login/controller/login-controller.ts @@ -0,0 +1,30 @@ +import { + Body, + Controller, + Inject, + Post, + Provide, + ALL, +} from '@midwayjs/decorator'; +import { LoginService } from '../service/login-service'; +import { BaseController } from '../../../basic/base-controller'; + +/** + */ +@Provide() +@Controller('/api/') +export class LoginController extends BaseController { + @Inject() + loginService: LoginService; + @Post('/login') + public async login( + @Body(ALL) + user + ) { + const token = await this.loginService.login(user); + return this.ok(token); + } + + @Post('/logout') + public logout() {} +} diff --git a/packages/ui/certd-server/src/modules/login/service/login-service.ts b/packages/ui/certd-server/src/modules/login/service/login-service.ts new file mode 100644 index 000000000..85ceaee5d --- /dev/null +++ b/packages/ui/certd-server/src/modules/login/service/login-service.ts @@ -0,0 +1,52 @@ +import { Config, Inject, Provide } from '@midwayjs/decorator'; +import { UserService } from '../../authority/service/user-service'; +import * as jwt from 'jsonwebtoken'; +import { CommonException } from '../../../basic/exception/common-exception'; + +/** + * 系统用户 + */ +@Provide() +export class LoginService { + @Inject() + userService: UserService; + @Config('biz.jwt') + private jwt: any; + + /** + * login + */ + async login(user) { + console.assert(user.username != null, '用户名不能为空'); + const info = await this.userService.findOne({ username: user.username }); + if (info == null) { + throw new CommonException('用户名或密码错误'); + } + const right = this.userService.checkPassword(user.password, info.password); + if (!right) { + throw new CommonException('用户名或密码错误'); + } + + return this.generateToken(info); + } + + /** + * 生成token + * @param user 用户对象 + */ + async generateToken(user) { + const tokenInfo = { + username: user.username, + id: user.id, + }; + const expire = this.jwt.expire; + const token = jwt.sign(tokenInfo, this.jwt.secret, { + expiresIn: expire, + }); + + return { + token, + expire, + }; + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/auto/auto-register-cron.ts b/packages/ui/certd-server/src/modules/pipeline/auto/auto-register-cron.ts new file mode 100644 index 000000000..263d67090 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/auto/auto-register-cron.ts @@ -0,0 +1,26 @@ +import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/decorator"; +import { PipelineService } from '../service/pipeline-service'; +import { logger } from '../../../utils/logger'; + +@Autoload() +@Scope(ScopeEnum.Singleton) +export class AutoRegisterCron { + @Inject() + pipelineService: PipelineService; + + // @Inject() + // echoPlugin: EchoPlugin; + + @Init() + async init() { + logger.info('加载定时trigger开始'); + await this.pipelineService.onStartup(); + // logger.info(this.echoPlugin, this.echoPlugin.test); + // logger.info('加载定时trigger完成'); + // + // const meta = getClassMetadata(CLASS_KEY, this.echoPlugin); + // console.log('meta', meta); + // const metas = listPropertyDataFromClass(CLASS_KEY, this.echoPlugin); + // console.log('metas', metas); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/controller/access-controller.ts b/packages/ui/certd-server/src/modules/pipeline/controller/access-controller.ts new file mode 100644 index 000000000..529cdd7d6 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/controller/access-controller.ts @@ -0,0 +1,80 @@ +import { + ALL, + Body, + Controller, + Inject, + Post, + Provide, + Query, +} from '@midwayjs/decorator'; +import { CrudController } from '../../../basic/crud-controller'; +import { AccessService } from '../service/access-service'; + +/** + * 授权 + */ +@Provide() +@Controller('/api/pi/access') +export class AccessController extends CrudController { + @Inject() + service: AccessService; + + getService() { + return this.service; + } + + @Post('/page') + async page(@Body(ALL) body) { + body.query = body.query ?? {}; + body.query.userId = this.ctx.user.id; + return super.page(body); + } + + @Post('/list') + async list(@Body(ALL) body) { + body.userId = this.ctx.user.id; + return super.list(body); + } + + @Post('/add') + async add(@Body(ALL) bean) { + bean.userId = this.ctx.user.id; + return super.add(bean); + } + + @Post('/update') + async update(@Body(ALL) bean) { + await this.service.checkUserId(bean.id, this.ctx.user.id); + return super.update(bean); + } + @Post('/info') + async info(@Query('id') id) { + await this.service.checkUserId(id, this.ctx.user.id); + return super.info(id); + } + + @Post('/delete') + async delete(@Query('id') id) { + await this.service.checkUserId(id, this.ctx.user.id); + return super.delete(id); + } + + @Post('/define') + async define(@Query('type') type) { + const provider = this.service.getDefineByType(type); + return this.ok(provider); + } + + @Post('/accessTypeDict') + async getAccessTypeDict() { + const list = this.service.getDefineList(); + const dict = []; + for (const item of list) { + dict.push({ + value: item.name, + label: item.title, + }); + } + return this.ok(dict); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/controller/dns-provider-controller.ts b/packages/ui/certd-server/src/modules/pipeline/controller/dns-provider-controller.ts new file mode 100644 index 000000000..99f11467b --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/controller/dns-provider-controller.ts @@ -0,0 +1,40 @@ +import { + ALL, + Controller, + Inject, + Post, + Provide, + Query, +} from '@midwayjs/decorator'; +import { DnsProviderService } from '../service/dns-provider-service'; +import { BaseController } from '../../../basic/base-controller'; + +/** + * 插件 + */ +@Provide() +@Controller('/api/pi/dnsProvider') +export class DnsProviderController extends BaseController { + @Inject() + service: DnsProviderService; + + @Post('/list') + async list(@Query(ALL) query) { + query.userId = this.ctx.user.id; + const list = this.service.getList(); + return this.ok(list); + } + + @Post('/dnsProviderTypeDict') + async getDnsProviderTypeDict() { + const list = this.service.getList(); + const dict = []; + for (const item of list) { + dict.push({ + value: item.name, + label: item.title, + }); + } + return this.ok(dict); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/controller/history-controller.ts b/packages/ui/certd-server/src/modules/pipeline/controller/history-controller.ts new file mode 100644 index 000000000..a8410979d --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/controller/history-controller.ts @@ -0,0 +1,106 @@ +import { + ALL, + Body, + Controller, + Inject, + Post, + Provide, + Query, +} from '@midwayjs/decorator'; +import { CrudController } from '../../../basic/crud-controller'; +import { PipelineEntity } from '../entity/pipeline'; +import { HistoryService } from '../service/history-service'; +import { HistoryLogService } from '../service/history-log-service'; +import { HistoryEntity } from '../entity/history'; +import { HistoryLogEntity } from '../entity/history-log'; + +/** + * 证书 + */ +@Provide() +@Controller('/api/pi/history') +export class HistoryController extends CrudController { + @Inject() + service: HistoryService; + @Inject() + logService: HistoryLogService; + + getService() { + return this.service; + } + + @Post('/page') + async page(@Body(ALL) body) { + body.query.userId = this.ctx.user.id; + return super.page(body); + } + + @Post('/list') + async list(@Body(ALL) body) { + body.userId = this.ctx.user.id; + if (body.pipelineId == null) { + return this.ok([]); + } + const buildQuery = qb => { + qb.limit(10); + }; + const listRet = await this.getService().list( + body, + { prop: 'id', asc: false }, + buildQuery + ); + return this.ok(listRet); + } + + @Post('/add') + async add(@Body(ALL) bean: PipelineEntity) { + bean.userId = this.ctx.user.id; + return super.add(bean); + } + + @Post('/update') + async update(@Body(ALL) bean) { + await this.service.checkUserId(bean.id, this.ctx.user.id); + return super.update(bean); + } + + @Post('/save') + async save(@Body(ALL) bean: HistoryEntity) { + bean.userId = this.ctx.user.id; + if (bean.id > 0) { + await this.service.checkUserId(bean.id, this.ctx.user.id); + } + await this.service.save(bean); + return this.ok(bean.id); + } + + @Post('/saveLog') + async saveLog(@Body(ALL) bean: HistoryLogEntity) { + bean.userId = this.ctx.user.id; + if (bean.id > 0) { + await this.service.checkUserId(bean.id, this.ctx.user.id); + } + await this.logService.save(bean); + return this.ok(bean.id); + } + + @Post('/delete') + async delete(@Query('id') id) { + await this.service.checkUserId(id, this.ctx.user.id); + return super.delete(id); + } + + @Post('/detail') + async detail(@Query('id') id) { + await this.service.checkUserId(id, this.ctx.user.id); + const detail = await this.service.detail(id); + return this.ok(detail); + } + + @Post('/logs') + async logs(@Query('id') id) { + await this.logService.checkUserId(id, this.ctx.user.id); + const logInfo = await this.logService.info(id); + return this.ok(logInfo); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/controller/pipeline-controller.ts b/packages/ui/certd-server/src/modules/pipeline/controller/pipeline-controller.ts new file mode 100644 index 000000000..c19685439 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/controller/pipeline-controller.ts @@ -0,0 +1,77 @@ +import { + ALL, + Body, + Controller, + Inject, + Post, + Provide, + Query, +} from '@midwayjs/decorator'; +import { CrudController } from '../../../basic/crud-controller'; +import { PipelineService } from '../service/pipeline-service'; +import { PipelineEntity } from '../entity/pipeline'; + +/** + * 证书 + */ +@Provide() +@Controller('/api/pi/pipeline') +export class PipelineController extends CrudController { + @Inject() + service: PipelineService; + + getService() { + return this.service; + } + + @Post('/page') + async page(@Body(ALL) body) { + body.query.userId = this.ctx.user.id; + const buildQuery = qb => { + qb.where({}); + }; + return super.page({ ...body, buildQuery }); + } + + @Post('/add') + async add(@Body(ALL) bean: PipelineEntity) { + bean.userId = this.ctx.user.id; + return super.add(bean); + } + + @Post('/update') + async update(@Body(ALL) bean) { + await this.service.checkUserId(bean.id, this.ctx.user.id); + return super.update(bean); + } + + @Post('/save') + async save(@Body(ALL) bean: PipelineEntity) { + bean.userId = this.ctx.user.id; + if (bean.id > 0) { + await this.service.checkUserId(bean.id, this.ctx.user.id); + } + await this.service.save(bean); + return this.ok(bean.id); + } + + @Post('/delete') + async delete(@Query('id') id) { + await this.service.checkUserId(id, this.ctx.user.id); + return super.delete(id); + } + + @Post('/detail') + async detail(@Query('id') id) { + await this.service.checkUserId(id, this.ctx.user.id); + const detail = await this.service.detail(id); + return this.ok(detail); + } + + @Post('/trigger') + async trigger(@Query('id') id) { + await this.service.checkUserId(id, this.ctx.user.id); + await this.service.trigger(id); + return this.ok({}); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/controller/plugin-controller.ts b/packages/ui/certd-server/src/modules/pipeline/controller/plugin-controller.ts new file mode 100644 index 000000000..4501d2c85 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/controller/plugin-controller.ts @@ -0,0 +1,27 @@ +import { + ALL, + Controller, + Inject, + Post, + Provide, + Query, +} from '@midwayjs/decorator'; +import { BaseController } from '../../../basic/base-controller'; +import { PluginService } from '../service/plugin-service'; + +/** + * 插件 + */ +@Provide() +@Controller('/api/pi/plugin') +export class PluginController extends BaseController { + @Inject() + service: PluginService; + + @Post('/list') + async list(@Query(ALL) query) { + query.userId = this.ctx.user.id; + const list = this.service.getList(); + return this.ok(list); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/entity/access.ts b/packages/ui/certd-server/src/modules/pipeline/entity/access.ts new file mode 100644 index 000000000..8855c1ebf --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/entity/access.ts @@ -0,0 +1,33 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +/** + * 授权配置 + */ +@Entity('cd_access') +export class AccessEntity { + @PrimaryGeneratedColumn() + id: number; + @Column({ name: 'user_id', comment: '用户id' }) + userId: number; + @Column({ comment: '名称', length: 100 }) + name: string; + + @Column({ comment: '类型', length: 100 }) + type: string; + + @Column({ name: 'setting', comment: '设置', length: 1024, nullable: true }) + setting: string; + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; +} diff --git a/packages/ui/certd-server/src/modules/pipeline/entity/history-log.ts b/packages/ui/certd-server/src/modules/pipeline/entity/history-log.ts new file mode 100644 index 000000000..bdeeeeaf9 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/entity/history-log.ts @@ -0,0 +1,41 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('pi_history_log') +export class HistoryLogEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'user_id', comment: '用户id' }) + userId: number; + + @Column({ name: 'pipeline_id', comment: '流水线' }) + pipelineId: number; + + @Column({ name: 'history_id', comment: '历史id' }) + historyId: number; + + @Column({ + name: 'node_id', + comment: '任务节点id', + length: 100, + nullable: true, + }) + nodeId: string; + + @Column({ comment: '日志内容', length: 40960, nullable: true }) + logs: string; + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; +} diff --git a/packages/ui/certd-server/src/modules/pipeline/entity/history.ts b/packages/ui/certd-server/src/modules/pipeline/entity/history.ts new file mode 100644 index 000000000..ab1425f54 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/entity/history.ts @@ -0,0 +1,38 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('pi_history') +export class HistoryEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'user_id', comment: '用户id' }) + userId: number; + + @Column({ name: 'pipeline_id', comment: '流水线' }) + pipelineId: number; + @Column({ comment: '运行状态', length: 40960, nullable: true }) + pipeline: string; + + @Column({ comment: '结果状态', length: 20, nullable: true }) + status: string; + + @Column({ + name: 'end_time', + comment: '结束时间', + nullable: true, + }) + endTime: Date; + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; +} diff --git a/packages/ui/certd-server/src/modules/pipeline/entity/pipeline.ts b/packages/ui/certd-server/src/modules/pipeline/entity/pipeline.ts new file mode 100644 index 000000000..c9f74d5b7 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/entity/pipeline.ts @@ -0,0 +1,52 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('pi_pipeline') +export class PipelineEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'user_id', comment: '用户id' }) + userId: number; + + @Column({ name: 'title', comment: '标题' }) + title: number; + + @Column({ comment: '配置', length: 40960 }) + content: string; + + @Column({ + name: 'keep_history_count', + comment: '历史记录保持数量', + nullable: true, + }) + keepHistoryCount: number; + + @Column({ comment: '备注', length: 100, nullable: true }) + remark: string; + + @Column({ comment: '状态', length: 100, nullable: true }) + status: string; + + @Column({ comment: '启用/禁用', nullable: true, default: false }) + disabled: boolean; + + @Column({ + name: 'last_history_time', + comment: '最后一次执行时间', + nullable: true, + }) + lastHistoryTime: number; + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; +} diff --git a/packages/ui/certd-server/src/modules/pipeline/entity/storage.ts b/packages/ui/certd-server/src/modules/pipeline/entity/storage.ts new file mode 100644 index 000000000..96138c478 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/entity/storage.ts @@ -0,0 +1,35 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('pi_storage') +export class StorageEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'user_id', comment: '用户id' }) + userId: number; + + @Column({ name: 'scope', comment: '范围' }) + scope: string; + + @Column({ name: 'namespace', comment: '命名空间' }) + namespace: string; + + @Column({ comment: 'key', length: 100, nullable: true }) + key: string; + + @Column({ comment: 'value', length: 40960, nullable: true }) + value: string; + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; +} diff --git a/packages/ui/certd-server/src/modules/pipeline/entity/vo/history-detail.ts b/packages/ui/certd-server/src/modules/pipeline/entity/vo/history-detail.ts new file mode 100644 index 000000000..39d6efdbe --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/entity/vo/history-detail.ts @@ -0,0 +1,12 @@ +import { HistoryEntity } from '../history'; +import { HistoryLogEntity } from '../history-log'; + +export class HistoryDetail { + history: HistoryEntity; + log: HistoryLogEntity; + + constructor(history: HistoryEntity, log: HistoryLogEntity) { + this.history = history; + this.log = log; + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/entity/vo/pipeline-detail.ts b/packages/ui/certd-server/src/modules/pipeline/entity/vo/pipeline-detail.ts new file mode 100644 index 000000000..e7a5f4b32 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/entity/vo/pipeline-detail.ts @@ -0,0 +1,13 @@ +import { PipelineEntity } from '../pipeline'; +import { HistoryEntity } from '../history'; +import { HistoryLogEntity } from '../history-log'; + +export class PipelineDetail { + pipeline: PipelineEntity; + constructor(pipeline: PipelineEntity) { + this.pipeline = pipeline; + } + + last: HistoryEntity; + logs: HistoryLogEntity[]; +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/access-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/access-service.ts new file mode 100644 index 000000000..b911fc295 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/access-service.ts @@ -0,0 +1,44 @@ +import { Provide, Scope, ScopeEnum } from "@midwayjs/decorator"; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseService } from '../../../basic/base-service'; +import { AccessEntity } from '../entity/access'; +import { + accessRegistry, + IAccessService, +} from '@certd/pipeline'; + +/** + * 授权 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class AccessService + extends BaseService + implements IAccessService +{ + @InjectEntityModel(AccessEntity) + repository: Repository; + + getRepository() { + return this.repository; + } + + async getById(id: any): Promise { + const entity = await this.info(id); + // const access = accessRegistry.get(entity.type); + const setting = JSON.parse(entity.setting); + return { + id: entity.id, + ...setting, + }; + } + + getDefineList() { + return accessRegistry.getDefineList(); + } + + getDefineByType(type) { + return accessRegistry.getDefine(type); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/db-storage.ts b/packages/ui/certd-server/src/modules/pipeline/service/db-storage.ts new file mode 100644 index 000000000..c95357315 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/db-storage.ts @@ -0,0 +1,47 @@ +import { IStorage } from '@certd/pipeline/src/core/storage'; +import { StorageService } from './storage-service'; + +export class DbStorage implements IStorage { + /** + * 范围: user / pipeline / runtime / task + */ + storageService: StorageService; + userId: number; + constructor(userId: number, storageService: StorageService) { + this.userId = userId; + this.storageService = storageService; + } + + async get( + scope: string, + namespace: string, + key: string + ): Promise { + const storageEntity = await this.storageService.get({ + userId: this.userId, + scope: scope, + namespace: namespace, + key, + }); + + if (storageEntity != null) { + return storageEntity.value; + } + return null; + } + + async set( + scope: string, + namespace: string, + key: string, + value: string + ): Promise { + await this.storageService.set({ + userId: this.userId, + scope: scope, + namespace: namespace, + key, + value, + }); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/dns-provider-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/dns-provider-service.ts new file mode 100644 index 000000000..0473841b8 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/dns-provider-service.ts @@ -0,0 +1,9 @@ +import { Provide, Scope, ScopeEnum } from "@midwayjs/decorator"; +import { dnsProviderRegistry } from '@certd/plugin-cert'; +@Provide() +@Scope(ScopeEnum.Singleton) +export class DnsProviderService { + getList() { + return dnsProviderRegistry.getDefineList(); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/history-log-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/history-log-service.ts new file mode 100644 index 000000000..46aaa74b8 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/history-log-service.ts @@ -0,0 +1,27 @@ +import { Provide, Scope, ScopeEnum } from "@midwayjs/decorator"; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseService } from '../../../basic/base-service'; +import { HistoryLogEntity } from '../entity/history-log'; + +/** + * 证书申请 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class HistoryLogService extends BaseService { + @InjectEntityModel(HistoryLogEntity) + repository: Repository; + + getRepository() { + return this.repository; + } + + async save(bean: HistoryLogEntity) { + if (bean.id > 0) { + await this.update(bean); + } else { + await this.add(bean); + } + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/history-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/history-service.ts new file mode 100644 index 000000000..832b9ea86 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/history-service.ts @@ -0,0 +1,81 @@ +import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/decorator"; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseService } from '../../../basic/base-service'; +import { HistoryEntity } from '../entity/history'; +import { PipelineEntity } from '../entity/pipeline'; +import { HistoryDetail } from '../entity/vo/history-detail'; +import { HistoryLogService } from './history-log-service'; + +/** + * 证书申请 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class HistoryService extends BaseService { + @InjectEntityModel(HistoryEntity) + repository: Repository; + @Inject() + logService: HistoryLogService; + getRepository() { + return this.repository; + } + + async save(bean: HistoryEntity) { + if (bean.id > 0) { + await this.update(bean); + } else { + await this.add(bean); + } + } + + async detail(historyId: string) { + const entity = await this.info(historyId); + const log = await this.logService.info(historyId); + return new HistoryDetail(entity, log); + } + + async start(pipeline: PipelineEntity) { + const bean = { + userId: pipeline.userId, + pipelineId: pipeline.id, + title: pipeline.title, + status: 'start', + }; + const { id } = await this.add(bean); + //清除大于pipeline.keepHistoryCount的历史记录 + this.clear(pipeline.id, pipeline.keepHistoryCount); + return id; + } + + private async clear(pipelineId: number, keepCount = 30) { + const count = await this.repository.count({ + where: { + pipelineId, + }, + }); + if (count <= keepCount) { + return; + } + let shouldDeleteCount = count - keepCount; + const deleteCountBatch = 100; + while (shouldDeleteCount > 0) { + const list = await this.repository.find({ + select: { + id: true, + }, + where: { + pipelineId, + }, + order: { + id: 'ASC', + }, + skip: 0, + take: deleteCountBatch, + }); + await this.repository.remove(list); + + shouldDeleteCount -= deleteCountBatch; + } + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts new file mode 100644 index 000000000..b267f93e3 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts @@ -0,0 +1,241 @@ +import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/decorator'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { BaseService } from '../../../basic/base-service'; +import { PipelineEntity } from '../entity/pipeline'; +import { PipelineDetail } from '../entity/vo/pipeline-detail'; +import { Executor, Pipeline, RunHistory } from '@certd/pipeline'; +import { AccessService } from './access-service'; +import { DbStorage } from './db-storage'; +import { StorageService } from './storage-service'; +import { Cron } from '../../../plugins/cron/cron'; +import { HistoryService } from './history-service'; +import { HistoryEntity } from '../entity/history'; +import { HistoryLogEntity } from '../entity/history-log'; +import { HistoryLogService } from './history-log-service'; +import { logger } from '../../../utils/logger'; + +/** + * 证书申请 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class PipelineService extends BaseService { + @InjectEntityModel(PipelineEntity) + repository: Repository; + + @Inject() + accessService: AccessService; + @Inject() + storageService: StorageService; + @Inject() + historyService: HistoryService; + @Inject() + historyLogService: HistoryLogService; + + @Inject() + cron: Cron; + + getRepository() { + return this.repository; + } + + async update(entity) { + await super.update(entity); + + await this.registerTriggerById(entity.id); + } + + private async registerTriggerById(pipelineId) { + if (pipelineId == null) { + return; + } + const info = await this.info(pipelineId); + if (info && !info.disabled) { + const pipeline = JSON.parse(info.content); + this.registerTriggers(pipeline); + } + } + + /** + * 获取详情 + * @param id + */ + async detail(id) { + const pipeline = await this.info(id); + return new PipelineDetail(pipeline); + } + + async save(bean: PipelineEntity) { + const pipeline = JSON.parse(bean.content); + bean.title = pipeline.title; + await this.addOrUpdate(bean); + await this.registerTriggerById(bean.id); + } + + /** + * 应用启动后初始加载记录 + */ + async onStartup() { + const idEntityList = await this.repository.find({ + select: { + id: true, + }, + where: { + disabled: false, + }, + }); + const ids = idEntityList.map(item => { + return item.id; + }); + + //id 分段 + const idsSpan = []; + let arr = []; + for (let i = 0; i < ids.length; i++) { + if (i % 20 === 0) { + arr = []; + idsSpan.push(arr); + } + arr.push(ids[i]); + } + + //分段加载记录 + for (const idArr of idsSpan) { + const list = await this.repository.findBy({ + id: In(idArr), + }); + + for (const entity of list) { + const pipeline = JSON.parse(entity.content ?? '{}'); + this.registerTriggers(pipeline); + } + } + logger.info('定时器数量:', this.cron.getList()); + } + + registerTriggers(pipeline?: Pipeline) { + if (pipeline?.triggers == null) { + return; + } + for (const trigger of pipeline.triggers) { + this.registerCron(pipeline.id, trigger); + } + } + + async trigger(id) { + this.cron.register({ + name: `pipeline.${id}.trigger.once`, + cron: null, + job: async () => { + await this.run(id, null); + }, + }); + logger.info('定时器数量:', this.cron.getList()); + } + + registerCron(pipelineId, trigger) { + let cron = trigger.props?.cron; + if (cron == null) { + return; + } + if(cron.startsWith("*")){ + cron = "0"+ cron.substring(1,cron.length) + return + } + this.cron.register({ + name: this.buildCronKey(pipelineId, trigger.id), + cron: cron, + job: async () => { + logger.info('定时任务触发:', pipelineId, trigger.id); + await this.run(pipelineId, trigger.id); + }, + }); + } + + async run(id, triggerId) { + const entity: PipelineEntity = await this.info(id); + const pipeline = JSON.parse(entity.content); + + if (!pipeline.stages || pipeline.stages.length === 0) { + return; + } + + const triggerType = this.getTriggerType(triggerId, pipeline); + if (triggerType == null) { + return; + } + + const onChanged = async (history: RunHistory) => { + //保存执行历史 + await this.saveHistory(history); + }; + + const userId = entity.userId; + const historyId = await this.historyService.start(entity); + + const executor = new Executor({ + userId, + pipeline, + onChanged, + accessService: this.accessService, + storage: new DbStorage(userId, this.storageService), + }); + + await executor.run(historyId, triggerType); + } + + private getTriggerType(triggerId, pipeline) { + let triggerType = 'user'; + if (triggerId != null) { + //如果不是手动触发 + //查找trigger + const found = this.findTrigger(pipeline, triggerId); + if (!found) { + //如果没有找到triggerId,说明被用户删掉了,这里再删除一次 + this.cron.remove(this.buildCronKey(pipeline.id, triggerId)); + triggerType = null; + } else { + logger.info('timer trigger:' + found.id, found.title, found.cron); + triggerType = 'timer'; + } + } + return triggerType; + } + + private buildCronKey(pipelineId, triggerId) { + return `pipeline.${pipelineId}.trigger.${triggerId}`; + } + + private findTrigger(pipeline, triggerId) { + for (const trigger of pipeline.triggers) { + if (trigger.id === triggerId) { + return trigger; + } + } + return; + } + + private async saveHistory(history: RunHistory) { + //修改pipeline状态 + const pipelineEntity = new PipelineEntity(); + pipelineEntity.id = parseInt(history.pipeline.id); + pipelineEntity.status = history.pipeline.status.status; + pipelineEntity.lastHistoryTime = history.pipeline.status.startTime; + await this.update(pipelineEntity); + + const entity: HistoryEntity = new HistoryEntity(); + entity.id = parseInt(history.id); + entity.userId = history.pipeline.userId; + entity.pipeline = JSON.stringify(history.pipeline); + await this.historyService.save(entity); + + const logEntity: HistoryLogEntity = new HistoryLogEntity(); + logEntity.id = entity.id; + logEntity.userId = entity.userId; + logEntity.pipelineId = entity.pipelineId; + logEntity.historyId = entity.id; + logEntity.logs = JSON.stringify(history.logs); + await this.historyLogService.addOrUpdate(logEntity); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/plugin-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/plugin-service.ts new file mode 100644 index 000000000..c02715450 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/plugin-service.ts @@ -0,0 +1,15 @@ +import { Provide, Scope, ScopeEnum } from "@midwayjs/decorator"; +import { pluginRegistry } from '@certd/pipeline'; +@Provide() +@Scope(ScopeEnum.Singleton) +export class PluginService { + getList() { + const collection = pluginRegistry.storage; + const list = []; + for (const key in collection) { + const Plugin = collection[key]; + list.push({ ...Plugin.define, key }); + } + return list; + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/storage-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/storage-service.ts new file mode 100644 index 000000000..bcf862964 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/storage-service.ts @@ -0,0 +1,56 @@ +import { Provide, Scope, ScopeEnum } from "@midwayjs/decorator"; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseService } from '../../../basic/base-service'; +import { StorageEntity } from '../entity/storage'; + +/** + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class StorageService extends BaseService { + @InjectEntityModel(StorageEntity) + repository: Repository; + + getRepository() { + return this.repository; + } + + async get(where: { + scope: any; + namespace: any; + userId: number; + key: string; + }) { + if (where.userId == null) { + throw new Error('userId 不能为空'); + } + return await this.repository.findOne({ + where, + }); + } + + async set(entity: { + id?: any; + scope: any; + namespace: any; + userId: number; + value: string; + key: string; + }) { + entity.id = null; + const query = { ...entity }; + delete query.value; + const ret = await this.get(query); + if (ret != null) { + entity.id = ret.id; + if (ret.userId !== entity.userId) { + throw new Error('您没有权限修改此数据'); + } + await this.repository.save(entity); + } else { + await this.repository.insert(entity); + } + return; + } +} diff --git a/packages/ui/certd-server/src/plugins/cron/configuration.ts b/packages/ui/certd-server/src/plugins/cron/configuration.ts new file mode 100644 index 000000000..b9a6b89ea --- /dev/null +++ b/packages/ui/certd-server/src/plugins/cron/configuration.ts @@ -0,0 +1,27 @@ +import { Config, Configuration, Logger } from '@midwayjs/decorator'; +import { ILogger } from '@midwayjs/logger'; +import { IMidwayContainer } from '@midwayjs/core'; +import { Cron } from './cron'; + +// ... (see below) ... +@Configuration({ + namespace: 'cron', + //importConfigs: [join(__dirname, './config')], +}) +export class CronConfiguration { + @Config() + config; + @Logger() + logger: ILogger; + + cron: Cron; + async onReady(container: IMidwayContainer) { + this.logger.info('cron start'); + this.cron = new Cron({ + logger: this.logger, + ...this.config, + }); + container.registerObject('cron', this.cron); + this.logger.info('cron started'); + } +} diff --git a/packages/ui/certd-server/src/plugins/cron/cron.ts b/packages/ui/certd-server/src/plugins/cron/cron.ts new file mode 100644 index 000000000..e0380707e --- /dev/null +++ b/packages/ui/certd-server/src/plugins/cron/cron.ts @@ -0,0 +1,38 @@ +import cron from 'node-cron'; +export type CronTask = { + /** + * 为空则为单次执行 + */ + cron: string; + job: () => Promise; + name: string; +}; +export class Cron { + logger; + constructor(opts) { + this.logger = opts.logger; + } + + register(task: CronTask) { + if (!task.cron) { + this.logger.info(`[cron] register once : [${task.name}]`); + task.job(); + return; + } + this.logger.info(`[cron] register cron : [${task.name}] ,${task.cron}`); + cron.schedule(task.cron, task.job, { + name: task.name, + }); + } + + remove(taskName: string) { + this.logger.info(`[cron] remove : [${taskName}]`); + const tasks = cron.getTasks() as Map; + tasks.delete(taskName); + } + + getList() { + const tasks = cron.getTasks(); + return tasks.size; + } +} diff --git a/packages/ui/certd-server/src/plugins/cron/index.ts b/packages/ui/certd-server/src/plugins/cron/index.ts new file mode 100644 index 000000000..69c716a09 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/cron/index.ts @@ -0,0 +1,5 @@ +// src/index.ts +export { CronConfiguration as Configuration } from './configuration'; +// export * from './controller/user'; +// export * from './controller/api'; +// export * from './service/user'; diff --git a/packages/ui/certd-server/src/plugins/pipeline/index.ts b/packages/ui/certd-server/src/plugins/pipeline/index.ts new file mode 100644 index 000000000..388562a88 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/pipeline/index.ts @@ -0,0 +1,6 @@ +// src/index.ts +import '@certd/plugin-all'; +export { PipelineConfiguration as Configuration } from '@certd/pipeline'; +// export * from './controller/user'; +// export * from './controller/api'; +// export * from './service/user'; diff --git a/packages/ui/certd-server/src/plugins/pipeline/test/echo-plugin.ts b/packages/ui/certd-server/src/plugins/pipeline/test/echo-plugin.ts new file mode 100644 index 000000000..7adee962d --- /dev/null +++ b/packages/ui/certd-server/src/plugins/pipeline/test/echo-plugin.ts @@ -0,0 +1,27 @@ +import { ILogger } from "@midwayjs/logger"; +import { ITaskPlugin,Autowire, IsTaskPlugin, TaskInput } from "@certd/pipeline"; + +@IsTaskPlugin({ + name: "EchoPlugin", + title: "测试插件", + desc: "test", +}) +export class EchoPlugin implements ITaskPlugin { + @TaskInput({ + title: "测试属性", + component: { + name: "text", + }, + }) + test?: string; + + @Autowire() + // @ts-ignore + logger: ILogger; + + async onInit(){} + + async execute(): Promise { + return Promise.resolve(undefined); + } +} diff --git a/packages/ui/certd-server/src/utils/logger.ts b/packages/ui/certd-server/src/utils/logger.ts new file mode 100644 index 000000000..d4fea9ea7 --- /dev/null +++ b/packages/ui/certd-server/src/utils/logger.ts @@ -0,0 +1,12 @@ +const log4js = require('log4js'); +const level = process.env.NODE_ENV === 'development' ? 'debug' : 'info'; +const path = require('path'); +const filename = path.join('/logs/server.log'); +log4js.configure({ + appenders: { + std: { type: 'stdout', level: 'debug' }, + file: { type: 'file', pattern: 'yyyy-MM-dd', daysToKeep: 3, filename }, + }, + categories: { default: { appenders: ['std'], level } }, +}); +export const logger = log4js.getLogger('fast'); diff --git a/packages/ui/certd-server/src/utils/random.ts b/packages/ui/certd-server/src/utils/random.ts new file mode 100644 index 000000000..6be6f6e2d --- /dev/null +++ b/packages/ui/certd-server/src/utils/random.ts @@ -0,0 +1,43 @@ +const numbers = '0123456789'; +const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; +const specials = '~!@#$%^*()_+-=[]{}|;:,./<>?'; + +/** + * Generate random string + * @param {Number} length + * @param {Object} options + */ +function randomStr(length, options) { + length || (length = 8); + options || (options = {}); + + let chars = ''; + let result = ''; + + if (options === true) { + chars = numbers + letters; + } else if (typeof options === 'string') { + chars = options; + } else { + if (options.numbers !== false) { + chars += typeof options.numbers === 'string' ? options.numbers : numbers; + } + + if (options.letters !== false) { + chars += typeof options.letters === 'string' ? options.letters : letters; + } + + if (options.specials) { + chars += + typeof options.specials === 'string' ? options.specials : specials; + } + } + + while (length > 0) { + length--; + result += chars[Math.floor(Math.random() * chars.length)]; + } + return result; +} + +export const RandomUtil = { randomStr }; diff --git a/packages/ui/certd-server/test/controller/api.test.ts b/packages/ui/certd-server/test/controller/api.test.ts new file mode 100644 index 000000000..28dcfd035 --- /dev/null +++ b/packages/ui/certd-server/test/controller/api.test.ts @@ -0,0 +1,25 @@ +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework } from '@midwayjs/koa'; +import * as assert from 'assert'; + +describe('test/controller/home.test.ts', () => { + + it('should POST /api/get_user', async () => { + // create app + const app = await createApp(); + + // make request + const result = await createHttpRequest(app).post('/api/get_user').query({ uid: 123 }); + + // use expect by jest + expect(result.status).toBe(200); + expect(result.body.message).toBe('OK'); + + // or use assert + assert.deepStrictEqual(result.status, 200); + assert.deepStrictEqual(result.body.data.uid, '123'); + + // close app + await close(app); + }); +}); diff --git a/packages/ui/certd-server/test/controller/home.test.ts b/packages/ui/certd-server/test/controller/home.test.ts new file mode 100644 index 000000000..2e9578892 --- /dev/null +++ b/packages/ui/certd-server/test/controller/home.test.ts @@ -0,0 +1,26 @@ +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework } from '@midwayjs/koa'; +import * as assert from 'assert'; + +describe('test/controller/home.test.ts', () => { + + it('should GET /', async () => { + // create app + const app = await createApp(); + + // make request + const result = await createHttpRequest(app).get('/'); + + // use expect by jest + expect(result.status).toBe(200); + expect(result.text).toBe('Hello Midwayjs!'); + + // or use assert + assert.deepStrictEqual(result.status, 200); + assert.deepStrictEqual(result.text, 'Hello Midwayjs!'); + + // close app + await close(app); + }); + +}); diff --git a/packages/ui/certd-server/tsconfig.json b/packages/ui/certd-server/tsconfig.json new file mode 100644 index 000000000..d4f44496e --- /dev/null +++ b/packages/ui/certd-server/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":true, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "esModuleInterop": true, + "pretty": true, + "declaration": true, + "typeRoots": [ "./typings", "./node_modules/@types"], + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +}