ui更新完成

master
doublekou 1 year ago
commit c0ffd8ab4d

@ -0,0 +1,28 @@
# https://editorconfig.org
root = true
[*] # 标识针对所有文件
charset = utf-8
indent_style = space # 缩进风格tab | space
indent_size = 2 # 缩进大小
end_of_line = lf # 换行符
insert_final_newline = true # 始终在文件末尾插入一个新行
trim_trailing_whitespace = true # 删除文件中换行符之前的所有空白字符
[*.md] # 标识针对 md 文件
insert_final_newline = false
trim_trailing_whitespace = false
[*.{js,ts,jsx,tsx,html}]
semicolon=false
ij_javascript_use_double_quotes = false
ij_typescript_use_double_quotes = false
ij_any_space_inside_empty_tag = true
ij_html_space_inside_empty_tag = true
ij_any_line_comment_add_space = true
ij_javascript_spaces_within_object_literal_braces = true
ij_typescript_spaces_within_object_literal_braces = true
ij_javascript_spaces_within_type_literal_braces = true
ij_typescript_spaces_within_type_literal_braces = true
ij_typescript_spaces_within_imports = true

@ -0,0 +1,2 @@
VITE_API_URL=/api
VITE_API_TIME_OUT=10000

@ -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/"

30
.gitignore vendored

@ -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,7 @@
semi: false # 语句末尾是否加分号
singleQuote: true # 使用单引号代替双引号
printWidth: 100 # 超过多长进行换行
trailingComma: 'none' # 多行输入的尾逗号是否添加
arrowParens: 'avoid' # (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid省略括号
endOfLine: 'lf' # 格式化换行符,默认值 lf
# vueIndentScriptAndStyle: true # vue script 标签的缩进开启

