ui更新完成
commit
c0ffd8ab4d
@ -0,0 +1,4 @@
|
||||
VITE_API_URL=/api
|
||||
VITE_API_TIME_OUT=30000
|
||||
VITE_PASSWORD_SECRET_KEY===BallCat-Auth==
|
||||
VITE_IMAGE_DOMAIN=https://hccake-img.oss-cn-shanghai.aliyuncs.com
|
@ -0,0 +1,4 @@
|
||||
VITE_API_URL=/api
|
||||
VITE_API_TIME_OUT=10000
|
||||
VITE_PASSWORD_SECRET_KEY===BallCat-Auth==
|
||||
VITE_IMAGE_DOMAIN=https://hccake-img.oss-cn-shanghai.aliyuncs.com
|
@ -0,0 +1,4 @@
|
||||
VITE_API_URL=/api
|
||||
VITE_API_TIME_OUT=10000
|
||||
VITE_PASSWORD_SECRET_KEY===BallCat-Auth==
|
||||
VITE_IMAGE_DOMAIN=https://hccake-img.oss-cn-shanghai.aliyuncs.com
|
@ -0,0 +1,79 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"EffectScope": true,
|
||||
"InjectionKey": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": true,
|
||||
"acceptHMRUpdate": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"createPinia": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"defineStore": true,
|
||||
"effectScope": true,
|
||||
"getActivePinia": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"mapActions": true,
|
||||
"mapGetters": true,
|
||||
"mapState": true,
|
||||
"mapStores": true,
|
||||
"mapWritableState": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeRouteLeave": true,
|
||||
"onBeforeRouteUpdate": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"setActivePinia": true,
|
||||
"setMapStoreSuffix": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"storeToRefs": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useLink": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSlots": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
'@vue/eslint-config-prettier',
|
||||
'./.eslintrc-auto-import.json' // auto-imports 使用
|
||||
],
|
||||
env: {
|
||||
node: true,
|
||||
'vue/setup-compiler-macros': true
|
||||
},
|
||||
rules: {
|
||||
// 允许使用 any
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
// 允许使用 @ts-ignore 注释
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
// 允许空方法
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
// 允许非空断言
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'vue/no-template-shadow': 'off'
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||
|
||||
name: Deploy to preview server
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
# 手动触发事件
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run build task
|
||||
run: pnpm build-only
|
||||
|
||||
- name: Deploy to Server
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.PREVIEW_SERVER_SSH_PRIVATE_KEY }}
|
||||
ARGS: '-avz --delete'
|
||||
SOURCE: 'dist/'
|
||||
REMOTE_HOST: ${{ secrets.PREVIEW_SERVER_HOST }}
|
||||
REMOTE_USER: ${{ secrets.PREVIEW_SERVER_USER }}
|
||||
TARGET: ${{ secrets.VUE3_TARGET }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.eslintcache
|
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no -- commitlint --edit $1
|
@ -0,0 +1,8 @@
|
||||
command_exists () {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Workaround for Windows 10, Git Bash and Yarn
|
||||
if command_exists winpty && test -t 1; then
|
||||
exec < /dev/tty
|
||||
fi
|
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"src/**/*.{vue,js,jsx,cjs,mjs,ts,tsx,cts,mts,css}": [
|
||||
"eslint --fix"
|
||||
]
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
/dist/*
|
||||
.local
|
||||
.output.js
|
||||
/node_modules/**
|
||||
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
|
||||
/public/*
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin"
|
||||
]
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@ballcat/commitlint-config-gitmoji']
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=yes" />
|
||||
<title><%- title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<div class="noscript-container">
|
||||
Hi there! Please
|
||||
<div class="noscript-enableJS">
|
||||
<a href="https://www.enablejavascript.io/en" target="_blank" rel="noopener noreferrer">
|
||||
<b>enable Javascript</b>
|
||||
</a>
|
||||
</div>
|
||||
in your browser to use <%- title %>, Out-of-the-box mid-stage front/design solution!
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#app {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
}
|
||||
.noscript-container {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
margin-top: 90px;
|
||||
font-size: 20px;
|
||||
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode',
|
||||
Geneva, Verdana, sans-serif;
|
||||
}
|
||||
.noscript-enableJS {
|
||||
padding-right: 3px;
|
||||
padding-left: 3px;
|
||||
}
|
||||
.page-loading-warp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 36px;
|
||||
}
|
||||
.load-spin {
|
||||
position: absolute;
|
||||
display: none;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
opacity: 0;
|
||||
-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
-webkit-font-feature-settings: 'tnum';
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
.load-spin-spinning {
|
||||
position: static;
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.load-spin-dot {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.load-spin-dot-item {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 100%;
|
||||
-webkit-transform: scale(0.75);
|
||||
-ms-transform: scale(0.75);
|
||||
transform: scale(0.75);
|
||||
-webkit-transform-origin: 50% 50%;
|
||||
-ms-transform-origin: 50% 50%;
|
||||
transform-origin: 50% 50%;
|
||||
opacity: 0.3;
|
||||
-webkit-animation: antspinmove 1s infinite linear alternate;
|
||||
animation: antSpinMove 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.load-spin-dot-item:nth-child(1) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.load-spin-dot-item:nth-child(2) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.load-spin-dot-item:nth-child(3) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
-webkit-animation-delay: 0.8s;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
|
||||
.load-spin-dot-item:nth-child(4) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
-webkit-animation-delay: 1.2s;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
|
||||
.load-spin-dot-spin {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
-webkit-animation: antrotate 1.2s infinite linear;
|
||||
animation: antRotate 1.2s infinite linear;
|
||||
}
|
||||
|
||||
.load-spin-lg .load-spin-dot {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.load-spin-lg .load-spin-dot i {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
.load-spin-blur {
|
||||
background: #fff;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
background-color: #f4f7f9;
|
||||
"
|
||||
>
|
||||
<img src="./src/assets/logo.png" alt="logo" width="90" />
|
||||
<div class="page-loading-warp">
|
||||
<div class="load-spin load-spin-lg load-spin-spinning">
|
||||
<span class="load-spin-dot load-spin-dot-spin">
|
||||
<i class="load-spin-dot-item"></i>
|
||||
<i class="load-spin-dot-item"></i>
|
||||
<i class="load-spin-dot-item"></i>
|
||||
<i class="load-spin-dot-item"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
color: rgb(0 0 0 / 65%);
|
||||
"
|
||||
>
|
||||
<%- title %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "ballcat-ui-vue3",
|
||||
"version": "0.0.0",
|
||||
"packageManager": "pnpm@8.2.0",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "run-p type-check build-only",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
||||
"preview": "vite preview --port 5050",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"preinstall": "node ./scripts/preinstall.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
"@ballcat/vue-cropper": "^1.0.5",
|
||||
"@ckpack/vue-color": "^1.4.1",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/shared": "^10.1.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"ant-design-vue": "^3.2.20",
|
||||
"axios": "^1.4.0",
|
||||
"cropperjs": "^1.5.13",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"json-bigint": "^1.0.0",
|
||||
"mitt": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.3",
|
||||
"qs": "^6.11.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ballcat/commitlint-config-gitmoji": "^1.1.0",
|
||||
"@commitlint/cli": "^17.6.3",
|
||||
"@intlify/unplugin-vue-i18n": "^0.10.0",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@tsconfig/node18": "^2.0.1",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/json-bigint": "^1.0.1",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/node": "^18.16.8",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"@wangeditor/core": "^1.1.19",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-plugin-vue": "^9.13.0",
|
||||
"husky": "^8.0.3",
|
||||
"less": "^4.1.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.8",
|
||||
"typescript": "~5.0.4",
|
||||
"unplugin-auto-import": "^0.16.0",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^4.3.8",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vue-tsc": "~1.6.5"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,20 @@
|
||||
import Card from './components/Card'
|
||||
import type { CardType, CardProps } from './types'
|
||||
|
||||
export type ProCardProps = CardProps
|
||||
|
||||
export type ProCardType = CardType & {
|
||||
isProCard: boolean
|
||||
Group: typeof Group
|
||||
}
|
||||
|
||||
const Group = (props: CardProps) => <Card bodyStyle={{ padding: 0 }} {...props} />
|
||||
|
||||
// 当前不对底层 Card 做封装,仅挂载子组件,直接导出
|
||||
// @ts-ignore
|
||||
const ProCard: ProCardType = Card
|
||||
|
||||
ProCard.isProCard = true
|
||||
ProCard.Group = Group
|
||||
|
||||
export default ProCard
|
@ -0,0 +1,75 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
@import (reference) 'ant-design-vue/es/style/mixins/index.less';
|
||||
|
||||
@pro-card-prefix-cls: ~'@{ant-prefix}-pro-card';
|
||||
@card-action-icon-size: 16px;
|
||||
|
||||
.@{pro-card-prefix-cls} {
|
||||
&-actions {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: @card-actions-background;
|
||||
border-top: @border-width-base @border-style-base @border-color-split;
|
||||
.clearfix;
|
||||
|
||||
.@{ant-prefix}-space {
|
||||
gap: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > li,
|
||||
.@{ant-prefix}-space-item {
|
||||
flex: 1;
|
||||
float: left;
|
||||
margin: @card-actions-li-margin;
|
||||
color: @text-color-secondary;
|
||||
text-align: center;
|
||||
|
||||
> a {
|
||||
color: @text-color-secondary;
|
||||
transition: color 0.3s;
|
||||
&:hover {
|
||||
color: @primary-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 32px;
|
||||
font-size: @font-size-base;
|
||||
line-height: @line-height-base;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color-hover;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
a:not(.@{ant-prefix}-btn),
|
||||
> .anticon {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
color: @text-color-secondary;
|
||||
line-height: 22px;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
> .anticon {
|
||||
font-size: @card-action-icon-size;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: @border-width-base @border-style-base @border-color-split;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import './index.less'
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import type { VueNode } from '../../../types'
|
||||
|
||||
export type ProCardActionsProps = {
|
||||
/**
|
||||
* 自定义前缀
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
prefixCls?: string
|
||||
/** 操作按钮 */
|
||||
actions?: VueNode
|
||||
}
|
||||
|
||||
const ProCardActions: FunctionalComponent<ProCardActionsProps> = props => {
|
||||
const { actions, prefixCls } = props
|
||||
if (Array.isArray(actions) && actions?.length) {
|
||||
return (
|
||||
<ul class={`${prefixCls}-actions`}>
|
||||
{actions.map((action, index) => (
|
||||
<li style={{ width: `${100 / actions.length}%` }} key={`action-${index}`}>
|
||||
<span>{action}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
if (actions) return <ul class={`${prefixCls}-actions`}>{actions}</ul>
|
||||
return null
|
||||
}
|
||||
|
||||
export default ProCardActions
|
@ -0,0 +1,216 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
|
||||
@pro-card-prefix-cls: ~'@{ant-prefix}-pro-card';
|
||||
@card-hoverable-hover-border: transparent;
|
||||
@pro-card-default-border: @border-width-base @border-style-base @border-color-split;
|
||||
|
||||
// == when focus or actived
|
||||
.pro-card-active() {
|
||||
background-color: @item-active-bg;
|
||||
border-color: @outline-color;
|
||||
}
|
||||
|
||||
.@{pro-card-prefix-cls} {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: @component-background;
|
||||
border-radius: @card-radius;
|
||||
|
||||
&-col {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-border {
|
||||
border: @pro-card-default-border;
|
||||
}
|
||||
|
||||
&-hoverable {
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.3s, border-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: @card-hoverable-hover-border;
|
||||
box-shadow: @card-shadow;
|
||||
}
|
||||
|
||||
&.@{pro-card-prefix-cls}-checked:hover {
|
||||
border-color: @outline-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-checked {
|
||||
.pro-card-active();
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 6px solid @primary-color;
|
||||
border-bottom: 6px solid transparent;
|
||||
border-left: 6px solid transparent;
|
||||
border-top-right-radius: 2px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.pro-card-active();
|
||||
}
|
||||
|
||||
&-size-small {
|
||||
.@{pro-card-prefix-cls} {
|
||||
&-header {
|
||||
padding: @card-head-padding-sm @card-padding-base-sm;
|
||||
padding-bottom: 0;
|
||||
|
||||
&-border {
|
||||
& {
|
||||
padding-bottom: @card-head-padding-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: @card-head-font-size-sm;
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: @card-padding-base-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-ghost {
|
||||
background-color: transparent;
|
||||
|
||||
> .@{pro-card-prefix-cls} {
|
||||
&-header {
|
||||
padding-right: 0;
|
||||
padding-bottom: @card-head-padding;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-split > &-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-split-vertical {
|
||||
border-right: @border-width-base @border-style-base @border-color-split;
|
||||
}
|
||||
|
||||
&-split-horizontal {
|
||||
border-bottom: @border-width-base @border-style-base @border-color-split;
|
||||
}
|
||||
|
||||
&-contain-card > &-body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-body-direction-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-body-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&-collapse {
|
||||
> .@{pro-card-prefix-cls} {
|
||||
&-header {
|
||||
padding-bottom: @card-head-padding;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&-body {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: @card-head-padding @card-padding-base;
|
||||
padding-bottom: 0;
|
||||
&-border {
|
||||
& {
|
||||
padding-bottom: @card-head-padding;
|
||||
}
|
||||
border-bottom: @border-width-base @border-style-base @border-color-split;
|
||||
}
|
||||
|
||||
&-collapsible {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: @card-head-color;
|
||||
font-weight: 500;
|
||||
font-size: @card-head-font-size;
|
||||
}
|
||||
|
||||
&-extra {
|
||||
color: @card-head-extra-color;
|
||||
}
|
||||
|
||||
&-type-inner {
|
||||
.@{pro-card-prefix-cls}-header {
|
||||
background-color: @background-color-light;
|
||||
}
|
||||
}
|
||||
|
||||
&-collapsible-icon {
|
||||
margin-right: 8px;
|
||||
color: @icon-color-hover;
|
||||
:hover {
|
||||
color: @primary-color-hover;
|
||||
}
|
||||
|
||||
& svg {
|
||||
transition: transform @animation-duration-base;
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: @card-padding-base;
|
||||
|
||||
&-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loop-grid-columns(@index) when (@index > 0) {
|
||||
.@{pro-card-prefix-cls}-col-@{index} {
|
||||
flex-shrink: 0;
|
||||
width: percentage((@index / @grid-columns));
|
||||
}
|
||||
.loop-grid-columns((@index - 1));
|
||||
}
|
||||
|
||||
.@{pro-card-prefix-cls}-col-0 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loop-grid-columns(@grid-columns);
|
@ -0,0 +1,42 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
@pro-card-prefix-cls: ~'@{ant-prefix}-pro-card';
|
||||
|
||||
@gradient-min: fade(@card-skeleton-bg, 20%);
|
||||
@gradient-max: fade(@card-skeleton-bg, 40%);
|
||||
|
||||
.@{pro-card-prefix-cls} {
|
||||
&-loading {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-loading &-body {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-loading-content {
|
||||
width: 100%;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-loading-block {
|
||||
height: 14px;
|
||||
margin: 4px 0;
|
||||
background: linear-gradient(90deg, @gradient-min, @gradient-max, @gradient-min);
|
||||
background-size: 600% 600%;
|
||||
border-radius: @card-radius;
|
||||
animation: card-loading 1.4s ease infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes card-loading {
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import './index.less'
|
||||
|
||||
import { Row, Col } from 'ant-design-vue'
|
||||
import 'ant-design-vue/es/grid/style/index.less'
|
||||
|
||||
import type { CSSProperties, FunctionalComponent } from 'vue'
|
||||
|
||||
type LoadingProps = {
|
||||
/** Prefix */
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
const Loading: FunctionalComponent<LoadingProps> = (props, { attrs }) => {
|
||||
const { prefix } = props
|
||||
|
||||
return (
|
||||
<div class={`${prefix}-loading-content`} style={attrs.style as CSSProperties}>
|
||||
<Row gutter={8}>
|
||||
<Col span={22}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={8}>
|
||||
<Col span={8}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
<Col span={15}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={8}>
|
||||
<Col span={6}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={8}>
|
||||
<Col span={13}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={8}>
|
||||
<Col span={4}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<div class={`${prefix}-loading-block`} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
@ -0,0 +1,8 @@
|
||||
import ProCard from './ProCard'
|
||||
import type { ProCardProps } from './ProCard'
|
||||
|
||||
import type { ProCardTabsProps } from './types'
|
||||
|
||||
export type { ProCardTabsProps, ProCardProps }
|
||||
|
||||
export default ProCard
|
@ -0,0 +1 @@
|
||||
export type ProFieldEmptyText = string | false
|
@ -0,0 +1,14 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
|
||||
@pro-field-light-wrapper: ~'@{ant-prefix}-pro-field-light-wrapper';
|
||||
|
||||
.@{pro-field-light-wrapper}-collapse-label {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.@{pro-field-light-wrapper}-container {
|
||||
.@{ant-prefix}-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import './index.less'
|
||||
import type { VueNode } from '../../../types'
|
||||
import type { Placement } from 'ant-design-vue/es/vc-select/BaseSelect'
|
||||
|
||||
export type SizeType = 'small' | 'middle' | 'large' | undefined
|
||||
|
||||
export type LightWrapperProps = {
|
||||
label?: VueNode
|
||||
disabled?: boolean
|
||||
placeholder?: VueNode
|
||||
size?: SizeType
|
||||
value?: any
|
||||
onChange?: (value?: any) => void
|
||||
onBlur?: (value?: any) => void
|
||||
valuePropName?: string
|
||||
customLightMode?: boolean
|
||||
light?: boolean
|
||||
/**
|
||||
* @name 自定义label的值
|
||||
*
|
||||
* @example <caption>自定义数组的转化</caption>
|
||||
* labelFormatter={(value) =>value.join('-')} }
|
||||
*/
|
||||
labelFormatter?: (value: any) => string
|
||||
bordered?: boolean
|
||||
otherFieldProps?: any
|
||||
valueType?: string
|
||||
allowClear?: boolean
|
||||
footerRender?: LightFilterFooterRender
|
||||
placement?: Placement
|
||||
}
|
||||
|
||||
export type LightFilterFooterRender =
|
||||
| ((
|
||||
/**
|
||||
* 确认选择的值
|
||||
*/
|
||||
onConfirm?: (e?: MouseEvent) => void,
|
||||
/**
|
||||
* 清除选择
|
||||
*/
|
||||
onClear?: (e?: MouseEvent) => void
|
||||
) => JSX.Element | false)
|
||||
| false
|
@ -0,0 +1,68 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
|
||||
@basicLayout-prefix-cls: ~'@{ant-prefix}-pro-basicLayout';
|
||||
@pro-layout-header-height: 48px;
|
||||
|
||||
.@{basicLayout-prefix-cls} {
|
||||
// BFC
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
.@{ant-prefix}-layout-header {
|
||||
&.@{ant-prefix}-pro-fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
&.@{ant-prefix}-pro-header-light {
|
||||
background: @component-background;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
position: relative;
|
||||
margin: 16px;
|
||||
|
||||
.@{ant-prefix}-pro-page-container {
|
||||
margin: -16px -16px 0;
|
||||
}
|
||||
|
||||
&-disable-margin {
|
||||
margin: 0;
|
||||
|
||||
.@{ant-prefix}-pro-page-container {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
> .@{ant-prefix}-layout {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// children should support fixed
|
||||
.@{basicLayout-prefix-cls}-is-children.@{basicLayout-prefix-cls}-fix-siderbar {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
.@{basicLayout-prefix-cls}-has-header {
|
||||
// tech-page-container
|
||||
.tech-page-container {
|
||||
height: calc(100vh - @pro-layout-header-height);
|
||||
}
|
||||
.@{basicLayout-prefix-cls}-is-children.@{basicLayout-prefix-cls}-has-header {
|
||||
.tech-page-container {
|
||||
height: calc(100vh - @pro-layout-header-height - @pro-layout-header-height);
|
||||
}
|
||||
.@{basicLayout-prefix-cls}-is-children {
|
||||
min-height: calc(100vh - @pro-layout-header-height);
|
||||
&.@{basicLayout-prefix-cls}-fix-siderbar {
|
||||
height: calc(100vh - @pro-layout-header-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import { Layout } from 'ant-design-vue'
|
||||
import GlobalFooter from './components/GlobalFooter'
|
||||
|
||||
import type { CSSProperties, PropType } from 'vue'
|
||||
import type { WithFalse } from './types'
|
||||
import type { VueNodeOrRender } from '#/types'
|
||||
|
||||
export type FooterProps = {
|
||||
links?: WithFalse<
|
||||
{
|
||||
key?: string
|
||||
title: VueNodeOrRender
|
||||
href: string
|
||||
blankTarget?: boolean
|
||||
}[]
|
||||
>
|
||||
copyright?: WithFalse<string>
|
||||
prefixCls?: string
|
||||
footerStyle?: CSSProperties
|
||||
}
|
||||
|
||||
const footerViewProps = {
|
||||
links: {
|
||||
type: [Object, Boolean] as PropType<FooterProps['links']>,
|
||||
default: undefined
|
||||
},
|
||||
copyright: {
|
||||
type: [String, Boolean] as PropType<FooterProps['copyright']>,
|
||||
default: undefined
|
||||
},
|
||||
prefixCls: String as PropType<FooterProps['prefixCls']>,
|
||||
footerStyle: Object as PropType<FooterProps['footerStyle']>
|
||||
}
|
||||
|
||||
const FooterView = defineComponent({
|
||||
name: 'FooterView',
|
||||
props: footerViewProps,
|
||||
setup(props, { slots, attrs }) {
|
||||
return () => (
|
||||
<Layout.Footer class={attrs.class} style={{ padding: 0, ...(attrs.style as CSSProperties) }}>
|
||||
<GlobalFooter
|
||||
links={props.links}
|
||||
prefixCls={props.prefixCls}
|
||||
copyright={props.copyright}
|
||||
style={{ ...(props.footerStyle as CSSProperties) }}
|
||||
>
|
||||
{slots}
|
||||
</GlobalFooter>
|
||||
</Layout.Footer>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default FooterView
|
@ -0,0 +1,20 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
|
||||
@pro-layout-fixed-header-prefix-cls: ~'@{ant-prefix}-pro-fixed-header';
|
||||
@pro-layout-header-prefix-cls: ~'@{ant-prefix}-pro-header';
|
||||
|
||||
.@{pro-layout-fixed-header-prefix-cls} {
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
|
||||
&-action {
|
||||
transition: width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.@{pro-layout-header-prefix-cls} {
|
||||
&-realDark {
|
||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 65%);
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
import './Header.less'
|
||||
|
||||
import { Layout } from 'ant-design-vue'
|
||||
|
||||
import GlobalHeader, { globalHeaderProps } from './components/GlobalHeader'
|
||||
|
||||
import TopNavHeader from './components/TopNavHeader'
|
||||
import { clearMenuItem } from './utils/utils'
|
||||
import { getRender } from './utils'
|
||||
|
||||
import { VueNodeOrRenderPropType, WithFalseVueNodeOrRenderPropType } from '#/types'
|
||||
import type { VueNodeOrRender } from '#/types'
|
||||
import type { HeaderContentRender, HeaderRender, HeaderTitleRender } from './renderTypes'
|
||||
|
||||
import type { WithFalse } from './types'
|
||||
import type { PropType, CSSProperties, Slots, ExtractPropTypes } from 'vue'
|
||||
import type { PrivateSiderMenuProps } from './components/SiderMenu/SiderMenu'
|
||||
|
||||
export const headerViewProps = () => ({
|
||||
// 集成
|
||||
...globalHeaderProps(),
|
||||
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
logo: {
|
||||
type: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
|
||||
default: () => undefined
|
||||
},
|
||||
headerRender: {
|
||||
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<HeaderRender>>,
|
||||
default: () => undefined
|
||||
},
|
||||
headerTitleRender: {
|
||||
type: [Function, Boolean] as PropType<WithFalse<HeaderTitleRender>>,
|
||||
default: () => undefined
|
||||
},
|
||||
headerContentRender: {
|
||||
type: [Function, Boolean] as PropType<WithFalse<HeaderContentRender>>,
|
||||
default: () => undefined
|
||||
},
|
||||
siderWidth: {
|
||||
type: Number,
|
||||
default: 208
|
||||
},
|
||||
hasSiderMenu: Boolean,
|
||||
visible: Boolean
|
||||
})
|
||||
|
||||
export type HeaderViewProps = Partial<ExtractPropTypes<ReturnType<typeof headerViewProps>>>
|
||||
|
||||
// TODO slots 支持
|
||||
const renderContent = (props: HeaderViewProps & PrivateSiderMenuProps, slots: Slots) => {
|
||||
const clearMenuData = clearMenuItem(props.menuData || [])
|
||||
|
||||
const headerContentRender = getRender<HeaderContentRender>(props, slots, 'headerContentRender')
|
||||
|
||||
let defaultDom
|
||||
if (props.layout === 'top' && !props.isMobile) {
|
||||
defaultDom = (
|
||||
<TopNavHeader
|
||||
theme={props.navTheme as 'light' | 'dark'}
|
||||
mode="horizontal"
|
||||
onCollapse={props.onCollapse}
|
||||
{...props}
|
||||
menuData={clearMenuData}
|
||||
>
|
||||
{slots}
|
||||
</TopNavHeader>
|
||||
)
|
||||
} else {
|
||||
defaultDom = (
|
||||
<GlobalHeader onCollapse={props.onCollapse} {...props} menuData={clearMenuData}>
|
||||
{{
|
||||
...slots,
|
||||
default: () => headerContentRender && headerContentRender(props, null)
|
||||
}}
|
||||
</GlobalHeader>
|
||||
)
|
||||
}
|
||||
|
||||
const headerRender = getRender<HeaderRender>(props, slots, 'headerRender')
|
||||
if (headerRender && typeof headerRender === 'function') {
|
||||
return headerRender(props, defaultDom)
|
||||
}
|
||||
return defaultDom
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicHeader',
|
||||
props: headerViewProps(),
|
||||
setup(props, { slots, attrs }) {
|
||||
const needFixedHeader = computed(() => props.fixedHeader || props.layout === 'mix')
|
||||
const isTop = computed(() => props.layout === 'top')
|
||||
|
||||
const className = computed(() => [
|
||||
attrs.class,
|
||||
{
|
||||
[`${props.prefixCls}-fixed-header`]: needFixedHeader.value,
|
||||
[`${props.prefixCls}-fixed-header-action`]: !props.collapsed,
|
||||
[`${props.prefixCls}-top-menu`]: isTop.value,
|
||||
[`${props.prefixCls}-header-${props.navTheme}`]: props.navTheme && props.layout !== 'mix'
|
||||
}
|
||||
])
|
||||
|
||||
/** 计算侧边栏的宽度,不然导致左边的样式会出问题 */
|
||||
const width = computed(() => {
|
||||
const needSettingWidth =
|
||||
needFixedHeader.value && props.hasSiderMenu && !isTop.value && !props.isMobile
|
||||
return props.layout !== 'mix' && needSettingWidth
|
||||
? `calc(100% - ${props.collapsed ? 48 : props.siderWidth}px)`
|
||||
: '100%'
|
||||
})
|
||||
|
||||
const right = computed(() => (needFixedHeader.value ? 0 : undefined))
|
||||
|
||||
return () => (
|
||||
<>
|
||||
{needFixedHeader.value && (
|
||||
<Layout.Header
|
||||
style={{
|
||||
height: `${props.headerHeight}px`,
|
||||
lineHeight: `${props.headerHeight}px`,
|
||||
background: 'transparent'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Layout.Header
|
||||
style={{
|
||||
padding: 0,
|
||||
height: `${props.headerHeight}px`,
|
||||
lineHeight: `${props.headerHeight}px`,
|
||||
width: width.value,
|
||||
zIndex: props.layout === 'mix' ? 100 : 19,
|
||||
right: right.value,
|
||||
...(attrs.style as CSSProperties)
|
||||
}}
|
||||
class={className.value}
|
||||
>
|
||||
{renderContent(props as any, slots)}
|
||||
</Layout.Header>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
@ -0,0 +1,43 @@
|
||||
import type { BreadcrumbProps } from 'ant-design-vue'
|
||||
// import type { BreadcrumbListReturn } from './utils/getBreadcrumbProps'
|
||||
import type { PureSettings } from './defaultSettings'
|
||||
import type { MenuDataItem } from './types'
|
||||
import type { WaterMarkProps } from './components/WaterMark'
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
export type RouteContextType = {
|
||||
// breadcrumb?: BreadcrumbListReturn
|
||||
menuData?: MenuDataItem[]
|
||||
isMobile?: boolean
|
||||
prefixCls?: string
|
||||
collapsed?: boolean
|
||||
hasSiderMenu?: boolean
|
||||
hasHeader?: boolean
|
||||
siderWidth?: number
|
||||
isChildrenLayout?: boolean
|
||||
hasFooterToolbar?: boolean
|
||||
hasFooter?: boolean
|
||||
setHasFooterToolbar?: (hasFooterToolbar: boolean) => void
|
||||
pageTitleInfo?: {
|
||||
title: string
|
||||
id: string
|
||||
pageName: string
|
||||
}
|
||||
matchMenus?: MenuDataItem[]
|
||||
matchMenuKeys?: string[]
|
||||
currentMenu?: PureSettings & MenuDataItem
|
||||
/** PageHeader 的 BreadcrumbProps 配置,会透传下去 */
|
||||
breadcrumbProps?: BreadcrumbProps
|
||||
waterMarkProps?: WaterMarkProps
|
||||
} & Partial<PureSettings>
|
||||
|
||||
export const routeContextInjectKey = Symbol(
|
||||
'routeContextInjectKey'
|
||||
) as InjectionKey<RouteContextType>
|
||||
|
||||
const defaultPrefixCls = 'ant'
|
||||
|
||||
export const getPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => {
|
||||
if (customizePrefixCls) return customizePrefixCls
|
||||
return suffixCls ? `${defaultPrefixCls}-${suffixCls}` : defaultPrefixCls
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import { Layout } from 'ant-design-vue'
|
||||
|
||||
export interface WrapContentProps {
|
||||
isChildrenLayout?: boolean
|
||||
location?: any
|
||||
contentHeight?: number | string
|
||||
ErrorBoundary?: any
|
||||
}
|
||||
|
||||
// TODO 异常处理
|
||||
const WrapContent: FunctionalComponent<WrapContentProps> = (props, { slots, attrs }) => {
|
||||
// const ErrorComponent = props.ErrorBoundary || ErrorBoundary
|
||||
return (
|
||||
// {props.ErrorBoundary === false ? (
|
||||
// <Layout.Content class={className} style={style}>
|
||||
// {slots.default}
|
||||
// </Layout.Content>
|
||||
// ) : (
|
||||
// <ErrorComponent>
|
||||
// <Layout.Content class={className} style={style}>
|
||||
// {children}
|
||||
// </Layout.Content>
|
||||
// </ErrorComponent>
|
||||
// )}
|
||||
<Layout.Content class={attrs.class} style={attrs.style}>
|
||||
{slots.default?.()}
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export default WrapContent
|
@ -0,0 +1,30 @@
|
||||
// TODO 图标按需加载
|
||||
import * as AntIcons from '@ant-design/icons-vue'
|
||||
import { createVNode, defineComponent } from 'vue'
|
||||
|
||||
const AntIcon = defineComponent({
|
||||
name: 'AntIcon',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const iconDom = computed(() => {
|
||||
let iconType = props.type
|
||||
.replace(/-([a-z])/g, (p, m) => m.toUpperCase())
|
||||
.replace(/^\S/, s => s.toUpperCase())
|
||||
|
||||
if (!iconType.endsWith('Outlined')) {
|
||||
iconType = iconType + 'Outlined'
|
||||
}
|
||||
// @ts-ignore
|
||||
const antIcon = AntIcons[iconType]
|
||||
return antIcon ? createVNode(antIcon) : props.type
|
||||
})
|
||||
return () => iconDom.value
|
||||
}
|
||||
})
|
||||
|
||||
export default AntIcon
|
@ -0,0 +1,33 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
|
||||
@pro-footer-bar-prefix-cls: ~'@{ant-prefix}-pro-footer-bar';
|
||||
|
||||
.@{pro-footer-bar-prefix-cls} {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
line-height: 44px;
|
||||
background: @component-background;
|
||||
border-top: 1px solid @border-color-split;
|
||||
box-shadow: @shadow-1-up;
|
||||
transition: width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
&-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-right {
|
||||
> * {
|
||||
margin-right: 8px;
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import './index.less'
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
|
||||
import type { VueNodeOrRender } from '#/types'
|
||||
import type { RouteContextType } from '../../RouteContext'
|
||||
import type { CSSProperties, PropType, VNode } from 'vue'
|
||||
import { VueNodeOrRenderPropType } from '#/types'
|
||||
import { getPrefixCls, routeContextInjectKey } from '../../RouteContext'
|
||||
|
||||
export type FooterToolbarProps = {
|
||||
extra?: VueNodeOrRender
|
||||
renderContent?: (
|
||||
props: FooterToolbarProps & RouteContextType & { leftWidth?: string },
|
||||
dom: JSX.Element
|
||||
) => VNode
|
||||
prefixCls?: string
|
||||
}
|
||||
|
||||
const FooterToolbar = defineComponent({
|
||||
props: {
|
||||
extra: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
|
||||
renderContent: {
|
||||
type: Function as PropType<FooterToolbarProps['renderContent']>,
|
||||
default: undefined
|
||||
},
|
||||
prefixCls: { type: String, default: undefined }
|
||||
},
|
||||
setup(props, { slots, attrs }) {
|
||||
const routeContext = inject(routeContextInjectKey, {})
|
||||
const prefixCls = props.prefixCls || getPrefixCls('pro')
|
||||
const baseClassName = `${prefixCls}-footer-bar`
|
||||
|
||||
const width = computed<string | undefined>(() => {
|
||||
if (!routeContext.hasSiderMenu) {
|
||||
return undefined
|
||||
}
|
||||
// 0 or undefined
|
||||
if (!routeContext.siderWidth) {
|
||||
return '100%'
|
||||
}
|
||||
return routeContext.isMobile ? '100%' : `calc(100% - ${routeContext.siderWidth}px)`
|
||||
})
|
||||
|
||||
/** 告诉 props 是否存在 footerBar */
|
||||
onMounted(() => routeContext?.setHasFooterToolbar?.(true))
|
||||
onUnmounted(() => routeContext?.setHasFooterToolbar?.(false))
|
||||
|
||||
return () => {
|
||||
const dom = (
|
||||
<>
|
||||
<div class={`${baseClassName}-left`}>{props.extra}</div>
|
||||
<div class={`${baseClassName}-right`}>{slots.default?.()}</div>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
class={[attrs.class, `${baseClassName}`]}
|
||||
style={{ width: width.value, ...(attrs.style as CSSProperties) }}
|
||||
// @ts-ignore
|
||||
{...reactiveOmit(restProps, 'prefixCls', 'ex')}
|
||||
>
|
||||
{props.renderContent
|
||||
? props.renderContent(
|
||||
{
|
||||
...props,
|
||||
...routeContext,
|
||||
leftWidth: width.value
|
||||
},
|
||||
dom
|
||||
)
|
||||
: dom}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default FooterToolbar
|
@ -0,0 +1,32 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
|
||||
@pro-global-footer-prefix-cls: ~'@{ant-prefix}-pro-global-footer';
|
||||
|
||||
.@{pro-global-footer-prefix-cls} {
|
||||
margin: 48px 0 24px 0;
|
||||
padding: 0 16px;
|
||||
text-align: center;
|
||||
|
||||
&-links {
|
||||
margin-bottom: 8px;
|
||||
|
||||
a {
|
||||
color: @text-color-secondary;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-copyright {
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import './index.less'
|
||||
import { getVueNode } from '../../utils'
|
||||
|
||||
import type { WithFalse } from '../../types'
|
||||
import type { CSSProperties, PropType, Slot } from 'vue'
|
||||
import type { VueNodeOrRender } from '#/types'
|
||||
import { getPrefixCls } from '#/layout/RouteContext'
|
||||
|
||||
type LinkInfo = {
|
||||
key?: string
|
||||
title: VueNodeOrRender
|
||||
href: string
|
||||
blankTarget?: boolean
|
||||
}
|
||||
|
||||
export type Link = WithFalse<VueNodeOrRender | LinkInfo[]>
|
||||
|
||||
export interface GlobalFooterProps {
|
||||
links: Link
|
||||
copyright?: WithFalse<string>
|
||||
prefixCls?: string
|
||||
}
|
||||
|
||||
function renderLinks(links: Link, linksSlot?: Slot) {
|
||||
if (Array.isArray(links)) {
|
||||
if (links.length === 0) {
|
||||
return null
|
||||
}
|
||||
return (links as LinkInfo[]).map(link => (
|
||||
<a
|
||||
key={link.key}
|
||||
title={link.key}
|
||||
target={link.blankTarget ? '_blank' : '_self'}
|
||||
href={link.href}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{link.title}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
return getVueNode(links as WithFalse<VueNodeOrRender>, linksSlot)
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'GlobalFooter',
|
||||
props: {
|
||||
links: {
|
||||
type: [Object, Function, String, Boolean, Array] as PropType<Link>,
|
||||
default: () => {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
copyright: {
|
||||
type: [Object, Function, String, Boolean] as PropType<WithFalse<VueNodeOrRender>>,
|
||||
default: () => {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
prefixCls: {
|
||||
type: String,
|
||||
default: 'pro-global-footer'
|
||||
}
|
||||
},
|
||||
setup(props, { slots, attrs }) {
|
||||
return () => {
|
||||
const linksDom = renderLinks(props.links, slots.links)
|
||||
const copyrightDom = getVueNode(props.copyright, slots.copyright)
|
||||
if (linksDom && copyrightDom == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const globalPrefixCls = getPrefixCls()
|
||||
const baseClassName = `${globalPrefixCls}-${props.prefixCls}`
|
||||
const clsString = [baseClassName, attrs.class]
|
||||
|
||||
return (
|
||||
<div class={clsString} style={attrs.style as CSSProperties}>
|
||||
{linksDom && <div class={`${baseClassName}-links`}>{linksDom}</div>}
|
||||
{copyrightDom && <div class={`${baseClassName}-copyright`}>{copyrightDom}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
@ -0,0 +1,98 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
@import (reference) '../../BasicLayout.less';
|
||||
|
||||
@pro-layout-global-header-prefix-cls: ~'@{ant-prefix}-pro-global-header';
|
||||
|
||||
@pro-layout-header-bg: @component-background;
|
||||
@pro-layout-header-hover-bg: @component-background;
|
||||
@pro-layout-header-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
.@{pro-layout-global-header-prefix-cls} {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
background: @pro-layout-header-bg;
|
||||
box-shadow: @pro-layout-header-box-shadow;
|
||||
> * {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-collapsed-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&-layout {
|
||||
&-mix {
|
||||
background-color: @layout-sider-background;
|
||||
.@{pro-layout-global-header-prefix-cls}-logo {
|
||||
h1 {
|
||||
color: @btn-primary-color;
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
color: @btn-primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-logo {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
img {
|
||||
height: 28px;
|
||||
}
|
||||
h1 {
|
||||
height: 32px;
|
||||
margin: 0 0 0 12px;
|
||||
color: @primary-color;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-logo-rtl {
|
||||
a {
|
||||
h1 {
|
||||
margin: 0 12px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-menu {
|
||||
.anticon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.@{ant-prefix}-dropdown-menu-item {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
height: @pro-layout-header-height;
|
||||
.action {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
> i {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
&:hover,
|
||||
&.opened {
|
||||
background: @primary-color;
|
||||
}
|
||||
.@{ant-prefix}-badge {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
@pro-layout-grid-content-prefix-cls: ~'@{ant-prefix}-pro-grid-content';
|
||||
|
||||
.@{pro-layout-grid-content-prefix-cls} {
|
||||
width: 100%;
|
||||
&.wide {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import './GridContent.less'
|
||||
|
||||
import type { PureSettings } from '../../defaultSettings'
|
||||
import type { CSSProperties, FunctionalComponent } from 'vue'
|
||||
import { getPrefixCls, routeContextInjectKey } from '#/layout/RouteContext'
|
||||
|
||||
type GridContentProps = {
|
||||
contentWidth?: PureSettings['contentWidth']
|
||||
prefixCls?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This component can support contentWidth so you don't need to calculate the width
|
||||
* contentWidth=Fixed, width will is 1200
|
||||
*
|
||||
* @param props
|
||||
* @param attrs
|
||||
* @param slots
|
||||
*/
|
||||
const GridContent: FunctionalComponent<GridContentProps> = (props, { attrs, slots }) => {
|
||||
const routeContext = inject(routeContextInjectKey, {})
|
||||
const prefixCls = props.prefixCls || getPrefixCls('pro')
|
||||
const contentWidth = props.contentWidth || routeContext.contentWidth
|
||||
const className = `${prefixCls}-grid-content`
|
||||
|
||||
return (
|
||||
<div
|
||||
class={[className, attrs.class, { wide: contentWidth === 'Fixed' }]}
|
||||
style={attrs.style as CSSProperties}
|
||||
>
|
||||
<div class={`${prefixCls}-grid-content-children`}>{slots.default?.()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GridContent
|
@ -0,0 +1,116 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
|
||||
@pro-layout-page-container: ~'@{ant-prefix}-pro-page-container';
|
||||
|
||||
@pro-layout-page-container-content-margin: 24px 24px 0;
|
||||
@pro-layout-page-container-content-padding: inherit;
|
||||
@pro-layout-page-container-bg-color: inherit;
|
||||
@pro-layout-page-container-warp-bg-color: @component-background;
|
||||
|
||||
.@{pro-layout-page-container}-children-content {
|
||||
margin: @pro-layout-page-container-content-margin;
|
||||
padding: @pro-layout-page-container-content-padding;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container} {
|
||||
background-color: @pro-layout-page-container-bg-color;
|
||||
&-warp {
|
||||
background-color: @pro-layout-page-container-warp-bg-color;
|
||||
.@{ant-prefix}-tabs-nav {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
&-ghost {
|
||||
.@{pro-layout-page-container}-warp {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-children-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-main {
|
||||
.@{pro-layout-page-container}-detail {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-title-content {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-title,
|
||||
.@{pro-layout-page-container}-content {
|
||||
flex: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-extraContent,
|
||||
.@{pro-layout-page-container}-main {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-logo {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-extraContent {
|
||||
min-width: 242px;
|
||||
margin-left: 88px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-xl) {
|
||||
.@{pro-layout-page-container}-main {
|
||||
.@{pro-layout-page-container}-extraContent {
|
||||
margin-left: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-lg) {
|
||||
.@{pro-layout-page-container}-main {
|
||||
.@{pro-layout-page-container}-extraContent {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-md) {
|
||||
.@{pro-layout-page-container}-main {
|
||||
.@{pro-layout-page-container}-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.@{pro-layout-page-container}-action,
|
||||
.@{pro-layout-page-container}-extraContent {
|
||||
margin-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-sm) {
|
||||
.@{pro-layout-page-container}-detail {
|
||||
display: block;
|
||||
}
|
||||
.@{pro-layout-page-container}-extraContent {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { Spin } from 'ant-design-vue'
|
||||
import 'ant-design-vue/es/spin/style/index.less'
|
||||
|
||||
import type { SpinProps } from 'ant-design-vue'
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
|
||||
const PageLoading: FunctionalComponent<SpinProps> = (props: SpinProps) => (
|
||||
<div style={{ paddingTop: '100px', textAlign: 'center' }}>
|
||||
<Spin size="large" {...props} />
|
||||
</div>
|
||||
)
|
||||
|
||||
export default PageLoading
|
@ -0,0 +1,49 @@
|
||||
import { Tooltip } from 'ant-design-vue'
|
||||
import { CheckOutlined } from '@ant-design/icons-vue'
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
|
||||
export type BlockCheckboxProps = {
|
||||
value: string
|
||||
onChange: (key: string) => void
|
||||
list?: {
|
||||
title: string
|
||||
key: string
|
||||
}[]
|
||||
configType: string
|
||||
prefixCls: string
|
||||
}
|
||||
|
||||
const BlockCheckbox: FunctionalComponent<BlockCheckboxProps> = props => {
|
||||
const baseClassName = `${props.prefixCls}-drawer-block-checkbox`
|
||||
const domList = (props.list || []).map(item => (
|
||||
<Tooltip title={item.title} key={item.key}>
|
||||
<div
|
||||
class={[
|
||||
`${baseClassName}-item`,
|
||||
`${baseClassName}-item-${item.key}`,
|
||||
`${baseClassName}-${props.configType}-item`
|
||||
]}
|
||||
onClick={() => props.onChange(item.key)}
|
||||
>
|
||||
<CheckOutlined
|
||||
class={`${baseClassName}-selectIcon`}
|
||||
style={{
|
||||
display: props.value === item.key ? 'block' : 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))
|
||||
return (
|
||||
<div
|
||||
class={baseClassName}
|
||||
style={{
|
||||
minHeight: '42px'
|
||||
}}
|
||||
>
|
||||
{domList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlockCheckbox
|
@ -0,0 +1,124 @@
|
||||
import { List, Tooltip, Select, Switch } from 'ant-design-vue'
|
||||
import { defaultSettings } from '../../defaultSettings'
|
||||
import { getFormatMessage } from './index'
|
||||
|
||||
import type { ProSettings } from '../../defaultSettings'
|
||||
import type { SettingItemProps } from './index'
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
|
||||
export const renderLayoutSettingItem = ({ item }: { item: SettingItemProps; index: number }) => {
|
||||
const action = item.action
|
||||
// const action = React.cloneElement(item.action, {
|
||||
// disabled: item.disabled
|
||||
// })
|
||||
return (
|
||||
<Tooltip title={item.disabled ? item.disabledReason : ''} placement="left">
|
||||
<List.Item actions={[action]}>
|
||||
<span style={{ opacity: item.disabled ? 0.5 : 1 }}>{item.title}</span>
|
||||
</List.Item>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
const LayoutSetting: FunctionalComponent<{
|
||||
settings: Partial<ProSettings>
|
||||
changeSetting: (key: string, value: any, hideLoading?: boolean) => void
|
||||
}> = props => {
|
||||
const formatMessage = getFormatMessage()
|
||||
const { contentWidth, splitMenus, fixedHeader, layout, fixSiderbar } =
|
||||
props.settings || defaultSettings
|
||||
|
||||
return (
|
||||
<List
|
||||
split={false}
|
||||
dataSource={[
|
||||
{
|
||||
title: formatMessage({
|
||||
id: 'app.setting.content-width',
|
||||
defaultMessage: 'Content Width'
|
||||
}),
|
||||
action: (
|
||||
<Select
|
||||
value={contentWidth || 'Fixed'}
|
||||
size="small"
|
||||
class="content-width"
|
||||
// @ts-ignore
|
||||
onSelect={(value: string) => {
|
||||
props.changeSetting('contentWidth', value)
|
||||
}}
|
||||
style={{ width: '80px' }}
|
||||
>
|
||||
{layout === 'side' ? null : (
|
||||
<Select.Option value="Fixed">
|
||||
{formatMessage({
|
||||
id: 'app.setting.content-width.fixed',
|
||||
defaultMessage: 'Fixed'
|
||||
})}
|
||||
</Select.Option>
|
||||
)}
|
||||
<Select.Option value="Fluid">
|
||||
{formatMessage({
|
||||
id: 'app.setting.content-width.fluid',
|
||||
defaultMessage: 'Fluid'
|
||||
})}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: formatMessage({
|
||||
id: 'app.setting.fixedheader',
|
||||
defaultMessage: 'Fixed Header'
|
||||
}),
|
||||
action: (
|
||||
<Switch
|
||||
size="small"
|
||||
class="fixed-header"
|
||||
checked={!!fixedHeader}
|
||||
onChange={checked => {
|
||||
props.changeSetting('fixedHeader', checked)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: formatMessage({
|
||||
id: 'app.setting.fixedsidebar',
|
||||
defaultMessage: 'Fixed Sidebar'
|
||||
}),
|
||||
disabled: layout === 'top',
|
||||
disabledReason: formatMessage({
|
||||
id: 'app.setting.fixedsidebar.hint',
|
||||
defaultMessage: 'Works on Side Menu Layout'
|
||||
}),
|
||||
action: (
|
||||
<Switch
|
||||
size="small"
|
||||
class="fix-siderbar"
|
||||
checked={!!fixSiderbar}
|
||||
disabled={layout === 'top'}
|
||||
onChange={checked => props.changeSetting('fixSiderbar', checked)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: formatMessage({ id: 'app.setting.splitMenus' }),
|
||||
disabled: layout !== 'mix',
|
||||
action: (
|
||||
<Switch
|
||||
size="small"
|
||||
checked={!!splitMenus}
|
||||
class="split-menus"
|
||||
disabled={layout !== 'mix'}
|
||||
onChange={checked => {
|
||||
props.changeSetting('splitMenus', checked)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
renderItem={renderLayoutSettingItem}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutSetting
|
@ -0,0 +1,41 @@
|
||||
import { Switch, List } from 'ant-design-vue'
|
||||
import { getFormatMessage } from './index'
|
||||
import { renderLayoutSettingItem } from './LayoutChange'
|
||||
|
||||
import type { ProSettings } from '../../defaultSettings'
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
|
||||
const RegionalSetting: FunctionalComponent<{
|
||||
settings: Partial<ProSettings>
|
||||
changeSetting: (key: string, value: any, hideLoading?: boolean) => void
|
||||
}> = props => {
|
||||
const formatMessage = getFormatMessage()
|
||||
const regionalSetting = ['header', 'footer', 'menu', 'menuHeader']
|
||||
return (
|
||||
<List
|
||||
split={false}
|
||||
// @ts-ignore
|
||||
renderItem={renderLayoutSettingItem}
|
||||
dataSource={regionalSetting.map(key => {
|
||||
return {
|
||||
title: formatMessage({ id: `app.setting.regionalsettings.${key}` }),
|
||||
action: (
|
||||
<Switch
|
||||
size="small"
|
||||
class={`regional-${key}`}
|
||||
checked={
|
||||
// @ts-ignore
|
||||
props.settings[`${key}Render`] || props.settings[`${key}Render`] === undefined
|
||||
}
|
||||
onChange={checked =>
|
||||
props.changeSetting(`${key}Render`, checked === true ? undefined : false)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegionalSetting
|
@ -0,0 +1,25 @@
|
||||
@import (reference) 'index.less';
|
||||
|
||||
.@{ant-pro-setting-drawer}-content {
|
||||
.theme-color {
|
||||
margin-top: 16px;
|
||||
overflow: hidden;
|
||||
.theme-color-title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.theme-color-block {
|
||||
float: left;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 8px;
|
||||
margin-right: 8px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import './ThemeColor.less'
|
||||
|
||||
import { CheckOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
import { Tooltip } from 'ant-design-vue'
|
||||
import type { FunctionalComponent, HtmlHTMLAttributes } from 'vue'
|
||||
|
||||
export type TagProps = {
|
||||
color: string
|
||||
check: boolean
|
||||
} & HtmlHTMLAttributes
|
||||
|
||||
const Tag: FunctionalComponent<TagProps> = props => (
|
||||
<div onClick={props.onClick} style={{ backgroundColor: props.color }}>
|
||||
{props.check ? <CheckOutlined /> : ''}
|
||||
</div>
|
||||
)
|
||||
|
||||
export type ThemeColorProps = {
|
||||
colorList?: {
|
||||
key: string
|
||||
color: string
|
||||
}[]
|
||||
value: string
|
||||
onChange: (color: string) => void
|
||||
formatMessage: (data: { id: any; defaultMessage?: string }) => string
|
||||
}
|
||||
|
||||
const ThemeColor: FunctionalComponent<ThemeColorProps> = props => {
|
||||
if (!props.colorList || props.colorList?.length < 1) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div class="theme-color">
|
||||
<div class="theme-color-content">
|
||||
{props.colorList?.map(({ key, color }) => {
|
||||
if (!key) return
|
||||
return (
|
||||
<Tooltip
|
||||
key={color}
|
||||
title={props.formatMessage({
|
||||
id: `app.setting.themecolor.${key}`
|
||||
})}
|
||||
>
|
||||
<Tag
|
||||
class="theme-color-block"
|
||||
color={color}
|
||||
check={props.value === color}
|
||||
onClick={() => props.onChange && props.onChange(color)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThemeColor
|
@ -0,0 +1,161 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
|
||||
@ant-pro-setting-drawer: ~'@{ant-prefix}-pro-setting-drawer';
|
||||
|
||||
.@{ant-pro-setting-drawer} {
|
||||
&-content {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
.@{ant-prefix}-list-item {
|
||||
span {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-block-checkbox {
|
||||
display: flex;
|
||||
&-item {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 36px;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2.5px 0 rgba(0, 0, 0, 0.18);
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
content: '';
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: #fff;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&-realDark {
|
||||
background-color: fade(@menu-dark-bg, 85%);
|
||||
&::before {
|
||||
background-color: fade(@menu-dark-bg, 65%);
|
||||
content: '';
|
||||
}
|
||||
&::after {
|
||||
background-color: fade(@menu-dark-bg, 85%);
|
||||
}
|
||||
}
|
||||
|
||||
// 亮色主题
|
||||
&-light {
|
||||
&::before {
|
||||
background-color: @white;
|
||||
content: '';
|
||||
}
|
||||
&::after {
|
||||
background-color: @white;
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色主题
|
||||
&-dark,
|
||||
// 侧边菜单布局
|
||||
&-side {
|
||||
&::before {
|
||||
z-index: 1;
|
||||
background-color: @menu-dark-bg;
|
||||
content: '';
|
||||
}
|
||||
&::after {
|
||||
background-color: @white;
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部菜单布局
|
||||
&-top {
|
||||
&::before {
|
||||
background-color: transparent;
|
||||
content: '';
|
||||
}
|
||||
&::after {
|
||||
background-color: @menu-dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部菜单布局
|
||||
&-mix {
|
||||
&::before {
|
||||
background-color: @white;
|
||||
content: '';
|
||||
}
|
||||
&::after {
|
||||
background-color: @menu-dark-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-selectIcon {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
bottom: 4px;
|
||||
color: @primary-color;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
.action {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-color_block {
|
||||
display: inline-block;
|
||||
width: 38px;
|
||||
height: 22px;
|
||||
margin: 4px;
|
||||
margin-right: 12px;
|
||||
vertical-align: middle;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-bottom: 12px;
|
||||
color: @heading-color;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&-handle {
|
||||
position: absolute;
|
||||
top: 240px;
|
||||
right: 300px;
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
background-color: @primary-color;
|
||||
border-radius: 4px 0 0 4px;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&-production-hint {
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
@ -0,0 +1,419 @@
|
||||
import './index.less'
|
||||
|
||||
import {
|
||||
CloseOutlined,
|
||||
CopyOutlined,
|
||||
NotificationOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
ConfigProvider,
|
||||
Divider,
|
||||
Drawer,
|
||||
List,
|
||||
message,
|
||||
Switch
|
||||
} from 'ant-design-vue'
|
||||
import 'ant-design-vue/es/alert/style'
|
||||
import 'ant-design-vue/es/button/style'
|
||||
import 'ant-design-vue/es/divider/style'
|
||||
import 'ant-design-vue/es/drawer/style'
|
||||
import 'ant-design-vue/es/list/style'
|
||||
import 'ant-design-vue/es/message/style'
|
||||
import 'ant-design-vue/es/switch/style'
|
||||
import 'ant-design-vue/es/tooltip/style'
|
||||
import 'ant-design-vue/es/select/style'
|
||||
|
||||
import type { ProSettings } from '../../defaultSettings'
|
||||
import { defaultSettings } from '../../defaultSettings'
|
||||
|
||||
import BlockCheckbox from './BlockCheckbox'
|
||||
import ThemeColor from './ThemeColor'
|
||||
import { gLocaleObject } from '../../locales'
|
||||
import LayoutSetting, { renderLayoutSettingItem } from './LayoutChange'
|
||||
import RegionalSetting from './RegionalChange'
|
||||
import { genStringToTheme } from '../../utils/utils'
|
||||
import type { VueNodeOrRender } from '#/types'
|
||||
import type { PropType, FunctionalComponent } from 'vue'
|
||||
|
||||
type BodyProps = {
|
||||
title: string
|
||||
prefixCls: string
|
||||
}
|
||||
|
||||
const Body: FunctionalComponent<BodyProps> = (props, { slots }) => (
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h3 class={`${props.prefixCls}-drawer-title`}>{props.title}</h3>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
)
|
||||
|
||||
export type SettingItemProps = {
|
||||
title: VueNodeOrRender
|
||||
action: VueNodeOrRender
|
||||
disabled?: boolean
|
||||
disabledReason?: VueNodeOrRender
|
||||
}
|
||||
|
||||
export type SettingDrawerProps = {
|
||||
settings?: ProSettings
|
||||
collapse?: boolean
|
||||
getContainer?: any
|
||||
hideHintAlert?: boolean
|
||||
hideCopyButton?: boolean
|
||||
/** 使用实验性质的黑色主题 */
|
||||
enableDarkTheme?: boolean
|
||||
prefixCls?: string
|
||||
colorList?: false | { key: string; color: string }[]
|
||||
onSettingChange?: (settings: ProSettings) => void
|
||||
pathname?: string
|
||||
disableUrlParams?: boolean
|
||||
themeOnly?: boolean
|
||||
}
|
||||
|
||||
const settingDrawerProps = {
|
||||
defaultSettings: {
|
||||
type: Object as PropType<ProSettings>,
|
||||
default: () => defaultSettings
|
||||
},
|
||||
settings: {
|
||||
type: Object as PropType<ProSettings>,
|
||||
default: () => defaultSettings
|
||||
},
|
||||
collapse: Boolean,
|
||||
getContainer: [Function, Object] as PropType<any>,
|
||||
hideHintAlert: Boolean,
|
||||
hideCopyButton: Boolean,
|
||||
/** 使用实验性质的黑色主题 */
|
||||
enableDarkTheme: Boolean,
|
||||
prefixCls: {
|
||||
type: String,
|
||||
default: 'ant-pro'
|
||||
},
|
||||
colorList: {
|
||||
type: [Boolean, Array] as PropType<SettingDrawerProps['colorList']>,
|
||||
default: () => [
|
||||
{ key: 'daybreak', color: '#1890ff' },
|
||||
{ key: 'dust', color: '#F5222D' },
|
||||
{ key: 'volcano', color: '#FA541C' },
|
||||
{ key: 'sunset', color: '#FAAD14' },
|
||||
{ key: 'cyan', color: '#13C2C2' },
|
||||
{ key: 'green', color: '#52C41A' },
|
||||
{ key: 'geekblue', color: '#2F54EB' },
|
||||
{ key: 'purple', color: '#722ED1' }
|
||||
]
|
||||
},
|
||||
onSettingChange: Function as PropType<SettingDrawerProps['onSettingChange']>,
|
||||
pathname: { type: String, default: window.location.pathname },
|
||||
disableUrlParams: { type: Boolean, default: true },
|
||||
themeOnly: Boolean
|
||||
}
|
||||
|
||||
type FormatMessageFunc = (data: { id: string; defaultMessage?: string }) => string
|
||||
export const getFormatMessage = (): FormatMessageFunc => {
|
||||
return ({ id }: { id: string; defaultMessage?: string }): string => {
|
||||
const locales = gLocaleObject()
|
||||
return locales[id]
|
||||
}
|
||||
}
|
||||
|
||||
const updateTheme = async (dark: boolean, color?: string) => {
|
||||
if (typeof window === 'undefined') return
|
||||
if (typeof window.MutationObserver === 'undefined') return
|
||||
|
||||
if (!ConfigProvider.config) return
|
||||
ConfigProvider.config({
|
||||
theme: {
|
||||
primaryColor: genStringToTheme(color) || '#1890ff'
|
||||
}
|
||||
})
|
||||
|
||||
// if (dark) {
|
||||
// const defaultTheme = {
|
||||
// brightness: 100,
|
||||
// contrast: 90,
|
||||
// sepia: 10
|
||||
// }
|
||||
//
|
||||
// const defaultFixes = {
|
||||
// invert: [],
|
||||
// css: '',
|
||||
// ignoreInlineStyle: ['.react-switch-handle'],
|
||||
// ignoreImageAnalysis: [],
|
||||
// disableStyleSheetsProxy: true
|
||||
// }
|
||||
// if (window.MutationObserver && window.fetch) {
|
||||
// setFetch(window.fetch)
|
||||
// darkreaderEnable(defaultTheme, defaultFixes)
|
||||
// }
|
||||
// } else {
|
||||
// if (window.MutationObserver) darkreaderDisable()
|
||||
// }
|
||||
}
|
||||
|
||||
const genCopySettingJson = (settingState: ProSettings) =>
|
||||
JSON.stringify({ ...settingState }, null, 2)
|
||||
|
||||
/**
|
||||
* 可视化配置组件
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
const SettingDrawer = defineComponent({
|
||||
props: settingDrawerProps,
|
||||
emits: ['update:collapse', 'update:settings'],
|
||||
setup(props, { emit }) {
|
||||
// const firstRender = ref<boolean>(true)
|
||||
|
||||
// 隐藏显示,支持 双向绑定
|
||||
const show = ref<boolean>(false)
|
||||
const setShow = (isShow: boolean) => {
|
||||
show.value = isShow
|
||||
emit('update:collapse', show.value)
|
||||
}
|
||||
watchEffect(() => {
|
||||
show.value = props.collapse
|
||||
})
|
||||
|
||||
const settingState = reactive<ProSettings>({})
|
||||
watchEffect(() => {
|
||||
Object.assign(settingState, props.settings)
|
||||
})
|
||||
|
||||
// TODO 语言切换
|
||||
|
||||
// 监听更新主题色
|
||||
const changeTheme = () =>
|
||||
updateTheme(settingState.navTheme === 'realDark', settingState.primaryColor)
|
||||
watch(() => settingState.primaryColor, changeTheme, { immediate: true })
|
||||
watch(() => settingState.navTheme, changeTheme)
|
||||
|
||||
/**
|
||||
* 修改设置
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
const changeSetting = (key: string, value: string | boolean | number) => {
|
||||
// @ts-ignore
|
||||
settingState[key] = value
|
||||
|
||||
if (key === 'layout') {
|
||||
settingState.contentWidth = value === 'top' ? 'Fixed' : 'Fluid'
|
||||
}
|
||||
if (key === 'layout' && value !== 'mix') {
|
||||
settingState.splitMenus = false
|
||||
}
|
||||
if (key === 'layout' && value === 'mix') {
|
||||
settingState.navTheme = 'light'
|
||||
}
|
||||
if (key === 'colorWeak' && value === true) {
|
||||
const dom = document.querySelector('body')
|
||||
if (dom) {
|
||||
dom.dataset.prosettingdrawer = dom.style.filter
|
||||
dom.style.filter = 'invert(80%)'
|
||||
}
|
||||
}
|
||||
if (key === 'colorWeak' && value === false) {
|
||||
const dom = document.querySelector('body')
|
||||
if (dom) {
|
||||
dom.style.filter = dom.dataset.prosettingdrawer || 'none'
|
||||
delete dom.dataset.prosettingdrawer
|
||||
}
|
||||
}
|
||||
|
||||
emit('update:settings', toRaw(settingState))
|
||||
}
|
||||
|
||||
const formatMessage = getFormatMessage()
|
||||
|
||||
return () => {
|
||||
const baseClassName = `${props.prefixCls}-setting`
|
||||
return (
|
||||
<Drawer
|
||||
visible={show.value}
|
||||
width={300}
|
||||
closable={false}
|
||||
onClose={() => setShow(false)}
|
||||
placement="right"
|
||||
getContainer={props.getContainer}
|
||||
handle={
|
||||
<div class={`${baseClassName}-drawer-handle`} onClick={() => setShow(!show.value)}>
|
||||
{show.value ? (
|
||||
<CloseOutlined style={{ color: '#fff', fontSize: 20 }} />
|
||||
) : (
|
||||
<SettingOutlined style={{ color: '#fff', fontSize: 20 }} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
zIndex: 999
|
||||
}}
|
||||
>
|
||||
<div class={`${baseClassName}-drawer-content`}>
|
||||
<Body
|
||||
title={formatMessage({
|
||||
id: 'app.setting.pagestyle',
|
||||
defaultMessage: 'Page style setting'
|
||||
})}
|
||||
prefixCls={baseClassName}
|
||||
>
|
||||
<BlockCheckbox
|
||||
prefixCls={baseClassName}
|
||||
list={[
|
||||
{
|
||||
key: 'light',
|
||||
title: formatMessage({
|
||||
id: 'app.setting.pagestyle.light',
|
||||
defaultMessage: '亮色菜单风格'
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'dark',
|
||||
title: formatMessage({
|
||||
id: 'app.setting.pagestyle.dark',
|
||||
defaultMessage: '暗色菜单风格'
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'realDark',
|
||||
title: formatMessage({
|
||||
id: 'app.setting.pagestyle.realdark',
|
||||
defaultMessage: '暗色菜单风格'
|
||||
})
|
||||
}
|
||||
].filter(item => {
|
||||
if (item.key === 'dark' && settingState.layout === 'mix') return false
|
||||
return !(item.key === 'realDark' && !props.enableDarkTheme)
|
||||
})}
|
||||
value={settingState.navTheme!}
|
||||
configType="theme"
|
||||
key="navTheme"
|
||||
onChange={value => changeSetting('navTheme', value)}
|
||||
/>
|
||||
</Body>
|
||||
{props.colorList !== false && (
|
||||
<Body
|
||||
title={formatMessage({
|
||||
id: 'app.setting.themecolor',
|
||||
defaultMessage: 'Theme color'
|
||||
})}
|
||||
prefixCls={baseClassName}
|
||||
>
|
||||
<ThemeColor
|
||||
colorList={props.colorList}
|
||||
value={genStringToTheme(settingState.primaryColor)}
|
||||
formatMessage={formatMessage}
|
||||
onChange={color => changeSetting('primaryColor', color)}
|
||||
/>
|
||||
</Body>
|
||||
)}
|
||||
{!props.themeOnly && (
|
||||
<>
|
||||
<Divider />
|
||||
<Body
|
||||
prefixCls={baseClassName}
|
||||
title={formatMessage({ id: 'app.setting.navigationmode' })}
|
||||
>
|
||||
<BlockCheckbox
|
||||
prefixCls={baseClassName}
|
||||
value={settingState.layout!}
|
||||
key="layout"
|
||||
configType="layout"
|
||||
list={[
|
||||
{
|
||||
key: 'side',
|
||||
title: formatMessage({ id: 'app.setting.sidemenu' })
|
||||
},
|
||||
{
|
||||
key: 'top',
|
||||
title: formatMessage({ id: 'app.setting.topmenu' })
|
||||
},
|
||||
{
|
||||
key: 'mix',
|
||||
title: formatMessage({ id: 'app.setting.mixmenu' })
|
||||
}
|
||||
]}
|
||||
onChange={value => changeSetting('layout', value)}
|
||||
/>
|
||||
</Body>
|
||||
<LayoutSetting settings={settingState} changeSetting={changeSetting} />
|
||||
<Divider />
|
||||
|
||||
<Body
|
||||
prefixCls={baseClassName}
|
||||
title={formatMessage({ id: 'app.setting.regionalsettings' })}
|
||||
>
|
||||
<RegionalSetting settings={settingState} changeSetting={changeSetting} />
|
||||
</Body>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Body
|
||||
prefixCls={baseClassName}
|
||||
title={formatMessage({ id: 'app.setting.othersettings' })}
|
||||
>
|
||||
<List
|
||||
split={false}
|
||||
renderItem={renderLayoutSettingItem}
|
||||
dataSource={[
|
||||
{
|
||||
title: formatMessage({ id: 'app.setting.weakmode' }),
|
||||
action: (
|
||||
<Switch
|
||||
size="small"
|
||||
class="color-weak"
|
||||
v-model:checked={settingState.colorWeak}
|
||||
onChange={checked => {
|
||||
changeSetting('colorWeak', checked)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Body>
|
||||
{props.hideHintAlert && props.hideCopyButton ? null : <Divider />}
|
||||
|
||||
{props.hideHintAlert ? null : (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={formatMessage({
|
||||
id: 'app.setting.production.hint'
|
||||
})}
|
||||
icon={<NotificationOutlined />}
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.hideCopyButton ? null : (
|
||||
<Button
|
||||
block
|
||||
icon={<CopyOutlined />}
|
||||
style={{ marginBottom: '24px' }}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(genCopySettingJson(settingState))
|
||||
message.success(formatMessage({ id: 'app.setting.copyinfo' }))
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{formatMessage({ id: 'app.setting.copy' })}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default SettingDrawer
|
@ -0,0 +1,206 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
@import (reference) '../../BasicLayout.less';
|
||||
|
||||
@import 'ant-design-vue/es/menu/style/index.less';
|
||||
|
||||
@pro-layout-sider-menu-prefix-cls: ~'@{ant-prefix}-pro-sider';
|
||||
|
||||
@nav-header-height: @pro-layout-header-height;
|
||||
|
||||
.@{pro-layout-sider-menu-prefix-cls} {
|
||||
position: relative;
|
||||
background-color: @layout-sider-background;
|
||||
border-right: 0;
|
||||
|
||||
// 这里关掉了动画,不然使用无法兼容
|
||||
.@{ant-prefix}-menu {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.@{ant-prefix}-layout-sider-light {
|
||||
.@{ant-prefix}-menu-item a {
|
||||
color: @heading-color;
|
||||
}
|
||||
.@{ant-prefix}-menu-item-selected a,
|
||||
.@{ant-prefix}-menu-item a:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-logo {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 16px;
|
||||
cursor: pointer;
|
||||
transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
> a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
margin: 0 0 0 12px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
vertical-align: middle;
|
||||
animation: pro-layout-title-hide 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
&-extra {
|
||||
margin-bottom: 16px;
|
||||
padding: 0 16px;
|
||||
&-no-logo {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-menu {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
min-height: 100%;
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
}
|
||||
|
||||
.@{ant-prefix}-layout-sider-children {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 5px rgba(37, 37, 37, 0.05);
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 5px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
&.@{ant-prefix}-layout-sider-collapsed {
|
||||
.@{ant-prefix}-menu-inline-collapsed {
|
||||
width: 48px;
|
||||
}
|
||||
.@{pro-layout-sider-menu-prefix-cls} {
|
||||
&-logo {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.@{ant-prefix}-layout-sider.@{pro-layout-sider-menu-prefix-cls}-fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
|
||||
> .@{ant-prefix}-menu-root {
|
||||
:not(.@{pro-layout-sider-menu-prefix-cls}-link-menu) {
|
||||
height: ~'calc(100vh - @{nav-header-height})';
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-light {
|
||||
background-color: @component-background;
|
||||
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
|
||||
|
||||
.@{ant-prefix}-layout-sider-children {
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 5px rgba(0, 21, 41, 0.05);
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 5px rgba(0, 21, 41, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.@{pro-layout-sider-menu-prefix-cls}-logo {
|
||||
h1 {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
.@{ant-prefix}-menu-light {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
.@{pro-layout-sider-menu-prefix-cls}-collapsed-button {
|
||||
border-top: @border-width-base @border-style-base @border-color-split;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 14px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
&-links {
|
||||
width: 100%;
|
||||
ul.@{ant-prefix}-menu-root {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-collapsed-button {
|
||||
border-top: @border-width-base @border-style-base rgba(0, 0, 0, 0.25);
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.top-nav-menu li.@{ant-prefix}-menu-item {
|
||||
height: 100%;
|
||||
line-height: 1;
|
||||
}
|
||||
.drawer .drawer-content {
|
||||
background: @layout-sider-background;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pro-layout-title-hide {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
80% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
display: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import { Drawer } from 'ant-design-vue'
|
||||
|
||||
import SiderMenu, { privateSiderMenuProps, siderMenuProps } from './SiderMenu'
|
||||
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
const SiderMenuWrapper = defineComponent({
|
||||
name: 'SiderMenuWrapper',
|
||||
props: {
|
||||
...siderMenuProps(),
|
||||
...privateSiderMenuProps()
|
||||
},
|
||||
setup(props, { slots, attrs }) {
|
||||
// TODO 计算 flatMenuKeys
|
||||
|
||||
// 当切换设备为手机时,会自动折叠菜单
|
||||
watch(
|
||||
() => props.isMobile,
|
||||
() => {
|
||||
if (props.isMobile == true) {
|
||||
props.onCollapse?.(true)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
if (props.hide) {
|
||||
return null
|
||||
}
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
watchEffect(() => {
|
||||
// @ts-ignore
|
||||
drawerVisible.value = !props.collapsed
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
return () =>
|
||||
props.isMobile ? (
|
||||
<>
|
||||
<Drawer
|
||||
visible={drawerVisible.value}
|
||||
placement="left"
|
||||
class={[`${props.prefixCls}-drawer-sider`, attrs.class]}
|
||||
onClose={() => props.onCollapse?.(true)}
|
||||
style={{
|
||||
padding: 0,
|
||||
height: '100vh',
|
||||
...(attrs.style as CSSProperties)
|
||||
}}
|
||||
closable={false}
|
||||
getContainer={props.getContainer}
|
||||
width={props.siderWidth}
|
||||
bodyStyle={{ height: '100vh', padding: 0, display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SiderMenu
|
||||
{...props}
|
||||
// @ts-ignore
|
||||
class={[`${props.prefixCls}-sider`, attrs.class]}
|
||||
collapsed={props.isMobile ? false : props.collapsed}
|
||||
splitMenus={false}
|
||||
>
|
||||
{slots}
|
||||
</SiderMenu>
|
||||
</Drawer>
|
||||
</>
|
||||
) : (
|
||||
<SiderMenu
|
||||
class={[`${props.prefixCls}-sider`, attrs.class]}
|
||||
{...props}
|
||||
// @ts-ignore
|
||||
style={attrs.style}
|
||||
>
|
||||
{slots}
|
||||
</SiderMenu>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default SiderMenuWrapper
|
@ -0,0 +1,73 @@
|
||||
@root-entry-name: 'default';
|
||||
@import (reference) 'ant-design-vue/es/style/themes/index.less';
|
||||
@import (reference) '../../BasicLayout.less';
|
||||
|
||||
@top-nav-header-prefix-cls: ~'@{ant-prefix}-pro-top-nav-header';
|
||||
|
||||
.@{top-nav-header-prefix-cls} {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 21, 41, 0.12);
|
||||
transition: background 0.3s, width 0.2s;
|
||||
|
||||
.@{ant-prefix}-menu {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: @component-background;
|
||||
.@{top-nav-header-prefix-cls}-logo {
|
||||
h1 {
|
||||
color: @heading-color;
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding-left: 16px;
|
||||
&-left {
|
||||
display: flex;
|
||||
min-width: 192px;
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: @btn-primary-color;
|
||||
}
|
||||
|
||||
&-logo {
|
||||
position: relative;
|
||||
min-width: 165px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
img,
|
||||
a > svg {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: inline-block;
|
||||
margin: 0 0 0 12px;
|
||||
color: @btn-primary-color;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
&-menu {
|
||||
min-width: 0;
|
||||
.@{ant-prefix}-menu.@{ant-prefix}-menu-horizontal {
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
import './index.less'
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { default as ResizeObserver } from 'ant-design-vue/es/vc-resize-observer'
|
||||
import type { SiderMenuProps } from '../SiderMenu/SiderMenu'
|
||||
import type { GlobalHeaderProps } from '../GlobalHeader'
|
||||
import type { CSSProperties, FunctionalComponent } from 'vue'
|
||||
import type { HeaderViewProps } from '../../Header'
|
||||
import { defaultRenderLogoAndTitle } from '../SiderMenu/SiderMenu'
|
||||
import { ref } from 'vue'
|
||||
import BaseMenu from '../SiderMenu/BaseMenu'
|
||||
import { getRender } from '#/layout/utils'
|
||||
import type { RightContentRender } from '#/layout/renderTypes'
|
||||
|
||||
export type TopNavHeaderProps = SiderMenuProps & GlobalHeaderProps
|
||||
|
||||
/**
|
||||
* 抽离出来是为了防止 rightSize 经常改变导致菜单 render
|
||||
*
|
||||
* @param param0
|
||||
* @param props
|
||||
*/
|
||||
export const RightContent: FunctionalComponent<TopNavHeaderProps> = props => {
|
||||
const rightSize = ref<string>('auto')
|
||||
|
||||
/** 减少一下渲染的次数 */
|
||||
const setRightSizeDebounceFn = useDebounceFn((width: number) => {
|
||||
rightSize.value = `${width}px`
|
||||
}, 160)
|
||||
|
||||
return (
|
||||
<div class={`${props.prefixCls}-right-content`} style={{ minWidth: rightSize.value }}>
|
||||
<div style={{ paddingRight: '8px' }}>
|
||||
<ResizeObserver
|
||||
onResize={({ width }: { width: number }) => {
|
||||
setRightSizeDebounceFn(width)
|
||||
}}
|
||||
>
|
||||
{props.rightContentRender && (
|
||||
<div class={`${props.prefixCls}-right-content-resize`}>
|
||||
{props.rightContentRender({ ...props } as HeaderViewProps)}
|
||||
</div>
|
||||
)}
|
||||
</ResizeObserver>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const TopNavHeader: FunctionalComponent<TopNavHeaderProps> = (props, { slots, attrs }) => {
|
||||
const prefixCls = `${props.prefixCls || 'ant-pro'}-top-nav-header`
|
||||
|
||||
const headerDom = defaultRenderLogoAndTitle(
|
||||
{ ...props, collapsed: false },
|
||||
slots,
|
||||
props.layout === 'mix' ? 'headerTitleRender' : undefined
|
||||
)
|
||||
|
||||
const className = computed(() => [prefixCls, attrs.class, { light: props.theme === 'light' }])
|
||||
|
||||
// @ts-ignore
|
||||
const defaultDom = <BaseMenu {...props} {...props.menuProps} />
|
||||
|
||||
const headerContentDom = props.headerContentRender
|
||||
? props.headerContentRender?.(props as HeaderViewProps, defaultDom)
|
||||
: defaultDom
|
||||
|
||||
const rightContentRender = getRender<RightContentRender>(props, slots, 'rightContentRender')
|
||||
|
||||
return (
|
||||
<div class={className.value} style={attrs.style as CSSProperties}>
|
||||
<div class={`${prefixCls}-main ${props.contentWidth === 'Fixed' ? 'wide' : ''}`}>
|
||||
<div class={`${prefixCls}-main-left`} onClick={props.onMenuHeaderClick}>
|
||||
<div class={`${prefixCls}-logo`} key="logo" id="logo">
|
||||
{headerDom}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1 }} class={`${prefixCls}-menu`}>
|
||||
{headerContentDom}
|
||||
</div>
|
||||
{rightContentRender && (
|
||||
<RightContent {...props} rightContentRender={rightContentRender} prefixCls={prefixCls} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopNavHeader
|
@ -0,0 +1,5 @@
|
||||
import settingDrawer from './en-US/settingDrawer'
|
||||
|
||||
export default {
|
||||
...settingDrawer
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import zhLocal from './zh-CN'
|
||||
import zhTWLocal from './zh-TW'
|
||||
import enUSLocal from './en-US'
|
||||
import itITLocal from './it-IT'
|
||||
import koKRLocal from './ko-KR'
|
||||
|
||||
const locales = {
|
||||
'zh-CN': zhLocal,
|
||||
'zh-TW': zhTWLocal,
|
||||
'en-US': enUSLocal,
|
||||
'it-IT': itITLocal,
|
||||
'ko-KR': koKRLocal
|
||||
}
|
||||
|
||||
type GLocaleWindow = {
|
||||
g_locale: keyof typeof locales
|
||||
}
|
||||
|
||||
export type LocaleType = keyof typeof locales
|
||||
|
||||
export const getLanguage = (): string => {
|
||||
// support ssr
|
||||
// if (!isBrowser()) return 'zh-CN'
|
||||
const lang = window.localStorage.getItem('umi_locale')
|
||||
return lang || (window as unknown as GLocaleWindow).g_locale || navigator.language
|
||||
}
|
||||
|
||||
export const gLocaleObject = (): Record<string, string> => {
|
||||
const gLocale = getLanguage()
|
||||
// @ts-ignore
|
||||
return locales[gLocale] || locales['zh-CN']
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import settingDrawer from './it-IT/settingDrawer'
|
||||
|
||||
export default {
|
||||
...settingDrawer
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
export default {
|
||||
'app.setting.pagestyle': 'Impostazioni di stile',
|
||||
'app.setting.pagestyle.dark': 'Tema scuro',
|
||||
'app.setting.pagestyle.light': 'Tema chiaro',
|
||||
'app.setting.content-width': 'Largezza contenuto',
|
||||
'app.setting.content-width.fixed': 'Fissa',
|
||||
'app.setting.content-width.fluid': 'Fluida',
|
||||
'app.setting.themecolor': 'Colore del tema',
|
||||
'app.setting.themecolor.dust': 'Rosso polvere',
|
||||
'app.setting.themecolor.volcano': 'Vulcano',
|
||||
'app.setting.themecolor.sunset': 'Arancione tramonto',
|
||||
'app.setting.themecolor.cyan': 'Ciano',
|
||||
'app.setting.themecolor.green': 'Verde polare',
|
||||
'app.setting.themecolor.daybreak': 'Blu cielo mattutino (default)',
|
||||
'app.setting.themecolor.geekblue': 'Blu geek',
|
||||
'app.setting.themecolor.purple': 'Viola dorato',
|
||||
'app.setting.navigationmode': 'Modalità di navigazione',
|
||||
'app.setting.sidemenu': 'Menu laterale',
|
||||
'app.setting.topmenu': 'Menu in testata',
|
||||
'app.setting.mixmenu': 'Menu misto',
|
||||
'app.setting.splitMenus': 'Menu divisi',
|
||||
'app.setting.fixedheader': 'Testata fissa',
|
||||
'app.setting.fixedsidebar': 'Menu laterale fisso',
|
||||
'app.setting.fixedsidebar.hint': 'Solo se selezionato Menu laterale',
|
||||
'app.setting.hideheader': 'Nascondi testata durante lo scorrimento',
|
||||
'app.setting.hideheader.hint': 'Solo se abilitato Nascondi testata durante lo scorrimento',
|
||||
'app.setting.othersettings': 'Altre impostazioni',
|
||||
'app.setting.weakmode': 'Inverti colori',
|
||||
'app.setting.copy': 'Copia impostazioni',
|
||||
'app.setting.loading': 'Carico tema...',
|
||||
'app.setting.copyinfo':
|
||||
'Impostazioni copiate con successo! Incolla il contenuto in config/defaultSettings.js',
|
||||
'app.setting.production.hint':
|
||||
'Questo pannello è visibile solo durante lo sviluppo. Le impostazioni devono poi essere modificate manulamente'
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import settingDrawer from './ko-KR/settingDrawer'
|
||||
|
||||
export default {
|
||||
...settingDrawer
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
export default {
|
||||
'app.setting.pagestyle': '스타일 설정',
|
||||
'app.setting.pagestyle.dark': '다크 모드',
|
||||
'app.setting.pagestyle.light': '라이트 모드',
|
||||
'app.setting.content-width': '컨텐츠 너비',
|
||||
'app.setting.content-width.fixed': '고정',
|
||||
'app.setting.content-width.fluid': '흐름',
|
||||
'app.setting.themecolor': '테마 색상',
|
||||
'app.setting.themecolor.dust': 'Dust Red',
|
||||
'app.setting.themecolor.volcano': 'Volcano',
|
||||
'app.setting.themecolor.sunset': 'Sunset Orange',
|
||||
'app.setting.themecolor.cyan': 'Cyan',
|
||||
'app.setting.themecolor.green': 'Polar Green',
|
||||
'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
|
||||
'app.setting.themecolor.geekblue': 'Geek Blue',
|
||||
'app.setting.themecolor.purple': 'Golden Purple',
|
||||
'app.setting.navigationmode': '네비게이션 모드',
|
||||
'app.setting.regionalsettings': '영역별 설정',
|
||||
'app.setting.regionalsettings.header': '헤더',
|
||||
'app.setting.regionalsettings.menu': '메뉴',
|
||||
'app.setting.regionalsettings.footer': '바닥글',
|
||||
'app.setting.regionalsettings.menuHeader': '메뉴 헤더',
|
||||
'app.setting.sidemenu': '메뉴 사이드 배치',
|
||||
'app.setting.topmenu': '메뉴 상단 배치',
|
||||
'app.setting.mixmenu': '혼합형 배치',
|
||||
'app.setting.splitMenus': '메뉴 분리',
|
||||
'app.setting.fixedheader': '헤더 고정',
|
||||
'app.setting.fixedsidebar': '사이드바 고정',
|
||||
'app.setting.fixedsidebar.hint': "'메뉴 사이드 배치'를 선택했을 때 동작함",
|
||||
'app.setting.hideheader': '스크롤 중 헤더 감추기',
|
||||
'app.setting.hideheader.hint': "'헤더 감추기 옵션'을 선택했을 때 동작함",
|
||||
'app.setting.othersettings': '다른 설정',
|
||||
'app.setting.weakmode': '고대비 모드',
|
||||
'app.setting.copy': '설정값 복사',
|
||||
'app.setting.loading': '테마 로딩 중',
|
||||
'app.setting.copyinfo':
|
||||
'복사 성공. src/models/settings.js에 있는 defaultSettings를 교체해 주세요.',
|
||||
'app.setting.production.hint':
|
||||
'설정 판넬은 개발 환경에서만 보여집니다. 직접 수동으로 변경바랍니다.'
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
const locales = {
|
||||
'zh-CN': null,
|
||||
'en-US': null
|
||||
}
|
||||
|
||||
export type LocaleType = keyof typeof locales
|
@ -0,0 +1,5 @@
|
||||
import settingDrawer from './zh-CN/settingDrawer'
|
||||
|
||||
export default {
|
||||
...settingDrawer
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
export default {
|
||||
'app.setting.pagestyle': '整体风格设置',
|
||||
'app.setting.pagestyle.dark': '暗色菜单风格',
|
||||
'app.setting.pagestyle.light': '亮色菜单风格',
|
||||
'app.setting.pagestyle.realdark': '暗色风格(实验功能)',
|
||||
'app.setting.content-width': '内容区域宽度',
|
||||
'app.setting.content-width.fixed': '定宽',
|
||||
'app.setting.content-width.fluid': '流式',
|
||||
'app.setting.themecolor': '主题色',
|
||||
'app.setting.themecolor.dust': '薄暮',
|
||||
'app.setting.themecolor.volcano': '火山',
|
||||
'app.setting.themecolor.sunset': '日暮',
|
||||
'app.setting.themecolor.cyan': '明青',
|
||||
'app.setting.themecolor.green': '极光绿',
|
||||
'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
|
||||
'app.setting.themecolor.geekblue': '极客蓝',
|
||||
'app.setting.themecolor.purple': '酱紫',
|
||||
'app.setting.navigationmode': '导航模式',
|
||||
'app.setting.regionalsettings': '内容区域',
|
||||
'app.setting.regionalsettings.header': '顶栏',
|
||||
'app.setting.regionalsettings.menu': '菜单',
|
||||
'app.setting.regionalsettings.footer': '页脚',
|
||||
'app.setting.regionalsettings.menuHeader': '菜单头',
|
||||
'app.setting.sidemenu': '侧边菜单布局',
|
||||
'app.setting.topmenu': '顶部菜单布局',
|
||||
'app.setting.mixmenu': '混合菜单布局',
|
||||
'app.setting.splitMenus': '自动分割菜单',
|
||||
'app.setting.fixedheader': '固定 Header',
|
||||
'app.setting.fixedsidebar': '固定侧边菜单',
|
||||
'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
|
||||
'app.setting.hideheader': '下滑时隐藏 Header',
|
||||
'app.setting.hideheader.hint': '固定 Header 时可配置',
|
||||
'app.setting.othersettings': '其他设置',
|
||||
'app.setting.weakmode': '色弱模式',
|
||||
'app.setting.copy': '拷贝设置',
|
||||
'app.setting.loading': '正在加载主题',
|
||||
'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
|
||||
'app.setting.production.hint':
|
||||
'配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件'
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import settingDrawer from './zh-TW/settingDrawer'
|
||||
|
||||
export default {
|
||||
...settingDrawer
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
export default {
|
||||
'app.setting.pagestyle': '整體風格設置',
|
||||
'app.setting.pagestyle.dark': '暗色菜單風格',
|
||||
'app.setting.pagestyle.realdark': '暗色風格(实验功能)',
|
||||
'app.setting.pagestyle.light': '亮色菜單風格',
|
||||
'app.setting.content-width': '內容區域寬度',
|
||||
'app.setting.content-width.fixed': '定寬',
|
||||
'app.setting.content-width.fluid': '流式',
|
||||
'app.setting.themecolor': '主題色',
|
||||
'app.setting.themecolor.dust': '薄暮',
|
||||
'app.setting.themecolor.volcano': '火山',
|
||||
'app.setting.themecolor.sunset': '日暮',
|
||||
'app.setting.themecolor.cyan': '明青',
|
||||
'app.setting.themecolor.green': '極光綠',
|
||||
'app.setting.themecolor.daybreak': '拂曉藍(默認)',
|
||||
'app.setting.themecolor.geekblue': '極客藍',
|
||||
'app.setting.themecolor.purple': '醬紫',
|
||||
'app.setting.navigationmode': '導航模式',
|
||||
'app.setting.sidemenu': '側邊菜單布局',
|
||||
'app.setting.topmenu': '頂部菜單布局',
|
||||
'app.setting.mixmenu': '混合菜單布局',
|
||||
'app.setting.splitMenus': '自动分割菜单',
|
||||
'app.setting.fixedheader': '固定 Header',
|
||||
'app.setting.fixedsidebar': '固定側邊菜單',
|
||||
'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置',
|
||||
'app.setting.hideheader': '下滑時隱藏 Header',
|
||||
'app.setting.hideheader.hint': '固定 Header 時可配置',
|
||||
'app.setting.othersettings': '其他設置',
|
||||
'app.setting.weakmode': '色弱模式',
|
||||
'app.setting.copy': '拷貝設置',
|
||||
'app.setting.loading': '正在加載主題',
|
||||
'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置',
|
||||
'app.setting.production.hint':
|
||||
'配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件'
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/** 判断是否是图片链接 */
|
||||
export function isImg(path: string): boolean {
|
||||
return /\w.(png|jpg|jpeg|svg|webp|gif|bmp)$/i.test(path)
|
||||
}
|
||||
|
||||
export const isNil = (value: any) => value === null || value === undefined
|
||||
|
||||
export const isUrl = (path: string | undefined): boolean => {
|
||||
if (!path) return false
|
||||
if (!path.startsWith('http')) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const url = new URL(path)
|
||||
return !!url
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验是否不是数组且不为空 **/
|
||||
export const notNullArray = (value: any) => Array.isArray(value) && value.length > 0
|
@ -0,0 +1,30 @@
|
||||
import type { MenuDataItem } from '../types'
|
||||
|
||||
const childrenPropsName = 'routes'
|
||||
function stripQueryStringAndHashFromPath(url: string) {
|
||||
return url.split('?')[0].split('#')[0]
|
||||
}
|
||||
/**
|
||||
* 获取打平的 menuData
|
||||
* 以 path 为 key
|
||||
* @param menuData
|
||||
*/
|
||||
export const getFlatMenus = (menuData: MenuDataItem[] = []) => {
|
||||
let menus: MenuDataItem = {}
|
||||
menuData.forEach(item => {
|
||||
if (!item || !item.key) {
|
||||
return
|
||||
}
|
||||
const routerChildren = item.children || item[childrenPropsName]
|
||||
menus[stripQueryStringAndHashFromPath(item.path || item.key || '/')] = {
|
||||
...item
|
||||
}
|
||||
menus[item.key || item.path || '/'] = { ...item }
|
||||
if (routerChildren) {
|
||||
menus = { ...menus, ...getFlatMenus(routerChildren) }
|
||||
}
|
||||
})
|
||||
return menus
|
||||
}
|
||||
|
||||
export default getFlatMenus
|
@ -0,0 +1,26 @@
|
||||
import type { Slot, Slots } from 'vue'
|
||||
import type { VueNode, VueNodeOrRender } from '#/types'
|
||||
|
||||
export function getVueNode(customRender: VueNodeOrRender, slot?: Slot, props?: any[]): VueNode {
|
||||
if (customRender === false) {
|
||||
return null
|
||||
}
|
||||
if (customRender === true || customRender == null) {
|
||||
return slot ? slot(props) : null
|
||||
}
|
||||
if (typeof customRender === 'function') {
|
||||
return customRender(props)
|
||||
}
|
||||
return customRender
|
||||
}
|
||||
|
||||
export function getRender<T>(
|
||||
props: Record<string, unknown>,
|
||||
slots: Slots,
|
||||
key = 'default'
|
||||
): T | false {
|
||||
if (props[key] === false) {
|
||||
return false
|
||||
}
|
||||
return (props[key] || slots[key]) as T
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import type { MenuDataItem } from '../types'
|
||||
import { isUrl, notNullArray } from './checkUtils'
|
||||
|
||||
/**
|
||||
* 如果不是 / 开头的和父节点做一下合并
|
||||
* 如果是 / 开头的不作任何处理
|
||||
* 如果是 url 也直接返回
|
||||
* @param path
|
||||
* @param parentPath
|
||||
*/
|
||||
const mergePath = (path = '', parentPath = '/') => {
|
||||
if ((path || parentPath).startsWith('/')) {
|
||||
return path
|
||||
}
|
||||
if (isUrl(path)) {
|
||||
return path
|
||||
}
|
||||
return `/${parentPath}/${path}`.replace(/\/\//g, '/').replace(/\/\//g, '/')
|
||||
}
|
||||
|
||||
function toMenuItem(routeList?: RouteRecordRaw[], parentPath?: string) {
|
||||
if (routeList == null || !Array.isArray(routeList) || routeList.length === 0) {
|
||||
return
|
||||
}
|
||||
return routeList.map(route => {
|
||||
const path = mergePath(route.path, parentPath)
|
||||
const menuDataItem: MenuDataItem = {
|
||||
...route.meta,
|
||||
key: path,
|
||||
path: path
|
||||
}
|
||||
|
||||
if (notNullArray(route.children)) {
|
||||
menuDataItem.children = toMenuItem(route.children, path)
|
||||
}
|
||||
return menuDataItem
|
||||
})
|
||||
}
|
||||
|
||||
export function transformRouteToMenuItem(routes: RouteRecordRaw[], parentPath = '/') {
|
||||
const parentRoute = routes.find(route => route.path === parentPath)
|
||||
return toMenuItem(parentRoute?.children)
|
||||
}
|
||||
|
||||
export const getOpenKeysFromMenuData = (menuData?: MenuDataItem[]) => {
|
||||
return (menuData || []).reduce((pre, item) => {
|
||||
if (item.key) {
|
||||
pre.push(item.key)
|
||||
}
|
||||
if (item.children) {
|
||||
const newArray: string[] = pre.concat(getOpenKeysFromMenuData(item.children) || [])
|
||||
return newArray
|
||||
}
|
||||
return pre
|
||||
}, [] as string[])
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import type { MenuDataItem } from '../types'
|
||||
|
||||
const themeConfig = {
|
||||
daybreak: '#1890ff',
|
||||
dust: '#F5222D',
|
||||
volcano: '#FA541C',
|
||||
sunset: '#FAAD14',
|
||||
cyan: '#13C2C2',
|
||||
green: '#52C41A',
|
||||
geekblue: '#2F54EB',
|
||||
purple: '#722ED1'
|
||||
}
|
||||
|
||||
/**
|
||||
* Daybreak-> #1890ff
|
||||
*
|
||||
* @param val
|
||||
*/
|
||||
export function genStringToTheme(val?: string): string {
|
||||
// @ts-ignore
|
||||
return val && themeConfig[val] ? themeConfig[val] : val
|
||||
}
|
||||
|
||||
export function clearMenuItem(menusData: MenuDataItem[]): MenuDataItem[] {
|
||||
return menusData
|
||||
.map(item => {
|
||||
const children: MenuDataItem[] = item.children || []
|
||||
const finalItem = { ...item }
|
||||
|
||||
if (!finalItem.name || finalItem.hideInMenu) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (finalItem && finalItem?.children) {
|
||||
if (
|
||||
!finalItem.hideChildrenInMenu &&
|
||||
children.some(child => child && child.name && !child.hideInMenu)
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
children: clearMenuItem(children)
|
||||
}
|
||||
}
|
||||
// children 为空就直接删掉
|
||||
delete finalItem.children
|
||||
}
|
||||
return finalItem
|
||||
})
|
||||
.filter(item => item) as MenuDataItem[]
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
export default {
|
||||
moneySymbol: '$',
|
||||
form: {
|
||||
lightFilter: {
|
||||
more: 'المزيد',
|
||||
clear: 'نظف',
|
||||
confirm: 'تأكيد',
|
||||
itemUnit: 'عناصر'
|
||||
}
|
||||
},
|
||||
tableForm: {
|
||||
search: 'ابحث',
|
||||
reset: 'إعادة تعيين',
|
||||
submit: 'ارسال',
|
||||
collapsed: 'مُقلص',
|
||||
expand: 'مُوسع',
|
||||
inputPlaceholder: 'الرجاء الإدخال',
|
||||
selectPlaceholder: 'الرجاء الإختيار'
|
||||
},
|
||||
alert: {
|
||||
clear: 'نظف',
|
||||
selected: 'محدد',
|
||||
item: 'عنصر'
|
||||
},
|
||||
pagination: {
|
||||
total: {
|
||||
range: ' ',
|
||||
total: 'من',
|
||||
item: 'عناصر'
|
||||
}
|
||||
},
|
||||
tableToolBar: {
|
||||
leftPin: 'ثبت على اليسار',
|
||||
rightPin: 'ثبت على اليمين',
|
||||
noPin: 'الغاء التثبيت',
|
||||
leftFixedTitle: 'لصق على اليسار',
|
||||
rightFixedTitle: 'لصق على اليمين',
|
||||
noFixedTitle: 'إلغاء الإلصاق',
|
||||
reset: 'إعادة تعيين',
|
||||
columnDisplay: 'الأعمدة المعروضة',
|
||||
columnSetting: 'الإعدادات',
|
||||
fullScreen: 'وضع كامل الشاشة',
|
||||
exitFullScreen: 'الخروج من وضع كامل الشاشة',
|
||||
reload: 'تحديث',
|
||||
density: 'الكثافة',
|
||||
densityDefault: 'افتراضي',
|
||||
densityLarger: 'أكبر',
|
||||
densityMiddle: 'وسط',
|
||||
densitySmall: 'مدمج'
|
||||
},
|
||||
stepsForm: {
|
||||
next: 'التالي',
|
||||
prev: 'السابق'
|
||||
},
|
||||
loginForm: {
|
||||
submitText: 'تسجيل الدخول'
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
export default {
|
||||
moneySymbol: '€',
|
||||
tableForm: {
|
||||
search: 'Cercar',
|
||||
reset: 'Netejar',
|
||||
submit: 'Enviar',
|
||||
collapsed: 'Expandir',
|
||||
expand: 'Col·lapsar',
|
||||
inputPlaceholder: 'Introduïu valor',
|
||||
selectPlaceholder: 'Seleccioneu valor'
|
||||
},
|
||||
alert: {
|
||||
clear: 'Netejar',
|
||||
selected: 'Seleccionat',
|
||||
item: 'Article'
|
||||
},
|
||||
pagination: {
|
||||
total: {
|
||||
range: ' ',
|
||||
total: 'de',
|
||||
item: 'articles'
|
||||
}
|
||||
},
|
||||
tableToolBar: {
|
||||
leftPin: "Pin a l'esquerra",
|
||||
rightPin: 'Pin a la dreta',
|
||||
noPin: 'Sense Pin',
|
||||
leftFixedTitle: "Fixat a l'esquerra",
|
||||
rightFixedTitle: 'Fixat a la dreta',
|
||||
noFixedTitle: 'Sense fixar',
|
||||
reset: 'Reiniciar',
|
||||
columnDisplay: 'Mostrar Columna',
|
||||
columnSetting: 'Configuració',
|
||||
fullScreen: 'Pantalla Completa',
|
||||
exitFullScreen: 'Sortir Pantalla Completa',
|
||||
reload: 'Refrescar',
|
||||
density: 'Densitat',
|
||||
densityDefault: 'Per Defecte',
|
||||
densityLarger: 'Llarg',
|
||||
densityMiddle: 'Mitjà',
|
||||
densitySmall: 'Compacte'
|
||||
},
|
||||
stepsForm: {
|
||||
next: 'Següent',
|
||||
prev: 'Anterior',
|
||||
submit: 'Finalizar'
|
||||
},
|
||||
loginForm: {
|
||||
submitText: 'Entrar'
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
export default {
|
||||
moneySymbol: '€',
|
||||
form: {
|
||||
lightFilter: {
|
||||
more: 'Mehr',
|
||||
clear: 'Zurücksetzen',
|
||||
confirm: 'Bestätigen',
|
||||
itemUnit: 'Einträge'
|
||||
}
|
||||
},
|
||||
tableForm: {
|
||||
search: 'Suchen',
|
||||
reset: 'Zurücksetzen',
|
||||
submit: 'Absenden',
|
||||
collapsed: 'Zeige mehr',
|
||||
expand: 'Zeige weniger',
|
||||
inputPlaceholder: 'Bitte eingeben',
|
||||
selectPlaceholder: 'Bitte auswählen'
|
||||
},
|
||||
alert: {
|
||||
clear: 'Zurücksetzen',
|
||||
selected: 'Ausgewählt',
|
||||
item: 'Eintrag'
|
||||
},
|
||||
pagination: {
|
||||
total: {
|
||||
range: ' ',
|
||||
total: 'von',
|
||||
item: 'Einträgen'
|
||||
}
|
||||
},
|
||||
tableToolBar: {
|
||||
leftPin: 'Links anheften',
|
||||
rightPin: 'Rechts anheften',
|
||||
noPin: 'Nicht angeheftet',
|
||||
leftFixedTitle: 'Links fixiert',
|
||||
rightFixedTitle: 'Rechts fixiert',
|
||||
noFixedTitle: 'Nicht fixiert',
|
||||
reset: 'Zurücksetzen',
|
||||
columnDisplay: 'Angezeigte Reihen',
|
||||
columnSetting: 'Einstellungen',
|
||||
fullScreen: 'Vollbild',
|
||||
exitFullScreen: 'Vollbild verlassen',
|
||||
reload: 'Aktualisieren',
|
||||
density: 'Abstand',
|
||||
densityDefault: 'Standard',
|
||||
densityLarger: 'Größer',
|
||||
densityMiddle: 'Mittel',
|
||||
densitySmall: 'Kompakt'
|
||||
},
|
||||
stepsForm: {
|
||||
next: 'Weiter',
|
||||
prev: 'Zurück',
|
||||
submit: 'Abschließen'
|
||||
},
|
||||
loginForm: {
|
||||
submitText: 'Anmelden'
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
export default {
|
||||
moneySymbol: '£',
|
||||
form: {
|
||||
lightFilter: {
|
||||
more: 'More',
|
||||
clear: 'Clear',
|
||||
confirm: 'Confirm',
|
||||
itemUnit: 'Items'
|
||||
}
|
||||
},
|
||||
tableForm: {
|
||||
search: 'Query',
|
||||
reset: 'Reset',
|
||||
submit: 'Submit',
|
||||
collapsed: 'Expand',
|
||||
expand: 'Collapse',
|
||||
inputPlaceholder: 'Please enter',
|
||||
selectPlaceholder: 'Please select'
|
||||
},
|
||||
alert: {
|
||||
clear: 'Clear',
|
||||
selected: 'Selected',
|
||||
item: 'Item'
|
||||
},
|
||||
pagination: {
|
||||
total: {
|
||||
range: ' ',
|
||||
total: 'of',
|
||||
item: 'items'
|
||||
}
|
||||
},
|
||||
tableToolBar: {
|
||||
leftPin: 'Pin to left',
|
||||
rightPin: 'Pin to right',
|
||||
noPin: 'Unpinned',
|
||||
leftFixedTitle: 'Fixed the left',
|
||||
rightFixedTitle: 'Fixed the right',
|
||||
noFixedTitle: 'Not Fixed',
|
||||
reset: 'Reset',
|
||||
columnDisplay: 'Column Display',
|
||||
columnSetting: 'Settings',
|
||||
fullScreen: 'Full Screen',
|
||||
exitFullScreen: 'Exit Full Screen',
|
||||
reload: 'Refresh',
|
||||
density: 'Density',
|
||||
densityDefault: 'Default',
|
||||
densityLarger: 'Larger',
|
||||
densityMiddle: 'Middle',
|
||||
densitySmall: 'Compact'
|
||||
},
|
||||
stepsForm: {
|
||||
next: 'Next',
|
||||
prev: 'Previous',
|
||||
submit: 'Finish'
|
||||
},
|
||||
loginForm: {
|
||||
submitText: 'Login'
|
||||
},
|
||||
editableTable: {
|
||||
action: {
|
||||
save: 'Save',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete'
|
||||
}
|
||||
},
|
||||
switch: {
|
||||
open: 'open',
|
||||
close: 'close'
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
export default {
|
||||
moneySymbol: '$',
|
||||
form: {
|
||||
lightFilter: {
|
||||
more: 'More',
|
||||
clear: 'Clear',
|
||||
confirm: 'Confirm',
|
||||
itemUnit: 'Items'
|
||||
}
|
||||
},
|
||||
tableForm: {
|
||||
search: 'Query',
|
||||
reset: 'Reset',
|
||||
submit: 'Submit',
|
||||
collapsed: 'Expand',
|
||||
expand: 'Collapse',
|
||||
inputPlaceholder: 'Please enter',
|
||||
selectPlaceholder: 'Please select'
|
||||
},
|
||||
alert: {
|
||||
clear: 'Clear',
|
||||
selected: 'Selected',
|
||||
item: 'Item'
|
||||
},
|
||||
pagination: {
|
||||
total: {
|
||||
range: ' ',
|
||||
total: 'of',
|
||||
item: 'items'
|
||||
}
|
||||
},
|
||||
tableToolBar: {
|
||||
leftPin: 'Pin to left',
|
||||
rightPin: 'Pin to right',
|
||||
noPin: 'Unpinned',
|
||||
leftFixedTitle: 'Fixed the left',
|
||||
rightFixedTitle: 'Fixed the right',
|
||||
noFixedTitle: 'Not Fixed',
|
||||
reset: 'Reset',
|
||||
columnDisplay: 'Column Display',
|
||||
columnSetting: 'Settings',
|
||||
fullScreen: 'Full Screen',
|
||||
exitFullScreen: 'Exit Full Screen',
|
||||
reload: 'Refresh',
|
||||
density: 'Density',
|
||||
densityDefault: 'Default',
|
||||
densityLarger: 'Larger',
|
||||
densityMiddle: 'Middle',
|
||||
densitySmall: 'Compact'
|
||||
},
|
||||
stepsForm: {
|
||||
next: 'Next',
|
||||
prev: 'Previous',
|
||||
submit: 'Finish'
|
||||
},
|
||||
loginForm: {
|
||||
submitText: 'Login'
|
||||
},
|
||||
editableTable: {
|
||||
action: {
|
||||
save: 'Save',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete'
|
||||
}
|
||||
},
|
||||
switch: {
|
||||
open: 'open',
|
||||
close: 'close'
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
export default {
|
||||
moneySymbol: '€',
|
||||
form: {
|
||||
lightFilter: {
|
||||
more: 'Más',
|
||||
clear: 'Limpiar',
|
||||
confirm: 'Confirmar',
|
||||
itemsUnits: 'Objetos'
|
||||
}
|
||||
},
|
||||
tableForm: {
|
||||
search: 'Buscar',
|
||||
reset: 'Limpiar',
|
||||
submit: 'Submit',
|
||||
collapsed: 'Expandir',
|
||||
expand: 'Colapsar',
|
||||
inputPlaceholder: 'Ingrese valor',
|
||||
selectPlaceholder: 'Seleccione valor'
|
||||
},
|
||||
alert: {
|
||||
clear: 'Limpiar',
|
||||
selected: 'Seleccionado',
|
||||
item: 'Articulo'
|
||||
},
|
||||
pagination: {
|
||||
total: {
|
||||
range: ' ',
|
||||
total: 'de',
|
||||
item: 'artículos'
|
||||
}
|
||||
},
|
||||
tableToolBar: {
|
||||
leftPin: 'Pin a la izquierda',
|
||||
rightPin: 'Pin a la derecha',
|
||||
noPin: 'Sin Pin',
|
||||
leftFixedTitle: 'Fijado a la izquierda',
|
||||
rightFixedTitle: 'Fijado a la derecha',
|
||||
noFixedTitle: 'Sin Fijar',
|
||||
reset: 'Reiniciar',
|
||||
columnDisplay: 'Mostrar Columna',
|
||||
columnSetting: 'Configuración',
|
||||
fullScreen: 'Pantalla Completa',
|
||||
exitFullScreen: 'Salir Pantalla Completa',
|
||||
reload: 'Refrescar',
|
||||
density: 'Densidad',
|
||||
densityDefault: 'Por Defecto',
|
||||
densityLarger: 'Largo',
|
||||
densityMiddle: 'Medio',
|
||||
densitySmall: 'Compacto'
|
||||
},
|
||||
stepsForm: {
|
||||
next: 'Siguiente',
|
||||
prev: 'Anterior',
|
||||
submit: 'Finalizar'
|
||||
},
|
||||
loginForm: {
|
||||
submitText: 'Entrar'
|
||||
},
|
||||
editableTable: {
|
||||
action: {
|
||||
save: 'Guardar',
|
||||
cancel: 'Descartar',
|
||||
delete: 'Borrar'
|
||||
}
|
||||
},
|
||||
switch: {
|
||||
open: 'abrir',
|
||||
close: 'cerrar'
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
export default {
|
||||
moneySymbol: 'تومان',
|
||||
form: {
|
||||
lightFilter: {
|
||||
more: 'بیشتر',
|
||||
clear: 'پاک کردن',
|
||||
confirm: 'تایید',
|
||||
itemUnit: 'مورد'
|
||||
}
|
||||
},
|
||||
tableForm: {
|
||||
search: 'جستجو',
|
||||
reset: 'بازنشانی',
|
||||
submit: 'تایید',
|
||||
collapsed: 'نمایش بیشتر',
|
||||
expand: 'نمایش کمتر',
|
||||
inputPlaceholder: 'پیدا کنید',
|
||||
selectPlaceholder: 'انتخاب کنید'
|
||||
},
|
||||
alert: {
|
||||
clear: 'پاک سازی',
|
||||
selected: 'انتخاب',
|
||||
item: 'مورد'
|
||||
},
|
||||
pagination: {
|
||||
total: {
|
||||
range: ' ',
|
||||
total: 'از',
|
||||
item: 'مورد'
|
||||
}
|
||||
},
|
||||
tableToolBar: {
|
||||
leftPin: 'سنجاق به چپ',
|
||||
rightPin: 'سنجاق به راست',
|
||||
noPin: 'سنجاق نشده',
|
||||
leftFixedTitle: 'ثابت شده در چپ',
|
||||
rightFixedTitle: 'ثابت شده در راست',
|
||||
noFixedTitle: 'شناور',
|
||||
reset: 'بازنشانی',
|
||||
columnDisplay: 'نمایش همه',
|
||||
columnSetting: 'تنظیمات',
|
||||
fullScreen: 'تمام صفحه',
|
||||
exitFullScreen: 'خروج از حالت تمام صفحه',
|
||||
reload: 'تازه سازی',
|
||||
density: 'تراکم',
|
||||
densityDefault: 'پیش فرض',
|
||||
densityLarger: 'بزرگ',
|
||||
densityMiddle: 'متوسط',
|
||||
densitySmall: 'کوچک'
|
||||
},
|
||||
stepsForm: {
|
||||
next: 'بعدی',
|
||||
prev: 'قبلی',
|
||||
submit: 'اتمام'
|
||||
},
|
||||
loginForm: {
|
||||
submitText: 'ورود'
|
||||
},
|
||||
editableTable: {
|
||||
action: {
|
||||
save: 'ذخیره',
|
||||
cancel: 'لغو',
|
||||
delete: 'حذف'
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
export default {
|
||||
moneySymbol: '€',
|
||||
form: {
|
||||
lightFilter: {
|
||||
more: 'Plus',
|
||||
clear: 'Effacer',
|
||||
confirm: 'Confirmer',
|
||||
itemUnit: 'Items'
|
||||
}
|
||||
},
|
||||
tableForm: {
|
||||
search: 'Rechercher',
|
||||
reset: 'Réinitialiser',
|
||||
submit: 'Envoyer',
|
||||
collapsed: 'Agrandir',
|
||||
expand: 'Réduire',
|
||||
inputPlaceholder: 'Entrer une valeur',
|
||||
selectPlaceholder: 'Sélectionner une valeur'
|
||||
},
|
||||
alert: {
|
||||
clear: 'Réinitialiser',
|
||||
selected: 'Sélectionné',
|
||||
item: 'Item'
|
||||
},
|
||||
pagination: {
|
||||
total: {
|
||||
range: ' ',
|
||||
total: 'sur',
|
||||
item: 'éléments'
|
||||
}
|
||||
},
|
||||
tableToolBar: {
|
||||
leftPin: 'Épingler à gauche',
|
||||
rightPin: 'Épingler à gauche',
|
||||
noPin: 'Sans épingle',
|
||||
leftFixedTitle: 'Fixer à gauche',
|
||||
rightFixedTitle: 'Fixer à droite',
|
||||
noFixedTitle: 'Non fixé',
|
||||
reset: 'Réinitialiser',
|
||||
columnDisplay: 'Affichage colonne',
|
||||
columnSetting: 'Réglages',
|
||||
fullScreen: 'Plein écran',
|
||||
exitFullScreen: 'Quitter Plein écran',
|
||||
reload: 'Rafraichir',
|
||||
density: 'Densité',
|
||||
densityDefault: 'Par défaut',
|
||||
densityLarger: 'Larger',
|
||||
densityMiddle: 'Moyenne',
|
||||
densitySmall: 'Compacte'
|
||||
},
|
||||
stepsForm: {
|
||||
next: 'Suivante',
|
||||
prev: 'Précédente',
|
||||
submit: 'Finaliser'
|
||||
},
|
||||
loginForm: {
|
||||
submitText: 'Se connecter'
|
||||
},
|
||||
editableTable: {
|
||||
action: {
|
||||
save: 'Sauvegarder',
|
||||
cancel: 'Annuler',
|
||||
delete: 'Supprimer'
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue