Browse Source

refactor: use js-sdk/admin-api (#378)

* 1.3.0-beta.2

* fix: token expire.

* pref: #291

* refactor: use js-sdk/admin-api

* chore: remove unnecessary files

* chore: remove unnecessary files

* chore: remove unnecessary files

* chore: remove unnecessary files

* refactor: developer/Environment.vue

* chore: remove unnecessary files

* refactor: login auth

* refactor: login auth

* refactor: remove api url setting

* refactor: custom sheet list

* style: reformat code

* refactor: logout

* refactor: remove setTimeout when fetch api

* fix: auto login error

* fix: post update error

* fix: backup

* fix: turn on developer mode error

* fix: mfa setting error

* chore: remove unnecessary files

* feat: add interceptors

* refactor: api client

* refactor: api client

* chore(deps): upgrade admin-api

* refactor: 重构认证

* fix: 修复认证请求头参数

* refactor: login

* feat: add error handle

* refactor: api client

* refactor: refresh token

* refactor: attachment upload

* refactor: upload component

* refactor: upload

* fix: tag save

* fix: github api request

* fix: installation page

* feat: add version field for html

* fix: option.list to option.listAsMapView

* fix: directory base path of static storage

* chore: upgrade halo sdk version

Co-authored-by: guqing <1484563614@qq.com>
pull/381/head
Ryan Wang 3 years ago committed by GitHub
parent
commit
48d145f053
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .env
  2. 1
      .env.development
  3. 1
      .env.jsdelivr
  4. 5
      .gitignore
  5. 3
      package.json
  6. 84
      pnpm-lock.yaml
  7. 2
      public/index.html
  8. 2
      src/App.vue
  9. 84
      src/api/actuator.js
  10. 117
      src/api/admin.js
  11. 127
      src/api/attachment.js
  12. 125
      src/api/backup.js
  13. 85
      src/api/category.js
  14. 140
      src/api/comment.js
  15. 54
      src/api/journal.js
  16. 22
      src/api/journalComment.js
  17. 61
      src/api/link.js
  18. 91
      src/api/log.js
  19. 15
      src/api/mail.js
  20. 92
      src/api/menu.js
  21. 15
      src/api/migrate.js
  22. 79
      src/api/option.js
  23. 45
      src/api/photo.js
  24. 154
      src/api/post.js
  25. 66
      src/api/postComment.js
  26. 110
      src/api/sheet.js
  27. 78
      src/api/static.js
  28. 21
      src/api/statistics.js
  29. 50
      src/api/tag.js
  30. 195
      src/api/theme.js
  31. 65
      src/api/user.js
  32. 46
      src/components/Attachment/AttachmentSelectDrawer.vue
  33. 39
      src/components/Attachment/AttachmentUploadModal.vue
  34. 10
      src/components/Button/ReactiveButton.vue
  35. 6
      src/components/Editor/MarkdownEditor.vue
  36. 2
      src/components/Editor/RichTextEditor.vue
  37. 26
      src/components/Ellipsis/Ellipsis.vue
  38. 1
      src/components/GlobalFooter/index.js
  39. 13
      src/components/GlobalHeader/GlobalHeader.vue
  40. 1
      src/components/GlobalHeader/index.js
  41. 15
      src/components/Login/LoginForm.vue
  42. 7
      src/components/Login/LoginModal.vue
  43. 10
      src/components/Menu/SideMenu.vue
  44. 1
      src/components/Menu/index.js
  45. 6
      src/components/Post/MetaEditor.vue
  46. 55
      src/components/SettingDrawer/SettingDrawer.vue
  47. 1
      src/components/SettingDrawer/index.js
  48. 2
      src/components/SettingDrawer/setting.js
  49. 4
      src/components/Tools/HeadInfo.vue
  50. 44
      src/components/Tools/HeaderComment.vue
  51. 28
      src/components/Tools/Logo.vue
  52. 29
      src/components/Tools/UserMenu.vue
  53. 47
      src/components/Upload/FilePondUpload.vue
  54. 33
      src/components/_util/util.js
  55. 2
      src/components/index.js
  56. 2
      src/config/defaultSettings.js
  57. 2
      src/config/router.config.js
  58. 18
      src/core/bootstrap.js
  59. 28
      src/core/lazy_lib/components_use.js
  60. 33
      src/layouts/BasicLayout.vue
  61. 23
      src/layouts/PageView.vue
  62. 3
      src/mixins/mixin.js
  63. 10
      src/router/guard/permissionGuard.js
  64. 2
      src/router/index.js
  65. 7
      src/store/getters.js
  66. 2
      src/store/index.js
  67. 40
      src/store/modules/app.js
  68. 9
      src/store/modules/option.js
  69. 60
      src/store/modules/permission.js
  70. 24
      src/store/modules/user.js
  71. 3
      src/store/mutation-types.js
  72. 2
      src/styles/global.less
  73. 1
      src/styles/style.less
  74. 117
      src/utils/api-client.js
  75. 1
      src/utils/datetime.js
  76. 2
      src/utils/domUtil.js
  77. 20
      src/utils/encrypt.js
  78. 145
      src/utils/service.js
  79. 2
      src/utils/util.js
  80. 87
      src/views/attachment/AttachmentList.vue
  81. 71
      src/views/attachment/components/AttachmentDetailModal.vue
  82. 85
      src/views/attachment/components/AttachmentDrawer.vue
  83. 1
      src/views/comment/CommentList.vue
  84. 131
      src/views/comment/components/CommentDetail.vue
  85. 278
      src/views/comment/components/CommentTab.vue
  86. 57
      src/views/comment/components/TargetCommentDrawer.vue
  87. 20
      src/views/comment/components/TargetCommentTree.vue
  88. 150
      src/views/dashboard/Dashboard.vue
  89. 7
      src/views/dashboard/components/AnalysisCard.vue
  90. 25
      src/views/dashboard/components/JournalPublishCard.vue
  91. 98
      src/views/dashboard/components/LogListDrawer.vue
  92. 29
      src/views/dashboard/components/RecentCommentTab.vue
  93. 2
      src/views/exception/ExceptionPage.vue
  94. 86
      src/views/interface/MenuList.vue
  95. 18
      src/views/interface/ThemeEdit.vue
  96. 152
      src/views/interface/ThemeList.vue
  97. 39
      src/views/interface/components/MenuForm.vue
  98. 77
      src/views/interface/components/MenuInternalLinkSelector.vue
  99. 53
      src/views/interface/components/MenuTreeNode.vue
  100. 123
      src/views/interface/components/ThemeSettingDrawer.vue
  101. Some files were not shown because too many files have changed in this diff Show More

1
.env

@ -1,2 +1,3 @@
NODE_ENV=production NODE_ENV=production
PUBLIC_PATH=/ PUBLIC_PATH=/
VUE_APP_API_URL=/

1
.env.development

@ -1,2 +1,3 @@
NODE_ENV=development NODE_ENV=development
PUBLIC_PATH=/ PUBLIC_PATH=/
VUE_APP_API_URL=http://localhost:8090

1
.env.jsdelivr

@ -1,2 +1,3 @@
NODE_ENV=production NODE_ENV=production
PUBLIC_PATH=https://cdn.jsdelivr.net/npm/halo-admin@1.4.13/dist/ PUBLIC_PATH=https://cdn.jsdelivr.net/npm/halo-admin@1.4.13/dist/
VUE_APP_API_URL=/

5
.gitignore vendored

@ -21,3 +21,8 @@ pnpm-debug.log*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# NodeJs package manager
yarn.lock
package-lock.json

3
package.json

@ -24,8 +24,9 @@
"@codemirror/basic-setup": "^0.19.0", "@codemirror/basic-setup": "^0.19.0",
"@codemirror/lang-html": "^0.19.3", "@codemirror/lang-html": "^0.19.3",
"@codemirror/lang-java": "^0.19.1", "@codemirror/lang-java": "^0.19.1",
"@halo-dev/admin-api": "^1.0.0-alpha.44",
"ant-design-vue": "^1.7.8", "ant-design-vue": "^1.7.8",
"axios": "^0.21.4", "crypto-js": "^4.1.1",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"enquire.js": "^2.1.6", "enquire.js": "^2.1.6",
"filepond": "^4.30.3", "filepond": "^4.30.3",

84
pnpm-lock.yaml

@ -5,6 +5,7 @@ specifiers:
'@codemirror/basic-setup': ^0.19.0 '@codemirror/basic-setup': ^0.19.0
'@codemirror/lang-html': ^0.19.3 '@codemirror/lang-html': ^0.19.3
'@codemirror/lang-java': ^0.19.1 '@codemirror/lang-java': ^0.19.1
'@halo-dev/admin-api': ^1.0.0-alpha.44
'@vue/cli-plugin-babel': ^3.12.1 '@vue/cli-plugin-babel': ^3.12.1
'@vue/cli-plugin-eslint': ^4.5.15 '@vue/cli-plugin-eslint': ^4.5.15
'@vue/cli-plugin-unit-jest': ^4.5.15 '@vue/cli-plugin-unit-jest': ^4.5.15
@ -12,11 +13,11 @@ specifiers:
'@vue/eslint-config-prettier': ^6.0.0 '@vue/eslint-config-prettier': ^6.0.0
'@vue/test-utils': ^1.2.2 '@vue/test-utils': ^1.2.2
ant-design-vue: ^1.7.8 ant-design-vue: ^1.7.8
axios: ^0.21.4
babel-core: 7.0.0-bridge.0 babel-core: 7.0.0-bridge.0
babel-eslint: ^10.1.0 babel-eslint: ^10.1.0
babel-jest: ^26.6.3 babel-jest: ^26.6.3
babel-plugin-import: ^1.13.3 babel-plugin-import: ^1.13.3
crypto-js: ^4.1.1
dayjs: ^1.10.7 dayjs: ^1.10.7
enquire.js: ^2.1.6 enquire.js: ^2.1.6
eslint: ^6.8.0 eslint: ^6.8.0
@ -53,8 +54,9 @@ dependencies:
'@codemirror/basic-setup': 0.19.0 '@codemirror/basic-setup': 0.19.0
'@codemirror/lang-html': 0.19.3 '@codemirror/lang-html': 0.19.3
'@codemirror/lang-java': 0.19.1 '@codemirror/lang-java': 0.19.1
'@halo-dev/admin-api': 1.0.0-alpha.44
ant-design-vue: 1.7.8_9065e7474e033a8e4b95615fc8e6c36c ant-design-vue: 1.7.8_9065e7474e033a8e4b95615fc8e6c36c
axios: 0.21.4 crypto-js: 4.1.1
dayjs: 1.10.7 dayjs: 1.10.7
enquire.js: 2.1.6 enquire.js: 2.1.6
filepond: 4.30.3 filepond: 4.30.3
@ -1346,6 +1348,36 @@ packages:
purgecss: 2.3.0 purgecss: 2.3.0
dev: true dev: true
/@halo-dev/admin-api/1.0.0-alpha.44:
resolution: {integrity: sha512-nCJsx4gDxCjkoGJKsBpcgLhAUc8RYrqKMGM1VSi9t64xTFdXDjIJgtcIQuBgJ0b4R58JIp6A6ti5S764vz4BDA==}
engines: {node: '>=12'}
dependencies:
'@halo-dev/rest-api-client': 1.0.0-alpha.44
transitivePeerDependencies:
- debug
dev: false
/@halo-dev/logger/1.0.0-alpha.44:
resolution: {integrity: sha512-ORHP6pj8wLb+mwsk+pYqvH9tqNWTr+96AiZgtSMcdwojA1KupFSVzHN0aqpk8HeGfSrBNbMz4VT6ey+OVnWrcQ==}
engines: {node: '>=12.0.0'}
dependencies:
tslib: 2.3.1
dev: false
/@halo-dev/rest-api-client/1.0.0-alpha.44:
resolution: {integrity: sha512-fCzh7ihLpI7hrq0S2M9YjROTvoTT5JeEOqv5O/hon4bTfpBqEb7REjjYMONR8lJNgmEI9wzviEhn7Xxg7nX/vA==}
engines: {node: '>=12'}
dependencies:
'@halo-dev/logger': 1.0.0-alpha.44
axios: 0.24.0
form-data: 4.0.0
js-base64: 3.7.2
qs: 6.10.1
store: 2.0.12
transitivePeerDependencies:
- debug
dev: false
/@hapi/address/2.1.4: /@hapi/address/2.1.4:
resolution: {integrity: sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==} resolution: {integrity: sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==}
deprecated: Moved to 'npm install @sideway/address' deprecated: Moved to 'npm install @sideway/address'
@ -2787,7 +2819,6 @@ packages:
/asynckit/0.4.0: /asynckit/0.4.0:
resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=}
dev: true
/atob/2.1.2: /atob/2.1.2:
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
@ -2816,8 +2847,8 @@ packages:
resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==} resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==}
dev: true dev: true
/axios/0.21.4: /axios/0.24.0:
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==}
dependencies: dependencies:
follow-redirects: 1.14.4 follow-redirects: 1.14.4
transitivePeerDependencies: transitivePeerDependencies:
@ -3453,7 +3484,6 @@ packages:
dependencies: dependencies:
function-bind: 1.1.1 function-bind: 1.1.1
get-intrinsic: 1.1.1 get-intrinsic: 1.1.1
dev: true
/call-me-maybe/1.0.1: /call-me-maybe/1.0.1:
resolution: {integrity: sha1-JtII6onje1y95gJQoV8DHBak1ms=} resolution: {integrity: sha1-JtII6onje1y95gJQoV8DHBak1ms=}
@ -3824,7 +3854,6 @@ packages:
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
dev: true
/commander/2.17.1: /commander/2.17.1:
resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==} resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==}
@ -4128,6 +4157,10 @@ packages:
randomfill: 1.0.4 randomfill: 1.0.4
dev: true dev: true
/crypto-js/4.1.1:
resolution: {integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==}
dev: false
/css-color-names/0.0.4: /css-color-names/0.0.4:
resolution: {integrity: sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=} resolution: {integrity: sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=}
dev: true dev: true
@ -4508,7 +4541,6 @@ packages:
/delayed-stream/1.0.0: /delayed-stream/1.0.0:
resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=} resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
dev: true
/delegate/3.2.0: /delegate/3.2.0:
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==} resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
@ -5537,6 +5569,15 @@ packages:
mime-types: 2.1.32 mime-types: 2.1.32
dev: true dev: true
/form-data/4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.32
dev: false
/forwarded/0.2.0: /forwarded/0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -5614,7 +5655,6 @@ packages:
/function-bind/1.1.1: /function-bind/1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
/functional-red-black-tree/1.0.1: /functional-red-black-tree/1.0.1:
resolution: {integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=} resolution: {integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=}
@ -5636,7 +5676,6 @@ packages:
function-bind: 1.1.1 function-bind: 1.1.1
has: 1.0.3 has: 1.0.3
has-symbols: 1.0.2 has-symbols: 1.0.2
dev: true
/get-own-enumerable-property-symbols/3.0.2: /get-own-enumerable-property-symbols/3.0.2:
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
@ -5844,7 +5883,6 @@ packages:
/has-symbols/1.0.2: /has-symbols/1.0.2:
resolution: {integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==} resolution: {integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dev: true
/has-tostringtag/1.0.0: /has-tostringtag/1.0.0:
resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
@ -5889,7 +5927,6 @@ packages:
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
dependencies: dependencies:
function-bind: 1.1.1 function-bind: 1.1.1
dev: true
/hash-base/3.1.0: /hash-base/3.1.0:
resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==}
@ -7248,6 +7285,10 @@ packages:
- supports-color - supports-color
dev: true dev: true
/js-base64/3.7.2:
resolution: {integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==}
dev: false
/js-beautify/1.14.0: /js-beautify/1.14.0:
resolution: {integrity: sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ==} resolution: {integrity: sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -7886,7 +7927,6 @@ packages:
/mime-db/1.49.0: /mime-db/1.49.0:
resolution: {integrity: sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==} resolution: {integrity: sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: true
/mime-db/1.50.0: /mime-db/1.50.0:
resolution: {integrity: sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==} resolution: {integrity: sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==}
@ -7898,7 +7938,6 @@ packages:
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dependencies: dependencies:
mime-db: 1.49.0 mime-db: 1.49.0
dev: true
/mime/1.6.0: /mime/1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
@ -8320,7 +8359,6 @@ packages:
/object-inspect/1.11.0: /object-inspect/1.11.0:
resolution: {integrity: sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==} resolution: {integrity: sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==}
dev: true
/object-is/1.1.5: /object-is/1.1.5:
resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
@ -9365,6 +9403,13 @@ packages:
engines: {node: '>=0.6.0', teleport: '>=0.2.0'} engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
dev: true dev: true
/qs/6.10.1:
resolution: {integrity: sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.4
dev: false
/qs/6.5.2: /qs/6.5.2:
resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==} resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
@ -10024,7 +10069,6 @@ packages:
call-bind: 1.0.2 call-bind: 1.0.2
get-intrinsic: 1.1.1 get-intrinsic: 1.1.1
object-inspect: 1.11.0 object-inspect: 1.11.0
dev: true
/sigmund/1.0.1: /sigmund/1.0.1:
resolution: {integrity: sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=} resolution: {integrity: sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=}
@ -10307,6 +10351,10 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/store/2.0.12:
resolution: {integrity: sha1-jFNOKguDH3K3X8XxEZhXxE711ZM=}
dev: false
/stream-browserify/2.0.2: /stream-browserify/2.0.2:
resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==}
dependencies: dependencies:
@ -10859,6 +10907,10 @@ packages:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true dev: true
/tslib/2.3.1:
resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==}
dev: false
/tty-browserify/0.0.0: /tty-browserify/0.0.0:
resolution: {integrity: sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=} resolution: {integrity: sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=}
dev: true dev: true

2
public/index.html

@ -7,7 +7,7 @@
<meta name="renderer" content="webkit"> <meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="robots" content="noindex,nofollow" /> <meta name="robots" content="noindex,nofollow" />
<meta name="generator" content="Halo 1.4.13" /> <meta name="generator" content="Halo <%= htmlWebpackPlugin.options.version %>" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<title>Halo Dashboard</title> <title>Halo Dashboard</title>
<style> <style>

2
src/App.vue

