Browse Source

Merge branch 'anncwb:main' into main

pull/1240/head
1sm 5 years ago
committed by GitHub
parent
commit
a9cad15435
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 28
      CHANGELOG.en_US.md
  2. 54
      CHANGELOG.md
  3. 35
      CHANGELOG.zh_CN.md
  4. 4
      build/utils.ts
  5. 47
      build/vite/plugin/styleImport.ts
  6. 9
      mock/demo/account.ts
  7. 92
      package.json
  8. 1
      prettier.config.js
  9. 3
      src/api/demo/account.ts
  10. 2
      src/api/sys/user.ts
  11. 4
      src/components/CardList/index.ts
  12. 178
      src/components/CardList/src/CardList.vue
  13. 25
      src/components/CardList/src/data.ts
  14. 2
      src/components/CodeEditor/index.ts
  15. 18
      src/components/CodeEditor/src/CodeEditor.vue
  16. 10
      src/components/CodeEditor/src/codemirror/CodeMirror.vue
  17. 5
      src/components/CodeEditor/src/typing.ts
  18. 121
      src/components/Dropdown/src/Dropdown.vue
  19. 37
      src/components/Excel/src/ImportExcel.vue
  20. 9
      src/components/Form/src/components/ApiSelect.vue
  21. 2
      src/components/Form/src/components/FormItem.vue
  22. 158
      src/components/Icon/src/IconPicker.vue
  23. 2
      src/components/Markdown/index.ts
  24. 2
      src/components/Markdown/src/Markdown.vue
  25. 22
      src/components/Markdown/src/MarkdownViewer.vue
  26. 5
      src/components/Modal/src/BasicModal.vue
  27. 1
      src/components/Modal/src/hooks/useModal.ts
  28. 3
      src/components/Page/src/PageWrapper.vue
  29. 16
      src/components/Table/src/BasicTable.vue
  30. 6
      src/components/Table/src/components/TableAction.vue
  31. 10
      src/components/Table/src/components/editable/EditableCell.vue
  32. 13
      src/components/Table/src/components/editable/index.ts
  33. 2
      src/components/Table/src/components/settings/ColumnSetting.vue
  34. 8
      src/components/Table/src/hooks/useCustomRow.ts
  35. 32
      src/components/Table/src/hooks/useDataSource.ts
  36. 8
      src/components/Table/src/hooks/useTable.ts
  37. 9
      src/components/Table/src/hooks/useTableStyle.ts
  38. 6
      src/components/Table/src/types/table.ts
  39. 95
      src/components/Tree/src/Tree.vue
  40. 25
      src/components/Tree/src/TreeHeader.vue
  41. 27
      src/components/Tree/src/props.ts
  42. 8
      src/components/Upload/src/useUpload.ts
  43. 82
      src/hooks/web/useContentHeight.ts
  44. 6
      src/layouts/default/header/components/Breadcrumb.vue
  45. 13
      src/layouts/default/tabs/useTabDropdown.ts
  46. 1
      src/locales/lang/en/routes/demo.ts
  47. 1
      src/locales/lang/zh-CN/routes/demo.ts
  48. 8
      src/router/guard/stateGuard.ts
  49. 22
      src/router/helper/routeHelper.ts
  50. 8
      src/router/routes/modules/demo/comp.ts
  51. 45
      src/store/modules/multipleTab.ts
  52. 57
      src/store/modules/user.ts
  53. 1
      src/utils/http/axios/axiosTransform.ts
  54. 12
      src/utils/http/axios/index.ts
  55. 3
      src/views/demo/charts/SaleRadar.vue
  56. 32
      src/views/demo/comp/card-list/index.vue
  57. 1
      src/views/demo/comp/upload/index.vue
  58. 21
      src/views/demo/editor/json/index.vue
  59. 28
      src/views/demo/editor/markdown/index.vue
  60. 2
      src/views/demo/excel/ImportExcel.vue
  61. 32
      src/views/demo/feat/session-timeout/index.vue
  62. 2
      src/views/demo/setup/index.vue
  63. 26
      src/views/demo/table/EditRowTable.vue
  64. 12
      src/views/demo/table/TreeTable.vue
  65. 10
      tests/server/package.json
  66. 2
      types/axios.d.ts
  67. 8293
      yarn.lock

28
CHANGELOG.en_US.md

@ -1,3 +1,31 @@
## 2.7.2(2021-09-14)
### ✨ Features
- **BasicForm** New `Divider` in the form component for dividing the area of longer forms
- **BasicTable**
- Cell editor adds submit callback, which will decide whether to submit data to the form based on the result returned by the callback function
- Add check method for row editing, allowing only check but not submit value, so asynchronously save data successfully before submit to table
- Fix the problem that the `rowClassName` property cannot be used at the same time as `striped`.
- New component **MarkdownViewer** for displaying rich text in Markdown format
### 🐛 Bug Fixes
- **CodeEditor** Fix JSON editor throwing exception when formatting invalid JSON text
- **Tinymce** fixes an issue where inline mode throws an exception in some scenarios
- **BasicTable**
- Repair the problem that the editing icon is not displayed when the content of editable cell is empty
- Repair the problem that the total row at the end of the table sometimes fails to align with the columns in the main part of the table.
- **MarkDown** Repair the problem that the value of initial value property does not work.
- **BasicUpload** Repair the problem that `accept` property does not support `MIME` and suffix name starting with dot.
- **ApiSelect** Fix the problem of type definition of `value` property.
- **Other**
- Repair the problem that some wrapper components give error when using slots.
- Repair the problem that `theme` parameter of `useECharts` does not work.
- Repair the problem that when `Token` is invalid, pressing F5 to refresh the page may cause abnormal page loading.
- Repair the problem that the improper call of `useRedo` may lead to `path` redirection abnormality.
- Repair the problem that `vite` custom mode name does not support underscore.
## 2.7.1(2021-08-16)
- Upgrade vue 3.2, if the operation fails, delete node_modules and reinstall it

54
CHANGELOG.md