@ -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,60 @@
# ballcat-ui-vue3
项目开发中....
二开时请不要修改 pro-componets 内部文件,其对标 react 版本的 pro-componets
如有任何需要修改的问题,或者和 react 版本不一致的现象,请提 issues, 会尽快解决。
目前功能还在移植中,由于工作量太大,初版本只考虑移植 pro-layout 以及 pro-table(精简掉 searchform 模块)
后续 pro-components 将独立出一个仓库进行维护,同时会发布到 npm 仓库,方便引用。
## 包管理工具
项目强制要求使用 pnpm 进行依赖管理,使用 npm 或者 yarn 下载依赖将会报错。
```shell
npm install -g pnpm
pnpm install
```
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/vuejs/language-tools/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
pnpm install
```
### Compile and Hot-Reload for Development
```sh
pnpm run dev
```
### Type-Check, Compile and Minify for Production
```sh
pnpm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
pnpm run lint
```

@ -0,0 +1,3 @@
module.exports = {
extends: ['./node_modules/@ballcat/commitlint-config-gitmoji']
}

@ -0,0 +1,38 @@
## 文本使用规范:
### 新建New
"New" 可以用来表示用户正在创建新的数据或对象,例如新建账户、新建文档、新建任务等。
"New" 通常用于界面的按钮或菜单项上,用于启动新建操作。
### 创建Create
"Create" 可以用于描述用户正在创建新的数据或对象,例如创建账户、创建文档、创建任务等。
"Create" 部分情况下可以与 "NEW" 互换使用,但是 "Create" 更倾向于表述一个行为,可以作为新建表单的提交按钮。
### 添加: Add
“Add” 意味着在现有的集合中添加一个新的对象或元素,例如添加一个新的条目到列表中,或向现有的组织中添加新的成员。
### 编辑Edit
通常用来表示用户对某个数据或信息进行修改,例如编辑文本、编辑图片等。
### 更新Update
通常用来表示数据或信息的更新,例如更新软件、更新文章、更新个人资料等。
类似于 "Create", "Update" 也更倾向于表述一个行为。
### 改变: Change
通常是更改一个已经存在的状态或值,例如更改颜色、更改语言、更改设置等。
### 删除Delete
"Delete" 通常用于表示用户将某些数据或对象彻底删除,例如删除文件、删除账户、删除联系人等。
"Delete" 可能会触发某些警告或确认对话框,以确保用户意识到自己正在执行的操作可能不可逆转。
### 移除Remove
"Remove" 通常用于表示用户从某个列表或集合中删除某些数据或对象,例如从购物车中移除商品、从播放列表中移除歌曲等。
"Remove" 不一定会将数据或对象从系统中彻底删除,而是可能将其移动到其他位置或状态。
### 提交Submit
"Submit" 通常用于表示用户已经完成某项操作并将其提交到系统或其他人进行处理,例如提交表单、提交评论、提交订单等。
通常情况下,提交操作可能需要验证用户输入的数据或进行其他额外的处理,例如检查表单是否完整、验证邮箱地址是否有效等。
### 保存Save
"Save" 通常用于表示用户需要保存当前的操作或状态,例如保存文档、保存设置、保存账户信息等。
通常情况下,保存操作不需要进行额外的验证或处理,只是将当前的数据或状态保存到系统中, 适用于在编辑表单中做为操作按钮。

@ -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>

15305
package-lock.json generated

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,263 @@
import './index.less'
import { Grid, Tabs } from 'ant-design-vue'
import 'ant-design-vue/es/grid/style/index.less'
import 'ant-design-vue/es/tabs/style/index.less'
import { RightOutlined } from '@ant-design/icons-vue'
import type { Breakpoint, Gutter } from '../../types'
import Loading from '../Loading'
import Actions from '../Actions'
import { getPrefixCls } from '#/layout/RouteContext'
import { defineComponent, watchEffect } from 'vue'
import type { CSSProperties } from 'vue'
const { useBreakpoint } = Grid
import { useToggle } from '@vueuse/core'
import { cardProps } from '../../types'
import LabelIconTip from '../../../utils/components/LabelIconTip'
const Card = defineComponent({
// eslint-disable-next-line vue/multi-word-component-names
name: 'Card',
slots: ['loading'],
props: cardProps(),
setup(props, { attrs, slots }) {
const screens = useBreakpoint()
const [collapsed, setCollapsed] = useToggle(false)
watchEffect(() => {
props.collapsed && (collapsed.value = props.collapsed)
})
// 顺序决定如何进行响应式取值,按最大响应值依次取值,请勿修改。
const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs']
/**
* gutter, antd
*
* @param gut
*/
const getNormalizedGutter = (gut: Gutter | Gutter[]) => {
const results: [number, number] = [0, 0]
const normalizedGutter = Array.isArray(gut) ? gut : [gut, 0]
normalizedGutter.forEach((g, index) => {
if (typeof g === 'object') {
for (let i = 0; i < responsiveArray.length; i += 1) {
const breakpoint: Breakpoint = responsiveArray[i]
if (screens.value[breakpoint] && g[breakpoint] !== undefined) {
results[index] = g[breakpoint] as number
break
}
}
} else {
results[index] = g || 0
}
})
return results
}
/**
* style
*
* @param withStyle
* @param appendStyle style
*/
const getStyle = (withStyle: boolean, appendStyle: CSSProperties) => {
return withStyle ? appendStyle : {}
}
// const getColSpanStyle = (colSpan: CardProps['colSpan']) => {
// let span = colSpan
//
// // colSpan 响应式
// if (typeof colSpan === 'object') {
// for (let i = 0; i < responsiveArray.length; i += 1) {
// const breakpoint: Breakpoint = responsiveArray[i]
// if (screens.value[breakpoint] && colSpan[breakpoint] !== undefined) {
// span = colSpan[breakpoint]
// break
// }
// }
// }
//
// // 当 colSpan 为 30% 或 300px 时
// const colSpanStyle = getStyle(typeof span === 'string' && /\d%|\dpx/i.test(span), {
// width: span as string,
// flexShrink: 0
// })
//
// return { span, colSpanStyle }
// }
const prefixCls = getPrefixCls('pro-card')
const [horizonalGutter, verticalGutter] = getNormalizedGutter(props.gutter)
// 判断是否套了卡片,如果套了的话将自身卡片内部内容的 padding 设置为0
const containProCard = false
// const childrenArray = React.Children.toArray(children) as ProCardChildType[]
//
// const childrenModified = childrenArray.map((element, index) => {
// if (element?.type?.isProCard) {
// containProCard = true
//
// // 宽度
// const { colSpan } = element.props
// const { span, colSpanStyle } = getColSpanStyle(colSpan)
//
// const columnClassName = [
// [`${prefixCls}-col`],
// {
// [`${prefixCls}-split-vertical`]:
// split === 'vertical' && index !== childrenArray.length - 1,
// [`${prefixCls}-split-horizontal`]:
// split === 'horizontal' && index !== childrenArray.length - 1,
// [`${prefixCls}-col-${span}`]: typeof span === 'number' && span >= 0 && span <= 24
// }
// ]
//
// return (
// <div
// style={{
// ...colSpanStyle,
// ...getStyle(horizonalGutter! > 0, {
// paddingRight: horizonalGutter / 2,
// paddingLeft: horizonalGutter / 2
// }),
// ...getStyle(verticalGutter! > 0, {
// paddingTop: verticalGutter / 2,
// paddingBottom: verticalGutter / 2
// })
// }}
// key={`pro-card-col-${element?.key || index}`}
// class={columnClassName}
// >
// {React.cloneElement(element)}
// </div>
// )
// }
// return element
// })
const cardCls = [
`${prefixCls}`,
attrs.class,
{
[`${prefixCls}-border`]: props.bordered,
[`${prefixCls}-contain-card`]: containProCard,
[`${prefixCls}-loading`]: props.loading,
[`${prefixCls}-split`]: props.split === 'vertical' || props.split === 'horizontal',
[`${prefixCls}-ghost`]: props.ghost,
[`${prefixCls}-hoverable`]: props.hoverable,
[`${prefixCls}-size-${props.size}`]: props.size,
[`${prefixCls}-type-${props.type}`]: props.type,
[`${prefixCls}-collapse`]: collapsed.value,
[`${prefixCls}-checked`]: props.checked
}
]
const bodyCls = [
`${prefixCls}-body`,
{
[`${prefixCls}-body-center`]: props.layout === 'center',
[`${prefixCls}-body-direction-column`]:
props.split === 'horizontal' || props.direction === 'column',
[`${prefixCls}-body-wrap`]: props.wrap && containProCard
}
]
const cardBodyStyle = {
...getStyle(horizonalGutter! > 0, {
marginRight: -horizonalGutter / 2,
marginLeft: -horizonalGutter / 2
}),
...getStyle(verticalGutter! > 0, {
marginTop: -verticalGutter / 2,
marginBottom: -verticalGutter / 2
}),
...props.bodyStyle
}
const loadingDOM = slots.loading ? (
slots.loading()
) : (
<Loading
prefix={prefixCls}
// @ts-ignore
style={
props.bodyStyle.padding === 0 || props.bodyStyle.padding === '0px'
? { padding: 24 }
: undefined
}
/>
)
// 非受控情况下展示
const collapsibleButton =
props.collapsible &&
props.collapsed === undefined &&
(props.collapsibleIconRender ? (
props.collapsibleIconRender({ collapsed: collapsed.value })
) : (
<RightOutlined
rotate={!collapsed.value ? 90 : undefined}
class={`${prefixCls}-collapsible-icon`}
/>
))
return () => (
<div
class={cardCls}
style={attrs.style as CSSProperties}
onClick={e => {
props.onChecked?.(e)
props?.onClick?.(e)
}}
>
{(props.title || props.extra || collapsibleButton) && (
<div
class={[
`${prefixCls}-header`,
{
[`${prefixCls}-header-border`]: props.headerBordered || props.type === 'inner',
[`${prefixCls}-header-collapsible`]: collapsibleButton
}
]}
style={props.headStyle}
onClick={() => {
if (collapsibleButton) setCollapsed(!collapsed)
}}
>
<div class={`${prefixCls}-title`}>
{collapsibleButton}
<LabelIconTip
label={props.title}
tooltip={props.tooltip || props.tip}
subTitle={props.subTitle}
/>
</div>
{props.extra && <div class={`${prefixCls}-extra`}>{props.extra}</div>}
</div>
)}
{props.tabs ? (
<div class={`${prefixCls}-tabs`}>
<Tabs onChange={props.tabs.onChange} {...props.tabs}>
{props.loading ? loadingDOM : slots.default?.()}
</Tabs>
</div>
) : (
<div class={bodyCls} style={cardBodyStyle}>
{props.loading ? loadingDOM : slots.default?.()}
</div>
)}
{<Actions actions={props.actions} prefixCls={prefixCls} />}
</div>
)
}
})
export default Card

@ -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,98 @@
import type { TabsProps, TooltipProps } from 'ant-design-vue'
import type { TabPaneProps } from 'ant-design-vue'
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'
import type { VueNode } from '../types'
import { VueNodePropType } from '../types'
export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs'
export type Gutter = number | Partial<Record<Breakpoint, number>>
// eslint-disable-next-line @typescript-eslint/ban-types
export type ProCardTabsProps = {} & TabsProps
export type ColSpanType = number | string
export const cardProps = () => ({
/** 标题样式 */
headStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
/** 内容样式 */
bodyStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
/** 页头是否有分割线 */
headerBordered: { type: Boolean, default: false },
/** 卡片标题 */
title: VueNodePropType as PropType<VueNode>,
/** 副标题 */
subTitle: VueNodePropType as PropType<VueNode>,
/** 标题说明 */
tooltip: [Object, String] as PropType<string | TooltipProps>,
/** @deprecated 你可以使用 tooltip这个更改是为了与 antd 统一 */
tip: String,
/** 右上角自定义区域 */
extra: VueNodePropType as PropType<VueNode>,
/** 布局center 代表垂直居中 */
layout: String as PropType<'default' | 'center'>,
/** 卡片类型 */
type: String as PropType<'default' | 'inner'>,
/** 指定 Flex 方向,仅在嵌套子卡片时有效 */
direction: String as PropType<'column' | 'row'>,
/** 是否自动换行,仅在嵌套子卡片时有效 */
wrap: { type: Boolean, default: false },
/** 尺寸 */
size: String as PropType<'default' | 'small'>,
/** 加载中 */
loading: [Boolean, Object, String] as PropType<boolean | VueNode>,
/** 栅格布局宽度24 栅格,支持指定宽度或百分,需要支持响应式 colSpan={{ xs: 12, sm: 6 }} */
colSpan: [Number, String, Object] as PropType<
ColSpanType | Partial<Record<Breakpoint, ColSpanType>>
>,
/** 栅格间距 */
gutter: {
type: [Number, Object] as PropType<Gutter | Gutter[]>,
default: 0
},
/** 操作按钮 */
actions: Array as PropType<VueNode[]>,
/** 拆分卡片方式 */
split: String as PropType<'vertical' | 'horizontal'>,
/** 是否有边框 */
bordered: { type: Boolean, default: false },
/**
*
*
* @default false
*/
hoverable: { type: Boolean, default: false },
/** 幽灵模式,即是否取消卡片内容区域的 padding 和 背景颜色。 */
ghost: { type: Boolean, default: false },
/** 是否可折叠 */
collapsible: { type: Boolean, default: false },
/** 受控 collapsed 属性 */
collapsed: { type: Boolean, default: false },
/** 折叠按钮自定义节点 */
collapsibleIconRender: Function as PropType<({ collapsed }: { collapsed: boolean }) => VueNode>,
/** 收起卡片的事件 */
onCollapse: Function as PropType<(collapsed: boolean) => void>,
/** 标签栏配置 */
tabs: Object as PropType<ProCardTabsProps>,
/** 前缀 */
prefixCls: String,
/** ProCard 的 ref */
// ref: React.Ref<HTMLDivElement | undefined>
/** 是否展示选中样式 */
checked: { type: Boolean, default: false },
/** 选中改变 */
onChecked: Function as PropType<(e: MouseEvent) => void>,
/** 点击事件 */
onClick: Function as PropType<(e: MouseEvent) => void>
})
export type CardProps = Partial<ExtractPropTypes<ReturnType<typeof cardProps>>>
export type ProCardTabPaneProps = {
/** Key */
key?: string
/** ProCard 相关属性透传 */
cardProps?: CardProps
} & TabPaneProps
export type CardType = CardProps

@ -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,89 @@
import { Space } from 'ant-design-vue'
import { DownOutlined } from '@ant-design/icons-vue'
import type { IntlType } from '../../../provider'
import type { CSSProperties, FunctionalComponent } from 'vue'
import type { VueNode } from '../../../types'
import { useIntl } from '../../../provider'
import omitBoolean from '../../../utils/omitBoolean'
import { getPrefixCls } from '../../../layout/RouteContext'
export type ActionsProps = {
submitter: VueNode
/** 是否收起 */
collapsed?: boolean
/** 收起按钮的事件 */
onCollapse?: (collapsed: boolean) => void
setCollapsed: (collapse: boolean) => void
isForm?: boolean
style?: CSSProperties
/** 收起按钮的 render */
collapseRender?:
| ((
collapsed: boolean,
/** 是否应该展示,有两种情况 列只有三列,不需要收起 form 模式 不需要收起 */
props: ActionsProps,
intl: IntlType,
hiddenNum?: false | number
) => VueNode)
| false
/** 隐藏个数 */
hiddenNum?: false | number
}
const defaultCollapseRender: ActionsProps['collapseRender'] = (collapsed, _, intl, hiddenNum) => {
if (collapsed) {
return (
<>
{intl.getMessage('tableForm.collapsed', '展开')}
{hiddenNum && `(${hiddenNum})`}
<DownOutlined
style={{
marginLeft: '0.5em',
transition: '0.3s all',
transform: `rotate(${collapsed ? 0 : 0.5}turn)`
}}
/>
</>
)
}
return (
<>
{intl.getMessage('tableForm.expand', '收起')}
<DownOutlined
style={{
marginLeft: '0.5em',
transition: '0.3s all',
transform: `rotate(${collapsed ? 0 : 0.5}turn)`
}}
/>
</>
)
}
/**
* FormFooter
*
* @param props
*/
const Actions: FunctionalComponent<ActionsProps> = props => {
const { setCollapsed, collapsed = false, submitter, style, hiddenNum } = props
const intl = useIntl()
const collapseRender = omitBoolean(props.collapseRender) || defaultCollapseRender
return (
<Space style={style} size={16}>
{submitter}
{props.collapseRender !== false && (
<a
class={getPrefixCls('pro-form-collapse-button')}
onClick={() => setCollapsed(!collapsed)}
>
{collapseRender?.(collapsed, props, intl, hiddenNum)}
</a>
)}
</Space>
)
}
export default Actions

@ -0,0 +1,106 @@
/* eslint-disable no-param-reassign */
import type { RowProps } from 'ant-design-vue'
import type { FormProps } from 'ant-design-vue/es/form/Form'
import type { ActionsProps } from './Actions'
import type { VueNode } from '../../../types'
export type SpanConfig =
| number
| {
xs: number
sm: number
md: number
lg: number
xl: number
xxl: number
}
export type BaseQueryFilterProps = Omit<ActionsProps, 'submitter' | 'setCollapsed' | 'isForm'> & {
className?: string
defaultCollapsed?: boolean
/**
* @name layout
* @type 'horizontal' | 'inline' | 'vertical';
*/
layout?: FormProps['layout']
defaultColsNumber?: number
/**
* @name
*
* @example 80
* labelWidth={80}
* @example 140
* labelWidth={140}
* @example
* labelWidth="auto"
*/
labelWidth?: number | 'auto'
/**
* @name 线
* @description `layout` `vertical`
*/
split?: boolean
/**
* @name 8
*
* @example 4
* span={6}
*
* @example 3
* span={6}
*
* @example
* span={xs: 24, sm: 12, md: 8, lg: 6, xl: 6, xxl: 6}
* */
span?: SpanConfig
/**
* @name
* */
searchText?: string
/**
* @name
*/
resetText?: string
/**
* @name
*
* @example searchGutter={24}
* */
searchGutter?: RowProps['gutter']
/**
* @param searchConfig
* @param props {
* type?: 'form' | 'list' | 'table' | 'cardList' | undefined;
* form: FormInstance;
* submit: () => void;
* collapse: boolean;
* setCollapse: (collapse: boolean) => void;
* showCollapseButton: boolean; }
* @name render
*
*
* @example
* optionRender={(searchConfig, props, dom) =>[ <a key="clear"></a>,...dom]}
*
* @example
*
* optionRender={(searchConfig) => [<a key="submit" onClick={()=> searchConfig?.form?.submit()}></a>]}
*/
optionRender?:
| ((
searchConfig: Omit<BaseQueryFilterProps, 'submitter' | 'isForm'>,
props: Omit<BaseQueryFilterProps, 'searchConfig'>,
dom: VueNode[]
) => VueNode[])
| false
/**
* @name Form.Item rule
*/
ignoreRules?: boolean
/**
* @name collapse
*/
showHiddenNum?: boolean
}

@ -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,383 @@
import './BasicLayout.less'
import { Layout } from 'ant-design-vue'
import 'ant-design-vue/es/layout/style/index.less'
import HeaderView, { headerViewProps } from './Header'
import useMediaQuery from '../utils/hooks/useMediaQuery'
import WrapContent from './WrapContent'
import PageLoading from './components/PageLoading'
import { getRender } from './utils'
import SiderMenuWrapper from './components/SiderMenu'
import { clearMenuItem } from './utils/utils'
import { privateSiderMenuProps, siderMenuProps } from './components/SiderMenu/SiderMenu'
import { transformRouteToMenuItem } from './utils/menuUtils'
import { toRefs } from '@vueuse/core'
import type { CSSProperties, PropType, Slots, VNode, ExtractPropTypes } from 'vue'
import type { MenuDataItem, MessageDescriptor, WithFalse } from './types'
import type { LocaleType } from './locales/types'
import type { WaterMarkProps } from './components/WaterMark'
import type { FooterRender, MenuRender, MultiTabRender } from './renderTypes'
import type { VueNode, VueNodeOrRender } from '#/types'
import { VueNodeOrRenderPropType } from '#/types'
import type { RouteRecordRaw } from 'vue-router'
import { getPrefixCls, routeContextInjectKey } from './RouteContext'
import FooterView from './Footer'
export type LayoutBreadcrumbProps = {
minLength?: number
}
const basicLayoutProps = () => ({
...privateSiderMenuProps(),
// 侧边菜单属性
...siderMenuProps(),
// 头部相关属性
...headerViewProps(),
/**
*
*/
routes: Array as PropType<RouteRecordRaw[]>,
/**
* layout 西 context
*
* @example pure={true}
*/
pure: { type: Boolean, default: false },
/**
* logo urlReact false
*
* @example logo logo="https://avatars1.githubusercontent.com/u/8186664?s=460&v=4"
* @example logo logo={<img src="https://avatars1.githubusercontent.com/u/8186664?s=460&v=4"/>}
* @example logo false logo logo={false}
* @example logo logo={()=> <img src="https://avatars1.githubusercontent.com/u/8186664?s=460&v=4"/> }
* */
logo: {
type: VueNodeOrRenderPropType as PropType<WithFalse<VueNodeOrRender>>,
default: undefined
},
/**
* layout loading loading
*/
loading: { type: Boolean, default: false },
/**
*
*
* @description "zh-CN" | "zh-TW" | "en-US" | "it-IT" | "ko-KR"
* @example layout="zh-CN"
* @example layout="en-US"
*/
locale: { type: String as PropType<LocaleType>, default: 'zh-CN' },
/**
* @name layout true
*
* @example collapsed={true}
*/
collapsed: { type: Boolean, default: undefined },
/**
*
*
* @example onCollapse=(collapsed)=>{ setCollapsed(collapsed) };
*/
onCollapse: Function as PropType<(collapsed: boolean) => void>,
/**
*
*
* @example dom footerRender={false}
* @example 使 layout DefaultFooter footerRender={() => (<DefaultFooter copyright="这是一条测试文案"/>}
*/
footerRender: {
type: VueNodeOrRenderPropType as PropType<WithFalse<FooterRender>>,
default: undefined
},
/**
* menuData
* @see 使 menu.request params
*
* @example menuDataRender=((menuData) => { return menuData.filter(item => item.name !== 'test') })
* @example menuDataRender={(menuData) => { return menuData.concat({ path: '/test', name: '测试', icon: 'smile' }) }}
* @example menuDataRender={(menuData) => { return menuData.map(item => { if (item.name === 'test') { item.name = '测试' } return item }) }}
* @example menuDataRender={(menuData) => { return menuData.reduce((pre, item) => { return pre.concat(item.children || []) }, []) }}
*/
menuDataRender: Function as PropType<(menuData: MenuDataItem[]) => MenuDataItem[]>,
/**
*
*/
formatMessage: Function as PropType<(message: MessageDescriptor) => string>,
/**
*
*
* @see true
* @example disableMobile={true}
*/
disableMobile: { type: Boolean, default: false },
/**
* content
*
* @example contentStyle={{ backgroundColor: 'red '}}
*/
contentStyle: Object as PropType<CSSProperties>,
/**
* content margin
*
* @example margin disableContentMargin={true}
*/
disableContentMargin: { type: Boolean, default: false },
/** 水印的相关配置 */
waterMarkProps: Object as PropType<WaterMarkProps>,
/** 是否是子布局 */
isChildrenLayout: { type: Boolean, default: false }
})
export type BasicLayoutProps = Partial<ExtractPropTypes<ReturnType<typeof basicLayoutProps>>>
const headerRender = (
props: BasicLayoutProps & { hasSiderMenu: boolean },
slots: Slots,
matchMenuKeys: string[] = []
): VNode | null => {
if (props.headerRender === false || props.pure) {
return null
}
return (
// @ts-ignore TODO
<HeaderView matchMenuKeys={matchMenuKeys} {...props}>
{slots}
</HeaderView>
)
}
const footerRender = (props: BasicLayoutProps, slots: Slots): VueNode => {
if (props.footerRender === false || props.pure) {
return null
}
const render = getRender<FooterRender>(props, slots, 'footerRender')
if (render) {
return render({ ...props }, <FooterView>{slots}</FooterView>)
}
return null
}
const renderSiderMenu = (
props: BasicLayoutProps,
slots: Slots,
matchMenuKeys: string[] = []
): VueNodeOrRender => {
// 指定了不渲染或者精简模式直接返回 null
if (props.menuRender === false || props.pure) {
return null
}
// 如果是顶部导航,且不是手机模式不渲染
if (props.layout === 'top' && !props.isMobile) {
return null
}
let { menuData } = props
/** 如果是分割菜单模式,需要专门实现一下 */
if (props.splitMenus && (props.openKeys !== false || props.layout === 'mix') && !props.isMobile) {
const [key] = matchMenuKeys
if (key) {
menuData = props.menuData?.find(item => item.key === key)?.children || []
} else {
menuData = []
}
}
// 这里走了可以少一次循环
const clearMenuData = clearMenuItem(menuData || [])
if (clearMenuData && clearMenuData?.length < 1 && props.splitMenus) {
return null
}
const defaultDom = (
<SiderMenuWrapper
matchMenuKeys={matchMenuKeys}
{...props}
style={
props.navTheme === 'realDark'
? {
boxShadow: '0 2px 8px 0 rgba(0, 0, 0, 65%)'
}
: {}
}
// 这里走了可以少一次循环
menuData={clearMenuData}
>
{slots}
</SiderMenuWrapper>
)
const menuRender = getRender<MenuRender>(props, slots, 'menuRender')
return menuRender ? menuRender(props, defaultDom) : defaultDom
}
export default defineComponent({
name: 'BasicLayout',
props: basicLayoutProps(),
slots: ['default', 'logo', 'menuHeaderRender', 'menuFooterRender'],
setup(props, { slots, attrs }) {
const prefixCls = props.prefixCls ?? getPrefixCls('pro')
const baseClassName = `${prefixCls}-basicLayout`
// gen className
const className = computed(() => [
'ant-design-pro',
baseClassName,
{
[`screen-${colSize.value}`]: colSize.value,
[`${baseClassName}-top-menu`]: props.layout === 'top',
[`${baseClassName}-fix-siderbar`]: props.fixSiderbar,
[`${baseClassName}-${props.layout}`]: props.layout
}
])
const contentClassName = computed(() => ({
[`${baseClassName}-content`]: true,
[`${baseClassName}-has-header`]: !!headerDom.value,
[`${baseClassName}-content-disable-margin`]: props.disableContentMargin
}))
// TODO这里处理数据转换为题
const menuInfoData = computed(() => transformRouteToMenuItem(props.routes || []))
const colSize = useMediaQuery()
const isMobile = computed(() => colSize.value === 'sm' || colSize.value === 'xs')
// ToDo collapsed 的双向绑定
// siderMenuDom 为空的时候,不需要 padding
const genLayoutStyle: CSSProperties = {
position: 'relative'
}
// if is some layout children, don't need min height
watchEffect(() => {
if (props.isChildrenLayout || (props.contentStyle && props.contentStyle.minHeight)) {
genLayoutStyle.minHeight = '0px'
}
})
// If it is a fix menu, calculate padding
// don't need padding in phone mode
const leftSiderWidth = computed(() => {
const hasLeftPadding = props.layout !== 'top' && !isMobile.value
if (hasLeftPadding) {
return props.collapsed ? 48 : props.siderWidth
}
return 0
})
// render sider dom
const siderMenuDom = computed(() =>
renderSiderMenu(
{
...props,
menuData: menuInfoData.value,
isMobile: isMobile.value,
theme: props.navTheme === 'dark' ? 'dark' : 'light',
prefixCls: prefixCls
},
slots,
props.matchMenuKeys
)
)
// render header dom
const headerViewProps = computed<BasicLayoutProps & { hasSiderMenu: boolean }>(() => ({
...props,
hasSiderMenu: !!siderMenuDom.value,
menuData: menuInfoData.value,
isMobile: isMobile.value,
theme: props.navTheme === 'dark' ? 'dark' : 'light',
prefixCls: prefixCls,
siderWidth: leftSiderWidth.value
}))
const headerDom = computed(() =>
headerRender(headerViewProps.value, slots, props.matchMenuKeys)
)
// render footer dom
const footerDom = computed(() =>
footerRender(
{
...props,
isMobile: isMobile.value,
collapsed: props.collapsed
},
slots
)
)
const hasFooterToolbar = ref(false)
const setHasFooterToolbar = (has: boolean) => {
hasFooterToolbar.value = has
}
// TODO pick 属性,防止传递太多无效数据下去
const routeContext = reactive({
...toRefs(props),
// breadcrumb: breadcrumbProps,
// @ts-ignore
menuDa: menuInfoData.value!,
isMobile: isMobile.value,
collapsed: props.collapsed,
isChildrenLayout: true,
// title: pageTitleInfo.pageName,
hasSiderMenu: !!siderMenuDom.value,
hasHeader: !!headerDom.value,
siderWidth: leftSiderWidth.value,
hasFooter: !!footerDom.value,
hasFooterToolbar: hasFooterToolbar.value,
setHasFooterToolbar
// matchMenus,
// matchMenuKeys,
// currentMenu,
})
provide(routeContextInjectKey, routeContext)
return () => {
const multiTabRender = getRender<MultiTabRender>(props, slots, 'multiTabRender')
const multiTabDom = multiTabRender && multiTabRender(headerViewProps.value)
return (
<div class={className.value}>
<Layout style={{ minHeight: '100%', ...(attrs.style as CSSProperties) }}>
{siderMenuDom.value}
<div style={genLayoutStyle} class={'ant-layout'}>
{headerDom.value}
{multiTabDom}
{/*@ts-ignore*/}
<WrapContent class={contentClassName.value} style={props.contentStyle}>
{props.loading ? <PageLoading /> : slots.default?.()}
</WrapContent>
{footerDom.value}
</div>
</Layout>
</div>
)
}
}
})

@ -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,188 @@
import './index.less'
import { getRender } from '../../utils'
import {
defaultRenderCollapsedButton,
defaultRenderLogo,
defaultRenderLogoAndTitle,
privateSiderMenuProps
} from '../SiderMenu/SiderMenu'
import { pureSettingsProps } from '../../defaultSettings'
import type { MenuDataItem, WithFalse } from '../../types'
import type { PureSettings } from '../../defaultSettings'
import type { HeaderViewProps } from '../../Header'
import type { CSSProperties, PropType, ExtractPropTypes } from 'vue'
import type { SiderMenuProps } from '../SiderMenu/SiderMenu'
import type { HeaderContentRender, MenuRender, RightContentRender } from '../../renderTypes'
import { VueNodeOrRenderPropType } from '#/types'
import type { VueNodeOrRender } from '#/types'
import TopNavHeader from '#/layout/components/TopNavHeader'
import { clearMenuItem } from '#/layout/utils/utils'
export const globalHeaderProps = () => ({
...privateSiderMenuProps(),
...pureSettingsProps,
// 覆盖下默认值
headerTheme: {
type: String as PropType<PureSettings['headerTheme']>,
default: 'dark'
},
// 自有属性
collapsed: { type: Boolean, default: undefined },
onCollapse: {
type: Function as PropType<(collapsed: boolean) => void>,
default: undefined
},
isMobile: { type: Boolean, default: undefined },
logo: {
type: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
default: () => undefined
},
/**
* menuRender SiderMenu
*
* @example menuRender={(props,defaultDom)=> props.collapsed ? null : defaultDom}
* @example menuRender={false}
*/
menuRender: {
type: [Function, Boolean] as PropType<WithFalse<MenuRender>>,
default: () => undefined
},
/**
* ,
*
* @example : rightRender={(props) => <Avatar shape="square" size="small" icon={<UserOutlined />} />}
* @example : rightRender={(props) => [<Button type="primary"></Button>,<Button type="primary"></Button>]}
*/
rightContentRender: {
type: [Function, Boolean] as PropType<WithFalse<RightContentRender>>,
default: () => undefined
},
prefixCls: { type: String, default: undefined },
menuData: {
type: Array as PropType<MenuDataItem[]>,
default: () => undefined
},
onMenuHeaderClick: Function as PropType<(e: MouseEvent) => void>,
menuHeaderRender: {
type: [Function, Boolean] as PropType<SiderMenuProps['menuHeaderRender']>,
default: undefined
},
/**
* menu
*
* @example headerContentRender={(props) => <div> </div>}
*/
headerContentRender: {
type: [Function, Boolean] as PropType<WithFalse<HeaderContentRender>>,
default: undefined
},
collapsedButtonRender: {
type: [Function, Boolean] as PropType<SiderMenuProps['collapsedButtonRender']>,
default: () => defaultRenderCollapsedButton
},
splitMenus: { type: Boolean, default: undefined }
})
export type GlobalHeaderProps = Partial<ExtractPropTypes<ReturnType<typeof globalHeaderProps>>>
const renderLogo = (
props: SiderMenuProps,
menuHeaderRender: SiderMenuProps['menuHeaderRender'],
logoDom: VueNodeOrRender
) => {
if (menuHeaderRender === false) {
return null
}
if (menuHeaderRender) {
return menuHeaderRender(props, logoDom, null)
}
return logoDom
}
export default defineComponent({
name: 'GlobalHeader',
props: globalHeaderProps(),
setup(props, { slots, attrs }) {
// TODO 布局方向支持
const direction = undefined
const baseClassName = `${props.prefixCls}-global-header`
const className = computed(() => [
attrs.class,
baseClassName,
{ [`${baseClassName}-layout-${props.layout}`]: props.layout && props.headerTheme === 'dark' }
])
return () => {
if (props.layout === 'mix' && !props.isMobile && props.splitMenus) {
const noChildrenMenuData = (props.menuData || []).map(item => ({
...item,
children: undefined
}))
const clearMenuData = clearMenuItem(noChildrenMenuData)
return (
<TopNavHeader
mode="horizontal"
{...props}
splitMenus={false}
menuData={clearMenuData}
theme={props.headerTheme as 'light' | 'dark'}
>
{slots}
</TopNavHeader>
)
}
const logoClassNames = [
`${baseClassName}-logo`,
{ [`${baseClassName}-logo-rtl`]: direction === 'rtl' }
]
// 添加 slots 支持
const logoRender = getRender<VueNodeOrRender>(props, slots, 'logo')
const logoDom = (
<span class={logoClassNames} key="logo">
<a>{defaultRenderLogo(logoRender)}</a>
</span>
)
const rightContentRender = getRender<RightContentRender>(props, slots, 'rightContentRender')
return (
<div class={className.value} style={attrs.style as CSSProperties}>
{props.isMobile && renderLogo(props, props.menuHeaderRender, logoDom)}
{props.isMobile && props.collapsedButtonRender && (
<span
class={`${baseClassName}-collapsed-button`}
onClick={() => {
if (props.onCollapse) {
props.onCollapse(!props.collapsed)
}
}}
>
{props.collapsedButtonRender(props.collapsed)}
</span>
)}
{props.layout === 'mix' && !props.isMobile && (
<>
<div class={logoClassNames} onClick={props.onMenuHeaderClick}>
{defaultRenderLogoAndTitle(
{ ...props, collapsed: false },
slots,
'headerTitleRender'
)}
</div>
</>
)}
<div style={{ flex: 1 }}>{slots.default?.()}</div>
{rightContentRender && rightContentRender(props as HeaderViewProps)}
</div>
)
}
}
})

@ -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,385 @@
import './index.less'
import { PageHeader, Tabs, Affix, Breadcrumb } from 'ant-design-vue'
import 'ant-design-vue/es/page-header/style/index.less'
import 'ant-design-vue/es/tabs/style/index.less'
import 'ant-design-vue/es/affix/style/index.less'
import 'ant-design-vue/es/breadcrumb/style/index.less'
import type {
TabsProps,
AffixProps,
PageHeaderProps,
TabPaneProps,
SpinProps,
BreadcrumbProps
} from 'ant-design-vue'
import GridContent from '../GridContent'
import FooterToolbar from '../FooterToolbar'
import PageLoading from '../PageLoading'
import type { WithFalse } from '../../types'
import type { WaterMarkProps } from '../WaterMark'
import WaterMark from '../WaterMark'
import type { CSSProperties, ExtractPropTypes, FunctionalComponent, PropType } from 'vue'
import type { VueNode } from 'ant-design-vue/es/_util/type'
import { getPrefixCls, routeContextInjectKey } from '../../RouteContext'
import { reactiveOmit, reactivePick } from '@vueuse/core'
import type { VueNodeOrRender } from '#/types'
import { VueNodeOrRenderPropType, WithFalseVueNodeOrRenderPropType } from '#/types'
import { pageHeaderProps } from 'ant-design-vue/es/page-header'
import omit from 'ant-design-vue/es/_util/omit'
const antvPageHeaderPropsKeys = Object.keys(pageHeaderProps())
export const pageHeaderTabConfig = () => ({
/** tabs 的列表 */
tabList: Array as PropType<(TabPaneProps & { key?: string | number })[]>,
/** 当前选中 tab 的 key */
tabActiveKey: [String, Number] as PropType<TabsProps['activeKey']>,
/** tab 修改时触发 */
onTabChange: Function as PropType<TabsProps['onChange']>,
/** tab 上额外的区域 */
tabBarExtraContent: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
/** tabs 的其他配置 */
tabProps: Object as PropType<TabsProps>,
/** 固定 PageHeader 到页面顶部 */
fixedHeader: { type: Boolean, default: undefined }
})
export type PageHeaderTabConfig = Partial<ExtractPropTypes<ReturnType<typeof pageHeaderTabConfig>>>
type PageHeaderRender = (props: PageContainerProps) => VueNodeOrRender
export const pageContainerProps = () => ({
...pageHeaderTabConfig(),
...omit(pageHeaderProps(), ['title', 'footer', 'breadcrumb']),
title: {
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<VueNodeOrRender>>,
default: undefined
},
content: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
extraContent: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
prefixCls: String,
footer: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
/** 是否显示背景色 */
ghost: { type: Boolean, default: undefined },
/**
* PageHeader ( antd )
*/
header: Object as PropType<Partial<PageHeaderProps>>,
/** pageHeader */
pageHeaderRender: {
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<PageHeaderRender>>,
default: undefined
},
/** 固钉的配置 (与 antd 完全相同) */
affixProps: Object as PropType<AffixProps>,
/** 内容是否加载中 (只加载内容区域) */
loading: {
type: [Object, Function, Boolean] as PropType<boolean | SpinProps | VueNodeOrRender>,
default: false
},
// TODO 由于 ant-design-vue 暂时不支持这个属性,所以先注释
/** 自定义 breadcrumb,返回false不展示 */
// breadcrumbRender: {
// type: WithFalseCustomRenderPropType as PropType<WithFalse<PageHeaderProps['breadcrumb']>>,
// default: undefined
// },
/** 水印的配置 */
waterMarkProps: Object as PropType<WaterMarkProps>,
/** 配置面包屑 */
breadcrumb: Object as PropType<BreadcrumbProps>
})
export type PageContainerProps = Partial<ExtractPropTypes<ReturnType<typeof pageContainerProps>>>
function genLoading(spinProps: boolean | SpinProps) {
if (typeof spinProps === 'object') {
return spinProps
}
return { spinning: spinProps }
}
/**
* Render Footer tabList In order to be compatible with the old version of the PageHeader basically
* all the functions are implemented.
*/
const renderFooter = (props: Omit<PageContainerProps & { prefixedClassName: string }, 'title'>) => {
if (Array.isArray(props.tabList) || props.tabBarExtraContent) {
return (
<Tabs
class={`${props.prefixedClassName}-tabs`}
activeKey={props.tabActiveKey}
onChange={key => {
if (props.onTabChange) {
props.onTabChange(key)
}
}}
tabBarExtraContent={props.tabBarExtraContent}
{...props.tabProps}
>
{props.tabList?.map((item, index) => (
<Tabs.TabPane {...item} tab={item.tab} key={item.key || index} />
))}
</Tabs>
)
}
return null
}
const renderPageHeader = (
content: VueNodeOrRender,
extraContent: VueNodeOrRender,
prefixedClassName: string
): VueNode => {
if (!content && !extraContent) {
return null
}
return (
<div class={`${prefixedClassName}-detail`}>
<div class={`${prefixedClassName}-main`}>
<div class={`${prefixedClassName}-row`}>
{content && <div class={`${prefixedClassName}-content`}>{content}</div>}
{extraContent && <div class={`${prefixedClassName}-extraContent`}>{extraContent}</div>}
</div>
</div>
</div>
)
}
/**
* ProLayout 使
*
* @param props
* @returns
*/
const ProBreadcrumb: FunctionalComponent<BreadcrumbProps> = props => {
const routeContext = inject(routeContextInjectKey)
return (
<div
style={{
height: '100%',
display: 'flex',
alignItems: 'center'
}}
>
{/* @ts-ignore TODO 面包屑透传处理 */}
<Breadcrumb {...routeContext?.breadcrumb} {...routeContext?.breadcrumbProps} {...props} />
</div>
)
}
// eslint-disable-next-line vue/one-component-per-file
const ProPageHeader = defineComponent({
name: 'ProPageHeader',
inheritAttrs: false,
props: {
...pageContainerProps(),
prefixedClassName: { type: String, default: '' }
},
setup(props, { slots }) {
const routeContext = inject(routeContextInjectKey, {})
const restProps = reactiveOmit(
props,
'title',
'content',
'pageHeaderRender',
'header',
'prefixedClassName',
'extraContent',
'prefixCls'
)
if (props.pageHeaderRender === false) {
return null
}
if (props.pageHeaderRender) {
return <> {props.pageHeaderRender({ ...props, ...routeContext })}</>
}
const pageHeaderTitle = computed(() => {
if (!props.title && props.title !== false) {
return routeContext.title
} else {
return props.title
}
})
// @ts-ignore
const antdPageHeaderProps = reactivePick(restProps, antvPageHeaderPropsKeys)
// @ts-ignore
const localPageHeaderProps: PageHeaderProps = {
...antdPageHeaderProps,
footer: renderFooter({
...restProps,
prefixedClassName: props.prefixedClassName
}),
...props.header,
title: pageHeaderTitle.value
}
const { breadcrumb } = localPageHeaderProps as {
breadcrumb: BreadcrumbProps
}
const noHasBreadCrumb = !breadcrumb || (!breadcrumb?.itemRender && !breadcrumb?.routes?.length)
if (
['title', 'subTitle', 'extra', 'tags', 'footer', 'avatar', 'backIcon'].every(
// @ts-ignore
item => !localPageHeaderProps[item]
) &&
noHasBreadCrumb &&
!props.content &&
!props.extraContent
) {
return null
}
return () => (
<div class={`${props.prefixedClassName}-warp`}>
<PageHeader
{...localPageHeaderProps}
breadcrumb={{ ...localPageHeaderProps.breadcrumb, ...routeContext.breadcrumbProps }}
prefixCls={props.prefixCls}
>
{{
...slots,
default: () =>
slots.headerContent?.(props) ||
renderPageHeader(props.content, props.extraContent, props.prefixedClassName)
}}
</PageHeader>
</div>
)
}
})
const pageHeaderSlot = [
'backIcon',
'avatar',
'breadcrumb',
'title',
'subTitle',
'tags',
'extra',
'footer'
]
// eslint-disable-next-line vue/one-component-per-file
const PageContainer = defineComponent({
name: 'PageContainer',
inheritAttrs: false,
props: pageContainerProps(),
slots: [...pageHeaderSlot, 'loading', 'pageHeaderRender', 'headerContent', 'extraContent'],
setup(props, { attrs, slots }) {
const restProps = reactiveOmit(props, 'loading', 'footer', 'affixProps', 'ghost', 'fixedHeader')
const value = inject(routeContextInjectKey, {})
const prefixCls = props.prefixCls || getPrefixCls('pro')
const prefixedClassName = computed(() => `${prefixCls}-page-container`)
const containerClassName = computed(() => [
prefixedClassName.value,
attrs.class,
{
[`${prefixCls}-page-container-ghost`]: props.ghost,
[`${prefixCls}-page-container-with-footer`]: props.footer
}
])
const renderLoading = (): VueNode => {
// 当loading时一个合法的ReactNode时说明用户使用了自定义loading,直接返回改自定义loading
if (slots.loading) {
return slots.loading()
}
// 当传递过来的是布尔值并且为false时说明不需要显示loading,返回null
if (typeof props.loading === 'boolean' && !props.loading) {
return null
}
// 如非上述两种情况那么要么用户传了一个true,要么用户传了loading配置使用genLoading生成loading配置后返回PageLoading
const spinProps = genLoading(props.loading as boolean | SpinProps)
// 如果传的是loading配置但spinning传的是false也不需要显示loading
return spinProps.spinning ? <PageLoading {...spinProps} /> : null
}
function renderContent(loadingDom: VueNode, content: VueNode): VueNode {
// 只要loadingDom非空我们就渲染loadingDom,否则渲染内容
const dom = loadingDom || content
if (props.waterMarkProps || value.waterMarkProps) {
const waterMarkProps = {
...value.waterMarkProps,
...props.waterMarkProps
}
return <WaterMark {...waterMarkProps}>{dom}</WaterMark>
}
return dom
}
return () => {
const pageHeaderDom = (
<ProPageHeader
{...restProps}
ghost={props.ghost}
prefixCls={undefined}
prefixedClassName={prefixedClassName.value}
>
{reactiveOmit(slots, 'default', 'loading')}
</ProPageHeader>
)
const content = slots.default ? (
<>
<div class={`${prefixedClassName.value}-children-content`}>{slots.default()}</div>
{value.hasFooterToolbar && <div style={{ height: '48px', marginTop: '24px' }} />}
</>
) : null
const loadingDom = renderLoading()
const renderContentDom = renderContent(loadingDom, content)
return (
<div style={attrs.style as CSSProperties} class={containerClassName.value}>
{props.fixedHeader && pageHeaderDom ? (
// 在 hasHeader 且 fixedHeader 的情况下,才需要设置高度
// @ts-ignore
<Affix
{...props.affixProps}
offsetTop={value.hasHeader && value.fixedHeader ? value.headerHeight : 0}
>
{pageHeaderDom}
</Affix>
) : (
pageHeaderDom
)}
{renderContentDom && <GridContent>{renderContentDom}</GridContent>}
{props.footer && <FooterToolbar prefixCls={prefixCls}>{props.footer}</FooterToolbar>}
</div>
)
}
}
})
export { ProPageHeader, ProBreadcrumb }
export default PageContainer

@ -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,329 @@
import './index.less'
import { menuProps } from 'ant-design-vue/es/menu/src/Menu'
import { pureSettingsProps, defaultSettings } from '../../defaultSettings'
import { isImg, isUrl } from '../../utils/checkUtils'
import { Menu, Skeleton } from 'ant-design-vue'
import Icon, { createFromIconfontCN } from '@ant-design/icons-vue'
import type { MenuDataItem, MessageDescriptor, WithFalse } from '../../types'
import type { MenuProps, MenuTheme } from 'ant-design-vue'
import type { MenuItemRender, SubMenuItemRender } from '../../renderTypes'
import type { PropType, ExtractPropTypes } from 'vue'
import type { SelectEventHandler, SelectInfo } from 'ant-design-vue/es/menu/src/interface'
import type { Key } from 'ant-design-vue/es/_util/type'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import omit from 'ant-design-vue/es/_util/omit'
import { WithFalseVueNodeOrRenderPropType } from '#/types'
import type { VueNodeOrRender } from '#/types'
import { redirectPath } from '@/config'
import AntIcon from '#/layout/components/AntIcon/index'
export const baseMenuProps = () => ({
...omit(menuProps(), ['openKeys', 'onOpenChange']),
...pureSettingsProps,
/** 默认的是否展开,会受到 breakpoint 的影响 */
defaultCollapsed: { type: Boolean, default: undefined },
collapsed: { type: Boolean, default: undefined },
splitMenus: { type: Boolean, default: undefined },
isMobile: { type: Boolean, default: undefined },
menuData: Array as PropType<MenuDataItem[]>,
onCollapse: Function as PropType<(collapsed: boolean) => void>,
openKeys: [Array, Boolean] as PropType<WithFalse<string[]> | undefined>,
handleOpenChange: Function as PropType<(openKeys: Key[]) => void>,
iconPrefixes: String,
/** 要给菜单的props, 参考antd-menu的属性。https://ant.design/components/menu-cn/ */
menuProps: Object as PropType<MenuProps>,
theme: String as PropType<MenuTheme>,
formatMessage: Function as PropType<(message: MessageDescriptor) => string>,
/**
* props
* @see 使 menuItemRender
*
* @example 使 a subMenuItemRender={(item, defaultDom) => { return <a onClick={()=> history.push(item.path) }>{defaultDom}</a> }}
* @example subMenuItemRender={(item, defaultDom) => { return <a onClick={()=> log.click(item.name) }>{defaultDom}</a> }}
*/
subMenuItemRender: WithFalseVueNodeOrRenderPropType as PropType<SubMenuItemRender>,
/**
* props Router 使
* @see 使 subMenuItemRender
*
* @example 使 a menuItemRender={(item, defaultDom) => { return <a onClick={()=> history.push(item.path) }>{defaultDom}</a> }}
* @example 使 Link menuItemRender={(item, defaultDom) => { return <Link to={item.path}>{defaultDom}</Link> }}
*/
menuItemRender: WithFalseVueNodeOrRenderPropType as PropType<MenuItemRender>,
/**
* menuData menuDataRender postMenuData
*
* @example postMenuData={(menuData) => { return menuData.map(item => { return { ...item, icon: <Icon type={item.icon} /> } }) }}
*/
postMenuData: {
type: Function as PropType<(menusData?: MenuDataItem[]) => MenuDataItem[]>,
default: (data?: MenuDataItem[]) => data || []
}
})
export type BaseMenuProps = Partial<ExtractPropTypes<ReturnType<typeof baseMenuProps>>>
let IconFont = createFromIconfontCN({
scriptUrl: defaultSettings.iconfontUrl
})
// Allow menu.js config icon as string or ReactNode
// icon: 'setting',
// icon: 'icon-geren' #For Iconfont ,
// icon: 'http://demo.com/icon.png',
// icon: '/favicon.png',
// icon: <Icon type="setting" />,
const getIcon = (icon?: string | VueNodeOrRender, iconPrefixes = 'icon-'): VueNodeOrRender => {
if (typeof icon === 'string' && icon !== '') {
if (isUrl(icon) || isImg(icon)) {
return (
<Icon component={() => <img src={icon} alt="icon" class="ant-pro-sider-menu-icon" />} />
)
}
if (icon.startsWith(iconPrefixes)) {
return <IconFont type={icon} />
}
// @ts-ignore
return <AntIcon type={icon} />
}
return icon
}
class MenuUtil {
constructor(props: BaseMenuProps) {
this.props = props
}
props: BaseMenuProps
getNavMenuItems = (menusData: MenuDataItem[] = [], isChildren: boolean) =>
menusData.map(item => this.getSubMenuOrItem(item, isChildren)).filter(item => item)
/** Get SubMenu or Item */
getSubMenuOrItem = (item: MenuDataItem, isChildren: boolean): any => {
const children = item?.children || item?.routes
if (Array.isArray(children) && children.length > 0) {
const name = this.getIntlName(item)
const { subMenuItemRender, prefixCls, menu, iconPrefixes } = this.props
// get defaultTitle by menuItemRender
const defaultTitle = item.icon ? (
<span class={`${prefixCls}-menu-item`} title={name}>
{getIcon(item.icon, iconPrefixes)}
<span class={`${prefixCls}-menu-item-title`}>{name}</span>
</span>
) : (
<span class={`${prefixCls}-menu-item`} title={name}>
{name}
</span>
)
// subMenu only title render
const title = subMenuItemRender
? subMenuItemRender({ ...item, isUrl: false }, defaultTitle, this.props)
: defaultTitle
const MenuParent = menu?.type === 'group' ? Menu.ItemGroup : Menu.SubMenu
return (
<MenuParent key={item.key || item.path} title={title} onTitleClick={item.onTitleClick}>
{this.getNavMenuItems(children, true)}
</MenuParent>
)
}
return (
<Menu.Item
key={item.key || item.path}
disabled={item.disabled}
onClick={(e: Event) => {
if (isUrl(item?.path)) {
window.open(item.path, '_blank')
}
item.onTitleClick?.(e)
}}
>
{{
default: () => this.getMenuItemPath(item),
icon: () => getIcon(item.icon, this.props.iconPrefixes)
}}
</Menu.Item>
)
}
getIntlName = (item: MenuDataItem) => {
const { name, locale } = item
const { menu, formatMessage } = this.props
if (locale && menu?.locale !== false) {
return formatMessage?.({
id: locale,
defaultMessage: name
})
}
return name
}
/**
* http. Link a Judge whether it is http link.return a or Link
*
* @memberof SiderMenu
*/
getMenuItemPath = (item: MenuDataItem): VueNodeOrRender => {
const itemPath = this.conversionPath(item.path || '/')
// TODO 这个 location 的传递问题
//const { location = { pathname: '/' } } = this.props
const location = { pathname: '/' }
// if local is true formatMessage all name。
const name = this.getIntlName(item)
const { prefixCls } = this.props
const isHttpUrl = isUrl(itemPath)
const defaultItem = (
<span class={[`${prefixCls}-menu-item`, { [`${prefixCls}-menu-item-link`]: isHttpUrl }]}>
<span class={`${prefixCls}-menu-item-title`}>{name}</span>
</span>
)
if (this.props.menuItemRender) {
const renderItemProps = {
...item,
isUrl: isHttpUrl,
itemPath,
isMobile: this.props.isMobile,
replace: itemPath === location.pathname,
onClick: () => {
if (isHttpUrl) window.open(itemPath)
if (this.props.onCollapse) this.props.onCollapse(true)
},
children: undefined
}
return this.props.menuItemRender(renderItemProps, defaultItem, this.props)
}
return defaultItem
}
conversionPath = (path: string) => {
if (path && path.indexOf('http') === 0) {
return path
}
return `/${path || ''}`.replace(/\/+/g, '/')
}
}
function getOpenKeys(props: BaseMenuProps, route: RouteLocationNormalizedLoaded) {
// 折叠的时候,或者 top 菜单模式的时候openKeys 需要置空
if (!props.collapsed && ['side', 'mix'].includes(props.layout || 'mix')) {
return route.matched.filter(r => r.path !== route.path && r.path !== '/').map(r => r.path)
}
return []
}
export default defineComponent({
name: 'BaseMenu',
inheritAttrs: false,
props: baseMenuProps(),
setup(props, { attrs }) {
// TODO defaultOpenAll 支持,目前 react 版本的示例中是无效果的,所以这里暂时不处理
// const initOpenKeys = () => {
// if (props.menu?.defaultOpenAll) {
// return getOpenKeysFromMenuData(props.menuData) || []
// }
// return props.openKeys || []
// }
// 根据路由赋值当前选中和打开的菜单
const route = useRoute()
const localOpenKeys = ref<Key[]>([])
const localSelectedKeys = ref<string[]>([])
watchEffect(() => {
// 进行 redirect 的时候不处理,如果要把高级组件剥离,这个前缀不能写死需要透传过来
if (route.path.startsWith(redirectPath)) return
localOpenKeys.value = getOpenKeys(props, route)
localSelectedKeys.value = route.matched.filter(x => x.path !== '/').map(x => x.path)
})
console.log(props.menuData)
// 选中菜单的时候进行路由切换
const router = useRouter()
const handleSelect: SelectEventHandler = (args: SelectInfo): void => {
// 忽略外链类型
if (isUrl(args.key as string)) {
return
}
router.push(args.key as string)
localSelectedKeys.value = args.selectedKeys as string[]
// emit('update:selectedKeys', args.selectedKeys)
}
// 打开菜单时候的触发事件 TODO 支持排他展开
const defaultHandleOpenChange = (openKeys: Key[]) => {
localOpenKeys.value = openKeys
}
const handleOpenChange = props.handleOpenChange ?? defaultHandleOpenChange
watchEffect(() => {
// reset IconFont
if (props.iconfontUrl) {
IconFont = createFromIconfontCN({
scriptUrl: props.iconfontUrl
})
}
})
const cls = computed(() => [attrs.class, { 'top-nav-menu': props.mode === 'horizontal' }])
// sync props
const menuUtils = new MenuUtil(props)
return () => {
if (props.menu?.loading) {
return (
<div
style={
props.mode?.includes('inline')
? { padding: 24 }
: {
marginTop: 16
}
}
>
<Skeleton
active
title={false}
paragraph={{
rows: props.mode?.includes('inline') ? 6 : 1
}}
/>
</div>
)
}
const finallyData = props.postMenuData ? props.postMenuData(props.menuData) : props.menuData
if (finallyData && finallyData?.length < 1) {
return null
}
return (
<Menu
key="Menu"
mode={props.mode}
inlineIndent={16}
theme={props.theme}
style={attrs.style}
class={cls.value}
openKeys={localOpenKeys.value}
selectedKeys={localSelectedKeys.value}
onSelect={handleSelect}
onOpenChange={handleOpenChange}
{...props.menuProps}
>
{menuUtils.getNavMenuItems(finallyData, false)}
</Menu>
)
}
}
})

@ -0,0 +1,337 @@
import './index.less'
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue'
import { VueNodeOrRenderPropType, WithFalseVueNodeOrRenderPropType } from '#/types'
import { Layout, Menu, MenuItem } from 'ant-design-vue'
import BaseMenu, { baseMenuProps } from './BaseMenu'
import type { VueNodeOrRender } from '#/types'
import type { WithFalse } from '../../types'
import type { CSSProperties, ExtractPropTypes, FunctionalComponent, PropType, Slots } from 'vue'
import type { SiderProps } from 'ant-design-vue'
import type {
CollapsedButtonRender,
MenuContentRender,
MenuExtraReander,
MenuFootRender,
MenuHeaderRender
} from '../../renderTypes'
import { getVueNode, getRender } from '../../utils'
export const siderMenuProps = () => ({
...baseMenuProps(),
logo: {
type: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
default: undefined
},
siderWidth: { type: Number, default: 208 },
/**
* logo title
*
* @example logo : menuHeaderRender={(logo,title)=> title}
* @example title : menuHeaderRender={(logo,title)=> logo}
* @example title, logo menuHeaderRender={(logo,title,props)=> props.collapsed ? logo : title}
* @example : menuHeaderRender={false}
*/
menuHeaderRender: {
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<MenuHeaderRender>>,
default: undefined
},
/**
*
*
* @example menuFooterRender={()=><a href="https://pro.ant.design">pro.ant.design</a>}
* @example dom menuFooterRender={()=>collapsed? null :<a href="https://pro.ant.design">pro.ant.design</a>}
*/
menuFooterRender: {
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<MenuFootRender>>,
default: undefined
},
/**
* ,dom
*
* @example menuContentRender={(props,defaultDom)=><div style={{backgroundColor:"red"}}>{defaultDom}</div>}
* @example menuContentRender={(props)=> return <div></div>}
*/
menuContentRender: {
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<MenuContentRender>>,
default: undefined
},
/**
* title logo
*
* @example menuExtraRender={()=>(<Search placeholder="请输入" />)}
* @example dom menuExtraRender={()=>collapsed? null : <Search placeholder="请输入" />}
*/
menuExtraRender: {
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<MenuExtraReander>>,
default: false
},
/**
*
*
* @example 使 collapsedButtonRender={(collapsed)=>collapsed?"展开":"收起"})}
* @example 使icon collapsedButtonRender={(collapsed)=>collapsed?<MenuUnfoldOutlined />:<MenuFoldOutlined />}
* @example collapsedButtonRender={false}
*/
collapsedButtonRender: {
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<CollapsedButtonRender>>,
default: undefined
},
/**
* false
*
* @example breakpoint={false}
* @example breakpoint={'xs'}
*/
breakpoint: {
type: [String, Boolean] as PropType<SiderProps['breakpoint'] | false>,
default: 'lg'
},
/**
* logo title
*
* @example onMenuHeaderClick={()=>{ history.push('/') }}
*/
onMenuHeaderClick: {
type: Function as PropType<(e: MouseEvent) => void>,
default: undefined
},
/**
*
*
* @example links={[<a href="ant.design"> 访 </a>,<a href="help.ant.design"> </a>]}
*/
links: {
type: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
default: undefined
},
// TODO 这个放到事件里面
onOpenChange: {
type: Function as PropType<(openKeys: WithFalse<string[]>) => void>,
default: undefined
},
getContainer: { type: Boolean, default: false },
logoStyle: {
type: Object as PropType<CSSProperties>,
default: () => undefined
},
hide: { type: Boolean, default: undefined }
})
export type SiderMenuProps = Partial<ExtractPropTypes<ReturnType<typeof siderMenuProps>>>
export const privateSiderMenuProps = () => ({
matchMenuKeys: Array as PropType<string[]>
})
export type PrivateSiderMenuProps = Partial<
ExtractPropTypes<ReturnType<typeof privateSiderMenuProps>>
>
export const defaultRenderLogo = (
logo: VueNodeOrRender | (() => VueNodeOrRender)
): VueNodeOrRender => {
if (typeof logo === 'string') {
return <img src={logo} alt="logo" />
}
if (typeof logo === 'function') {
return logo()
}
return logo
}
export const defaultRenderLogoAndTitle = (
props: SiderMenuProps,
slots: Slots,
renderKey = 'menuHeaderRender'
): VueNodeOrRender => {
if (props.layout === 'mix' && renderKey === 'menuHeaderRender') {
return null
}
const renderFunction = getRender<MenuHeaderRender>(props, slots, renderKey)
if (renderFunction === false) {
return null
}
const logRender = getRender<VueNodeOrRender>(props, slots, 'logo')
const logoDom = defaultRenderLogo(logRender)
const titleRender = getVueNode(props.title, slots.title)
const titleDom = <h1>{titleRender ?? 'Ball Cat'}</h1>
if (renderFunction) {
// when collapsed, no render title
return renderFunction(props, logoDom, props.collapsed ? null : titleDom)
}
return (
<a>
{logoDom}
{props.collapsed ? null : titleDom}
</a>
)
}
export const defaultRenderCollapsedButton = (collapsed?: boolean) =>
collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />
function getCollapsedButtonRender(props: SiderMenuProps & PrivateSiderMenuProps, slots: Slots) {
if (props.collapsedButtonRender == false) {
return false
}
const render = getRender<CollapsedButtonRender>(props, slots, 'collapsedButtonRender')
return render || defaultRenderCollapsedButton
}
const SiderMenu: FunctionalComponent<SiderMenuProps & PrivateSiderMenuProps> = (
props,
{ slots, attrs }
) => {
const baseClassName = `${props.prefixCls}-sider`
const siderClassName = {
[`${baseClassName}`]: true,
[`${baseClassName}-fixed`]: props.fixSiderbar,
[`${baseClassName}-layout-${props.layout}`]: props.layout && !props.isMobile,
[`${baseClassName}-light`]: props.theme !== 'dark'
}
const headerDom = defaultRenderLogoAndTitle(props, slots)
// const { flatMenuKeys } = MenuCounter.useContainer()
const flatMenuKeys: string[] = []
const extraDom = props.menuExtraRender && props.menuExtraRender(props)
const menuDom = props.menuContentRender !== false && flatMenuKeys && (
<BaseMenu
{...props}
key="base-menu"
mode="inline"
// @ts-ignore
handleOpenChange={props.onOpenChange}
style={{ width: '100%' }}
class={`${baseClassName}-menu`}
/>
)
const menuRenderDom = props.menuContentRender ? props.menuContentRender(props, menuDom) : menuDom
const collapsedButtonRender = getCollapsedButtonRender(props, slots)
const menuFooterRender = getRender<MenuFootRender>(props, slots, 'menuFooterRender')
return (
<>
{props.fixSiderbar && (
<div
class={`${baseClassName}-fix-place`}
style={{
width: `${props.collapsed ? 48 : props.siderWidth}px`,
overflow: 'hidden',
flex: `0 0 ${props.collapsed ? 48 : props.siderWidth}px`,
maxWidth: `${props.collapsed ? 48 : props.siderWidth}px`,
minWidth: `${props.collapsed ? 48 : props.siderWidth}px`,
transition: `background-color 0.3s, min-width 0.3s, max-width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1)`,
...(attrs.style as CSSProperties)
}}
/>
)}
<Layout.Sider
collapsible
trigger={null}
collapsed={props.collapsed}
breakpoint={props.breakpoint === false ? undefined : props.breakpoint}
onCollapse={collapse => {
if (props.isMobile) return
props.onCollapse?.(collapse)
}}
collapsedWidth={48}
style={{
overflow: 'hidden',
paddingTop:
props.layout === 'mix' && !props.isMobile ? `${props.headerHeight}px` : undefined,
...(attrs.style as CSSProperties)
}}
width={props.siderWidth}
theme={props.theme}
class={siderClassName}
>
{headerDom && (
<div
class={[
`${baseClassName}-logo`,
{
[`${baseClassName}-collapsed`]: props.collapsed
}
]}
onClick={props.layout !== 'mix' ? props.onMenuHeaderClick : undefined}
id="logo"
style={props.logoStyle}
>
{headerDom}
</div>
)}
{extraDom && (
<div class={`${baseClassName}-extra ${!headerDom && `${baseClassName}-extra-no-logo`}`}>
{extraDom}
</div>
)}
<div
style={{
flex: 1,
overflowY: 'auto',
overflowX: 'hidden'
}}
>
{menuRenderDom}
</div>
<div class={`${baseClassName}-links`}>
{collapsedButtonRender !== false && (
<Menu
theme={props.theme}
inlineIndent={16}
class={`${baseClassName}-link-menu`}
selectedKeys={[]}
openKeys={[]}
mode="inline"
>
<MenuItem
key={'collapsed'}
class={`${baseClassName}-collapsed-button`}
title={false}
onClick={() => {
if (props.onCollapse) {
props.onCollapse(!props.collapsed)
}
}}
>
{collapsedButtonRender(props.collapsed)}
</MenuItem>
</Menu>
)}
</div>
{menuFooterRender && (
<div
class={[
`${baseClassName}-footer`,
{ [`${baseClassName}-footer-collapsed`]: !props.collapsed }
]}
>
{menuFooterRender(props)}
</div>
)}
</Layout.Sider>
</>
)
}
export default SiderMenu

@ -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,232 @@
import type { CSSProperties, PropType } from 'vue'
import { getPrefixCls } from '../../RouteContext'
export type WaterMarkProps = {
/** 水印样式 */
markStyle?: CSSProperties
/** 水印类名 */
markClassName?: string
/** 水印之间的水平间距 */
gapX?: number
/** 水印之间的垂直间距 */
gapY?: number
/** 追加的水印元素的z-index */
zIndex?: number
/** 水印的宽度 */
width?: number
/** 水印的高度 */
height?: number
/** 水印在canvas 画布上绘制的垂直偏移量,正常情况下,水印绘制在中间位置, 即 offsetTop = gapY / 2 */
offsetTop?: number // 水印图片距离绘制 canvas 单元的顶部距离
/** 水印在canvas 画布上绘制的水平偏移量, 正常情况下,水印绘制在中间位置, 即 offsetTop = gapX / 2 */
offsetLeft?: number
/** 水印绘制时,旋转的角度,单位 ° */
rotate?: number
/** ClassName 前缀 */
prefixCls?: string
/** 高清印图片源, 为了高清屏幕显示,建议使用 2倍或3倍图优先使用图片渲染水印。 */
image?: string
/** 水印文字内容 */
content?: string | string[]
/** 文字颜色 */
fontColor?: string
/** 文字样式 */
fontStyle?: 'none' | 'normal' | 'italic' | 'oblique'
/** 文字族 */
fontFamily?: string
/** 文字粗细 */
fontWeight?: 'normal' | 'light' | 'weight' | number
/** 文字大小 */
fontSize?: number | string
}
const waterMarkProps = {
markStyle: {
type: Object as PropType<WaterMarkProps['markStyle']>,
default: () => undefined
},
markClassName: {
type: String as PropType<WaterMarkProps['markClassName']>,
default: ''
},
gapX: {
type: Number,
default: 212
},
gapY: {
type: Number,
default: 222
},
// antd 内容层 zIndex 基本上在 10 以下 https://github.com/ant-design/ant-design/blob/6192403b2ce517c017f9e58a32d58774921c10cd/components/style/themes/default.less#L335
zIndex: {
type: Number,
default: 9
},
width: {
type: Number,
default: 120
},
height: {
type: Number,
default: 64
},
offsetTop: {
type: Number,
default: undefined
},
offsetLeft: {
type: Number,
default: undefined
},
// 默认旋转 -22 度
rotate: {
type: Number,
default: -22
},
prefixCls: {
type: String,
default: ''
},
image: {
type: String,
default: ''
},
content: {
type: [String, Array] as PropType<WaterMarkProps['content']>,
default: ''
},
fontColor: {
type: String,
default: 'rgba(0,0,0,.15)'
},
fontStyle: {
type: String,
default: 'normal'
},
fontFamily: {
type: String,
default: 'sans-serif'
},
fontWeight: {
type: [Number, String] as PropType<WaterMarkProps['fontWeight']>,
default: 'normal'
},
fontSize: {
type: [Number, String] as PropType<WaterMarkProps['fontSize']>,
default: 16
}
}
/**
* CSS
*
* @param context
* @see api CanvasRenderingContext2D
*/
const getPixelRatio = (context: any) => {
if (!context) {
return 1
}
const backingStore =
context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio ||
1
return (window.devicePixelRatio || 1) / backingStore
}
const WaterMark = defineComponent({
name: 'WaterMark',
props: waterMarkProps,
setup(props, { slots, attrs }) {
const prefixCls = getPrefixCls('pro-layout-watermark', props.prefixCls)
const wrapperCls = [`${prefixCls}-wrapper`, attrs.class]
const waterMakrCls = [prefixCls, props.markClassName]
const base64Url = ref('')
watchEffect(() => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const ratio = getPixelRatio(ctx)
const canvasWidth = `${(props.gapX + props.width) * ratio}px`
const canvasHeight = `${(props.gapY + props.height) * ratio}px`
const canvasOffsetLeft = props.offsetLeft || props.gapX / 2
const canvasOffsetTop = props.offsetTop || props.gapY / 2
canvas.setAttribute('width', canvasWidth)
canvas.setAttribute('height', canvasHeight)
if (ctx) {
// 旋转字符 rotate
ctx.translate(canvasOffsetLeft * ratio, canvasOffsetTop * ratio)
ctx.rotate((Math.PI / 180) * Number(props.rotate))
const markWidth = props.width * ratio
const markHeight = props.height * ratio
if (props.image) {
const img = new Image()
img.crossOrigin = 'anonymous'
img.referrerPolicy = 'no-referrer'
img.src = props.image
img.onload = () => {
ctx.drawImage(img, 0, 0, markWidth, markHeight)
base64Url.value = canvas.toDataURL()
}
} else if (props.content) {
const markSize = Number(props.fontSize) * ratio
ctx.font = `${props.fontStyle} normal ${props.fontWeight} ${markSize}px/${markHeight}px ${props.fontFamily}`
ctx.fillStyle = props.fontColor
if (Array.isArray(props.content)) {
props.content?.forEach((item: string, index: number) =>
ctx.fillText(item, 0, index * 50)
)
} else {
ctx.fillText(props.content, 0, 0)
}
base64Url.value = canvas.toDataURL()
}
} else {
// eslint-disable-next-line no-console
console.error('当前环境不支持Canvas')
}
})
return () => (
<div
style={{
position: 'relative',
...(attrs.style as CSSProperties)
}}
class={wrapperCls}
>
{slots.default?.()}
<div
class={waterMakrCls}
style={{
zIndex: props.zIndex,
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
backgroundSize: `${props.gapX + props.width}px`,
pointerEvents: 'none',
backgroundRepeat: 'repeat',
...(base64Url
? {
backgroundImage: `url('${base64Url}')`
}
: null),
...props.markStyle
}}
/>
</div>
)
}
})
export default WaterMark

@ -0,0 +1,229 @@
import type { MenuTheme } from 'ant-design-vue/es/menu'
import type { PropType } from 'vue'
export interface MenuDataItem {
children?: MenuDataItem[]
hideChildrenInMenu?: boolean
hideInMenu?: boolean
icon?: any
locale?: string | false
name?: string
key?: string
pro_layout_parentKeys?: string[]
path?: string
parentKeys?: string[]
[key: string]: any
}
/**
*
* @type 'Fluid' | 'Fixed'
*/
export type ContentWidth = 'Fluid' | 'Fixed'
/**
* layout
* @type 'side' | 'top' | 'mix'
*/
export type LayoutMode = 'side' | 'top' | 'mix'
export type RenderSetting = {
headerRender?: false
footerRender?: false
menuRender?: false
menuHeaderRender?: false
}
export type PureSettings = {
/**
* theme for nav menu
*
* @type "light" | "dark" | "realDark"
*/
navTheme?: MenuTheme | 'realDark' | undefined
/**
* mix
* @type "light" | "dark"
*/
headerTheme?: MenuTheme
/**
* customize header height
* @example 64 headerHeight={64}
*/
headerHeight?: number
/**
* layout
* @type 'side' | 'top' | 'mix'
*
* @example layout="top"
* @example layout="side"
* @example layout="mix"
*/
layout?: LayoutMode
/** layout of content: `Fluid` or `Fixed`, only works when layout is top */
contentWidth?: ContentWidth
/** sticky header */
fixedHeader?: boolean
/** sticky siderbar */
fixSiderbar?: boolean
/**
* menu
*
* @example menu={{ locale: false }}
* @example menu={{ defaultOpenAll:true }}
* @example loading menu={{ loading: true }}
* @example menu={{params:{ pathname } request: async (params) => { return [{name:"主页",path=params.pathname}]} }}
* @example 使 MenuGroup menu={{ mode: 'group' }}
* @example menu={{ autoClose: false }}
* @example menu={{ ignoreFlatMenu: true }}
*/
menu?: {
/**
*
*/
locale?: boolean
/**
*
*/
defaultOpenAll?: boolean
/**
*
*/
ignoreFlatMenu?: boolean
/**
* loading
*/
loading?: boolean
/**
* loading
*/
onLoadingChange?: (loading?: boolean) => void
/**
* params request
*
*/
params?: Record<string, any>
/**
* params request
*/
request?: (
params: Record<string, any>,
defaultMenuData: MenuDataItem[]
) => Promise<MenuDataItem[]>
/**
*
*/
type?: 'sub' | 'group'
/**
*
*/
autoClose?: false
}
/**
* false layout pageName pageName - title
*
* Layout title
*/
title?: string | false
/**
* Your custom iconfont Symbol script Url eg//at.alicdn.com/t/font_1039637_btcrd5co4w.js
* Iconfont Usage: https://github.com/ant-design/ant-design-pro/pull/3517
*/
iconfontUrl?: string
/** 主色,需要配合 umi 使用 */
primaryColor?: string
/** 全局增加滤镜 */
colorWeak?: boolean
/**
* mix
*
*
*/
splitMenus?: boolean
}
export type ProSettings = PureSettings & RenderSetting
export const defaultSettings: ProSettings = {
navTheme: 'dark',
headerTheme: 'dark',
layout: 'side',
contentWidth: 'Fluid',
fixedHeader: false,
fixSiderbar: false,
headerHeight: 48,
iconfontUrl: '',
primaryColor: '#1890ff',
splitMenus: false,
// 布局内容默认都渲染
headerRender: undefined,
footerRender: undefined,
menuRender: undefined,
menuHeaderRender: undefined
}
export const pureSettingsProps = {
navTheme: {
type: String as PropType<PureSettings['navTheme']>,
default: defaultSettings.navTheme
},
headerTheme: {
type: String as PropType<PureSettings['headerTheme']>,
default: defaultSettings.headerTheme
},
headerHeight: {
type: Number as PropType<PureSettings['headerHeight']>,
default: defaultSettings.headerHeight
},
layout: {
type: String as PropType<PureSettings['layout']>,
default: defaultSettings.layout
},
contentWidth: {
type: String as PropType<PureSettings['contentWidth']>,
default: defaultSettings.contentWidth
},
fixedHeader: {
type: Boolean as PropType<PureSettings['fixedHeader']>,
default: defaultSettings.fixedHeader
},
fixSiderbar: {
type: Boolean as PropType<PureSettings['fixSiderbar']>,
default: defaultSettings.fixSiderbar
},
menu: {
type: Object as PropType<PureSettings['menu']>,
default: () => {
return {
locale: false
}
}
},
title: {
type: String as PropType<PureSettings['title']>,
default: () => defaultSettings.title
},
iconfontUrl: {
type: String as PropType<PureSettings['iconfontUrl']>,
default: () => defaultSettings.iconfontUrl
},
primaryColor: {
type: String as PropType<PureSettings['primaryColor']>,
default: () => defaultSettings.primaryColor
},
colorWeak: {
type: Boolean as PropType<PureSettings['colorWeak']>,
default: () => defaultSettings.colorWeak
},
splitMenus: {
type: Boolean,
default: false
}
}

@ -0,0 +1,5 @@
import settingDrawer from './en-US/settingDrawer'
export default {
...settingDrawer
}

@ -0,0 +1,40 @@
export default {
'app.setting.pagestyle': 'Page style setting',
'app.setting.pagestyle.dark': 'Dark Menu style',
'app.setting.pagestyle.light': 'Light Menu style',
'app.setting.pagestyle.realdark': 'Dark style (Beta)',
'app.setting.content-width': 'Content Width',
'app.setting.content-width.fixed': 'Fixed',
'app.setting.content-width.fluid': 'Fluid',
'app.setting.themecolor': 'Theme Color',
'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': 'Navigation Mode',
'app.setting.regionalsettings': 'Regional Settings',
'app.setting.regionalsettings.header': 'Header',
'app.setting.regionalsettings.menu': 'Menu',
'app.setting.regionalsettings.footer': 'Footer',
'app.setting.regionalsettings.menuHeader': 'Menu Header',
'app.setting.sidemenu': 'Side Menu Layout',
'app.setting.topmenu': 'Top Menu Layout',
'app.setting.mixmenu': 'Mix Menu Layout',
'app.setting.splitMenus': 'Split Menus',
'app.setting.fixedheader': 'Fixed Header',
'app.setting.fixedsidebar': 'Fixed Sidebar',
'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
'app.setting.hideheader': 'Hidden Header when scrolling',
'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
'app.setting.othersettings': 'Other Settings',
'app.setting.weakmode': 'Weak Mode',
'app.setting.copy': 'Copy Setting',
'app.setting.loading': 'Loading theme',
'app.setting.copyinfo': 'copy successplease replace defaultSettings in src/models/setting.js',
'app.setting.production.hint':
'Setting panel shows in development environment only, please manually modify'
}

@ -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,55 @@
import type { VNode } from 'vue'
import type { PrivateSiderMenuProps, SiderMenuProps } from './components/SiderMenu/SiderMenu'
import type { HeaderViewProps } from './Header'
import type { MenuDataItem } from './types'
import type { BaseMenuProps } from './components/SiderMenu/BaseMenu'
import type { VueNode, VueNodeOrRender } from '#/types'
// 多页签的渲染
export type MultiTabRender = (props: HeaderViewProps) => VueNode
// 头部渲染器
export type HeaderRender = (props: HeaderViewProps, defaultDom: VueNodeOrRender) => VueNode
// 头部标题渲染器
export type HeaderTitleRender = (
props: HeaderViewProps,
logo?: VueNodeOrRender,
title?: VueNodeOrRender
) => VueNode
// 头部内容渲染器
export type HeaderContentRender = (props: HeaderViewProps, defaultDom?: VueNodeOrRender) => VueNode
// 头部右侧内容渲染器
export type RightContentRender = (props: HeaderViewProps) => VueNode
// 处理父级菜单的 props可以复写菜单的点击功能一般用于埋点
export type SubMenuItemRender = (
item: MenuDataItem & { isUrl: boolean },
defaultDom: VueNodeOrRender,
menuProps: BaseMenuProps
) => VueNode
// 子菜单的渲染器
export type MenuItemRender = (
item: MenuDataItem & { isUrl: boolean; onClick: () => void },
defaultDom: VueNodeOrRender,
menuProps: BaseMenuProps & Partial<PrivateSiderMenuProps>
) => VueNode
// 菜单渲染器
export type MenuRender = (props: HeaderViewProps, defaultDom: VueNodeOrRender) => VueNode
// 菜单头部渲染器
export type MenuHeaderRender = (
props: SiderMenuProps,
logo: VueNodeOrRender,
title: VueNodeOrRender
) => VueNode
// 菜单底部渲染器
export type MenuFootRender = (props?: SiderMenuProps) => VueNode
// 菜单内容渲染器
export type MenuContentRender = (props: SiderMenuProps, defaultDom: VueNodeOrRender) => VueNode
// 菜单在 logo 和 content 之间的扩展区域渲染器,一般用来放搜索框
export type MenuExtraReander = (props: SiderMenuProps) => VueNode
// 菜单折叠按钮的渲染器
export type CollapsedButtonRender = (collapsed?: boolean) => VueNode
// 整体布局的底部渲染器
export type FooterRender = (props: HeaderViewProps, defaultDom: VNode) => VueNode

@ -0,0 +1,41 @@
import type { VueNodeOrRender } from '#/types'
export type WithFalse<T> = T | false
export type MenuDataItem = {
/** 子菜单 */
children?: MenuDataItem[]
/** 在菜单中隐藏子节点 */
hideChildrenInMenu?: boolean
/** 在菜单中隐藏自己和子节点 */
hideInMenu?: boolean
/** 菜单的icon */
icon?: VueNodeOrRender
/** 自定义菜单的国际化 key */
locale?: string | false
/** 菜单的名字 */
name?: string
/** 用于标定选中的值,默认是 path */
key?: string
/** disable 菜单选项 */
disabled?: boolean
/** 路径,可以设定为网页链接 */
path?: string
/**
* parentKeys
*
*
*/
parentKeys?: string[]
/** 隐藏自己,并且将子节点提升到与自己平级 */
flatMenu?: boolean
/** 指定外链打开形式同a标签 */
target?: string
[key: string]: any
}
export type MessageDescriptor = {
id: any
description?: string
defaultMessage?: string
}

@ -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,254 @@
import arEG from './locale/ar_EG'
import zhCN from './locale/zh_CN'
import enUS from './locale/en_US'
import enGB from './locale/en_GB'
import viVN from './locale/vi_VN'
import itIT from './locale/it_IT'
import esES from './locale/es_ES'
import caES from './locale/ca_ES'
import jaJP from './locale/ja_JP'
import ruRU from './locale/ru_RU'
import srRS from './locale/sr_RS'
import msMY from './locale/ms_MY'
import zhTW from './locale/zh_TW'
import frFR from './locale/fr_FR'
import ptBR from './locale/pt_BR'
import koKR from './locale/ko_KR'
import idID from './locale/id_ID'
import deDE from './locale/de_DE'
import faIR from './locale/fa_IR'
import trTR from './locale/tr_TR'
import plPL from './locale/pl_PL'
import hrHR from './locale/hr_HR'
import type { VueKey, VueNode } from '#/types'
export type ProSchemaValueEnumType = {
/** @name 演示的文案 */
text: VueNode
/** @name 预定的颜色 */
status?: string
/** @name 自定义的颜色 */
color?: string
/** @name 是否禁用 */
disabled?: boolean
}
/**
* Map Object
*
* @name ValueEnum
*/
export type ProSchemaValueEnumMap = Map<VueKey, ProSchemaValueEnumType | VueNode>
export type ProSchemaValueEnumObj = Record<string, ProSchemaValueEnumType | VueNode>
export type BaseProFieldFC = {
/** 值的类型 */
text: VueNode
/** 放置到组件上 props */
fieldProps?: any
/** 模式类型 */
mode: ProFieldFCMode
/** 简约模式 */
plain?: boolean
/** 轻量模式 */
light?: boolean
/** Label */
label?: VueNode
/** 映射值的类型 */
valueEnum?: ProSchemaValueEnumObj | ProSchemaValueEnumMap
/** 唯一的key用于网络请求 */
proFieldKey?: VueKey
}
export type ProFieldFCMode = 'read' | 'edit' | 'update'
/** Render 第二个参数,里面包含了一些常用的参数 */
export type ProFieldFCRenderProps = {
mode?: ProFieldFCMode
readonly?: boolean
placeholder?: string | string[]
value?: any
onChange?: (...rest: any[]) => void
} & BaseProFieldFC
export type ProRenderFieldPropsType = {
render?:
| ((
text: any,
props: Omit<ProFieldFCRenderProps, 'value' | 'onChange'>,
dom: JSX.Element
) => JSX.Element)
| undefined
renderFormItem?:
| ((text: any, props: ProFieldFCRenderProps, dom: JSX.Element) => JSX.Element)
| undefined
}
export type IntlType = {
locale: string
getMessage: (id: string, defaultMessage: string) => string
}
function get(
source: Record<string, unknown>,
path: string,
defaultValue?: string
): string | undefined {
// a[3].b -> a.3.b
const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.')
let result = source
let message = defaultValue
// eslint-disable-next-line no-restricted-syntax
for (const p of paths) {
message = Object(result)[p]
result = Object(result)[p]
if (message === undefined) {
return defaultValue
}
}
return message
}
/**
*
*
* @param locale
* @param localeMap
*/
export const createIntl = (locale: string, localeMap: Record<string, any>): IntlType => ({
getMessage: (id: string, defaultMessage: string) =>
get(localeMap, id, defaultMessage) || defaultMessage,
locale
})
const arEGIntl = createIntl('ar_EG', arEG)
const zhCNIntl = createIntl('zh_CN', zhCN)
const enUSIntl = createIntl('en_US', enUS)
const enGBIntl = createIntl('en_GB', enGB)
const viVNIntl = createIntl('vi_VN', viVN)
const itITIntl = createIntl('it_IT', itIT)
const jaJPIntl = createIntl('ja_JP', jaJP)
const esESIntl = createIntl('es_ES', esES)
const caESIntl = createIntl('ca_ES', caES)
const ruRUIntl = createIntl('ru_RU', ruRU)
const srRSIntl = createIntl('sr_RS', srRS)
const msMYIntl = createIntl('ms_MY', msMY)
const zhTWIntl = createIntl('zh_TW', zhTW)
const frFRIntl = createIntl('fr_FR', frFR)
const ptBRIntl = createIntl('pt_BR', ptBR)
const koKRIntl = createIntl('ko_KR', koKR)
const idIDIntl = createIntl('id_ID', idID)
const deDEIntl = createIntl('de_DE', deDE)
const faIRIntl = createIntl('fa_IR', faIR)
const trTRIntl = createIntl('tr_TR', trTR)
const plPLIntl = createIntl('pl_PL', plPL)
const hrHRIntl = createIntl('hr_', hrHR)
const intlMap = {
'ar-EG': arEGIntl,
'zh-CN': zhCNIntl,
'en-US': enUSIntl,
'en-GB': enGBIntl,
'vi-VN': viVNIntl,
'it-IT': itITIntl,
'ja-JP': jaJPIntl,
'es-ES': esESIntl,
'ca-ES': caESIntl,
'ru-RU': ruRUIntl,
'sr-RS': srRSIntl,
'ms-MY': msMYIntl,
'zh-TW': zhTWIntl,
'fr-FR': frFRIntl,
'pt-BR': ptBRIntl,
'ko-KR': koKRIntl,
'id-ID': idIDIntl,
'de-DE': deDEIntl,
'fa-IR': faIRIntl,
'tr-TR': trTRIntl,
'pl-PL': plPLIntl,
'hr-HR': hrHRIntl
}
const intlMapKeys = Object.keys(intlMap)
export type ParamsType = Record<string, any>
export {
arEGIntl,
enUSIntl,
enGBIntl,
zhCNIntl,
viVNIntl,
itITIntl,
jaJPIntl,
esESIntl,
caESIntl,
ruRUIntl,
srRSIntl,
msMYIntl,
zhTWIntl,
frFRIntl,
ptBRIntl,
koKRIntl,
idIDIntl,
deDEIntl,
faIRIntl,
trTRIntl,
plPLIntl,
hrHRIntl,
intlMap,
intlMapKeys
}
export type ConfigContextPropsType = {
intl: IntlType
valueTypeMap: Record<string, ProRenderFieldPropsType>
}
// const ConfigContext = React.createContext<ConfigContextPropsType>({
// intl: {
// ...zhCNIntl,
// locale: 'default'
// },
// valueTypeMap: {}
// })
//
// const { Consumer: ConfigConsumer, Provider: ConfigProvider } = ConfigContext
/**
* antd key locale key
*
* @param localeKey
*/
// const findIntlKeyByAntdLocaleKey = <T extends string | undefined>(localeKey: T) => {
// if (!localeKey) {
// return 'zh-CN' as T
// }
// const localeName = localeKey.toLocaleLowerCase()
// return intlMapKeys.find(intlKey => {
// const LowerCaseKey = intlKey.toLocaleLowerCase()
// return LowerCaseKey.includes(localeName)
// }) as T
// }
// export { ConfigConsumer, ConfigProvider, createIntl }
export function useIntl(): IntlType {
// const { locale } = useContext(AntdConfigProvider.ConfigContext)
// const { intl } = useContext(ConfigContext)
//
// if (intl && intl.locale !== 'default') {
// return intl
// }
//
// if (locale?.locale) {
// return intlMap[findIntlKeyByAntdLocaleKey(locale.locale)]
// }
return zhCNIntl
}
// export default ConfigContext

@ -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…
Cancel
Save