@ -8,7 +8,7 @@
<script> <script>
import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN' import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import { deviceEnquire, DEVICE_TYPE } from '@/utils/device' import { DEVICE_TYPE, deviceEnquire } from '@/utils/device'
export default { export default {
data() { data() {

84
src/api/actuator.js

@ -1,84 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/actuator'
const actuatorApi = {}
actuatorApi.logfile = () => {
return service({
url: `${baseUrl}/logfile`,
method: 'get'
})
}
actuatorApi.env = () => {
return service({
url: `${baseUrl}/env`,
method: 'get'
})
}
actuatorApi.getSystemCpuCount = () => {
return service({
url: `${baseUrl}/metrics/system.cpu.count`,
method: 'get'
})
}
actuatorApi.getSystemCpuUsage = () => {
return service({
url: `${baseUrl}/metrics/system.cpu.usage`,
method: 'get'
})
}
actuatorApi.getProcessUptime = () => {
return service({
url: `${baseUrl}/metrics/process.uptime`,
method: 'get'
})
}
actuatorApi.getProcessStartTime = () => {
return service({
url: `${baseUrl}/metrics/process.start.time`,
method: 'get'
})
}
actuatorApi.getProcessCpuUsage = () => {
return service({
url: `${baseUrl}/metrics/process.cpu.usage`,
method: 'get'
})
}
actuatorApi.getJvmMemoryMax = () => {
return service({
url: `${baseUrl}/metrics/jvm.memory.max`,
method: 'get'
})
}
actuatorApi.getJvmMemoryCommitted = () => {
return service({
url: `${baseUrl}/metrics/jvm.memory.committed`,
method: 'get'
})
}
actuatorApi.getJvmMemoryUsed = () => {
return service({
url: `${baseUrl}/metrics/jvm.memory.used`,
method: 'get'
})
}
actuatorApi.getJvmGcPause = () => {
return service({
url: `${baseUrl}/metrics/jvm.gc.pause`,
method: 'get'
})
}
export default actuatorApi

117
src/api/admin.js

@ -1,117 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin'
const adminApi = {}
adminApi.counts = () => {
return service({
url: `${baseUrl}/counts`,
method: 'get'
})
}
adminApi.isInstalled = () => {
return service({
url: `${baseUrl}/is_installed`,
method: 'get'
})
}
adminApi.environments = () => {
return service({
url: `${baseUrl}/environments`,
method: 'get'
})
}
adminApi.install = data => {
return service({
url: `${baseUrl}/installations`,
data: data,
method: 'post'
})
}
adminApi.loginPreCheck = (username, password) => {
return service({
url: `${baseUrl}/login/precheck`,
data: {
username: username,
password: password
},
method: 'post'
})
}
adminApi.login = (username, password, authcode) => {
return service({
url: `${baseUrl}/login`,
data: {
username: username,
password: password,
authcode: authcode
},
method: 'post'
})
}
adminApi.logout = () => {
return service({
url: `${baseUrl}/logout`,
method: 'post'
})
}
adminApi.refreshToken = refreshToken => {
return service({
url: `${baseUrl}/refresh/${refreshToken}`,
method: 'post'
})
}
adminApi.sendResetCode = param => {
return service({
url: `${baseUrl}/password/code`,
data: param,
method: 'post'
})
}
adminApi.resetPassword = param => {
return service({
url: `${baseUrl}/password/reset`,
data: param,
method: 'put'
})
}
adminApi.updateAdminAssets = () => {
return service({
url: `${baseUrl}/halo-admin`,
method: 'put',
timeout: 600 * 1000
})
}
adminApi.getLogFiles = lines => {
return service({
url: `${baseUrl}/halo/logfile`,
params: {
lines: lines
},
method: 'get'
})
}
adminApi.downloadLogFiles = lines => {
return service({
url: `${baseUrl}/halo/logfile/download`,
params: {
lines: lines
},
method: 'get'
})
}
export default adminApi

127
src/api/attachment.js

@ -1,127 +0,0 @@
import axios from 'axios'
import service from '@/utils/service'
const baseUrl = '/api/admin/attachments'
const attachmentApi = {}
attachmentApi.query = params => {
return service({
url: baseUrl,
params: params,
method: 'get'
})
}
attachmentApi.get = attachmentId => {
return service({
url: `${baseUrl}/${attachmentId}`,
method: 'get'
})
}
attachmentApi.delete = attachmentId => {
return service({
url: `${baseUrl}/${attachmentId}`,
method: 'delete'
})
}
attachmentApi.deleteInBatch = attachmentIds => {
return service({
url: `${baseUrl}`,
method: 'delete',
data: attachmentIds,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
attachmentApi.update = (attachmentId, attachment) => {
return service({
url: `${baseUrl}/${attachmentId}`,
method: 'put',
data: attachment
})
}
attachmentApi.getMediaTypes = () => {
return service({
url: `${baseUrl}/media_types`,
method: 'get'
})
}
attachmentApi.getTypes = () => {
return service({
url: `${baseUrl}/types`,
method: 'get'
})
}
attachmentApi.CancelToken = axios.CancelToken
attachmentApi.isCancel = axios.isCancel
attachmentApi.upload = (formData, uploadProgress, cancelToken) => {
return service({
url: `${baseUrl}/upload`,
timeout: 8640000, // 24 hours
data: formData, // form data
onUploadProgress: uploadProgress,
cancelToken: cancelToken,
method: 'post'
})
}
attachmentApi.uploads = (formDatas, uploadProgress, cancelToken) => {
return service({
url: `${baseUrl}/uploads`,
timeout: 8640000, // 24 hours
data: formDatas, // form data
onUploadProgress: uploadProgress,
cancelToken: cancelToken,
method: 'post'
})
}
attachmentApi.type = {
LOCAL: {
type: 'LOCAL',
text: '本地'
},
SMMS: {
type: 'SMMS',
text: 'SM.MS'
},
UPOSS: {
type: 'UPOSS',
text: '又拍云'
},
QINIUOSS: {
type: 'QINIUOSS',
text: '七牛云'
},
ALIOSS: {
type: 'ALIOSS',
text: '阿里云'
},
BAIDUBOS: {
type: 'BAIDUBOS',
text: '百度云'
},
TENCENTCOS: {
type: 'TENCENTCOS',
text: '腾讯云'
},
HUAWEIOBS: {
type: 'HUAWEIOBS',
text: '华为云'
},
MINIO: {
type: 'MINIO',
text: 'MinIO'
}
}
export default attachmentApi

125
src/api/backup.js

@ -1,125 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/backups'
const backupApi = {}
backupApi.importMarkdown = (formData, uploadProgress, cancelToken) => {
return service({
url: `${baseUrl}/markdown/import`,
timeout: 8640000, // 24 hours
data: formData, // form data
onUploadProgress: uploadProgress,
cancelToken: cancelToken,
method: 'post'
})
}
backupApi.backupWorkDir = options => {
return service({
url: `${baseUrl}/work-dir`,
method: 'post',
data: options,
timeout: 8640000 // 24 hours
})
}
backupApi.listWorkDirOptions = () => {
return service({
url: `${baseUrl}/work-dir/options`,
method: 'get'
})
}
backupApi.listWorkDirBackups = () => {
return service({
url: `${baseUrl}/work-dir`,
method: 'get'
})
}
backupApi.fetchWorkDir = filename => {
return service({
url: `${baseUrl}/work-dir/fetch?filename=${filename}`,
method: 'get'
})
}
backupApi.deleteWorkDirBackup = filename => {
return service({
url: `${baseUrl}/work-dir`,
params: {
filename: filename
},
method: 'delete'
})
}
backupApi.exportData = () => {
return service({
url: `${baseUrl}/data`,
method: 'post',
timeout: 8640000 // 24 hours
})
}
backupApi.listExportedData = () => {
return service({
url: `${baseUrl}/data`,
method: 'get'
})
}
backupApi.fetchData = filename => {
return service({
url: `${baseUrl}/data/fetch?filename=${filename}`,
method: 'get'
})
}
backupApi.deleteExportedData = filename => {
return service({
url: `${baseUrl}/data`,
params: {
filename: filename
},
method: 'delete'
})
}
backupApi.exportMarkdowns = needFrontMatter => {
return service({
url: `${baseUrl}/markdown/export`,
method: 'post',
data: {
needFrontMatter: needFrontMatter
},
timeout: 8640000 // 24 hours
})
}
backupApi.listExportedMarkdowns = () => {
return service({
url: `${baseUrl}/markdown/export`,
method: 'get'
})
}
backupApi.fetchMarkdown = filename => {
return service({
url: `${baseUrl}/markdown/fetch?filename=${filename}`,
method: 'get'
})
}
backupApi.deleteExportedMarkdown = filename => {
return service({
url: `${baseUrl}/markdown/export`,
params: {
filename: filename
},
method: 'delete'
})
}
export default backupApi

85
src/api/category.js

@ -1,85 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/categories'
const categoryApi = {}
categoryApi.listAll = (more = false) => {
return service({
url: `${baseUrl}`,
params: {
more: more
},
method: 'get'
})
}
categoryApi.listTree = () => {
return service({
url: `${baseUrl}/tree_view`,
method: 'get'
})
}
categoryApi.create = category => {
return service({
url: baseUrl,
data: category,
method: 'post'
})
}
categoryApi.delete = categoryId => {
return service({
url: `${baseUrl}/${categoryId}`,
method: 'delete'
})
}
categoryApi.get = categoryId => {
return service({
url: `${baseUrl}/${categoryId}`,
method: 'get'
})
}
categoryApi.update = (categoryId, category) => {
return service({
url: `${baseUrl}/${categoryId}`,
data: category,
method: 'put'
})
}
function concreteTree(parentCategory, categories) {
categories.forEach(category => {
if (parentCategory.key === category.parentId) {
if (!parentCategory.children) {
parentCategory.children = []
}
parentCategory.children.push({
key: category.id,
title: category.name,
isLeaf: false
})
}
})
if (parentCategory.children) {
parentCategory.children.forEach(category => concreteTree(category, categories))
} else {
parentCategory.isLeaf = true
}
}
categoryApi.concreteTree = categories => {
const topCategoryNode = {
key: 0,
title: 'top',
children: []
}
concreteTree(topCategoryNode, categories)
return topCategoryNode.children
}
export default categoryApi

140
src/api/comment.js

@ -1,140 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin'
const commentApi = {}
commentApi.latestComment = (target, top, status) => {
return service({
url: `${baseUrl}/${target}/comments/latest`,
params: {
top: top,
status: status
},
method: 'get'
})
}
commentApi.queryComment = (target, params) => {
return service({
url: `${baseUrl}/${target}/comments`,
params: params,
method: 'get'
})
}
commentApi.commentTree = (target, id, params) => {
return service({
url: `${baseUrl}/${target}/comments/${id}/tree_view`,
params: params,
method: 'get'
})
}
commentApi.updateStatus = (target, commentId, status) => {
return service({
url: `${baseUrl}/${target}/comments/${commentId}/status/${status}`,
method: 'put'
})
}
commentApi.updateStatusInBatch = (target, ids, status) => {
return service({
url: `${baseUrl}/${target}/comments/status/${status}`,
data: ids,
method: 'put'
})
}
commentApi.delete = (target, commentId) => {
return service({
url: `${baseUrl}/${target}/comments/${commentId}`,
method: 'delete'
})
}
commentApi.deleteInBatch = (target, ids) => {
return service({
url: `${baseUrl}/${target}/comments`,
data: ids,
method: 'delete'
})
}
commentApi.create = (target, comment) => {
return service({
url: `${baseUrl}/${target}/comments`,
data: comment,
method: 'post'
})
}
commentApi.update = (target, commentId, comment) => {
return service({
url: `${baseUrl}/${target}/comments/${commentId}`,
data: comment,
method: 'put'
})
}
/**
* Creates a comment.
* @param {String} target
* @param {Object} comment
*/
function createComment(target, comment) {
return service({
url: `${baseUrl}/${target}/comments`,
method: 'post',
data: comment
})
}
// Creation api
commentApi.createPostComment = comment => {
return createComment('posts', comment)
}
commentApi.createSheetComment = comment => {
return createComment('sheets', comment)
}
commentApi.createJournalComment = comment => {
return createComment('journals', comment)
}
commentApi.createComment = (comment, type) => {
if (type === 'sheet') {
return commentApi.createSheetComment(comment)
}
if (type === 'journal') {
return commentApi.createJournalComment(comment)
}
return commentApi.createPostComment(comment)
}
commentApi.commentStatus = {
PUBLISHED: {
value: 'PUBLISHED',
color: 'green',
status: 'success',
text: '已发布'
},
AUDITING: {
value: 'AUDITING',
color: 'yellow',
status: 'warning',
text: '待审核'
},
RECYCLE: {
value: 'RECYCLE',
color: 'red',
status: 'error',
text: '回收站'
}
}
export default commentApi

54
src/api/journal.js

@ -1,54 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/journals'
const journalApi = {}
journalApi.query = params => {
return service({
url: baseUrl,
params: params,
method: 'get'
})
}
journalApi.create = journal => {
return service({
url: baseUrl,
data: journal,
method: 'post'
})
}
journalApi.update = (journalId, journal) => {
return service({
url: `${baseUrl}/${journalId}`,
data: journal,
method: 'put'
})
}
journalApi.delete = journalId => {
return service({
url: `${baseUrl}/${journalId}`,
method: 'delete'
})
}
journalApi.commentTree = journalId => {
return service({
url: `${baseUrl}/${journalId}/comments/tree_view`,
method: 'get'
})
}
journalApi.journalType = {
PUBLIC: {
text: '公开'
},
INTIMATE: {
text: '私密'
}
}
export default journalApi

22
src/api/journalComment.js

@ -1,22 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/journals/comments'
const journalCommentApi = {}
journalCommentApi.create = comment => {
return service({
url: baseUrl,
data: comment,
method: 'post'
})
}
journalCommentApi.delete = commentId => {
return service({
url: `${baseUrl}/${commentId}`,
method: 'delete'
})
}
export default journalCommentApi

61
src/api/link.js

@ -1,61 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/links'
const linkApi = {}
linkApi.listAll = () => {
return service({
url: `${baseUrl}`,
method: 'get'
})
}
linkApi.create = link => {
return service({
url: baseUrl,
data: link,
method: 'post'
})
}
linkApi.get = linkId => {
return service({
url: `${baseUrl}/${linkId}`,
method: 'get'
})
}
linkApi.getByParse = url => {
return service({
url: `${baseUrl}/parse`,
params: {
url: url
},
method: 'get'
})
}
linkApi.update = (linkId, link) => {
return service({
url: `${baseUrl}/${linkId}`,
data: link,
method: 'put'
})
}
linkApi.delete = linkId => {
return service({
url: `${baseUrl}/${linkId}`,
method: 'delete'
})
}
linkApi.listTeams = () => {
return service({
url: `${baseUrl}/teams`,
method: 'get'
})
}
export default linkApi

91
src/api/log.js

@ -1,91 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/logs'
const logApi = {}
logApi.listLatest = top => {
return service({
url: `${baseUrl}/latest`,
params: {
top: top
},
method: 'get'
})
}
logApi.pageBy = logPagination => {
return service({
url: baseUrl,
params: logPagination,
method: 'get'
})
}
logApi.clear = () => {
return service({
url: `${baseUrl}/clear`,
method: 'get'
})
}
logApi.logTypes = {
BLOG_INITIALIZED: {
value: 0,
text: '博客初始化'
},
POST_PUBLISHED: {
value: 5,
text: '文章发布'
},
POST_EDITED: {
value: 15,
text: '文章修改'
},
POST_DELETED: {
value: 20,
text: '文章删除'
},
LOGGED_IN: {
value: 25,
text: '用户登录'
},
LOGGED_OUT: {
value: 30,
text: '注销登录'
},
LOGIN_FAILED: {
value: 35,
text: '登录失败'
},
PASSWORD_UPDATED: {
value: 40,
text: '修改密码'
},
PROFILE_UPDATED: {
value: 45,
text: '资料修改'
},
SHEET_PUBLISHED: {
value: 50,
text: '页面发布'
},
SHEET_EDITED: {
value: 55,
text: '页面修改'
},
SHEET_DELETED: {
value: 60,
text: '页面删除'
},
MFA_UPDATED: {
value: 65,
text: '两步验证'
},
LOGGED_PRE_CHECK: {
value: 70,
text: '登录验证'
}
}
export default logApi

15
src/api/mail.js

@ -1,15 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/mails'
const mailApi = {}
mailApi.testMail = mailData => {
return service({
url: `${baseUrl}/test`,
method: 'post',
data: mailData
})
}
export default mailApi

92
src/api/menu.js

@ -1,92 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/menus'
const menuApi = {}
menuApi.listAll = () => {
return service({
url: baseUrl,
method: 'get'
})
}
menuApi.listTree = () => {
return service({
url: `${baseUrl}/tree_view`,
method: 'get'
})
}
menuApi.listTreeByTeam = team => {
return service({
url: `${baseUrl}/team/tree_view`,
params: {
team: team
},
method: 'get'
})
}
menuApi.create = menu => {
return service({
url: baseUrl,
data: menu,
method: 'post'
})
}
menuApi.createBatch = menus => {
return service({
url: `${baseUrl}/batch`,
data: menus,
method: 'post'
})
}
menuApi.updateBatch = menus => {
return service({
url: `${baseUrl}/batch`,
data: menus,
method: 'put'
})
}
menuApi.delete = menuId => {
return service({
url: `${baseUrl}/${menuId}`,
method: 'delete'
})
}
menuApi.deleteBatch = menuIds => {
return service({
url: `${baseUrl}/batch`,
data: menuIds,
method: 'delete'
})
}
menuApi.get = menuId => {
return service({
url: `${baseUrl}/${menuId}`,
method: 'get'
})
}
menuApi.update = (menuId, menu) => {
return service({
url: `${baseUrl}/${menuId}`,
data: menu,
method: 'put'
})
}
menuApi.listTeams = () => {
return service({
url: `${baseUrl}/teams`,
method: 'get'
})
}
export default menuApi

15
src/api/migrate.js

@ -1,15 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/migrations'
const migrateApi = {}
migrateApi.migrate = formData => {
return service({
url: `${baseUrl}/halo`,
data: formData,
method: 'post'
})
}
export default migrateApi

79
src/api/option.js

@ -1,79 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/options'
const optionApi = {}
optionApi.listAll = () => {
return service({
url: `${baseUrl}/map_view`,
method: 'get'
})
}
optionApi.listAllByKeys = keys => {
return service({
url: `${baseUrl}/map_view/keys`,
data: keys,
method: 'post'
})
}
optionApi.query = params => {
return service({
url: `${baseUrl}/list_view`,
params: params,
method: 'get'
})
}
optionApi.save = options => {
return service({
url: `${baseUrl}/map_view/saving`,
method: 'post',
data: options
})
}
optionApi.create = option => {
return service({
url: baseUrl,
data: option,
method: 'post'
})
}
optionApi.delete = optionId => {
return service({
url: `${baseUrl}/${optionId}`,
method: 'delete'
})
}
optionApi.get = optionId => {
return service({
url: `${baseUrl}/${optionId}`,
method: 'get'
})
}
optionApi.update = (optionId, option) => {
return service({
url: `${baseUrl}/${optionId}`,
data: option,
method: 'put'
})
}
optionApi.type = {
INTERNAL: {
value: 'INTERNAL',
text: '系统'
},
CUSTOM: {
value: 'CUSTOM',
text: '自定义'
}
}
export default optionApi

45
src/api/photo.js

@ -1,45 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/photos'
const photoApi = {}
photoApi.query = params => {
return service({
url: baseUrl,
params: params,
method: 'get'
})
}
photoApi.create = photo => {
return service({
url: baseUrl,
data: photo,
method: 'post'
})
}
photoApi.update = (photoId, photo) => {
return service({
url: `${baseUrl}/${photoId}`,
method: 'put',
data: photo
})
}
photoApi.delete = photoId => {
return service({
url: `${baseUrl}/${photoId}`,
method: 'delete'
})
}
photoApi.listTeams = () => {
return service({
url: `${baseUrl}/teams`,
method: 'get'
})
}
export default photoApi

154
src/api/post.js

@ -1,154 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/posts'
const postApi = {}
postApi.listLatest = top => {
return service({
url: `${baseUrl}/latest`,
params: {
top: top
},
method: 'get'
})
}
postApi.query = params => {
return service({
url: baseUrl,
params: params,
method: 'get'
})
}
postApi.get = postId => {
return service({
url: `${baseUrl}/${postId}`,
method: 'get'
})
}
postApi.create = (postToCreate, autoSave) => {
return service({
url: baseUrl,
method: 'post',
data: postToCreate,
params: {
autoSave: autoSave
}
})
}
postApi.update = (postId, postToUpdate, autoSave) => {
return service({
url: `${baseUrl}/${postId}`,
method: 'put',
data: postToUpdate,
params: {
autoSave: autoSave
}
})
}
postApi.updateDraft = (postId, content) => {
return service({
url: `${baseUrl}/${postId}/status/draft/content`,
method: 'put',
data: {
content: content
}
})
}
postApi.updateStatus = (postId, status) => {
return service({
url: `${baseUrl}/${postId}/status/${status}`,
method: 'put'
})
}
postApi.updateStatusInBatch = (ids, status) => {
return service({
url: `${baseUrl}/status/${status}`,
data: ids,
method: 'put'
})
}
postApi.delete = postId => {
return service({
url: `${baseUrl}/${postId}`,
method: 'delete'
})
}
postApi.deleteInBatch = ids => {
return service({
url: `${baseUrl}`,
data: ids,
method: 'delete'
})
}
postApi.preview = postId => {
return service({
url: `${baseUrl}/preview/${postId}`,
method: 'get'
})
}
postApi.postStatus = {
PUBLISHED: {
value: 'PUBLISHED',
color: 'green',
status: 'success',
text: '已发布'
},
DRAFT: {
value: 'DRAFT',
color: 'yellow',
status: 'warning',
text: '草稿'
},
RECYCLE: {
value: 'RECYCLE',
color: 'red',
status: 'error',
text: '回收站'
},
INTIMATE: {
value: 'INTIMATE',
color: 'blue',
status: 'success',
text: '私密'
}
}
postApi.permalinkType = {
DEFAULT: {
type: 'DEFAULT',
text: '默认'
},
YEAR: {
type: 'YEAR',
text: '年份型'
},
DATE: {
type: 'DATE',
text: '年月型'
},
DAY: {
type: 'DAY',
text: '年月日型'
},
ID: {
type: 'ID',
text: 'ID 型'
},
ID_SLUG: {
type: 'ID_SLUG',
text: 'ID 别名型'
}
}
export default postApi

66
src/api/postComment.js

@ -1,66 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/posts/comments'
const postCommentApi = {}
postCommentApi.listLatest = (top, status) => {
return service({
url: `${baseUrl}/latest`,
params: {
top: top,
status: status
},
method: 'get'
})
}
postCommentApi.query = params => {
return service({
url: baseUrl,
params: params,
method: 'get'
})
}
postCommentApi.updateStatus = (commentId, status) => {
return service({
url: `${baseUrl}/${commentId}/status/${status}`,
method: 'put'
})
}
postCommentApi.delete = commentId => {
return service({
url: `${baseUrl}/${commentId}`,
method: 'delete'
})
}
postCommentApi.create = comment => {
return service({
url: baseUrl,
data: comment,
method: 'post'
})
}
postCommentApi.commentStatus = {
PUBLISHED: {
color: 'green',
status: 'success',
text: '已发布'
},
AUDITING: {
color: 'yellow',
status: 'warning',
text: '待审核'
},
RECYCLE: {
color: 'red',
status: 'error',
text: '回收站'
}
}
export default postCommentApi

110
src/api/sheet.js

@ -1,110 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/sheets'
const sheetApi = {}
sheetApi.list = params => {
return service({
url: baseUrl,
params: params,
method: 'get'
})
}
sheetApi.listIndependent = () => {
return service({
url: `${baseUrl}/independent`,
method: 'get'
})
}
sheetApi.get = sheetId => {
return service({
url: `${baseUrl}/${sheetId}`,
method: 'get'
})
}
sheetApi.create = (sheetToCreate, autoSave) => {
return service({
url: baseUrl,
method: 'post',
data: sheetToCreate,
params: {
autoSave: autoSave
}
})
}
sheetApi.update = (sheetId, sheetToUpdate, autoSave) => {
return service({
url: `${baseUrl}/${sheetId}`,
method: 'put',
data: sheetToUpdate,
params: {
autoSave: autoSave
}
})
}
sheetApi.updateDraft = (sheetId, content) => {
return service({
url: `${baseUrl}/${sheetId}/status/draft/content`,
method: 'put',
data: {
content: content
}
})
}
sheetApi.updateStatus = (sheetId, status) => {
return service({
url: `${baseUrl}/${sheetId}/${status}`,
method: 'put'
})
}
sheetApi.delete = sheetId => {
return service({
url: `${baseUrl}/${sheetId}`,
method: 'delete'
})
}
sheetApi.preview = sheetId => {
return service({
url: `${baseUrl}/preview/${sheetId}`,
method: 'get'
})
}
sheetApi.sheetStatus = {
PUBLISHED: {
color: 'green',
status: 'success',
text: '已发布'
},
DRAFT: {
color: 'yellow',
status: 'warning',
text: '草稿'
},
RECYCLE: {
color: 'red',
status: 'error',
text: '回收站'
}
}
sheetApi.permalinkType = {
SECONDARY: {
type: 'SECONDARY',
text: '二级路径'
},
ROOT: {
type: 'ROOT',
text: '根路径'
}
}
export default sheetApi

78
src/api/static.js

@ -1,78 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/statics'
const staticApi = {}
staticApi.list = () => {
return service({
url: baseUrl,
method: 'get'
})
}
staticApi.delete = path => {
return service({
url: baseUrl,
params: {
path: path
},
method: 'delete'
})
}
staticApi.createFolder = (basePath, folderName) => {
return service({
url: baseUrl,
params: {
basePath: basePath,
folderName: folderName
},
method: 'post'
})
}
staticApi.upload = (formData, uploadProgress, cancelToken, basePath) => {
return service({
url: `${baseUrl}/upload`,
timeout: 8640000,
data: formData,
params: {
basePath: basePath
},
onUploadProgress: uploadProgress,
cancelToken: cancelToken,
method: 'post'
})
}
staticApi.rename = (basePath, newName) => {
return service({
url: `${baseUrl}/rename`,
params: {
basePath: basePath,
newName: newName
},
method: 'post'
})
}
staticApi.getContent = url => {
return service({
url: `${url}`,
method: 'get'
})
}
staticApi.save = (path, content) => {
return service({
url: `${baseUrl}/files`,
data: {
path: path,
content: content
},
method: 'put'
})
}
export default staticApi

21
src/api/statistics.js

@ -1,21 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/statistics'
const statisticsApi = {}
statisticsApi.statistics = () => {
return service({
url: `${baseUrl}`,
method: 'get'
})
}
statisticsApi.statisticsWithUser = () => {
return service({
url: `${baseUrl}/user`,
method: 'get'
})
}
export default statisticsApi

50
src/api/tag.js

@ -1,50 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/tags'
const tagApi = {}
tagApi.listAll = (more = false) => {
return service({
url: baseUrl,
params: {
more: more
},
method: 'get'
})
}
tagApi.createWithName = name => {
return service({
url: baseUrl,
data: {
name: name
},
method: 'post'
})
}
tagApi.create = tag => {
return service({
url: baseUrl,
data: tag,
method: 'post'
})
}
tagApi.update = (tagId, tag) => {
return service({
url: `${baseUrl}/${tagId}`,
data: tag,
method: 'put'
})
}
tagApi.delete = tagId => {
return service({
url: `${baseUrl}/${tagId}`,
method: 'delete'
})
}
export default tagApi

195
src/api/theme.js

@ -1,195 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/themes'
const themeApi = {}
themeApi.list = () => {
return service({
url: `${baseUrl}`,
method: 'get'
})
}
themeApi.listFilesActivated = () => {
return service({
url: `${baseUrl}/activation/files`,
method: 'get'
})
}
themeApi.listFiles = themeId => {
return service({
url: `${baseUrl}/${themeId}/files`,
method: 'get'
})
}
themeApi.customSheetTpls = () => {
return service({
url: `${baseUrl}/activation/template/custom/sheet`,
method: 'get'
})
}
themeApi.customPostTpls = () => {
return service({
url: `${baseUrl}/activation/template/custom/post`,
method: 'get'
})
}
themeApi.active = theme => {
return service({
url: `${baseUrl}/${theme}/activation`,
method: 'post'
})
}
themeApi.getActivatedTheme = () => {
return service({
url: `${baseUrl}/activation`,
method: 'get'
})
}
themeApi.update = themeId => {
return service({
url: `${baseUrl}/fetching/${themeId}`,
timeout: 60000,
method: 'put'
})
}
themeApi.delete = (key, deleteSettings) => {
return service({
url: `${baseUrl}/${key}`,
params: {
deleteSettings: deleteSettings
},
method: 'delete'
})
}
themeApi.fetchConfiguration = themeId => {
return service({
url: `${baseUrl}/${themeId}/configurations`,
method: 'get'
})
}
themeApi.fetchSettings = themeId => {
return service({
url: `${baseUrl}/${themeId}/settings`,
method: 'get'
})
}
themeApi.saveSettings = (themeId, settings) => {
return service({
url: `${baseUrl}/${themeId}/settings`,
data: settings,
method: 'post'
})
}
themeApi.getProperty = themeId => {
return service({
url: `${baseUrl}/${themeId}`,
method: 'get'
})
}
themeApi.upload = (formData, uploadProgress, cancelToken) => {
return service({
url: `${baseUrl}/upload`,
timeout: 86400000, // 24 hours
data: formData, // form data
onUploadProgress: uploadProgress,
cancelToken: cancelToken,
method: 'post'
})
}
themeApi.updateByUpload = (formData, uploadProgress, cancelToken, themeId) => {
return service({
url: `${baseUrl}/upload/${themeId}`,
timeout: 86400000, // 24 hours
data: formData, // form data
onUploadProgress: uploadProgress,
cancelToken: cancelToken,
method: 'put'
})
}
themeApi.fetching = url => {
return service({
url: `${baseUrl}/fetching`,
timeout: 60000,
params: {
uri: url
},
method: 'post'
})
}
themeApi.getContent = path => {
return service({
url: `${baseUrl}/files/content`,
params: {
path: path
},
method: 'get'
})
}
themeApi.getContent = (themeId, path) => {
return service({
url: `${baseUrl}/${themeId}/files/content`,
params: {
path: path
},
method: 'get'
})
}
themeApi.saveContent = (path, content) => {
return service({
url: `${baseUrl}/files/content`,
data: {
path: path,
content: content
},
method: 'put'
})
}
themeApi.saveContent = (themeId, path, content) => {
return service({
url: `${baseUrl}/${themeId}/files/content`,
data: {
path: path,
content: content
},
method: 'put'
})
}
themeApi.reload = () => {
return service({
url: `${baseUrl}/reload`,
method: 'post'
})
}
themeApi.exists = template => {
return service({
url: `${baseUrl}/activation/template/exists`,
method: 'get',
params: {
template: template
}
})
}
export default themeApi

65
src/api/user.js

@ -1,65 +0,0 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/users'
const userApi = {}
userApi.getProfile = () => {
return service({
url: `${baseUrl}/profiles`,
method: 'get'
})
}
userApi.updateProfile = profile => {
return service({
url: `${baseUrl}/profiles`,
method: 'put',
data: profile
})
}
userApi.updatePassword = (oldPassword, newPassword) => {
return service({
url: `${baseUrl}/profiles/password`,
method: 'put',
data: {
oldPassword: oldPassword,
newPassword: newPassword
}
})
}
userApi.mfaGenerate = mfaType => {
return service({
url: `${baseUrl}/mfa/generate`,
method: 'put',
data: {
mfaType: mfaType
}
})
}
userApi.mfaUpdate = (mfaType, mfaKey, authcode) => {
return service({
url: `${baseUrl}/mfa/update`,
method: 'put',
data: {
mfaType: mfaType,
mfaKey: mfaKey,
authcode: authcode
}
})
}
userApi.mfaCheck = authcode => {
return service({
url: `${baseUrl}/mfa/check`,
method: 'put',
data: {
authcode: authcode
}
})
}
export default userApi

46
src/components/Attachment/AttachmentSelectDrawer.vue

@ -1,34 +1,34 @@
<template> <template>
<div> <div>
<a-drawer <a-drawer
:afterVisibleChange="handleAfterVisibleChanged"
:title="title" :title="title"
:visible="visible"
:width="isMobile() ? '100%' : drawerWidth" :width="isMobile() ? '100%' : drawerWidth"
closable closable
:visible="visible"
destroyOnClose destroyOnClose
@close="onClose" @close="onClose"
:afterVisibleChange="handleAfterVisibleChanged"
> >
<a-row type="flex" align="middle"> <a-row align="middle" type="flex">
<a-input-search placeholder="搜索" v-model="queryParam.keyword" @search="handleQuery()" enterButton /> <a-input-search v-model="queryParam.keyword" enterButton placeholder="搜索" @search="handleQuery()" />
</a-row> </a-row>
<a-divider /> <a-divider />
<a-row type="flex" align="middle"> <a-row align="middle" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-spin :spinning="loading" class="attachments-group"> <a-spin :spinning="loading" class="attachments-group">
<a-empty v-if="attachments.length === 0" /> <a-empty v-if="attachments.length === 0" />
<div <div
v-else
class="attach-item attachments-group-item"
v-for="(item, index) in attachments" v-for="(item, index) in attachments"
v-else
:key="index" :key="index"
class="attach-item attachments-group-item"
@click="handleSelectAttachment(item)" @click="handleSelectAttachment(item)"
> >
<span v-if="!handleJudgeMediaType(item)" class="attachments-group-item-type">{{ item.suffix }}</span> <span v-if="!handleJudgeMediaType(item)" class="attachments-group-item-type">{{ item.suffix }}</span>
<span <span
v-else v-else
class="attachments-group-item-img"
:style="`background-image:url(${item.thumbPath})`" :style="`background-image:url(${item.thumbPath})`"
class="attachments-group-item-img"
loading="lazy" loading="lazy"
/> />
</div> </div>
@ -39,30 +39,28 @@
<div class="page-wrapper"> <div class="page-wrapper">
<a-pagination <a-pagination
:current="pagination.page" :current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size" :defaultPageSize="pagination.size"
@change="handlePaginationChange" :total="pagination.total"
showLessItems showLessItems
@change="handlePaginationChange"
></a-pagination> ></a-pagination>
</div> </div>
<a-divider class="divider-transparent" /> <a-divider class="divider-transparent" />
<div class="bottom-control"> <div class="bottom-control">
<a-space> <a-space>
<a-button type="dashed" v-if="isChooseAvatar" @click="handleSelectGravatar">使用 Gravatar</a-button> <a-button v-if="isChooseAvatar" type="dashed" @click="handleSelectGravatar">使用 Gravatar</a-button>
<a-button @click="handleShowUploadModal" type="primary">上传附件</a-button> <a-button type="primary" @click="handleShowUploadModal">上传附件</a-button>
</a-space> </a-space>
</div> </div>
</a-drawer> </a-drawer>
<a-modal title="上传附件" v-model="uploadVisible" :footer="null" :afterClose="onUploadClose" destroyOnClose> <AttachmentUploadModal :visible.sync="uploadVisible" @close="onUploadClose" />
<FilePondUpload ref="upload" :uploadHandler="uploadHandler"></FilePondUpload>
</a-modal>
</div> </div>
</template> </template>
<script> <script>
import { mixin, mixinDevice } from '@/mixins/mixin.js' import { mixin, mixinDevice } from '@/mixins/mixin.js'
import attachmentApi from '@/api/attachment' import apiClient from '@/utils/api-client'
export default { export default {
name: 'AttachmentSelectDrawer', name: 'AttachmentSelectDrawer',
@ -109,8 +107,7 @@ export default {
sort: null, sort: null,
keyword: null keyword: null
}, },
attachments: [], attachments: []
uploadHandler: attachmentApi.upload
} }
}, },
methods: { methods: {
@ -122,16 +119,14 @@ export default {
this.queryParam.page = this.pagination.page - 1 this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort this.queryParam.sort = this.pagination.sort
attachmentApi apiClient.attachment
.query(this.queryParam) .list(this.queryParam)
.then(response => { .then(response => {
this.attachments = response.data.data.content this.attachments = response.data.content
this.pagination.total = response.data.data.total this.pagination.total = response.data.total
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.loading = false
this.loading = false
}, 200)
}) })
}, },
handleQuery() { handleQuery() {
@ -149,7 +144,6 @@ export default {
this.handleListAttachments() this.handleListAttachments()
}, },
onUploadClose() { onUploadClose() {
this.$refs.upload.handleClearFileList()
this.handlePaginationChange(1, this.pagination.size) this.handlePaginationChange(1, this.pagination.size)
}, },
handleAfterVisibleChanged(visible) { handleAfterVisibleChanged(visible) {

39
src/components/Attachment/AttachmentUploadModal.vue

@ -0,0 +1,39 @@
<template>
<a-modal v-model="modalVisible" :afterClose="onClose" :footer="null" destroyOnClose title="上传附件">
<FilePondUpload ref="upload" :uploadHandler="uploadHandler"></FilePondUpload>
</a-modal>
</template>
<script>
import apiClient from '@/utils/api-client'
export default {
name: 'AttachmentUploadModal',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
uploadHandler: (file, options) => apiClient.attachment.upload(file, options)
}
},
computed: {
modalVisible: {
get() {
return this.visible
},
set(value) {
this.$emit('update:visible', value)
}
}
},
methods: {
onClose() {
this.$refs.upload.handleClearFileList()
this.$emit('close')
}
}
}
</script>

10
src/components/Button/ReactiveButton.vue

@ -1,13 +1,13 @@
<template> <template>
<a-button <a-button
:type="computedType" :block="block"
@click="handleClick"
:icon="computedIcon" :icon="computedIcon"
:loading="loading" :loading="loading"
:size="size" :size="size"
:block="block" :type="computedType"
>{{ computedText }}</a-button @click="handleClick"
> >{{ computedText }}
</a-button>
</template> </template>
<script> <script>
export default { export default {

6
src/components/Editor/MarkdownEditor.vue

@ -14,7 +14,7 @@
import { toolbars } from '@/core/const' import { toolbars } from '@/core/const'
import { haloEditor } from 'halo-editor' import { haloEditor } from 'halo-editor'
import 'halo-editor/dist/css/index.css' import 'halo-editor/dist/css/index.css'
import attachmentApi from '@/api/attachment' import apiClient from '@/utils/api-client'
export default { export default {
name: 'MarkdownEditor', name: 'MarkdownEditor',
@ -46,10 +46,10 @@ export default {
handleAttachmentUpload(pos, $file) { handleAttachmentUpload(pos, $file) {
const formdata = new FormData() const formdata = new FormData()
formdata.append('file', $file) formdata.append('file', $file)
attachmentApi.upload(formdata).then(response => { apiClient.attachment.upload(formdata).then(response => {
const responseObject = response.data const responseObject = response.data
const HaloEditor = this.$refs.md const HaloEditor = this.$refs.md
HaloEditor.$img2Url(pos, encodeURI(responseObject.data.path)) HaloEditor.$img2Url(pos, encodeURI(responseObject.path))
}) })
}, },
handleSaveDraft() { handleSaveDraft() {

2
src/components/Editor/RichTextEditor.vue

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<a-input type="textarea" v-model="originalContent" :rows="16" /> <a-input v-model="originalContent" :rows="16" type="textarea" />
</div> </div>
</template> </template>
<script> <script>

26
src/components/Ellipsis/Ellipsis.vue

@ -1,6 +1,30 @@
<script> <script>
import Tooltip from 'ant-design-vue/es/tooltip' import Tooltip from 'ant-design-vue/es/tooltip'
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
const getStrFullLength = (str = '') =>
str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
return pre + 1
}
return pre + 2
}, 0)
const cutStrByFullLength = (str = '', maxLength) => {
let showLength = 0
return str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
showLength += 1
} else {
showLength += 2
}
if (showLength <= maxLength) {
return pre + cur
}
return pre
}, '')
}
export default { export default {
name: 'Ellipsis', name: 'Ellipsis',

1
src/components/GlobalFooter/index.js

@ -1,2 +1,3 @@
import GlobalFooter from './GlobalFooter' import GlobalFooter from './GlobalFooter'
export default GlobalFooter export default GlobalFooter

13
src/components/GlobalHeader/GlobalHeader.vue

@ -12,19 +12,19 @@
<div v-if="mode === 'sidemenu'" class="header"> <div v-if="mode === 'sidemenu'" class="header">
<a-icon <a-icon
v-if="device === 'mobile'" v-if="device === 'mobile'"
class="trigger"
:type="collapsed ? 'menu-fold' : 'menu-unfold'" :type="collapsed ? 'menu-fold' : 'menu-unfold'"
class="trigger"
@click="toggle" @click="toggle"
/> />
<a-icon v-else class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggle" /> <a-icon v-else :type="collapsed ? 'menu-unfold' : 'menu-fold'" class="trigger" @click="toggle" />
<user-menu></user-menu> <user-menu></user-menu>
</div> </div>
<div v-else :class="['top-nav-header-index', theme]"> <div v-else :class="['top-nav-header-index', theme]">
<div class="header-index-wide"> <div class="header-index-wide">
<div class="header-index-left"> <div class="header-index-left">
<logo class="top-nav-header" v-if="device !== 'mobile'" /> <logo v-if="device !== 'mobile'" class="top-nav-header" />
<s-menu v-if="device !== 'mobile'" mode="horizontal" :menu="menus" :theme="theme" /> <s-menu v-if="device !== 'mobile'" :menu="menus" :theme="theme" mode="horizontal" />
<a-icon v-else class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle" /> <a-icon v-else :type="collapsed ? 'menu-fold' : 'menu-unfold'" class="trigger" @click="toggle" />
</div> </div>
<user-menu class="header-index-right"></user-menu> <user-menu class="header-index-right"></user-menu>
</div> </div>
@ -120,12 +120,15 @@ export default {
position: relative; position: relative;
z-index: 999; z-index: 999;
} }
.showHeader-enter-active { .showHeader-enter-active {
transition: all 0.25s ease; transition: all 0.25s ease;
} }
.showHeader-leave-active { .showHeader-leave-active {
transition: all 0.5s ease; transition: all 0.5s ease;
} }
.showHeader-enter, .showHeader-enter,
.showHeader-leave-to { .showHeader-leave-to {
opacity: 0; opacity: 0;

1
src/components/GlobalHeader/index.js

@ -1,2 +1,3 @@
import GlobalHeader from './GlobalHeader' import GlobalHeader from './GlobalHeader'
export default GlobalHeader export default GlobalHeader

15
src/components/Login/LoginForm.vue

@ -36,13 +36,13 @@
</div> </div>
</template> </template>
<script> <script>
import adminApi from '@/api/admin'
import { mapActions } from 'vuex' import { mapActions } from 'vuex'
import apiClient from '@/utils/api-client'
export default { export default {
name: 'LoginForm', name: 'LoginForm',
data() { data() {
const authcodeValidate = (rule, value, callback) => { const mfaValidate = (rule, value, callback) => {
if (!value && this.form.needAuthCode) { if (!value && this.form.needAuthCode) {
callback(new Error('* 请输入两步验证码')) callback(new Error('* 请输入两步验证码'))
} else { } else {
@ -59,7 +59,7 @@ export default {
rules: { rules: {
username: [{ required: true, message: '* 用户名/邮箱不能为空', trigger: ['change'] }], username: [{ required: true, message: '* 用户名/邮箱不能为空', trigger: ['change'] }],
password: [{ required: true, message: '* 密码不能为空', trigger: ['change'] }], password: [{ required: true, message: '* 密码不能为空', trigger: ['change'] }],
authcode: [{ validator: authcodeValidate, trigger: ['change'] }] authcode: [{ validator: mfaValidate, trigger: ['change'] }]
}, },
needAuthCode: false, needAuthCode: false,
logging: false logging: false
@ -78,10 +78,13 @@ export default {
_this.$refs.loginForm.validate(valid => { _this.$refs.loginForm.validate(valid => {
if (valid) { if (valid) {
_this.form.logging = true _this.form.logging = true
adminApi apiClient
.loginPreCheck(_this.form.model.username, _this.form.model.password) .needMFACode({
username: _this.form.model.username,
password: _this.form.model.password
})
.then(response => { .then(response => {
const data = response.data.data const data = response.data
if (data && data.needMFACode) { if (data && data.needMFACode) {
_this.form.needAuthCode = true _this.form.needAuthCode = true
_this.form.model.authcode = null _this.form.model.authcode = null

7
src/components/Login/LoginModal.vue

@ -2,10 +2,10 @@
<div> <div>
<a-modal <a-modal
v-model="loginModal" v-model="loginModal"
title="重新登录"
:footer="null" :footer="null"
:width="320"
:maskClosable="false" :maskClosable="false"
:width="320"
title="重新登录"
@cancel="handleCancelLogin" @cancel="handleCancelLogin"
> >
<LoginForm @success="onLoginSucceed" /> <LoginForm @success="onLoginSucceed" />
@ -14,7 +14,8 @@
</template> </template>
<script> <script>
import LoginForm from './LoginForm' import LoginForm from './LoginForm'
import { mapGetters, mapActions } from 'vuex' import { mapActions, mapGetters } from 'vuex'
export default { export default {
name: 'LoginModal', name: 'LoginModal',
components: { components: {

10
src/components/Menu/SideMenu.vue

@ -1,19 +1,19 @@
<template> <template>
<a-layout-sider <a-layout-sider
:class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null]"
width="256px"
:collapsible="collapsible"
v-model="collapsed" v-model="collapsed"
:class="['sider', isDesktop() ? null : 'shadow', theme, fixedSidebar ? 'ant-fixed-sidemenu' : null]"
:collapsible="collapsible"
:trigger="null" :trigger="null"
width="256px"
> >
<logo /> <logo />
<s-menu <s-menu
:collapsed="collapsed" :collapsed="collapsed"
:menu="menus" :menu="menus"
:theme="theme"
:mode="mode" :mode="mode"
@select="onSelect" :theme="theme"
style="padding: 16px 0px;" style="padding: 16px 0px;"
@select="onSelect"
></s-menu> ></s-menu>
</a-layout-sider> </a-layout-sider>
</template> </template>

1
src/components/Menu/index.js

@ -1,2 +1,3 @@
import SMenu from './menu' import SMenu from './menu'
export default SMenu export default SMenu

6
src/components/Post/MetaEditor.vue

@ -37,7 +37,7 @@
</div> </div>
</template> </template>
<script> <script>
import themeApi from '@/api/theme' import apiClient from '@/utils/api-client'
export default { export default {
name: 'MetaEditor', name: 'MetaEditor',
@ -93,8 +93,8 @@ export default {
*/ */
async handleListPresetMetasField() { async handleListPresetMetasField() {
try { try {
const response = await themeApi.getActivatedTheme() const response = await apiClient.theme.getActivatedTheme()
this.presetFields = response.data.data[`${this.target}MetaField`] || [] this.presetFields = response.data[`${this.target}MetaField`] || []
this.handleGenerateMetas() this.handleGenerateMetas()
} catch (e) { } catch (e) {

55
src/components/SettingDrawer/SettingDrawer.vue

@ -1,6 +1,6 @@
<template> <template>
<div class="setting-drawer" ref="settingDrawer"> <div ref="settingDrawer" class="setting-drawer">
<a-drawer width="300" closable @close="onClose" :visible="layoutSetting"> <a-drawer :visible="layoutSetting" closable width="300" @close="onClose">
<div class="setting-drawer-index-content"> <div class="setting-drawer-index-content">
<div class="mb-6"> <div class="mb-6">
<h3 class="setting-drawer-index-title">整体风格设置</h3> <h3 class="setting-drawer-index-title">整体风格设置</h3>
@ -8,8 +8,8 @@
<a-tooltip> <a-tooltip>
<template slot="title">暗色菜单风格</template> <template slot="title">暗色菜单风格</template>
<div class="setting-drawer-index-item" @click="handleMenuTheme('dark')"> <div class="setting-drawer-index-item" @click="handleMenuTheme('dark')">
<img src="/images/dark.svg" alt="dark" /> <img alt="dark" src="/images/dark.svg" />
<div class="setting-drawer-index-selectIcon" v-if="navTheme === 'dark'"> <div v-if="navTheme === 'dark'" class="setting-drawer-index-selectIcon">
<a-icon type="check" /> <a-icon type="check" />
</div> </div>
</div> </div>
@ -18,8 +18,8 @@
<a-tooltip> <a-tooltip>
<template slot="title">亮色菜单风格</template> <template slot="title">亮色菜单风格</template>
<div class="setting-drawer-index-item" @click="handleMenuTheme('light')"> <div class="setting-drawer-index-item" @click="handleMenuTheme('light')">
<img src="/images/dark.svg" alt="light" /> <img alt="light" src="/images/dark.svg" />
<div class="setting-drawer-index-selectIcon" v-if="navTheme !== 'dark'"> <div v-if="navTheme !== 'dark'" class="setting-drawer-index-selectIcon">
<a-icon type="check" /> <a-icon type="check" />
</div> </div>
</div> </div>
@ -30,10 +30,10 @@
<div class="mb-6"> <div class="mb-6">
<h3 class="setting-drawer-index-title">主题色</h3> <h3 class="setting-drawer-index-title">主题色</h3>
<div class="h-5"> <div class="h-5">
<a-tooltip class="setting-drawer-theme-color-colorBlock" v-for="(item, index) in colorList" :key="index"> <a-tooltip v-for="(item, index) in colorList" :key="index" class="setting-drawer-theme-color-colorBlock">
<template slot="title">{{ item.key }}</template> <template slot="title">{{ item.key }}</template>
<a-tag :color="item.color" @click="changeColor(item.color)"> <a-tag :color="item.color" @click="changeColor(item.color)">
<a-icon type="check" v-if="item.color === primaryColor"></a-icon> <a-icon v-if="item.color === primaryColor" type="check"></a-icon>
</a-tag> </a-tag>
</a-tooltip> </a-tooltip>
</div> </div>
@ -44,15 +44,15 @@
<div class="setting-drawer-index-blockChecbox"> <div class="setting-drawer-index-blockChecbox">
<div class="setting-drawer-index-item" @click="handleLayout('sidemenu')"> <div class="setting-drawer-index-item" @click="handleLayout('sidemenu')">
<img src="/images/sidemenu.svg" alt="sidemenu" /> <img alt="sidemenu" src="/images/sidemenu.svg" />
<div class="setting-drawer-index-selectIcon" v-if="layoutMode === 'sidemenu'"> <div v-if="layoutMode === 'sidemenu'" class="setting-drawer-index-selectIcon">
<a-icon type="check" /> <a-icon type="check" />
</div> </div>
</div> </div>
<div class="setting-drawer-index-item" @click="handleLayout('topmenu')"> <div class="setting-drawer-index-item" @click="handleLayout('topmenu')">
<img src="/images/topmenu.svg" alt="topmenu" /> <img alt="topmenu" src="/images/topmenu.svg" />
<div class="setting-drawer-index-selectIcon" v-if="layoutMode !== 'sidemenu'"> <div v-if="layoutMode !== 'sidemenu'" class="setting-drawer-index-selectIcon">
<a-icon type="check" /> <a-icon type="check" />
</div> </div>
</div> </div>
@ -67,13 +67,13 @@
该设定仅 [顶部栏导航] 时有效 该设定仅 [顶部栏导航] 时有效
</template> </template>
<a-select <a-select
:defaultValue="contentWidth"
size="small" size="small"
style="width: 80px;" style="width: 80px;"
:defaultValue="contentWidth"
@change="handleContentWidthChange" @change="handleContentWidthChange"
> >
<a-select-option value="Fixed">固定</a-select-option> <a-select-option value="Fixed">固定</a-select-option>
<a-select-option value="Fluid" v-if="layoutMode !== 'sidemenu'">流式</a-select-option> <a-select-option v-if="layoutMode !== 'sidemenu'" value="Fluid">流式</a-select-option>
</a-select> </a-select>
</a-tooltip> </a-tooltip>
<a-list-item-meta> <a-list-item-meta>
@ -81,7 +81,7 @@
</a-list-item-meta> </a-list-item-meta>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<a-switch slot="actions" size="small" :defaultChecked="fixedHeader" @change="handleFixedHeader" /> <a-switch slot="actions" :defaultChecked="fixedHeader" size="small" @change="handleFixedHeader" />
<a-list-item-meta> <a-list-item-meta>
<div slot="title">固定 Header</div> <div slot="title">固定 Header</div>
</a-list-item-meta> </a-list-item-meta>
@ -89,9 +89,9 @@
<a-list-item> <a-list-item>
<a-switch <a-switch
slot="actions" slot="actions"
size="small"
:disabled="!fixedHeader"
:defaultChecked="autoHideHeader" :defaultChecked="autoHideHeader"
:disabled="!fixedHeader"
size="small"
@change="handleFixedHeaderHidden" @change="handleFixedHeaderHidden"
/> />
<a-list-item-meta> <a-list-item-meta>
@ -104,10 +104,10 @@
<a-list-item> <a-list-item>
<a-switch <a-switch
slot="actions" slot="actions"
size="small" :defaultChecked="fixedSidebar"
:disabled="layoutMode === 'topmenu'" :disabled="layoutMode === 'topmenu'"
:defaultChecked="fixSiderbar" size="small"
@change="handleFixSiderbar" @change="handleFixedSidebar"
/> />
<a-list-item-meta> <a-list-item-meta>
<div slot="title" :style="{ opacity: layoutMode === 'topmenu' ? '0.5' : '1' }">固定侧边菜单</div> <div slot="title" :style="{ opacity: layoutMode === 'topmenu' ? '0.5' : '1' }">固定侧边菜单</div>
@ -123,7 +123,7 @@
<script> <script>
import config from '@/config/defaultSettings' import config from '@/config/defaultSettings'
import { updateTheme, colorList } from './setting' import { colorList, updateTheme } from './setting'
import { mixin, mixinDevice } from '@/mixins/mixin' import { mixin, mixinDevice } from '@/mixins/mixin'
import { mapActions, mapGetters } from 'vuex' import { mapActions, mapGetters } from 'vuex'
@ -157,7 +157,7 @@ export default {
handleLayout(mode) { handleLayout(mode) {
this.baseConfig.layout = mode this.baseConfig.layout = mode
this.$store.dispatch('ToggleLayoutMode', mode) this.$store.dispatch('ToggleLayoutMode', mode)
this.handleFixSiderbar(false) this.handleFixedSidebar(false)
if (mode === 'sidemenu') { if (mode === 'sidemenu') {
this.handleContentWidthChange('Fixed') this.handleContentWidthChange('Fixed')
} }
@ -181,14 +181,14 @@ export default {
this.baseConfig.autoHideHeader = autoHidden this.baseConfig.autoHideHeader = autoHidden
this.$store.dispatch('ToggleFixedHeaderHidden', autoHidden) this.$store.dispatch('ToggleFixedHeaderHidden', autoHidden)
}, },
handleFixSiderbar(fixed) { handleFixedSidebar(fixed) {
if (this.layoutMode === 'topmenu') { if (this.layoutMode === 'topmenu') {
this.baseConfig.fixSiderbar = false this.baseConfig.fixedSidebar = false
this.$store.dispatch('ToggleFixSiderbar', false) this.$store.dispatch('ToggleFixedSidebar', false)
return return
} }
this.baseConfig.fixSiderbar = fixed this.baseConfig.fixedSidebar = fixed
this.$store.dispatch('ToggleFixSiderbar', fixed) this.$store.dispatch('ToggleFixedSidebar', fixed)
} }
} }
} }
@ -223,6 +223,7 @@ export default {
} }
} }
} }
.setting-drawer-theme-color-colorBlock { .setting-drawer-theme-color-colorBlock {
width: 20px; width: 20px;
height: 20px; height: 20px;

1
src/components/SettingDrawer/index.js

@ -1,2 +1,3 @@
import SettingDrawer from './SettingDrawer' import SettingDrawer from './SettingDrawer'
export default SettingDrawer export default SettingDrawer

2
src/components/SettingDrawer/setting.js

@ -48,6 +48,7 @@ const updateTheme = primaryColor => {
return return
} }
const hideMessage = message.loading('正在编译主题!', 0) const hideMessage = message.loading('正在编译主题!', 0)
function buildIt() { function buildIt() {
if (!window.less) { if (!window.less) {
return return
@ -66,6 +67,7 @@ const updateTheme = primaryColor => {
}) })
}, 200) }, 200)
} }
if (!lessNodesAppended) { if (!lessNodesAppended) {
// insert less.js and color.less // insert less.js and color.less
const lessStyleNode = document.createElement('link') const lessStyleNode = document.createElement('link')

4
src/components/Tools/HeadInfo.vue

@ -1,5 +1,5 @@
<template> <template>
<div class="head-info" :class="center && 'center'"> <div :class="center && 'center'" class="head-info">
<span>{{ title }}</span> <span>{{ title }}</span>
<p>{{ content }}</p> <p>{{ content }}</p>
<em v-if="bordered" /> <em v-if="bordered" />
@ -49,12 +49,14 @@ export default {
line-height: 22px; line-height: 22px;
margin-bottom: 4px; margin-bottom: 4px;
} }
p { p {
color: rgba(0, 0, 0, 0.85); color: rgba(0, 0, 0, 0.85);
font-size: 24px; font-size: 24px;
line-height: 32px; line-height: 32px;
margin: 0; margin: 0;
} }
em { em {
background-color: #e8e8e8; background-color: #e8e8e8;
position: absolute; position: absolute;

44
src/components/Tools/HeaderComment.vue

@ -1,21 +1,21 @@
<template> <template>
<a-popover <a-popover
v-model="visible" v-model="visible"
trigger="click"
placement="bottomRight"
:autoAdjustOverflow="true"
:arrowPointAtCenter="true" :arrowPointAtCenter="true"
:autoAdjustOverflow="true"
:overlayStyle="{ width: '300px', top: '50px' }" :overlayStyle="{ width: '300px', top: '50px' }"
placement="bottomRight"
title="待审核评论" title="待审核评论"
trigger="click"
> >
<template slot="content"> <template slot="content">
<div class="custom-tab-wrapper"> <div class="custom-tab-wrapper">
<a-tabs v-model="activeKey" @change="handleTabsChanged" :animated="{ inkBar: true, tabPane: false }"> <a-tabs v-model="activeKey" :animated="{ inkBar: true, tabPane: false }" @change="handleTabsChanged">
<a-tab-pane tab="文章" key="post"> <a-tab-pane key="post" tab="文章">
<a-list :loading="postCommentsLoading" :dataSource="converttedPostComments"> <a-list :dataSource="converttedPostComments" :loading="postCommentsLoading">
<a-list-item slot="renderItem" slot-scope="item"> <a-list-item slot="renderItem" slot-scope="item">
<a-list-item-meta> <a-list-item-meta>
<a-avatar class="bg-white" slot="avatar" :src="item.avatar" size="large" /> <a-avatar slot="avatar" :src="item.avatar" class="bg-white" size="large" />
<template slot="title"> <template slot="title">
<a :href="item.authorUrl" target="_blank">{{ item.author }}</a <a :href="item.authorUrl" target="_blank">{{ item.author }}</a
><span v-html="item.content"></span> ><span v-html="item.content"></span>
@ -27,11 +27,11 @@
</a-list-item> </a-list-item>
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane tab="页面" key="sheet"> <a-tab-pane key="sheet" tab="页面">
<a-list :loading="sheetCommentsLoading" :dataSource="converttedSheetComments"> <a-list :dataSource="converttedSheetComments" :loading="sheetCommentsLoading">
<a-list-item slot="renderItem" slot-scope="item"> <a-list-item slot="renderItem" slot-scope="item">
<a-list-item-meta> <a-list-item-meta>
<a-avatar class="bg-white" slot="avatar" :src="item.avatar" size="large" /> <a-avatar slot="avatar" :src="item.avatar" class="bg-white" size="large" />
<template slot="title"> <template slot="title">
<a :href="item.authorUrl" target="_blank">{{ item.author }}</a <a :href="item.authorUrl" target="_blank">{{ item.author }}</a
><span v-html="item.content"></span> ><span v-html="item.content"></span>
@ -47,7 +47,7 @@
</div> </div>
</template> </template>
<span class="header-comment"> <span class="header-comment">
<a-badge dot v-if="postComments.length > 0 || sheetComments.length > 0"> <a-badge v-if="postComments.length > 0 || sheetComments.length > 0" dot>
<a-icon type="bell" /> <a-icon type="bell" />
</a-badge> </a-badge>
<a-badge v-else> <a-badge v-else>
@ -58,7 +58,7 @@
</template> </template>
<script> <script>
import commentApi from '@/api/comment' import apiClient from '@/utils/api-client'
import marked from 'marked' import marked from 'marked'
export default { export default {
@ -107,30 +107,26 @@ export default {
if (enableLoading) { if (enableLoading) {
this.postCommentsLoading = true this.postCommentsLoading = true
} }
commentApi apiClient.comment
.latestComment('posts', 5, 'AUDITING') .latest('posts', 5, 'AUDITING')
.then(response => { .then(response => {
this.postComments = response.data.data this.postComments = response.data
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.postCommentsLoading = false
this.postCommentsLoading = false
}, 200)
}) })
}, },
handleListSheetAuditingComments(enableLoading = true) { handleListSheetAuditingComments(enableLoading = true) {
if (enableLoading) { if (enableLoading) {
this.sheetCommentsLoading = true this.sheetCommentsLoading = true
} }
commentApi apiClient.comment
.latestComment('sheets', 5, 'AUDITING') .latest('sheets', 5, 'AUDITING')
.then(response => { .then(response => {
this.sheetComments = response.data.data this.sheetComments = response.data
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.sheetCommentsLoading = false
this.sheetCommentsLoading = false
}, 200)
}) })
}, },
handleTabsChanged(activeKey) { handleTabsChanged(activeKey) {

28
src/components/Tools/Logo.vue

@ -1,22 +1,20 @@
<template> <template>
<div class="logo"> <div class="logo">
<a href="javascript:void(0);" @click="onLogoClick()"> <a href="javascript:void(0);" @click="onLogoClick()">
<img src="/images/logo.svg" alt="Halo Logo" /> <img alt="Halo Logo" src="/images/logo.svg" />
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex' import { mapActions, mapGetters } from 'vuex'
import optionApi from '@/api/option' import apiClient from '@/utils/api-client'
export default { export default {
name: 'Logo', name: 'Logo',
data() { data() {
return { return {
clickCount: 0, clickCount: 0
optionsToCreate: {
developer_mode: true
}
} }
}, },
computed: { computed: {
@ -24,16 +22,22 @@ export default {
}, },
methods: { methods: {
...mapActions(['refreshOptionsCache']), ...mapActions(['refreshOptionsCache']),
onLogoClick() { async onLogoClick() {
this.clickCount++ this.clickCount++
if (this.clickCount === 10) { if (this.clickCount === 10) {
optionApi.save(this.optionsToCreate).then(() => { try {
this.refreshOptionsCache() await apiClient.option.saveMapView({ developer_mode: true })
await this.refreshOptionsCache()
this.$message.success(`开发者选项已启用!`) this.$message.success(`开发者选项已启用!`)
this.clickCount = 0 this.clickCount = 0
this.$router.push({ name: 'ToolList' }) this.$router.push({ name: 'ToolList' }).catch(() => {})
}) } catch (e) {
} else if (this.clickCount >= 5) { this.$log.error(e)
}
return
}
if (this.clickCount >= 5) {
if (this.options.developer_mode) { if (this.options.developer_mode) {
this.$message.info(`当前已启用开发者选项!`) this.$message.info(`当前已启用开发者选项!`)
this.clickCount = 0 this.clickCount = 0

29
src/components/Tools/UserMenu.vue

@ -16,8 +16,8 @@
</a> </a>
<header-comment class="action" /> <header-comment class="action" />
<a-dropdown> <a-dropdown>
<span class="action ant-dropdown-link user-dropdown-menu" v-if="user"> <span v-if="user" class="action ant-dropdown-link user-dropdown-menu">
<a-avatar class="avatar" size="small" :src="user.avatar || '//cn.gravatar.com/avatar/?s=256&d=mm'" /> <a-avatar :src="user.avatar || '//cn.gravatar.com/avatar/?s=256&d=mm'" class="avatar" size="small" />
</span> </span>
<a-menu slot="overlay" class="user-dropdown-menu-wrapper"> <a-menu slot="overlay" class="user-dropdown-menu-wrapper">
<a-menu-item key="0"> <a-menu-item key="0">
@ -53,25 +53,22 @@ export default {
methods: { methods: {
...mapActions(['logout', 'ToggleLayoutSetting']), ...mapActions(['logout', 'ToggleLayoutSetting']),
handleLogout() { handleLogout() {
const that = this const _this = this
this.$confirm({ this.$confirm({
title: '提示', title: '提示',
content: '确定要注销登录吗 ?', content: '确定要注销登录吗 ?',
onOk() { onOk: async () => {
return that try {
.logout({}) await _this.logout()
.then(() => { window.location.reload()
window.location.reload() } catch (e) {
_this.$message.error({
title: '错误',
description: e.message
}) })
.catch(err => { }
that.$message.error({ }
title: '错误',
description: err.message
})
})
},
onCancel() {}
}) })
}, },
handleShowLayoutSetting() { handleShowLayoutSetting() {

47
src/components/Upload/FilePondUpload.vue

@ -2,24 +2,24 @@
<div> <div>
<file-pond <file-pond
ref="pond" ref="pond"
:label-idle="label"
:name="name"
:allow-multiple="multiple"
:allowRevert="false"
:accepted-file-types="accepts" :accepted-file-types="accepts"
:maxParallelUploads="maxParallelUploads" :allow-multiple="multiple"
:allowImagePreview="allowImagePreview" :allowImagePreview="allowImagePreview"
:allowRevert="false"
:files="fileList"
:label-idle="label"
:maxFiles="maxFiles" :maxFiles="maxFiles"
:maxParallelUploads="maxParallelUploads"
:name="name"
:server="server"
fileValidateTypeLabelExpectedTypes="请选择 {lastType} 格式的文件"
labelFileProcessing="上传中" labelFileProcessing="上传中"
labelFileProcessingComplete="上传完成"
labelFileProcessingAborted="取消上传" labelFileProcessingAborted="取消上传"
labelFileProcessingComplete="上传完成"
labelFileProcessingError="上传错误" labelFileProcessingError="上传错误"
labelFileTypeNotAllowed="不支持当前文件格式"
labelTapToCancel="点击取消" labelTapToCancel="点击取消"
labelTapToRetry="点击重试" labelTapToRetry="点击重试"
labelFileTypeNotAllowed="不支持当前文件格式"
fileValidateTypeLabelExpectedTypes="请选择 {lastType} 格式的文件"
:files="fileList"
:server="server"
@init="handleFilePondInit" @init="handleFilePondInit"
> >
</file-pond> </file-pond>
@ -27,7 +27,7 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import axios from 'axios' import { Axios } from '@halo-dev/admin-api'
import vueFilePond from 'vue-filepond' import vueFilePond from 'vue-filepond'
import 'filepond/dist/filepond.min.css' import 'filepond/dist/filepond.min.css'
@ -50,7 +50,7 @@ export default {
required: false, required: false,
default: 'file' default: 'file'
}, },
filed: { field: {
type: String, type: String,
required: false, required: false,
default: '' default: ''
@ -102,22 +102,19 @@ export default {
return { return {
server: { server: {
process: (fieldName, file, metadata, load, error, progress, abort) => { process: (fieldName, file, metadata, load, error, progress, abort) => {
const formData = new FormData() const CancelToken = Axios.CancelToken
formData.append(fieldName, file, file.name)
const CancelToken = axios.CancelToken
const source = CancelToken.source() const source = CancelToken.source()
this.uploadHandler( this.uploadHandler(
formData, file,
progressEvent => { {
if (progressEvent.total > 0) { onUploadProgress: progressEvent => {
progress(progressEvent.lengthComputable, progressEvent.loaded, progressEvent.total) if (progressEvent.total > 0) {
} progress(progressEvent.lengthComputable, progressEvent.loaded, progressEvent.total)
}
},
cancelToken: source.token
}, },
source.token, this.field
this.filed,
file
) )
.then(response => { .then(response => {
load(response) load(response)

33
src/components/_util/util.js

@ -1,33 +0,0 @@
/**
* 获取字符串长度英文字符 长度1中文字符长度2
* @param {*} str
*/
export const getStrFullLength = (str = '') =>
str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
return pre + 1
}
return pre + 2
}, 0)
/**
* 截取字符串根据 maxLength 截取后返回
* @param {*} str
* @param {*} maxLength
*/
export const cutStrByFullLength = (str = '', maxLength) => {
let showLength = 0
return str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
showLength += 1
} else {
showLength += 2
}
if (showLength <= maxLength) {
return pre + cur
}
return pre
}, '')
}

2
src/components/index.js

@ -4,6 +4,7 @@ import Ellipsis from '@/components/Ellipsis'
import FooterToolbar from '@/components/FooterToolbar' import FooterToolbar from '@/components/FooterToolbar'
import FilePondUpload from '@/components/Upload/FilePondUpload' import FilePondUpload from '@/components/Upload/FilePondUpload'
import AttachmentSelectDrawer from './Attachment/AttachmentSelectDrawer' import AttachmentSelectDrawer from './Attachment/AttachmentSelectDrawer'
import AttachmentUploadModal from './Attachment/AttachmentUploadModal'
import ReactiveButton from './Button/ReactiveButton' import ReactiveButton from './Button/ReactiveButton'
const _components = { const _components = {
@ -11,6 +12,7 @@ const _components = {
FooterToolbar, FooterToolbar,
FilePondUpload, FilePondUpload,
AttachmentSelectDrawer, AttachmentSelectDrawer,
AttachmentUploadModal,
ReactiveButton ReactiveButton
} }

2
src/config/defaultSettings.js

@ -4,7 +4,7 @@ export default {
layout: 'topmenu', layout: 'topmenu',
contentWidth: 'Fixed', contentWidth: 'Fixed',
fixedHeader: false, fixedHeader: false,
fixSiderbar: false, fixedSidebar: false,
autoHideHeader: false, autoHideHeader: false,
storageOptions: { storageOptions: {
namespace: 'halo__', namespace: 'halo__',

2
src/config/router.config.js

@ -1,5 +1,5 @@
// eslint-disable-next-line // eslint-disable-next-line
import { BasicLayout, PageView, BlankLayout } from '@/layouts' import { BasicLayout, BlankLayout, PageView } from '@/layouts'
export const asyncRouterMap = [ export const asyncRouterMap = [
{ {

18
src/core/bootstrap.js vendored

@ -3,16 +3,15 @@ import store from '@/store/'
import { import {
ACCESS_TOKEN, ACCESS_TOKEN,
DEFAULT_COLOR, DEFAULT_COLOR,
DEFAULT_THEME, DEFAULT_CONTENT_WIDTH_TYPE,
DEFAULT_LAYOUT_MODE,
SIDEBAR_TYPE,
DEFAULT_FIXED_HEADER, DEFAULT_FIXED_HEADER,
DEFAULT_FIXED_HEADER_HIDDEN, DEFAULT_FIXED_HEADER_HIDDEN,
DEFAULT_FIXED_SIDEMENU, DEFAULT_FIXED_SIDEBAR,
DEFAULT_CONTENT_WIDTH_TYPE, DEFAULT_LAYOUT_MODE,
USER, DEFAULT_THEME,
API_URL, OPTIONS,
OPTIONS SIDEBAR_TYPE,
USER
} from '@/store/mutation-types' } from '@/store/mutation-types'
import config from '@/config/defaultSettings' import config from '@/config/defaultSettings'
@ -21,13 +20,12 @@ export default function Initializer() {
store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme)) store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme))
store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout)) store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout))
store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader)) store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader))
store.commit('TOGGLE_FIXED_SIDERBAR', Vue.ls.get(DEFAULT_FIXED_SIDEMENU, config.fixSiderbar)) store.commit('TOGGLE_FIXED_SIDEBAR', Vue.ls.get(DEFAULT_FIXED_SIDEBAR, config.fixedSidebar))
store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth)) store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth))
store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader)) store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader))
store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor)) store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor))
store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN)) store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN))
store.commit('SET_USER', Vue.ls.get(USER)) store.commit('SET_USER', Vue.ls.get(USER))
store.commit('SET_API_URL', Vue.ls.get(API_URL))
store.commit('SET_OPTIONS', Vue.ls.get(OPTIONS)) store.commit('SET_OPTIONS', Vue.ls.get(OPTIONS))
// last step // last step
} }

28
src/core/lazy_lib/components_use.js

@ -1,20 +1,24 @@
import Vue from 'vue' import Vue from 'vue'
import { import {
Affix, Affix,
Alert,
Anchor, Anchor,
AutoComplete, AutoComplete,
Alert,
Avatar, Avatar,
Badge, Badge,
Breadcrumb, Breadcrumb,
Button, Button,
Card, Card,
Collapse,
Checkbox, Checkbox,
Col, Col,
Collapse,
Comment,
ConfigProvider,
DatePicker, DatePicker,
Divider, Divider,
Drawer,
Dropdown, Dropdown,
Empty,
Form, Form,
FormModel, FormModel,
Icon, Icon,
@ -23,8 +27,8 @@ import {
Layout, Layout,
List, List,
LocaleProvider, LocaleProvider,
message,
Menu, Menu,
message,
Modal, Modal,
notification, notification,
PageHeader, PageHeader,
@ -33,26 +37,22 @@ import {
Popover, Popover,
Progress, Progress,
Radio, Radio,
Result,
Row, Row,
Select, Select,
Skeleton,
Space,
Spin, Spin,
Steps,
Switch, Switch,
Table, Table,
Tree,
TreeSelect,
Tabs, Tabs,
Tag, Tag,
Timeline,
TimePicker, TimePicker,
Tooltip, Tooltip,
Drawer, Tree,
Skeleton, TreeSelect
Comment,
ConfigProvider,
Timeline,
Steps,
Empty,
Result,
Space
} from 'ant-design-vue' } from 'ant-design-vue'
Vue.use(Affix) Vue.use(Affix)

33
src/layouts/BasicLayout.vue

@ -3,29 +3,29 @@
<!-- SideMenu --> <!-- SideMenu -->
<a-drawer <a-drawer
v-if="isMobile()" v-if="isMobile()"
placement="left"
:wrapClassName="`drawer-sider ${navTheme}`"
:closable="false" :closable="false"
:visible="collapsed" :visible="collapsed"
:wrapClassName="`drawer-sider ${navTheme}`"
placement="left"
@close="drawerClose" @close="drawerClose"
> >
<side-menu <side-menu
mode="inline"
:menus="menus"
:theme="navTheme"
:collapsed="false" :collapsed="false"
:collapsible="true" :collapsible="true"
:menus="menus"
:theme="navTheme"
mode="inline"
@menuSelect="menuSelect" @menuSelect="menuSelect"
></side-menu> ></side-menu>
</a-drawer> </a-drawer>
<side-menu <side-menu
v-else-if="isSideMenu()" v-else-if="isSideMenu()"
mode="inline"
:menus="menus"
:theme="navTheme"
:collapsed="collapsed" :collapsed="collapsed"
:collapsible="true" :collapsible="true"
:menus="menus"
:theme="navTheme"
mode="inline"
></side-menu> ></side-menu>
<a-layout <a-layout
@ -34,11 +34,11 @@
> >
<!-- layout header --> <!-- layout header -->
<global-header <global-header
:mode="layoutMode"
:menus="menus"
:theme="navTheme"
:collapsed="collapsed" :collapsed="collapsed"
:device="device" :device="device"
:menus="menus"
:mode="layoutMode"
:theme="navTheme"
@toggle="toggle" @toggle="toggle"
/> />
@ -63,7 +63,7 @@
<script> <script>
import { triggerWindowResizeEvent } from '@/utils/util' import { triggerWindowResizeEvent } from '@/utils/util'
import { mapState, mapActions } from 'vuex' import { mapActions } from 'vuex'
import { mixin, mixinDevice } from '@/mixins/mixin' import { mixin, mixinDevice } from '@/mixins/mixin'
import config from '@/config/defaultSettings' import config from '@/config/defaultSettings'
import { asyncRouterMap } from '@/config/router.config.js' import { asyncRouterMap } from '@/config/router.config.js'
@ -94,12 +94,8 @@ export default {
} }
}, },
computed: { computed: {
...mapState({
//
mainMenu: state => state.permission.addRouters
}),
contentPaddingLeft() { contentPaddingLeft() {
if (!this.fixSidebar || this.isMobile()) { if (!this.fixedSidebar || this.isMobile()) {
return '0' return '0'
} }
if (this.sidebarOpened) { if (this.sidebarOpened) {
@ -115,7 +111,6 @@ export default {
}, },
created() { created() {
this.menus = asyncRouterMap.find(item => item.path === '/').children this.menus = asyncRouterMap.find(item => item.path === '/').children
// this.menus = this.mainMenu.find((item) => item.path === '/').children
this.collapsed = !this.sidebarOpened this.collapsed = !this.sidebarOpened
}, },
mounted() { mounted() {
@ -141,7 +136,7 @@ export default {
if (this.sidebarOpened) { if (this.sidebarOpened) {
left = this.isDesktop() ? '256px' : '80px' left = this.isDesktop() ? '256px' : '80px'
} else { } else {
left = (this.isMobile() && '0') || (this.fixSidebar && '80px') || '0' left = (this.isMobile() && '0') || (this.fixedSidebar && '80px') || '0'
} }
return left return left
}, },

23
src/layouts/PageView.vue

@ -1,20 +1,20 @@
<template> <template>
<div :style="!$route.meta.hiddenHeaderContent ? 'margin: -24px -24px 0px;' : null"> <div :style="!$route.meta.hiddenHeaderContent ? 'margin: -24px -24px 0px;' : null">
<a-affix v-if="affix"> <a-affix v-if="affix">
<div class="page-header" v-if="!$route.meta.hiddenHeaderContent"> <div v-if="!$route.meta.hiddenHeaderContent" class="page-header">
<div class="page-header-index-wide"> <div class="page-header-index-wide">
<a-page-header :title="title" :sub-title="subTitle" :breadcrumb="{ props: { routes: breadList } }"> <a-page-header :breadcrumb="{ props: { routes: breadList } }" :sub-title="subTitle" :title="title">
<slot name="extra" slot="extra"> </slot> <slot slot="extra" name="extra"></slot>
<slot name="footer" slot="footer"> </slot> <slot slot="footer" name="footer"></slot>
</a-page-header> </a-page-header>
</div> </div>
</div> </div>
</a-affix> </a-affix>
<div class="page-header" v-if="!$route.meta.hiddenHeaderContent && !affix"> <div v-if="!$route.meta.hiddenHeaderContent && !affix" class="page-header">
<div class="page-header-index-wide"> <div class="page-header-index-wide">
<a-page-header :title="title" :sub-title="subTitle" :breadcrumb="{ props: { routes: breadList } }"> <a-page-header :breadcrumb="{ props: { routes: breadList } }" :sub-title="subTitle" :title="title">
<slot name="extra" slot="extra"> </slot> <slot slot="extra" name="extra"></slot>
<slot name="footer" slot="footer"> </slot> <slot slot="footer" name="footer"></slot>
</a-page-header> </a-page-header>
</div> </div>
</div> </div>
@ -75,6 +75,7 @@ export default {
background: #fff; background: #fff;
padding: 0 24px 0; padding: 0 24px 0;
border-bottom: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8;
.ant-page-header { .ant-page-header {
padding: 16px 0px; padding: 16px 0px;
} }
@ -83,6 +84,7 @@ export default {
.mobile .page-header, .mobile .page-header,
.tablet .page-header { .tablet .page-header {
padding: 0 !important; padding: 0 !important;
.ant-page-header { .ant-page-header {
padding: 16px; padding: 16px;
} }
@ -90,21 +92,26 @@ export default {
.content { .content {
margin: 24px 24px 0; margin: 24px 24px 0;
.link { .link {
margin-top: 16px; margin-top: 16px;
&:not(:empty) { &:not(:empty) {
margin-bottom: 16px; margin-bottom: 16px;
} }
a { a {
margin-right: 32px; margin-right: 32px;
height: 24px; height: 24px;
line-height: 24px; line-height: 24px;
display: inline-block; display: inline-block;
i { i {
font-size: 24px; font-size: 24px;
margin-right: 8px; margin-right: 8px;
vertical-align: middle; vertical-align: middle;
} }
span { span {
height: 24px; height: 24px;
line-height: 24px; line-height: 24px;

3
src/mixins/mixin.js

@ -12,8 +12,7 @@ const mixin = {
navTheme: state => state.app.theme, navTheme: state => state.app.theme,
primaryColor: state => state.app.color, primaryColor: state => state.app.color,
fixedHeader: state => state.app.fixedHeader, fixedHeader: state => state.app.fixedHeader,
fixSiderbar: state => state.app.fixSiderbar, fixedSidebar: state => state.app.fixedSidebar,
fixSidebar: state => state.app.fixSiderbar,
contentWidth: state => state.app.contentWidth, contentWidth: state => state.app.contentWidth,
autoHideHeader: state => state.app.autoHideHeader, autoHideHeader: state => state.app.autoHideHeader,
sidebarOpened: state => state.app.sidebar sidebarOpened: state => state.app.sidebar

10
src/router/guard/permissionGuard.js

@ -1,9 +1,8 @@
import Vue from 'vue'
import router from '@/router' import router from '@/router'
import store from '@/store' import store from '@/store'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import { setDocumentTitle, domTitle } from '@/utils/domUtil' import { domTitle, setDocumentTitle } from '@/utils/domUtil'
import adminApi from '@/api/admin' import apiClient from '@/utils/api-client'
NProgress.configure({ showSpinner: false, speed: 500 }) NProgress.configure({ showSpinner: false, speed: 500 })
@ -16,14 +15,13 @@ router.beforeEach(async (to, from, next) => {
NProgress.start() NProgress.start()
}, 250) }, 250)
to.meta && typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${domTitle}`) to.meta && typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${domTitle}`)
Vue.$log.debug('Token', store.getters.token)
if (store.getters.token) { if (store.getters.token) {
if (to.name === 'Install') { if (to.name === 'Install') {
next() next()
return return
} }
const response = await adminApi.isInstalled() const response = await apiClient.isInstalled()
if (!response.data.data) { if (!response.data) {
next({ next({
name: 'Install' name: 'Install'
}) })

2
src/router/index.js

@ -1,6 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import Router from 'vue-router' import Router from 'vue-router'
import { constantRouterMap, asyncRouterMap } from '@/config/router.config' import { asyncRouterMap, constantRouterMap } from '@/config/router.config'
Vue.use(Router) Vue.use(Router)

7
src/store/getters.js

@ -6,13 +6,6 @@ const getters = {
loginModal: state => state.app.loginModal, loginModal: state => state.app.loginModal,
token: state => state.user.token, token: state => state.user.token,
user: state => state.user.user, user: state => state.user.user,
addRouters: state => state.permission.addRouters,
apiUrl: state => {
if (state.app.apiUrl) {
return state.app.apiUrl
}
return `${window.location.protocol}//${window.location.host}`
},
options: state => state.option.options options: state => state.option.options
} }

2
src/store/index.js

@ -3,7 +3,6 @@ import Vuex from 'vuex'
import app from './modules/app' import app from './modules/app'
import user from './modules/user' import user from './modules/user'
import permission from './modules/permission'
import option from './modules/option' import option from './modules/option'
import getters from './getters' import getters from './getters'
@ -13,7 +12,6 @@ export default new Vuex.Store({
modules: { modules: {
app, app,
user, user,
permission,
option option
}, },
state: {}, state: {},

40
src/store/modules/app.js

@ -1,15 +1,14 @@
import Vue from 'vue' import Vue from 'vue'
import { import {
SIDEBAR_TYPE,
DEFAULT_THEME,
DEFAULT_LAYOUT_MODE,
DEFAULT_COLOR, DEFAULT_COLOR,
DEFAULT_CONTENT_WIDTH_TYPE,
DEFAULT_FIXED_HEADER, DEFAULT_FIXED_HEADER,
DEFAULT_FIXED_SIDEMENU,
DEFAULT_FIXED_HEADER_HIDDEN, DEFAULT_FIXED_HEADER_HIDDEN,
DEFAULT_CONTENT_WIDTH_TYPE, DEFAULT_FIXED_SIDEBAR,
API_URL, DEFAULT_LAYOUT_MODE,
LAYOUT_SETTING DEFAULT_THEME,
LAYOUT_SETTING,
SIDEBAR_TYPE
} from '@/store/mutation-types' } from '@/store/mutation-types'
const app = { const app = {
@ -20,22 +19,13 @@ const app = {
layout: '', layout: '',
contentWidth: '', contentWidth: '',
fixedHeader: false, fixedHeader: false,
fixSiderbar: false, fixedSidebar: false,
autoHideHeader: false, autoHideHeader: false,
color: null, color: null,
apiUrl: null,
layoutSetting: false, layoutSetting: false,
loginModal: false loginModal: false
}, },
mutations: { mutations: {
SET_API_URL: (state, apiUrl) => {
state.apiUrl = apiUrl
Vue.ls.set(API_URL, apiUrl)
},
RESTORE_API_URL: state => {
state.apiUrl = null
Vue.ls.set(API_URL, null)
},
SET_SIDEBAR_TYPE: (state, type) => { SET_SIDEBAR_TYPE: (state, type) => {
state.sidebar = type state.sidebar = type
Vue.ls.set(SIDEBAR_TYPE, type) Vue.ls.set(SIDEBAR_TYPE, type)
@ -59,9 +49,9 @@ const app = {
Vue.ls.set(DEFAULT_FIXED_HEADER, fixed) Vue.ls.set(DEFAULT_FIXED_HEADER, fixed)
state.fixedHeader = fixed state.fixedHeader = fixed
}, },
TOGGLE_FIXED_SIDERBAR: (state, fixed) => { TOGGLE_FIXED_SIDEBAR: (state, fixed) => {
Vue.ls.set(DEFAULT_FIXED_SIDEMENU, fixed) Vue.ls.set(DEFAULT_FIXED_SIDEBAR, fixed)
state.fixSiderbar = fixed state.fixedSidebar = fixed
}, },
TOGGLE_FIXED_HEADER_HIDDEN: (state, show) => { TOGGLE_FIXED_HEADER_HIDDEN: (state, show) => {
Vue.ls.set(DEFAULT_FIXED_HEADER_HIDDEN, show) Vue.ls.set(DEFAULT_FIXED_HEADER_HIDDEN, show)
@ -87,12 +77,6 @@ const app = {
setSidebar({ commit }, type) { setSidebar({ commit }, type) {
commit('SET_SIDEBAR_TYPE', type) commit('SET_SIDEBAR_TYPE', type)
}, },
CloseSidebar({ commit }) {
commit('CLOSE_SIDEBAR')
},
ToggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
},
ToggleTheme({ commit }, theme) { ToggleTheme({ commit }, theme) {
commit('TOGGLE_THEME', theme) commit('TOGGLE_THEME', theme)
}, },
@ -105,8 +89,8 @@ const app = {
} }
commit('TOGGLE_FIXED_HEADER', fixedHeader) commit('TOGGLE_FIXED_HEADER', fixedHeader)
}, },
ToggleFixSiderbar({ commit }, fixSiderbar) { ToggleFixedSidebar({ commit }, fixedSidebar) {
commit('TOGGLE_FIXED_SIDERBAR', fixSiderbar) commit('TOGGLE_FIXED_SIDEBAR', fixedSidebar)
}, },
ToggleFixedHeaderHidden({ commit }, show) { ToggleFixedHeaderHidden({ commit }, show) {
commit('TOGGLE_FIXED_HEADER_HIDDEN', show) commit('TOGGLE_FIXED_HEADER_HIDDEN', show)

9
src/store/modules/option.js

@ -1,6 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import { OPTIONS } from '@/store/mutation-types' import { OPTIONS } from '@/store/mutation-types'
import optionApi from '@/api/option' import apiClient from '@/utils/api-client'
const keys = [ const keys = [
'blog_url', 'blog_url',
'developer_mode', 'developer_mode',
@ -28,10 +29,10 @@ const option = {
actions: { actions: {
refreshOptionsCache({ commit }) { refreshOptionsCache({ commit }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
optionApi apiClient.option
.listAllByKeys(keys) .listAsMapViewByKeys(keys)
.then(response => { .then(response => {
commit('SET_OPTIONS', response.data.data) commit('SET_OPTIONS', response.data)
resolve(response) resolve(response)
}) })
.catch(error => { .catch(error => {

60
src/store/modules/permission.js

@ -1,60 +0,0 @@
import { asyncRouterMap, constantRouterMap } from '@/config/router.config'
/**
* 过滤账户是否拥有某一个权限并将菜单从加载列表移除
*
* @param permission
* @param route
* @returns {boolean}
*/
function hasPermission(permission, route) {
if (route.meta && route.meta.permission) {
let flag = false
for (let i = 0, len = permission.length; i < len; i++) {
flag = route.meta.permission.includes(permission[i])
if (flag) {
return true
}
}
return false
}
return true
}
function filterAsyncRouter(routerMap, roles) {
const accessedRouters = routerMap.filter(route => {
if (hasPermission(roles.permissionList, route)) {
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, roles)
}
return true
}
return false
})
return accessedRouters
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
}
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data
const accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
commit('SET_ROUTERS', accessedRouters)
resolve()
})
}
}
}
export default permission

24
src/store/modules/user.js

@ -1,7 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import { ACCESS_TOKEN, USER } from '@/store/mutation-types' import { ACCESS_TOKEN, USER } from '@/store/mutation-types'
import adminApi from '@/api/admin' import apiClient from '@/utils/api-client'
import userApi from '@/api/user'
const user = { const user = {
state: { state: {
@ -25,7 +24,7 @@ const user = {
actions: { actions: {
installCleanToken({ commit }, installData) { installCleanToken({ commit }, installData) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
adminApi apiClient.installation
.install(installData) .install(installData)
.then(response => { .then(response => {
commit('CLEAR_TOKEN') commit('CLEAR_TOKEN')
@ -38,10 +37,10 @@ const user = {
}, },
refreshUserCache({ commit }) { refreshUserCache({ commit }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
userApi apiClient.user
.getProfile() .getProfile()
.then(response => { .then(response => {
commit('SET_USER', response.data.data) commit('SET_USER', response.data)
resolve(response) resolve(response)
}) })
.catch(error => { .catch(error => {
@ -51,10 +50,10 @@ const user = {
}, },
login({ commit }, { username, password, authcode }) { login({ commit }, { username, password, authcode }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
adminApi apiClient
.login(username, password, authcode) .login({ username, password, authcode })
.then(response => { .then(response => {
const token = response.data.data const token = response.data
Vue.$log.debug('Got token', token) Vue.$log.debug('Got token', token)
commit('SET_TOKEN', token) commit('SET_TOKEN', token)
@ -67,10 +66,11 @@ const user = {
}, },
logout({ commit }) { logout({ commit }) {
return new Promise(resolve => { return new Promise(resolve => {
adminApi apiClient
.logout() .logout()
.then(() => { .then(() => {
commit('CLEAR_TOKEN') commit('CLEAR_TOKEN')
commit('SET_USER', {})
resolve() resolve()
}) })
.catch(() => { .catch(() => {
@ -80,17 +80,17 @@ const user = {
}, },
refreshToken({ commit }, refreshToken) { refreshToken({ commit }, refreshToken) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
adminApi apiClient
.refreshToken(refreshToken) .refreshToken(refreshToken)
.then(response => { .then(response => {
const token = response.data.data const token = response.data
Vue.$log.debug('Got token', token) Vue.$log.debug('Got token', token)
commit('SET_TOKEN', token) commit('SET_TOKEN', token)
resolve(response) resolve(response)
}) })
.catch(error => { .catch(error => {
const data = error.response.data const data = error.data
Vue.$log.debug('Refresh error data', data) Vue.$log.debug('Refresh error data', data)
if (data && data.status === 400 && data.data === refreshToken) { if (data && data.status === 400 && data.data === refreshToken) {
// The refresh token expired // The refresh token expired

3
src/store/mutation-types.js

@ -4,11 +4,10 @@ export const DEFAULT_THEME = 'DEFAULT_THEME'
export const DEFAULT_LAYOUT_MODE = 'DEFAULT_LAYOUT_MODE' export const DEFAULT_LAYOUT_MODE = 'DEFAULT_LAYOUT_MODE'
export const DEFAULT_COLOR = 'DEFAULT_COLOR' export const DEFAULT_COLOR = 'DEFAULT_COLOR'
export const DEFAULT_FIXED_HEADER = 'DEFAULT_FIXED_HEADER' export const DEFAULT_FIXED_HEADER = 'DEFAULT_FIXED_HEADER'
export const DEFAULT_FIXED_SIDEMENU = 'DEFAULT_FIXED_SIDEMENU' export const DEFAULT_FIXED_SIDEBAR = 'DEFAULT_FIXED_SIDEBAR'
export const DEFAULT_FIXED_HEADER_HIDDEN = 'DEFAULT_FIXED_HEADER_HIDDEN' export const DEFAULT_FIXED_HEADER_HIDDEN = 'DEFAULT_FIXED_HEADER_HIDDEN'
export const DEFAULT_CONTENT_WIDTH_TYPE = 'DEFAULT_CONTENT_WIDTH_TYPE' export const DEFAULT_CONTENT_WIDTH_TYPE = 'DEFAULT_CONTENT_WIDTH_TYPE'
export const USER = 'USER' export const USER = 'USER'
export const API_URL = 'API_URL'
export const OPTIONS = 'OPTIONS' export const OPTIONS = 'OPTIONS'
export const LAYOUT_SETTING = 'LAYOUT_SETTING' export const LAYOUT_SETTING = 'LAYOUT_SETTING'

2
src/styles/global.less

@ -819,6 +819,7 @@ body {
&-item { &-item {
padding: 0; padding: 0;
height: 140px; height: 140px;
&-img { &-img {
display: block; display: block;
height: 100%; height: 100%;
@ -826,6 +827,7 @@ body {
background-size: 100%; background-size: 100%;
background-position: center; background-position: center;
} }
.attachments-group &-type { .attachments-group &-type {
font-size: 38px; font-size: 38px;
text-transform: capitalize; text-transform: capitalize;

1
src/styles/style.less

@ -1,4 +1,5 @@
@import './animate.less'; @import './animate.less';
.container-wrapper { .container-wrapper {
background: #ffffff; background: #ffffff;
position: absolute; position: absolute;

117
src/utils/api-client.js

@ -0,0 +1,117 @@
import { AdminApiClient, Axios, HaloRestAPIClient } from '@halo-dev/admin-api'
import store from '@/store'
import { message, notification } from 'ant-design-vue'
import { isObject } from './util'
const apiUrl = process.env.VUE_APP_API_URL ? process.env.VUE_APP_API_URL : 'http://localhost:8080'
const haloRestApiClient = new HaloRestAPIClient({
baseUrl: apiUrl
})
const apiClient = new AdminApiClient(haloRestApiClient)
haloRestApiClient.interceptors.request.use(
config => {
const token = store.getters.token
if (token && token.access_token) {
config.headers['Admin-Authorization'] = token.access_token
}
return config
},
error => {
console.log('request error', error)
return Promise.reject(error)
}
)
let isRefreshingToken = false
let pendingRequests = []
haloRestApiClient.interceptors.response.use(
response => {
return response
},
async error => {
if (Axios.isCancel(error)) {
return Promise.reject(error)
}
if (/Network Error/.test(error.message)) {
message.error('网络错误,请检查网络连接')
return Promise.reject(error)
}
const token = store.getters.token
const originalRequest = error.config
const response = error.response
const data = response ? response.data : null
if (data) {
if (data.status === 400) {
const params = data.data
if (isObject(params)) {
const paramMessages = Object.keys(params || {}).map(key => params[key])
notification.error({
message: data.message,
description: h => {
const errorNodes = paramMessages.map(errorDetail => {
return h('a-alert', {
props: {
message: errorDetail,
banner: true,
showIcon: false,
type: 'error'
}
})
})
return h('div', errorNodes)
},
duration: 10
})
} else {
message.error(data.message)
}
return Promise.reject(error)
}
if (data.status === 401) {
if (!isRefreshingToken) {
isRefreshingToken = true
try {
await store.dispatch('refreshToken', token.refresh_token)
pendingRequests.forEach(callback => callback())
pendingRequests = []
return Axios(originalRequest)
} catch (e) {
message.warning('当前登录状态已失效,请重新登录')
await store.dispatch('ToggleLoginModal', true)
return Promise.reject(e)
} finally {
isRefreshingToken = false
}
} else {
return new Promise(resolve => {
pendingRequests.push(() => {
resolve(Axios(originalRequest))
})
})
}
}
message.error(data.message || '服务器错误')
return Promise.reject(error)
}
message.error('网络异常')
return Promise.reject(error)
}
)
export default apiClient
export { haloRestApiClient }

1
src/utils/datetime.js

@ -1,5 +1,6 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn') dayjs.locale('zh-cn')
function datetimeFormat(datetime = new Date(), pattern = 'YYYY-MM-DD HH:mm') { function datetimeFormat(datetime = new Date(), pattern = 'YYYY-MM-DD HH:mm') {

2
src/utils/domUtil.js

@ -2,7 +2,7 @@ export const setDocumentTitle = function(title) {
document.title = title document.title = title
const ua = navigator.userAgent const ua = navigator.userAgent
// eslint-disable-next-line // eslint-disable-next-line
const regex = /\bMicroMessenger\/([\d\.]+)/ const regex = /\bMicroMessenger\/([\d\.]+)/
if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) { if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) {
const i = document.createElement('iframe') const i = document.createElement('iframe')
i.src = '/favicon.ico' i.src = '/favicon.ico'

20
src/utils/encrypt.js

@ -0,0 +1,20 @@
import CryptoJS from 'crypto-js'
const CRYPTO_KEY = 'halo-crypt'
export default {
encrypt(plaintObject) {
if (!plaintObject) {
return undefined
}
return CryptoJS.AES.encrypt(JSON.stringify(plaintObject), CRYPTO_KEY).toString()
},
decrypt(ciphertext) {
if (!ciphertext) {
return undefined
}
const bytes = CryptoJS.AES.decrypt(ciphertext, CRYPTO_KEY)
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
}
}

145
src/utils/service.js

@ -1,145 +0,0 @@
import axios from 'axios'
import Vue from 'vue'
import { message, notification } from 'ant-design-vue'
import store from '@/store'
import { isObject } from './util'
const service = axios.create({
timeout: 10000,
withCredentials: true
})
function setTokenToHeader(config) {
// set token
const token = store.getters.token
Vue.$log.debug('Got token from store', token)
if (token && token.access_token) {
config.headers['Admin-Authorization'] = token.access_token
}
}
async function reRequest(error) {
const config = error.response.config
setTokenToHeader(config)
return await axios.request(config)
}
let refreshTask = null
async function refreshToken(error) {
const refreshToken = store.getters.token.refresh_token
try {
if (refreshTask === null) {
refreshTask = store.dispatch('refreshToken', refreshToken)
}
await refreshTask
} catch (err) {
if (err.response && err.response.data && err.response.data.data === refreshToken) {
message.warning('当前登录状态已失效,请重新登录')
store.dispatch('ToggleLoginModal', true)
}
Vue.$log.error('Failed to refresh token', err)
} finally {
refreshTask = null
}
// Rerequest the request
return reRequest(error)
}
function getFieldValidationError(data) {
if (!isObject(data) || !isObject(data.data)) {
return null
}
const errorDetail = data.data
return Object.keys(errorDetail).map(key => errorDetail[key])
}
service.interceptors.request.use(
config => {
config.baseURL = store.getters.apiUrl
setTokenToHeader(config)
return config
},
error => {
return Promise.reject(error)
}
)
service.interceptors.response.use(
response => {
return response
},
error => {
if (axios.isCancel(error)) {
Vue.$log.debug('Cancelled uploading by user.')
return Promise.reject(error)
}
Vue.$log.error('Response failed', error)
const response = error.response
const status = response ? response.status : -1
Vue.$log.error('Server response status', status)
const data = response ? response.data : null
if (data) {
let handled = false
// Business response
Vue.$log.error('Business response status', data.status)
if (data.status === 400) {
const errorDetails = getFieldValidationError(data)
if (errorDetails) {
handled = true
notification.error({
message: data.message,
description: h => {
const errorNodes = errorDetails.map(errorDetail => {
return h('a-alert', {
props: {
message: errorDetail,
banner: true,
showIcon: false,
type: 'error'
}
})
})
return h('div', errorNodes)
},
duration: 10
})
}
} else if (data.status === 401) {
if (store.getters.token && store.getters.token.access_token === data.data) {
const res = refreshToken(error)
if (res !== error) {
return res
}
} else {
// Login
message.warning('当前登录状态已失效,请重新登录')
store.dispatch('ToggleLoginModal', true)
}
} else if (data.status === 403) {
// TODO handle 403 status error
} else if (data.status === 404) {
// TODO handle 404 status error
} else if (data.status === 500) {
// TODO handle 500 status error
}
if (!handled) {
message.error(data.message)
}
} else {
message.error('网络异常')
}
return Promise.reject(error)
}
)
export default service

2
src/utils/util.js

@ -11,7 +11,7 @@ export function isObject(value) {
export function deepClone(source) { export function deepClone(source) {
if (!source && typeof source !== 'object') { if (!source && typeof source !== 'object') {
throw new Error('error arguments', 'deepClone') throw new Error('error arguments')
} }
const targetObj = source.constructor === Array ? [] : {} const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach(keys => { Object.keys(source).forEach(keys => {

87
src/views/attachment/AttachmentList.vue

@ -120,9 +120,9 @@
@showSizeChange="handlePageSizeChange" @showSizeChange="handlePageSizeChange"
/> />
</div> </div>
<a-modal v-model="upload.visible" :afterClose="onUploadClose" :footer="null" destroyOnClose title="上传附件">
<FilePondUpload ref="upload" :uploadHandler="upload.handler"></FilePondUpload> <AttachmentUploadModal :visible.sync="upload.visible" @close="onUploadClose" />
</a-modal>
<AttachmentDetailModal <AttachmentDetailModal
:addToPhoto="true" :addToPhoto="true"
:attachment="list.selected" :attachment="list.selected"
@ -141,9 +141,48 @@
import { mixin, mixinDevice } from '@/mixins/mixin.js' import { mixin, mixinDevice } from '@/mixins/mixin.js'
import { PageView } from '@/layouts' import { PageView } from '@/layouts'
import AttachmentDetailModal from './components/AttachmentDetailModal.vue' import AttachmentDetailModal from './components/AttachmentDetailModal.vue'
import attachmentApi from '@/api/attachment' import apiClient from '@/utils/api-client'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
const attachmentType = {
LOCAL: {
type: 'LOCAL',
text: '本地'
},
SMMS: {
type: 'SMMS',
text: 'SM.MS'
},
UPOSS: {
type: 'UPOSS',
text: '又拍云'
},
QINIUOSS: {
type: 'QINIUOSS',
text: '七牛云'
},
ALIOSS: {
type: 'ALIOSS',
text: '阿里云'
},
BAIDUBOS: {
type: 'BAIDUBOS',
text: '百度云'
},
TENCENTCOS: {
type: 'TENCENTCOS',
text: '腾讯云'
},
HUAWEIOBS: {
type: 'HUAWEIOBS',
text: '华为云'
},
MINIO: {
type: 'MINIO',
text: 'MinIO'
}
}
export default { export default {
components: { components: {
PageView, PageView,
@ -152,7 +191,7 @@ export default {
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
filters: { filters: {
typeText(type) { typeText(type) {
return attachmentApi.type[type].text return attachmentType[type].text
} }
}, },
data() { data() {
@ -167,9 +206,9 @@ export default {
params: { params: {
page: 0, page: 0,
size: 18, size: 18,
keyword: null, keyword: undefined,
mediaType: null, mediaType: undefined,
attachmentType: null attachmentType: undefined
} }
}, },
@ -184,7 +223,6 @@ export default {
}, },
upload: { upload: {
handler: attachmentApi.upload,
visible: false visible: false
}, },
@ -240,12 +278,12 @@ export default {
try { try {
this.list.loading = true this.list.loading = true
const response = await attachmentApi.query(this.list.params) const response = await apiClient.attachment.list(this.list.params)
this.list.data = response.data.data.content this.list.data = response.data.content
this.list.total = response.data.data.total this.list.total = response.data.total
this.list.hasNext = response.data.data.hasNext this.list.hasNext = response.data.hasNext
this.list.hasPrevious = response.data.data.hasPrevious this.list.hasPrevious = response.data.hasPrevious
} catch (error) { } catch (error) {
this.$log.error(error) this.$log.error(error)
} finally { } finally {
@ -260,9 +298,9 @@ export default {
try { try {
this.mediaTypes.loading = true this.mediaTypes.loading = true
const response = await attachmentApi.getMediaTypes() const response = await apiClient.attachment.listMediaTypes()
this.mediaTypes.data = response.data.data this.mediaTypes.data = response.data
} catch (error) { } catch (error) {
this.$log.error(error) this.$log.error(error)
} finally { } finally {
@ -277,9 +315,9 @@ export default {
try { try {
this.types.loading = true this.types.loading = true
const response = await attachmentApi.getTypes() const response = await apiClient.attachment.listTypes()
this.types.data = response.data.data this.types.data = response.data
} catch (error) { } catch (error) {
this.$log.error(error) this.$log.error(error)
} finally { } finally {
@ -343,8 +381,8 @@ export default {
okText: '确定', okText: '确定',
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
await attachmentApi.delete(item.id) await apiClient.attachment.delete(item.id)
this.handleListAttachments() await this.handleListAttachments()
} }
}) })
} }
@ -378,9 +416,9 @@ export default {
* Reset query params * Reset query params
*/ */
handleResetParam() { handleResetParam() {
this.list.params.keyword = null this.list.params.keyword = undefined
this.list.params.mediaType = null this.list.params.mediaType = undefined
this.list.params.attachmentType = null this.list.params.attachmentType = undefined
this.handlePageChange() this.handlePageChange()
this.handleListMediaTypes() this.handleListMediaTypes()
this.handleListTypes() this.handleListTypes()
@ -393,7 +431,6 @@ export default {
this.handlePageChange() this.handlePageChange()
}, },
onUploadClose() { onUploadClose() {
this.$refs.upload.handleClearFileList()
this.handlePageChange() this.handlePageChange()
this.handleListMediaTypes() this.handleListMediaTypes()
this.handleListTypes() this.handleListTypes()
@ -443,7 +480,7 @@ export default {
title: '确定要批量删除选中的附件吗?', title: '确定要批量删除选中的附件吗?',
content: '一旦删除不可恢复,请谨慎操作', content: '一旦删除不可恢复,请谨慎操作',
onOk() { onOk() {
attachmentApi apiClient.attachment
.deleteInBatch(that.batchSelectedAttachments) .deleteInBatch(that.batchSelectedAttachments)
.then(() => { .then(() => {
that.handleCancelMultipleSelection() that.handleCancelMultipleSelection()

71
src/views/attachment/components/AttachmentDetailModal.vue

@ -1,7 +1,7 @@
<template> <template>
<a-modal title="附件详情" :width="isMobile() ? '100%' : '50%'" v-model="modalVisible"> <a-modal v-model="modalVisible" :width="isMobile() ? '100%' : '50%'" title="附件详情">
<a-row type="flex" :gutter="24"> <a-row :gutter="24" type="flex">
<a-col :xl="9" :lg="9" :md="24" :sm="24" :xs="24"> <a-col :lg="9" :md="24" :sm="24" :xl="9" :xs="24">
<div class="attach-detail-img pb-3"> <div class="attach-detail-img pb-3">
<a v-if="isImage" :href="attachment.path" target="_blank"> <a v-if="isImage" :href="attachment.path" target="_blank">
<img :src="attachment.path" class="w-full" loading="lazy" /> <img :src="attachment.path" class="w-full" loading="lazy" />
@ -9,14 +9,14 @@
<div v-else>此文件不支持预览</div> <div v-else>此文件不支持预览</div>
</div> </div>
</a-col> </a-col>
<a-col :xl="15" :lg="15" :md="24" :sm="24" :xs="24"> <a-col :lg="15" :md="24" :sm="24" :xl="15" :xs="24">
<a-list itemLayout="horizontal"> <a-list itemLayout="horizontal">
<a-list-item style="padding-top: 0;"> <a-list-item style="padding-top: 0;">
<a-list-item-meta> <a-list-item-meta>
<template slot="description" v-if="editable"> <template v-if="editable" slot="description">
<a-input ref="nameInput" v-model="attachment.name" @blur="handleUpdateName" /> <a-input ref="nameInput" v-model="attachment.name" @blur="handleUpdateName" />
</template> </template>
<template slot="description" v-else>{{ attachment.name }}</template> <template v-else slot="description">{{ attachment.name }}</template>
<span slot="title"> <span slot="title">
附件名 附件名
<a href="javascript:void(0);"> <a href="javascript:void(0);">
@ -89,16 +89,16 @@
<template #footer> <template #footer>
<slot name="extraFooter" /> <slot name="extraFooter" />
<a-popconfirm title="你确定要删除该附件?" @confirm="handleDelete" okText="确定" cancelText="取消"> <a-popconfirm cancelText="取消" okText="确定" title="你确定要删除该附件?" @confirm="handleDelete">
<ReactiveButton <ReactiveButton
type="danger"
@callback="handleDeletedCallback"
:loading="deleting"
:errored="deleteErrored" :errored="deleteErrored"
text="删除" :loading="deleting"
erroredText="删除失败"
icon="delete" icon="delete"
loadedText="删除成功" loadedText="删除成功"
erroredText="删除失败" text="删除"
type="danger"
@callback="handleDeletedCallback"
></ReactiveButton> ></ReactiveButton>
</a-popconfirm> </a-popconfirm>
</template> </template>
@ -107,14 +107,53 @@
<script> <script>
import { mixin, mixinDevice } from '@/mixins/mixin.js' import { mixin, mixinDevice } from '@/mixins/mixin.js'
import attachmentApi from '@/api/attachment' import apiClient from '@/utils/api-client'
const attachmentType = {
LOCAL: {
type: 'LOCAL',
text: '本地'
},
SMMS: {
type: 'SMMS',
text: 'SM.MS'
},
UPOSS: {
type: 'UPOSS',
text: '又拍云'
},
QINIUOSS: {
type: 'QINIUOSS',
text: '七牛云'
},
ALIOSS: {
type: 'ALIOSS',
text: '阿里云'
},
BAIDUBOS: {
type: 'BAIDUBOS',
text: '百度云'
},
TENCENTCOS: {
type: 'TENCENTCOS',
text: '腾讯云'
},
HUAWEIOBS: {
type: 'HUAWEIOBS',
text: '华为云'
},
MINIO: {
type: 'MINIO',
text: 'MinIO'
}
}
export default { export default {
name: 'AttachmentDetailModal', name: 'AttachmentDetailModal',
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
filters: { filters: {
typeText(type) { typeText(type) {
return type ? attachmentApi.type[type].text : '' return type ? attachmentType[type].text : ''
} }
}, },
props: { props: {
@ -158,7 +197,7 @@ export default {
try { try {
this.deleting = true this.deleting = true
await attachmentApi.delete(this.attachment.id) await apiClient.attachment.delete(this.attachment.id)
} catch (error) { } catch (error) {
this.$log.error(error) this.$log.error(error)
this.deleteErrored = true this.deleteErrored = true
@ -202,7 +241,7 @@ export default {
return return
} }
try { try {
await attachmentApi.update(this.attachment.id, this.attachment) await apiClient.attachment.update(this.attachment.id, this.attachment)
} catch (error) { } catch (error) {
this.$log.error(error) this.$log.error(error)
} finally { } finally {

85
src/views/attachment/components/AttachmentDrawer.vue

@ -1,35 +1,35 @@
<template> <template>
<div> <div>
<a-drawer <a-drawer
title="附件库" :afterVisibleChange="handleAfterVisibleChanged"
:visible="visible"
:width="isMobile() ? '100%' : '480'" :width="isMobile() ? '100%' : '480'"
closable closable
:visible="visible"
destroyOnClose destroyOnClose
title="附件库"
@close="onClose" @close="onClose"
:afterVisibleChange="handleAfterVisibleChanged"
> >
<a-row type="flex" align="middle"> <a-row align="middle" type="flex">
<a-input-search placeholder="搜索附件" v-model="queryParam.keyword" @search="handleQuery()" enterButton /> <a-input-search v-model="queryParam.keyword" enterButton placeholder="搜索附件" @search="handleQuery()" />
</a-row> </a-row>
<a-divider /> <a-divider />
<a-row type="flex" align="middle"> <a-row align="middle" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-spin :spinning="loading" class="attachments-group"> <a-spin :spinning="loading" class="attachments-group">
<a-empty v-if="formattedDatas.length === 0" /> <a-empty v-if="formattedDatas.length === 0" />
<div <div
v-else
class="attach-item attachments-group-item"
v-for="(item, index) in formattedDatas" v-for="(item, index) in formattedDatas"
v-else
:key="index" :key="index"
class="attach-item attachments-group-item"
@click="handleShowDetailDrawer(item)" @click="handleShowDetailDrawer(item)"
@contextmenu.prevent="handleContextMenu($event, item)" @contextmenu.prevent="handleContextMenu($event, item)"
> >
<span v-if="!handleJudgeMediaType(item)" class="attachments-group-item-type">{{ item.suffix }}</span> <span v-if="!handleJudgeMediaType(item)" class="attachments-group-item-type">{{ item.suffix }}</span>
<span <span
v-else v-else
class="attachments-group-item-img"
:style="`background-image:url(${item.thumbPath})`" :style="`background-image:url(${item.thumbPath})`"
class="attachments-group-item-img"
loading="lazy" loading="lazy"
/> />
</div> </div>
@ -40,10 +40,10 @@
<div class="page-wrapper"> <div class="page-wrapper">
<a-pagination <a-pagination
:current="pagination.page" :current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size" :defaultPageSize="pagination.size"
@change="handlePaginationChange" :total="pagination.total"
showLessItems showLessItems
@change="handlePaginationChange"
></a-pagination> ></a-pagination>
</div> </div>
@ -55,20 +55,18 @@
/> --> /> -->
<a-divider class="divider-transparent" /> <a-divider class="divider-transparent" />
<div class="bottom-control"> <div class="bottom-control">
<a-button @click="uploadVisible = true" type="primary">上传附件</a-button> <a-button type="primary" @click="uploadVisible = true">上传附件</a-button>
</div> </div>
</a-drawer> </a-drawer>
<a-modal title="上传附件" v-model="uploadVisible" :footer="null" :afterClose="onUploadClose" destroyOnClose> <AttachmentUploadModal :visible.sync="uploadVisible" @close="onUploadClose" />
<FilePondUpload ref="upload" :uploadHandler="uploadHandler"></FilePondUpload>
</a-modal>
</div> </div>
</template> </template>
<script> <script>
import { mixin, mixinDevice } from '@/mixins/mixin.js' import { mixin, mixinDevice } from '@/mixins/mixin.js'
// import AttachmentDetailDrawer from './AttachmentDetailDrawer' // import AttachmentDetailDrawer from './AttachmentDetailDrawer'
import attachmentApi from '@/api/attachment' import apiClient from '@/utils/api-client'
export default { export default {
name: 'AttachmentDrawer', name: 'AttachmentDrawer',
@ -89,7 +87,44 @@ export default {
}, },
data() { data() {
return { return {
attachmentType: attachmentApi.type, attachmentType: {
LOCAL: {
type: 'LOCAL',
text: '本地'
},
SMMS: {
type: 'SMMS',
text: 'SM.MS'
},
UPOSS: {
type: 'UPOSS',
text: '又拍云'
},
QINIUOSS: {
type: 'QINIUOSS',
text: '七牛云'
},
ALIOSS: {
type: 'ALIOSS',
text: '阿里云'
},
BAIDUBOS: {
type: 'BAIDUBOS',
text: '百度云'
},
TENCENTCOS: {
type: 'TENCENTCOS',
text: '腾讯云'
},
HUAWEIOBS: {
type: 'HUAWEIOBS',
text: '华为云'
},
MINIO: {
type: 'MINIO',
text: 'MinIO'
}
},
detailVisible: false, detailVisible: false,
attachmentDrawerVisible: false, attachmentDrawerVisible: false,
uploadVisible: false, uploadVisible: false,
@ -107,8 +142,7 @@ export default {
keyword: null keyword: null
}, },
attachments: [], attachments: [],
selectedAttachment: {}, selectedAttachment: {}
uploadHandler: attachmentApi.upload
} }
}, },
computed: { computed: {
@ -171,16 +205,14 @@ export default {
this.queryParam.page = this.pagination.page - 1 this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort this.queryParam.sort = this.pagination.sort
attachmentApi apiClient.attachment
.query(this.queryParam) .list(this.queryParam)
.then(response => { .then(response => {
this.attachments = response.data.data.content this.attachments = response.data.content
this.pagination.total = response.data.data.total this.pagination.total = response.data.total
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.loading = false
this.loading = false
}, 200)
}) })
}, },
handleQuery() { handleQuery() {
@ -192,7 +224,6 @@ export default {
this.handleListAttachments() this.handleListAttachments()
}, },
onUploadClose() { onUploadClose() {
this.$refs.upload.handleClearFileList()
this.handlePaginationChange(1, this.pagination.size) this.handlePaginationChange(1, this.pagination.size)
}, },
handleAfterVisibleChanged(visible) { handleAfterVisibleChanged(visible) {

1
src/views/comment/CommentList.vue

@ -16,6 +16,7 @@
<script> <script>
import { PageView } from '@/layouts' import { PageView } from '@/layouts'
import CommentTab from './components/CommentTab' import CommentTab from './components/CommentTab'
export default { export default {
components: { components: {
PageView, PageView,

131
src/views/comment/components/CommentDetail.vue

@ -1,131 +0,0 @@
<template>
<a-drawer
title="评论详情"
:width="isMobile() ? '100%' : '480'"
closable
:visible="visible"
destroyOnClose
@close="onClose"
>
<a-row type="flex" align="middle">
<a-col :span="24">
<a-list itemLayout="horizontal">
<a-list-item>
<a-list-item-meta :description="comment.author">
<span slot="title">评论者昵称</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta :description="comment.email">
<span slot="title">评论者邮箱</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta :description="comment.ipAddress">
<span slot="title">评论者 IP</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta>
<a slot="description" target="_blank" :href="comment.authorUrl">{{ comment.authorUrl }}</a>
<span slot="title">评论者网址</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta>
<span slot="description">
<a-badge :status="comment.statusProperty.status" :text="comment.statusProperty.text" />
</span>
<span slot="title">评论状态</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta>
<a slot="description" target="_blank" :href="comment.post.fullPath" v-if="this.type == 'posts'">{{
comment.post.title
}}</a>
<a slot="description" target="_blank" :href="comment.sheet.fullPath" v-else-if="this.type == 'sheets'">{{
comment.sheet.title
}}</a>
<span slot="title" v-if="this.type == 'posts'">评论文章</span>
<span slot="title" v-else-if="this.type == 'sheets'">评论页面</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta>
<template slot="description" v-if="editable">
<a-input type="textarea" :autoSize="{ minRows: 5 }" v-model="comment.content" />
</template>
<span slot="description" v-html="comment.content" v-else></span>
<span slot="title">评论内容</span>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-col>
</a-row>
<a-divider class="divider-transparent" />
<div class="bottom-control">
<a-space>
<a-button type="dashed" @click="handleEditComment" v-if="!editable">编辑</a-button>
<a-button type="primary" @click="handleUpdateComment" v-if="editable">保存</a-button>
<a-popconfirm title="你确定要将此评论者加入黑名单?" okText="确定" cancelText="取消">
<a-button type="danger">加入黑名单</a-button>
</a-popconfirm>
</a-space>
</div>
</a-drawer>
</template>
<script>
import { mixin, mixinDevice } from '@/mixins/mixin.js'
import commentApi from '@/api/comment'
export default {
name: 'CommentDetail',
mixins: [mixin, mixinDevice],
components: {},
data() {
return {
editable: false,
commentStatus: commentApi.commentStatus,
keys: ['blog_url']
}
},
model: {
prop: 'visible',
event: 'close'
},
props: {
comment: {
type: Object,
required: true
},
visible: {
type: Boolean,
required: false,
default: true
},
type: {
type: String,
required: false,
default: 'posts',
validator: function(value) {
return ['posts', 'sheets', 'journals'].indexOf(value) !== -1
}
}
},
methods: {
handleEditComment() {
this.editable = true
},
handleUpdateComment() {
commentApi.update(this.type, this.comment.id, this.comment).then(response => {
this.$log.debug('Updated comment', response.data.data)
this.$message.success('评论修改成功!')
})
this.editable = false
},
onClose() {
this.$emit('close', false)
}
}
}
</script>

278
src/views/comment/components/CommentTab.vue

@ -1,20 +1,20 @@
<template> <template>
<div class="comment-tab-wrapper"> <div class="comment-tab-wrapper">
<a-card :bordered="false" :bodyStyle="{ padding: 0 }"> <a-card :bodyStyle="{ padding: 0 }" :bordered="false">
<div class="table-page-search-wrapper"> <div class="table-page-search-wrapper">
<a-form layout="inline"> <a-form layout="inline">
<a-row :gutter="48"> <a-row :gutter="48">
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
<a-form-item label="关键词:"> <a-form-item label="关键词:">
<a-input v-model="queryParam.keyword" @keyup.enter="handleQuery()" /> <a-input v-model="list.params.keyword" @keyup.enter="handleQuery()" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
<a-form-item label="评论状态:"> <a-form-item label="评论状态:">
<a-select v-model="queryParam.status" placeholder="请选择评论状态" @change="handleQuery()" allowClear> <a-select v-model="list.params.status" allowClear placeholder="请选择评论状态" @change="handleQuery()">
<a-select-option v-for="status in Object.keys(commentStatus)" :key="status" :value="status">{{ <a-select-option v-for="status in Object.keys(commentStatus)" :key="status" :value="status">
commentStatus[status].text {{ commentStatus[status].text }}
}}</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -32,19 +32,19 @@
</div> </div>
<div class="table-operator"> <div class="table-operator">
<a-dropdown v-show="queryParam.status != null && queryParam.status !== '' && !isMobile()"> <a-dropdown v-show="list.params.status != null && list.params.status !== '' && !isMobile()">
<a-menu slot="overlay"> <a-menu slot="overlay">
<a-menu-item key="1" v-if="queryParam.status === 'AUDITING'"> <a-menu-item v-if="list.params.status === 'AUDITING'" key="1">
<a href="javascript:void(0);" @click="handleEditStatusMore(commentStatus.PUBLISHED.value)"> <a href="javascript:void(0);" @click="handleEditStatusMore(commentStatus.PUBLISHED.value)">
通过 通过
</a> </a>
</a-menu-item> </a-menu-item>
<a-menu-item key="2" v-if="queryParam.status === 'PUBLISHED' || queryParam.status === 'AUDITING'"> <a-menu-item v-if="list.params.status === 'PUBLISHED' || list.params.status === 'AUDITING'" key="2">
<a href="javascript:void(0);" @click="handleEditStatusMore(commentStatus.RECYCLE.value)"> <a href="javascript:void(0);" @click="handleEditStatusMore(commentStatus.RECYCLE.value)">
移到回收站 移到回收站
</a> </a>
</a-menu-item> </a-menu-item>
<a-menu-item key="3" v-if="queryParam.status === 'RECYCLE'"> <a-menu-item v-if="list.params.status === 'RECYCLE'" key="3">
<a href="javascript:void(0);" @click="handleDeleteMore"> <a href="javascript:void(0);" @click="handleDeleteMore">
永久删除 永久删除
</a> </a>
@ -60,15 +60,15 @@
<!-- Mobile --> <!-- Mobile -->
<a-list <a-list
v-if="isMobile()" v-if="isMobile()"
:dataSource="formattedComments"
:loading="list.loading"
:pagination="false"
itemLayout="vertical" itemLayout="vertical"
size="large" size="large"
:pagination="false"
:dataSource="formattedComments"
:loading="loading"
> >
<a-list-item slot="renderItem" slot-scope="item, index" :key="index"> <a-list-item :key="index" slot="renderItem" slot-scope="item, index">
<template slot="actions"> <template slot="actions">
<a-dropdown placement="topLeft" :trigger="['click']"> <a-dropdown :trigger="['click']" placement="topLeft">
<span> <span>
<a-icon type="bars" /> <a-icon type="bars" />
</span> </span>
@ -85,9 +85,9 @@
<a-menu-item v-else-if="item.status === 'RECYCLE'"> <a-menu-item v-else-if="item.status === 'RECYCLE'">
<a-popconfirm <a-popconfirm
:title="'你确定要还原该评论?'" :title="'你确定要还原该评论?'"
@confirm="handleEditStatusClick(item.id, 'PUBLISHED')"
okText="确定"
cancelText="取消" cancelText="取消"
okText="确定"
@confirm="handleEditStatusClick(item.id, 'PUBLISHED')"
> >
<a href="javascript:void(0);">还原</a> <a href="javascript:void(0);">还原</a>
</a-popconfirm> </a-popconfirm>
@ -95,9 +95,9 @@
<a-menu-item v-if="item.status === 'PUBLISHED' || item.status === 'AUDITING'"> <a-menu-item v-if="item.status === 'PUBLISHED' || item.status === 'AUDITING'">
<a-popconfirm <a-popconfirm
:title="'你确定要将该评论移到回收站?'" :title="'你确定要将该评论移到回收站?'"
@confirm="handleEditStatusClick(item.id, 'RECYCLE')"
okText="确定"
cancelText="取消" cancelText="取消"
okText="确定"
@confirm="handleEditStatusClick(item.id, 'RECYCLE')"
> >
<a href="javascript:void(0);">回收站</a> <a href="javascript:void(0);">回收站</a>
</a-popconfirm> </a-popconfirm>
@ -105,9 +105,9 @@
<a-menu-item v-else-if="item.status === 'RECYCLE'"> <a-menu-item v-else-if="item.status === 'RECYCLE'">
<a-popconfirm <a-popconfirm
:title="'你确定要永久删除该评论?'" :title="'你确定要永久删除该评论?'"
@confirm="handleDeleteClick(item.id)"
okText="确定"
cancelText="取消" cancelText="取消"
okText="确定"
@confirm="handleDeleteClick(item.id)"
> >
<a href="javascript:void(0);">删除</a> <a href="javascript:void(0);">删除</a>
</a-popconfirm> </a-popconfirm>
@ -126,25 +126,26 @@
<a v-if="type === 'posts'" :href="item.post.fullPath" target="_blank">{{ item.post.title }}</a> <a v-if="type === 'posts'" :href="item.post.fullPath" target="_blank">{{ item.post.title }}</a>
<a v-if="type === 'sheets'" :href="item.sheet.fullPath" target="_blank">{{ item.sheet.title }}</a> <a v-if="type === 'sheets'" :href="item.sheet.fullPath" target="_blank">{{ item.sheet.title }}</a>
</template> </template>
<a-avatar slot="avatar" size="large" :src="item.avatar" /> <a-avatar slot="avatar" :src="item.avatar" size="large" />
<span <span
v-if="item.authorUrl"
slot="title" slot="title"
style="max-width: 300px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;" style="max-width: 300px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;"
v-if="item.authorUrl"
> >
<a-icon type="user" v-if="item.isAdmin" style="margin-right: 3px;" />&nbsp; <a-icon v-if="item.isAdmin" style="margin-right: 3px;" type="user" />&nbsp;
<a :href="item.authorUrl" target="_blank">{{ item.author }}</a> <a :href="item.authorUrl" target="_blank">{{ item.author }}</a>
&nbsp;<small style="color:rgba(0, 0, 0, 0.45)">{{ item.createTime | timeAgo }}</small> &nbsp;<small style="color:rgba(0, 0, 0, 0.45)">{{ item.createTime | timeAgo }}</small>
</span> </span>
<span <span
v-else
slot="title" slot="title"
style="max-width: 300px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;" style="max-width: 300px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;"
v-else
> >
<a-icon type="user" v-if="item.isAdmin" style="margin-right: 3px;" />&nbsp;{{ item.author }}&nbsp;<small <a-icon v-if="item.isAdmin" style="margin-right: 3px;" type="user" />&nbsp;{{ item.author }}&nbsp;<small
style="color:rgba(0, 0, 0, 0.45)" style="color:rgba(0, 0, 0, 0.45)"
>{{ item.createTime | timeAgo }}</small
> >
{{ item.createTime | timeAgo }}
</small>
</span> </span>
</a-list-item-meta> </a-list-item-meta>
<p v-html="item.content"></p> <p v-html="item.content"></p>
@ -153,33 +154,33 @@
<!-- Desktop --> <!-- Desktop -->
<a-table <a-table
v-else v-else
:columns="columns"
:dataSource="formattedComments"
:loading="list.loading"
:pagination="false"
:rowKey="comment => comment.id" :rowKey="comment => comment.id"
:rowSelection="{ :rowSelection="{
selectedRowKeys: selectedRowKeys, selectedRowKeys: selectedRowKeys,
onChange: onSelectionChange, onChange: onSelectionChange,
getCheckboxProps: getCheckboxProps getCheckboxProps: getCheckboxProps
}" }"
:columns="columns"
:dataSource="formattedComments"
:loading="loading"
:pagination="false"
scrollToFirstRowOnChange scrollToFirstRowOnChange
> >
<template slot="author" slot-scope="text, record"> <template slot="author" slot-scope="text, record">
<a-icon type="user" v-if="record.isAdmin" style="margin-right: 3px;" /> <a-icon v-if="record.isAdmin" style="margin-right: 3px;" type="user" />
<a :href="record.authorUrl" target="_blank" v-if="record.authorUrl">{{ text }}</a> <a v-if="record.authorUrl" :href="record.authorUrl" target="_blank">{{ text }}</a>
<span v-else>{{ text }}</span> <span v-else>{{ text }}</span>
</template> </template>
<p class="comment-content-wrapper" slot="content" slot-scope="content" v-html="content"></p> <p slot="content" slot-scope="content" class="comment-content-wrapper" v-html="content"></p>
<span slot="status" slot-scope="statusProperty"> <span slot="status" slot-scope="statusProperty">
<a-badge :status="statusProperty.status" :text="statusProperty.text" /> <a-badge :status="statusProperty.status" :text="statusProperty.text" />
</span> </span>
<a v-if="type === 'posts'" slot="post" slot-scope="post" :href="post.fullPath" target="_blank">{{ <a v-if="type === 'posts'" slot="post" slot-scope="post" :href="post.fullPath" target="_blank">
post.title {{ post.title }}
}}</a> </a>
<a v-if="type === 'sheets'" slot="sheet" slot-scope="sheet" :href="sheet.fullPath" target="_blank">{{ <a v-if="type === 'sheets'" slot="sheet" slot-scope="sheet" :href="sheet.fullPath" target="_blank">
sheet.title {{ sheet.title }}
}}</a> </a>
<span slot="createTime" slot-scope="createTime"> <span slot="createTime" slot-scope="createTime">
<a-tooltip placement="top"> <a-tooltip placement="top">
<template slot="title"> <template slot="title">
@ -189,8 +190,8 @@
</a-tooltip> </a-tooltip>
</span> </span>
<span slot="action" slot-scope="text, record"> <span slot="action" slot-scope="text, record">
<a-dropdown :trigger="['click']" v-if="record.status === 'AUDITING'"> <a-dropdown v-if="record.status === 'AUDITING'" :trigger="['click']">
<a href="javascript:void(0);" class="ant-dropdown-link">通过</a> <a class="ant-dropdown-link" href="javascript:void(0);">通过</a>
<a-menu slot="overlay"> <a-menu slot="overlay">
<a-menu-item key="1"> <a-menu-item key="1">
<a href="javascript:void(0);" @click="handleEditStatusClick(record.id, 'PUBLISHED')">通过</a> <a href="javascript:void(0);" @click="handleEditStatusClick(record.id, 'PUBLISHED')">通过</a>
@ -201,16 +202,16 @@
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
<a href="javascript:void(0);" v-else-if="record.status === 'PUBLISHED'" @click="handleReplyClick(record)" <a v-else-if="record.status === 'PUBLISHED'" href="javascript:void(0);" @click="handleReplyClick(record)">
>回复</a 回复
> </a>
<a-popconfirm <a-popconfirm
v-else-if="record.status === 'RECYCLE'"
:title="'你确定要还原该评论?'" :title="'你确定要还原该评论?'"
@confirm="handleEditStatusClick(record.id, 'PUBLISHED')"
okText="确定"
cancelText="取消" cancelText="取消"
v-else-if="record.status === 'RECYCLE'" okText="确定"
@confirm="handleEditStatusClick(record.id, 'PUBLISHED')"
> >
<a href="javascript:void(0);">还原</a> <a href="javascript:void(0);">还原</a>
</a-popconfirm> </a-popconfirm>
@ -218,21 +219,21 @@
<a-divider type="vertical" /> <a-divider type="vertical" />
<a-popconfirm <a-popconfirm
v-if="record.status === 'PUBLISHED' || record.status === 'AUDITING'"
:title="'你确定要将该评论移到回收站?'" :title="'你确定要将该评论移到回收站?'"
@confirm="handleEditStatusClick(record.id, 'RECYCLE')"
okText="确定"
cancelText="取消" cancelText="取消"
v-if="record.status === 'PUBLISHED' || record.status === 'AUDITING'" okText="确定"
@confirm="handleEditStatusClick(record.id, 'RECYCLE')"
> >
<a href="javascript:void(0);">回收站</a> <a href="javascript:void(0);">回收站</a>
</a-popconfirm> </a-popconfirm>
<a-popconfirm <a-popconfirm
v-else-if="record.status === 'RECYCLE'"
:title="'你确定要永久删除该评论?'" :title="'你确定要永久删除该评论?'"
@confirm="handleDeleteClick(record.id)"
okText="确定"
cancelText="取消" cancelText="取消"
v-else-if="record.status === 'RECYCLE'" okText="确定"
@confirm="handleDeleteClick(record.id)"
> >
<a href="javascript:void(0);">删除</a> <a href="javascript:void(0);">删除</a>
</a-popconfirm> </a-popconfirm>
@ -240,15 +241,15 @@
</a-table> </a-table>
<div class="page-wrapper"> <div class="page-wrapper">
<a-pagination <a-pagination
class="pagination"
:current="pagination.page" :current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size" :defaultPageSize="pagination.size"
:pageSizeOptions="['1', '2', '5', '10', '20', '50', '100']" :pageSizeOptions="['10', '20', '50', '100']"
showSizeChanger :total="pagination.total"
@showSizeChange="handlePaginationChange" class="pagination"
@change="handlePaginationChange"
showLessItems showLessItems
showSizeChanger
@change="handlePageChange"
@showSizeChange="handlePageSizeChange"
/> />
</div> </div>
</div> </div>
@ -256,26 +257,26 @@
<a-modal <a-modal
v-if="selectedComment" v-if="selectedComment"
:title="'回复给:' + selectedComment.author"
v-model="replyCommentVisible" v-model="replyCommentVisible"
@close="onReplyClose" :title="'回复给:' + selectedComment.author"
destroyOnClose destroyOnClose
@close="onReplyClose"
> >
<template slot="footer"> <template slot="footer">
<ReactiveButton <ReactiveButton
type="primary"
@click="handleCreateClick"
@callback="handleRepliedCallback"
:loading="replying"
:errored="replyErrored" :errored="replyErrored"
text="回复" :loading="replying"
loadedText="回复成功"
erroredText="回复失败" erroredText="回复失败"
loadedText="回复成功"
text="回复"
type="primary"
@callback="handleRepliedCallback"
@click="handleCreateClick"
></ReactiveButton> ></ReactiveButton>
</template> </template>
<a-form-model ref="replyCommentForm" :model="replyComment" :rules="replyCommentRules" layout="vertical"> <a-form-model ref="replyCommentForm" :model="replyComment" :rules="replyCommentRules" layout="vertical">
<a-form-model-item prop="content"> <a-form-model-item prop="content">
<a-input ref="contentInput" type="textarea" :autoSize="{ minRows: 8 }" v-model="replyComment.content" /> <a-input ref="contentInput" v-model="replyComment.content" :autoSize="{ minRows: 8 }" type="textarea" />
</a-form-model-item> </a-form-model-item>
</a-form-model> </a-form-model>
</a-modal> </a-modal>
@ -284,7 +285,7 @@
<script> <script>
import { mixin, mixinDevice } from '@/mixins/mixin.js' import { mixin, mixinDevice } from '@/mixins/mixin.js'
import marked from 'marked' import marked from 'marked'
import commentApi from '@/api/comment' import apiClient from '@/utils/api-client'
const postColumns = [ const postColumns = [
{ {
@ -366,6 +367,27 @@ const sheetColumns = [
scopedSlots: { customRender: 'action' } scopedSlots: { customRender: 'action' }
} }
] ]
const commentStatus = {
PUBLISHED: {
value: 'PUBLISHED',
color: 'green',
status: 'success',
text: '已发布'
},
AUDITING: {
value: 'AUDITING',
color: 'yellow',
status: 'warning',
text: '待审核'
},
RECYCLE: {
value: 'RECYCLE',
color: 'red',
status: 'error',
text: '回收站'
}
}
export default { export default {
name: 'CommentTab', name: 'CommentTab',
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
@ -381,31 +403,29 @@ export default {
}, },
data() { data() {
return { return {
columns: this.type === 'posts' ? postColumns : sheetColumns, commentStatus,
replyCommentVisible: false, replyCommentVisible: false,
pagination: {
page: 1, list: {
size: 10, data: [],
sort: null, loading: false,
total: 1 total: 0,
}, hasPrevious: false,
queryParam: { hasNext: false,
page: 0, params: {
size: 10, page: 0,
sort: null, size: 10,
keyword: null, keyword: null,
status: null status: null
}
}, },
selectedRowKeys: [], selectedRowKeys: [],
selectedRows: [], selectedRows: [],
comments: [],
selectedComment: {}, selectedComment: {},
replyComment: {}, replyComment: {},
replyCommentRules: { replyCommentRules: {
content: [{ required: true, message: '* 内容不能为空', trigger: ['change'] }] content: [{ required: true, message: '* 内容不能为空', trigger: ['change'] }]
}, },
loading: false,
commentStatus: commentApi.commentStatus,
replying: false, replying: false,
replyErrored: false replyErrored: false
} }
@ -415,38 +435,47 @@ export default {
}, },
computed: { computed: {
formattedComments() { formattedComments() {
return this.comments.map(comment => { return this.list.data.map(comment => {
comment.statusProperty = this.commentStatus[comment.status] comment.statusProperty = this.commentStatus[comment.status]
comment.content = marked(comment.content) comment.content = marked(comment.content)
return comment return comment
}) })
},
pagination() {
return {
page: this.list.params.page + 1,
size: this.list.params.size,
total: this.list.total
}
},
columns() {
return this.type === 'posts' ? postColumns : sheetColumns
} }
}, },
methods: { methods: {
handleListComments() { async handleListComments() {
this.loading = true try {
this.queryParam.page = this.pagination.page - 1 this.list.loading = true
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort const response = await apiClient.comment.list(this.type, this.list.params)
commentApi
.queryComment(this.type, this.queryParam) this.list.data = response.data.content
.then(response => { this.list.total = response.data.total
this.comments = response.data.data.content this.list.hasPrevious = response.data.hasPrevious
this.pagination.total = response.data.data.total this.list.hasNext = response.data.hasNext
}) } catch (e) {
.finally(() => { this.$log.error(e)
setTimeout(() => { } finally {
this.loading = false this.list.loading = false
}, 200) }
})
}, },
handleQuery() { handleQuery() {
this.handleClearRowKeys() this.handleClearRowKeys()
this.handlePaginationChange(1, this.pagination.size) this.handlePageChange(1)
}, },
handleEditStatusClick(commentId, status) { handleEditStatusClick(commentId, status) {
commentApi apiClient.comment
.updateStatus(this.type, commentId, status) .updateStatusById(this.type, commentId, status)
.then(() => { .then(() => {
this.$message.success('操作成功!') this.$message.success('操作成功!')
}) })
@ -455,7 +484,7 @@ export default {
}) })
}, },
handleDeleteClick(commentId) { handleDeleteClick(commentId) {
commentApi apiClient.comment
.delete(this.type, commentId) .delete(this.type, commentId)
.then(() => { .then(() => {
this.$message.success('删除成功!') this.$message.success('删除成功!')
@ -486,7 +515,7 @@ export default {
_this.$refs.replyCommentForm.validate(valid => { _this.$refs.replyCommentForm.validate(valid => {
if (valid) { if (valid) {
_this.replying = true _this.replying = true
commentApi apiClient.comment
.create(_this.type, _this.replyComment) .create(_this.type, _this.replyComment)
.catch(() => { .catch(() => {
_this.replyErrored = true _this.replyErrored = true
@ -509,24 +538,37 @@ export default {
this.handleListComments() this.handleListComments()
} }
}, },
handlePaginationChange(page, pageSize) {
this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`) /**
this.pagination.page = page * Handle page change
this.pagination.size = pageSize */
handlePageChange(page = 1) {
this.list.params.page = page - 1
this.handleListComments() this.handleListComments()
}, },
/**
* Handle page size change
*/
handlePageSizeChange(current, size) {
this.$log.debug(`Current: ${current}, PageSize: ${size}`)
this.list.params.page = 0
this.list.params.size = size
this.handleListComments()
},
handleResetParam() { handleResetParam() {
this.queryParam.keyword = null this.list.params.keyword = null
this.queryParam.status = null this.list.params.status = null
this.handleClearRowKeys() this.handleClearRowKeys()
this.handlePaginationChange(1, this.pagination.size) this.handlePageChange(1)
}, },
handleEditStatusMore(status) { handleEditStatusMore(status) {
if (this.selectedRowKeys.length <= 0) { if (this.selectedRowKeys.length <= 0) {
this.$message.info('请至少选择一项!') this.$message.info('请至少选择一项!')
return return
} }
commentApi apiClient.comment
.updateStatusInBatch(this.type, this.selectedRowKeys, status) .updateStatusInBatch(this.type, this.selectedRowKeys, status)
.then(() => { .then(() => {
this.$log.debug(`commentIds: ${this.selectedRowKeys}, status: ${status}`) this.$log.debug(`commentIds: ${this.selectedRowKeys}, status: ${status}`)
@ -541,7 +583,7 @@ export default {
this.$message.info('请至少选择一项!') this.$message.info('请至少选择一项!')
return return
} }
commentApi apiClient.comment
.deleteInBatch(this.type, this.selectedRowKeys) .deleteInBatch(this.type, this.selectedRowKeys)
.then(() => { .then(() => {
this.$log.debug(`delete: ${this.selectedRowKeys}`) this.$log.debug(`delete: ${this.selectedRowKeys}`)
@ -566,7 +608,7 @@ export default {
getCheckboxProps(comment) { getCheckboxProps(comment) {
return { return {
props: { props: {
disabled: this.queryParam.status == null || this.queryParam.status === '', disabled: this.list.params.status == null || this.list.params.status === '',
name: comment.author name: comment.author
} }
} }

57
src/views/comment/components/TargetCommentDrawer.vue

@ -1,20 +1,20 @@
<template> <template>
<a-drawer <a-drawer
title="评论列表" :afterVisibleChange="handleAfterVisibleChanged"
:visible="visible"
:width="isMobile() ? '100%' : '480'" :width="isMobile() ? '100%' : '480'"
closable closable
:visible="visible"
destroyOnClose destroyOnClose
title="评论列表"
@close="onClose" @close="onClose"
:afterVisibleChange="handleAfterVisibleChanged"
> >
<a-row type="flex" align="middle"> <a-row align="middle" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-list itemLayout="horizontal"> <a-list itemLayout="horizontal">
<a-list-item> <a-list-item>
<a-list-item-meta> <a-list-item-meta>
<template slot="description"> <template slot="description">
<p v-html="description" class="comment-drawer-content"></p> <p class="comment-drawer-content" v-html="description"></p>
</template> </template>
<h3 slot="title">{{ title }}</h3> <h3 slot="title">{{ title }}</h3>
</a-list-item-meta> </a-list-item-meta>
@ -26,13 +26,13 @@
<a-spin :spinning="list.loading"> <a-spin :spinning="list.loading">
<a-empty v-if="list.data.length === 0" /> <a-empty v-if="list.data.length === 0" />
<TargetCommentTree <TargetCommentTree
v-else
v-for="(comment, index) in list.data" v-for="(comment, index) in list.data"
v-else
:key="index" :key="index"
:comment="comment" :comment="comment"
@reply="handleCommentReply"
@delete="handleCommentDelete" @delete="handleCommentDelete"
@editStatus="handleEditStatusClick" @editStatus="handleEditStatusClick"
@reply="handleCommentReply"
/> />
</a-spin> </a-spin>
</a-col> </a-col>
@ -41,32 +41,32 @@
<div class="page-wrapper"> <div class="page-wrapper">
<a-pagination <a-pagination
:current="list.pagination.page" :current="list.pagination.page"
:total="list.pagination.total"
:defaultPageSize="list.pagination.size" :defaultPageSize="list.pagination.size"
@change="handlePaginationChange" :total="list.pagination.total"
showLessItems showLessItems
@change="handlePaginationChange"
></a-pagination> ></a-pagination>
</div> </div>
<a-divider class="divider-transparent" /> <a-divider class="divider-transparent" />
<div class="bottom-control"> <div class="bottom-control">
<a-button type="primary" @click="handleCommentReply({})">评论</a-button> <a-button type="primary" @click="handleCommentReply({})">评论</a-button>
</div> </div>
<a-modal :title="replyModalTitle" v-model="replyModal.visible" @close="onReplyModalClose" destroyOnClose> <a-modal v-model="replyModal.visible" :title="replyModalTitle" destroyOnClose @close="onReplyModalClose">
<template slot="footer"> <template slot="footer">
<ReactiveButton <ReactiveButton
type="primary"
@click="handleReplyClick"
@callback="handleReplyCallback"
:loading="replyModal.saving"
:errored="replyModal.saveErrored" :errored="replyModal.saveErrored"
text="回复" :loading="replyModal.saving"
loadedText="回复成功"
erroredText="回复失败" erroredText="回复失败"
loadedText="回复成功"
text="回复"
type="primary"
@callback="handleReplyCallback"
@click="handleReplyClick"
></ReactiveButton> ></ReactiveButton>
</template> </template>
<a-form-model ref="replyCommentForm" :model="replyModal.model" :rules="replyModal.rules" layout="vertical"> <a-form-model ref="replyCommentForm" :model="replyModal.model" :rules="replyModal.rules" layout="vertical">
<a-form-model-item prop="content"> <a-form-model-item prop="content">
<a-input ref="contentInput" type="textarea" :autoSize="{ minRows: 8 }" v-model="replyModal.model.content" /> <a-input ref="contentInput" v-model="replyModal.model.content" :autoSize="{ minRows: 8 }" type="textarea" />
</a-form-model-item> </a-form-model-item>
</a-form-model> </a-form-model>
</a-modal> </a-modal>
@ -75,7 +75,8 @@
<script> <script>
import { mixin, mixinDevice } from '@/mixins/mixin.js' import { mixin, mixinDevice } from '@/mixins/mixin.js'
import TargetCommentTree from './TargetCommentTree' import TargetCommentTree from './TargetCommentTree'
import commentApi from '@/api/comment' import apiClient from '@/utils/api-client'
export default { export default {
name: 'TargetCommentDrawer', name: 'TargetCommentDrawer',
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
@ -148,16 +149,14 @@ export default {
this.list.queryParam.page = this.list.pagination.page - 1 this.list.queryParam.page = this.list.pagination.page - 1
this.list.queryParam.size = this.list.pagination.size this.list.queryParam.size = this.list.pagination.size
this.list.queryParam.sort = this.list.pagination.sort this.list.queryParam.sort = this.list.pagination.sort
commentApi apiClient.comment
.commentTree(this.target, this.id, this.list.queryParam) .listAsTreeView(this.target, this.id, this.list.queryParam)
.then(response => { .then(response => {
this.list.data = response.data.data.content this.list.data = response.data.content
this.list.pagination.total = response.data.data.total this.list.pagination.total = response.data.total
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.list.loading = false
this.list.loading = false
}, 200)
}) })
}, },
handlePaginationChange(page, pageSize) { handlePaginationChange(page, pageSize) {
@ -179,7 +178,7 @@ export default {
_this.$refs.replyCommentForm.validate(valid => { _this.$refs.replyCommentForm.validate(valid => {
if (valid) { if (valid) {
_this.replyModal.saving = true _this.replyModal.saving = true
commentApi apiClient.comment
.create(_this.target, _this.replyModal.model) .create(_this.target, _this.replyModal.model)
.catch(() => { .catch(() => {
_this.replyModal.saveErrored = true _this.replyModal.saveErrored = true
@ -203,8 +202,8 @@ export default {
} }
}, },
handleEditStatusClick(comment, status) { handleEditStatusClick(comment, status) {
commentApi apiClient.comment
.updateStatus(this.target, comment.id, status) .updateStatusById(this.target, comment.id, status)
.then(() => { .then(() => {
this.$message.success('操作成功!') this.$message.success('操作成功!')
}) })
@ -213,7 +212,7 @@ export default {
}) })
}, },
handleCommentDelete(comment) { handleCommentDelete(comment) {
commentApi apiClient.comment
.delete(this.target, comment.id) .delete(this.target, comment.id)
.then(() => { .then(() => {
this.$message.success('删除成功!') this.$message.success('删除成功!')

20
src/views/comment/components/TargetCommentTree.vue

@ -2,7 +2,7 @@
<div> <div>
<a-comment> <a-comment>
<template slot="actions"> <template slot="actions">
<a-dropdown :trigger="['click']" v-if="comment.status === 'AUDITING'"> <a-dropdown v-if="comment.status === 'AUDITING'" :trigger="['click']">
<span href="javascript:void(0);">通过</span> <span href="javascript:void(0);">通过</span>
<a-menu slot="overlay"> <a-menu slot="overlay">
<a-menu-item key="1"> <a-menu-item key="1">
@ -19,9 +19,9 @@
<a-popconfirm <a-popconfirm
v-else-if="comment.status === 'RECYCLE'" v-else-if="comment.status === 'RECYCLE'"
:title="'你确定要还原该评论?'" :title="'你确定要还原该评论?'"
@confirm="handleEditStatusClick('PUBLISHED')"
okText="确定"
cancelText="取消" cancelText="取消"
okText="确定"
@confirm="handleEditStatusClick('PUBLISHED')"
> >
<span>还原</span> <span>还原</span>
</a-popconfirm> </a-popconfirm>
@ -29,22 +29,22 @@
<a-popconfirm <a-popconfirm
v-if="comment.status === 'PUBLISHED' || comment.status === 'AUDITING'" v-if="comment.status === 'PUBLISHED' || comment.status === 'AUDITING'"
:title="'你确定要将该评论移到回收站?'" :title="'你确定要将该评论移到回收站?'"
@confirm="handleEditStatusClick('RECYCLE')"
okText="确定"
cancelText="取消" cancelText="取消"
okText="确定"
@confirm="handleEditStatusClick('RECYCLE')"
> >
<span>回收站</span> <span>回收站</span>
</a-popconfirm> </a-popconfirm>
<a-popconfirm :title="'你确定要永久删除该评论?'" @confirm="handleDeleteClick" okText="确定" cancelText="取消"> <a-popconfirm :title="'你确定要永久删除该评论?'" cancelText="取消" okText="确定" @confirm="handleDeleteClick">
<span>删除</span> <span>删除</span>
</a-popconfirm> </a-popconfirm>
</template> </template>
<a slot="author" :href="comment.authorUrl" target="_blank"> <a slot="author" :href="comment.authorUrl" target="_blank">
<a-icon type="user" v-if="comment.isAdmin" style="margin-right: 3px;" /> <a-icon v-if="comment.isAdmin" style="margin-right: 3px;" type="user" />
{{ comment.author }} {{ comment.author }}
</a> </a>
<a-avatar size="large" slot="avatar" :src="comment.avatar" :alt="comment.author" /> <a-avatar slot="avatar" :alt="comment.author" :src="comment.avatar" size="large" />
<p slot="content" v-html="content"></p> <p slot="content" v-html="content"></p>
<a-tooltip slot="datetime"> <a-tooltip slot="datetime">
<span slot="title">{{ comment.createTime | moment }}</span> <span slot="title">{{ comment.createTime | moment }}</span>
@ -55,11 +55,11 @@
v-for="(child, index) in comment.children" v-for="(child, index) in comment.children"
:key="index" :key="index"
:comment="child" :comment="child"
v-on="$listeners"
v-bind="$attrs" v-bind="$attrs"
@reply="handleReplyClick"
@delete="handleDeleteClick" @delete="handleDeleteClick"
@editStatus="handleEditStatusClick" @editStatus="handleEditStatusClick"
@reply="handleReplyClick"
v-on="$listeners"
/> />
</template> </template>
</a-comment> </a-comment>

150
src/views/dashboard/Dashboard.vue

@ -1,26 +1,26 @@
<template> <template>
<page-view> <page-view>
<a-row :gutter="12"> <a-row :gutter="12">
<a-col :xl="6" :lg="6" :md="12" :sm="12" :xs="12" class="mb-3"> <a-col :lg="6" :md="12" :sm="12" :xl="6" :xs="12" class="mb-3">
<analysis-card title="文章" :number="statisticsData.postCount"> <analysis-card :number="statisticsData.postCount" title="文章">
<router-link :to="{ name: 'PostWrite' }" slot="action"> <router-link slot="action" :to="{ name: 'PostWrite' }">
<a-icon v-if="statisticsLoading" type="loading" /> <a-icon v-if="statisticsLoading" type="loading" />
<a-icon v-else type="plus" /> <a-icon v-else type="plus" />
</router-link> </router-link>
</analysis-card> </analysis-card>
</a-col> </a-col>
<a-col :xl="6" :lg="6" :md="12" :sm="12" :xs="12" class="mb-3"> <a-col :lg="6" :md="12" :sm="12" :xl="6" :xs="12" class="mb-3">
<analysis-card title="评论" :number="statisticsData.commentCount"> <analysis-card :number="statisticsData.commentCount" title="评论">
<router-link :to="{ name: 'Comments' }" slot="action"> <router-link slot="action" :to="{ name: 'Comments' }">
<a-icon v-if="statisticsLoading" type="loading" /> <a-icon v-if="statisticsLoading" type="loading" />
<a-icon v-else type="unordered-list" /> <a-icon v-else type="unordered-list" />
</router-link> </router-link>
</analysis-card> </analysis-card>
</a-col> </a-col>
<a-col :xl="6" :lg="6" :md="12" :sm="12" :xs="12" class="mb-3"> <a-col :lg="6" :md="12" :sm="12" :xl="6" :xs="12" class="mb-3">
<analysis-card title="阅读量" :number="statisticsData.visitCount"> <analysis-card :number="statisticsData.visitCount" title="阅读量">
<a-tooltip slot="action"> <a-tooltip slot="action">
<template slot="title"> 文章阅读共 {{ statisticsData.visitCount }} </template> <template slot="title"> 文章阅读共 {{ statisticsData.visitCount }} </template>
<a href="javascript:void(0);"> <a href="javascript:void(0);">
<a-icon v-if="statisticsLoading" type="loading" /> <a-icon v-if="statisticsLoading" type="loading" />
<a-icon v-else type="info-circle-o" /> <a-icon v-else type="info-circle-o" />
@ -28,8 +28,8 @@
</a-tooltip> </a-tooltip>
</analysis-card> </analysis-card>
</a-col> </a-col>
<a-col :xl="6" :lg="6" :md="12" :sm="12" :xs="12" class="mb-3"> <a-col :lg="6" :md="12" :sm="12" :xl="6" :xs="12" class="mb-3">
<analysis-card title="建立天数" :number="statisticsData.establishDays"> <analysis-card :number="statisticsData.establishDays" title="建立天数">
<a-tooltip slot="action"> <a-tooltip slot="action">
<template slot="title">博客建立于 {{ statisticsData.birthday | moment }}</template> <template slot="title">博客建立于 {{ statisticsData.birthday | moment }}</template>
<a href="javascript:void(0);"> <a href="javascript:void(0);">
@ -41,29 +41,31 @@
</a-col> </a-col>
</a-row> </a-row>
<a-row :gutter="12"> <a-row :gutter="12">
<a-col :xl="8" :lg="8" :md="12" :sm="24" :xs="24" class="mb-3"> <a-col :lg="8" :md="12" :sm="24" :xl="8" :xs="24" class="mb-3">
<a-card :bordered="false" title="新动态" :bodyStyle="{ padding: 0 }"> <a-card :bodyStyle="{ padding: 0 }" :bordered="false" title="新动态">
<div class="card-container"> <div class="card-container">
<a-tabs type="card"> <a-tabs type="card">
<a-tab-pane key="1" tab="最近文章"> <a-tab-pane key="1" tab="最近文章">
<a-list :loading="activityLoading" :dataSource="latestPosts"> <a-list :dataSource="latestPosts" :loading="activityLoading">
<a-list-item slot="renderItem" slot-scope="item, index" :key="index"> <a-list-item :key="index" slot="renderItem" slot-scope="item, index">
<a-list-item-meta> <a-list-item-meta>
<a <a
v-if="['PUBLISHED', 'INTIMATE'].includes(item.status)" v-if="['PUBLISHED', 'INTIMATE'].includes(item.status)"
slot="title" slot="title"
:href="item.fullPath" :href="item.fullPath"
target="_blank" target="_blank"
>{{ item.title }}</a
> >
{{ item.title }}
</a>
<a <a
v-else-if="item.status === 'DRAFT'" v-else-if="item.status === 'DRAFT'"
slot="title" slot="title"
href="javascript:void(0)" href="javascript:void(0)"
@click="handlePostPreview(item.id)" @click="handlePostPreview(item.id)"
>{{ item.title }}</a
> >
<a v-else-if="item.status === 'RECYCLE'" slot="title" href="javascript:void(0);" disabled> {{ item.title }}
</a>
<a v-else-if="item.status === 'RECYCLE'" slot="title" disabled href="javascript:void(0);">
{{ item.title }} {{ item.title }}
</a> </a>
</a-list-item-meta> </a-list-item-meta>
@ -74,18 +76,12 @@
<a-tab-pane key="2" tab="最近评论"> <a-tab-pane key="2" tab="最近评论">
<div class="custom-tab-wrapper"> <div class="custom-tab-wrapper">
<a-tabs :animated="{ inkBar: true, tabPane: false }"> <a-tabs :animated="{ inkBar: true, tabPane: false }">
<a-tab-pane tab="文章" key="1"> <a-tab-pane key="1" tab="文章">
<recent-comment-tab type="posts"></recent-comment-tab> <recent-comment-tab type="posts"></recent-comment-tab>
</a-tab-pane> </a-tab-pane>
<a-tab-pane tab="页面" key="2"> <a-tab-pane key="2" tab="页面">
<recent-comment-tab type="sheets"></recent-comment-tab> <recent-comment-tab type="sheets"></recent-comment-tab>
</a-tab-pane> </a-tab-pane>
<!-- <a-tab-pane
tab="日志"
key="3"
>
<recent-comment-tab type="journals"></recent-comment-tab>
</a-tab-pane>-->
</a-tabs> </a-tabs>
</div> </div>
</a-tab-pane> </a-tab-pane>
@ -93,11 +89,11 @@
</div> </div>
</a-card> </a-card>
</a-col> </a-col>
<a-col :xl="8" :lg="8" :md="12" :sm="24" :xs="24" class="mb-3"> <a-col :lg="8" :md="12" :sm="24" :xl="8" :xs="24" class="mb-3">
<JournalPublishCard /> <JournalPublishCard />
</a-col> </a-col>
<a-col :xl="8" :lg="8" :md="12" :sm="24" :xs="24" class="mb-3"> <a-col :lg="8" :md="12" :sm="24" :xl="8" :xs="24" class="mb-3">
<a-card :bordered="false" :bodyStyle="{ padding: '16px' }"> <a-card :bodyStyle="{ padding: '16px' }" :bordered="false">
<template slot="title"> <template slot="title">
操作记录 操作记录
<a-tooltip slot="action" title="更多"> <a-tooltip slot="action" title="更多">
@ -107,7 +103,7 @@
</a-tooltip> </a-tooltip>
</template> </template>
<a-list :dataSource="formattedLogDatas" :loading="logLoading"> <a-list :dataSource="formattedLogDatas" :loading="logLoading">
<a-list-item slot="renderItem" slot-scope="item, index" :key="index"> <a-list-item :key="index" slot="renderItem" slot-scope="item, index">
<a-list-item-meta :description="item.createTime | timeAgo"> <a-list-item-meta :description="item.createTime | timeAgo">
<span slot="title">{{ item.type }}</span> <span slot="title">{{ item.type }}</span>
</a-list-item-meta> </a-list-item-meta>
@ -129,9 +125,8 @@ import JournalPublishCard from './components/JournalPublishCard'
import RecentCommentTab from './components/RecentCommentTab' import RecentCommentTab from './components/RecentCommentTab'
import LogListDrawer from './components/LogListDrawer' import LogListDrawer from './components/LogListDrawer'
import postApi from '@/api/post' import apiClient from '@/utils/api-client'
import logApi from '@/api/log'
import statisticsApi from '@/api/statistics'
export default { export default {
name: 'Dashboard', name: 'Dashboard',
components: { components: {
@ -143,7 +138,64 @@ export default {
}, },
data() { data() {
return { return {
logTypes: logApi.logTypes, logTypes: {
BLOG_INITIALIZED: {
value: 0,
text: '博客初始化'
},
POST_PUBLISHED: {
value: 5,
text: '文章发布'
},
POST_EDITED: {
value: 15,
text: '文章修改'
},
POST_DELETED: {
value: 20,
text: '文章删除'
},
LOGGED_IN: {
value: 25,
text: '用户登录'
},
LOGGED_OUT: {
value: 30,
text: '注销登录'
},
LOGIN_FAILED: {
value: 35,
text: '登录失败'
},
PASSWORD_UPDATED: {
value: 40,
text: '修改密码'
},
PROFILE_UPDATED: {
value: 45,
text: '资料修改'
},
SHEET_PUBLISHED: {
value: 50,
text: '页面发布'
},
SHEET_EDITED: {
value: 55,
text: '页面修改'
},
SHEET_DELETED: {
value: 60,
text: '页面删除'
},
MFA_UPDATED: {
value: 65,
text: '两步验证'
},
LOGGED_PRE_CHECK: {
value: 70,
text: '登录验证'
}
},
activityLoading: false, activityLoading: false,
logLoading: false, logLoading: false,
statisticsLoading: true, statisticsLoading: true,
@ -196,47 +248,41 @@ export default {
methods: { methods: {
handleListLatestPosts() { handleListLatestPosts() {
this.activityLoading = true this.activityLoading = true
postApi apiClient.post
.listLatest(5) .latest(5)
.then(response => { .then(response => {
this.latestPosts = response.data.data this.latestPosts = response.data
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.activityLoading = false
this.activityLoading = false
}, 200)
}) })
}, },
handleListLatestLogs() { handleListLatestLogs() {
this.logLoading = true this.logLoading = true
logApi apiClient.log
.listLatest(5) .latest(5)
.then(response => { .then(response => {
this.latestLogs = response.data.data this.latestLogs = response.data
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.logLoading = false
this.logLoading = false
}, 200)
}) })
}, },
handleLoadStatistics() { handleLoadStatistics() {
statisticsApi apiClient.statistic
.statistics() .statistics()
.then(response => { .then(response => {
this.statisticsData = response.data.data this.statisticsData = response.data
}) })
.catch(() => { .catch(() => {
clearInterval(this.interval) clearInterval(this.interval)
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.statisticsLoading = false
this.statisticsLoading = false
}, 200)
}) })
}, },
handlePostPreview(postId) { handlePostPreview(postId) {
postApi.preview(postId).then(response => { apiClient.post.getPreviewLinkById(postId).then(response => {
window.open(response.data, '_blank') window.open(response.data, '_blank')
}) })
}, },

7
src/views/dashboard/components/AnalysisCard.vue

@ -12,10 +12,10 @@
<div class="number"> <div class="number">
<slot name="number"> <slot name="number">
<countTo <countTo
:startVal="startNumber"
:endVal="(typeof number === 'function' && number()) || number"
:duration="3000"
:autoplay="true" :autoplay="true"
:duration="3000"
:endVal="(typeof number === 'function' && number()) || number"
:startVal="startNumber"
></countTo> ></countTo>
</slot> </slot>
</div> </div>
@ -25,6 +25,7 @@
<script> <script>
import countTo from 'vue-count-to' import countTo from 'vue-count-to'
export default { export default {
name: 'AnalysisCard', name: 'AnalysisCard',
components: { components: {

25
src/views/dashboard/components/JournalPublishCard.vue

@ -1,41 +1,42 @@
<template> <template>
<a-card :bordered="false" :bodyStyle="{ padding: '16px' }"> <a-card :bodyStyle="{ padding: '16px' }" :bordered="false">
<template slot="title"> <template slot="title">
速记 速记
<a-tooltip slot="action" title="内容将保存到页面/所有页面/日志页面"> <a-tooltip slot="action" title="内容将保存到页面/所有页面/日志页面">
<a-icon type="info-circle-o" class="cursor-pointer" /> <a-icon class="cursor-pointer" type="info-circle-o" />
</a-tooltip> </a-tooltip>
</template> </template>
<a-form-model ref="journalForm" :model="form.model" :rules="form.rules" layout="vertical"> <a-form-model ref="journalForm" :model="form.model" :rules="form.rules" layout="vertical">
<a-form-model-item prop="sourceContent"> <a-form-model-item prop="sourceContent">
<a-input <a-input
type="textarea"
:autoSize="{ minRows: 8 }"
v-model="form.model.sourceContent" v-model="form.model.sourceContent"
:autoSize="{ minRows: 8 }"
placeholder="写点什么吧..." placeholder="写点什么吧..."
type="textarea"
/> />
</a-form-model-item> </a-form-model-item>
<a-form-model-item> <a-form-model-item>
<ReactiveButton <ReactiveButton
@click="handleCreateJournalClick" :errored="form.errored"
:loading="form.saving"
erroredText="发布失败"
loadedText="发布成功"
text="发布"
@callback=" @callback="
() => { () => {
if (!form.errored) form.model = {} if (!form.errored) form.model = {}
form.errored = false form.errored = false
} }
" "
:loading="form.saving" @click="handleCreateJournalClick"
:errored="form.errored"
text="发布"
loadedText="发布成功"
erroredText="发布失败"
></ReactiveButton> ></ReactiveButton>
</a-form-model-item> </a-form-model-item>
</a-form-model> </a-form-model>
</a-card> </a-card>
</template> </template>
<script> <script>
import journalApi from '@/api/journal' import apiClient from '@/utils/api-client'
export default { export default {
name: 'JournalPublishCard', name: 'JournalPublishCard',
data() { data() {
@ -56,7 +57,7 @@ export default {
_this.$refs.journalForm.validate(valid => { _this.$refs.journalForm.validate(valid => {
if (valid) { if (valid) {
_this.form.saving = true _this.form.saving = true
journalApi apiClient.journal
.create(_this.form.model) .create(_this.form.model)
.catch(() => { .catch(() => {
this.form.errored = true this.form.errored = true

98
src/views/dashboard/components/LogListDrawer.vue

@ -1,18 +1,18 @@
<template> <template>
<div> <div>
<a-drawer <a-drawer
title="操作日志" :afterVisibleChange="handleAfterVisibleChanged"
:visible="visible"
:width="isMobile() ? '100%' : '480'" :width="isMobile() ? '100%' : '480'"
closable closable
:visible="visible"
destroyOnClose destroyOnClose
title="操作日志"
@close="onClose" @close="onClose"
:afterVisibleChange="handleAfterVisibleChanged"
> >
<a-row type="flex" align="middle"> <a-row align="middle" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-list :loading="loading" :dataSource="formattedLogsDatas"> <a-list :dataSource="formattedLogsDatas" :loading="loading">
<a-list-item slot="renderItem" slot-scope="item, index" :key="index"> <a-list-item :key="index" slot="renderItem" slot-scope="item, index">
<a-list-item-meta :description="item.createTime | timeAgo"> <a-list-item-meta :description="item.createTime | timeAgo">
<span slot="title">{{ item.type }}</span> <span slot="title">{{ item.type }}</span>
</a-list-item-meta> </a-list-item-meta>
@ -22,22 +22,22 @@
<div class="page-wrapper"> <div class="page-wrapper">
<a-pagination <a-pagination
class="pagination"
:current="pagination.page" :current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size" :defaultPageSize="pagination.size"
:pageSizeOptions="['50', '100', '150', '200']" :pageSizeOptions="['50', '100', '150', '200']"
:total="pagination.total"
class="pagination"
showLessItems
showSizeChanger showSizeChanger
@showSizeChange="handlePaginationChange"
@change="handlePaginationChange" @change="handlePaginationChange"
showLessItems @showSizeChange="handlePaginationChange"
/> />
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
<a-divider class="divider-transparent" /> <a-divider class="divider-transparent" />
<div class="bottom-control"> <div class="bottom-control">
<a-popconfirm title="你确定要清空所有操作日志?" okText="确定" @confirm="handleClearLogs" cancelText="取消"> <a-popconfirm cancelText="取消" okText="确定" title="你确定要清空所有操作日志?" @confirm="handleClearLogs">
<a-button type="danger">清空操作日志</a-button> <a-button type="danger">清空操作日志</a-button>
</a-popconfirm> </a-popconfirm>
</div> </div>
@ -47,13 +47,71 @@
<script> <script>
import { mixin, mixinDevice } from '@/mixins/mixin.js' import { mixin, mixinDevice } from '@/mixins/mixin.js'
import logApi from '@/api/log' import apiClient from '@/utils/api-client'
export default { export default {
name: 'LogListDrawer', name: 'LogListDrawer',
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
data() { data() {
return { return {
logTypes: logApi.logTypes, logTypes: {
BLOG_INITIALIZED: {
value: 0,
text: '博客初始化'
},
POST_PUBLISHED: {
value: 5,
text: '文章发布'
},
POST_EDITED: {
value: 15,
text: '文章修改'
},
POST_DELETED: {
value: 20,
text: '文章删除'
},
LOGGED_IN: {
value: 25,
text: '用户登录'
},
LOGGED_OUT: {
value: 30,
text: '注销登录'
},
LOGIN_FAILED: {
value: 35,
text: '登录失败'
},
PASSWORD_UPDATED: {
value: 40,
text: '修改密码'
},
PROFILE_UPDATED: {
value: 45,
text: '资料修改'
},
SHEET_PUBLISHED: {
value: 50,
text: '页面发布'
},
SHEET_EDITED: {
value: 55,
text: '页面修改'
},
SHEET_DELETED: {
value: 60,
text: '页面删除'
},
MFA_UPDATED: {
value: 65,
text: '两步验证'
},
LOGGED_PRE_CHECK: {
value: 70,
text: '登录验证'
}
},
loading: true, loading: true,
logs: [], logs: [],
pagination: { pagination: {
@ -90,20 +148,18 @@ export default {
this.logQueryParam.page = this.pagination.page - 1 this.logQueryParam.page = this.pagination.page - 1
this.logQueryParam.size = this.pagination.size this.logQueryParam.size = this.pagination.size
this.logQueryParam.sort = this.pagination.sort this.logQueryParam.sort = this.pagination.sort
logApi apiClient.log
.pageBy(this.logQueryParam) .list(this.logQueryParam)
.then(response => { .then(response => {
this.logs = response.data.data.content this.logs = response.data.content
this.pagination.total = response.data.data.total this.pagination.total = response.data.total
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.loading = false
this.loading = false
}, 200)
}) })
}, },
handleClearLogs() { handleClearLogs() {
logApi apiClient.log
.clear() .clear()
.then(() => { .then(() => {
this.$message.success('清除成功!') this.$message.success('清除成功!')

29
src/views/dashboard/components/RecentCommentTab.vue

@ -1,8 +1,8 @@
<template> <template>
<a-list itemLayout="horizontal" :dataSource="formmatedCommentData" :loading="loading"> <a-list :dataSource="formmatedCommentData" :loading="loading" itemLayout="horizontal">
<a-list-item slot="renderItem" slot-scope="item, index" :key="index"> <a-list-item :key="index" slot="renderItem" slot-scope="item, index">
<a-comment :avatar="item.avatar"> <a-comment :avatar="item.avatar">
<template slot="author" v-if="type === 'posts'"> <template v-if="type === 'posts'" slot="author">
<a :href="item.authorUrl" target="_blank">{{ item.author }}</a> 发表在 <a <a :href="item.authorUrl" target="_blank">{{ item.author }}</a> 发表在 <a
v-if="['PUBLISHED', 'INTIMATE'].includes(item.post.status)" v-if="['PUBLISHED', 'INTIMATE'].includes(item.post.status)"
:href="item.post.fullPath" :href="item.post.fullPath"
@ -16,7 +16,7 @@
><a v-else href="javascript:void(0)">{{ item.post.title }}</a> ><a v-else href="javascript:void(0)">{{ item.post.title }}</a>
</template> </template>
<template slot="author" v-else-if="type === 'sheets'"> <template v-else-if="type === 'sheets'" slot="author">
<a :href="item.authorUrl" target="_blank">{{ item.author }}</a> 发表在 <a <a :href="item.authorUrl" target="_blank">{{ item.author }}</a> 发表在 <a
v-if="item.sheet.status === 'PUBLISHED'" v-if="item.sheet.status === 'PUBLISHED'"
:href="item.sheet.fullPath" :href="item.sheet.fullPath"
@ -33,7 +33,7 @@
<!-- <template slot="actions"> <!-- <template slot="actions">
<span>回复</span> <span>回复</span>
</template> --> </template> -->
<p class="comment-content-wrapper" slot="content" v-html="item.content"></p> <p slot="content" class="comment-content-wrapper" v-html="item.content"></p>
<a-tooltip slot="datetime" :title="item.createTime | moment"> <a-tooltip slot="datetime" :title="item.createTime | moment">
<span>{{ item.createTime | timeAgo }}</span> <span>{{ item.createTime | timeAgo }}</span>
</a-tooltip> </a-tooltip>
@ -43,11 +43,10 @@
</template> </template>
<script> <script>
import commentApi from '@/api/comment' import apiClient from '@/utils/api-client'
import postApi from '@/api/post'
import sheetApi from '@/api/sheet'
import marked from 'marked' import marked from 'marked'
export default { export default {
name: 'RecentCommentTab', name: 'RecentCommentTab',
props: { props: {
@ -80,24 +79,22 @@ export default {
methods: { methods: {
handleListTargetComments() { handleListTargetComments() {
this.loading = true this.loading = true
commentApi apiClient.comment
.latestComment(this.type, 5, 'PUBLISHED') .latest(this.type, 5, 'PUBLISHED')
.then(response => { .then(response => {
this.comments = response.data.data this.comments = response.data
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.loading = false
this.loading = false
}, 200)
}) })
}, },
handlePostPreview(postId) { handlePostPreview(postId) {
postApi.preview(postId).then(response => { apiClient.post.getPreviewLinkById(postId).then(response => {
window.open(response.data, '_blank') window.open(response.data, '_blank')
}) })
}, },
handleSheetPreview(sheetId) { handleSheetPreview(sheetId) {
sheetApi.preview(sheetId).then(response => { apiClient.post.getPreviewLinkById(sheetId).then(response => {
window.open(response.data, '_blank') window.open(response.data, '_blank')
}) })
} }

2
src/views/exception/ExceptionPage.vue

@ -1,6 +1,6 @@
<template> <template>
<div class="exception"> <div class="exception">
<a-result :status="type" :title="type" :subTitle="config[type].desc"> <a-result :status="type" :subTitle="config[type].desc" :title="type">
<template v-slot:extra> <template v-slot:extra>
<a-button type="primary" @click="handleToHome">返回仪表盘</a-button> <a-button type="primary" @click="handleToHome">返回仪表盘</a-button>
</template> </template>

86
src/views/interface/MenuList.vue

@ -1,28 +1,28 @@
<template> <template>
<page-view> <page-view>
<a-row :gutter="12"> <a-row :gutter="12">
<a-col :xl="6" :lg="6" :md="6" :sm="24" :xs="24" class="mb-3"> <a-col :lg="6" :md="6" :sm="24" :xl="6" :xs="24" class="mb-3">
<a-card :bodyStyle="{ padding: '16px' }" title="分组"> <a-card :bodyStyle="{ padding: '16px' }" title="分组">
<template slot="extra"> <template slot="extra">
<ReactiveButton <ReactiveButton
type="default"
@click="handleSetDefaultTeam"
@callback="handleSetDefaultTeamCallback"
:loading="teams.default.saving"
:errored="teams.default.errored" :errored="teams.default.errored"
text="设为默认" :loading="teams.default.saving"
loadedText="设置成功"
erroredText="设置失败" erroredText="设置失败"
loadedText="设置成功"
text="设为默认"
type="default"
@callback="handleSetDefaultTeamCallback"
@click="handleSetDefaultTeam"
></ReactiveButton> ></ReactiveButton>
</template> </template>
<div class="menu-teams"> <div class="menu-teams">
<a-spin :spinning="teams.loading"> <a-spin :spinning="teams.loading">
<a-empty v-if="teams.data.length === 0 && !teams.loading" /> <a-empty v-if="teams.data.length === 0 && !teams.loading" />
<a-menu <a-menu
v-if="teams.data.length > 0"
v-model="selectedTeam"
class="w-full" class="w-full"
mode="inline" mode="inline"
v-model="selectedTeam"
v-if="teams.data.length > 0"
@select="handleSelectedTeam" @select="handleSelectedTeam"
> >
<a-menu-item v-for="team in teams.data" :key="team"> <a-menu-item v-for="team in teams.data" :key="team">
@ -33,11 +33,11 @@
</div> </div>
<a-popover <a-popover
v-model="teams.form.visible" v-model="teams.form.visible"
destroyTooltipOnHide
placement="bottom"
title="新增分组" title="新增分组"
trigger="click" trigger="click"
placement="bottom"
@visibleChange="handleTeamFormVisibleChange" @visibleChange="handleTeamFormVisibleChange"
destroyTooltipOnHide
> >
<template slot="content"> <template slot="content">
<a-form-model <a-form-model
@ -56,42 +56,42 @@
</a-form-model-item> </a-form-model-item>
</a-form-model> </a-form-model>
</template> </template>
<a-button type="primary" block class="mt-3"> <a-button block class="mt-3" type="primary">
新增分组 新增分组
</a-button> </a-button>
</a-popover> </a-popover>
</a-card> </a-card>
</a-col> </a-col>
<a-col :xl="18" :lg="18" :md="18" :sm="24" :xs="24" class="pb-3"> <a-col :lg="18" :md="18" :sm="24" :xl="18" :xs="24" class="pb-3">
<a-card :bodyStyle="{ padding: '16px' }"> <a-card :bodyStyle="{ padding: '16px' }">
<template slot="title"> <template slot="title">
<span> <span>
{{ menuListTitle }} {{ menuListTitle }}
</span> </span>
<a-tooltip <a-tooltip
v-if="list.data.length <= 0 && !list.loading"
slot="action" slot="action"
title="分组下的菜单为空时,该分组也不会保存" title="分组下的菜单为空时,该分组也不会保存"
v-if="list.data.length <= 0 && !list.loading"
> >
<a-icon type="info-circle-o" class="cursor-pointer" /> <a-icon class="cursor-pointer" type="info-circle-o" />
</a-tooltip> </a-tooltip>
</template> </template>
<template slot="extra"> <template slot="extra">
<a-space> <a-space>
<ReactiveButton <ReactiveButton
@click="handleUpdateBatch" :disabled="list.data.length <= 0"
@callback="formBatch.errored = false"
:loading="formBatch.saving"
:errored="formBatch.errored" :errored="formBatch.errored"
text="保存" :loading="formBatch.saving"
loadedText="保存成功"
erroredText="保存失败" erroredText="保存失败"
:disabled="list.data.length <= 0" loadedText="保存成功"
text="保存"
@callback="formBatch.errored = false"
@click="handleUpdateBatch"
></ReactiveButton> ></ReactiveButton>
<a-button v-if="!form.visible" @click="handleOpenCreateMenuForm()" type="primary" ghost> <a-button v-if="!form.visible" ghost type="primary" @click="handleOpenCreateMenuForm()">
新增 新增
</a-button> </a-button>
<a-button v-else @click="handleCloseCreateMenuForm()" type="default"> <a-button v-else type="default" @click="handleCloseCreateMenuForm()">
取消新增 取消新增
</a-button> </a-button>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
@ -114,8 +114,8 @@
<MenuForm <MenuForm
v-if="form.visible" v-if="form.visible"
:menu="form.model" :menu="form.model"
@succeed="handleCreateMenuSucceed()"
@cancel="handleCloseCreateMenuForm()" @cancel="handleCloseCreateMenuForm()"
@succeed="handleCreateMenuSucceed()"
/> />
<a-empty v-if="list.data.length === 0 && !list.loading && !form.visible" /> <a-empty v-if="list.data.length === 0 && !list.loading && !form.visible" />
<MenuTreeNode v-model="list.data" :excludedTeams="excludedTeams" @reload="handleListMenus" /> <MenuTreeNode v-model="list.data" :excludedTeams="excludedTeams" @reload="handleListMenus" />
@ -142,8 +142,8 @@ import { deepClone } from '@/utils/util'
import { mapActions, mapGetters } from 'vuex' import { mapActions, mapGetters } from 'vuex'
// apis // apis
import menuApi from '@/api/menu' import apiClient from '@/utils/api-client'
import optionApi from '@/api/option'
export default { export default {
components: { PageView, MenuTreeNode, MenuForm, MenuInternalLinkSelector }, components: { PageView, MenuTreeNode, MenuForm, MenuInternalLinkSelector },
data() { data() {
@ -224,33 +224,29 @@ export default {
...mapActions(['refreshOptionsCache']), ...mapActions(['refreshOptionsCache']),
handleListTeams(autoSelectTeam = false) { handleListTeams(autoSelectTeam = false) {
this.teams.loading = true this.teams.loading = true
menuApi apiClient.menu
.listTeams() .listTeams()
.then(response => { .then(response => {
this.teams.data = response.data.data this.teams.data = response.data
if (!this.teams.selected || autoSelectTeam) { if (!this.teams.selected || autoSelectTeam) {
this.teams.selected = this.teams.data[0] this.teams.selected = this.teams.data[0]
} }
this.handleListMenus() this.handleListMenus()
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.teams.loading = false
this.teams.loading = false
}, 200)
}) })
}, },
handleListMenus() { handleListMenus() {
this.list.data = [] this.list.data = []
this.list.loading = true this.list.loading = true
menuApi apiClient.menu
.listTreeByTeam(this.teams.selected) .listTreeViewByTeam(this.teams.selected)
.then(response => { .then(response => {
this.list.data = response.data.data this.list.data = response.data
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.list.loading = false
this.list.loading = false
}, 200)
}) })
}, },
handleMenuMoved(pid, menus) { handleMenuMoved(pid, menus) {
@ -281,8 +277,8 @@ export default {
}, },
handleUpdateBatch() { handleUpdateBatch() {
this.formBatch.saving = true this.formBatch.saving = true
menuApi apiClient.menu
.updateBatch(this.computedMenusWithoutLevel) .updateInBatch(this.computedMenusWithoutLevel)
.catch(() => { .catch(() => {
this.formBatch.errored = true this.formBatch.errored = true
}) })
@ -299,7 +295,7 @@ export default {
title: '提示', title: '提示',
content: '确定要删除当前分组以及所有菜单?', content: '确定要删除当前分组以及所有菜单?',
onOk() { onOk() {
menuApi.deleteBatch(_this.computedMenuIds).finally(() => { apiClient.menu.deleteInBatch(_this.computedMenuIds).finally(() => {
_this.handleListTeams(true) _this.handleListTeams(true)
}) })
} }
@ -340,10 +336,12 @@ export default {
}, },
handleSetDefaultTeam() { handleSetDefaultTeam() {
this.teams.default.saving = true this.teams.default.saving = true
optionApi apiClient.option
.save({ .save([
default_menu_team: this.teams.selected {
}) default_menu_team: this.teams.selected
}
])
.catch(() => { .catch(() => {
this.teams.default.errored = true this.teams.default.errored = true
}) })

18
src/views/interface/ThemeEdit.vue

@ -41,7 +41,7 @@
</template> </template>
<script> <script>
import themeApi from '@/api/theme' import apiClient from '@/utils/api-client'
import ThemeFile from './components/ThemeFile' import ThemeFile from './components/ThemeFile'
import { PageView } from '@/layouts' import { PageView } from '@/layouts'
import Codemirror from '@/components/Codemirror/Codemirror' import Codemirror from '@/components/Codemirror/Codemirror'
@ -90,10 +90,10 @@ export default {
methods: { methods: {
handleListThemes() { handleListThemes() {
this.themes.loading = true this.themes.loading = true
themeApi apiClient.theme
.list() .list()
.then(response => { .then(response => {
this.themes.data = response.data.data this.themes.data = response.data
const activatedTheme = this.themes.data.find(item => item.activated) const activatedTheme = this.themes.data.find(item => item.activated)
@ -109,10 +109,10 @@ export default {
onSelectTheme(themeId) { onSelectTheme(themeId) {
this.files.data = [] this.files.data = []
this.files.loading = true this.files.loading = true
themeApi apiClient.theme
.listFiles(themeId) .listFiles(themeId)
.then(response => { .then(response => {
this.files.data = response.data.data this.files.data = response.data
this.files.content = '' this.files.content = ''
this.files.selected = {} this.files.selected = {}
}) })
@ -140,16 +140,16 @@ export default {
} }
}) })
} }
themeApi.getContent(this.themes.selectedId, file.path).then(response => { apiClient.theme.getTemplateContent(this.themes.selectedId, file.path).then(response => {
this.files.content = response.data.data this.files.content = response.data
this.files.selected = file this.files.selected = file
this.handleInitEditor() this.handleInitEditor()
}) })
}, },
handlerSaveContent() { handlerSaveContent() {
this.files.saving = true this.files.saving = true
themeApi apiClient.theme
.saveContent(this.themes.selectedId, this.files.selected.path, this.files.content) .updateTemplateContent(this.themes.selectedId, { path: this.files.selected.path, content: this.files.content })
.catch(() => { .catch(() => {
this.files.saveErrored = true this.files.saveErrored = true
}) })

152
src/views/interface/ThemeList.vue

@ -1,42 +1,55 @@
<template> <template>
<page-view affix :title="activatedTheme ? activatedTheme.name : '无'" subTitle="当前启用"> <page-view :title="activatedTheme ? activatedTheme.name : '无'" affix subTitle="当前启用">
<template slot="extra"> <template slot="extra">
<a-button icon="reload" :loading="list.loading" @click="handleRefreshThemesCache"> <a-button :loading="list.loading" icon="reload" @click="handleRefreshThemesCache">
刷新 刷新
</a-button> </a-button>
<a-button type="primary" icon="plus" @click="installModal.visible = true"> <a-button icon="plus" type="primary" @click="installModal.visible = true">
安装 安装
</a-button> </a-button>
</template> </template>
<a-row :gutter="12" type="flex" align="middle"> <a-row :gutter="12" align="middle" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-list <a-list
:grid="{ gutter: 12, xs: 1, sm: 1, md: 2, lg: 4, xl: 4, xxl: 4 }"
:dataSource="sortedThemes" :dataSource="sortedThemes"
:grid="{ gutter: 12, xs: 1, sm: 1, md: 2, lg: 4, xl: 4, xxl: 4 }"
:loading="list.loading" :loading="list.loading"
> >
<a-list-item slot="renderItem" slot-scope="item, index" :key="index"> <a-list-item :key="index" slot="renderItem" slot-scope="item, index">
<a-card hoverable :title="item.name" :bodyStyle="{ padding: 0 }"> <a-card :bodyStyle="{ padding: 0 }" :title="item.name" hoverable>
<div class="theme-screenshot"> <div class="theme-screenshot">
<img :alt="item.name" :src="item.screenshots || '/images/placeholder.jpg'" loading="lazy" /> <img :alt="item.name" :src="item.screenshots || '/images/placeholder.jpg'" loading="lazy" />
</div> </div>
<template class="ant-card-actions" slot="actions"> <template slot="actions" class="ant-card-actions">
<div v-if="item.activated"><a-icon type="unlock" theme="twoTone" style="margin-right:3px" />已启用</div> <div v-if="item.activated">
<div v-else @click="handleActiveTheme(item)"><a-icon type="lock" style="margin-right:3px" />启用</div> <a-icon style="margin-right:3px" theme="twoTone" type="unlock" />
已启用
</div>
<div v-else @click="handleActiveTheme(item)">
<a-icon style="margin-right:3px" type="lock" />
启用
</div>
<div @click="handleOpenThemeSettingDrawer(item)"> <div @click="handleOpenThemeSettingDrawer(item)">
<a-icon type="setting" style="margin-right:3px" />设置 <a-icon style="margin-right:3px" type="setting" />
设置
</div> </div>
<a-dropdown placement="topCenter" :trigger="['click']"> <a-dropdown :trigger="['click']" placement="topCenter">
<a class="ant-dropdown-link" href="#"> <a-icon type="ellipsis" style="margin-right:3px" />更多 </a> <a class="ant-dropdown-link" href="#">
<a-icon style="margin-right:3px" type="ellipsis" />
更多
</a>
<a-menu slot="overlay"> <a-menu slot="overlay">
<a-menu-item :key="1" :disabled="item.activated" @click="handleOpenThemeDeleteModal(item)"> <a-menu-item :key="1" :disabled="item.activated" @click="handleOpenThemeDeleteModal(item)">
<a-icon type="delete" style="margin-right:3px" />删除 <a-icon style="margin-right:3px" type="delete" />
删除
</a-menu-item> </a-menu-item>
<a-menu-item :key="2" v-if="item.repo" @click="handleConfirmRemoteUpdate(item)"> <a-menu-item v-if="item.repo" :key="2" @click="handleConfirmRemoteUpdate(item)">
<a-icon type="cloud" style="margin-right:3px" />在线更新 <a-icon style="margin-right:3px" type="cloud" />
在线更新
</a-menu-item> </a-menu-item>
<a-menu-item :key="3" @click="handleOpenLocalUpdateModal(item)"> <a-menu-item :key="3" @click="handleOpenLocalUpdateModal(item)">
<a-icon type="file" style="margin-right:3px" />从主题包更新 <a-icon style="margin-right:3px" type="file" />
从主题包更新
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
@ -48,64 +61,64 @@
</a-row> </a-row>
<ThemeSettingDrawer <ThemeSettingDrawer
:theme="themeSettingDrawer.selected"
v-model="themeSettingDrawer.visible" v-model="themeSettingDrawer.visible"
:theme="themeSettingDrawer.selected"
@close="onThemeSettingsDrawerClose" @close="onThemeSettingsDrawerClose"
/> />
<a-modal <a-modal
title="安装主题"
v-model="installModal.visible" v-model="installModal.visible"
destroyOnClose
:footer="null"
:bodyStyle="{ padding: '0 24px 24px' }"
:afterClose="onThemeInstallModalClose" :afterClose="onThemeInstallModalClose"
:bodyStyle="{ padding: '0 24px 24px' }"
:footer="null"
destroyOnClose
title="安装主题"
> >
<div class="custom-tab-wrapper"> <div class="custom-tab-wrapper">
<a-tabs :animated="{ inkBar: true, tabPane: false }"> <a-tabs :animated="{ inkBar: true, tabPane: false }">
<a-tab-pane tab="本地上传" key="1"> <a-tab-pane key="1" tab="本地上传">
<FilePondUpload <FilePondUpload
ref="upload" ref="upload"
name="file"
:accepts="['application/x-zip', 'application/x-zip-compressed', 'application/zip']" :accepts="['application/x-zip', 'application/x-zip-compressed', 'application/zip']"
label="点击选择主题包或将主题包拖拽到此处<br>仅支持 ZIP 格式的文件"
:uploadHandler="installModal.local.uploadHandler" :uploadHandler="installModal.local.uploadHandler"
label="点击选择主题包或将主题包拖拽到此处<br>仅支持 ZIP 格式的文件"
name="file"
@success="handleUploadSucceed" @success="handleUploadSucceed"
></FilePondUpload> ></FilePondUpload>
<a-alert type="info" closable> <a-alert closable type="info">
<template slot="message"> <template slot="message">
更多主题请访问 更多主题请访问
<a target="_blank" href="https://halo.run/themes.html">https://halo.run/themes</a> <a href="https://halo.run/themes.html" target="_blank">https://halo.run/themes</a>
</template> </template>
</a-alert> </a-alert>
</a-tab-pane> </a-tab-pane>
<a-tab-pane tab="远程下载" key="2"> <a-tab-pane key="2" tab="远程下载">
<a-form-model <a-form-model
ref="remoteInstallForm" ref="remoteInstallForm"
:model="installModal.remote" :model="installModal.remote"
:rules="installModal.remote.rules" :rules="installModal.remote.rules"
layout="vertical" layout="vertical"
> >
<a-form-model-item prop="url" label="远程地址:" help="* 支持 Git 仓库地址,ZIP 链接。"> <a-form-model-item help="* 支持 Git 仓库地址,ZIP 链接。" label="远程地址:" prop="url">
<a-input v-model="installModal.remote.url" /> <a-input v-model="installModal.remote.url" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item> <a-form-model-item>
<ReactiveButton <ReactiveButton
type="primary"
@click="handleRemoteFetching"
@callback="handleRemoteFetchCallback"
:loading="installModal.remote.fetching"
:errored="installModal.remote.fetchErrored" :errored="installModal.remote.fetchErrored"
text="下载" :loading="installModal.remote.fetching"
loadedText="下载成功"
erroredText="下载失败" erroredText="下载失败"
loadedText="下载成功"
text="下载"
type="primary"
@callback="handleRemoteFetchCallback"
@click="handleRemoteFetching"
></ReactiveButton> ></ReactiveButton>
</a-form-model-item> </a-form-model-item>
</a-form-model> </a-form-model>
<a-alert type="info" closable> <a-alert closable type="info">
<template slot="message"> <template slot="message">
目前仅支持远程 Git 仓库和 ZIP 下载链接更多主题请访问 目前仅支持远程 Git 仓库和 ZIP 下载链接更多主题请访问
<a target="_blank" href="https://halo.run/themes.html">https://halo.run/themes</a> <a href="https://halo.run/themes.html" target="_blank">https://halo.run/themes</a>
</template> </template>
</a-alert> </a-alert>
</a-tab-pane> </a-tab-pane>
@ -113,43 +126,43 @@
</div> </div>
</a-modal> </a-modal>
<a-modal <a-modal
title="更新主题"
v-model="localUpdateModel.visible" v-model="localUpdateModel.visible"
:afterClose="onThemeInstallModalClose"
:footer="null" :footer="null"
destroyOnClose destroyOnClose
:afterClose="onThemeInstallModalClose" title="更新主题"
> >
<FilePondUpload <FilePondUpload
ref="updateByupload" ref="updateByFile"
name="file"
:accepts="['application/x-zip', 'application/x-zip-compressed', 'application/zip']" :accepts="['application/x-zip', 'application/x-zip-compressed', 'application/zip']"
label="点击选择主题更新包或将主题更新包拖拽到此处<br>仅支持 ZIP 格式的文件" :field="localUpdateModel.selected.id"
:uploadHandler="localUpdateModel.uploadHandler"
:filed="localUpdateModel.selected.id"
:multiple="false" :multiple="false"
:uploadHandler="localUpdateModel.uploadHandler"
label="点击选择主题更新包或将主题更新包拖拽到此处<br>仅支持 ZIP 格式的文件"
name="file"
@success="handleUploadSucceed" @success="handleUploadSucceed"
></FilePondUpload> ></FilePondUpload>
</a-modal> </a-modal>
<a-modal <a-modal
title="提示"
v-model="themeDeleteModal.visible" v-model="themeDeleteModal.visible"
:width="416" :afterClose="onThemeDeleteModalClose"
:closable="false" :closable="false"
:width="416"
destroyOnClose destroyOnClose
:afterClose="onThemeDeleteModalClose" title="提示"
> >
<template slot="footer"> <template slot="footer">
<a-button @click="themeDeleteModal.visible = false"> <a-button @click="themeDeleteModal.visible = false">
取消 取消
</a-button> </a-button>
<ReactiveButton <ReactiveButton
@click="handleDeleteTheme(themeDeleteModal.selected.id, themeDeleteModal.deleteSettings)"
@callback="handleDeleteThemeCallback"
:loading="themeDeleteModal.deleting"
:errored="themeDeleteModal.deleteErrored" :errored="themeDeleteModal.deleteErrored"
text="确定" :loading="themeDeleteModal.deleting"
loadedText="删除成功"
erroredText="删除失败" erroredText="删除失败"
loadedText="删除成功"
text="确定"
@callback="handleDeleteThemeCallback"
@click="handleDeleteTheme(themeDeleteModal.selected.id, themeDeleteModal.deleteSettings)"
></ReactiveButton> ></ReactiveButton>
</template> </template>
<p>确定删除{{ themeDeleteModal.selected.name }}主题</p> <p>确定删除{{ themeDeleteModal.selected.name }}主题</p>
@ -163,7 +176,7 @@
<script> <script>
import ThemeSettingDrawer from './components/ThemeSettingDrawer' import ThemeSettingDrawer from './components/ThemeSettingDrawer'
import { PageView } from '@/layouts' import { PageView } from '@/layouts'
import themeApi from '@/api/theme' import apiClient from '@/utils/api-client'
export default { export default {
components: { components: {
@ -180,7 +193,7 @@ export default {
installModal: { installModal: {
visible: false, visible: false,
local: { local: {
uploadHandler: themeApi.upload uploadHandler: (file, options) => apiClient.theme.upload(file, options)
}, },
remote: { remote: {
@ -197,7 +210,7 @@ export default {
localUpdateModel: { localUpdateModel: {
visible: false, visible: false,
uploadHandler: themeApi.updateByUpload, uploadHandler: (file, options, field) => apiClient.theme.updateByUpload(file, options, field),
selected: {} selected: {}
}, },
@ -249,30 +262,28 @@ export default {
methods: { methods: {
handleListThemes() { handleListThemes() {
this.list.loading = true this.list.loading = true
themeApi apiClient.theme
.list() .list()
.then(response => { .then(response => {
this.list.data = response.data.data this.list.data = response.data
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.list.loading = false
this.list.loading = false
}, 200)
}) })
}, },
handleRefreshThemesCache() { handleRefreshThemesCache() {
themeApi.reload().finally(() => { apiClient.theme.reload().finally(() => {
this.handleListThemes() this.handleListThemes()
}) })
}, },
handleActiveTheme(theme) { handleActiveTheme(theme) {
themeApi.active(theme.id).finally(() => { apiClient.theme.active(theme.id).finally(() => {
this.handleListThemes() this.handleListThemes()
}) })
}, },
handleDeleteTheme(themeId, deleteSettings) { handleDeleteTheme(themeId, deleteSettings) {
this.themeDeleteModal.deleting = true this.themeDeleteModal.deleting = true
themeApi apiClient.theme
.delete(themeId, deleteSettings) .delete(themeId, deleteSettings)
.catch(() => { .catch(() => {
this.themeDeleteModal.deleteErrored = false this.themeDeleteModal.deleteErrored = false
@ -300,8 +311,8 @@ export default {
this.$refs.remoteInstallForm.validate(valid => { this.$refs.remoteInstallForm.validate(valid => {
if (valid) { if (valid) {
this.installModal.remote.fetching = true this.installModal.remote.fetching = true
themeApi apiClient.theme
.fetching(this.installModal.remote.url) .fetchTheme(this.installModal.remote.url)
.catch(() => { .catch(() => {
this.installModal.remote.fetchErrored = true this.installModal.remote.fetchErrored = true
}) })
@ -341,8 +352,8 @@ export default {
content: '确定更新【' + item.name + '】主题?', content: '确定更新【' + item.name + '】主题?',
onOk() { onOk() {
const hide = _this.$message.loading('更新中...', 0) const hide = _this.$message.loading('更新中...', 0)
themeApi apiClient.theme
.update(item.id) .updateThemeByFetching(item.id)
.then(() => { .then(() => {
_this.$message.success('更新成功!') _this.$message.success('更新成功!')
}) })
@ -350,16 +361,15 @@ export default {
hide() hide()
_this.handleListThemes() _this.handleListThemes()
}) })
}, }
onCancel() {}
}) })
}, },
onThemeInstallModalClose() { onThemeInstallModalClose() {
if (this.$refs.upload) { if (this.$refs.upload) {
this.$refs.upload.handleClearFileList() this.$refs.upload.handleClearFileList()
} }
if (this.$refs.updateByupload) { if (this.$refs.updateByFile) {
this.$refs.updateByupload.handleClearFileList() this.$refs.updateByFile.handleClearFileList()
} }
this.installModal.remote.url = null this.installModal.remote.url = null
this.handleListThemes() this.handleListThemes()

39
src/views/interface/components/MenuForm.vue

@ -1,45 +1,45 @@
<template> <template>
<div> <div>
<a-form-model <a-form-model
labelAlign="left"
ref="menuForm" ref="menuForm"
:model="menuModel" :model="menuModel"
:rules="form.rules" :rules="form.rules"
labelAlign="left"
@keyup.enter.native="handleCreateOrUpdateMenu" @keyup.enter.native="handleCreateOrUpdateMenu"
> >
<a-row :gutter="24"> <a-row :gutter="24">
<a-col :xl="8" :lg="8" :md="12" :sm="12" :xs="12"> <a-col :lg="8" :md="12" :sm="12" :xl="8" :xs="12">
<a-form-model-item label="名称" prop="name" help="* 页面上所显示的名称"> <a-form-model-item help="* 页面上所显示的名称" label="名称" prop="name">
<a-input v-model="menuModel.name" autoFocus /> <a-input v-model="menuModel.name" autoFocus />
</a-form-model-item> </a-form-model-item>
</a-col> </a-col>
<a-col :xl="8" :lg="8" :md="12" :sm="12" :xs="12"> <a-col :lg="8" :md="12" :sm="12" :xl="8" :xs="12">
<a-form-model-item label="地址" prop="url" help="* 菜单的地址"> <a-form-model-item help="* 菜单的地址" label="地址" prop="url">
<a-input v-model="menuModel.url" /> <a-input v-model="menuModel.url" />
</a-form-model-item> </a-form-model-item>
</a-col> </a-col>
<a-col :xl="8" :lg="8" :md="12" :sm="12" :xs="12"> <a-col :lg="8" :md="12" :sm="12" :xl="8" :xs="12">
<a-form-model-item label="图标" prop="icon" help="* 请根据主题的支持情况选填"> <a-form-model-item help="* 请根据主题的支持情况选填" label="图标" prop="icon">
<a-input v-model="menuModel.icon" /> <a-input v-model="menuModel.icon" />
</a-form-model-item> </a-form-model-item>
</a-col> </a-col>
<a-col :xl="8" :lg="8" :md="12" :sm="12" :xs="12"> <a-col :lg="8" :md="12" :sm="12" :xl="8" :xs="12">
<a-form-model-item label="打开方式" prop="target"> <a-form-model-item label="打开方式" prop="target">
<a-radio-group v-model="menuModel.target" :options="targets" /> <a-radio-group v-model="menuModel.target" :options="targets" />
</a-form-model-item> </a-form-model-item>
</a-col> </a-col>
<a-col :xl="8" :lg="8" :md="12" :sm="12" :xs="12"> <a-col :lg="8" :md="12" :sm="12" :xl="8" :xs="12">
<a-form-model-item label=" " :colon="false"> <a-form-model-item :colon="false" label=" ">
<a-space> <a-space>
<ReactiveButton <ReactiveButton
type="primary"
@click="handleCreateOrUpdateMenu"
@callback="handleSavedCallback"
:loading="form.saving"
:errored="form.errored" :errored="form.errored"
text="保存" :loading="form.saving"
loadedText="保存成功"
erroredText="保存失败" erroredText="保存失败"
loadedText="保存成功"
text="保存"
type="primary"
@callback="handleSavedCallback"
@click="handleCreateOrUpdateMenu"
></ReactiveButton> ></ReactiveButton>
<a-button @click="handleCancel">取消</a-button> <a-button @click="handleCancel">取消</a-button>
</a-space> </a-space>
@ -50,7 +50,8 @@
</div> </div>
</template> </template>
<script> <script>
import menuApi from '@/api/menu' import apiClient from '@/utils/api-client'
const targets = [ const targets = [
{ {
value: '_self', value: '_self',
@ -116,7 +117,7 @@ export default {
if (valid) { if (valid) {
_this.form.saving = true _this.form.saving = true
if (_this.isUpdateMode) { if (_this.isUpdateMode) {
menuApi apiClient.menu
.update(_this.menuModel.id, _this.menuModel) .update(_this.menuModel.id, _this.menuModel)
.catch(() => { .catch(() => {
_this.form.errored = true _this.form.errored = true
@ -127,7 +128,7 @@ export default {
}, 400) }, 400)
}) })
} else { } else {
menuApi apiClient.menu
.create(_this.menuModel) .create(_this.menuModel)
.catch(() => { .catch(() => {
_this.form.errored = true _this.form.errored = true

77
src/views/interface/components/MenuInternalLinkSelector.vue

@ -1,26 +1,26 @@
<template> <template>
<a-modal v-model="visible" title="从系统预设链接添加菜单" :width="1024" :bodyStyle="{ padding: '0 24px 24px' }"> <a-modal v-model="visible" :bodyStyle="{ padding: '0 24px 24px' }" :width="1024" title="从系统预设链接添加菜单">
<template slot="footer"> <template slot="footer">
<a-button @click="handleCancel"> <a-button @click="handleCancel">
取消 取消
</a-button> </a-button>
<ReactiveButton <ReactiveButton
@click="handleCreateBatch" :disabled="menus && menus.length <= 0"
@callback="handleCreateBatchCallback"
:loading="saving"
:errored="saveErrored" :errored="saveErrored"
text="添加" :loading="saving"
loadedText="添加成功"
erroredText="添加失败" erroredText="添加失败"
:disabled="menus && menus.length <= 0" loadedText="添加成功"
text="添加"
@callback="handleCreateBatchCallback"
@click="handleCreateBatch"
></ReactiveButton> ></ReactiveButton>
</template> </template>
<a-row :gutter="24"> <a-row :gutter="24">
<a-col :span="12"> <a-col :span="12">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="custom-tab-wrapper"> <div class="custom-tab-wrapper">
<a-tabs default-active-key="1" :animated="{ inkBar: true, tabPane: false }"> <a-tabs :animated="{ inkBar: true, tabPane: false }" default-active-key="1">
<a-tab-pane key="1" tab="分类目录" force-render> <a-tab-pane key="1" force-render tab="分类目录">
<a-list item-layout="horizontal"> <a-list item-layout="horizontal">
<a-list-item v-for="(category, index) in categories" :key="index"> <a-list-item v-for="(category, index) in categories" :key="index">
<a-list-item-meta> <a-list-item-meta>
@ -28,7 +28,7 @@
<span slot="description">{{ category.fullPath }}</span> <span slot="description">{{ category.fullPath }}</span>
</a-list-item-meta> </a-list-item-meta>
<template slot="actions"> <template slot="actions">
<a href="javascript:void(0);" class="text-base"> <a class="text-base" href="javascript:void(0);">
<a-icon type="plus-circle" @click="handleInsertPre(category.name, category.fullPath)" /> <a-icon type="plus-circle" @click="handleInsertPre(category.name, category.fullPath)" />
</a> </a>
</template> </template>
@ -43,7 +43,7 @@
<span slot="description">{{ tag.fullPath }}</span> <span slot="description">{{ tag.fullPath }}</span>
</a-list-item-meta> </a-list-item-meta>
<template slot="actions"> <template slot="actions">
<a href="javascript:void(0);" class="text-base"> <a class="text-base" href="javascript:void(0);">
<a-icon type="plus-circle" @click="handleInsertPre(tag.name, tag.fullPath)" /> <a-icon type="plus-circle" @click="handleInsertPre(tag.name, tag.fullPath)" />
</a> </a>
</template> </template>
@ -58,7 +58,7 @@
<span slot="description">{{ item.fullPath }}</span> <span slot="description">{{ item.fullPath }}</span>
</a-list-item-meta> </a-list-item-meta>
<template slot="actions"> <template slot="actions">
<a href="javascript:void(0);" class="text-base"> <a class="text-base" href="javascript:void(0);">
<a-icon type="plus-circle" @click="handleInsertPre(item.title, item.fullPath)" /> <a-icon type="plus-circle" @click="handleInsertPre(item.title, item.fullPath)" />
</a> </a>
</template> </template>
@ -73,7 +73,7 @@
<span slot="description">{{ item.fullPath }}</span> <span slot="description">{{ item.fullPath }}</span>
</a-list-item-meta> </a-list-item-meta>
<template slot="actions"> <template slot="actions">
<a href="javascript:void(0);" class="text-base"> <a class="text-base" href="javascript:void(0);">
<a-icon type="plus-circle" @click="handleInsertPre(item.title, item.fullPath)" /> <a-icon type="plus-circle" @click="handleInsertPre(item.title, item.fullPath)" />
</a> </a>
</template> </template>
@ -81,15 +81,15 @@
</a-list> </a-list>
<div class="page-wrapper"> <div class="page-wrapper">
<a-pagination <a-pagination
class="pagination"
:current="sheet.customs.pagination.page" :current="sheet.customs.pagination.page"
:total="sheet.customs.pagination.total"
:defaultPageSize="sheet.customs.pagination.size" :defaultPageSize="sheet.customs.pagination.size"
:pageSizeOptions="['1', '2', '5', '10', '20', '50', '100']" :pageSizeOptions="['10', '20', '50', '100']"
:total="sheet.customs.pagination.total"
class="pagination"
showLessItems
showSizeChanger showSizeChanger
@showSizeChange="handleSheetPaginationChange"
@change="handleSheetPaginationChange" @change="handleSheetPaginationChange"
showLessItems @showSizeChange="handleSheetPaginationChange"
/> />
</div> </div>
</a-tab-pane> </a-tab-pane>
@ -101,7 +101,7 @@
<span slot="description">{{ item.url }}</span> <span slot="description">{{ item.url }}</span>
</a-list-item-meta> </a-list-item-meta>
<template slot="actions"> <template slot="actions">
<a href="javascript:void(0);" class="text-base"> <a class="text-base" href="javascript:void(0);">
<a-icon type="plus-circle" @click="handleInsertPre(item.name, item.url)" /> <a-icon type="plus-circle" @click="handleInsertPre(item.name, item.url)" />
</a> </a>
</template> </template>
@ -115,7 +115,7 @@
<a-col :span="12"> <a-col :span="12">
<div class="custom-tab-wrapper"> <div class="custom-tab-wrapper">
<a-tabs default-active-key="1"> <a-tabs default-active-key="1">
<a-tab-pane key="1" tab="备选" force-render> <a-tab-pane key="1" force-render tab="备选">
<a-list item-layout="horizontal"> <a-list item-layout="horizontal">
<a-list-item v-for="(menu, index) in menus" :key="index"> <a-list-item v-for="(menu, index) in menus" :key="index">
<a-list-item-meta> <a-list-item-meta>
@ -123,7 +123,7 @@
<span slot="description">{{ menu.url }}</span> <span slot="description">{{ menu.url }}</span>
</a-list-item-meta> </a-list-item-meta>
<template slot="actions"> <template slot="actions">
<a href="javascript:void(0);" class="text-base" @click="handleRemovePre(index)"> <a class="text-base" href="javascript:void(0);" @click="handleRemovePre(index)">
<a-icon type="close-circle" /> <a-icon type="close-circle" />
</a> </a>
</template> </template>
@ -137,11 +137,7 @@
</a-modal> </a-modal>
</template> </template>
<script> <script>
import categoryApi from '@/api/category' import apiClient from '@/utils/api-client'
import tagApi from '@/api/tag'
import menuApi from '@/api/menu'
import sheetApi from '@/api/sheet'
import optionApi from '@/api/option'
export default { export default {
name: 'MenuInternalLinkSelector', name: 'MenuInternalLinkSelector',
@ -234,26 +230,29 @@ export default {
methods: { methods: {
handleFetchAll() { handleFetchAll() {
this.loading = true this.loading = true
Promise.all([optionApi.listAll(), categoryApi.listAll(true), tagApi.listAll(true), sheetApi.listIndependent()]) Promise.all([
apiClient.option.listAsMapView(),
apiClient.category.list({ sort: [], more: false }),
apiClient.tag.list({ more: false }),
apiClient.sheet.listIndependents()
])
.then(response => { .then(response => {
this.options = response[0].data.data this.options = response[0].data
this.categories = response[1].data.data this.categories = response[1].data
this.tags = response[2].data.data this.tags = response[2].data
this.sheet.independents = response[3].data.data this.sheet.independents = response[3].data
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.loading = false
this.loading = false
}, 200)
}) })
}, },
handleListSheets() { handleListSheets() {
this.sheet.customs.queryParam.page = this.sheet.customs.pagination.page - 1 this.sheet.customs.queryParam.page = this.sheet.customs.pagination.page - 1
this.sheet.customs.queryParam.size = this.sheet.customs.pagination.size this.sheet.customs.queryParam.size = this.sheet.customs.pagination.size
this.sheet.customs.queryParam.sort = this.sheet.customs.pagination.sort this.sheet.customs.queryParam.sort = this.sheet.customs.pagination.sort
sheetApi.list(this.sheet.customs.queryParam).then(response => { apiClient.sheet.list(this.sheet.customs.queryParam).then(response => {
this.sheet.customs.data = response.data.data.content this.sheet.customs.data = response.data.content
this.sheet.customs.pagination.total = response.data.data.total this.sheet.customs.pagination.total = response.data.total
}) })
}, },
handleSheetPaginationChange(page, pageSize) { handleSheetPaginationChange(page, pageSize) {
@ -278,8 +277,8 @@ export default {
}, },
handleCreateBatch() { handleCreateBatch() {
this.saving = true this.saving = true
menuApi apiClient.menu
.createBatch(this.menus) .createInBatch(this.menus)
.catch(() => { .catch(() => {
this.saveErrored = false this.saveErrored = false
}) })

53
src/views/interface/components/MenuTreeNode.vue

@ -1,30 +1,30 @@
<template> <template>
<a-list item-layout="horizontal"> <a-list item-layout="horizontal">
<draggable <draggable
v-bind="dragOptions"
tag="div"
class="item-container"
:list="list" :list="list"
:value="value" :value="value"
class="item-container"
handle=".mover"
tag="div"
v-bind="dragOptions"
@end="isDragging = false"
@input="emitter" @input="emitter"
@start="isDragging = true" @start="isDragging = true"
@end="isDragging = false"
handle=".mover"
> >
<transition-group> <transition-group>
<div :key="item.id" v-for="item in realValue"> <div v-for="item in realValue" :key="item.id">
<a-list-item class="menu-item"> <a-list-item class="menu-item">
<a-list-item-meta> <a-list-item-meta>
<span slot="title" class="inline-block font-bold title"> <span slot="title" class="inline-block font-bold title">
<a-icon class="cursor-pointer mover" type="bars" /> <a-icon class="cursor-pointer mover" type="bars" />
{{ item.name }} {{ item.name }}
<a-tooltip title="外部链接" v-if="item.target === '_blank'"> <a-tooltip v-if="item.target === '_blank'" title="外部链接">
<a-icon type="link" /> <a-icon type="link" />
</a-tooltip> </a-tooltip>
{{ item.formVisible ? '(正在编辑)' : '' }} {{ item.formVisible ? '(正在编辑)' : '' }}
</span> </span>
<span slot="description" class="inline-block"> <span slot="description" class="inline-block">
<a :href="item.url" target="_blank" class="ant-anchor-link-title"> {{ item.url }} </a> <a :href="item.url" class="ant-anchor-link-title" target="_blank"> {{ item.url }} </a>
</span> </span>
</a-list-item-meta> </a-list-item-meta>
<template slot="actions"> <template slot="actions">
@ -38,7 +38,7 @@
<template slot="actions"> <template slot="actions">
<a href="javascript:void(0);" @click="handleDelete(item.id)">删除</a> <a href="javascript:void(0);" @click="handleDelete(item.id)">删除</a>
</template> </template>
<template slot="actions" v-if="excludedTeams && excludedTeams.length > 0"> <template v-if="excludedTeams && excludedTeams.length > 0" slot="actions">
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a class="ant-dropdown-link" @click="e => e.preventDefault()"> <a class="ant-dropdown-link" @click="e => e.preventDefault()">
更多 更多
@ -46,20 +46,14 @@
</a> </a>
<a-menu slot="overlay"> <a-menu slot="overlay">
<a-sub-menu title="移动到分组"> <a-sub-menu title="移动到分组">
<a-menu-item <a-menu-item v-for="(team, index) in excludedTeams" :key="index" @click="handleMoveMenu(item, team)"
v-for="(team, index) in excludedTeams" >{{ team === '' ? '未分组' : team }}
:key="index" </a-menu-item>
@click="handleMoveMenu(item, team)"
>{{ team === '' ? '未分组' : team }}</a-menu-item
>
</a-sub-menu> </a-sub-menu>
<a-sub-menu title="复制到分组"> <a-sub-menu title="复制到分组">
<a-menu-item <a-menu-item v-for="(team, index) in excludedTeams" :key="index" @click="handleCopyMenu(item, team)"
v-for="(team, index) in excludedTeams" >{{ team === '' ? '未分组' : team }}
:key="index" </a-menu-item>
@click="handleCopyMenu(item, team)"
>{{ team === '' ? '未分组' : team }}</a-menu-item
>
</a-sub-menu> </a-sub-menu>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
@ -68,11 +62,11 @@
<MenuForm <MenuForm
v-if="item.formVisible" v-if="item.formVisible"
:menu="item" :menu="item"
@succeed="handleUpdateMenuSucceed(item)"
@cancel="handleCloseCreateMenuForm(item)" @cancel="handleCloseCreateMenuForm(item)"
@succeed="handleUpdateMenuSucceed(item)"
/> />
<div class="a-list-nested" style="margin-left: 44px;"> <div class="a-list-nested" style="margin-left: 44px;">
<MenuTreeNode :list="item.children" :excludedTeams="excludedTeams" @reload="onReloadEmit" /> <MenuTreeNode :excludedTeams="excludedTeams" :list="item.children" @reload="onReloadEmit" />
</div> </div>
</div> </div>
</transition-group> </transition-group>
@ -85,8 +79,9 @@ import draggable from 'vuedraggable'
import MenuForm from './MenuForm' import MenuForm from './MenuForm'
// apis // apis
import menuApi from '@/api/menu' import apiClient from '@/utils/api-client'
import { deepClone } from '@/utils/util' import { deepClone } from '@/utils/util'
export default { export default {
name: 'MenuTreeNode', name: 'MenuTreeNode',
components: { components: {
@ -140,7 +135,7 @@ export default {
title: '提示', title: '提示',
content: '确定要删除当前菜单?', content: '确定要删除当前菜单?',
onOk() { onOk() {
menuApi.delete(id).finally(() => { apiClient.menu.delete(id).finally(() => {
_this.onReloadEmit() _this.onReloadEmit()
}) })
} }
@ -162,7 +157,7 @@ export default {
menu.parentId = 0 menu.parentId = 0
menu.priority = 0 menu.priority = 0
menu.id = null menu.id = null
menuApi.create(menu).then(() => { apiClient.menu.create(menu).then(() => {
this.$emit('reload') this.$emit('reload')
}) })
}, },
@ -171,7 +166,7 @@ export default {
menu.team = team menu.team = team
menu.parentId = 0 menu.parentId = 0
menu.priority = 0 menu.priority = 0
menuApi.update(menu.id, menu).then(() => { apiClient.menu.update(menu.id, menu).then(() => {
this.$emit('reload') this.$emit('reload')
}) })
}, },
@ -186,18 +181,22 @@ export default {
opacity: 0.8; opacity: 0.8;
@apply bg-gray-200; @apply bg-gray-200;
} }
.chosen { .chosen {
opacity: 0.8; opacity: 0.8;
@apply bg-gray-200; @apply bg-gray-200;
padding: 0 5px; padding: 0 5px;
} }
.drag { .drag {
@apply bg-white; @apply bg-white;
padding: 0 5px; padding: 0 5px;
} }
::v-deep .ant-list-item-action { ::v-deep .ant-list-item-action {
display: none; display: none;
} }
::v-deep .menu-item:hover .ant-list-item-action { ::v-deep .menu-item:hover .ant-list-item-action {
display: block; display: block;
} }

123
src/views/interface/components/ThemeSettingDrawer.vue

@ -1,100 +1,100 @@
<template> <template>
<a-drawer <a-drawer
:afterVisibleChange="handleAfterVisibleChanged"
:title="`${theme.name} 主题设置`" :title="`${theme.name} 主题设置`"
width="100%" :visible="visible"
placement="right"
closable closable
destroyOnClose destroyOnClose
placement="right"
width="100%"
@close="onClose" @close="onClose"
:visible="visible"
:afterVisibleChange="handleAfterVisibleChanged"
> >
<a-row :gutter="12" type="flex"> <a-row :gutter="12" type="flex">
<a-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24" v-if="!viewMode"> <a-col v-if="!viewMode" :lg="12" :md="12" :sm="24" :xl="12" :xs="24">
<a-card :bordered="false"> <a-card :bordered="false">
<img :alt="theme.name" :src="theme.screenshots" slot="cover" /> <img slot="cover" :alt="theme.name" :src="theme.screenshots" />
<a-card-meta :description="theme.description"> <a-card-meta :description="theme.description">
<template slot="title"> <template slot="title">
<a :href="author.website" target="_blank">{{ author.name }}</a> <a :href="author.website" target="_blank">{{ author.name }}</a>
</template> </template>
<a-avatar v-if="theme.logo" :src="theme.logo" size="large" slot="avatar" /> <a-avatar v-if="theme.logo" slot="avatar" :src="theme.logo" size="large" />
<a-avatar v-else size="large" slot="avatar">{{ author.name }}</a-avatar> <a-avatar v-else slot="avatar" size="large">{{ author.name }}</a-avatar>
</a-card-meta> </a-card-meta>
</a-card> </a-card>
</a-col> </a-col>
<a-col :xl="formColValue" :lg="formColValue" :md="formColValue" :sm="24" :xs="24" style="padding-bottom: 50px;"> <a-col :lg="formColValue" :md="formColValue" :sm="24" :xl="formColValue" :xs="24" style="padding-bottom: 50px;">
<a-spin :spinning="settingLoading"> <a-spin :spinning="settingLoading">
<div class="card-container" v-if="themeConfigurations.length > 0"> <div v-if="themeConfigurations.length > 0" class="card-container">
<a-tabs type="card" defaultActiveKey="0"> <a-tabs defaultActiveKey="0" type="card">
<a-tab-pane v-for="(group, index) in themeConfigurations" :key="index.toString()" :tab="group.label"> <a-tab-pane v-for="(group, index) in themeConfigurations" :key="index.toString()" :tab="group.label">
<a-form layout="vertical" :wrapperCol="wrapperCol"> <a-form :wrapperCol="wrapperCol" layout="vertical">
<a-form-item v-for="(item, index1) in group.items" :label="item.label + ':'" :key="index1"> <a-form-item v-for="(item, index1) in group.items" :key="index1" :label="item.label + ':'">
<p v-if="item.description && item.description !== ''" slot="help" v-html="item.description"></p> <p v-if="item.description && item.description !== ''" slot="help" v-html="item.description"></p>
<a-input <a-input
v-if="item.type === 'TEXT'"
v-model="themeSettings[item.name]" v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue" :defaultValue="item.defaultValue"
:placeholder="item.placeholder" :placeholder="item.placeholder"
v-if="item.type === 'TEXT'"
/> />
<a-input <a-input
type="textarea" v-else-if="item.type === 'TEXTAREA'"
:autoSize="{ minRows: 5 }"
v-model="themeSettings[item.name]" v-model="themeSettings[item.name]"
:autoSize="{ minRows: 5 }"
:placeholder="item.placeholder" :placeholder="item.placeholder"
v-else-if="item.type === 'TEXTAREA'" type="textarea"
/> />
<a-radio-group <a-radio-group
v-else-if="item.type === 'RADIO'"
v-model="themeSettings[item.name]"
v-decorator="['radio-group']" v-decorator="['radio-group']"
:defaultValue="item.defaultValue" :defaultValue="item.defaultValue"
v-model="themeSettings[item.name]"
v-else-if="item.type === 'RADIO'"
> >
<a-radio v-for="(option, index2) in item.options" :key="index2" :value="option.value">{{ <a-radio v-for="(option, index2) in item.options" :key="index2" :value="option.value"
option.label >{{ option.label }}
}}</a-radio> </a-radio>
</a-radio-group> </a-radio-group>
<a-select <a-select
v-else-if="item.type === 'SELECT'"
v-model="themeSettings[item.name]" v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue" :defaultValue="item.defaultValue"
v-else-if="item.type === 'SELECT'"
> >
<a-select-option v-for="option in item.options" :key="option.value" :value="option.value">{{ <a-select-option v-for="option in item.options" :key="option.value" :value="option.value"
option.label >{{ option.label }}
}}</a-select-option> </a-select-option>
</a-select> </a-select>
<verte <verte
picker="square" v-else-if="item.type === 'COLOR'"
model="hex"
v-model="themeSettings[item.name]" v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue" :defaultValue="item.defaultValue"
v-else-if="item.type === 'COLOR'" model="hex"
picker="square"
style="display: inline-block;height: 24px;" style="display: inline-block;height: 24px;"
></verte> ></verte>
<a-input <a-input
v-else-if="item.type === 'ATTACHMENT'"
v-model="themeSettings[item.name]" v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue" :defaultValue="item.defaultValue"
v-else-if="item.type === 'ATTACHMENT'"
> >
<a href="javascript:void(0);" slot="addonAfter" @click="handleShowSelectAttachment(item.name)"> <a slot="addonAfter" href="javascript:void(0);" @click="handleShowSelectAttachment(item.name)">
<a-icon type="picture" /> <a-icon type="picture" />
</a> </a>
</a-input> </a-input>
<a-input-number <a-input-number
v-else-if="item.type === 'NUMBER'"
v-model="themeSettings[item.name]" v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue" :defaultValue="item.defaultValue"
v-else-if="item.type === 'NUMBER'"
style="width:100%" style="width:100%"
/> />
<a-switch <a-switch
v-else-if="item.type === 'SWITCH'"
v-model="themeSettings[item.name]" v-model="themeSettings[item.name]"
:defaultChecked="item.defaultValue" :defaultChecked="item.defaultValue"
v-else-if="item.type === 'SWITCH'"
/> />
<a-input <a-input
v-else
v-model="themeSettings[item.name]" v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue" :defaultValue="item.defaultValue"
:placeholder="item.placeholder" :placeholder="item.placeholder"
v-else
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -105,17 +105,17 @@
</a-spin> </a-spin>
</a-col> </a-col>
<a-col :xl="20" :lg="20" :md="20" :sm="24" :xs="24" v-if="viewMode" style="padding-bottom: 50px;"> <a-col v-if="viewMode" :lg="20" :md="20" :sm="24" :xl="20" :xs="24" style="padding-bottom: 50px;">
<a-card :bordered="true" :bodyStyle="{ padding: 0 }"> <a-card :bodyStyle="{ padding: 0 }" :bordered="true">
<iframe <iframe
id="themeViewIframe" id="themeViewIframe"
title="主题预览" :height="clientHeight - 165"
:src="options.blog_url"
border="0"
frameborder="0" frameborder="0"
scrolling="auto" scrolling="auto"
border="0" title="主题预览"
:src="options.blog_url"
width="100%" width="100%"
:height="clientHeight - 165"
> >
</iframe> </iframe>
</a-card> </a-card>
@ -124,27 +124,27 @@
<AttachmentSelectDrawer <AttachmentSelectDrawer
v-model="attachmentDrawerVisible" v-model="attachmentDrawerVisible"
@listenToSelect="handleSelectAttachment"
title="选择附件" title="选择附件"
@listenToSelect="handleSelectAttachment"
/> />
<footer-tool-bar v-if="themeConfigurations.length > 0" class="w-full"> <footer-tool-bar v-if="themeConfigurations.length > 0" class="w-full">
<a-space> <a-space>
<a-button v-if="!this.isMobile() && theme.activated && viewMode" type="primary" @click="toggleViewMode" ghost <a-button v-if="!this.isMobile() && theme.activated && viewMode" ghost type="primary" @click="toggleViewMode"
>普通模式</a-button >普通模式
> </a-button>
<a-button v-else-if="!this.isMobile() && theme.activated && !viewMode" type="dashed" @click="toggleViewMode" <a-button v-else-if="!this.isMobile() && theme.activated && !viewMode" type="dashed" @click="toggleViewMode"
>预览模式</a-button >预览模式
> </a-button>
<ReactiveButton <ReactiveButton
type="primary"
@click="handleSaveSettings"
@callback="saveErrored = false"
:loading="saving"
:errored="saveErrored" :errored="saveErrored"
text="保存" :loading="saving"
loadedText="保存成功"
erroredText="保存失败" erroredText="保存失败"
loadedText="保存成功"
text="保存"
type="primary"
@callback="saveErrored = false"
@click="handleSaveSettings"
></ReactiveButton> ></ReactiveButton>
</a-space> </a-space>
</footer-tool-bar> </footer-tool-bar>
@ -156,7 +156,8 @@ import { mapGetters } from 'vuex'
import FooterToolBar from '@/components/FooterToolbar' import FooterToolBar from '@/components/FooterToolbar'
import Verte from 'verte' import Verte from 'verte'
import 'verte/dist/verte.css' import 'verte/dist/verte.css'
import themeApi from '@/api/theme' import apiClient from '@/utils/api-client'
export default { export default {
name: 'ThemeSetting', name: 'ThemeSetting',
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
@ -212,26 +213,24 @@ export default {
methods: { methods: {
async handleFetchConfiguration() { async handleFetchConfiguration() {
this.settingLoading = true this.settingLoading = true
await themeApi.fetchConfiguration(this.theme.id).then(response => { await apiClient.theme.listConfigurations(this.theme.id).then(response => {
this.themeConfigurations = response.data.data this.themeConfigurations = response.data
}) })
this.handleFetchSettings() this.handleFetchSettings()
}, },
handleFetchSettings() { handleFetchSettings() {
themeApi apiClient.theme
.fetchSettings(this.theme.id) .listSettings(this.theme.id)
.then(response => { .then(response => {
this.themeSettings = response.data.data this.themeSettings = response.data
}) })
.finally(() => { .finally(() => {
setTimeout(() => { this.settingLoading = false
this.settingLoading = false
}, 200)
}) })
}, },
handleSaveSettings() { handleSaveSettings() {
this.saving = true this.saving = true
themeApi apiClient.theme
.saveSettings(this.theme.id, this.themeSettings) .saveSettings(this.theme.id, this.themeSettings)
.then(() => { .then(() => {
if (this.viewMode) { if (this.viewMode) {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save