@ -1,3 +1,57 @@
## [2.7.2](https://github.com/anncwb/vue-vben-admin/compare/v2.7.1...v2.7.2) (2021-09-13)
### Bug Fixes
- fixed token clear error ([9640484](https://github.com/anncwb/vue-vben-admin/commit/96404848955f84d57b88dd240ab3a57b7017103c))
- improve type introduction, fix [#1196](https://github.com/anncwb/vue-vben-admin/issues/1196) ([2820d5a](https://github.com/anncwb/vue-vben-admin/commit/2820d5a627260bb8eddfcd25df1cd7d1196932e8))
- **api-select:** fixed `value` prop define ([f87b0f2](https://github.com/anncwb/vue-vben-admin/commit/f87b0f2f5efe4e9977c4cc0742dbcaefbad2ca02)), closes [#1175](https://github.com/anncwb/vue-vben-admin/issues/1175)
- **card-list:** fixed build error ([628e820](https://github.com/anncwb/vue-vben-admin/commit/628e820684ce5d81f130548505efe83e8d516131))
- **code-editor:** fixed formatting error ([e7c9636](https://github.com/anncwb/vue-vben-admin/commit/e7c96363a1963b7733a9ee498403eb6a062160e6))
- **echarts:** theme setting supported ([93812f7](https://github.com/anncwb/vue-vben-admin/commit/93812f734ec85529aa27fc3100a2eaef8c7a6df5)), closes [#1095](https://github.com/anncwb/vue-vben-admin/issues/1095)
- **markdown:** the hierarchy of markDown components after full screen ([c8017b1](https://github.com/anncwb/vue-vben-admin/commit/c8017b1365ea49f95a26148a539f8c30d8a8631f))
- **markdown:** `value` not worked on init ([0bb9c03](https://github.com/anncwb/vue-vben-admin/commit/0bb9c035f77588c58d36b3fd45d89b9730cd70d7))
- **modal:** avoid style pollution to the whole world ([#1128](https://github.com/anncwb/vue-vben-admin/issues/1128)) ([6e7f6f8](https://github.com/anncwb/vue-vben-admin/commit/6e7f6f82ed2819e02e2b3114884e665d0762d7e9))
- **table:** `rowClassName` not worked with `striped` ([044e2e4](https://github.com/anncwb/vue-vben-admin/commit/044e2e4e866dd5b120daab03c47aba1ca1f9140a)), closes [#1167](https://github.com/anncwb/vue-vben-admin/issues/1167)
- **table:** 修复表格背景颜色再深色模式下会被穿透问题 ([#1133](https://github.com/anncwb/vue-vben-admin/issues/1133)) ([30fa4cf](https://github.com/anncwb/vue-vben-admin/commit/30fa4cfa2ab6229efc67224fd082e32da0a95d49))
- **table:** editable icon not show with empty cell ([edc3096](https://github.com/anncwb/vue-vben-admin/commit/edc30965653831b4572c5d5e067f556f4757ce75)), closes [#1103](https://github.com/anncwb/vue-vben-admin/issues/1103)
- **table:** fix table footer style ([a426b90](https://github.com/anncwb/vue-vben-admin/commit/a426b9027ef524f9033d510d0c74cd17b2ad5bcf)), closes [#1112](https://github.com/anncwb/vue-vben-admin/issues/1112)
- **table:** Solve the bug of setting ifshow to false in table column ([#1166](https://github.com/anncwb/vue-vben-admin/issues/1166)) ([5fa730c](https://github.com/anncwb/vue-vben-admin/commit/5fa730c49ae46fa448d49d597dc7b2b6a019b268))
- **table-action:** `divider` not work as expected ([7593ef6](https://github.com/anncwb/vue-vben-admin/commit/7593ef6a4f081ed800658b70316ab2f1e3ee631d))
- **tinymce:** fixed `inline` mode ([8e01377](https://github.com/anncwb/vue-vben-admin/commit/8e01377481a34cda221de6bbb01fc7d5b2824c82)), closes [#1092](https://github.com/anncwb/vue-vben-admin/issues/1092)
- **upload:** `accept` not work as expected ([656ee4e](https://github.com/anncwb/vue-vben-admin/commit/656ee4e5c9b363b6ab59aa071915414e5ee95de4))
- `getUserinfo` is compatible with empty roles data ([1ddfc31](https://github.com/anncwb/vue-vben-admin/commit/1ddfc31c3c4c792c5f741f6d0f0754ffc9a6613c))
- `slots` worked in `basicTable` and `basicModal` ([5138e44](https://github.com/anncwb/vue-vben-admin/commit/5138e447e74ef01309457d22f44129c8b1b2f815))
- `useRedo` called duplicate may cause exception ([1235978](https://github.com/anncwb/vue-vben-admin/commit/1235978ab23740dfb11e3de7ac26a7d10a4899dc)), closes [#1121](https://github.com/anncwb/vue-vben-admin/issues/1121)
- 修复 `apiSelect` 绑定值 `attrs` 的问题 ([#1172](https://github.com/anncwb/vue-vben-admin/issues/1172)) ([c753d94](https://github.com/anncwb/vue-vben-admin/commit/c753d945e08f72cab5bc8a585601cab6a0523fca))
- 修复弹窗全屏按钮异常关闭的问题([#1177](https://github.com/anncwb/vue-vben-admin/issues/1177)) ([#1182](https://github.com/anncwb/vue-vben-admin/issues/1182)) ([9e9ea3f](https://github.com/anncwb/vue-vben-admin/commit/9e9ea3f43d8c4b88649c1998bf89186b5f7ee6a2))
- 修改 axios 中 urlPrefix 字段不生效问题 ([#1170](https://github.com/anncwb/vue-vben-admin/issues/1170)) ([7df9b51](https://github.com/anncwb/vue-vben-admin/commit/7df9b513447d8deab2fd8e86fa23c807adb6d440))
- add loss action for userStore ([a36825a](https://github.com/anncwb/vue-vben-admin/commit/a36825a6d423aae9aaf1936ce55947ba8c2104b0))
- fix all types of errors, compatible with volar plugin ([e15b4f1](https://github.com/anncwb/vue-vben-admin/commit/e15b4f14db51812effd55670b3d2da7b082e00a7))
- fixed build warning for style of `intro.js` ([d27633f](https://github.com/anncwb/vue-vben-admin/commit/d27633fb31824e92cbeb24f8d626d8e33ce7179e)), closes [#1130](https://github.com/anncwb/vue-vben-admin/issues/1130)
- Improve content height calculation ([#1136](https://github.com/anncwb/vue-vben-admin/issues/1136)) ([6717fe6](https://github.com/anncwb/vue-vben-admin/commit/6717fe654e88e6a939a16c523832870388ec1886))
- name of vite `mode` support more characters ([9f68229](https://github.com/anncwb/vue-vben-admin/commit/9f6822991c4b2da78e0a5d0c7d6e0288f0d9d1cb)), closes [#1115](https://github.com/anncwb/vue-vben-admin/issues/1115)
- refresh failed while token invalid ([3a5d1a5](https://github.com/anncwb/vue-vben-admin/commit/3a5d1a5757c0a2be17e6dd370cbb023ddbb30d5e)), closes [#1101](https://github.com/anncwb/vue-vben-admin/issues/1101)
- warning in logout action ([b3307fe](https://github.com/anncwb/vue-vben-admin/commit/b3307fe2836fb6f9806d602d5bdb7e540c49f1b0))
- **tinymce:** fixed `tinymce` destory method ([fb43fad](https://github.com/anncwb/vue-vben-admin/commit/fb43fad555b093af23194bdb3670bc1347c0010f))
### Features
- **demo:** add `JsonPreview` demo ([83c1683](https://github.com/anncwb/vue-vben-admin/commit/83c1683bfdcf4ea33de771895b46e41f276969e8)), closes [#1146](https://github.com/anncwb/vue-vben-admin/issues/1146)
- **form:** add `Divider` for schema component type ([47a448b](https://github.com/anncwb/vue-vben-admin/commit/47a448b8aea572e54dac97dc4f9fb6c1c005685a))
- **form:** component `Divider` support `helpMessage` ([a5ff592](https://github.com/anncwb/vue-vben-admin/commit/a5ff59237f2eb6ea4c1770acc594c75bf1f6e95f))
- **markdown-viewer:** add new component ([73dc492](https://github.com/anncwb/vue-vben-admin/commit/73dc492b2a49793d945ccdae7f5c429c874f298c)), closes [#1181](https://github.com/anncwb/vue-vben-admin/issues/1181)
- **table:** 添加和支持动态删除和插入数据 ([#1152](https://github.com/anncwb/vue-vben-admin/issues/1152)) ([59a9087](https://github.com/anncwb/vue-vben-admin/commit/59a90877287a289f746eec97d12c2d3a1d5476b0))
- **table:** add `beforeEditSubmit` for editable cell ([2c867b3](https://github.com/anncwb/vue-vben-admin/commit/2c867b3d636d57cdc526a4ca600af7d747b7d833))
- **table:** add `onValid` for editRow ([ee7c31d](https://github.com/anncwb/vue-vben-admin/commit/ee7c31db44fd8f99f0d26da368e1d82b5630f309))
- **tree:** 1. 添加自定义数据过滤判断方法 2. 添加搜索完成自动展开结果选项 3. 添加搜索完成自动选中结果选项 4. 树节点数据变化时强制搜索(同步 searchData 避免展示错误) ([#1132](https://github.com/anncwb/vue-vben-admin/issues/1132)) ([e00578c](https://github.com/anncwb/vue-vben-admin/commit/e00578c40a585a4a35f235c0228aebaf62cea1ba))
- add CardList component ([0f5ddbf](https://github.com/anncwb/vue-vben-admin/commit/0f5ddbf1ec777fc238a94bd037d37ea787316757))
### Performance Improvements
- **tree:** 优化 Tree 搜索功能,添加搜索高亮功能,优化样式表现 ([#1153](https://github.com/anncwb/vue-vben-admin/issues/1153)) ([3b6b4f7](https://github.com/anncwb/vue-vben-admin/commit/3b6b4f73033e8757fd3a032f0910dfcc30dee151))
- not waiting for router.isReady ([2884e86](https://github.com/anncwb/vue-vben-admin/commit/2884e863ce826cd92cd782f40cdee31588bc6d32))
- optimize css volume ([466d4ed](https://github.com/anncwb/vue-vben-admin/commit/466d4edcd02fc91e2b4cdbbc3c501bfd2fde7a3d))
## [2.7.1](https://github.com/anncwb/vue-vben-admin/compare/v2.6.1...v2.7.1) (2021-08-16)
### Bug Fixes

35
CHANGELOG.zh_CN.md

@ -1,7 +1,37 @@
### ✨ Features
- **其它**
- `.env`文件中的`VITE_PROXY`配置支持单引号
- 移除 build 过程中的警告
### 🐛 Bug Fixes
- **BasicTable**
- 修复可编辑单元格某些情况下无法提交的问题
- 修复`inset`属性不起作用的问题
- 修复`useTable`与`BasicTable`实例的`reload`方法`await`表现不一致的问题
- 修复`clickToRowSelect`会无视行选择框 disabled 状态的问题
- **BasicModal**
- 修复点击遮罩、按下`Esc`键都不能关闭`Modal`的问题
- 修复点击关闭按钮、最大化按钮旁边的空白区域也会导致`Modal`关闭的问题
- **BasicTree** 修复节点插槽不起作用的问题
- **CodeEditor** 修复可能会造成的`Build`失败的问题
- **BasicForm** 修复自定义 FormItem 组件的内容宽度可能超出范围的问题
- **其它**
- 修复多标签在某些情况下关闭页签不会跳转路由的问题
- 修复部分组件可能会造成热更新异常的问题
- 修复直接`import`部分`antdv`子组件时会在 build 过程中报错的问题,如:TabPane、RadioGroup
## 2.7.2(2021-09-14)
### ✨ Features
- **BasicForm** 表单组件新增`Divider`,用于较长表单的区域分割
- **BasicTable** 单元格编辑新增提交回调,将根据回调函数返回的结果来决定是否将数据提交到表格
- **BasicTable**
- 单元格编辑新增提交回调,将根据回调函数返回的结果来决定是否将数据提交到表格
- 行编辑添加校验方法,允许只校验而不提交值,以便异步保存数据成功后才提交倒表格
- 修复`rowClassName`属性无法和`striped`同时使用的问题
- 新增组件 **MarkdownViewer** 用于显示 Markdown 格式的富文本
### 🐛 Bug Fixes
@ -10,6 +40,9 @@
- **BasicTable**
- 修复可编辑单元格的内容为空时,不会显示编辑图标的问题
- 修复表尾合计行与表格主体部分的列有时候未能对齐的问题
- **MarkDown** 修复初始 value 属性的值不起作用的问题
- **BasicUpload** 修复`accept`属性不支持`MIME`及点开头的后缀名的问题
- **ApiSelect** 修复`value`属性的类型定义问题
- **其它**
- 修复部分封装组件在使用插槽时报错的问题
- 修复`useECharts`的`theme`参数不起作用的问题

4
build/utils.ts

@ -28,9 +28,9 @@ export function wrapperEnv(envConf: Recordable): ViteEnv {
if (envName === 'VITE_PORT') {
realName = Number(realName);
}
if (envName === 'VITE_PROXY') {
if (envName === 'VITE_PROXY' && realName) {
try {
realName = JSON.parse(realName);
realName = JSON.parse(realName.replace(/'/g, '"'));
} catch (error) {
realName = '';
}

47
build/vite/plugin/styleImport.ts

@ -14,7 +14,52 @@ export function configStyleImportPlugin(isBuild: boolean) {
libraryName: 'ant-design-vue',
esModule: true,
resolveStyle: (name) => {
return `ant-design-vue/es/${name}/style/index`;
// 这里是“子组件”列表,无需额外引入样式文件
const ignoreList = [
'typography-text',
'typography-title',
'typography-paragraph',
'typography-link',
'anchor-link',
'sub-menu',
'menu-item',
'menu-item-group',
'dropdown-button',
'breadcrumb-item',
'breadcrumb-separator',
'input-password',
'input-search',
'input-group',
'form-item',
'radio-group',
'checkbox-group',
'layout-sider',
'layout-content',
'layout-footer',
'layout-header',
'step',
'select-option',
'select-opt-group',
'card-grid',
'card-meta',
'collapse-panel',
'descriptions-item',
'list-item',
'list-item-meta',
'table-column',
'table-column-group',
'tab-pane',
'tab-content',
'timeline-item',
'tree-node',
'skeleton-input',
'skeleton-avatar',
'skeleton-title',
'skeleton-paragraph',
'skeleton-image',
'skeleton-button',
];
return ignoreList.includes(name) ? '' : `ant-design-vue/es/${name}/style/index`;
},
},
],

9
mock/demo/account.ts

@ -1,5 +1,6 @@
import { MockMethod } from 'vite-plugin-mock';
import { resultSuccess, resultError } from '../_util';
import { ResultEnum } from '../../src/enums/httpEnum';
const userInfo = {
name: 'Vben',
@ -59,4 +60,12 @@ export default [
return resultError();
},
},
{
url: '/basic-api/user/tokenExpired',
method: 'post',
statusCode: 200,
response: () => {
return resultError('Token Expired!', { code: ResultEnum.TIMEOUT as number });
},
},
] as MockMethod[];

92
package.json

@ -1,6 +1,6 @@
{
"name": "vben-admin",
"version": "2.7.1",
"version": "2.7.2",
"author": {
"name": "vben",
"email": "anncwb@126.com",
@ -35,61 +35,63 @@
},
"dependencies": {
"@iconify/iconify": "^2.0.4",
"@logicflow/core": "^0.6.15",
"@logicflow/extension": "^0.6.15",
"@vueuse/core": "^6.0.0",
"@logicflow/core": "^0.6.16",
"@logicflow/extension": "^0.6.16",
"@vueuse/core": "^6.4.1",
"@zxcvbn-ts/core": "^1.0.0-beta.0",
"ant-design-vue": "2.2.6",
"axios": "^0.21.1",
"codemirror": "^5.62.3",
"ant-design-vue": "2.2.8",
"axios": "^0.21.4",
"codemirror": "^5.63.0",
"cropperjs": "^1.5.12",
"crypto-js": "^4.1.1",
"echarts": "^5.1.2",
"intro.js": "^4.1.0",
"echarts": "^5.2.1",
"intro.js": "^4.2.2",
"lodash-es": "^4.17.21",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.2.0",
"pinia": "2.0.0-rc.6",
"pinia": "2.0.0-rc.9",
"print-js": "^1.6.0",
"qrcode": "^1.4.4",
"resize-observer-polyfill": "^1.5.1",
"showdown": "^1.9.1",
"sortablejs": "^1.14.0",
"tinymce": "^5.8.2",
"vditor": "^3.8.6",
"vue": "3.2.4",
"vue-i18n": "9.1.7",
"vue-json-pretty": "1.8.1",
"tinymce": "^5.9.2",
"vditor": "^3.8.7",
"vue": "^3.2.16",
"vue-i18n": "^9.1.7",
"vue-json-pretty": "^2.0.4",
"vue-router": "^4.0.11",
"vue-types": "^4.0.3",
"xlsx": "^0.17.1"
"vue-types": "^4.1.0",
"xlsx": "^0.17.2"
},
"devDependencies": {
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@iconify/json": "^1.1.392",
"@iconify/json": "^1.1.405",
"@purge-icons/generated": "^0.7.0",
"@types/codemirror": "^5.60.2",
"@types/codemirror": "^5.60.3",
"@types/crypto-js": "^4.0.2",
"@types/fs-extra": "^9.0.12",
"@types/inquirer": "^7.3.3",
"@types/fs-extra": "^9.0.13",
"@types/inquirer": "^8.1.2",
"@types/intro.js": "^3.0.2",
"@types/jest": "^27.0.1",
"@types/lodash-es": "^4.17.4",
"@types/jest": "^27.0.2",
"@types/lodash-es": "^4.17.5",
"@types/mockjs": "^1.0.4",
"@types/node": "^16.7.1",
"@types/node": "^16.9.6",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.4.1",
"@types/qs": "^6.9.7",
"@types/showdown": "^1.9.4",
"@types/sortablejs": "^1.10.7",
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"@vitejs/plugin-legacy": "^1.5.1",
"@vitejs/plugin-vue": "^1.4.0",
"@vitejs/plugin-vue-jsx": "^1.1.7",
"@vue/compiler-sfc": "3.2.4",
"@vue/test-utils": "^2.0.0-rc.12",
"autoprefixer": "^10.3.2",
"@typescript-eslint/eslint-plugin": "^4.31.2",
"@typescript-eslint/parser": "^4.31.2",
"@vitejs/plugin-legacy": "^1.5.3",
"@vitejs/plugin-vue": "^1.9.1",
"@vitejs/plugin-vue-jsx": "^1.1.8",
"@vue/compiler-sfc": "3.2.16",
"@vue/test-utils": "^2.0.0-rc.15",
"autoprefixer": "^10.3.5",
"commitizen": "^4.2.4",
"conventional-changelog-cli": "^2.1.1",
"cross-env": "^7.0.3",
@ -97,32 +99,32 @@
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-define-config": "^1.0.9",
"eslint-plugin-jest": "^24.4.0",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-vue": "^7.16.0",
"eslint-plugin-jest": "^24.4.2",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.18.0",
"esno": "^0.9.1",
"fs-extra": "^10.0.0",
"http-server": "^13.0.1",
"http-server": "^13.0.2",
"husky": "^7.0.2",
"inquirer": "^8.1.2",
"inquirer": "^8.1.5",
"is-ci": "^3.0.0",
"jest": "^27.0.6",
"jest": "^27.2.1",
"less": "^4.1.1",
"lint-staged": "^11.1.2",
"npm-run-all": "^4.1.5",
"postcss": "^8.3.6",
"prettier": "^2.3.2",
"postcss": "^8.3.7",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.1",
"rimraf": "^3.0.2",
"rollup-plugin-visualizer": "5.5.2",
"rollup-plugin-visualizer": "^5.5.2",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-order": "^4.1.0",
"ts-jest": "^27.0.5",
"ts-node": "^10.2.1",
"typescript": "4.3.5",
"vite": "2.5.1",
"typescript": "^4.4.3",
"vite": "^2.5.10",
"vite-plugin-compression": "^0.3.5",
"vite-plugin-html": "^2.1.0",
"vite-plugin-imagemin": "^0.4.5",
@ -133,8 +135,8 @@
"vite-plugin-svg-icons": "^1.0.4",
"vite-plugin-theme": "^0.8.1",
"vite-plugin-vue-setup-extend": "^0.1.0",
"vite-plugin-windicss": "^1.3.0",
"vue-eslint-parser": "^7.10.0",
"vite-plugin-windicss": "^1.4.5",
"vue-eslint-parser": "^7.11.0",
"vue-tsc": "^0.3.0"
},
"resolutions": {

1
prettier.config.js

@ -8,7 +8,6 @@ module.exports = {
quoteProps: 'as-needed',
bracketSpacing: true,
trailingComma: 'all',
jsxBracketSameLine: false,
jsxSingleQuote: false,
arrowParens: 'always',
insertPragma: false,

3
src/api/demo/account.ts

@ -4,6 +4,7 @@ import { GetAccountInfoModel } from './model/accountModel';
enum Api {
ACCOUNT_INFO = '/account/getAccountInfo',
SESSION_TIMEOUT = '/user/sessionTimeout',
TOKEN_EXPIRED = '/user/tokenExpired',
}
// Get personal center-basic settings
@ -11,3 +12,5 @@ enum Api {
export const accountInfoApi = () => defHttp.get<GetAccountInfoModel>({ url: Api.ACCOUNT_INFO });
export const sessionTimeoutApi = () => defHttp.post<void>({ url: Api.SESSION_TIMEOUT });
export const tokenExpiredApi = () => defHttp.post<void>({ url: Api.TOKEN_EXPIRED });

2
src/api/sys/user.ts

@ -29,7 +29,7 @@ export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal')
* @description: getUserInfo
*/
export function getUserInfo() {
return defHttp.get<GetUserInfoModel>({ url: Api.GetUserInfo });
return defHttp.get<GetUserInfoModel>({ url: Api.GetUserInfo }, { errorMessageMode: 'none' });
}
export function getPermCode() {

4
src/components/CardList/index.ts

@ -0,0 +1,4 @@
import { withInstall } from '/@/utils';
import cardList from './src/CardList.vue';
export const CardList = withInstall(cardList);

178
src/components/CardList/src/CardList.vue

@ -0,0 +1,178 @@
<template>
<div class="p-2">
<div class="bg-white mb-2 p-4">
<BasicForm @register="registerForm" />
</div>
{{ sliderProp.width }}
<div class="bg-white p-2">
<List
:grid="{ gutter: 5, xs: 1, sm: 2, md: 4, lg: 4, xl: 6, xxl: grid }"
:data-source="data"
:pagination="paginationProp"
>
<template #header>
<div class="flex justify-end space-x-2"
><slot name="header"></slot>
<Tooltip>
<template #title>
<div class="w-50">每行显示数量</div
><Slider
id="slider"
v-bind="sliderProp"
v-model:value="grid"
@change="sliderChange"
/></template>
<Button><TableOutlined /></Button>
</Tooltip>
<Tooltip @click="fetch">
<template #title>刷新</template>
<Button><RedoOutlined /></Button>
</Tooltip>
</div>
</template>
<template #renderItem="{ item }">
<ListItem>
<Card>
<template #title></template>
<template #cover>
<div :class="height">
<Image :src="item.imgs[0]" />
</div>
</template>
<template class="ant-card-actions" #actions>
<!-- <SettingOutlined key="setting" />-->
<EditOutlined key="edit" />
<Dropdown
:trigger="['hover']"
:dropMenuList="[
{
text: '删除',
event: '1',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, item.id),
},
},
]"
popconfirm
>
<EllipsisOutlined key="ellipsis" />
</Dropdown>
</template>
<CardMeta>
<template #title>
<TypographyText :content="item.name" :ellipsis="{ tooltip: item.address }" />
</template>
<template #avatar>
<Avatar :src="item.avatar" />
</template>
<template #description>{{ item.time }}</template>
</CardMeta>
</Card>
</ListItem>
</template>
</List>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import {
EditOutlined,
EllipsisOutlined,
RedoOutlined,
TableOutlined,
} from '@ant-design/icons-vue';
import { List, Card, Image, Typography, Tooltip, Slider, Avatar } from 'ant-design-vue';
import { Dropdown } from '/@/components/Dropdown';
import { BasicForm, useForm } from '/@/components/Form';
import { propTypes } from '/@/utils/propTypes';
import { Button } from '/@/components/Button';
import { isFunction } from '/@/utils/is';
import { useSlider, grid } from './data';
const ListItem = List.Item;
const CardMeta = Card.Meta;
const TypographyText = Typography.Text;
// 获取slider属性
const sliderProp = computed(() => useSlider(4));
// 组件接收参数
const props = defineProps({
// 请求API的参数
params: propTypes.object.def({}),
//api
api: propTypes.func,
});
//暴露内部方法
const emit = defineEmits(['getMethod', 'delete']);
//数据
const data = ref([]);
// 切换每行个数
// cover图片自适应高度
//修改pageSize并重新请求数据
const height = computed(() => {
return `h-${120 - grid.value * 6}`;
});
//表单
const [registerForm, { validate }] = useForm({
schemas: [{ field: 'type', component: 'Input', label: '类型' }],
labelWidth: 80,
baseColProps: { span: 6 },
actionColOptions: { span: 24 },
autoSubmitOnEnter: true,
submitFunc: handleSubmit,
});
//表单提交
async function handleSubmit() {
const data = await validate();
await fetch(data);
}
function sliderChange(n) {
pageSize.value = n * 4;
fetch();
}
// 自动请求并暴露内部方法
onMounted(() => {
fetch();
emit('getMethod', fetch);
});
async function fetch(p = {}) {
const { api, params } = props;
if (api && isFunction(api)) {
const res = await api({ ...params, page: page.value, pageSize: pageSize.value, ...p });
data.value = res.items;
total.value = res.total;
}
}
//分页相关
const page = ref(1);
const pageSize = ref(36);
const total = ref(0);
const paginationProp = ref({
showSizeChanger: false,
showQuickJumper: true,
pageSize,
current: page,
total,
showTotal: (total) => `总 ${total} 条`,
onChange: pageChange,
onShowSizeChange: pageSizeChange,
});
function pageChange(p, pz) {
page.value = p;
pageSize.value = pz;
fetch();
}
function pageSizeChange(current, size) {
pageSize.value = size;
fetch();
}
async function handleDelete(id) {
emit('delete', id);
}
</script>

25
src/components/CardList/src/data.ts

@ -0,0 +1,25 @@
import { ref } from 'vue';
//每行个数
export const grid = ref(12);
// slider属性
export const useSlider = (min = 6, max = 12) => {
// 每行显示个数滑动条
const getMarks = () => {
const l = {};
for (let i = min; i < max + 1; i++) {
l[i] = {
style: {
color: '#fff',
},
label: i,
};
}
return l;
};
return {
min,
max,
marks: getMarks(),
step: 1,
};
};

2
src/components/CodeEditor/index.ts

@ -4,3 +4,5 @@ import jsonPreview from './src/json-preview/JsonPreview.vue';
export const CodeEditor = withInstall(codeEditor);
export const JsonPreview = withInstall(jsonPreview);
export * from './src/typing';

18
src/components/CodeEditor/src/CodeEditor.vue

@ -8,22 +8,22 @@
/>
</div>
</template>
<script lang="ts">
const MODE = {
JSON: 'application/json',
html: 'htmlmixed',
js: 'javascript',
};
</script>
<script lang="ts" setup>
import { computed } from 'vue';
import CodeMirrorEditor from './codemirror/CodeMirror.vue';
import { isString } from '/@/utils/is';
import { MODE } from './typing';
const props = defineProps({
value: { type: [Object, String] as PropType<Record<string, any> | string> },
mode: { type: String, default: MODE.JSON },
mode: {
type: String as PropType<MODE>,
default: MODE.JSON,
validator(value: any) {
// 这个值必须匹配下列字符串中的一个
return Object.values(MODE).includes(value);
},
},
readonly: { type: Boolean },
autoFormat: { type: Boolean, default: true },
});

10
src/components/CodeEditor/src/codemirror/CodeMirror.vue

@ -8,6 +8,7 @@
import { useAppStore } from '/@/store/modules/app';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import CodeMirror from 'codemirror';
import { MODE } from './../typing';
// css
import './codemirror.css';
import 'codemirror/theme/idea.css';
@ -18,7 +19,14 @@
import 'codemirror/mode/htmlmixed/htmlmixed';
const props = defineProps({
mode: { type: String, default: 'application/json' },
mode: {
type: String as PropType<MODE>,
default: MODE.JSON,
validator(value: any) {
// 这个值必须匹配下列字符串中的一个
return Object.values(MODE).includes(value);
},
},
value: { type: String, default: '' },
readonly: { type: Boolean, default: false },
});

5
src/components/CodeEditor/src/typing.ts

@ -0,0 +1,5 @@
export enum MODE {
JSON = 'application/json',
HTML = 'htmlmixed',
JS = 'javascript',
}

121
src/components/Dropdown/src/Dropdown.vue

@ -1,17 +1,17 @@
<template>
<Dropdown :trigger="trigger" v-bind="$attrs">
<a-dropdown :trigger="trigger" v-bind="$attrs">
<span>
<slot></slot>
</span>
<template #overlay>
<Menu :selectedKeys="selectedKeys">
<a-menu :selectedKeys="selectedKeys">
<template v-for="item in dropMenuList" :key="`${item.event}`">
<MenuItem
<a-menu-item
v-bind="getAttr(item.event)"
@click="handleClickMenu(item)"
:disabled="item.disabled"
>
<Popconfirm
<a-popconfirm
v-if="popconfirm && item.popConfirm"
v-bind="getPopConfirmAttrs(item.popConfirm)"
>
@ -22,86 +22,75 @@
<Icon :icon="item.icon" v-if="item.icon" />
<span class="ml-1">{{ item.text }}</span>
</div>
</Popconfirm>
</a-popconfirm>
<template v-else>
<Icon :icon="item.icon" v-if="item.icon" />
<span class="ml-1">{{ item.text }}</span>
</template>
</MenuItem>
<MenuDivider v-if="item.divider" :key="`d-${item.event}`" />
</a-menu-item>
<a-menu-divider v-if="item.divider" :key="`d-${item.event}`" />
</template>
</Menu>
</a-menu>
</template>
</Dropdown>
</a-dropdown>
</template>
<script lang="ts">
<script lang="ts" setup>
import { computed, PropType } from 'vue';
import type { DropMenu } from './typing';
import { defineComponent } from 'vue';
import { Dropdown, Menu, Popconfirm } from 'ant-design-vue';
import { Icon } from '/@/components/Icon';
import { omit } from 'lodash-es';
import { isFunction } from '/@/utils/is';
export default defineComponent({
name: 'BasicDropdown',
components: {
Dropdown,
Menu,
MenuItem: Menu.Item,
MenuDivider: Menu.Divider,
Icon,
Popconfirm,
},
props: {
popconfirm: Boolean,
/**
* the trigger mode which executes the drop-down action
* @default ['hover']
* @type string[]
*/
trigger: {
type: [Array] as PropType<('contextmenu' | 'click' | 'hover')[]>,
default: () => {
return ['contextmenu'];
},
},
dropMenuList: {
type: Array as PropType<(DropMenu & Recordable)[]>,
default: () => [],
},
selectedKeys: {
type: Array as PropType<string[]>,
default: () => [],
const ADropdown = Dropdown;
const AMenu = Menu;
const AMenuItem = Menu.Item;
const AMenuDivider = Menu.Divider;
const APopconfirm = Popconfirm;
const props = defineProps({
popconfirm: Boolean,
/**
* the trigger mode which executes the drop-down action
* @default ['hover']
* @type string[]
*/
trigger: {
type: [Array] as PropType<('contextmenu' | 'click' | 'hover')[]>,
default: () => {
return ['contextmenu'];
},
},
emits: ['menuEvent'],
setup(props, { emit }) {
function handleClickMenu(item: DropMenu) {
const { event } = item;
const menu = props.dropMenuList.find((item) => `${item.event}` === `${event}`);
emit('menuEvent', menu);
item.onClick?.();
}
dropMenuList: {
type: Array as PropType<(DropMenu & Recordable)[]>,
default: () => [],
},
selectedKeys: {
type: Array as PropType<string[]>,
default: () => [],
},
});
const getPopConfirmAttrs = computed(() => {
return (attrs) => {
const originAttrs = omit(attrs, ['confirm', 'cancel', 'icon']);
if (!attrs.onConfirm && attrs.confirm && isFunction(attrs.confirm))
originAttrs['onConfirm'] = attrs.confirm;
if (!attrs.onCancel && attrs.cancel && isFunction(attrs.cancel))
originAttrs['onCancel'] = attrs.cancel;
return originAttrs;
};
});
const emit = defineEmits(['menuEvent']);
return {
handleClickMenu,
getPopConfirmAttrs,
getAttr: (key: string | number) => ({ key }),
};
},
function handleClickMenu(item: DropMenu) {
const { event } = item;
const menu = props.dropMenuList.find((item) => `${item.event}` === `${event}`);
emit('menuEvent', menu);
item.onClick?.();
}
const getPopConfirmAttrs = computed(() => {
return (attrs) => {
const originAttrs = omit(attrs, ['confirm', 'cancel', 'icon']);
if (!attrs.onConfirm && attrs.confirm && isFunction(attrs.confirm))
originAttrs['onConfirm'] = attrs.confirm;
if (!attrs.onCancel && attrs.cancel && isFunction(attrs.cancel))
originAttrs['onCancel'] = attrs.cancel;
return originAttrs;
};
});
const getAttr = (key: string | number) => ({ key });
</script>

37
src/components/Excel/src/ImportExcel.vue

@ -15,12 +15,25 @@
<script lang="ts">
import { defineComponent, ref, unref } from 'vue';
import XLSX from 'xlsx';
import { dateUtil } from '/@/utils/dateUtil';
import type { ExcelData } from './typing';
export default defineComponent({
name: 'ImportExcel',
props: {
// 日期时间格式。如果不提供或者提供空值,将返回原始Date对象
dateFormat: {
type: String,
},
// 时区调整。实验性功能,仅为了解决读取日期时间值有偏差的问题。目前仅提供了+08:00时区的偏差修正值
// https://github.com/SheetJS/sheetjs/issues/1470#issuecomment-501108554
timeZone: {
type: Number,
default: 8,
},
},
emits: ['success', 'error'],
setup(_, { emit }) {
setup(props, { emit }) {
const inputRef = ref<HTMLInputElement | null>(null);
const loadingRef = ref<Boolean>(false);
@ -51,10 +64,28 @@
*/
function getExcelData(workbook: XLSX.WorkBook) {
const excelData: ExcelData[] = [];
const { dateFormat, timeZone } = props;
for (const sheetName of workbook.SheetNames) {
const worksheet = workbook.Sheets[sheetName];
const header: string[] = getHeaderRow(worksheet);
const results = XLSX.utils.sheet_to_json(worksheet);
let results = XLSX.utils.sheet_to_json(worksheet, {
raw: true,
dateNF: dateFormat, //Not worked
}) as object[];
results = results.map((row: object) => {
for (let field in row) {
if (row[field] instanceof Date) {
if (timeZone === 8) {
row[field].setSeconds(row[field].getSeconds() + 43);
}
if (dateFormat) {
row[field] = dateUtil(row[field]).format(dateFormat);
}
}
}
return row;
});
excelData.push({
header,
results,
@ -76,7 +107,7 @@
reader.onload = async (e) => {
try {
const data = e.target && e.target.result;
const workbook = XLSX.read(data, { type: 'array' });
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
// console.log(workbook);
/* DO SOMETHING WITH workbook HERE */
const excelData = getExcelData(workbook);

9
src/components/Form/src/components/ApiSelect.vue

@ -1,7 +1,7 @@
<template>
<Select
@dropdownVisibleChange="handleFetch"
v-bind="attrs"
v-bind="$attrs"
@change="handleChange"
:options="getOptions"
v-model:value="state"
@ -41,12 +41,7 @@
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([
propTypes.object,
propTypes.number,
propTypes.string,
propTypes.array,
]),
value: [Array, Object, String, Number],
numberToString: propTypes.bool,
api: {
type: Function as PropType<(arg?: Recordable) => Promise<OptionsItem[]>>,

2
src/components/Form/src/components/FormItem.vue

@ -340,7 +340,7 @@
wrapperCol={wrapperCol}
>
<div style="display:flex">
<div style="flex:1">{getContent()}</div>
<div style="flex:1;">{getContent()}</div>
{showSuffix && <span class="suffix">{getSuffix}</span>}
</div>
</Form.Item>

158
src/components/Icon/src/IconPicker.vue

@ -7,7 +7,7 @@
v-model:value="currentSelect"
>
<template #addonAfter>
<Popover
<a-popover
placement="bottomLeft"
trigger="click"
v-model="visible"
@ -17,7 +17,7 @@
<div class="flex justify-between">
<a-input
:placeholder="t('component.icon.search')"
@change="handleSearchChange"
@change="debounceHandleSearchChange"
allowClear
/>
</div>
@ -53,7 +53,7 @@
</ul>
</ScrollContainer>
<div class="flex py-2 items-center justify-center" v-if="getTotal >= pageSize">
<Pagination
<a-pagination
showLessItems
size="small"
:pageSize="pageSize"
@ -63,7 +63,7 @@
</div>
</div>
<template v-else
><div class="p-5"><Empty /></div>
><div class="p-5"><a-empty /></div>
</template>
</template>
@ -71,16 +71,14 @@
<SvgIcon :name="currentSelect" />
</span>
<Icon :icon="currentSelect || 'ion:apps-outline'" class="cursor-pointer px-2 py-1" v-else />
</Popover>
</a-popover>
</template>
</a-input>
</template>
<script lang="ts">
import { defineComponent, ref, watchEffect, watch, unref } from 'vue';
<script lang="ts" setup>
import { ref, watchEffect, watch, unref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { ScrollContainer } from '/@/components/Container';
import { Input, Popover, Pagination, Empty } from 'ant-design-vue';
import Icon from './Icon.vue';
import SvgIcon from './SvgIcon.vue';
@ -94,6 +92,12 @@
import { useMessage } from '/@/hooks/web/useMessage';
import svgIcons from 'virtual:svg-icons-names';
// 没有使用别名引入,是因为WebStorm当前版本还不能正确识别,会报unused警告
const AInput = Input;
const APopover = Popover;
const APagination = Pagination;
const AEmpty = Empty;
function getIcons() {
const data = iconsData as any;
const prefix: string = data?.prefix ?? '';
@ -110,88 +114,70 @@
return svgIcons.map((icon) => icon.replace('icon-', ''));
}
export default defineComponent({
name: 'IconPicker',
components: { [Input.name]: Input, Icon, Popover, ScrollContainer, Pagination, Empty, SvgIcon },
inheritAttrs: false,
props: {
value: propTypes.string,
width: propTypes.string.def('100%'),
pageSize: propTypes.number.def(140),
copy: propTypes.bool.def(false),
mode: propTypes.oneOf<('svg' | 'iconify')[]>(['svg', 'iconify']).def('iconify'),
const props = defineProps({
value: propTypes.string,
width: propTypes.string.def('100%'),
pageSize: propTypes.number.def(140),
copy: propTypes.bool.def(false),
mode: propTypes.oneOf<('svg' | 'iconify')[]>(['svg', 'iconify']).def('iconify'),
});
const emit = defineEmits(['change', 'update:value']);
const isSvgMode = props.mode === 'svg';
const icons = isSvgMode ? getSvgIcons() : getIcons();
const currentSelect = ref('');
const visible = ref(false);
const currentList = ref(icons);
const { t } = useI18n();
const { prefixCls } = useDesign('icon-picker');
const debounceHandleSearchChange = useDebounceFn(handleSearchChange, 100);
const { clipboardRef, isSuccessRef } = useCopyToClipboard(props.value);
const { createMessage } = useMessage();
const { getPaginationList, getTotal, setCurrentPage } = usePagination(
currentList,
props.pageSize,
);
watchEffect(() => {
currentSelect.value = props.value;
});
watch(
() => currentSelect.value,
(v) => {
emit('update:value', v);
return emit('change', v);
},
emits: ['change', 'update:value'],
setup(props, { emit }) {
const isSvgMode = props.mode === 'svg';
const icons = isSvgMode ? getSvgIcons() : getIcons();
const currentSelect = ref('');
const visible = ref(false);
const currentList = ref(icons);
const { t } = useI18n();
const { prefixCls } = useDesign('icon-picker');
const debounceHandleSearchChange = useDebounceFn(handleSearchChange, 100);
const { clipboardRef, isSuccessRef } = useCopyToClipboard(props.value);
const { createMessage } = useMessage();
const { getPaginationList, getTotal, setCurrentPage } = usePagination(
currentList,
props.pageSize,
);
watchEffect(() => {
currentSelect.value = props.value;
});
watch(
() => currentSelect.value,
(v) => {
emit('update:value', v);
return emit('change', v);
},
);
function handlePageChange(page: number) {
setCurrentPage(page);
}
);
function handleClick(icon: string) {
currentSelect.value = icon;
if (props.copy) {
clipboardRef.value = icon;
if (unref(isSuccessRef)) {
createMessage.success(t('component.icon.copy'));
}
}
}
function handlePageChange(page: number) {
setCurrentPage(page);
}
function handleSearchChange(e: ChangeEvent) {
const value = e.target.value;
if (!value) {
setCurrentPage(1);
currentList.value = icons;
return;
}
currentList.value = icons.filter((item) => item.includes(value));
function handleClick(icon: string) {
currentSelect.value = icon;
if (props.copy) {
clipboardRef.value = icon;
if (unref(isSuccessRef)) {
createMessage.success(t('component.icon.copy'));
}
}
}
return {
t,
prefixCls,
visible,
isSvgMode,
getTotal,
getPaginationList,
handlePageChange,
handleClick,
currentSelect,
handleSearchChange: debounceHandleSearchChange,
};
},
});
function handleSearchChange(e: ChangeEvent) {
const value = e.target.value;
if (!value) {
setCurrentPage(1);
currentList.value = icons;
return;
}
currentList.value = icons.filter((item) => item.includes(value));
}
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-icon-picker';

2
src/components/Markdown/index.ts

@ -1,5 +1,7 @@
import { withInstall } from '/@/utils';
import markDown from './src/Markdown.vue';
import markDownViewer from './src/MarkdownViewer.vue';
export const MarkDown = withInstall(markDown);
export const MarkdownViewer = withInstall(markDownViewer);
export * from './src/typing';

2
src/components/Markdown/src/Markdown.vue

@ -38,7 +38,7 @@
const { getLocale } = useLocale();
const { getDarkMode } = useRootSetting();
const valueRef = ref('');
const valueRef = ref(props.value || '');
watch(
[() => getDarkMode.value, () => initedRef.value],

22
src/components/Markdown/src/MarkdownViewer.vue

@ -0,0 +1,22 @@
<template>
<div v-html="getHtmlData" :class="$props.class" class="markdown-viewer"></div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import showdown from 'showdown';
const converter = new showdown.Converter();
converter.setOption('tables', true);
const props = defineProps({
value: { type: String },
class: { type: String },
});
const getHtmlData = computed(() => converter.makeHtml(props.value || ''));
</script>
<style scoped>
.markdown-viewer {
width: 100%;
}
</style>

5
src/components/Modal/src/BasicModal.vue

@ -72,6 +72,7 @@
import { basicProps } from './props';
import { useFullScreen } from './hooks/useModalFullScreen';
import { omit } from 'lodash-es';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'BasicModal',
@ -83,6 +84,7 @@
const visibleRef = ref(false);
const propsRef = ref<Partial<ModalProps> | null>(null);
const modalWrapperRef = ref<any>(null);
const { prefixCls } = useDesign('basic-modal');
// modal Bottom and top height
const extHeightRef = ref(0);
@ -175,7 +177,8 @@
// 取消事件
async function handleCancel(e: Event) {
e?.stopPropagation();
// 过滤自定义关闭按钮的空白区域
if ((e.target as HTMLElement)?.classList?.contains(prefixCls + '-close--custom')) return;
if (props.closeFunc && isFunction(props.closeFunc)) {
const isClose: boolean = await props.closeFunc();
visibleRef.value = !isClose;

1
src/components/Modal/src/hooks/useModal.ts

@ -48,6 +48,7 @@ export function useModal(): UseModalReturnType {
if (unref(loaded) && isProdMode() && modalMethod === unref(modal)) return;
modal.value = modalMethod;
loaded.value = true;
modalMethod.emitVisible = (visible: boolean, uid: number) => {
visibleData[uid] = visible;
};

3
src/components/Page/src/PageWrapper.vue

@ -61,6 +61,7 @@
contentFullHeight: propTypes.bool,
contentClass: propTypes.string,
fixedHeight: propTypes.bool,
upwardSpace: propTypes.oneOfType([propTypes.number, propTypes.string]).def(0),
},
setup(props, { slots, attrs }) {
const wrapperRef = ref(null);
@ -78,11 +79,13 @@
return props.contentFullHeight;
});
const getUpwardSpace = computed(() => props.upwardSpace);
const { redoHeight, setCompensation, contentHeight } = useContentHeight(
getIsContentFullHeight,
wrapperRef,
[headerRef, footerRef],
[contentRef],
getUpwardSpace,
);
setCompensation({ useLayoutFooter: true, elements: [footerRef] });

16
src/components/Table/src/BasicTable.vue

@ -141,6 +141,8 @@
getRawDataSource,
setTableData,
updateTableDataRecord,
deleteTableDataRecord,
insertTableDataRecord,
findTableDataRecord,
fetch,
getRowKey,
@ -279,6 +281,8 @@
setPagination,
setTableData,
updateTableDataRecord,
deleteTableDataRecord,
insertTableDataRecord,
findTableDataRecord,
redoHeight,
setSelectedRowKeys,
@ -361,12 +365,6 @@
}
}
&--inset {
.ant-table-wrapper {
padding: 0;
}
}
.ant-tag {
margin-right: 0;
}
@ -427,5 +425,11 @@
padding: 12px 8px;
}
}
&--inset {
.ant-table-wrapper {
padding: 0;
}
}
}
</style>

6
src/components/Table/src/components/TableAction.vue

@ -14,11 +14,7 @@
<Divider
type="vertical"
class="action-divider"
v-if="
divider &&
index < getActions.length - (dropDownActions ? 0 : 1) &&
getDropdownList.length > 0
"
v-if="divider && index < getActions.length - 1"
/>
</template>
<Dropdown

10
src/components/Table/src/components/editable/EditableCell.vue

@ -246,7 +246,7 @@
if (!record) return false;
const { key, dataIndex } = column;
const value = unref(currentValueRef);
if (!key || !dataIndex) return;
if (!key && !dataIndex) return;
const dataKey = (dataIndex || key) as string;
@ -364,13 +364,7 @@
/* eslint-disable */
props.record.onSubmitEdit = async () => {
if (isArray(props.record?.submitCbs)) {
const validFns = (props.record?.validCbs || []).map((fn) => fn());
const res = await Promise.all(validFns);
const pass = res.every((item) => !!item);
if (!pass) return;
if (!props.record?.onValid?.()) return;
const submitFns = props.record?.submitCbs || [];
submitFns.forEach((fn) => fn(false, false));
table.emit?.('edit-row-end');

13
src/components/Table/src/components/editable/index.ts

@ -3,6 +3,7 @@ import type { BasicColumn } from '/@/components/Table/src/types/table';
import { h, Ref } from 'vue';
import EditableCell from './EditableCell.vue';
import { isArray } from '/@/utils/is';
interface Params {
text: string;
@ -12,12 +13,23 @@ interface Params {
export function renderEditCell(column: BasicColumn) {
return ({ text: value, record, index }: Params) => {
record.onValid = async () => {
if (isArray(record?.validCbs)) {
const validFns = (record?.validCbs || []).map((fn) => fn());
const res = await Promise.all(validFns);
return res.every((item) => !!item);
} else {
return false;
}
};
record.onEdit = async (edit: boolean, submit = false) => {
if (!submit) {
record.editable = edit;
}
if (!edit && submit) {
if (!(await record.onValid())) return false;
const res = await record.onSubmitEdit?.();
if (res) {
record.editable = false;
@ -44,6 +56,7 @@ export function renderEditCell(column: BasicColumn) {
export type EditRecordRow<T = Recordable> = Partial<
{
onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
onValid: () => Promise<boolean>;
editable: boolean;
onCancel: Fn;
onSubmit: Fn;

2
src/components/Table/src/components/settings/ColumnSetting.vue

@ -42,7 +42,7 @@
<ScrollContainer>
<CheckboxGroup v-model:value="checkedList" @change="onChange" ref="columnListRef">
<template v-for="item in plainOptions" :key="item.value">
<div :class="`${prefixCls}__check-item`">
<div :class="`${prefixCls}__check-item`" v-if="!('ifShow' in item && !item.ifShow)">
<DragOutlined class="table-coulmn-drag-icon" />
<Checkbox :value="item.value">
{{ item.label }}

8
src/components/Table/src/hooks/useCustomRow.ts

@ -46,6 +46,14 @@ export function useCustomRow(
const isCheckbox = rowSelection.type === 'checkbox';
if (isCheckbox) {
// 找到tr
const tr: HTMLElement = (e as MouseEvent)
.composedPath?.()
.find((dom: HTMLElement) => dom.tagName === 'TR') as HTMLElement;
if (!tr) return;
// 找到Checkbox,检查是否为disabled
const checkBox = tr.querySelector('input[type=checkbox]');
if (!checkBox || checkBox.hasAttribute('disabled')) return;
if (!keys.includes(key)) {
setSelectedRowKeys([...keys, key]);
return;

32
src/components/Table/src/hooks/useDataSource.ts

@ -160,6 +160,31 @@ export function useDataSource(
}
}
function deleteTableDataRecord(record: Recordable | Recordable[]): Recordable | undefined {
if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
const records = !Array.isArray(record) ? [record] : record;
const recordIndex = records
.map((item) => dataSourceRef.value.findIndex((s) => s.key === item.key)) // 取序号
.filter((item) => item !== undefined)
.sort((a, b) => b - a); // 从大到小排序
for (const index of recordIndex) {
unref(dataSourceRef).splice(index, 1);
unref(propsRef).dataSource?.splice(index, 1);
}
setPagination({
total: unref(propsRef).dataSource?.length,
});
return unref(propsRef).dataSource;
}
function insertTableDataRecord(record: Recordable, index: number): Recordable | undefined {
if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
index = index ?? dataSourceRef.value?.length;
unref(dataSourceRef).splice(index, 0, record);
unref(propsRef).dataSource?.splice(index, 0, record);
return unref(propsRef).dataSource;
}
function findTableDataRecord(rowKey: string | number) {
if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
@ -250,7 +275,7 @@ export function useDataSource(
setPagination({
current: currentTotalPage,
});
fetch(opt);
return await fetch(opt);
}
}
@ -270,6 +295,7 @@ export function useDataSource(
items: unref(resultItems),
total: resultTotal,
});
return resultItems;
} catch (error) {
emit('fetch-error', error);
dataSourceRef.value = [];
@ -294,7 +320,7 @@ export function useDataSource(
}
async function reload(opt?: FetchParams) {
await fetch(opt);
return await fetch(opt);
}
onMounted(() => {
@ -314,6 +340,8 @@ export function useDataSource(
reload,
updateTableData,
updateTableDataRecord,
deleteTableDataRecord,
insertTableDataRecord,
findTableDataRecord,
handleTableChange,
};

8
src/components/Table/src/hooks/useTable.ts

@ -68,7 +68,7 @@ export function useTable(tableProps?: Props): [
getForm: () => FormActionType;
} = {
reload: async (opt?: FetchParams) => {
getTableInstance().reload(opt);
return await getTableInstance().reload(opt);
},
setProps: (props: Partial<BasicTableProps>) => {
getTableInstance().setProps(props);
@ -122,6 +122,12 @@ export function useTable(tableProps?: Props): [
updateTableData: (index: number, key: string, value: any) => {
return getTableInstance().updateTableData(index, key, value);
},
deleteTableDataRecord: (record: Recordable | Recordable[]) => {
return getTableInstance().deleteTableDataRecord(record);
},
insertTableDataRecord: (record: Recordable | Recordable[], index?: number) => {
return getTableInstance().insertTableDataRecord(record, index);
},
updateTableDataRecord: (rowKey: string | number, record: Recordable) => {
return getTableInstance().updateTableDataRecord(rowKey, record);
},

9
src/components/Table/src/hooks/useTableStyle.ts

@ -6,11 +6,14 @@ import { isFunction } from '/@/utils/is';
export function useTableStyle(propsRef: ComputedRef<BasicTableProps>, prefixCls: string) {
function getRowClassName(record: TableCustomRecord, index: number) {
const { striped, rowClassName } = unref(propsRef);
if (!striped) return;
const classNames: string[] = [];
if (striped) {
classNames.push((index || 0) % 2 === 1 ? `${prefixCls}-row__striped` : '');
}
if (rowClassName && isFunction(rowClassName)) {
return rowClassName(record);
classNames.push(rowClassName(record, index));
}
return (index || 0) % 2 === 1 ? `${prefixCls}-row__striped` : '';
return classNames.filter((cls) => !!cls).join(' ');
}
return { getRowClassName };

6
src/components/Table/src/types/table.ts

@ -25,7 +25,7 @@ export interface TableRowSelection<T = any> extends ITableRowSelection {
/**
* Callback executed when select/deselect one row
* @type FunctionT
* @type Function
*/
onSelect?: (record: T, selected: boolean, selectedRows: Object[], nativeEvent: Event) => any;
@ -95,6 +95,8 @@ export interface TableActionType {
setPagination: (info: Partial<PaginationProps>) => void;
setTableData: <T = Recordable>(values: T[]) => void;
updateTableDataRecord: (rowKey: string | number, record: Recordable) => Recordable | void;
deleteTableDataRecord: (record: Recordable | Recordable[]) => Recordable | void;
insertTableDataRecord: (record: Recordable, index?: number) => Recordable | void;
findTableDataRecord: (rowKey: string | number) => Recordable | void;
getColumns: (opt?: GetColumnsParams) => BasicColumn[];
setColumns: (columns: BasicColumn[] | string[]) => void;
@ -289,7 +291,7 @@ export interface BasicTableProps<T = any> {
* Row's className
* @type Function
*/
rowClassName?: (record: TableCustomRecord<T>) => string;
rowClassName?: (record: TableCustomRecord<T>, index: number) => string;
/**
* Row selection config

95
src/components/Tree/src/Tree.vue

@ -19,9 +19,9 @@
import { ScrollContainer } from '/@/components/Container';
import { omit, get, difference } from 'lodash-es';
import { isArray, isBoolean, isFunction } from '/@/utils/is';
import { isArray, isBoolean, isEmpty, isFunction } from '/@/utils/is';
import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
import { filter } from '/@/utils/helper/treeHelper';
import { filter, treeToList } from '/@/utils/helper/treeHelper';
import { useTree } from './useTree';
import { useContextMenu } from '/@/hooks/web/useContextMenu';
@ -60,6 +60,7 @@
const searchState = reactive({
startSearch: false,
searchText: '',
searchData: [] as TreeItem[],
});
@ -199,28 +200,70 @@
state.checkStrictly = strictly;
}
const searchText = ref('');
watchEffect(() => {
if (props.searchValue !== searchText.value) searchText.value = props.searchValue;
});
watch(
() => props.searchValue,
(val) => {
if (val !== searchState.searchText) {
searchState.searchText = val;
}
},
{
immediate: true,
},
);
watch(
() => props.treeData,
(val) => {
if (val) {
handleSearch(searchState.searchText);
}
},
);
function handleSearch(searchValue: string) {
if (searchValue !== searchText.value) searchText.value = searchValue;
if (searchValue !== searchState.searchText) searchState.searchText = searchValue;
emit('update:searchValue', searchValue);
if (!searchValue) {
searchState.startSearch = false;
return;
}
const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
unref(props);
searchState.startSearch = true;
const { title: titleField } = unref(getReplaceFields);
const { title: titleField, key: keyField } = unref(getReplaceFields);
const matchedKeys: string[] = [];
searchState.searchData = filter(
unref(treeDataRef),
(node) => {
return node[titleField]?.includes(searchValue) ?? false;
const result = filterFn
? filterFn(searchValue, node, unref(getReplaceFields))
: node[titleField]?.includes(searchValue) ?? false;
if (result) {
matchedKeys.push(node[keyField]);
}
return result;
},
unref(getReplaceFields),
);
if (expandOnSearch) {
const expandKeys = treeToList(searchState.searchData).map((val) => {
return val[keyField];
});
if (expandKeys && expandKeys.length) {
setExpandedKeys(expandKeys);
}
}
if (checkOnSearch && checkable && matchedKeys.length) {
setCheckedKeys(matchedKeys);
}
if (selectedOnSearch && matchedKeys.length) {
setSelectedKeys(matchedKeys);
}
}
function handleClickNode(key: string, children: TreeItem[]) {
@ -311,7 +354,7 @@
handleSearch(value);
},
getSearchValue: () => {
return searchText.value;
return searchState.searchText;
},
};
@ -342,6 +385,8 @@
if (!data) {
return null;
}
const searchText = searchState.searchText;
const { highlight } = unref(props);
return data.map((item) => {
const {
title: titleField,
@ -352,6 +397,23 @@
const propsData = omit(item, 'title');
const icon = getIcon({ ...item, level }, item.icon);
const children = get(item, childrenField) || [];
const title = get(item, titleField);
const searchIdx = title.indexOf(searchText);
const isHighlight =
searchState.startSearch && !isEmpty(searchText) && highlight && searchIdx !== -1;
const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;
const titleDom = isHighlight ? (
<span class={unref(getBindValues)?.blockNode ? `${prefixCls}__content` : ''}>
<span>{title.substr(0, searchIdx)}</span>
<span style={highlightStyle}>{searchText}</span>
<span>{title.substr(searchIdx + searchText.length)}</span>
</span>
) : (
title
);
return (
<Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}>
{{
@ -360,16 +422,13 @@
class={`${prefixCls}-title pl-2`}
onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
>
{slots?.title ? (
getSlot(slots, 'title', item)
{item.slots?.title ? (
getSlot(slots, item.slots?.title, item)
) : (
<>
{icon && <TreeIcon icon={icon} />}
<span
class={unref(getBindValues)?.blockNode ? `${prefixCls}__content` : ''}
>
{get(item, titleField)}
</span>
{titleDom}
{/*{get(item, titleField)}*/}
<span class={`${prefixCls}__actions`}>
{renderAction({ ...item, level })}
</span>
@ -400,7 +459,7 @@
helpMessage={helpMessage}
onStrictlyChange={onStrictlyChange}
onSearch={handleSearch}
searchText={unref(searchText)}
searchText={searchState.searchText}
>
{extendSlots(slots)}
</TreeHeader>

25
src/components/Tree/src/TreeHeader.vue

@ -5,8 +5,11 @@
{{ title }}
</BasicTitle>
<div class="flex flex-1 justify-end items-center cursor-pointer" v-if="search || toolbar">
<div class="mr-1 w-2/3" v-if="search">
<div
class="flex flex-1 justify-self-stretch items-center cursor-pointer"
v-if="search || toolbar"
>
<div :class="getInputSearchCls" v-if="search">
<InputSearch
:placeholder="t('common.searchText')"
size="small"
@ -31,7 +34,7 @@
</div>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { PropType } from 'vue';
import { defineComponent, computed, ref, watch } from 'vue';
import { Dropdown, Menu, Input } from 'ant-design-vue';
@ -80,10 +83,22 @@
searchText: propTypes.string,
},
emits: ['strictly-change', 'search'],
setup(props, { emit }) {
setup(props, { emit, slots }) {
const { t } = useI18n();
const searchValue = ref('');
const getInputSearchCls = computed(() => {
const titleExists = slots.headerTitle || props.title;
return [
'mr-1',
'w-full',
// titleExists ? 'w-2/3' : 'w-full',
{
['ml-5']: titleExists,
},
];
});
const toolbarList = computed(() => {
const { checkable } = props;
const defaultToolbarList = [
@ -157,7 +172,7 @@
// debounceEmitChange(e.target.value);
// }
return { t, toolbarList, handleMenuClick, searchValue };
return { t, toolbarList, handleMenuClick, searchValue, getInputSearchCls };
},
});
</script>

27
src/components/Tree/src/props.ts

@ -1,5 +1,12 @@
import type { PropType } from 'vue';
import type { ReplaceFields, ActionItem, Keys, CheckKeys, ContextMenuOptions } from './typing';
import type {
ReplaceFields,
ActionItem,
Keys,
CheckKeys,
ContextMenuOptions,
TreeItem,
} from './typing';
import type { ContextMenuItem } from '/@/hooks/web/useContextMenu';
import type { TreeDataItem } from 'ant-design-vue/es/tree/Tree';
import { propTypes } from '/@/utils/propTypes';
@ -66,6 +73,24 @@ export const basicProps = {
rightMenuList: {
type: Array as PropType<ContextMenuItem[]>,
},
// 自定义数据过滤判断方法(注: 不是整个过滤方法,而是内置过滤的判断方法,用于增强原本仅能通过title进行过滤的方式)
filterFn: {
type: Function as PropType<
(searchValue: any, node: TreeItem, replaceFields: ReplaceFields) => boolean
>,
default: null,
},
// 高亮搜索值,仅高亮具体匹配值(通过title)值为true时使用默认色值,值为#xxx时使用此值替代且高亮开启
highlight: {
type: [Boolean, String] as PropType<Boolean | String>,
default: false,
},
// 搜索完成时自动展开结果
expandOnSearch: propTypes.bool.def(false),
// 搜索完成自动选中所有结果,当且仅当 checkable===true 时生效
checkOnSearch: propTypes.bool.def(false),
// 搜索完成自动select所有结果
selectedOnSearch: propTypes.bool.def(false),
};
export const treeNodeProps = {

8
src/components/Upload/src/useUpload.ts

@ -22,7 +22,13 @@ export function useUploadType({
});
const getStringAccept = computed(() => {
return unref(getAccept)
.map((item) => `.${item}`)
.map((item) => {
if (item.indexOf('/') > 0 || item.startsWith('.')) {
return item;
} else {
return `.${item}`;
}
})
.join(',');
});

82
src/hooks/web/useContentHeight.ts

@ -1,8 +1,9 @@
import { ComputedRef, nextTick, Ref, ref, unref, watch } from 'vue';
import { ComputedRef, isRef, nextTick, Ref, ref, unref, watch } from 'vue';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import { useLayoutHeight } from '/@/layouts/default/content/useContentViewHeight';
import { getViewportOffset } from '/@/utils/domUtils';
import { isNumber, isString } from '/@/utils/is';
export interface CompensationHeight {
// 使用 layout Footer 高度作为判断补偿高度的条件
@ -11,6 +12,8 @@ export interface CompensationHeight {
elements?: Ref[];
}
type Upward = number | string | null | undefined;
/**
* 动态计算内容高度,根据锚点dom最下坐标到屏幕最下坐标,根据传入dom的高度、padding、margin等值进行动态计算
* 最终获取合适的内容高度
@ -20,6 +23,7 @@ export interface CompensationHeight {
* @param subtractHeightRefs 待减去高度的组件列表 Ref<ElRef | ComponentRef>
* @param substractSpaceRefs 待减去空闲空间(margins/paddings)的组件列表 Ref<ElRef | ComponentRef>
* @param offsetHeightRef 计算偏移的响应式高度,计算高度时将直接减去此值
* @param upwardSpace 向上递归减去空闲空间的 层级 或 直到指定class为止 数值为2代表向上递归两次|数值为ant-layout表示向上递归直到碰见.ant-layout为止
* @returns 响应式高度
*/
export function useContentHeight(
@ -27,6 +31,7 @@ export function useContentHeight(
anchorRef: Ref,
subtractHeightRefs: Ref[],
substractSpaceRefs: Ref[],
upwardSpace: Ref<Upward> | ComputedRef<Upward> | Upward = 0,
offsetHeightRef: Ref<number> = ref(0),
) {
const contentHeight: Ref<Nullable<number>> = ref(null);
@ -45,23 +50,33 @@ export function useContentHeight(
});
}
function calcSubtractSpace(element: HTMLDivElement | null | undefined): number {
function calcSubtractSpace(
element: Element | null | undefined,
direction: 'all' | 'top' | 'bottom' = 'all',
): number {
function numberPx(px: string) {
return Number(px.replace(/[^\d]/g, ''));
}
let subtractHeight = 0;
const ZERO_PX = '0px';
let marginBottom = ZERO_PX;
let marginTop = ZERO_PX;
if (element) {
const cssStyle = getComputedStyle(element);
marginBottom = cssStyle?.marginBottom ?? ZERO_PX;
marginTop = cssStyle?.marginTop ?? ZERO_PX;
}
if (marginBottom) {
const contentMarginBottom = Number(marginBottom.replace(/[^\d]/g, ''));
subtractHeight += contentMarginBottom;
}
if (marginTop) {
const contentMarginTop = Number(marginTop.replace(/[^\d]/g, ''));
subtractHeight += contentMarginTop;
const marginTop = numberPx(cssStyle?.marginTop ?? ZERO_PX);
const marginBottom = numberPx(cssStyle?.marginBottom ?? ZERO_PX);
const paddingTop = numberPx(cssStyle?.paddingTop ?? ZERO_PX);
const paddingBottom = numberPx(cssStyle?.paddingBottom ?? ZERO_PX);
if (direction === 'all') {
subtractHeight += marginTop;
subtractHeight += marginBottom;
subtractHeight += paddingTop;
subtractHeight += paddingBottom;
} else if (direction === 'top') {
subtractHeight += marginTop;
subtractHeight += paddingTop;
} else {
subtractHeight += marginBottom;
subtractHeight += paddingBottom;
}
}
return subtractHeight;
}
@ -80,11 +95,11 @@ export function useContentHeight(
// Add a delay to get the correct height
await nextTick();
const wrapperEl = getEl(unref(anchorRef));
if (!wrapperEl) {
const anchorEl = getEl(unref(anchorRef));
if (!anchorEl) {
return;
}
const { bottomIncludeBody } = getViewportOffset(wrapperEl);
const { bottomIncludeBody } = getViewportOffset(anchorEl);
// substract elements height
let substractHeight = 0;
@ -93,17 +108,46 @@ export function useContentHeight(
});
// subtract margins / paddings
let substractSpaceHeight = 0;
let substractSpaceHeight = calcSubtractSpace(anchorEl) ?? 0;
substractSpaceRefs.forEach((item) => {
substractSpaceHeight += calcSubtractSpace(getEl(unref(item)));
});
// upwardSpace
let upwardSpaceHeight = 0;
function upward(element: Element | null, upwardLvlOrClass: number | string | null | undefined) {
if (element && upwardLvlOrClass) {
const parent = element.parentElement;
if (parent) {
if (isString(upwardLvlOrClass)) {
if (!parent.classList.contains(upwardLvlOrClass)) {
upwardSpaceHeight += calcSubtractSpace(parent, 'bottom');
upward(parent, upwardLvlOrClass);
} else {
upwardSpaceHeight += calcSubtractSpace(parent, 'bottom');
}
} else if (isNumber(upwardLvlOrClass)) {
if (upwardLvlOrClass > 0) {
upwardSpaceHeight += calcSubtractSpace(parent, 'bottom');
upward(parent, --upwardLvlOrClass);
}
}
}
}
}
if (isRef(upwardSpace)) {
upward(anchorEl, unref(upwardSpace));
} else {
upward(anchorEl, upwardSpace);
}
let height =
bottomIncludeBody -
unref(layoutFooterHeightRef) -
unref(offsetHeightRef) -
substractHeight -
substractSpaceHeight;
substractSpaceHeight -
upwardSpaceHeight;
// compensation height
const calcCompensationHeight = () => {

6
src/layouts/default/header/components/Breadcrumb.vue

@ -101,12 +101,12 @@
if (!meta) {
return !!name;
}
const { title, hideBreadcrumb, hideMenu } = meta;
if (!title || hideBreadcrumb || hideMenu) {
const { title, hideBreadcrumb } = meta;
if (!title || hideBreadcrumb) {
return false;
}
return true;
}).filter((item) => !item.meta?.hideBreadcrumb || !item.meta?.hideMenu);
}).filter((item) => !item.meta?.hideBreadcrumb);
}
function handleClick(route: RouteLocationMatched, paths: string[], e: Event) {

13
src/layouts/default/tabs/useTabDropdown.ts

@ -34,18 +34,21 @@ export function useTabDropdown(tabContentProps: TabContentProps, getIsTabs: Comp
const { meta } = unref(getTargetTab);
const { path } = unref(currentRoute);
// Refresh button
const curItem = state.current;
const isCurItem = curItem ? curItem.path === path : false;
// Refresh button
const index = state.currentIndex;
const refreshDisabled = curItem ? curItem.path !== path : true;
const refreshDisabled = !isCurItem;
// Close left
const closeLeftDisabled = index === 0;
const closeLeftDisabled = index === 0 || !isCurItem;
const disabled = tabStore.getTabList.length === 1;
// Close right
const closeRightDisabled =
index === tabStore.getTabList.length - 1 && tabStore.getLastDragEndIndex >= 0;
!isCurItem || (index === tabStore.getTabList.length - 1 && tabStore.getLastDragEndIndex >= 0);
const dropMenuList: DropMenu[] = [
{
icon: 'ion:reload-sharp',
@ -78,7 +81,7 @@ export function useTabDropdown(tabContentProps: TabContentProps, getIsTabs: Comp
icon: 'dashicons:align-center',
event: MenuEventEnum.CLOSE_OTHER,
text: t('layout.multipleTab.closeOther'),
disabled: disabled,
disabled: disabled || !isCurItem,
},
{
icon: 'clarity:minus-line',

1
src/locales/lang/en/routes/demo.ts

@ -45,6 +45,7 @@ export default {
time: 'Relative Time',
cropperImage: 'Cropper Image',
cardList: 'Card List',
},
editor: {
editor: 'Editor',

1
src/locales/lang/zh-CN/routes/demo.ts

@ -44,6 +44,7 @@ export default {
time: '相对时间',
cropperImage: '图片裁剪',
cardList: '卡片列表',
},
editor: {
editor: '编辑器',

8
src/router/guard/stateGuard.ts

@ -8,12 +8,12 @@ import { removeTabChangeListener } from '/@/logics/mitt/routeChange';
export function createStateGuard(router: Router) {
router.afterEach((to) => {
const tabStore = useMultipleTabStore();
const userStore = useUserStore();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
// Just enter the login page and clear the authentication information
if (to.path === PageEnum.BASE_LOGIN) {
const tabStore = useMultipleTabStore();
const userStore = useUserStore();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
appStore.resetAllState();
permissionStore.resetState();
tabStore.resetState();

22
src/router/helper/routeHelper.ts

@ -1,7 +1,7 @@
import type { AppRouteModule, AppRouteRecordRaw } from '/@/router/types';
import type { Router, RouteRecordNormalized } from 'vue-router';
import { getParentLayout, LAYOUT } from '/@/router/constant';
import { getParentLayout, LAYOUT, EXCEPTION_COMPONENT } from '/@/router/constant';
import { cloneDeep, omit } from 'lodash-es';
import { warn } from '/@/utils/log';
import { createRouter, createWebHashHistory } from 'vue-router';
@ -27,7 +27,7 @@ function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
const { component, name } = item;
const { children } = item;
if (component) {
const layoutFound = LayoutMap.get(component as string);
const layoutFound = LayoutMap.get(component.toUpperCase());
if (layoutFound) {
item.component = layoutFound;
} else {
@ -46,20 +46,24 @@ function dynamicImport(
) {
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
let k = key.replace('../../views', '');
const lastIndex = k.lastIndexOf('.');
k = k.substring(0, lastIndex);
return k === component;
const k = key.replace('../../views', '');
const startFlag = component.startsWith('/');
const endFlag = component.endsWith('.vue') || component.endsWith('.tsx');
const startIndex = startFlag ? 0 : 1;
const lastIndex = endFlag ? k.length : k.lastIndexOf('.');
return k.substring(startIndex, lastIndex) === component;
});
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0];
return dynamicViewsModules[matchKey];
}
if (matchKeys?.length > 1) {
} else if (matchKeys?.length > 1) {
warn(
'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure',
);
return;
} else {
warn('在src/views/下找不到`' + component + '.vue` 或 `' + component + '.tsx`, 请自行创建!');
return EXCEPTION_COMPONENT;
}
}
@ -80,6 +84,8 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul
meta.affix = false;
route.meta = meta;
}
} else {
warn('请正确配置路由:' + route?.name + '的component属性');
}
route.children && asyncImportRoute(route.children);
});

8
src/router/routes/modules/demo/comp.ts

@ -534,6 +534,14 @@ const comp: AppRouteModule = {
title: t('routes.demo.comp.loading'),
},
},
{
path: 'cardList',
name: 'CardListDemo',
component: () => import('/@/views/demo/comp/card-list/index.vue'),
meta: {
title: t('routes.demo.comp.cardList'),
},
},
],
};

45
src/store/modules/multipleTab.ts

@ -26,6 +26,15 @@ function handleGotoPage(router: Router) {
go(unref(router.currentRoute).path, true);
}
const getToTarget = (tabItem: RouteLocationNormalized) => {
const { params, path, query } = tabItem;
return {
params: params || {},
path,
query: query || {},
};
};
const cacheTab = projectSetting.multiTabsSetting.cache;
export const useMultipleTabStore = defineStore({
@ -147,15 +156,6 @@ export const useMultipleTabStore = defineStore({
},
async closeTab(tab: RouteLocationNormalized, router: Router) {
const getToTarget = (tabItem: RouteLocationNormalized) => {
const { params, path, query } = tabItem;
return {
params: params || {},
path,
query: query || {},
};
};
const close = (route: RouteLocationNormalized) => {
const { fullPath, meta: { affix } = {} } = route;
if (affix) {
@ -196,13 +196,36 @@ export const useMultipleTabStore = defineStore({
toTarget = getToTarget(page);
}
close(currentRoute.value);
replace(toTarget);
await replace(toTarget);
},
// Close according to key
async closeTabByKey(key: string, router: Router) {
const index = this.tabList.findIndex((item) => (item.fullPath || item.path) === key);
index !== -1 && this.closeTab(this.tabList[index], router);
if (index !== -1) {
await this.closeTab(this.tabList[index], router);
const { currentRoute, replace } = router;
// 检查当前路由是否存在于tabList中
const isActivated = this.tabList.findIndex((item) => {
return item.fullPath === currentRoute.value.fullPath;
});
// 如果当前路由不存在于TabList中,尝试切换到其它路由
if (isActivated === -1) {
let pageIndex;
if (index > 0) {
pageIndex = index - 1;
} else if (index < this.tabList.length - 1) {
pageIndex = index + 1;
} else {
pageIndex = -1;
}
if (pageIndex >= 0) {
const page = this.tabList[index - 1];
const toTarget = getToTarget(page);
await replace(toTarget);
}
}
}
},
// Sort the tabs

57
src/store/modules/user.ts

@ -15,6 +15,7 @@ import { usePermissionStore } from '/@/store/modules/permission';
import { RouteRecordRaw } from 'vue-router';
import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
import { isArray } from '/@/utils/is';
import { h } from 'vue';
interface UserState {
userInfo: Nullable<UserInfo>;
@ -57,14 +58,14 @@ export const useUserStore = defineStore({
},
actions: {
setToken(info: string | undefined) {
this.token = info;
this.token = info ? info : ''; // for null or undefined value
setAuthCache(TOKEN_KEY, info);
},
setRoleList(roleList: RoleEnum[]) {
this.roleList = roleList;
setAuthCache(ROLES_KEY, roleList);
},
setUserInfo(info: UserInfo) {
setUserInfo(info: UserInfo | null) {
this.userInfo = info;
this.lastUpdateTime = new Date().getTime();
setAuthCache(USER_INFO_KEY, info);
@ -94,30 +95,35 @@ export const useUserStore = defineStore({
// save token
this.setToken(token);
// get user info
const userInfo = await this.getUserInfoAction();
const sessionTimeout = this.sessionTimeout;
if (sessionTimeout) {
this.setSessionTimeout(false);
} else if (goHome) {
const permissionStore = usePermissionStore();
if (!permissionStore.isDynamicAddedRoute) {
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
permissionStore.setDynamicAddedRoute(true);
}
await router.replace(userInfo.homePath || PageEnum.BASE_HOME);
}
return userInfo;
return this.afterLoginAction(goHome);
} catch (error) {
return Promise.reject(error);
}
},
async getUserInfoAction(): Promise<UserInfo> {
async afterLoginAction(goHome?: boolean): Promise<GetUserInfoModel | null> {
if (!this.getToken) return null;
// get user info
const userInfo = await this.getUserInfoAction();
const sessionTimeout = this.sessionTimeout;
if (sessionTimeout) {
this.setSessionTimeout(false);
} else {
const permissionStore = usePermissionStore();
if (!permissionStore.isDynamicAddedRoute) {
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
permissionStore.setDynamicAddedRoute(true);
}
goHome && (await router.replace(userInfo?.homePath || PageEnum.BASE_HOME));
}
return userInfo;
},
async getUserInfoAction(): Promise<UserInfo | null> {
if (!this.getToken) return null;
const userInfo = await getUserInfo();
const { roles = [] } = userInfo;
if (isArray(roles)) {
@ -134,7 +140,7 @@ export const useUserStore = defineStore({
* @description: logout
*/
async logout(goLogin = false) {
if (this.token) {
if (this.getToken) {
try {
await doLogout();
} catch {
@ -143,6 +149,7 @@ export const useUserStore = defineStore({
}
this.setToken(undefined);
this.setSessionTimeout(false);
this.setUserInfo(null);
goLogin && router.push(PageEnum.BASE_LOGIN);
},
@ -154,8 +161,8 @@ export const useUserStore = defineStore({
const { t } = useI18n();
createConfirm({
iconType: 'warning',
title: t('sys.app.logoutTip'),
content: t('sys.app.logoutMessage'),
title: () => h('span', t('sys.app.logoutTip')),
content: () => h('span', t('sys.app.logoutMessage')),
onOk: async () => {
await this.logout(true);
},

1
src/utils/http/axios/axiosTransform.ts

@ -6,7 +6,6 @@ import type { RequestOptions, Result } from '/#/axios';
export interface CreateAxiosOptions extends AxiosRequestConfig {
authenticationScheme?: string;
urlPrefix?: string;
transform?: AxiosTransform;
requestOptions?: RequestOptions;
}

12
src/utils/http/axios/index.ts

@ -15,6 +15,7 @@ import { setObjToUrlParams, deepMerge } from '/@/utils';
import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
import { useI18n } from '/@/hooks/web/useI18n';
import { joinTimestamp, formatRequestDate } from './helper';
import { useUserStoreWithOut } from '/@/store/modules/user';
const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix;
@ -61,6 +62,9 @@ const transform: AxiosTransform = {
switch (code) {
case ResultEnum.TIMEOUT:
timeoutMsg = t('sys.api.timeoutMessage');
const userStore = useUserStoreWithOut();
userStore.setToken(undefined);
userStore.logout(true);
break;
default:
if (message) {
@ -81,7 +85,7 @@ const transform: AxiosTransform = {
// 请求之前处理config
beforeRequestHook: (config, options) => {
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true } = options;
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
if (joinPrefix) {
config.url = `${urlPrefix}${config.url}`;
@ -199,8 +203,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
timeout: 10 * 1000,
// 基础接口地址
// baseURL: globSetting.apiUrl,
// 接口可能会有通用的地址部分,可以统一抽取出来
urlPrefix: urlPrefix,
headers: { 'Content-Type': ContentTypeEnum.JSON },
// 如果是form-data格式
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
@ -222,6 +225,8 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
errorMessageMode: 'message',
// 接口地址
apiUrl: globSetting.apiUrl,
// 接口拼接地址
urlPrefix: urlPrefix,
// 是否加入时间戳
joinTime: true,
// 忽略重复请求
@ -240,5 +245,6 @@ export const defHttp = createAxios();
// export const otherHttp = createAxios({
// requestOptions: {
// apiUrl: 'xxx',
// urlPrefix: 'xxx',
// },
// });

3
src/views/demo/charts/SaleRadar.vue

@ -4,8 +4,9 @@
</Card>
</template>
<script lang="ts">
import { defineComponent, Ref, ref, watch } from 'vue';
import type { Ref } from 'vue';
import { defineComponent, ref, watch } from 'vue';
import { Card } from 'ant-design-vue';
import { useECharts } from '/@/hooks/web/useECharts';

32
src/views/demo/comp/card-list/index.vue

@ -0,0 +1,32 @@
<template>
<PageWrapper title="卡片列表示例" content="基础封装">
<CardList :params="params" :api="demoListApi" @getMethod="getMethod" @delete="handleDel">
<template #header>
<Button type="primary" color="error"> 按钮1 </Button>
<Button type="primary" color="success"> 按钮2 </Button>
</template>
</CardList>
</PageWrapper>
</template>
<script lang="ts" setup>
import { CardList } from '/@/components/CardList';
import { Button } from '/@/components/Button';
import { PageWrapper } from '/@/components/Page';
import { demoListApi } from '/@/api/demo/table';
import { useMessage } from '/@/hooks/web/useMessage';
const { notification } = useMessage();
// 请求api时附带参数
const params = {};
let reload = () => {};
// 获取内部fetch方法;
function getMethod(m: any) {
reload = m;
}
//删除按钮事件
function handleDel(id) {
console.log(id);
notification.success({ message: `成功删除${id}` });
reload();
}
</script>

1
src/views/demo/comp/upload/index.vue

@ -7,6 +7,7 @@
@change="handleChange"
:api="uploadApi"
class="my-5"
:accept="['image/*']"
/>
<a-alert message="嵌入表单,加入表单校验" />

21
src/views/demo/editor/json/index.vue

@ -14,8 +14,8 @@
</PageWrapper>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { CodeEditor } from '/@/components/CodeEditor';
import { defineComponent, ref, unref, h } from 'vue';
import { CodeEditor, JsonPreview, MODE } from '/@/components/CodeEditor';
import { PageWrapper } from '/@/components/Page';
import { Radio, Space, Modal } from 'ant-design-vue';
@ -62,27 +62,34 @@
ASpace: Space,
},
setup() {
const modeValue = ref('application/json');
const modeValue = ref<MODE>(MODE.JSON);
const value = ref(jsonData);
function handleModeChange(e: ChangeEvent) {
const mode = e.target.value;
if (mode === 'application/json') {
if (mode === MODE.JSON) {
value.value = jsonData;
return;
}
if (mode === 'htmlmixed') {
if (mode === MODE.HTML) {
value.value = htmlData;
return;
}
if (mode === 'javascript') {
if (mode === MODE.JS) {
value.value = jsData;
return;
}
}
function showData() {
Modal.info({ title: '编辑器当前值', content: value.value });
if (unref(modeValue) === 'application/json') {
Modal.info({
title: '编辑器当前值',
content: h(JsonPreview, { data: JSON.parse(value.value) }),
});
} else {
Modal.info({ title: '编辑器当前值', content: value.value });
}
}
return { value, modeValue, handleModeChange, showData };

28
src/views/demo/editor/markdown/index.vue

@ -1,22 +1,30 @@
<template>
<PageWrapper title="MarkDown组件示例">
<a-button @click="toggleTheme" class="mb-2" type="primary"> 黑暗主题 </a-button>
<a-button @click="clearValue" class="mb-2" type="default"> 清空内容 </a-button>
<MarkDown
v-model:value="value"
@change="handleChange"
ref="markDownRef"
placeholder="这是占位文本"
/>
<div>
<a-button @click="toggleTheme" class="mb-2" type="primary"> 黑暗主题 </a-button>
<a-button @click="clearValue" class="mb-2" type="default"> 清空内容 </a-button>
<MarkDown
v-model:value="value"
@change="handleChange"
ref="markDownRef"
placeholder="这是占位文本"
/>
</div>
<div class="mt-2">
<a-card title="Markdown Viewer 组件演示">
<MarkdownViewer :value="value" />
</a-card>
</div>
</PageWrapper>
</template>
<script lang="ts">
import { defineComponent, ref, unref } from 'vue';
import { MarkDown, MarkDownActionType } from '/@/components/Markdown';
import { MarkDown, MarkDownActionType, MarkdownViewer } from '/@/components/Markdown';
import { PageWrapper } from '/@/components/Page';
import { Card } from 'ant-design-vue';
export default defineComponent({
components: { MarkDown, PageWrapper },
components: { MarkDown, PageWrapper, MarkdownViewer, ACard: Card },
setup() {
const markDownRef = ref<Nullable<MarkDownActionType>>(null);
const valueRef = ref(`

2
src/views/demo/excel/ImportExcel.vue

@ -1,6 +1,6 @@
<template>
<PageWrapper title="excel数据导入示例">
<ImpExcel @success="loadDataSuccess">
<ImpExcel @success="loadDataSuccess" dateFormat="YYYY-MM-DD">
<a-button class="m-3"> 导入Excel </a-button>
</ImpExcel>
<BasicTable

32
src/views/demo/feat/session-timeout/index.vue

@ -3,7 +3,15 @@
title="登录过期示例"
content="用户登录过期示例,不再跳转登录页,直接生成页面覆盖当前页面,方便保持过期前的用户状态!"
>
<a-button type="primary" @click="test">点击触发用户登录过期</a-button>
<a-card title="请点击下面的按钮访问测试接口" extra="所访问的接口会返回Token过期响应">
<a-card-grid style="width: 50%; text-align: center">
<a-button type="primary" @click="test1">HttpStatus == 401</a-button>
</a-card-grid>
<a-card-grid style="width: 50%; text-align: center">
<span></span>
<a-button class="ml-4" type="primary" @click="test2">Response.code == 401</a-button>
</a-card-grid>
</a-card>
</PageWrapper>
</template>
<script lang="ts">
@ -11,24 +19,36 @@
import { PageWrapper } from '/@/components/Page';
import { useUserStore } from '/@/store/modules/user';
import { sessionTimeoutApi } from '/@/api/demo/account';
import { sessionTimeoutApi, tokenExpiredApi } from '/@/api/demo/account';
import { Card } from 'ant-design-vue';
export default defineComponent({
name: 'TestSessionTimeout',
components: { PageWrapper },
components: { ACardGrid: Card.Grid, ACard: Card, PageWrapper },
setup() {
const userStore = useUserStore();
async function test() {
// 示例网站生产环境用得是mock数据,所以不能返回401,
async function test1() {
// 示例网站生产环境用的是mock数据,不能返回Http状态码,
// 所以在生产环境直接改变状态来达到测试效果
if (import.meta.env.PROD) {
userStore.setToken(undefined);
userStore.setSessionTimeout(true);
} else {
// 这个api会返回状态码为401的响应
await sessionTimeoutApi();
}
}
return { test };
async function test2() {
// 这个api会返回code为401的json数据,Http状态码为200
try {
await tokenExpiredApi();
} catch (err) {
console.log('接口访问错误:', (err as Error).message || '错误');
}
}
return { test1, test2 };
},
});
</script>

2
src/views/demo/setup/index.vue

@ -8,7 +8,7 @@
import { PageWrapper } from '/@/components/Page';
import { useDesign } from '/@/hooks/web/useDesign';
import intro from 'intro.js';
import 'intro.js/introjs.css';
import 'intro.js/minified/introjs.min.css';
export default defineComponent({
components: { PageWrapper },

26
src/views/demo/table/EditRowTable.vue

@ -21,6 +21,8 @@
import { demoListApi } from '/@/api/demo/table';
import { treeOptionsListApi } from '/@/api/demo/tree';
import { cloneDeep } from 'lodash-es';
import { useMessage } from '/@/hooks/web/useMessage';
const columns: BasicColumn[] = [
{
@ -162,6 +164,7 @@
export default defineComponent({
components: { BasicTable, TableAction },
setup() {
const { createMessage: msg } = useMessage();
const currentEditKeyRef = ref('');
const [registerTable] = useTable({
title: '可编辑行示例',
@ -192,9 +195,26 @@
}
async function handleSave(record: EditRecordRow) {
const pass = await record.onEdit?.(false, true);
if (pass) {
currentEditKeyRef.value = '';
// 校验
msg.loading({ content: '正在保存...', duration: 0, key: 'saving' });
const valid = await record.onValid?.();
if (valid) {
try {
const data = cloneDeep(record.editValueRefs);
console.log(data);
//TODO 此处将数据提交给服务器保存
// ...
// 保存之后提交编辑状态
const pass = await record.onEdit?.(false, true);
if (pass) {
currentEditKeyRef.value = '';
}
msg.success({ content: '数据已保存', key: 'saving' });
} catch (error) {
msg.error({ content: '保存失败', key: 'saving' });
}
} else {
msg.error({ content: '请填写正确的数据', key: 'saving' });
}
}

12
src/views/demo/table/TreeTable.vue

@ -19,7 +19,17 @@
const [register, { expandAll, collapseAll }] = useTable({
title: '树形表格',
isTreeTable: true,
rowSelection: { type: 'checkbox' },
rowSelection: {
type: 'checkbox',
getCheckboxProps(record: Recordable) {
// Demo: 第一行(id为0)的选择框禁用
if (record.id === '0') {
return { disabled: true };
} else {
return { disabled: false };
}
},
},
titleHelpMessage: '树形组件不能和序列号列同时存在',
columns: getBasicColumns(),
dataSource: getTreeTableData(),

10
tests/server/package.json

@ -11,7 +11,7 @@
},
"dependencies": {
"fs-extra": "^10.0.0",
"koa": "^2.13.1",
"koa": "^2.13.3",
"koa-body": "^4.2.0",
"koa-bodyparser": "^4.3.0",
"koa-route": "^3.2.0",
@ -24,13 +24,13 @@
"@types/koa": "^2.13.4",
"@types/koa-bodyparser": "^5.0.2",
"@types/koa-router": "^7.4.4",
"@types/node": "^16.7.1",
"nodemon": "^2.0.12",
"@types/node": "^16.9.6",
"nodemon": "^2.0.13",
"pm2": "^5.1.1",
"rimraf": "^3.0.2",
"ts-node": "^10.2.1",
"tsconfig-paths": "^3.11.0",
"tsup": "^4.14.0",
"typescript": "^4.3.5"
"tsup": "^5.1.0",
"typescript": "^4.4.3"
}
}

2
types/axios.d.ts

@ -14,6 +14,8 @@ export interface RequestOptions {
joinPrefix?: boolean;
// Interface address, use the default apiUrl if you leave it blank
apiUrl?: string;
// 请求拼接路径
urlPrefix?: string;
// Error message prompt type
errorMessageMode?: ErrorMessageMode;
// Whether to add a timestamp

8293
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save