1035 changed files with 62340 additions and 60399 deletions
@ -1,8 +0,0 @@ |
|||
# port |
|||
VITE_PORT = 4200 |
|||
|
|||
# spa-title |
|||
VITE_GLOB_APP_TITLE = AbpPro |
|||
|
|||
# spa shortname |
|||
VITE_GLOB_APP_SHORT_NAME = AbpPro |
|||
@ -1,34 +0,0 @@ |
|||
# Whether to open mock |
|||
VITE_USE_MOCK = true |
|||
|
|||
# public path |
|||
VITE_PUBLIC_PATH = / |
|||
|
|||
# Cross-domain proxy, you can configure multiple |
|||
# Please note that no line breaks |
|||
VITE_PROXY = [["/basic-api","http://localhost:3000"],["/upload","http://localhost:3300/upload"]] |
|||
# VITE_PROXY=[["/api","https://vvbin.cn/test"]] |
|||
|
|||
# Delete console |
|||
VITE_DROP_CONSOLE = false |
|||
|
|||
# Basic interface address SPA |
|||
VITE_GLOB_API_URL=/basic-api |
|||
|
|||
# File upload address, optional |
|||
VITE_GLOB_UPLOAD_URL=/upload |
|||
|
|||
# Interface prefix |
|||
VITE_GLOB_API_URL_PREFIX= |
|||
|
|||
|
|||
VITE_BUILD_COMPRESS = 'none' |
|||
# 认证授权服务器地址 |
|||
VITE_AUTH_URL= http://localhost:44354 |
|||
|
|||
# 接口地址 |
|||
|
|||
VITE_API_URL= http://localhost:44314/gateway |
|||
|
|||
# WEBSOCKE 地址 |
|||
VITE_WEBSOCKE_URL= http://localhost:44314/ws |
|||
@ -1,47 +0,0 @@ |
|||
# Whether to open mock |
|||
VITE_USE_MOCK = true |
|||
|
|||
# public path |
|||
VITE_PUBLIC_PATH = / |
|||
|
|||
# Delete console |
|||
VITE_DROP_CONSOLE = true |
|||
|
|||
# Whether to enable gzip or brotli compression |
|||
# Optional: gzip | brotli | none |
|||
# If you need multiple forms, you can use `,` to separate |
|||
VITE_BUILD_COMPRESS = 'gzip' |
|||
|
|||
# Whether to delete origin files when using compress, default false |
|||
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false |
|||
|
|||
# Basic interface address SPA |
|||
VITE_GLOB_API_URL=/basic-api |
|||
|
|||
# File upload address, optional |
|||
# It can be forwarded by nginx or write the actual address directly |
|||
VITE_GLOB_UPLOAD_URL=/upload |
|||
|
|||
# Interface prefix |
|||
VITE_GLOB_API_URL_PREFIX= |
|||
|
|||
# Whether to enable image compression |
|||
VITE_USE_IMAGEMIN= true |
|||
|
|||
# use pwa |
|||
VITE_USE_PWA = false |
|||
|
|||
# Is it compatible with older browsers |
|||
VITE_LEGACY = false |
|||
|
|||
|
|||
# 认证授权服务器地址 |
|||
VITE_AUTH_URL= http://120.24.194.14:8013 |
|||
|
|||
# 接口地址 |
|||
|
|||
VITE_API_URL= http://120.24.194.14:8014/gateway |
|||
|
|||
|
|||
# WEBSOCKE 地址 |
|||
VITE_WEBSOCKE_URL= http://120.24.194.14:8014/ws |
|||
@ -1,78 +0,0 @@ |
|||
// @ts-check
|
|||
const { defineConfig } = require('eslint-define-config'); |
|||
module.exports = defineConfig({ |
|||
root: true, |
|||
env: { |
|||
browser: true, |
|||
node: true, |
|||
es6: true, |
|||
}, |
|||
parser: 'vue-eslint-parser', |
|||
parserOptions: { |
|||
parser: '@typescript-eslint/parser', |
|||
ecmaVersion: 2020, |
|||
sourceType: 'module', |
|||
jsxPragma: 'React', |
|||
ecmaFeatures: { |
|||
jsx: true, |
|||
}, |
|||
}, |
|||
extends: [ |
|||
'plugin:vue/vue3-recommended', |
|||
'plugin:@typescript-eslint/recommended', |
|||
'prettier', |
|||
'plugin:prettier/recommended', |
|||
'plugin:jest/recommended', |
|||
], |
|||
rules: { |
|||
'vue/script-setup-uses-vars': 'error', |
|||
'@typescript-eslint/ban-ts-ignore': 'off', |
|||
'@typescript-eslint/explicit-function-return-type': 'off', |
|||
'@typescript-eslint/no-explicit-any': 'off', |
|||
'@typescript-eslint/no-var-requires': 'off', |
|||
'@typescript-eslint/no-empty-function': 'off', |
|||
'vue/custom-event-name-casing': 'off', |
|||
'no-use-before-define': 'off', |
|||
'@typescript-eslint/no-use-before-define': 'off', |
|||
'@typescript-eslint/ban-ts-comment': 'off', |
|||
'@typescript-eslint/ban-types': 'off', |
|||
'@typescript-eslint/no-non-null-assertion': 'off', |
|||
'@typescript-eslint/explicit-module-boundary-types': 'off', |
|||
'@typescript-eslint/no-unused-vars': [ |
|||
'error', |
|||
{ |
|||
argsIgnorePattern: '^_', |
|||
varsIgnorePattern: '^_', |
|||
}, |
|||
], |
|||
'no-unused-vars': [ |
|||
'error', |
|||
{ |
|||
argsIgnorePattern: '^_', |
|||
varsIgnorePattern: '^_', |
|||
}, |
|||
], |
|||
'space-before-function-paren': 'off', |
|||
|
|||
'vue/attributes-order': 'off', |
|||
'vue/one-component-per-file': 'off', |
|||
'vue/html-closing-bracket-newline': 'off', |
|||
'vue/max-attributes-per-line': 'off', |
|||
'vue/multiline-html-element-content-newline': 'off', |
|||
'vue/singleline-html-element-content-newline': 'off', |
|||
'vue/attribute-hyphenation': 'off', |
|||
'vue/require-default-prop': 'off', |
|||
'vue/html-self-closing': [ |
|||
'error', |
|||
{ |
|||
html: { |
|||
void: 'always', |
|||
normal: 'never', |
|||
component: 'always', |
|||
}, |
|||
svg: 'always', |
|||
math: 'always', |
|||
}, |
|||
], |
|||
}, |
|||
}); |
|||
@ -1,29 +0,0 @@ |
|||
node_modules |
|||
.DS_Store |
|||
dist |
|||
.npmrc |
|||
.cache |
|||
|
|||
tests/server/static |
|||
tests/server/static/upload |
|||
|
|||
.local |
|||
# local env files |
|||
.env.local |
|||
.env.*.local |
|||
.eslintcache |
|||
|
|||
# Log files |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
pnpm-debug.log* |
|||
|
|||
# Editor directories and files |
|||
.idea |
|||
# .vscode |
|||
*.suo |
|||
*.ntvs* |
|||
*.njsproj |
|||
*.sln |
|||
*.sw? |
|||
@ -1,48 +0,0 @@ |
|||
# test directories |
|||
__tests__ |
|||
test |
|||
tests |
|||
powered-test |
|||
|
|||
# asset directories |
|||
docs |
|||
doc |
|||
website |
|||
images |
|||
assets |
|||
|
|||
# examples |
|||
example |
|||
examples |
|||
|
|||
# code coverage directories |
|||
coverage |
|||
.nyc_output |
|||
|
|||
# build scripts |
|||
Makefile |
|||
Gulpfile.js |
|||
Gruntfile.js |
|||
|
|||
# configs |
|||
appveyor.yml |
|||
circle.yml |
|||
codeship-services.yml |
|||
codeship-steps.yml |
|||
wercker.yml |
|||
.tern-project |
|||
.gitattributes |
|||
.editorconfig |
|||
.*ignore |
|||
.eslintrc |
|||
.jshintrc |
|||
.flowconfig |
|||
.documentup.json |
|||
.yarn-metadata.json |
|||
.travis.yml |
|||
|
|||
# misc |
|||
*.md |
|||
|
|||
!istanbul-reports/lib/html/assets |
|||
!istanbul-api/node_modules/istanbul-reports/lib/html/assets |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,9 +0,0 @@ |
|||
FROM nginx:1.17.3-alpine as base |
|||
EXPOSE 80 |
|||
|
|||
COPY /_nginx/nginx.conf /etc/nginx/nginx.conf |
|||
COPY /_nginx/env.js /etc/nginx/env.js |
|||
COPY /_nginx/default.conf /etc/nginx/conf.d/default.conf |
|||
COPY /dist/ /usr/share/nginx/html |
|||
|
|||
CMD ["nginx", "-g", "daemon off;"] |
|||
@ -1,169 +0,0 @@ |
|||
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="200" height="200" src="https://anncwb.github.io/anncwb/images/logo.png"> </a> <br> <br> |
|||
|
|||
[](LICENSE) |
|||
|
|||
<h1>Vue vben admin</h1> |
|||
</div> |
|||
|
|||
**English** | [中文](./README.zh-CN.md) |
|||
|
|||
## Introduction |
|||
|
|||
Vue Vben Admin is a free and open source middle and back-end template. Using the latest `vue3`, `vite2`, `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference. |
|||
|
|||
## Feature |
|||
|
|||
- **State of The Art Development**:Use front-end front-end technology development such as Vue3/vite2 |
|||
- **TypeScript**: Application-level JavaScript language |
|||
- **Theming**: Configurable themes |
|||
- **International**:Built-in complete internationalization program |
|||
- **Mock Server** Built-in mock data scheme |
|||
- **Authority** Built-in complete dynamic routing permission generation scheme. |
|||
- **Component** Multiple commonly used components are encapsulated twice |
|||
|
|||
## 预览 |
|||
|
|||
- [vue-vben-admin](https://vvbin.cn/next/) - Full version Chinese site |
|||
- [vue-vben-admin-gh-pages](https://anncwb.github.io/vue-vben-admin/) - Full version of the github site |
|||
- [vben-admin-thin-next](https://vvbin.cn/thin/next/) - Simplified Chinese site |
|||
- [vben-admin-thin-gh-pages](https://anncwb.github.io/vben-admin-thin-next/) -Simplified github site |
|||
|
|||
Test account: vben/123456 |
|||
|
|||
<p align="center"> |
|||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png"> |
|||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png"> |
|||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png"> |
|||
</p> |
|||
|
|||
### Use Gitpod |
|||
|
|||
Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately. |
|||
|
|||
[](https://gitpod.io/#https://github.com/anncwb/vue-vben-admin) |
|||
|
|||
## Documentation |
|||
|
|||
[Document](https://vvbin.cn/doc-next/) |
|||
|
|||
## Preparation |
|||
|
|||
- [node](http://nodejs.org/) and [git](https://git-scm.com/) - Project development environment |
|||
- [Vite](https://vitejs.dev/) - Familiar with vite features |
|||
- [Vue3](https://v3.vuejs.org/) - Familiar with Vue basic syntax |
|||
- [TypeScript](https://www.typescriptlang.org/) - Familiar with the basic syntax of `TypeScript` |
|||
- [Es6+](http://es6.ruanyifeng.com/) - Familiar with es6 basic syntax |
|||
- [Vue-Router-Next](https://next.router.vuejs.org/) - Familiar with the basic use of vue-router |
|||
- [Ant-Design-Vue](https://2x.antdv.com/docs/vue/introduce-cn/) - ui basic use |
|||
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs basic syntax |
|||
|
|||
## Install and use |
|||
|
|||
- Get the project code |
|||
|
|||
```bash |
|||
git clone https://github.com/anncwb/vue-vben-admin.git |
|||
``` |
|||
|
|||
- Installation dependencies |
|||
|
|||
```bash |
|||
cd vue-vben-admin |
|||
|
|||
yarn install |
|||
|
|||
``` |
|||
|
|||
- run |
|||
|
|||
```bash |
|||
yarn serve |
|||
``` |
|||
|
|||
- build |
|||
|
|||
```bash |
|||
yarn build |
|||
``` |
|||
|
|||
## Change Log |
|||
|
|||
[CHANGELOG](./CHANGELOG.zh_CN.md) |
|||
|
|||
## Project |
|||
|
|||
- [vue-vben-admin](https://github.com/anncwb/vue-vben-admin) - full version |
|||
- [vue-vben-admin-thin-next](https://github.com/anncwb/vben-admin-thin-next) - Simplified version |
|||
|
|||
## How to contribute |
|||
|
|||
You are very welcome to join Or submit a Pull Request。 |
|||
|
|||
**Pull Request:** |
|||
|
|||
1. Fork code! |
|||
2. Create your own branch: `git checkout -b feat/xxxx` |
|||
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'` |
|||
4. Push your branch: `git push origin feat/xxxx` |
|||
5. submit`pull request` |
|||
|
|||
## Git Contribution submission specification |
|||
|
|||
- reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) |
|||
|
|||
- `feat` Add new features |
|||
- `fix` Fix the problem/BUG |
|||
- `style` The code style is related and does not affect the running result |
|||
- `perf` Optimization/performance improvement |
|||
- `refactor` Refactor |
|||
- `revert` Undo edit |
|||
- `test` Test related |
|||
- `docs` Documentation/notes |
|||
- `chore` Dependency update/scaffolding configuration modification etc. |
|||
- `workflow` Workflow improvements |
|||
- `ci` Continuous integration |
|||
- `types` Type definition file changes |
|||
- `wip` In development |
|||
|
|||
## Related warehouse |
|||
|
|||
If these plugins are helpful to you, you can give a star support |
|||
|
|||
- [vite-plugin-mock](https://github.com/anncwb/vite-plugin-mock) - Used for local and development environment data mock |
|||
- [vite-plugin-html](https://github.com/anncwb/vite-plugin-html) - Used for html template conversion and compression |
|||
- [vite-plugin-style-import](https://github.com/anncwb/vite-plugin-style-import) - Used for component library style introduction on demand |
|||
- [vite-plugin-theme](https://github.com/anncwb/vite-plugin-theme) - Used for online switching of theme colors and other color-related configurations |
|||
- [vite-plugin-imagemin](https://github.com/anncwb/vite-plugin-imagemin) - Used to pack compressed image resources |
|||
- [vite-plugin-compression](https://github.com/anncwb/vite-plugin-compression) - Used to pack input .gz|.brotil files |
|||
- [vite-plugin-svg-icons](https://github.com/anncwb/vite-plugin-svg-icons) - Used to quickly generate svg sprite |
|||
|
|||
## Browser support |
|||
|
|||
The `Chrome 80+` browser is recommended for local development |
|||
|
|||
Support modern browsers, not IE |
|||
|
|||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | |
|||
| :-: | :-: | :-: | :-: | :-: | |
|||
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | |
|||
|
|||
## Maintainer |
|||
|
|||
[@Vben](https://github.com/anncwb) |
|||
|
|||
## Donate |
|||
|
|||
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support! |
|||
|
|||
 |
|||
|
|||
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a> |
|||
|
|||
## Discord |
|||
|
|||
- [github discussions](https://github.com/anncwb/vue-vben-admin/discussions) |
|||
- [Discord](https://discord.gg/8GuAdwDhj6) |
|||
|
|||
## License |
|||
|
|||
[MIT © Vben-2020](./LICENSE) |
|||
@ -1,174 +0,0 @@ |
|||
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="200" height="200" src="https://anncwb.github.io/anncwb/images/logo.png"> </a> <br> <br> |
|||
|
|||
[](LICENSE) |
|||
|
|||
<h1>Vue vben admin</h1> |
|||
</div> |
|||
|
|||
**中文** | [English](./README.md) |
|||
|
|||
## 简介 |
|||
|
|||
Vue Vben Admin 是一个免费开源的中后台模版。使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。 |
|||
|
|||
## 特性 |
|||
|
|||
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发 |
|||
- **TypeScript**: 应用程序级 JavaScript 的语言 |
|||
- **主题**:可配置的主题 |
|||
- **国际化**:内置完善的国际化方案 |
|||
- **Mock 数据** 内置 Mock 数据方案 |
|||
- **权限** 内置完善的动态路由权限生成方案 |
|||
- **组件** 二次封装了多个常用的组件 |
|||
|
|||
## 预览 |
|||
|
|||
- [vue-vben-admin](https://vvbin.cn/next/) - 完整版中文站点 |
|||
- [vue-vben-admin-gh-pages](https://anncwb.github.io/vue-vben-admin/) - 完整版 github 站点 |
|||
- [vben-admin-thin-next](https://vvbin.cn/thin/next/) - 简化版中文站点 |
|||
- [vben-admin-thin-gh-pages](https://anncwb.github.io/vben-admin-thin-next/) - 简化版 github 站点 |
|||
|
|||
测试账号: vben/123456 |
|||
|
|||
<p align="center"> |
|||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png"> |
|||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png"> |
|||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png"> |
|||
</p> |
|||
|
|||
### 使用 Gitpod |
|||
|
|||
在 Gitpod(适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码. |
|||
|
|||
[](https://gitpod.io/#https://github.com/anncwb/vue-vben-admin) |
|||
|
|||
## 文档 |
|||
|
|||
[文档地址](https://vvbin.cn/doc-next/) |
|||
|
|||
## 准备 |
|||
|
|||
- [node](http://nodejs.org/) 和 [git](https://git-scm.com/) -项目开发环境 |
|||
- [Vite](https://vitejs.dev/) - 熟悉 vite 特性 |
|||
- [Vue3](https://v3.vuejs.org/) - 熟悉 Vue 基础语法 |
|||
- [TypeScript](https://www.typescriptlang.org/) - 熟悉`TypeScript`基本语法 |
|||
- [Es6+](http://es6.ruanyifeng.com/) - 熟悉 es6 基本语法 |
|||
- [Vue-Router-Next](https://next.router.vuejs.org/) - 熟悉 vue-router 基本使用 |
|||
- [Ant-Design-Vue](https://2x.antdv.com/docs/vue/introduce-cn/) - ui 基本使用 |
|||
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法 |
|||
|
|||
## 安装使用 |
|||
|
|||
- 获取项目代码 |
|||
|
|||
```bash |
|||
git clone https://github.com/anncwb/vue-vben-admin.git |
|||
``` |
|||
|
|||
- 安装依赖 |
|||
|
|||
```bash |
|||
cd vue-vben-admin |
|||
|
|||
yarn install |
|||
|
|||
``` |
|||
|
|||
- 运行 |
|||
|
|||
```bash |
|||
yarn serve |
|||
``` |
|||
|
|||
- 打包 |
|||
|
|||
```bash |
|||
yarn build |
|||
``` |
|||
|
|||
## 更新日志 |
|||
|
|||
[CHANGELOG](./CHANGELOG.zh_CN.md) |
|||
|
|||
## 项目地址 |
|||
|
|||
- [vue-vben-admin](https://github.com/anncwb/vue-vben-admin) - 完整版 |
|||
- [vue-vben-admin-thin-next](https://github.com/anncwb/vben-admin-thin-next) - 简化版 |
|||
|
|||
## 如何贡献 |
|||
|
|||
非常欢迎你的加入 或者提交一个 Pull Request。 |
|||
|
|||
**Pull Request:** |
|||
|
|||
1. Fork 代码! |
|||
2. 创建自己的分支: `git checkout -b feat/xxxx` |
|||
3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'` |
|||
4. 推送您的分支: `git push origin feat/xxxx` |
|||
5. 提交`pull request` |
|||
|
|||
## Git 贡献提交规范 |
|||
|
|||
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) |
|||
|
|||
- `feat` 增加新功能 |
|||
- `fix` 修复问题/BUG |
|||
- `style` 代码风格相关无影响运行结果的 |
|||
- `perf` 优化/性能提升 |
|||
- `refactor` 重构 |
|||
- `revert` 撤销修改 |
|||
- `test` 测试相关 |
|||
- `docs` 文档/注释 |
|||
- `chore` 依赖更新/脚手架配置修改等 |
|||
- `workflow` 工作流改进 |
|||
- `ci` 持续集成 |
|||
- `types` 类型定义文件更改 |
|||
- `wip` 开发中 |
|||
|
|||
## 浏览器支持 |
|||
|
|||
本地开发推荐使用`Chrome 80+` 浏览器 |
|||
|
|||
支持现代浏览器, 不支持 IE |
|||
|
|||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | |
|||
| :-: | :-: | :-: | :-: | :-: | |
|||
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | |
|||
|
|||
## 相关仓库 |
|||
|
|||
如果这些插件对你有帮助,可以给一个 star 支持下 |
|||
|
|||
- [vite-plugin-mock](https://github.com/anncwb/vite-plugin-mock) - 用于本地及开发环境数据 mock |
|||
- [vite-plugin-html](https://github.com/anncwb/vite-plugin-html) - 用于 html 模版转换及压缩 |
|||
- [vite-plugin-style-import](https://github.com/anncwb/vite-plugin-style-import) - 用于组件库样式按需引入 |
|||
- [vite-plugin-theme](https://github.com/anncwb/vite-plugin-theme) - 用于在线切换主题色等颜色相关配置 |
|||
- [vite-plugin-imagemin](https://github.com/anncwb/vite-plugin-imagemin) - 用于打包压缩图片资源 |
|||
- [vite-plugin-compression](https://github.com/anncwb/vite-plugin-compression) - 用于打包输出.gz|.brotil 文件 |
|||
- [vite-plugin-svg-icons](https://github.com/anncwb/vite-plugin-svg-icons) - 用于快速生成 svg 雪碧图 |
|||
|
|||
## 后台整合示例 |
|||
|
|||
- [lamp-cloud](https://github.com/zuihou/lamp-cloud) - 基于 SpringCloud Alibaba 的微服务中后台快速开发平台 |
|||
|
|||
## 维护者 |
|||
|
|||
[@Vben](https://github.com/anncwb) |
|||
|
|||
## 捐赠 |
|||
|
|||
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持! |
|||
|
|||
 |
|||
|
|||
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a> |
|||
|
|||
## 交流 |
|||
|
|||
`Vue-vben-Admin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供 QQ 交流群使用问题欢迎在群内提问。 |
|||
|
|||
- QQ 群 `569291866` |
|||
|
|||
## License |
|||
|
|||
[MIT © Vben-2020](./LICENSE) |
|||
@ -1,72 +0,0 @@ |
|||
import path from 'path'; |
|||
import fs from 'fs-extra'; |
|||
import inquirer from 'inquirer'; |
|||
import chalk from 'chalk'; |
|||
import pkg from '../../../package.json'; |
|||
|
|||
async function generateIcon() { |
|||
const dir = path.resolve(process.cwd(), 'node_modules/@iconify/json'); |
|||
|
|||
const raw = await fs.readJSON(path.join(dir, 'collections.json')); |
|||
|
|||
const collections = Object.entries(raw).map(([id, v]) => ({ |
|||
...(v as any), |
|||
id, |
|||
})); |
|||
|
|||
const choices = collections.map((item) => ({ key: item.id, value: item.id, name: item.name })); |
|||
|
|||
inquirer |
|||
.prompt([ |
|||
{ |
|||
type: 'list', |
|||
name: 'useType', |
|||
choices: [ |
|||
{ key: 'local', value: 'local', name: 'Local' }, |
|||
{ key: 'onLine', value: 'onLine', name: 'OnLine' }, |
|||
], |
|||
message: 'How to use icons?', |
|||
}, |
|||
{ |
|||
type: 'list', |
|||
name: 'iconSet', |
|||
choices: choices, |
|||
message: 'Select the icon set that needs to be generated?', |
|||
}, |
|||
{ |
|||
type: 'input', |
|||
name: 'output', |
|||
message: 'Select the icon set that needs to be generated?', |
|||
default: 'src/components/Icon/data', |
|||
}, |
|||
]) |
|||
.then(async (answers) => { |
|||
const { iconSet, output, useType } = answers; |
|||
const outputDir = path.resolve(process.cwd(), output); |
|||
fs.ensureDir(outputDir); |
|||
const genCollections = collections.filter((item) => [iconSet].includes(item.id)); |
|||
const prefixSet: string[] = []; |
|||
for (const info of genCollections) { |
|||
const data = await fs.readJSON(path.join(dir, 'json', `${info.id}.json`)); |
|||
if (data) { |
|||
const { prefix } = data; |
|||
const isLocal = useType === 'local'; |
|||
const icons = Object.keys(data.icons).map( |
|||
(item) => `${isLocal ? prefix + ':' : ''}${item}` |
|||
); |
|||
|
|||
await fs.writeFileSync( |
|||
path.join(output, `icons.data.ts`), |
|||
`export default ${isLocal ? JSON.stringify(icons) : JSON.stringify({ prefix, icons })}` |
|||
); |
|||
prefixSet.push(prefix); |
|||
} |
|||
} |
|||
fs.emptyDir(path.join(process.cwd(), 'node_modules/.vite')); |
|||
console.log( |
|||
`✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - Icon generated successfully:' + `[${prefixSet}]` |
|||
); |
|||
}); |
|||
} |
|||
|
|||
generateIcon(); |
|||
@ -1,44 +0,0 @@ |
|||
/** |
|||
* Generate additional configuration files when used for packaging. The file can be configured with some global variables, so that it can be changed directly externally without repackaging |
|||
*/ |
|||
import { GLOB_CONFIG_FILE_NAME, OUTPUT_DIR } from '../constant'; |
|||
import fs, { writeFileSync } from 'fs-extra'; |
|||
import chalk from 'chalk'; |
|||
|
|||
import { getRootPath, getEnvConfig } from '../utils'; |
|||
import { getConfigFileName } from '../getConfigFileName'; |
|||
|
|||
import pkg from '../../package.json'; |
|||
|
|||
function createConfig( |
|||
{ |
|||
configName, |
|||
config, |
|||
configFileName = GLOB_CONFIG_FILE_NAME, |
|||
}: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} } |
|||
) { |
|||
try { |
|||
const windowConf = `window.${configName}`; |
|||
// Ensure that the variable will not be modified
|
|||
const configStr = `${windowConf}=${JSON.stringify(config)};
|
|||
Object.freeze(${windowConf}); |
|||
Object.defineProperty(window, "${configName}", { |
|||
configurable: false, |
|||
writable: false, |
|||
}); |
|||
`.replace(/\s/g, '');
|
|||
fs.mkdirp(getRootPath(OUTPUT_DIR)); |
|||
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr); |
|||
|
|||
console.log(chalk.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`); |
|||
console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n'); |
|||
} catch (error) { |
|||
console.log(chalk.red('configuration file configuration file failed to package:\n' + error)); |
|||
} |
|||
} |
|||
|
|||
export function runBuildConfig() { |
|||
const config = getEnvConfig(); |
|||
const configFileName = getConfigFileName(config); |
|||
createConfig({ config, configName: configFileName }); |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
// #!/usr/bin/env node
|
|||
|
|||
import { runBuildConfig } from './buildConf'; |
|||
import chalk from 'chalk'; |
|||
|
|||
import pkg from '../../package.json'; |
|||
|
|||
export const runBuild = async () => { |
|||
try { |
|||
const argvList = process.argv.splice(2); |
|||
|
|||
// Generate configuration file
|
|||
if (!argvList.includes('disabled-config')) { |
|||
runBuildConfig(); |
|||
} |
|||
|
|||
console.log(`✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - build successfully!'); |
|||
} catch (error) { |
|||
console.log(chalk.red('vite build error:\n' + error)); |
|||
process.exit(1); |
|||
} |
|||
}; |
|||
runBuild(); |
|||
@ -1,92 +0,0 @@ |
|||
import fs from 'fs'; |
|||
import path from 'path'; |
|||
import dotenv from 'dotenv'; |
|||
|
|||
export function isDevFn(mode: string): boolean { |
|||
return mode === 'development'; |
|||
} |
|||
|
|||
export function isProdFn(mode: string): boolean { |
|||
return mode === 'production'; |
|||
} |
|||
|
|||
/** |
|||
* Whether to generate package preview |
|||
*/ |
|||
export function isReportMode(): boolean { |
|||
return process.env.REPORT === 'true'; |
|||
} |
|||
|
|||
// Read all environment variable configuration files to process.env
|
|||
export function wrapperEnv(envConf: Recordable): ViteEnv { |
|||
const ret: any = {}; |
|||
|
|||
for (const envName of Object.keys(envConf)) { |
|||
let realName = envConf[envName].replace(/\\n/g, '\n'); |
|||
realName = realName === 'true' ? true : realName === 'false' ? false : realName; |
|||
|
|||
if (envName === 'VITE_PORT') { |
|||
realName = Number(realName); |
|||
} |
|||
if (envName === 'VITE_PROXY') { |
|||
try { |
|||
realName = JSON.parse(realName); |
|||
} catch (error) { |
|||
realName = ''; |
|||
} |
|||
} |
|||
ret[envName] = realName; |
|||
if (typeof realName === 'string') { |
|||
process.env[envName] = realName; |
|||
} else if (typeof realName === 'object') { |
|||
process.env[envName] = JSON.stringify(realName); |
|||
} |
|||
} |
|||
return ret; |
|||
} |
|||
|
|||
/** |
|||
* 获取当前环境下生效的配置文件名 |
|||
*/ |
|||
function getConfFiles() { |
|||
const script = process.env.npm_lifecycle_script; |
|||
const reg = new RegExp('--mode ([a-z]+)'); |
|||
const result = reg.exec(script as string) as any; |
|||
if (result) { |
|||
const mode = result[1] as string; |
|||
return ['.env', `.env.${mode}`]; |
|||
} |
|||
return ['.env', '.env.production']; |
|||
} |
|||
|
|||
/** |
|||
* Get the environment variables starting with the specified prefix |
|||
* @param match prefix |
|||
* @param confFiles ext |
|||
*/ |
|||
export function getEnvConfig(match = 'VITE_GLOB_', confFiles = getConfFiles()) { |
|||
let envConfig = {}; |
|||
confFiles.forEach((item) => { |
|||
try { |
|||
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item))); |
|||
envConfig = { ...envConfig, ...env }; |
|||
} catch (e) { |
|||
console.error(`Error in parsing ${item}`, e); |
|||
} |
|||
}); |
|||
const reg = new RegExp(`^(${match})`); |
|||
Object.keys(envConfig).forEach((key) => { |
|||
if (!reg.test(key)) { |
|||
Reflect.deleteProperty(envConfig, key); |
|||
} |
|||
}); |
|||
return envConfig; |
|||
} |
|||
|
|||
/** |
|||
* Get user root directory |
|||
* @param dir file path |
|||
*/ |
|||
export function getRootPath(...dir: string[]) { |
|||
return path.resolve(process.cwd(), ...dir); |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
// TODO
|
|||
import type { GetManualChunk } from 'rollup'; |
|||
|
|||
//
|
|||
const vendorLibs: { match: string[]; output: string }[] = [ |
|||
// {
|
|||
// match: ['xlsx'],
|
|||
// output: 'xlsx',
|
|||
// },
|
|||
]; |
|||
|
|||
// @ts-ignore
|
|||
export const configManualChunk: GetManualChunk = (id: string) => { |
|||
if (/[\\/]node_modules[\\/]/.test(id)) { |
|||
const matchItem = vendorLibs.find((item) => { |
|||
const reg = new RegExp(`[\\/]node_modules[\\/]_?(${item.match.join('|')})(.*)`, 'ig'); |
|||
return reg.test(id); |
|||
}); |
|||
return matchItem ? matchItem.output : null; |
|||
} |
|||
}; |
|||
@ -1,35 +0,0 @@ |
|||
/** |
|||
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated |
|||
* https://github.com/anncwb/vite-plugin-compression
|
|||
*/ |
|||
import type { Plugin } from 'vite'; |
|||
|
|||
import compressPlugin from 'vite-plugin-compression'; |
|||
|
|||
export function configCompressPlugin( |
|||
compress: 'gzip' | 'brotli' | 'none', |
|||
deleteOriginFile = false |
|||
): Plugin | Plugin[] { |
|||
const compressList = compress.split(','); |
|||
|
|||
const plugins: Plugin[] = []; |
|||
|
|||
if (compressList.includes('gzip')) { |
|||
plugins.push( |
|||
compressPlugin({ |
|||
ext: '.gz', |
|||
deleteOriginFile, |
|||
}) |
|||
); |
|||
} |
|||
if (compressList.includes('brotli')) { |
|||
plugins.push( |
|||
compressPlugin({ |
|||
ext: '.br', |
|||
algorithm: 'brotliCompress', |
|||
deleteOriginFile, |
|||
}) |
|||
); |
|||
} |
|||
return plugins; |
|||
} |
|||
@ -1,25 +0,0 @@ |
|||
import type { Plugin } from 'vite'; |
|||
|
|||
/** |
|||
* TODO |
|||
* Temporarily solve the Vite circular dependency problem, and wait for a better solution to fix it later. I don't know what problems this writing will bring. |
|||
* @returns |
|||
*/ |
|||
|
|||
export function configHmrPlugin(): Plugin { |
|||
return { |
|||
name: 'singleHMR', |
|||
handleHotUpdate({ modules, file }) { |
|||
if (file.match(/xml$/)) return []; |
|||
|
|||
modules.forEach((m) => { |
|||
if (!m.url.match(/\.(css|less)/)) { |
|||
m.importedModules = new Set(); |
|||
m.importers = new Set(); |
|||
} |
|||
}); |
|||
|
|||
return modules; |
|||
}, |
|||
}; |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
/** |
|||
* Plugin to minimize and use ejs template syntax in index.html. |
|||
* https://github.com/anncwb/vite-plugin-html
|
|||
*/ |
|||
import type { Plugin } from 'vite'; |
|||
|
|||
import html from 'vite-plugin-html'; |
|||
|
|||
import pkg from '../../../package.json'; |
|||
import { GLOB_CONFIG_FILE_NAME } from '../../constant'; |
|||
|
|||
export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) { |
|||
const { VITE_GLOB_APP_TITLE, VITE_PUBLIC_PATH } = env; |
|||
|
|||
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`; |
|||
|
|||
const getAppConfigSrc = () => { |
|||
return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`; |
|||
}; |
|||
|
|||
const htmlPlugin: Plugin[] = html({ |
|||
minify: isBuild, |
|||
inject: { |
|||
// Inject data into ejs template
|
|||
injectData: { |
|||
title: VITE_GLOB_APP_TITLE, |
|||
}, |
|||
// Embed the generated app.config.js file
|
|||
tags: isBuild |
|||
? [ |
|||
{ |
|||
tag: 'script', |
|||
attrs: { |
|||
src: getAppConfigSrc(), |
|||
}, |
|||
}, |
|||
] |
|||
: [], |
|||
}, |
|||
}); |
|||
return htmlPlugin; |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
// Image resource files used to compress the output of the production environment
|
|||
// https://github.com/anncwb/vite-plugin-imagemin
|
|||
|
|||
import viteImagemin from 'vite-plugin-imagemin'; |
|||
|
|||
export function configImageminPlugin() { |
|||
const plugin = viteImagemin({ |
|||
gifsicle: { |
|||
optimizationLevel: 7, |
|||
interlaced: false, |
|||
}, |
|||
optipng: { |
|||
optimizationLevel: 7, |
|||
}, |
|||
mozjpeg: { |
|||
quality: 20, |
|||
}, |
|||
pngquant: { |
|||
quality: [0.8, 0.9], |
|||
speed: 4, |
|||
}, |
|||
svgo: { |
|||
plugins: [ |
|||
{ |
|||
name: 'removeViewBox', |
|||
}, |
|||
{ |
|||
name: 'removeEmptyAttrs', |
|||
active: false, |
|||
}, |
|||
], |
|||
}, |
|||
}); |
|||
return plugin; |
|||
} |
|||
@ -1,81 +0,0 @@ |
|||
import type { Plugin } from 'vite'; |
|||
|
|||
import vue from '@vitejs/plugin-vue'; |
|||
import vueJsx from '@vitejs/plugin-vue-jsx'; |
|||
import legacy from '@vitejs/plugin-legacy'; |
|||
|
|||
import purgeIcons from 'vite-plugin-purge-icons'; |
|||
import windiCSS from 'vite-plugin-windicss'; |
|||
import { configHtmlPlugin } from './html'; |
|||
import { configPwaConfig } from './pwa'; |
|||
import { configMockPlugin } from './mock'; |
|||
import { configCompressPlugin } from './compress'; |
|||
import { configStyleImportPlugin } from './styleImport'; |
|||
import { configVisualizerConfig } from './visualizer'; |
|||
import { configThemePlugin } from './theme'; |
|||
import { configImageminPlugin } from './imagemin'; |
|||
import { configSvgIconsPlugin } from './svgSprite'; |
|||
import { configHmrPlugin } from './hmr'; |
|||
|
|||
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) { |
|||
const { |
|||
VITE_USE_IMAGEMIN, |
|||
VITE_USE_MOCK, |
|||
VITE_LEGACY, |
|||
VITE_BUILD_COMPRESS, |
|||
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE, |
|||
} = viteEnv; |
|||
|
|||
const vitePlugins: (Plugin | Plugin[])[] = [ |
|||
// have to
|
|||
vue(), |
|||
// have to
|
|||
vueJsx(), |
|||
]; |
|||
|
|||
// vite-plugin-windicss
|
|||
vitePlugins.push(windiCSS()); |
|||
|
|||
// TODO
|
|||
!isBuild && vitePlugins.push(configHmrPlugin()); |
|||
|
|||
// @vitejs/plugin-legacy
|
|||
VITE_LEGACY && isBuild && vitePlugins.push(legacy()); |
|||
|
|||
// vite-plugin-html
|
|||
vitePlugins.push(configHtmlPlugin(viteEnv, isBuild)); |
|||
|
|||
// vite-plugin-svg-icons
|
|||
vitePlugins.push(configSvgIconsPlugin(isBuild)); |
|||
|
|||
// vite-plugin-mock
|
|||
VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild)); |
|||
|
|||
// vite-plugin-purge-icons
|
|||
vitePlugins.push(purgeIcons()); |
|||
|
|||
// vite-plugin-style-import
|
|||
vitePlugins.push(configStyleImportPlugin(isBuild)); |
|||
|
|||
// rollup-plugin-visualizer
|
|||
vitePlugins.push(configVisualizerConfig()); |
|||
|
|||
//vite-plugin-theme
|
|||
vitePlugins.push(configThemePlugin(isBuild)); |
|||
|
|||
// The following plugins only work in the production environment
|
|||
if (isBuild) { |
|||
//vite-plugin-imagemin
|
|||
VITE_USE_IMAGEMIN && vitePlugins.push(configImageminPlugin()); |
|||
|
|||
// rollup-plugin-gzip
|
|||
vitePlugins.push( |
|||
configCompressPlugin(VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE) |
|||
); |
|||
|
|||
// vite-plugin-pwa
|
|||
vitePlugins.push(configPwaConfig(viteEnv)); |
|||
} |
|||
|
|||
return vitePlugins; |
|||
} |
|||
@ -1,34 +0,0 @@ |
|||
/** |
|||
* Zero-config PWA for Vite |
|||
* https://github.com/antfu/vite-plugin-pwa
|
|||
*/ |
|||
|
|||
import { VitePWA } from 'vite-plugin-pwa'; |
|||
|
|||
export function configPwaConfig(env: ViteEnv) { |
|||
const { VITE_USE_PWA, VITE_GLOB_APP_TITLE, VITE_GLOB_APP_SHORT_NAME } = env; |
|||
|
|||
if (VITE_USE_PWA) { |
|||
// vite-plugin-pwa
|
|||
const pwaPlugin = VitePWA({ |
|||
manifest: { |
|||
name: VITE_GLOB_APP_TITLE, |
|||
short_name: VITE_GLOB_APP_SHORT_NAME, |
|||
icons: [ |
|||
{ |
|||
src: './resource/img/pwa-192x192.png', |
|||
sizes: '192x192', |
|||
type: 'image/png', |
|||
}, |
|||
{ |
|||
src: './resource/img/pwa-512x512.png', |
|||
sizes: '512x512', |
|||
type: 'image/png', |
|||
}, |
|||
], |
|||
}, |
|||
}); |
|||
return pwaPlugin; |
|||
} |
|||
return []; |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
/** |
|||
* Introduces component library styles on demand. |
|||
* https://github.com/anncwb/vite-plugin-style-import
|
|||
*/ |
|||
|
|||
import styleImport from 'vite-plugin-style-import'; |
|||
|
|||
export function configStyleImportPlugin(isBuild: boolean) { |
|||
if (!isBuild) return []; |
|||
const styleImportPlugin = styleImport({ |
|||
libs: [ |
|||
{ |
|||
libraryName: 'ant-design-vue', |
|||
esModule: true, |
|||
resolveStyle: (name) => { |
|||
return `ant-design-vue/es/${name}/style/index`; |
|||
}, |
|||
}, |
|||
], |
|||
}); |
|||
return styleImportPlugin; |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
/** |
|||
* Vite Plugin for fast creating SVG sprites. |
|||
* https://github.com/anncwb/vite-plugin-svg-icons
|
|||
*/ |
|||
|
|||
import SvgIconsPlugin from 'vite-plugin-svg-icons'; |
|||
import path from 'path'; |
|||
|
|||
export function configSvgIconsPlugin(isBuild: boolean) { |
|||
const svgIconsPlugin = SvgIconsPlugin({ |
|||
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')], |
|||
svgoOptions: isBuild, |
|||
// default
|
|||
symbolId: 'icon-[dir]-[name]', |
|||
}); |
|||
return svgIconsPlugin; |
|||
} |
|||
@ -1,89 +0,0 @@ |
|||
/** |
|||
* Vite plugin for website theme color switching |
|||
* https://github.com/anncwb/vite-plugin-theme
|
|||
*/ |
|||
import type { Plugin } from 'vite'; |
|||
import path from 'path'; |
|||
import { |
|||
viteThemePlugin, |
|||
antdDarkThemePlugin, |
|||
mixLighten, |
|||
mixDarken, |
|||
tinycolor, |
|||
} from 'vite-plugin-theme'; |
|||
import { getThemeColors, generateColors } from '../../config/themeConfig'; |
|||
import { generateModifyVars } from '../../generate/generateModifyVars'; |
|||
|
|||
export function configThemePlugin(isBuild: boolean): Plugin[] { |
|||
const colors = generateColors({ |
|||
mixDarken, |
|||
mixLighten, |
|||
tinycolor, |
|||
}); |
|||
const plugin = [ |
|||
viteThemePlugin({ |
|||
resolveSelector: (s) => { |
|||
s = s.trim(); |
|||
switch (s) { |
|||
case '.ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon': |
|||
return '.ant-steps-item-icon > .ant-steps-icon'; |
|||
case '.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)': |
|||
case '.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover': |
|||
case '.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active': |
|||
return s; |
|||
case '.ant-steps-item-icon > .ant-steps-icon': |
|||
return s; |
|||
case '.ant-select-item-option-selected:not(.ant-select-item-option-disabled)': |
|||
return s; |
|||
default: |
|||
if (s.indexOf('.ant-btn') >= -1) { |
|||
// 按钮被重新定制过,需要过滤掉class防止覆盖
|
|||
return s; |
|||
} |
|||
} |
|||
return s.startsWith('[data-theme') ? s : `[data-theme] ${s}`; |
|||
}, |
|||
colorVariables: [...getThemeColors(), ...colors], |
|||
}), |
|||
antdDarkThemePlugin({ |
|||
preloadFiles: [ |
|||
path.resolve(process.cwd(), 'node_modules/ant-design-vue/dist/antd.less'), |
|||
//path.resolve(process.cwd(), 'node_modules/ant-design-vue/dist/antd.dark.less'),
|
|||
path.resolve(process.cwd(), 'src/design/index.less'), |
|||
], |
|||
filter: (id) => (isBuild ? !id.endsWith('antd.less') : true), |
|||
// extractCss: false,
|
|||
darkModifyVars: { |
|||
...generateModifyVars(true), |
|||
'text-color': '#c9d1d9', |
|||
'primary-1': 'rgb(255 255 255 / 8%)', |
|||
'text-color-base': '#c9d1d9', |
|||
'component-background': '#151515', |
|||
'heading-color': 'rgb(255 255 255 / 65%)', |
|||
// black: '#0e1117',
|
|||
// #8b949e
|
|||
'text-color-secondary': '#8b949e', |
|||
'border-color-base': '#303030', |
|||
// 'border-color-split': '#30363d',
|
|||
'item-active-bg': '#111b26', |
|||
'app-content-background': 'rgb(255 255 255 / 4%)', |
|||
'tree-node-selected-bg': '#11263c', |
|||
|
|||
'alert-success-border-color': '#274916', |
|||
'alert-success-bg-color': '#162312', |
|||
'alert-success-icon-color': '#49aa19', |
|||
'alert-info-border-color': '#153450', |
|||
'alert-info-bg-color': '#111b26', |
|||
'alert-info-icon-color': '#177ddc', |
|||
'alert-warning-border-color': '#594214', |
|||
'alert-warning-bg-color': '#2b2111', |
|||
'alert-warning-icon-color': '#d89614', |
|||
'alert-error-border-color': '#58181c', |
|||
'alert-error-bg-color': '#2a1215', |
|||
'alert-error-icon-color': '#a61d24', |
|||
}, |
|||
}), |
|||
]; |
|||
|
|||
return plugin as unknown as Plugin[]; |
|||
} |
|||
@ -1,34 +0,0 @@ |
|||
/** |
|||
* Used to parse the .env.development proxy configuration |
|||
*/ |
|||
import type { ProxyOptions } from 'vite'; |
|||
|
|||
type ProxyItem = [string, string]; |
|||
|
|||
type ProxyList = ProxyItem[]; |
|||
|
|||
type ProxyTargetList = Record<string, ProxyOptions & { rewrite: (path: string) => string }>; |
|||
|
|||
const httpsRE = /^https:\/\//; |
|||
|
|||
/** |
|||
* Generate proxy |
|||
* @param list |
|||
*/ |
|||
export function createProxy(list: ProxyList = []) { |
|||
const ret: ProxyTargetList = {}; |
|||
for (const [prefix, target] of list) { |
|||
const isHttps = httpsRE.test(target); |
|||
|
|||
// https://github.com/http-party/node-http-proxy#options
|
|||
ret[prefix] = { |
|||
target: target, |
|||
changeOrigin: true, |
|||
ws: true, |
|||
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''), |
|||
// https is require secure=false
|
|||
...(isHttps ? { secure: false } : {}), |
|||
}; |
|||
} |
|||
return ret; |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
module.exports = { |
|||
ignores: [(commit) => commit.includes('init')], |
|||
extends: ['@commitlint/config-conventional'], |
|||
rules: { |
|||
'body-leading-blank': [2, 'always'], |
|||
'footer-leading-blank': [1, 'always'], |
|||
'header-max-length': [2, 'always', 108], |
|||
'subject-empty': [2, 'never'], |
|||
'type-empty': [2, 'never'], |
|||
'type-enum': [ |
|||
2, |
|||
'always', |
|||
[ |
|||
'feat', |
|||
'fix', |
|||
'perf', |
|||
'style', |
|||
'docs', |
|||
'test', |
|||
'refactor', |
|||
'build', |
|||
'ci', |
|||
'chore', |
|||
'revert', |
|||
'wip', |
|||
'workflow', |
|||
'types', |
|||
'release', |
|||
], |
|||
], |
|||
}, |
|||
}; |
|||
@ -1,170 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en" id="htmlRoot"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> |
|||
<meta name="renderer" content="webkit" /> |
|||
<meta |
|||
name="viewport" |
|||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" |
|||
/> |
|||
<meta |
|||
http-equiv="Content-Security-Policy" |
|||
content="default-src *; style-src * 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' *" |
|||
/> |
|||
<title><%= title %></title> |
|||
<link rel="icon" href="/favicon.ico" /> |
|||
</head> |
|||
|
|||
<body> |
|||
<script> |
|||
(() => { |
|||
var htmlRoot = document.getElementById('htmlRoot'); |
|||
var theme = window.localStorage.getItem('__APP__DARK__MODE__'); |
|||
if (htmlRoot && theme) { |
|||
htmlRoot.setAttribute('data-theme', theme); |
|||
theme = htmlRoot = null; |
|||
} |
|||
})(); |
|||
</script> |
|||
<div id="app"> |
|||
<style> |
|||
html[data-theme='dark'] .app-loading { |
|||
background-color: #2c344a; |
|||
} |
|||
|
|||
html[data-theme='dark'] .app-loading .app-loading-title { |
|||
color: rgba(255, 255, 255, 0.85); |
|||
} |
|||
|
|||
.app-loading { |
|||
display: flex; |
|||
width: 100%; |
|||
height: 100%; |
|||
justify-content: center; |
|||
align-items: center; |
|||
flex-direction: column; |
|||
background-color: #f4f7f9; |
|||
} |
|||
|
|||
.app-loading .app-loading-wrap { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
display: flex; |
|||
-webkit-transform: translate3d(-50%, -50%, 0); |
|||
transform: translate3d(-50%, -50%, 0); |
|||
justify-content: center; |
|||
align-items: center; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.app-loading .dots { |
|||
display: flex; |
|||
padding: 98px; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.app-loading .app-loading-title { |
|||
display: flex; |
|||
margin-top: 30px; |
|||
font-size: 30px; |
|||
color: rgba(0, 0, 0, 0.85); |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.app-loading .app-loading-logo { |
|||
display: block; |
|||
width: 90px; |
|||
margin: 0 auto; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.dot { |
|||
position: relative; |
|||
display: inline-block; |
|||
width: 48px; |
|||
height: 48px; |
|||
margin-top: 30px; |
|||
font-size: 32px; |
|||
transform: rotate(45deg); |
|||
box-sizing: border-box; |
|||
animation: antRotate 1.2s infinite linear; |
|||
} |
|||
|
|||
.dot i { |
|||
position: absolute; |
|||
display: block; |
|||
width: 20px; |
|||
height: 20px; |
|||
background-color: #0065cc; |
|||
border-radius: 100%; |
|||
opacity: 0.3; |
|||
transform: scale(0.75); |
|||
animation: antSpinMove 1s infinite linear alternate; |
|||
transform-origin: 50% 50%; |
|||
} |
|||
|
|||
.dot i:nth-child(1) { |
|||
top: 0; |
|||
left: 0; |
|||
} |
|||
|
|||
.dot i:nth-child(2) { |
|||
top: 0; |
|||
right: 0; |
|||
-webkit-animation-delay: 0.4s; |
|||
animation-delay: 0.4s; |
|||
} |
|||
|
|||
.dot i:nth-child(3) { |
|||
right: 0; |
|||
bottom: 0; |
|||
-webkit-animation-delay: 0.8s; |
|||
animation-delay: 0.8s; |
|||
} |
|||
|
|||
.dot i:nth-child(4) { |
|||
bottom: 0; |
|||
left: 0; |
|||
-webkit-animation-delay: 1.2s; |
|||
animation-delay: 1.2s; |
|||
} |
|||
@keyframes antRotate { |
|||
to { |
|||
-webkit-transform: rotate(405deg); |
|||
transform: rotate(405deg); |
|||
} |
|||
} |
|||
@-webkit-keyframes antRotate { |
|||
to { |
|||
-webkit-transform: rotate(405deg); |
|||
transform: rotate(405deg); |
|||
} |
|||
} |
|||
@keyframes antSpinMove { |
|||
to { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
@-webkit-keyframes antSpinMove { |
|||
to { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
</style> |
|||
<div class="app-loading"> |
|||
<div class="app-loading-wrap"> |
|||
<img src="/resource/img/logo.png" class="app-loading-logo" alt="Logo" /> |
|||
<div class="app-loading-dots"> |
|||
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span> |
|||
</div> |
|||
<div class="app-loading-title"><%= title %></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<script type="module" src="/src/main.ts"></script> |
|||
</body> |
|||
</html> |
|||
@ -1,36 +0,0 @@ |
|||
export default { |
|||
preset: 'ts-jest', |
|||
roots: ['<rootDir>/tests/'], |
|||
clearMocks: true, |
|||
moduleDirectories: ['node_modules', 'src'], |
|||
moduleFileExtensions: ['js', 'ts', 'vue', 'tsx', 'jsx', 'json', 'node'], |
|||
modulePaths: ['<rootDir>/src', '<rootDir>/node_modules'], |
|||
testMatch: [ |
|||
'**/tests/**/*.[jt]s?(x)', |
|||
'**/?(*.)+(spec|test).[tj]s?(x)', |
|||
'(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$', |
|||
], |
|||
testPathIgnorePatterns: [ |
|||
'<rootDir>/tests/server/', |
|||
'<rootDir>/tests/__mocks__/', |
|||
'/node_modules/', |
|||
], |
|||
transform: { |
|||
'^.+\\.tsx?$': 'ts-jest', |
|||
}, |
|||
transformIgnorePatterns: ['<rootDir>/tests/__mocks__/', '/node_modules/'], |
|||
// A map from regular expressions to module names that allow to stub out resources with a single module
|
|||
moduleNameMapper: { |
|||
'\\.(vs|fs|vert|frag|glsl|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': |
|||
'<rootDir>/tests/__mocks__/fileMock.ts', |
|||
'\\.(sass|s?css|less)$': '<rootDir>/tests/__mocks__/styleMock.ts', |
|||
'\\?worker$': '<rootDir>/tests/__mocks__/workerMock.ts', |
|||
'^/@/(.*)$': '<rootDir>/src/$1', |
|||
}, |
|||
testEnvironment: 'jsdom', |
|||
verbose: true, |
|||
collectCoverage: false, |
|||
coverageDirectory: 'coverage', |
|||
collectCoverageFrom: ['src/**/*.{js,ts,vue}'], |
|||
coveragePathIgnorePatterns: ['^.+\\.d\\.ts$'], |
|||
}; |
|||
@ -1,60 +0,0 @@ |
|||
// Interface data format used to return a unified format
|
|||
|
|||
export function resultSuccess<T = Recordable>(result: T, { message = 'ok' } = {}) { |
|||
return { |
|||
code: 0, |
|||
result, |
|||
message, |
|||
type: 'success', |
|||
}; |
|||
} |
|||
|
|||
export function resultPageSuccess<T = any>( |
|||
page: number, |
|||
pageSize: number, |
|||
list: T[], |
|||
{ message = 'ok' } = {} |
|||
) { |
|||
const pageData = pagination(page, pageSize, list); |
|||
|
|||
return { |
|||
...resultSuccess({ |
|||
items: pageData, |
|||
total: list.length, |
|||
}), |
|||
message, |
|||
}; |
|||
} |
|||
|
|||
export function resultError(message = 'Request failed', { code = -1, result = null } = {}) { |
|||
return { |
|||
code, |
|||
result, |
|||
message, |
|||
type: 'error', |
|||
}; |
|||
} |
|||
|
|||
export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]): T[] { |
|||
const offset = (pageNo - 1) * Number(pageSize); |
|||
const ret = |
|||
offset + Number(pageSize) >= array.length |
|||
? array.slice(offset, array.length) |
|||
: array.slice(offset, offset + Number(pageSize)); |
|||
return ret; |
|||
} |
|||
|
|||
export interface requestParams { |
|||
method: string; |
|||
body: any; |
|||
headers?: { authorization?: string }; |
|||
query: any; |
|||
} |
|||
|
|||
/** |
|||
* @description 本函数用于从request数据中获取token,请根据项目的实际情况修改 |
|||
* |
|||
*/ |
|||
export function getRequestToken({ headers }: requestParams): string | undefined { |
|||
return headers?.authorization; |
|||
} |
|||
@ -1,62 +0,0 @@ |
|||
import { MockMethod } from 'vite-plugin-mock'; |
|||
import { resultSuccess, resultError } from '../_util'; |
|||
|
|||
const userInfo = { |
|||
name: 'Vben', |
|||
userid: '00000001', |
|||
email: 'test@gmail.com', |
|||
signature: '海纳百川,有容乃大', |
|||
introduction: '微笑着,努力着,欣赏着', |
|||
title: '交互专家', |
|||
group: '某某某事业群-某某平台部-某某技术部-UED', |
|||
tags: [ |
|||
{ |
|||
key: '0', |
|||
label: '很有想法的', |
|||
}, |
|||
{ |
|||
key: '1', |
|||
label: '专注设计', |
|||
}, |
|||
{ |
|||
key: '2', |
|||
label: '辣~', |
|||
}, |
|||
{ |
|||
key: '3', |
|||
label: '大长腿', |
|||
}, |
|||
{ |
|||
key: '4', |
|||
label: '川妹子', |
|||
}, |
|||
{ |
|||
key: '5', |
|||
label: '海纳百川', |
|||
}, |
|||
], |
|||
notifyCount: 12, |
|||
unreadCount: 11, |
|||
country: 'China', |
|||
address: 'Xiamen City 77', |
|||
phone: '0592-268888888', |
|||
}; |
|||
|
|||
export default [ |
|||
{ |
|||
url: '/basic-api/account/getAccountInfo', |
|||
timeout: 1000, |
|||
method: 'get', |
|||
response: () => { |
|||
return resultSuccess(userInfo); |
|||
}, |
|||
}, |
|||
{ |
|||
url: '/basic-api/user/sessionTimeout', |
|||
method: 'post', |
|||
statusCode: 401, |
|||
response: () => { |
|||
return resultError(); |
|||
}, |
|||
}, |
|||
] as MockMethod[]; |
|||
@ -1,28 +0,0 @@ |
|||
import { MockMethod } from 'vite-plugin-mock'; |
|||
import { resultSuccess } from '../_util'; |
|||
|
|||
const demoList = (keyword) => { |
|||
const result = { |
|||
list: [] as any[], |
|||
}; |
|||
for (let index = 0; index < 20; index++) { |
|||
result.list.push({ |
|||
name: `${keyword ?? ''}选项${index}`, |
|||
id: `${index}`, |
|||
}); |
|||
} |
|||
return result; |
|||
}; |
|||
|
|||
export default [ |
|||
{ |
|||
url: '/basic-api/select/getDemoOptions', |
|||
timeout: 1000, |
|||
method: 'get', |
|||
response: ({ query }) => { |
|||
const { keyword } = query; |
|||
console.log(keyword); |
|||
return resultSuccess(demoList(keyword)); |
|||
}, |
|||
}, |
|||
] as MockMethod[]; |
|||
@ -1,52 +0,0 @@ |
|||
import { MockMethod } from 'vite-plugin-mock'; |
|||
import { Random } from 'mockjs'; |
|||
import { resultPageSuccess } from '../_util'; |
|||
|
|||
function getRandomPics(count = 10): string[] { |
|||
const arr: string[] = []; |
|||
for (let i = 0; i < count; i++) { |
|||
arr.push(Random.image('800x600', Random.color(), Random.color(), Random.title())); |
|||
} |
|||
return arr; |
|||
} |
|||
|
|||
const demoList = (() => { |
|||
const result: any[] = []; |
|||
for (let index = 0; index < 60; index++) { |
|||
result.push({ |
|||
id: `${index}`, |
|||
beginTime: '@datetime', |
|||
endTime: '@datetime', |
|||
address: '@city()', |
|||
name: '@cname()', |
|||
name1: '@cname()', |
|||
name2: '@cname()', |
|||
name3: '@cname()', |
|||
name4: '@cname()', |
|||
name5: '@cname()', |
|||
name6: '@cname()', |
|||
name7: '@cname()', |
|||
name8: '@cname()', |
|||
avatar: Random.image('400x400', Random.color(), Random.color(), Random.first()), |
|||
imgArr: getRandomPics(Math.ceil(Math.random() * 3) + 1), |
|||
imgs: getRandomPics(Math.ceil(Math.random() * 3) + 1), |
|||
date: `@date('yyyy-MM-dd')`, |
|||
time: `@time('HH:mm')`, |
|||
'no|100000-10000000': 100000, |
|||
'status|1': ['normal', 'enable', 'disable'], |
|||
}); |
|||
} |
|||
return result; |
|||
})(); |
|||
|
|||
export default [ |
|||
{ |
|||
url: '/basic-api/table/getDemoList', |
|||
timeout: 100, |
|||
method: 'get', |
|||
response: ({ query }) => { |
|||
const { page = 1, pageSize = 20 } = query; |
|||
return resultPageSuccess(page, pageSize, demoList); |
|||
}, |
|||
}, |
|||
] as MockMethod[]; |
|||
@ -1,114 +0,0 @@ |
|||
import { MockMethod } from 'vite-plugin-mock'; |
|||
import { resultError, resultSuccess, getRequestToken, requestParams } from '../_util'; |
|||
|
|||
export function createFakeUserList() { |
|||
return [ |
|||
{ |
|||
userId: '1', |
|||
username: 'vben', |
|||
realName: 'Vben Admin', |
|||
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=640', |
|||
desc: 'manager', |
|||
password: '123456', |
|||
token: 'fakeToken1', |
|||
homePath: '/dashboard/analysis', |
|||
roles: [ |
|||
{ |
|||
roleName: 'Super Admin', |
|||
value: 'super', |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
userId: '2', |
|||
username: 'test', |
|||
password: '123456', |
|||
realName: 'test user', |
|||
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=339449197&s=640', |
|||
desc: 'tester', |
|||
token: 'fakeToken2', |
|||
homePath: '/dashboard/workbench', |
|||
roles: [ |
|||
{ |
|||
roleName: 'Tester', |
|||
value: 'test', |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
const fakeCodeList: any = { |
|||
'1': ['1000', '3000', '5000'], |
|||
|
|||
'2': ['2000', '4000', '6000'], |
|||
}; |
|||
export default [ |
|||
// mock user login
|
|||
{ |
|||
url: '/basic-api/login', |
|||
timeout: 200, |
|||
method: 'post', |
|||
response: ({ body }) => { |
|||
const { username, password } = body; |
|||
const checkUser = createFakeUserList().find( |
|||
(item) => item.username === username && password === item.password |
|||
); |
|||
if (!checkUser) { |
|||
return resultError('Incorrect account or password!'); |
|||
} |
|||
const { userId, username: _username, token, realName, desc, roles } = checkUser; |
|||
return resultSuccess({ |
|||
roles, |
|||
userId, |
|||
username: _username, |
|||
token, |
|||
realName, |
|||
desc, |
|||
}); |
|||
}, |
|||
}, |
|||
{ |
|||
url: '/basic-api/getUserInfo', |
|||
method: 'get', |
|||
response: (request: requestParams) => { |
|||
const token = getRequestToken(request); |
|||
if (!token) return resultError('Invalid token'); |
|||
const checkUser = createFakeUserList().find((item) => item.token === token); |
|||
if (!checkUser) { |
|||
return resultError('The corresponding user information was not obtained!'); |
|||
} |
|||
return resultSuccess(checkUser); |
|||
}, |
|||
}, |
|||
{ |
|||
url: '/basic-api/getPermCode', |
|||
timeout: 200, |
|||
method: 'get', |
|||
response: (request: requestParams) => { |
|||
const token = getRequestToken(request); |
|||
if (!token) return resultError('Invalid token'); |
|||
const checkUser = createFakeUserList().find((item) => item.token === token); |
|||
if (!checkUser) { |
|||
return resultError('Invalid token!'); |
|||
} |
|||
const codeList = fakeCodeList[checkUser.userId]; |
|||
|
|||
return resultSuccess(codeList); |
|||
}, |
|||
}, |
|||
{ |
|||
url: '/basic-api/logout', |
|||
timeout: 200, |
|||
method: 'get', |
|||
response: (request: requestParams) => { |
|||
const token = getRequestToken(request); |
|||
if (!token) return resultError('Invalid token'); |
|||
const checkUser = createFakeUserList().find((item) => item.token === token); |
|||
if (!checkUser) { |
|||
return resultError('Invalid token!'); |
|||
} |
|||
return resultSuccess(undefined, { message: 'Token has been destroyed' }); |
|||
}, |
|||
}, |
|||
] as MockMethod[]; |
|||
@ -1,74 +0,0 @@ |
|||
{ |
|||
"runtime": "Net60", |
|||
"defaultVariables": null, |
|||
"documentGenerator": { |
|||
"fromDocument": { |
|||
"url": "http://localhost:44315/swagger/AbpPro/swagger.json", |
|||
"output": null, |
|||
"newLineBehavior": "Auto" |
|||
} |
|||
}, |
|||
"codeGenerators": { |
|||
"openApiToTypeScriptClient": { |
|||
"className": "{controller}ServiceProxy", |
|||
"moduleName": "", |
|||
"namespace": "", |
|||
"typeScriptVersion": 2.7, |
|||
"template": "Axios", |
|||
"promiseType": "Promise", |
|||
"httpClass": "HttpClient", |
|||
"withCredentials": false, |
|||
"useSingletonProvider": false, |
|||
"injectionTokenType": "OpaqueToken", |
|||
"rxJsVersion": 6.0, |
|||
"dateTimeType": "MomentJS", |
|||
"nullValue": "Undefined", |
|||
"generateClientClasses": true, |
|||
"generateClientInterfaces": false, |
|||
"generateOptionalParameters": false, |
|||
"exportTypes": true, |
|||
"wrapDtoExceptions": false, |
|||
"exceptionClass": "ApiException", |
|||
"clientBaseClass": "ServiceProxyBase", |
|||
"wrapResponses": false, |
|||
"wrapResponseMethods": [], |
|||
"generateResponseClasses": true, |
|||
"responseClass": "SwaggerResponse", |
|||
"protectedMethods": [], |
|||
"configurationClass": null, |
|||
"useTransformOptionsMethod": true, |
|||
"useTransformResultMethod": true, |
|||
"generateDtoTypes": true, |
|||
"operationGenerationMode": "MultipleClientsFromFirstTagAndPathSegments", |
|||
"markOptionalProperties": false, |
|||
"generateCloneMethod": false, |
|||
"typeStyle": "Class", |
|||
"enumStyle": "Enum", |
|||
"useLeafType": false, |
|||
"classTypes": [], |
|||
"extendedClasses": [], |
|||
"extensionCode": null, |
|||
"generateDefaultValues": true, |
|||
"excludedTypeNames": [], |
|||
"excludedParameterNames": [], |
|||
"handleReferences": false, |
|||
"generateConstructorInterface": true, |
|||
"convertConstructorInterfaceData": false, |
|||
"importRequiredTypes": true, |
|||
"useGetBaseUrlMethod": false, |
|||
"baseUrlTokenName": null, |
|||
"queryNullValue": "", |
|||
"useAbortSignal": false, |
|||
"inlineNamedDictionaries": false, |
|||
"inlineNamedAny": false, |
|||
"templateDirectory": "./templates", |
|||
"typeNameGeneratorType": null, |
|||
"propertyNameGeneratorType": null, |
|||
"enumNameGeneratorType": null, |
|||
"serviceHost": null, |
|||
"serviceSchemes": null, |
|||
"output": "../src/services/ServiceProxies.ts", |
|||
"newLineBehavior": "Auto" |
|||
} |
|||
} |
|||
} |
|||
@ -1,154 +0,0 @@ |
|||
{ |
|||
"name": "vben-admin", |
|||
"version": "2.7.1", |
|||
"author": { |
|||
"name": "vben", |
|||
"email": "anncwb@126.com", |
|||
"url": "https://github.com/anncwb" |
|||
}, |
|||
"scripts": { |
|||
"nswag": "cd nswag &.\\refresh.bat", |
|||
"bootstrap": "yarn install", |
|||
"serve": "npm run dev", |
|||
"dev": "vite", |
|||
"build": "cross-env NODE_ENV=production vite build && esno ./build/script/postBuild.ts", |
|||
"build:test": "cross-env vite build --mode test && esno ./build/script/postBuild.ts", |
|||
"build:no-cache": "yarn clean:cache && npm run build", |
|||
"report": "cross-env REPORT=true npm run build", |
|||
"type:check": "vue-tsc --noEmit --skipLibCheck", |
|||
"preview": "npm run build && vite preview", |
|||
"preview:dist": "vite preview", |
|||
"log": "conventional-changelog -p angular -i CHANGELOG.md -s", |
|||
"clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite", |
|||
"clean:lib": "rimraf node_modules", |
|||
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix", |
|||
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"", |
|||
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", |
|||
"lint:pretty": "pretty-quick --staged", |
|||
"test:unit": "jest", |
|||
"test:unit-coverage": "jest --coverage", |
|||
"test:gzip": "http-server dist --cors --gzip -c-1", |
|||
"test:br": "http-server dist --cors --brotli -c-1", |
|||
"reinstall": "rimraf yarn.lock && rimraf package.lock.json && rimraf node_modules && npm run bootstrap", |
|||
"gen:icon": "esno ./build/generate/icon/index.ts" |
|||
}, |
|||
"dependencies": { |
|||
"@iconify/iconify": "^2.0.3", |
|||
"@logicflow/core": "^0.6.9", |
|||
"@logicflow/extension": "^0.6.9", |
|||
"@microsoft/signalr": "^5.0.8", |
|||
"@vueuse/core": "^5.3.0", |
|||
"@zxcvbn-ts/core": "^1.0.0-beta.0", |
|||
"ali-oss": "^6.16.0", |
|||
"ant-design-vue": "2.2.6", |
|||
"axios": "^0.21.1", |
|||
"crypto-js": "^4.1.1", |
|||
"echarts": "^5.1.2", |
|||
"jwt-decode": "^3.1.2", |
|||
"lodash-es": "^4.17.21", |
|||
"mockjs": "^1.1.0", |
|||
"nprogress": "^0.2.0", |
|||
"nswag": "^13.15.9", |
|||
"oidc-client": "^1.11.5", |
|||
"path-to-regexp": "^6.2.0", |
|||
"pinia": "2.0.0-rc.4", |
|||
"print-js": "^1.6.0", |
|||
"qrcode": "^1.4.4", |
|||
"resize-observer-polyfill": "^1.5.1", |
|||
"sortablejs": "^1.14.0", |
|||
"vue": "3.2.2", |
|||
"vue-i18n": "9.1.7", |
|||
"vue-router": "^4.0.12", |
|||
"vue-types": "^4.0.3" |
|||
}, |
|||
"devDependencies": { |
|||
"@commitlint/cli": "^13.1.0", |
|||
"@commitlint/config-conventional": "^13.1.0", |
|||
"@iconify/json": "^1.1.386", |
|||
"@purge-icons/generated": "^0.7.0", |
|||
"@types/codemirror": "^5.60.2", |
|||
"@types/crypto-js": "^4.0.2", |
|||
"@types/fs-extra": "^9.0.12", |
|||
"@types/inquirer": "^7.3.3", |
|||
"@types/intro.js": "^3.0.2", |
|||
"@types/jest": "^27.0.1", |
|||
"@types/lodash-es": "^4.17.4", |
|||
"@types/mockjs": "^1.0.4", |
|||
"@types/node": "^16.6.1", |
|||
"@types/nprogress": "^0.2.0", |
|||
"@types/qrcode": "^1.4.1", |
|||
"@types/qs": "^6.9.7", |
|||
"@types/sortablejs": "^1.10.7", |
|||
"@typescript-eslint/eslint-plugin": "^4.29.1", |
|||
"@typescript-eslint/parser": "^4.29.1", |
|||
"@vitejs/plugin-legacy": "^1.5.1", |
|||
"@vitejs/plugin-vue": "^1.4.0", |
|||
"@vitejs/plugin-vue-jsx": "^1.1.7", |
|||
"@vue/compiler-sfc": "3.2.2", |
|||
"@vue/test-utils": "^2.0.0-rc.12", |
|||
"autoprefixer": "^10.3.1", |
|||
"commitizen": "^4.2.4", |
|||
"conventional-changelog-cli": "^2.1.1", |
|||
"cross-env": "^7.0.3", |
|||
"dotenv": "^10.0.0", |
|||
"eslint": "^7.32.0", |
|||
"eslint-config-prettier": "^8.3.0", |
|||
"eslint-define-config": "^1.0.9", |
|||
"eslint-plugin-jest": "^24.4.0", |
|||
"eslint-plugin-prettier": "^3.4.0", |
|||
"eslint-plugin-vue": "^7.16.0", |
|||
"esno": "^0.8.0", |
|||
"fs-extra": "^10.0.0", |
|||
"http-server": "^13.0.0", |
|||
"husky": "^7.0.1", |
|||
"inquirer": "^8.1.2", |
|||
"is-ci": "^3.0.0", |
|||
"jest": "^27.0.6", |
|||
"less": "^4.1.1", |
|||
"lint-staged": "^11.1.2", |
|||
"npm-run-all": "^4.1.5", |
|||
"postcss": "^8.3.6", |
|||
"prettier": "^2.3.2", |
|||
"pretty-quick": "^3.1.1", |
|||
"rimraf": "^3.0.2", |
|||
"rollup-plugin-visualizer": "5.5.2", |
|||
"stylelint": "^13.13.1", |
|||
"stylelint-config-prettier": "^8.0.2", |
|||
"stylelint-config-standard": "^22.0.0", |
|||
"stylelint-order": "^4.1.0", |
|||
"ts-jest": "^27.0.4", |
|||
"ts-node": "^10.2.0", |
|||
"typescript": "4.3.5", |
|||
"vite": "2.6.14", |
|||
"vite-plugin-compression": "^0.3.3", |
|||
"vite-plugin-html": "^2.0.7", |
|||
"vite-plugin-imagemin": "^0.4.3", |
|||
"vite-plugin-mock": "^2.9.4", |
|||
"vite-plugin-purge-icons": "^0.7.0", |
|||
"vite-plugin-pwa": "^0.10.0", |
|||
"vite-plugin-style-import": "^1.1.1", |
|||
"vite-plugin-svg-icons": "^1.0.4", |
|||
"vite-plugin-theme": "^0.8.1", |
|||
"vite-plugin-windicss": "^1.2.7", |
|||
"vue-eslint-parser": "^7.10.0", |
|||
"vue-tsc": "^0.2.3" |
|||
}, |
|||
"resolutions": { |
|||
"//": "Used to install imagemin dependencies, because imagemin may not be installed in China. If it is abroad, you can delete it", |
|||
"bin-wrapper": "npm:bin-wrapper-china", |
|||
"rollup": "^2.56.2", |
|||
"gifsicle": "5.2.0" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git+https://github.com/anncwb/vue-vben-admin.git" |
|||
}, |
|||
"license": "MIT", |
|||
"bugs": { |
|||
"url": "https://github.com/anncwb/vue-vben-admin/issues" |
|||
}, |
|||
"homepage": "https://github.com/anncwb/vue-vben-admin", |
|||
"engines": { |
|||
"node": "^12 || >=14" |
|||
} |
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
module.exports = { |
|||
printWidth: 100, |
|||
tabWidth: 2, |
|||
useTabs: false, |
|||
semi: true, |
|||
vueIndentScriptAndStyle: true, |
|||
singleQuote: true, |
|||
quoteProps: 'as-needed', |
|||
bracketSpacing: true, |
|||
trailingComma: 'es5', |
|||
jsxBracketSameLine: false, |
|||
jsxSingleQuote: false, |
|||
arrowParens: 'always', |
|||
insertPragma: false, |
|||
requirePragma: false, |
|||
proseWrap: 'never', |
|||
htmlWhitespaceSensitivity: 'strict', |
|||
endOfLine: 'auto', |
|||
rangeStart: 0, |
|||
}; |
|||
@ -1,19 +0,0 @@ |
|||
<template> |
|||
<ConfigProvider :locale="getAntdLocale"> |
|||
<AppProvider> |
|||
<RouterView /> |
|||
</AppProvider> |
|||
</ConfigProvider> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { ConfigProvider } from 'ant-design-vue'; |
|||
import { AppProvider } from '/@/components/Application'; |
|||
import { useTitle } from '/@/hooks/web/useTitle'; |
|||
import { useLocale } from '/@/locales/useLocale'; |
|||
|
|||
// support Multi-language |
|||
const { getAntdLocale } = useLocale(); |
|||
|
|||
useTitle(); |
|||
</script> |
|||
@ -1,9 +0,0 @@ |
|||
export interface BasicPageParams { |
|||
page: number; |
|||
pageSize: number; |
|||
} |
|||
|
|||
export interface BasicFetchResult<T extends any> { |
|||
items: T[]; |
|||
total: number; |
|||
} |
|||
@ -1,84 +0,0 @@ |
|||
/** |
|||
* @description: Login interface parameters |
|||
*/ |
|||
export interface LoginParams { |
|||
username: string; |
|||
password: string; |
|||
tenantId: string; |
|||
} |
|||
|
|||
/** |
|||
* @description: Get user information |
|||
*/ |
|||
export interface GetUserInfoByUserIdParams { |
|||
userId: string | number; |
|||
} |
|||
|
|||
export interface RoleInfo { |
|||
roleName: string; |
|||
value: string; |
|||
} |
|||
|
|||
/** |
|||
* @description: Login interface return value |
|||
*/ |
|||
export interface LoginResultModel { |
|||
userId: string | number; |
|||
token: string; |
|||
role: RoleInfo; |
|||
} |
|||
|
|||
/** |
|||
* @description: Get user information return value |
|||
*/ |
|||
export interface GetUserInfoByUserIdModel { |
|||
roles: RoleInfo[]; |
|||
// 用户id
|
|||
userId: string | number; |
|||
// 用户名
|
|||
username: string; |
|||
// 真实名字
|
|||
realName: string; |
|||
// 介绍
|
|||
desc?: string; |
|||
} |
|||
|
|||
/** |
|||
* @description: Login interface parameters |
|||
*/ |
|||
export interface LoginParams { |
|||
username: string; |
|||
password: string; |
|||
tenantId: string; |
|||
} |
|||
|
|||
export interface RoleInfo { |
|||
roleName: string; |
|||
value: string; |
|||
} |
|||
|
|||
/** |
|||
* @description: Login interface return value |
|||
*/ |
|||
export interface LoginResultModel { |
|||
userId: string | number; |
|||
token: string; |
|||
role: RoleInfo; |
|||
} |
|||
|
|||
/** |
|||
* @description: Get user information return value |
|||
*/ |
|||
export interface GetUserInfoModel { |
|||
roles: RoleInfo[]; |
|||
// 用户id
|
|||
userId: string | number; |
|||
// 用户名
|
|||
username: string; |
|||
// 真实名字
|
|||
realName: string; |
|||
// 头像
|
|||
avatar: string; |
|||
// 介绍
|
|||
desc?: string; |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
import { UploadApiResult } from './model/uploadModel'; |
|||
import { defHttp } from '/@/utils/http/axios'; |
|||
import { UploadFileParams } from '/#/axios'; |
|||
import { useGlobSetting } from '/@/hooks/setting'; |
|||
|
|||
const { uploadUrl = '' } = useGlobSetting(); |
|||
|
|||
/** |
|||
* @description: Upload interface |
|||
*/ |
|||
export function uploadApi( |
|||
params: UploadFileParams, |
|||
onUploadProgress: (progressEvent: ProgressEvent) => void |
|||
) { |
|||
return defHttp.uploadFile<UploadApiResult>( |
|||
{ |
|||
url: uploadUrl, |
|||
onUploadProgress, |
|||
}, |
|||
params |
|||
); |
|||
} |
|||
@ -1,98 +0,0 @@ |
|||
import { defHttp } from '/@/utils/http/axios'; |
|||
import { |
|||
LoginParams, |
|||
LoginResultModel, |
|||
GetUserInfoByUserIdParams, |
|||
GetUserInfoByUserIdModel, |
|||
} from './model/userModel'; |
|||
|
|||
import { ErrorMessageMode } from '/#/axios'; |
|||
import { |
|||
AccountServiceProxy, |
|||
LoginOutput, |
|||
LoginInput, |
|||
AbpApplicationConfigurationServiceProxy, |
|||
} from '/@/services/ServiceProxies'; |
|||
enum Api { |
|||
Login = '/login', |
|||
Logout = '/logout', |
|||
GetUserInfoById = '/getUserInfoById', |
|||
GetPermCodeByUserId = '/getPermCodeByUserId', |
|||
// GetUserInfo = '/getUserInfo',
|
|||
// GetPermCode = '/getPermCode',
|
|||
} |
|||
/** |
|||
* 登录 |
|||
* @param input |
|||
* @returns |
|||
*/ |
|||
export function login(input: LoginInput): Promise<LoginOutput> { |
|||
const _loginServiceProxy = new AccountServiceProxy(); |
|||
return _loginServiceProxy.login(input); |
|||
} |
|||
|
|||
/** |
|||
* sts登录 |
|||
* @param token |
|||
* @returns |
|||
*/ |
|||
export function id4(token: string): Promise<LoginOutput> { |
|||
const _accountServiceProxy = new AccountServiceProxy(); |
|||
return _accountServiceProxy.id4(token); |
|||
} |
|||
|
|||
/** |
|||
* sts登录 |
|||
* @param token |
|||
* @returns |
|||
*/ |
|||
export function github(code: string): Promise<LoginOutput> { |
|||
debugger; |
|||
const _accountServiceProxy = new AccountServiceProxy(); |
|||
return _accountServiceProxy.github(code); |
|||
} |
|||
|
|||
/** |
|||
* 获取应用程序配置 |
|||
* @returns |
|||
*/ |
|||
export function getAbpApplicationConfiguration() { |
|||
const _abpApplicationConfigurationServiceProxy = new AbpApplicationConfigurationServiceProxy(); |
|||
return _abpApplicationConfigurationServiceProxy.applicationConfiguration(); |
|||
} |
|||
|
|||
/** |
|||
* @description: getUserInfoById |
|||
*/ |
|||
export function getUserInfoById(params: GetUserInfoByUserIdParams) { |
|||
return defHttp.get<GetUserInfoByUserIdModel>({ |
|||
url: Api.GetUserInfoById, |
|||
params, |
|||
}); |
|||
} |
|||
|
|||
export function getPermCodeByUserId(params: GetUserInfoByUserIdParams) { |
|||
return defHttp.get<string[]>({ |
|||
url: Api.GetPermCodeByUserId, |
|||
params, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* @description: user login api |
|||
*/ |
|||
export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') { |
|||
return defHttp.post<LoginResultModel>( |
|||
{ |
|||
url: Api.Login, |
|||
params, |
|||
}, |
|||
{ |
|||
errorMessageMode: mode, |
|||
} |
|||
); |
|||
} |
|||
|
|||
export function doLogout() { |
|||
return defHttp.get({ url: Api.Logout }); |
|||
} |
|||
@ -1,76 +0,0 @@ |
|||
<template> |
|||
<div v-if="getShowDarkModeToggle" :class="getClass" @click="toggleDarkMode"> |
|||
<div :class="`${prefixCls}-inner`"> </div> |
|||
<SvgIcon size="14" name="sun" /> |
|||
<SvgIcon size="14" name="moon" /> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { computed, unref } from 'vue'; |
|||
import { SvgIcon } from '/@/components/Icon'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; |
|||
import { updateHeaderBgColor, updateSidebarBgColor } from '/@/logics/theme/updateBackground'; |
|||
import { updateDarkTheme } from '/@/logics/theme/dark'; |
|||
import { ThemeEnum } from '/@/enums/appEnum'; |
|||
|
|||
const { prefixCls } = useDesign('dark-switch'); |
|||
const { getDarkMode, setDarkMode, getShowDarkModeToggle } = useRootSetting(); |
|||
|
|||
const isDark = computed(() => getDarkMode.value === ThemeEnum.DARK); |
|||
|
|||
const getClass = computed(() => [ |
|||
prefixCls, |
|||
{ |
|||
[`${prefixCls}--dark`]: unref(isDark), |
|||
}, |
|||
]); |
|||
|
|||
function toggleDarkMode() { |
|||
const darkMode = getDarkMode.value === ThemeEnum.DARK ? ThemeEnum.LIGHT : ThemeEnum.DARK; |
|||
setDarkMode(darkMode); |
|||
updateDarkTheme(darkMode); |
|||
updateHeaderBgColor(); |
|||
updateSidebarBgColor(); |
|||
} |
|||
</script> |
|||
<style lang="less" scoped> |
|||
@prefix-cls: ~'@{namespace}-dark-switch'; |
|||
|
|||
html[data-theme='dark'] { |
|||
.@{prefix-cls} { |
|||
border: 1px solid rgb(196, 188, 188); |
|||
} |
|||
} |
|||
|
|||
.@{prefix-cls} { |
|||
position: relative; |
|||
display: flex; |
|||
width: 50px; |
|||
height: 26px; |
|||
padding: 0 6px; |
|||
margin-left: auto; |
|||
cursor: pointer; |
|||
background-color: #151515; |
|||
border-radius: 30px; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
|
|||
&-inner { |
|||
position: absolute; |
|||
z-index: 1; |
|||
width: 18px; |
|||
height: 18px; |
|||
background-color: #fff; |
|||
border-radius: 50%; |
|||
transition: transform 0.5s, background-color 0.5s; |
|||
will-change: transform; |
|||
} |
|||
|
|||
&--dark { |
|||
.@{prefix-cls}-inner { |
|||
transform: translateX(calc(100% + 2px)); |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,76 +0,0 @@ |
|||
<!-- |
|||
* @Author: Vben |
|||
* @Description: Multi-language switching component |
|||
--> |
|||
<template> |
|||
<Dropdown |
|||
placement="bottomCenter" |
|||
:trigger="['click']" |
|||
:dropMenuList="localeList" |
|||
:selectedKeys="selectedKeys" |
|||
@menuEvent="handleMenuEvent" |
|||
overlayClassName="app-locale-picker-overlay" |
|||
> |
|||
<span class="cursor-pointer flex items-center"> |
|||
<Icon icon="ion:language" /> |
|||
<span v-if="showText" class="ml-1">{{ getLocaleText }}</span> |
|||
</span> |
|||
</Dropdown> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import type { LocaleType } from '/#/config'; |
|||
import type { DropMenu } from '/@/components/Dropdown'; |
|||
import { ref, watchEffect, unref, computed } from 'vue'; |
|||
import { Dropdown } from '/@/components/Dropdown'; |
|||
import { Icon } from '/@/components/Icon'; |
|||
import { useLocale } from '/@/locales/useLocale'; |
|||
import { localeList } from '/@/settings/localeSetting'; |
|||
|
|||
const props = defineProps({ |
|||
/** |
|||
* Whether to display text |
|||
*/ |
|||
showText: { type: Boolean, default: true }, |
|||
/** |
|||
* Whether to refresh the interface when changing |
|||
*/ |
|||
reload: { type: Boolean }, |
|||
}); |
|||
|
|||
const selectedKeys = ref<string[]>([]); |
|||
|
|||
const { changeLocale, getLocale } = useLocale(); |
|||
|
|||
const getLocaleText = computed(() => { |
|||
const key = selectedKeys.value[0]; |
|||
if (!key) { |
|||
return ''; |
|||
} |
|||
return localeList.find((item) => item.event === key)?.text; |
|||
}); |
|||
|
|||
watchEffect(() => { |
|||
selectedKeys.value = [unref(getLocale)]; |
|||
}); |
|||
|
|||
async function toggleLocale(lang: LocaleType | string) { |
|||
await changeLocale(lang as LocaleType); |
|||
selectedKeys.value = [lang as string]; |
|||
props.reload && location.reload(); |
|||
} |
|||
|
|||
function handleMenuEvent(menu: DropMenu) { |
|||
if (unref(getLocale) === menu.event) { |
|||
return; |
|||
} |
|||
toggleLocale(menu.event as string); |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less"> |
|||
.app-locale-picker-overlay { |
|||
.ant-dropdown-menu-item { |
|||
min-width: 160px; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,92 +0,0 @@ |
|||
<!-- |
|||
* @Author: Vben |
|||
* @Description: logo component |
|||
--> |
|||
<template> |
|||
<div class="anticon" :class="getAppLogoClass" @click="goHome"> |
|||
<img src="../../../assets/images/logo.png" /> |
|||
<div class="ml-2 truncate md:opacity-100" :class="getTitleClass" v-show="showTitle"> |
|||
{{ title }} |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { computed, unref } from 'vue'; |
|||
import { useGlobSetting } from '/@/hooks/setting'; |
|||
import { useGo } from '/@/hooks/web/usePage'; |
|||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { PageEnum } from '/@/enums/pageEnum'; |
|||
import { useUserStore } from '/@/store/modules/user'; |
|||
|
|||
const props = defineProps({ |
|||
/** |
|||
* The theme of the current parent component |
|||
*/ |
|||
theme: { type: String, validator: (v: string) => ['light', 'dark'].includes(v) }, |
|||
/** |
|||
* Whether to show title |
|||
*/ |
|||
showTitle: { type: Boolean, default: true }, |
|||
/** |
|||
* The title is also displayed when the menu is collapsed |
|||
*/ |
|||
alwaysShowTitle: { type: Boolean }, |
|||
}); |
|||
|
|||
const { prefixCls } = useDesign('app-logo'); |
|||
const { getCollapsedShowTitle } = useMenuSetting(); |
|||
const userStore = useUserStore(); |
|||
const { title } = useGlobSetting(); |
|||
const go = useGo(); |
|||
|
|||
const getAppLogoClass = computed(() => [ |
|||
prefixCls, |
|||
props.theme, |
|||
{ 'collapsed-show-title': unref(getCollapsedShowTitle) }, |
|||
]); |
|||
|
|||
const getTitleClass = computed(() => [ |
|||
`${prefixCls}__title`, |
|||
{ |
|||
'xs:opacity-0': !props.alwaysShowTitle, |
|||
}, |
|||
]); |
|||
|
|||
function goHome() { |
|||
go(userStore.getUserInfo.homePath || PageEnum.BASE_HOME); |
|||
} |
|||
</script> |
|||
<style lang="less" scoped> |
|||
@prefix-cls: ~'@{namespace}-app-logo'; |
|||
|
|||
.@{prefix-cls} { |
|||
display: flex; |
|||
align-items: center; |
|||
padding-left: 7px; |
|||
cursor: pointer; |
|||
transition: all 0.2s ease; |
|||
|
|||
&.light { |
|||
border-bottom: 1px solid @border-color-base; |
|||
} |
|||
|
|||
&.collapsed-show-title { |
|||
padding-left: 20px; |
|||
} |
|||
|
|||
&.light &__title { |
|||
color: @primary-color; |
|||
} |
|||
|
|||
&.dark &__title { |
|||
color: @white; |
|||
} |
|||
|
|||
&__title { |
|||
font-size: 16px; |
|||
font-weight: 700; |
|||
transition: all 0.5s; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,56 +0,0 @@ |
|||
<template> |
|||
<div :class="`${prefixCls}`"> |
|||
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="ant-design:enter-outlined" /> |
|||
<span>{{ t('component.app.toSearch') }}</span> |
|||
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="ion:arrow-up-outline" /> |
|||
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="ion:arrow-down-outline" /> |
|||
<span>{{ t('component.app.toNavigate') }}</span> |
|||
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="mdi:keyboard-esc" /> |
|||
<span>{{ t('common.closeText') }}</span> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import AppSearchKeyItem from './AppSearchKeyItem.vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
const { prefixCls } = useDesign('app-search-footer'); |
|||
const { t } = useI18n(); |
|||
</script> |
|||
<style lang="less" scoped> |
|||
@prefix-cls: ~'@{namespace}-app-search-footer'; |
|||
|
|||
.@{prefix-cls} { |
|||
position: relative; |
|||
display: flex; |
|||
height: 44px; |
|||
padding: 0 16px; |
|||
font-size: 12px; |
|||
color: #666; |
|||
background-color: @component-background; |
|||
border-top: 1px solid @border-color-base; |
|||
border-radius: 0 0 16px 16px; |
|||
align-items: center; |
|||
flex-shrink: 0; |
|||
|
|||
&-item { |
|||
display: flex; |
|||
width: 20px; |
|||
height: 18px; |
|||
padding-bottom: 2px; |
|||
margin-right: 0.4em; |
|||
background-color: linear-gradient(-225deg, #d5dbe4, #f8f8f8); |
|||
border-radius: 2px; |
|||
box-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff, |
|||
0 1px 2px 1px rgba(30, 35, 90, 0.4); |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
&:nth-child(2), |
|||
&:nth-child(3), |
|||
&:nth-child(6) { |
|||
margin-left: 14px; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,266 +0,0 @@ |
|||
<template> |
|||
<Teleport to="body"> |
|||
<transition name="zoom-fade" mode="out-in"> |
|||
<div :class="getClass" @click.stop v-if="visible"> |
|||
<div :class="`${prefixCls}-content`" v-click-outside="handleClose"> |
|||
<div :class="`${prefixCls}-input__wrapper`"> |
|||
<a-input |
|||
:class="`${prefixCls}-input`" |
|||
:placeholder="t('common.searchText')" |
|||
ref="inputRef" |
|||
allow-clear |
|||
@change="handleSearch" |
|||
> |
|||
<template #prefix> |
|||
<SearchOutlined /> |
|||
</template> |
|||
</a-input> |
|||
<span :class="`${prefixCls}-cancel`" @click="handleClose"> |
|||
{{ t('common.cancelText') }} |
|||
</span> |
|||
</div> |
|||
|
|||
<div :class="`${prefixCls}-not-data`" v-show="getIsNotData"> |
|||
{{ t('component.app.searchNotData') }} |
|||
</div> |
|||
|
|||
<ul :class="`${prefixCls}-list`" v-show="!getIsNotData" ref="scrollWrap"> |
|||
<li |
|||
:ref="setRefs(index)" |
|||
v-for="(item, index) in searchResult" |
|||
:key="item.path" |
|||
:data-index="index" |
|||
@mouseenter="handleMouseenter" |
|||
@click="handleEnter" |
|||
:class="[ |
|||
`${prefixCls}-list__item`, |
|||
{ |
|||
[`${prefixCls}-list__item--active`]: activeIndex === index, |
|||
}, |
|||
]" |
|||
> |
|||
<div :class="`${prefixCls}-list__item-icon`"> |
|||
<Icon :icon="item.icon || 'mdi:form-select'" :size="20" /> |
|||
</div> |
|||
<div :class="`${prefixCls}-list__item-text`"> |
|||
{{ item.name }} |
|||
</div> |
|||
<div :class="`${prefixCls}-list__item-enter`"> |
|||
<Icon icon="ant-design:enter-outlined" :size="20" /> |
|||
</div> |
|||
</li> |
|||
</ul> |
|||
<AppSearchFooter /> |
|||
</div> |
|||
</div> |
|||
</transition> |
|||
</Teleport> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { computed, unref, ref, watch, nextTick } from 'vue'; |
|||
import { SearchOutlined } from '@ant-design/icons-vue'; |
|||
import AppSearchFooter from './AppSearchFooter.vue'; |
|||
import Icon from '/@/components/Icon'; |
|||
import vClickOutside from '/@/directives/clickOutside'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useRefs } from '/@/hooks/core/useRefs'; |
|||
import { useMenuSearch } from './useMenuSearch'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import { useAppInject } from '/@/hooks/web/useAppInject'; |
|||
|
|||
const props = defineProps({ |
|||
visible: { type: Boolean }, |
|||
}); |
|||
|
|||
const emit = defineEmits(['close']); |
|||
|
|||
const scrollWrap = ref(null); |
|||
const inputRef = ref<Nullable<HTMLElement>>(null); |
|||
|
|||
const { t } = useI18n(); |
|||
const { prefixCls } = useDesign('app-search-modal'); |
|||
const [refs, setRefs] = useRefs(); |
|||
const { getIsMobile } = useAppInject(); |
|||
|
|||
const { handleSearch, searchResult, keyword, activeIndex, handleEnter, handleMouseenter } = |
|||
useMenuSearch(refs, scrollWrap, emit); |
|||
|
|||
const getIsNotData = computed(() => !keyword || unref(searchResult).length === 0); |
|||
|
|||
const getClass = computed(() => { |
|||
return [ |
|||
prefixCls, |
|||
{ |
|||
[`${prefixCls}--mobile`]: unref(getIsMobile), |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
watch( |
|||
() => props.visible, |
|||
(visible: boolean) => { |
|||
visible && |
|||
nextTick(() => { |
|||
unref(inputRef)?.focus(); |
|||
}); |
|||
} |
|||
); |
|||
|
|||
function handleClose() { |
|||
searchResult.value = []; |
|||
emit('close'); |
|||
} |
|||
</script> |
|||
<style lang="less" scoped> |
|||
@prefix-cls: ~'@{namespace}-app-search-modal'; |
|||
@footer-prefix-cls: ~'@{namespace}-app-search-footer'; |
|||
.@{prefix-cls} { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 800; |
|||
display: flex; |
|||
width: 100%; |
|||
height: 100%; |
|||
padding-top: 50px; |
|||
background-color: rgba(0, 0, 0, 0.25); |
|||
justify-content: center; |
|||
|
|||
&--mobile { |
|||
padding: 0; |
|||
|
|||
> div { |
|||
width: 100%; |
|||
} |
|||
|
|||
.@{prefix-cls}-input { |
|||
width: calc(100% - 38px); |
|||
} |
|||
|
|||
.@{prefix-cls}-cancel { |
|||
display: inline-block; |
|||
} |
|||
|
|||
.@{prefix-cls}-content { |
|||
width: 100%; |
|||
height: 100%; |
|||
border-radius: 0; |
|||
} |
|||
|
|||
.@{footer-prefix-cls} { |
|||
display: none; |
|||
} |
|||
|
|||
.@{prefix-cls}-list { |
|||
height: calc(100% - 80px); |
|||
max-height: unset; |
|||
|
|||
&__item { |
|||
&-enter { |
|||
opacity: 0 !important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
&-content { |
|||
position: relative; |
|||
width: 632px; |
|||
margin: 0 auto auto auto; |
|||
background-color: @component-background; |
|||
border-radius: 16px; |
|||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); |
|||
flex-direction: column; |
|||
} |
|||
|
|||
&-input__wrapper { |
|||
display: flex; |
|||
padding: 14px 14px 0 14px; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
&-input { |
|||
width: 100%; |
|||
height: 48px; |
|||
font-size: 1.5em; |
|||
color: #1c1e21; |
|||
border-radius: 6px; |
|||
|
|||
span[role='img'] { |
|||
color: #999; |
|||
} |
|||
} |
|||
|
|||
&-cancel { |
|||
display: none; |
|||
font-size: 1em; |
|||
color: #666; |
|||
} |
|||
|
|||
&-not-data { |
|||
display: flex; |
|||
width: 100%; |
|||
height: 100px; |
|||
font-size: 0.9; |
|||
color: rgb(150 159 175); |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
&-list { |
|||
max-height: 472px; |
|||
padding: 0 14px; |
|||
padding-bottom: 20px; |
|||
margin: 0 auto; |
|||
margin-top: 14px; |
|||
overflow: auto; |
|||
|
|||
&__item { |
|||
position: relative; |
|||
display: flex; |
|||
width: 100%; |
|||
height: 56px; |
|||
padding-bottom: 4px; |
|||
padding-left: 14px; |
|||
margin-top: 8px; |
|||
font-size: 14px; |
|||
color: @text-color-base; |
|||
cursor: pointer; |
|||
background-color: @component-background; |
|||
border-radius: 4px; |
|||
box-shadow: 0 1px 3px 0 #d4d9e1; |
|||
align-items: center; |
|||
|
|||
> div:first-child, |
|||
> div:last-child { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
&--active { |
|||
color: #fff; |
|||
background-color: @primary-color; |
|||
|
|||
.@{prefix-cls}-list__item-enter { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
&-icon { |
|||
width: 30px; |
|||
} |
|||
|
|||
&-text { |
|||
flex: 1; |
|||
} |
|||
|
|||
&-enter { |
|||
width: 30px; |
|||
opacity: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,114 +0,0 @@ |
|||
<script lang="tsx"> |
|||
import type { CSSProperties, PropType } from 'vue'; |
|||
import { defineComponent, computed, unref } from 'vue'; |
|||
import { Tooltip } from 'ant-design-vue'; |
|||
import { InfoCircleOutlined } from '@ant-design/icons-vue'; |
|||
import { getPopupContainer } from '/@/utils'; |
|||
import { isString, isArray } from '/@/utils/is'; |
|||
import { getSlot } from '/@/utils/helper/tsxHelper'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
|
|||
const props = { |
|||
/** |
|||
* Help text max-width |
|||
* @default: 600px |
|||
*/ |
|||
maxWidth: { type: String, default: '600px' }, |
|||
/** |
|||
* Whether to display the serial number |
|||
* @default: false |
|||
*/ |
|||
showIndex: { type: Boolean }, |
|||
/** |
|||
* Help text font color |
|||
* @default: #ffffff |
|||
*/ |
|||
color: { type: String, default: '#ffffff' }, |
|||
/** |
|||
* Help text font size |
|||
* @default: 14px |
|||
*/ |
|||
fontSize: { type: String, default: '14px' }, |
|||
/** |
|||
* Help text list |
|||
*/ |
|||
placement: { type: String, default: 'right' }, |
|||
/** |
|||
* Help text list |
|||
*/ |
|||
text: { type: [Array, String] as PropType<string[] | string> }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicHelp', |
|||
components: { Tooltip }, |
|||
props, |
|||
setup(props, { slots }) { |
|||
const { prefixCls } = useDesign('basic-help'); |
|||
|
|||
const getTooltipStyle = computed( |
|||
(): CSSProperties => ({ color: props.color, fontSize: props.fontSize }) |
|||
); |
|||
|
|||
const getOverlayStyle = computed((): CSSProperties => ({ maxWidth: props.maxWidth })); |
|||
|
|||
function renderTitle() { |
|||
const textList = props.text; |
|||
|
|||
if (isString(textList)) { |
|||
return <p>{textList}</p>; |
|||
} |
|||
|
|||
if (isArray(textList)) { |
|||
return textList.map((text, index) => { |
|||
return ( |
|||
<p key={text}> |
|||
<> |
|||
{props.showIndex ? `${index + 1}. ` : ''} |
|||
{text} |
|||
</> |
|||
</p> |
|||
); |
|||
}); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
return () => { |
|||
return ( |
|||
<Tooltip |
|||
overlayClassName={`${prefixCls}__wrap`} |
|||
title={<div style={unref(getTooltipStyle)}>{renderTitle()}</div>} |
|||
autoAdjustOverflow={true} |
|||
overlayStyle={unref(getOverlayStyle)} |
|||
placement={props.placement as 'right'} |
|||
getPopupContainer={() => getPopupContainer()} |
|||
> |
|||
<span class={prefixCls}>{getSlot(slots) || <InfoCircleOutlined />}</span> |
|||
</Tooltip> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-basic-help'; |
|||
|
|||
.@{prefix-cls} { |
|||
display: inline-block; |
|||
margin-left: 6px; |
|||
font-size: 14px; |
|||
color: @text-color-help-dark; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
color: @primary-color; |
|||
} |
|||
|
|||
&__wrap { |
|||
p { |
|||
margin-bottom: 0; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,39 +0,0 @@ |
|||
<template> |
|||
<Button v-bind="getBindValue" :class="getButtonClass" @click="onClick"> |
|||
<template #default="data"> |
|||
<Icon :icon="preIcon" v-if="preIcon" :size="iconSize" /> |
|||
<slot v-bind="data || {}"></slot> |
|||
<Icon :icon="postIcon" v-if="postIcon" :size="iconSize" /> |
|||
</template> |
|||
</Button> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
export default { |
|||
name: 'AButton', |
|||
inheritAttrs: false, |
|||
}; |
|||
</script> |
|||
<script lang="ts" setup> |
|||
import { computed, unref } from 'vue'; |
|||
import { Button } from 'ant-design-vue'; |
|||
import Icon from '/@/components/Icon/src/Icon.vue'; |
|||
import { buttonProps } from './props'; |
|||
import { useAttrs } from '/@/hooks/core/useAttrs'; |
|||
|
|||
const props = defineProps(buttonProps); |
|||
// get component class |
|||
const attrs = useAttrs({ excludeDefaultKeys: false }); |
|||
const getButtonClass = computed(() => { |
|||
const { color, disabled } = props; |
|||
return [ |
|||
{ |
|||
[`ant-btn-${color}`]: !!color, |
|||
[`is-disabled`]: disabled, |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
// get inherit binding value |
|||
const getBindValue = computed(() => ({ ...unref(attrs), ...props })); |
|||
</script> |
|||
@ -1,54 +0,0 @@ |
|||
<script lang="ts"> |
|||
import { computed, defineComponent, h, unref } from 'vue'; |
|||
import BasicButton from './BasicButton.vue'; |
|||
import { Popconfirm } from 'ant-design-vue'; |
|||
import { extendSlots } from '/@/utils/helper/tsxHelper'; |
|||
import { omit } from 'lodash-es'; |
|||
import { useAttrs } from '/@/hooks/core/useAttrs'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
const props = { |
|||
/** |
|||
* Whether to enable the drop-down menu |
|||
* @default: true |
|||
*/ |
|||
enable: { |
|||
type: Boolean, |
|||
default: true, |
|||
}, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'PopButton', |
|||
inheritAttrs: false, |
|||
props, |
|||
setup(props, { slots }) { |
|||
const { t } = useI18n(); |
|||
const attrs = useAttrs(); |
|||
|
|||
// get inherit binding value |
|||
const getBindValues = computed(() => { |
|||
return Object.assign( |
|||
{ |
|||
okText: t('common.okText'), |
|||
cancelText: t('common.cancelText'), |
|||
}, |
|||
{ ...props, ...unref(attrs) } |
|||
); |
|||
}); |
|||
|
|||
return () => { |
|||
const bindValues = omit(unref(getBindValues), 'icon'); |
|||
const btnBind = omit(bindValues, 'title') as Recordable; |
|||
if (btnBind.disabled) btnBind.color = ''; |
|||
const Button = h(BasicButton, btnBind, extendSlots(slots)); |
|||
|
|||
// If it is not enabled, it is a normal button |
|||
if (!props.enable) { |
|||
return Button; |
|||
} |
|||
return h(Popconfirm, bindValues, { default: () => Button }); |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,6 +0,0 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import codeEditor from './src/CodeEditor.vue'; |
|||
import jsonPreview from './src/json-preview/JsonPreview.vue'; |
|||
|
|||
export const CodeEditor = withInstall(codeEditor); |
|||
export const JsonPreview = withInstall(jsonPreview); |
|||
@ -1,46 +0,0 @@ |
|||
<template> |
|||
<div class="h-full"> |
|||
<CodeMirrorEditor |
|||
:value="getValue" |
|||
@change="handleValueChange" |
|||
:mode="mode" |
|||
:readonly="readonly" |
|||
/> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
const MODE = { |
|||
JSON: 'application/json', |
|||
html: 'htmlmixed', |
|||
js: 'javascript', |
|||
}; |
|||
</script> |
|||
<script lang="ts" setup> |
|||
import { computed } from 'vue'; |
|||
import CodeMirrorEditor from './codemirror/CodeMirror.vue'; |
|||
import { isString } from '/@/utils/is'; |
|||
|
|||
const props = defineProps({ |
|||
value: { type: [Object, String] as PropType<Record<string, any> | string> }, |
|||
mode: { type: String, default: MODE.JSON }, |
|||
readonly: { type: Boolean }, |
|||
}); |
|||
|
|||
const emit = defineEmits(['change', 'update:value']); |
|||
|
|||
const getValue = computed(() => { |
|||
const { value, mode } = props; |
|||
if (mode !== MODE.JSON) { |
|||
return value as string; |
|||
} |
|||
return isString(value) |
|||
? JSON.stringify(JSON.parse(value), null, 2) |
|||
: JSON.stringify(value, null, 2); |
|||
}); |
|||
|
|||
function handleValueChange(v) { |
|||
emit('update:value', v); |
|||
emit('change', v); |
|||
} |
|||
</script> |
|||
@ -1,105 +0,0 @@ |
|||
<template> |
|||
<div class="relative !h-full w-full overflow-hidden" ref="el"> </div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { ref, onMounted, onUnmounted, watchEffect, watch, unref, nextTick } from 'vue'; |
|||
import { useDebounceFn } from '@vueuse/core'; |
|||
import { useAppStore } from '/@/store/modules/app'; |
|||
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn'; |
|||
import CodeMirror from 'codemirror'; |
|||
// css |
|||
import './codemirror.css'; |
|||
import 'codemirror/theme/idea.css'; |
|||
import 'codemirror/theme/material-palenight.css'; |
|||
// modes |
|||
import 'codemirror/mode/javascript/javascript'; |
|||
import 'codemirror/mode/css/css'; |
|||
import 'codemirror/mode/htmlmixed/htmlmixed'; |
|||
|
|||
const props = defineProps({ |
|||
mode: { type: String, default: 'application/json' }, |
|||
value: { type: String, default: '' }, |
|||
readonly: { type: Boolean, default: false }, |
|||
}); |
|||
|
|||
const emit = defineEmits(['change']); |
|||
|
|||
const el = ref(); |
|||
let editor: Nullable<CodeMirror.Editor>; |
|||
|
|||
const debounceRefresh = useDebounceFn(refresh, 100); |
|||
const appStore = useAppStore(); |
|||
|
|||
watch( |
|||
() => props.value, |
|||
async (value) => { |
|||
await nextTick(); |
|||
const oldValue = editor?.getValue(); |
|||
if (value !== oldValue) { |
|||
editor?.setValue(value ? value : ''); |
|||
} |
|||
}, |
|||
{ flush: 'post' } |
|||
); |
|||
|
|||
watchEffect(() => { |
|||
editor?.setOption('mode', props.mode); |
|||
}); |
|||
|
|||
watch( |
|||
() => appStore.getDarkMode, |
|||
async () => { |
|||
setTheme(); |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
} |
|||
); |
|||
|
|||
function setTheme() { |
|||
unref(editor)?.setOption( |
|||
'theme', |
|||
appStore.getDarkMode === 'light' ? 'idea' : 'material-palenight' |
|||
); |
|||
} |
|||
|
|||
function refresh() { |
|||
editor?.refresh(); |
|||
} |
|||
|
|||
async function init() { |
|||
const addonOptions = { |
|||
autoCloseBrackets: true, |
|||
autoCloseTags: true, |
|||
foldGutter: true, |
|||
gutters: ['CodeMirror-linenumbers'], |
|||
}; |
|||
|
|||
editor = CodeMirror(el.value!, { |
|||
value: '', |
|||
mode: props.mode, |
|||
readOnly: props.readonly, |
|||
tabSize: 2, |
|||
theme: 'material-palenight', |
|||
lineWrapping: true, |
|||
lineNumbers: true, |
|||
...addonOptions, |
|||
}); |
|||
editor?.setValue(props.value); |
|||
setTheme(); |
|||
editor?.on('change', () => { |
|||
emit('change', editor?.getValue()); |
|||
}); |
|||
} |
|||
|
|||
onMounted(async () => { |
|||
await nextTick(); |
|||
init(); |
|||
useWindowSizeFn(debounceRefresh); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
editor = null; |
|||
}); |
|||
</script> |
|||
@ -1,539 +0,0 @@ |
|||
/* BASICS */ |
|||
|
|||
.CodeMirror { |
|||
--base: #545281; |
|||
--comment: hsl(210, 25%, 60%); |
|||
--keyword: #af4ab1; |
|||
--variable: #0055d1; |
|||
--function: #c25205; |
|||
--string: #2ba46d; |
|||
--number: #c25205; |
|||
--tags: #d00; |
|||
--qualifier: #ff6032; |
|||
--important: var(--string); |
|||
|
|||
position: relative; |
|||
height: auto; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
font-family: var(--font-code); |
|||
background: white; |
|||
direction: ltr; |
|||
} |
|||
|
|||
/* PADDING */ |
|||
|
|||
.CodeMirror-lines { |
|||
min-height: 1px; /* prevents collapsing before first draw */ |
|||
padding: 4px 0; /* Vertical padding around content */ |
|||
cursor: text; |
|||
} |
|||
|
|||
.CodeMirror-scrollbar-filler, |
|||
.CodeMirror-gutter-filler { |
|||
background-color: white; /* The little square between H and V scrollbars */ |
|||
} |
|||
|
|||
/* GUTTER */ |
|||
|
|||
.CodeMirror-gutters { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 3; |
|||
min-height: 100%; |
|||
white-space: nowrap; |
|||
background-color: transparent; |
|||
border-right: 1px solid #ddd; |
|||
} |
|||
|
|||
.CodeMirror-linenumber { |
|||
min-width: 20px; |
|||
padding: 0 3px 0 5px; |
|||
color: var(--comment); |
|||
text-align: right; |
|||
white-space: nowrap; |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
.CodeMirror-guttermarker { |
|||
color: black; |
|||
} |
|||
|
|||
.CodeMirror-guttermarker-subtle { |
|||
color: #999; |
|||
} |
|||
|
|||
/* FOLD GUTTER */ |
|||
|
|||
.CodeMirror-foldmarker { |
|||
font-family: arial; |
|||
line-height: 0.3; |
|||
color: #414141; |
|||
text-shadow: #f96 1px 1px 2px, #f96 -1px -1px 2px, #f96 1px -1px 2px, #f96 -1px 1px 2px; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter { |
|||
width: 0.7em; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter-open, |
|||
.CodeMirror-foldgutter-folded { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter-open::after, |
|||
.CodeMirror-foldgutter-folded::after { |
|||
position: relative; |
|||
top: -0.1em; |
|||
display: inline-block; |
|||
font-size: 0.8em; |
|||
content: '>'; |
|||
opacity: 0.8; |
|||
transform: rotate(90deg); |
|||
transition: transform 0.2s; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter-folded::after { |
|||
transform: none; |
|||
} |
|||
|
|||
/* CURSOR */ |
|||
|
|||
.CodeMirror-cursor { |
|||
position: absolute; |
|||
width: 0; |
|||
pointer-events: none; |
|||
border-right: none; |
|||
border-left: 1px solid black; |
|||
} |
|||
|
|||
/* Shown when moving in bi-directional text */ |
|||
.CodeMirror div.CodeMirror-secondarycursor { |
|||
border-left: 1px solid silver; |
|||
} |
|||
|
|||
.cm-fat-cursor .CodeMirror-cursor { |
|||
width: auto; |
|||
background: #7e7; |
|||
border: 0 !important; |
|||
} |
|||
|
|||
.cm-fat-cursor div.CodeMirror-cursors { |
|||
z-index: 1; |
|||
} |
|||
|
|||
.cm-fat-cursor-mark { |
|||
background-color: rgba(20, 255, 20, 0.5); |
|||
-webkit-animation: blink 1.06s steps(1) infinite; |
|||
-moz-animation: blink 1.06s steps(1) infinite; |
|||
animation: blink 1.06s steps(1) infinite; |
|||
} |
|||
|
|||
.cm-animate-fat-cursor { |
|||
width: auto; |
|||
background-color: #7e7; |
|||
border: 0; |
|||
-webkit-animation: blink 1.06s steps(1) infinite; |
|||
-moz-animation: blink 1.06s steps(1) infinite; |
|||
animation: blink 1.06s steps(1) infinite; |
|||
} |
|||
@-moz-keyframes blink { |
|||
50% { |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
@-webkit-keyframes blink { |
|||
50% { |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
@keyframes blink { |
|||
50% { |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
|
|||
.cm-tab { |
|||
display: inline-block; |
|||
text-decoration: inherit; |
|||
} |
|||
|
|||
.CodeMirror-rulers { |
|||
position: absolute; |
|||
top: -50px; |
|||
right: 0; |
|||
bottom: -20px; |
|||
left: 0; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.CodeMirror-ruler { |
|||
position: absolute; |
|||
top: 0; |
|||
bottom: 0; |
|||
border-left: 1px solid #ccc; |
|||
} |
|||
|
|||
/* DEFAULT THEME */ |
|||
.cm-s-default.CodeMirror { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.cm-s-default .cm-header { |
|||
color: blue; |
|||
} |
|||
|
|||
.cm-s-default .cm-quote { |
|||
color: #090; |
|||
} |
|||
|
|||
.cm-negative { |
|||
color: #d44; |
|||
} |
|||
|
|||
.cm-positive { |
|||
color: #292; |
|||
} |
|||
|
|||
.cm-header, |
|||
.cm-strong { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.cm-em { |
|||
font-style: italic; |
|||
} |
|||
|
|||
.cm-link { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
.cm-strikethrough { |
|||
text-decoration: line-through; |
|||
} |
|||
|
|||
.cm-s-default .cm-atom, |
|||
.cm-s-default .cm-def, |
|||
.cm-s-default .cm-property, |
|||
.cm-s-default .cm-variable-2, |
|||
.cm-s-default .cm-variable-3, |
|||
.cm-s-default .cm-punctuation { |
|||
color: var(--base); |
|||
} |
|||
|
|||
.cm-s-default .cm-hr, |
|||
.cm-s-default .cm-comment { |
|||
color: var(--comment); |
|||
} |
|||
|
|||
.cm-s-default .cm-attribute, |
|||
.cm-s-default .cm-keyword { |
|||
color: var(--keyword); |
|||
} |
|||
|
|||
.cm-s-default .cm-variable { |
|||
color: var(--variable); |
|||
} |
|||
|
|||
.cm-s-default .cm-bracket, |
|||
.cm-s-default .cm-tag { |
|||
color: var(--tags); |
|||
} |
|||
|
|||
.cm-s-default .cm-number { |
|||
color: var(--number); |
|||
} |
|||
|
|||
.cm-s-default .cm-string, |
|||
.cm-s-default .cm-string-2 { |
|||
color: var(--string); |
|||
} |
|||
|
|||
.cm-s-default .cm-type { |
|||
color: #085; |
|||
} |
|||
|
|||
.cm-s-default .cm-meta { |
|||
color: #555; |
|||
} |
|||
|
|||
.cm-s-default .cm-qualifier { |
|||
color: var(--qualifier); |
|||
} |
|||
|
|||
.cm-s-default .cm-builtin { |
|||
color: #7539ff; |
|||
} |
|||
|
|||
.cm-s-default .cm-link { |
|||
color: var(--flash); |
|||
} |
|||
|
|||
.cm-s-default .cm-error { |
|||
color: #ff008c; |
|||
} |
|||
|
|||
.cm-invalidchar { |
|||
color: #ff008c; |
|||
} |
|||
|
|||
.CodeMirror-composing { |
|||
border-bottom: 2px solid; |
|||
} |
|||
|
|||
/* Default styles for common addons */ |
|||
|
|||
div.CodeMirror span.CodeMirror-matchingbracket { |
|||
color: #0b0; |
|||
} |
|||
|
|||
div.CodeMirror span.CodeMirror-nonmatchingbracket { |
|||
color: #a22; |
|||
} |
|||
|
|||
.CodeMirror-matchingtag { |
|||
background: rgba(255, 150, 0, 0.3); |
|||
} |
|||
|
|||
.CodeMirror-activeline-background { |
|||
background: #e8f2ff; |
|||
} |
|||
|
|||
/* STOP */ |
|||
|
|||
/* The rest of this file contains styles related to the mechanics of |
|||
the editor. You probably shouldn't touch them. */ |
|||
|
|||
.CodeMirror-scroll { |
|||
position: relative; |
|||
height: 100%; |
|||
padding-bottom: 30px; |
|||
margin-right: -30px; |
|||
|
|||
/* 30px is the magic margin used to hide the element's real scrollbars */ |
|||
|
|||
/* See overflow: hidden in .CodeMirror */ |
|||
margin-bottom: -30px; |
|||
overflow: scroll !important; /* Things will break if this is overridden */ |
|||
outline: none; /* Prevent dragging from highlighting the element */ |
|||
} |
|||
|
|||
.CodeMirror-sizer { |
|||
position: relative; |
|||
margin-bottom: 20px !important; |
|||
border-right: 30px solid transparent; |
|||
} |
|||
|
|||
/* The fake, visible scrollbars. Used to force redraw during scrolling |
|||
before actual scrolling happens, thus preventing shaking and |
|||
flickering artifacts. */ |
|||
.CodeMirror-vscrollbar, |
|||
.CodeMirror-hscrollbar, |
|||
.CodeMirror-scrollbar-filler, |
|||
.CodeMirror-gutter-filler { |
|||
position: absolute; |
|||
z-index: 6; |
|||
display: none; |
|||
} |
|||
|
|||
.CodeMirror-vscrollbar { |
|||
top: 0; |
|||
right: 0; |
|||
overflow-x: hidden; |
|||
overflow-y: scroll; |
|||
} |
|||
|
|||
.CodeMirror-hscrollbar { |
|||
bottom: 0; |
|||
left: 0; |
|||
overflow-x: scroll; |
|||
overflow-y: hidden; |
|||
} |
|||
|
|||
.CodeMirror-scrollbar-filler { |
|||
right: 0; |
|||
bottom: 0; |
|||
} |
|||
|
|||
.CodeMirror-gutter-filler { |
|||
bottom: 0; |
|||
left: 0; |
|||
} |
|||
|
|||
.CodeMirror-gutter { |
|||
display: inline-block; |
|||
height: 100%; |
|||
margin-bottom: -30px; |
|||
white-space: normal; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
.CodeMirror-gutter-wrapper { |
|||
position: absolute; |
|||
z-index: 4; |
|||
background: none !important; |
|||
border: none !important; |
|||
} |
|||
|
|||
.CodeMirror-gutter-background { |
|||
position: absolute; |
|||
top: 0; |
|||
bottom: 0; |
|||
z-index: 4; |
|||
} |
|||
|
|||
.CodeMirror-gutter-elt { |
|||
position: absolute; |
|||
z-index: 4; |
|||
cursor: default; |
|||
} |
|||
|
|||
.CodeMirror-gutter-wrapper ::selection { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.CodeMirror-gutter-wrapper ::-moz-selection { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.CodeMirror pre { |
|||
position: relative; |
|||
z-index: 2; |
|||
padding: 0 4px; /* Horizontal padding of content */ |
|||
margin: 0; |
|||
overflow: visible; |
|||
font-family: inherit; |
|||
font-size: inherit; |
|||
line-height: inherit; |
|||
color: inherit; |
|||
word-wrap: normal; |
|||
white-space: pre; |
|||
background: transparent; |
|||
border-width: 0; |
|||
|
|||
/* Reset some styles that the rest of the page might have set */ |
|||
-moz-border-radius: 0; |
|||
-webkit-border-radius: 0; |
|||
border-radius: 0; |
|||
-webkit-tap-highlight-color: transparent; |
|||
-webkit-font-variant-ligatures: contextual; |
|||
font-variant-ligatures: contextual; |
|||
} |
|||
|
|||
.CodeMirror-wrap pre { |
|||
word-break: normal; |
|||
word-wrap: break-word; |
|||
white-space: pre-wrap; |
|||
} |
|||
|
|||
.CodeMirror-linebackground { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
z-index: 0; |
|||
} |
|||
|
|||
.CodeMirror-linewidget { |
|||
position: relative; |
|||
z-index: 2; |
|||
padding: 0.1px; /* Force widget margins to stay inside of the container */ |
|||
} |
|||
|
|||
.CodeMirror-rtl pre { |
|||
direction: rtl; |
|||
} |
|||
|
|||
.CodeMirror-code { |
|||
outline: none; |
|||
} |
|||
|
|||
/* Force content-box sizing for the elements where we expect it */ |
|||
.CodeMirror-scroll, |
|||
.CodeMirror-sizer, |
|||
.CodeMirror-gutter, |
|||
.CodeMirror-gutters, |
|||
.CodeMirror-linenumber { |
|||
-moz-box-sizing: content-box; |
|||
box-sizing: content-box; |
|||
} |
|||
|
|||
.CodeMirror-measure { |
|||
position: absolute; |
|||
width: 100%; |
|||
height: 0; |
|||
overflow: hidden; |
|||
visibility: hidden; |
|||
} |
|||
|
|||
.CodeMirror-measure pre { |
|||
position: static; |
|||
} |
|||
|
|||
div.CodeMirror-cursors { |
|||
position: relative; |
|||
z-index: 3; |
|||
visibility: hidden; |
|||
} |
|||
|
|||
div.CodeMirror-dragcursors { |
|||
visibility: visible; |
|||
} |
|||
|
|||
.CodeMirror-focused div.CodeMirror-cursors { |
|||
visibility: visible; |
|||
} |
|||
|
|||
.CodeMirror-selected { |
|||
background: #d9d9d9; |
|||
} |
|||
|
|||
.CodeMirror-focused .CodeMirror-selected { |
|||
background: #d7d4f0; |
|||
} |
|||
|
|||
.CodeMirror-crosshair { |
|||
cursor: crosshair; |
|||
} |
|||
|
|||
.CodeMirror-line::selection, |
|||
.CodeMirror-line > span::selection, |
|||
.CodeMirror-line > span > span::selection { |
|||
background: #d7d4f0; |
|||
} |
|||
|
|||
.CodeMirror-line::-moz-selection, |
|||
.CodeMirror-line > span::-moz-selection, |
|||
.CodeMirror-line > span > span::-moz-selection { |
|||
background: #d7d4f0; |
|||
} |
|||
|
|||
.cm-searching { |
|||
background-color: #ffa; |
|||
background-color: rgba(255, 255, 0, 0.4); |
|||
} |
|||
|
|||
/* Used to force a border model for a node */ |
|||
.cm-force-border { |
|||
padding-right: 0.1px; |
|||
} |
|||
|
|||
@media print { |
|||
/* Hide the cursor when printing */ |
|||
.CodeMirror div.CodeMirror-cursors { |
|||
visibility: hidden; |
|||
} |
|||
} |
|||
|
|||
/* See issue #2901 */ |
|||
.cm-tab-wrap-hack::after { |
|||
content: ''; |
|||
} |
|||
|
|||
/* Help users use markselection to safely style text background */ |
|||
span.CodeMirror-selectedtext { |
|||
background: none; |
|||
} |
|||
@ -1,93 +0,0 @@ |
|||
<template> |
|||
<Scrollbar ref="scrollbarRef" class="scroll-container" v-bind="$attrs"> |
|||
<slot></slot> |
|||
</Scrollbar> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent, ref, unref, nextTick } from 'vue'; |
|||
import { Scrollbar, ScrollbarType } from '/@/components/Scrollbar'; |
|||
import { useScrollTo } from '/@/hooks/event/useScrollTo'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'ScrollContainer', |
|||
components: { Scrollbar }, |
|||
setup() { |
|||
const scrollbarRef = ref<Nullable<ScrollbarType>>(null); |
|||
|
|||
/** |
|||
* Scroll to the specified position |
|||
*/ |
|||
function scrollTo(to: number, duration = 500) { |
|||
const scrollbar = unref(scrollbarRef); |
|||
if (!scrollbar) { |
|||
return; |
|||
} |
|||
nextTick(() => { |
|||
const wrap = unref(scrollbar.wrap); |
|||
if (!wrap) { |
|||
return; |
|||
} |
|||
const { start } = useScrollTo({ |
|||
el: wrap, |
|||
to, |
|||
duration, |
|||
}); |
|||
start(); |
|||
}); |
|||
} |
|||
|
|||
function getScrollWrap() { |
|||
const scrollbar = unref(scrollbarRef); |
|||
if (!scrollbar) { |
|||
return null; |
|||
} |
|||
return scrollbar.wrap; |
|||
} |
|||
|
|||
/** |
|||
* Scroll to the bottom |
|||
*/ |
|||
function scrollBottom() { |
|||
const scrollbar = unref(scrollbarRef); |
|||
if (!scrollbar) { |
|||
return; |
|||
} |
|||
nextTick(() => { |
|||
const wrap = unref(scrollbar.wrap); |
|||
if (!wrap) { |
|||
return; |
|||
} |
|||
const scrollHeight = wrap.scrollHeight as number; |
|||
const { start } = useScrollTo({ |
|||
el: wrap, |
|||
to: scrollHeight, |
|||
}); |
|||
start(); |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
scrollbarRef, |
|||
scrollTo, |
|||
scrollBottom, |
|||
getScrollWrap, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
.scroll-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
|
|||
.scrollbar__wrap { |
|||
margin-bottom: 18px !important; |
|||
overflow-x: hidden; |
|||
} |
|||
|
|||
.scrollbar__view { |
|||
box-sizing: border-box; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,105 +0,0 @@ |
|||
<template> |
|||
<div :class="prefixCls"> |
|||
<CollapseHeader v-bind="$props" :prefixCls="prefixCls" :show="show" @expand="handleExpand"> |
|||
<template #title> |
|||
<slot name="title"></slot> |
|||
</template> |
|||
<template #action> |
|||
<slot name="action"></slot> |
|||
</template> |
|||
</CollapseHeader> |
|||
|
|||
<div class="p-2"> |
|||
<CollapseTransition :enable="canExpan"> |
|||
<Skeleton v-if="loading" :active="loading" /> |
|||
<div :class="`${prefixCls}__body`" v-else v-show="show"> |
|||
<slot></slot> |
|||
</div> |
|||
</CollapseTransition> |
|||
</div> |
|||
<div :class="`${prefixCls}__footer`" v-if="$slots.footer"> |
|||
<slot name="footer"></slot> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import type { PropType } from 'vue'; |
|||
import { ref } from 'vue'; |
|||
// component |
|||
import { Skeleton } from 'ant-design-vue'; |
|||
import { CollapseTransition } from '/@/components/Transition'; |
|||
import CollapseHeader from './CollapseHeader.vue'; |
|||
import { triggerWindowResize } from '/@/utils/event'; |
|||
// hook |
|||
import { useTimeoutFn } from '/@/hooks/core/useTimeout'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
|
|||
const props = defineProps({ |
|||
title: { type: String, default: '' }, |
|||
loading: { type: Boolean }, |
|||
/** |
|||
* Can it be expanded |
|||
*/ |
|||
canExpan: { type: Boolean, default: true }, |
|||
/** |
|||
* Warm reminder on the right side of the title |
|||
*/ |
|||
helpMessage: { |
|||
type: [Array, String] as PropType<string[] | string>, |
|||
default: '', |
|||
}, |
|||
/** |
|||
* Whether to trigger window.resize when expanding and contracting, |
|||
* Can adapt to tables and forms, when the form shrinks, the form triggers resize to adapt to the height |
|||
*/ |
|||
triggerWindowResize: { type: Boolean }, |
|||
/** |
|||
* Delayed loading time |
|||
*/ |
|||
lazyTime: { type: Number, default: 0 }, |
|||
}); |
|||
|
|||
const show = ref(true); |
|||
|
|||
const { prefixCls } = useDesign('collapse-container'); |
|||
|
|||
/** |
|||
* @description: Handling development events |
|||
*/ |
|||
function handleExpand() { |
|||
show.value = !show.value; |
|||
if (props.triggerWindowResize) { |
|||
// 200 milliseconds here is because the expansion has animation, |
|||
useTimeoutFn(triggerWindowResize, 200); |
|||
} |
|||
} |
|||
</script> |
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-collapse-container'; |
|||
|
|||
.@{prefix-cls} { |
|||
background-color: @component-background; |
|||
border-radius: 2px; |
|||
transition: all 0.3s ease-in-out; |
|||
|
|||
&__header { |
|||
display: flex; |
|||
height: 32px; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
border-bottom: 1px solid @border-color-light; |
|||
} |
|||
|
|||
&__footer { |
|||
border-top: 1px solid @border-color-light; |
|||
} |
|||
|
|||
&__action { |
|||
display: flex; |
|||
text-align: right; |
|||
flex: 1; |
|||
align-items: center; |
|||
justify-content: flex-end; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,207 +0,0 @@ |
|||
<script lang="tsx"> |
|||
import type { ContextMenuItem, ItemContentProps, Axis } from './typing'; |
|||
import type { FunctionalComponent, CSSProperties } from 'vue'; |
|||
import { defineComponent, nextTick, onMounted, computed, ref, unref, onUnmounted } from 'vue'; |
|||
import Icon from '/@/components/Icon'; |
|||
import { Menu, Divider } from 'ant-design-vue'; |
|||
|
|||
const prefixCls = 'context-menu'; |
|||
|
|||
const props = { |
|||
width: { type: Number, default: 156 }, |
|||
customEvent: { type: Object as PropType<Event>, default: null }, |
|||
styles: { type: Object as PropType<CSSProperties> }, |
|||
showIcon: { type: Boolean, default: true }, |
|||
axis: { |
|||
// The position of the right mouse button click |
|||
type: Object as PropType<Axis>, |
|||
default() { |
|||
return { x: 0, y: 0 }; |
|||
}, |
|||
}, |
|||
items: { |
|||
// The most important list, if not, will not be displayed |
|||
type: Array as PropType<ContextMenuItem[]>, |
|||
default() { |
|||
return []; |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
const ItemContent: FunctionalComponent<ItemContentProps> = (props) => { |
|||
const { item } = props; |
|||
return ( |
|||
<span |
|||
style="display: inline-block; width: 100%; " |
|||
class="px-4" |
|||
onClick={props.handler.bind(null, item)} |
|||
> |
|||
{props.showIcon && item.icon && <Icon class="mr-2" icon={item.icon} />} |
|||
<span>{item.label}</span> |
|||
</span> |
|||
); |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'ContextMenu', |
|||
props, |
|||
setup(props) { |
|||
const wrapRef = ref<ElRef>(null); |
|||
const showRef = ref(false); |
|||
|
|||
const getStyle = computed((): CSSProperties => { |
|||
const { axis, items, styles, width } = props; |
|||
const { x, y } = axis || { x: 0, y: 0 }; |
|||
const menuHeight = (items || []).length * 40; |
|||
const menuWidth = width; |
|||
const body = document.body; |
|||
|
|||
const left = body.clientWidth < x + menuWidth ? x - menuWidth : x; |
|||
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y; |
|||
return { |
|||
...styles, |
|||
width: `${width}px`, |
|||
left: `${left + 1}px`, |
|||
top: `${top + 1}px`, |
|||
}; |
|||
}); |
|||
|
|||
onMounted(() => { |
|||
nextTick(() => (showRef.value = true)); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
const el = unref(wrapRef); |
|||
el && document.body.removeChild(el); |
|||
}); |
|||
|
|||
function handleAction(item: ContextMenuItem, e: MouseEvent) { |
|||
const { handler, disabled } = item; |
|||
if (disabled) { |
|||
return; |
|||
} |
|||
showRef.value = false; |
|||
e?.stopPropagation(); |
|||
e?.preventDefault(); |
|||
handler?.(); |
|||
} |
|||
|
|||
function renderMenuItem(items: ContextMenuItem[]) { |
|||
return items.map((item) => { |
|||
const { disabled, label, children, divider = false } = item; |
|||
|
|||
const contentProps = { |
|||
item, |
|||
handler: handleAction, |
|||
showIcon: props.showIcon, |
|||
}; |
|||
|
|||
if (!children || children.length === 0) { |
|||
return ( |
|||
<> |
|||
<Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}> |
|||
<ItemContent {...contentProps} /> |
|||
</Menu.Item> |
|||
{divider ? <Divider key={`d-${label}`} /> : null} |
|||
</> |
|||
); |
|||
} |
|||
if (!unref(showRef)) return null; |
|||
|
|||
return ( |
|||
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup`}> |
|||
{{ |
|||
title: () => <ItemContent {...contentProps} />, |
|||
default: () => renderMenuItem(children), |
|||
}} |
|||
</Menu.SubMenu> |
|||
); |
|||
}); |
|||
} |
|||
return () => { |
|||
if (!unref(showRef)) { |
|||
return null; |
|||
} |
|||
const { items } = props; |
|||
return ( |
|||
<Menu |
|||
inlineIndent={12} |
|||
mode="vertical" |
|||
class={prefixCls} |
|||
ref={wrapRef} |
|||
style={unref(getStyle)} |
|||
> |
|||
{renderMenuItem(items)} |
|||
</Menu> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@default-height: 42px !important; |
|||
|
|||
@small-height: 36px !important; |
|||
|
|||
@large-height: 36px !important; |
|||
|
|||
.item-style() { |
|||
li { |
|||
display: inline-block; |
|||
width: 100%; |
|||
height: @default-height; |
|||
margin: 0 !important; |
|||
line-height: @default-height; |
|||
|
|||
span { |
|||
line-height: @default-height; |
|||
} |
|||
|
|||
> div { |
|||
margin: 0 !important; |
|||
} |
|||
|
|||
&:not(.ant-menu-item-disabled):hover { |
|||
color: @text-color-base; |
|||
background-color: @item-hover-bg; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.context-menu { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 200; |
|||
display: block; |
|||
width: 156px; |
|||
margin: 0; |
|||
list-style: none; |
|||
background-color: @component-background; |
|||
border: 1px solid rgba(0, 0, 0, 0.08); |
|||
border-radius: 0.25rem; |
|||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.1), |
|||
0 1px 5px 0 rgba(0, 0, 0, 0.06); |
|||
background-clip: padding-box; |
|||
user-select: none; |
|||
|
|||
.item-style(); |
|||
|
|||
.ant-divider { |
|||
margin: 0 0; |
|||
} |
|||
|
|||
&__popup { |
|||
.ant-divider { |
|||
margin: 0 0; |
|||
} |
|||
|
|||
.item-style(); |
|||
} |
|||
|
|||
.ant-menu-submenu-title, |
|||
.ant-menu-item { |
|||
padding: 0 !important; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,35 +0,0 @@ |
|||
export interface Axis { |
|||
x: number; |
|||
y: number; |
|||
} |
|||
|
|||
export interface ContextMenuItem { |
|||
label: string; |
|||
icon?: string; |
|||
disabled?: boolean; |
|||
handler?: Fn; |
|||
divider?: boolean; |
|||
children?: ContextMenuItem[]; |
|||
} |
|||
export interface CreateContextOptions { |
|||
event: MouseEvent; |
|||
icon?: string; |
|||
styles?: any; |
|||
items?: ContextMenuItem[]; |
|||
} |
|||
|
|||
export interface ContextMenuProps { |
|||
event?: MouseEvent; |
|||
styles?: any; |
|||
items: ContextMenuItem[]; |
|||
customEvent?: MouseEvent; |
|||
axis?: Axis; |
|||
width?: number; |
|||
showIcon?: boolean; |
|||
} |
|||
|
|||
export interface ItemContentProps { |
|||
showIcon: boolean | undefined; |
|||
item: ContextMenuItem; |
|||
handler: Fn; |
|||
} |
|||
@ -1,283 +0,0 @@ |
|||
<template> |
|||
<BasicModal |
|||
v-bind="$attrs" |
|||
@register="register" |
|||
:title="t('component.cropper.modalTitle')" |
|||
width="800px" |
|||
:canFullscreen="false" |
|||
@ok="handleOk" |
|||
:okText="t('component.cropper.okText')" |
|||
> |
|||
<div :class="prefixCls"> |
|||
<div :class="`${prefixCls}-left`"> |
|||
<div :class="`${prefixCls}-cropper`"> |
|||
<CropperImage |
|||
v-if="src" |
|||
:src="src" |
|||
height="300px" |
|||
:circled="circled" |
|||
@cropend="handleCropend" |
|||
@ready="handleReady" |
|||
/> |
|||
</div> |
|||
|
|||
<div :class="`${prefixCls}-toolbar`"> |
|||
<Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload"> |
|||
<Tooltip :title="t('component.cropper.selectImage')" placement="bottom"> |
|||
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" /> |
|||
</Tooltip> |
|||
</Upload> |
|||
<Space> |
|||
<Tooltip :title="t('component.cropper.btn_reset')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:reload-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('reset')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_rotate_left')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-left-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', -45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_rotate_right')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-right-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', 45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_scale_x')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-h" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleX')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_scale_y')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-v" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleY')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_zoom_in')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-in-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', 0.1)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_zoom_out')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-out-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', -0.1)" |
|||
/> |
|||
</Tooltip> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
<div :class="`${prefixCls}-right`"> |
|||
<div :class="`${prefixCls}-preview`"> |
|||
<img :src="previewSource" v-if="previewSource" :alt="t('component.cropper.preview')" /> |
|||
</div> |
|||
<template v-if="previewSource"> |
|||
<div :class="`${prefixCls}-group`"> |
|||
<Avatar :src="previewSource" size="large" /> |
|||
<Avatar :src="previewSource" :size="48" /> |
|||
<Avatar :src="previewSource" :size="64" /> |
|||
<Avatar :src="previewSource" :size="80" /> |
|||
</div> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</BasicModal> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { CropendResult, Cropper } from './typing'; |
|||
|
|||
import { defineComponent, ref } from 'vue'; |
|||
import CropperImage from './Cropper.vue'; |
|||
import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { BasicModal, useModalInner } from '/@/components/Modal'; |
|||
import { dataURLtoBlob } from '/@/utils/file/base64Conver'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
type apiFunParams = { file: Blob; name: string; filename: string }; |
|||
|
|||
const props = { |
|||
circled: { type: Boolean, default: true }, |
|||
uploadApi: { |
|||
type: Function as PropType<(params: apiFunParams) => Promise<any>>, |
|||
}, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CropperModal', |
|||
components: { BasicModal, Space, CropperImage, Upload, Avatar, Tooltip }, |
|||
props, |
|||
emits: ['uploadSuccess', 'register'], |
|||
setup(props, { emit }) { |
|||
let filename = ''; |
|||
const src = ref(''); |
|||
const previewSource = ref(''); |
|||
const cropper = ref<Cropper>(); |
|||
let scaleX = 1; |
|||
let scaleY = 1; |
|||
|
|||
const { prefixCls } = useDesign('cropper-am'); |
|||
const [register, { closeModal, setModalProps }] = useModalInner(); |
|||
const { t } = useI18n(); |
|||
|
|||
// Block upload |
|||
function handleBeforeUpload(file: File) { |
|||
const reader = new FileReader(); |
|||
reader.readAsDataURL(file); |
|||
src.value = ''; |
|||
previewSource.value = ''; |
|||
reader.onload = function (e) { |
|||
src.value = (e.target?.result as string) ?? ''; |
|||
filename = file.name; |
|||
}; |
|||
return false; |
|||
} |
|||
|
|||
function handleCropend({ imgBase64 }: CropendResult) { |
|||
previewSource.value = imgBase64; |
|||
} |
|||
|
|||
function handleReady(cropperInstance: Cropper) { |
|||
cropper.value = cropperInstance; |
|||
} |
|||
|
|||
function handlerToolbar(event: string, arg?: number) { |
|||
if (event === 'scaleX') { |
|||
scaleX = arg = scaleX === -1 ? 1 : -1; |
|||
} |
|||
if (event === 'scaleY') { |
|||
scaleY = arg = scaleY === -1 ? 1 : -1; |
|||
} |
|||
cropper?.value?.[event]?.(arg); |
|||
} |
|||
|
|||
async function handleOk() { |
|||
const uploadApi = props.uploadApi; |
|||
if (uploadApi && isFunction(uploadApi)) { |
|||
const blob = dataURLtoBlob(previewSource.value); |
|||
try { |
|||
setModalProps({ confirmLoading: true }); |
|||
const result = await uploadApi({ name: 'file', file: blob, filename }); |
|||
emit('uploadSuccess', { source: previewSource.value, data: result.data }); |
|||
closeModal(); |
|||
} finally { |
|||
setModalProps({ confirmLoading: false }); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return { |
|||
t, |
|||
prefixCls, |
|||
src, |
|||
register, |
|||
previewSource, |
|||
handleBeforeUpload, |
|||
handleCropend, |
|||
handleReady, |
|||
handlerToolbar, |
|||
handleOk, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-cropper-am'; |
|||
|
|||
.@{prefix-cls} { |
|||
display: flex; |
|||
|
|||
&-left, |
|||
&-right { |
|||
height: 340px; |
|||
} |
|||
|
|||
&-left { |
|||
width: 55%; |
|||
} |
|||
|
|||
&-right { |
|||
width: 45%; |
|||
} |
|||
|
|||
&-cropper { |
|||
height: 300px; |
|||
background: #eee; |
|||
background-image: linear-gradient( |
|||
45deg, |
|||
rgba(0, 0, 0, 0.25) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgba(0, 0, 0, 0.25) 0 |
|||
), |
|||
linear-gradient( |
|||
45deg, |
|||
rgba(0, 0, 0, 0.25) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgba(0, 0, 0, 0.25) 0 |
|||
); |
|||
background-position: 0 0, 12px 12px; |
|||
background-size: 24px 24px; |
|||
} |
|||
|
|||
&-toolbar { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
&-preview { |
|||
width: 220px; |
|||
height: 220px; |
|||
margin: 0 auto; |
|||
overflow: hidden; |
|||
border: 1px solid @border-color-base; |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
&-group { |
|||
display: flex; |
|||
padding-top: 8px; |
|||
margin-top: 8px; |
|||
border-top: 1px solid @border-color-base; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,162 +0,0 @@ |
|||
<template> |
|||
<div :class="getClass" :style="getStyle"> |
|||
<div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal"> |
|||
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle"> |
|||
<Icon |
|||
icon="ant-design:cloud-upload-outlined" |
|||
:size="getIconWidth" |
|||
:style="getImageWrapperStyle" |
|||
color="#d6d6d6" |
|||
/> |
|||
</div> |
|||
<img :src="sourceValue" v-if="sourceValue" alt="avatar" /> |
|||
</div> |
|||
<a-button |
|||
:class="`${prefixCls}-upload-btn`" |
|||
@click="openModal" |
|||
v-if="showBtn" |
|||
v-bind="btnProps" |
|||
> |
|||
{{ btnText ? btnText : t('component.cropper.selectImage') }} |
|||
</a-button> |
|||
|
|||
<CopperModal |
|||
@register="register" |
|||
@uploadSuccess="handleUploadSuccess" |
|||
:uploadApi="uploadApi" |
|||
:src="sourceValue" |
|||
/> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { |
|||
defineComponent, |
|||
computed, |
|||
CSSProperties, |
|||
unref, |
|||
ref, |
|||
watchEffect, |
|||
watch, |
|||
PropType, |
|||
} from 'vue'; |
|||
import CopperModal from './CopperModal.vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useModal } from '/@/components/Modal'; |
|||
import { useMessage } from '/@/hooks/web/useMessage'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import type { ButtonProps } from '/@/components/Button'; |
|||
import Icon from '/@/components/Icon'; |
|||
|
|||
const props = { |
|||
width: { type: [String, Number], default: '200px' }, |
|||
value: { type: String }, |
|||
showBtn: { type: Boolean, default: true }, |
|||
btnProps: { type: Object as PropType<ButtonProps> }, |
|||
btnText: { type: String, default: '' }, |
|||
uploadApi: { type: Function as PropType<({ file: Blob, name: string }) => Promise<void>> }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CropperAvatar', |
|||
components: { CopperModal, Icon }, |
|||
props, |
|||
emits: ['update:value', 'change'], |
|||
setup(props, { emit, expose }) { |
|||
const sourceValue = ref(props.value || ''); |
|||
const { prefixCls } = useDesign('cropper-avatar'); |
|||
const [register, { openModal, closeModal }] = useModal(); |
|||
const { createMessage } = useMessage(); |
|||
const { t } = useI18n(); |
|||
|
|||
const getClass = computed(() => [prefixCls]); |
|||
|
|||
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px'); |
|||
|
|||
const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px'); |
|||
|
|||
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) })); |
|||
|
|||
const getImageWrapperStyle = computed( |
|||
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }) |
|||
); |
|||
|
|||
watchEffect(() => { |
|||
sourceValue.value = props.value || ''; |
|||
}); |
|||
|
|||
watch( |
|||
() => sourceValue.value, |
|||
(v: string) => { |
|||
emit('update:value', v); |
|||
} |
|||
); |
|||
|
|||
function handleUploadSuccess({ source }) { |
|||
sourceValue.value = source; |
|||
emit('change', source); |
|||
createMessage.success(t('component.cropper.uploadSuccess')); |
|||
} |
|||
|
|||
expose({ openModal: openModal.bind(null, true), closeModal }); |
|||
|
|||
return { |
|||
t, |
|||
prefixCls, |
|||
register, |
|||
openModal, |
|||
getIconWidth, |
|||
sourceValue, |
|||
getClass, |
|||
getImageWrapperStyle, |
|||
getStyle, |
|||
handleUploadSuccess, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
@prefix-cls: ~'@{namespace}-cropper-avatar'; |
|||
|
|||
.@{prefix-cls} { |
|||
display: inline-block; |
|||
text-align: center; |
|||
|
|||
&-image-wrapper { |
|||
overflow: hidden; |
|||
cursor: pointer; |
|||
background: @component-background; |
|||
border: 1px solid @border-color-base; |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
} |
|||
} |
|||
|
|||
&-image-mask { |
|||
opacity: 0; |
|||
position: absolute; |
|||
width: inherit; |
|||
height: inherit; |
|||
border-radius: inherit; |
|||
border: inherit; |
|||
background: rgba(0, 0, 0, 0.4); |
|||
cursor: pointer; |
|||
-webkit-transition: opacity 0.4s; |
|||
transition: opacity 0.4s; |
|||
|
|||
::v-deep(svg) { |
|||
margin: auto; |
|||
} |
|||
} |
|||
|
|||
&-image-mask:hover { |
|||
opacity: 40; |
|||
} |
|||
|
|||
&-upload-btn { |
|||
margin: 10px auto; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,181 +0,0 @@ |
|||
<script lang="tsx"> |
|||
import type { DescriptionProps, DescInstance, DescItem } from './typing'; |
|||
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index'; |
|||
import type { CSSProperties } from 'vue'; |
|||
import type { CollapseContainerOptions } from '/@/components/Container/index'; |
|||
import { defineComponent, computed, ref, unref } from 'vue'; |
|||
import { get } from 'lodash-es'; |
|||
import { Descriptions } from 'ant-design-vue'; |
|||
import { CollapseContainer } from '/@/components/Container/index'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
import { getSlot } from '/@/utils/helper/tsxHelper'; |
|||
import { useAttrs } from '/@/hooks/core/useAttrs'; |
|||
|
|||
const props = { |
|||
useCollapse: { type: Boolean, default: true }, |
|||
title: { type: String, default: '' }, |
|||
size: { |
|||
type: String, |
|||
validator: (v) => ['small', 'default', 'middle', undefined].includes(v), |
|||
default: 'small', |
|||
}, |
|||
bordered: { type: Boolean, default: true }, |
|||
column: { |
|||
type: [Number, Object] as PropType<number | Recordable>, |
|||
default: () => { |
|||
return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 }; |
|||
}, |
|||
}, |
|||
collapseOptions: { |
|||
type: Object as PropType<CollapseContainerOptions>, |
|||
default: null, |
|||
}, |
|||
schema: { |
|||
type: Array as PropType<DescItem[]>, |
|||
default: () => [], |
|||
}, |
|||
data: { type: Object }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'Description', |
|||
props, |
|||
emits: ['register'], |
|||
setup(props, { slots, emit }) { |
|||
const propsRef = ref<Partial<DescriptionProps> | null>(null); |
|||
|
|||
const { prefixCls } = useDesign('description'); |
|||
const attrs = useAttrs(); |
|||
|
|||
// Custom title component: get title |
|||
const getMergeProps = computed(() => { |
|||
return { |
|||
...props, |
|||
...(unref(propsRef) as Recordable), |
|||
} as DescriptionProps; |
|||
}); |
|||
|
|||
const getProps = computed(() => { |
|||
const opt = { |
|||
...unref(getMergeProps), |
|||
title: undefined, |
|||
}; |
|||
return opt as DescriptionProps; |
|||
}); |
|||
|
|||
/** |
|||
* @description: Whether to setting title |
|||
*/ |
|||
const useWrapper = computed(() => !!unref(getMergeProps).title); |
|||
|
|||
/** |
|||
* @description: Get configuration Collapse |
|||
*/ |
|||
const getCollapseOptions = computed((): CollapseContainerOptions => { |
|||
return { |
|||
// Cannot be expanded by default |
|||
canExpand: false, |
|||
...unref(getProps).collapseOptions, |
|||
}; |
|||
}); |
|||
|
|||
const getDescriptionsProps = computed(() => { |
|||
return { ...unref(attrs), ...unref(getProps) } as DescriptionsProps; |
|||
}); |
|||
|
|||
/** |
|||
* @description:设置desc |
|||
*/ |
|||
function setDescProps(descProps: Partial<DescriptionProps>): void { |
|||
// Keep the last setDrawerProps |
|||
propsRef.value = { ...(unref(propsRef) as Recordable), ...descProps } as Recordable; |
|||
} |
|||
|
|||
// Prevent line breaks |
|||
function renderLabel({ label, labelMinWidth, labelStyle }: DescItem) { |
|||
if (!labelStyle && !labelMinWidth) { |
|||
return label; |
|||
} |
|||
|
|||
const labelStyles: CSSProperties = { |
|||
...labelStyle, |
|||
minWidth: `${labelMinWidth}px `, |
|||
}; |
|||
return <div style={labelStyles}>{label}</div>; |
|||
} |
|||
|
|||
function renderItem() { |
|||
const { schema, data } = unref(getProps); |
|||
return unref(schema) |
|||
.map((item) => { |
|||
const { render, field, span, show, contentMinWidth } = item; |
|||
|
|||
if (show && isFunction(show) && !show(data)) { |
|||
return null; |
|||
} |
|||
|
|||
const getContent = () => { |
|||
const _data = unref(getProps)?.data; |
|||
if (!_data) { |
|||
return null; |
|||
} |
|||
const getField = get(_data, field); |
|||
return isFunction(render) ? render(getField, _data) : getField ?? ''; |
|||
}; |
|||
|
|||
const width = contentMinWidth; |
|||
return ( |
|||
<Descriptions.Item label={renderLabel(item)} key={field} span={span}> |
|||
{() => { |
|||
if (!contentMinWidth) { |
|||
return getContent(); |
|||
} |
|||
const style: CSSProperties = { |
|||
minWidth: `${width}px`, |
|||
}; |
|||
return <div style={style}>{getContent()}</div>; |
|||
}} |
|||
</Descriptions.Item> |
|||
); |
|||
}) |
|||
.filter((item) => !!item); |
|||
} |
|||
|
|||
const renderDesc = () => { |
|||
return ( |
|||
<Descriptions class={`${prefixCls}`} {...(unref(getDescriptionsProps) as any)}> |
|||
{renderItem()} |
|||
</Descriptions> |
|||
); |
|||
}; |
|||
|
|||
const renderContainer = () => { |
|||
const content = props.useCollapse ? renderDesc() : <div>{renderDesc()}</div>; |
|||
// Reduce the dom level |
|||
if (!props.useCollapse) { |
|||
return content; |
|||
} |
|||
|
|||
const { canExpand, helpMessage } = unref(getCollapseOptions); |
|||
const { title } = unref(getMergeProps); |
|||
|
|||
return ( |
|||
<CollapseContainer title={title} canExpan={canExpand} helpMessage={helpMessage}> |
|||
{{ |
|||
default: () => content, |
|||
action: () => getSlot(slots, 'action'), |
|||
}} |
|||
</CollapseContainer> |
|||
); |
|||
}; |
|||
|
|||
const methods: DescInstance = { |
|||
setDescProps, |
|||
}; |
|||
|
|||
emit('register', methods); |
|||
return () => (unref(useWrapper) ? renderContainer() : renderDesc()); |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,50 +0,0 @@ |
|||
import type { VNode, CSSProperties } from 'vue'; |
|||
import type { CollapseContainerOptions } from '/@/components/Container/index'; |
|||
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index'; |
|||
|
|||
export interface DescItem { |
|||
labelMinWidth?: number; |
|||
contentMinWidth?: number; |
|||
labelStyle?: CSSProperties; |
|||
field: string; |
|||
label: string | VNode | JSX.Element; |
|||
// Merge column
|
|||
span?: number; |
|||
show?: (...arg: any) => boolean; |
|||
// render
|
|||
render?: ( |
|||
val: any, |
|||
data: Recordable |
|||
) => VNode | undefined | JSX.Element | Element | string | number; |
|||
} |
|||
|
|||
export interface DescriptionProps extends DescriptionsProps { |
|||
// Whether to include the collapse component
|
|||
useCollapse?: boolean; |
|||
/** |
|||
* item configuration |
|||
* @type DescItem |
|||
*/ |
|||
schema: DescItem[]; |
|||
/** |
|||
* 数据 |
|||
* @type object |
|||
*/ |
|||
data: Recordable; |
|||
/** |
|||
* Built-in CollapseContainer component configuration |
|||
* @type CollapseContainerOptions |
|||
*/ |
|||
collapseOptions?: CollapseContainerOptions; |
|||
} |
|||
|
|||
export interface DescInstance { |
|||
setDescProps(descProps: Partial<DescriptionProps>): void; |
|||
} |
|||
|
|||
export type Register = (descInstance: DescInstance) => void; |
|||
|
|||
/** |
|||
* @description: |
|||
*/ |
|||
export type UseDescReturnType = [Register, DescInstance]; |
|||
@ -1,256 +0,0 @@ |
|||
<template> |
|||
<Drawer :class="prefixCls" @close="onClose" v-bind="getBindValues"> |
|||
<template #title v-if="!$slots.title"> |
|||
<DrawerHeader |
|||
:title="getMergeProps.title" |
|||
:isDetail="isDetail" |
|||
:showDetailBack="showDetailBack" |
|||
@close="onClose" |
|||
> |
|||
<template #titleToolbar> |
|||
<slot name="titleToolbar"></slot> |
|||
</template> |
|||
</DrawerHeader> |
|||
</template> |
|||
<template v-else #title> |
|||
<slot name="title"></slot> |
|||
</template> |
|||
|
|||
<ScrollContainer |
|||
:style="getScrollContentStyle" |
|||
v-loading="getLoading" |
|||
:loading-tip="loadingText || t('common.loadingText')" |
|||
> |
|||
<slot></slot> |
|||
</ScrollContainer> |
|||
<DrawerFooter v-bind="getProps" @close="onClose" @ok="handleOk" :height="getFooterHeight"> |
|||
<template #[item]="data" v-for="item in Object.keys($slots)"> |
|||
<slot :name="item" v-bind="data || {}"></slot> |
|||
</template> |
|||
</DrawerFooter> |
|||
</Drawer> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { DrawerInstance, DrawerProps } from './typing'; |
|||
import type { CSSProperties } from 'vue'; |
|||
import { |
|||
defineComponent, |
|||
ref, |
|||
computed, |
|||
watch, |
|||
unref, |
|||
nextTick, |
|||
toRaw, |
|||
getCurrentInstance, |
|||
} from 'vue'; |
|||
import { Drawer } from 'ant-design-vue'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import { isFunction, isNumber } from '/@/utils/is'; |
|||
import { deepMerge } from '/@/utils'; |
|||
import DrawerFooter from './components/DrawerFooter.vue'; |
|||
import DrawerHeader from './components/DrawerHeader.vue'; |
|||
import { ScrollContainer } from '/@/components/Container'; |
|||
import { basicProps } from './props'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useAttrs } from '/@/hooks/core/useAttrs'; |
|||
|
|||
export default defineComponent({ |
|||
components: { Drawer, ScrollContainer, DrawerFooter, DrawerHeader }, |
|||
inheritAttrs: false, |
|||
props: basicProps, |
|||
emits: ['visible-change', 'ok', 'close', 'register'], |
|||
setup(props, { emit }) { |
|||
const visibleRef = ref(false); |
|||
const attrs = useAttrs(); |
|||
const propsRef = ref<Partial<Nullable<DrawerProps>>>(null); |
|||
|
|||
const { t } = useI18n(); |
|||
const { prefixVar, prefixCls } = useDesign('basic-drawer'); |
|||
|
|||
const drawerInstance: DrawerInstance = { |
|||
setDrawerProps: setDrawerProps, |
|||
emitVisible: undefined, |
|||
}; |
|||
|
|||
const instance = getCurrentInstance(); |
|||
|
|||
instance && emit('register', drawerInstance, instance.uid); |
|||
|
|||
const getMergeProps = computed((): DrawerProps => { |
|||
return deepMerge(toRaw(props), unref(propsRef)); |
|||
}); |
|||
|
|||
const getProps = computed((): DrawerProps => { |
|||
const opt = { |
|||
placement: 'right', |
|||
...unref(attrs), |
|||
...unref(getMergeProps), |
|||
visible: unref(visibleRef), |
|||
}; |
|||
opt.title = undefined; |
|||
const { isDetail, width, wrapClassName, getContainer } = opt; |
|||
if (isDetail) { |
|||
if (!width) { |
|||
opt.width = '100%'; |
|||
} |
|||
const detailCls = `${prefixCls}__detail`; |
|||
opt.wrapClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls; |
|||
|
|||
if (!getContainer) { |
|||
// TODO type error? |
|||
opt.getContainer = `.${prefixVar}-layout-content` as any; |
|||
} |
|||
} |
|||
return opt as DrawerProps; |
|||
}); |
|||
|
|||
const getBindValues = computed((): DrawerProps => { |
|||
return { |
|||
...attrs, |
|||
...unref(getProps), |
|||
}; |
|||
}); |
|||
|
|||
// Custom implementation of the bottom button, |
|||
const getFooterHeight = computed(() => { |
|||
const { footerHeight, showFooter } = unref(getProps); |
|||
if (showFooter && footerHeight) { |
|||
return isNumber(footerHeight) |
|||
? `${footerHeight}px` |
|||
: `${footerHeight.replace('px', '')}px`; |
|||
} |
|||
return `0px`; |
|||
}); |
|||
|
|||
const getScrollContentStyle = computed((): CSSProperties => { |
|||
const footerHeight = unref(getFooterHeight); |
|||
return { |
|||
position: 'relative', |
|||
height: `calc(100% - ${footerHeight})`, |
|||
}; |
|||
}); |
|||
|
|||
const getLoading = computed(() => { |
|||
return !!unref(getProps)?.loading; |
|||
}); |
|||
|
|||
watch( |
|||
() => props.visible, |
|||
(newVal, oldVal) => { |
|||
if (newVal !== oldVal) visibleRef.value = newVal; |
|||
}, |
|||
{ deep: true } |
|||
); |
|||
|
|||
watch( |
|||
() => visibleRef.value, |
|||
(visible) => { |
|||
nextTick(() => { |
|||
emit('visible-change', visible); |
|||
instance && drawerInstance.emitVisible?.(visible, instance.uid); |
|||
}); |
|||
} |
|||
); |
|||
|
|||
// Cancel event |
|||
async function onClose(e: Recordable) { |
|||
const { closeFunc } = unref(getProps); |
|||
emit('close', e); |
|||
if (closeFunc && isFunction(closeFunc)) { |
|||
const res = await closeFunc(); |
|||
visibleRef.value = !res; |
|||
return; |
|||
} |
|||
visibleRef.value = false; |
|||
} |
|||
|
|||
function setDrawerProps(props: Partial<DrawerProps>): void { |
|||
// Keep the last setDrawerProps |
|||
propsRef.value = deepMerge(unref(propsRef) || ({} as any), props); |
|||
|
|||
if (Reflect.has(props, 'visible')) { |
|||
visibleRef.value = !!props.visible; |
|||
} |
|||
} |
|||
|
|||
function handleOk() { |
|||
emit('ok'); |
|||
} |
|||
|
|||
return { |
|||
onClose, |
|||
t, |
|||
prefixCls, |
|||
getMergeProps, |
|||
getScrollContentStyle, |
|||
getProps, |
|||
getLoading, |
|||
getBindValues, |
|||
getFooterHeight, |
|||
handleOk, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@header-height: 60px; |
|||
@detail-header-height: 40px; |
|||
@prefix-cls: ~'@{namespace}-basic-drawer'; |
|||
@prefix-cls-detail: ~'@{namespace}-basic-drawer__detail'; |
|||
|
|||
.@{prefix-cls} { |
|||
.ant-drawer-wrapper-body { |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.ant-drawer-close { |
|||
&:hover { |
|||
color: @error-color; |
|||
} |
|||
} |
|||
|
|||
.ant-drawer-body { |
|||
height: calc(100% - @header-height); |
|||
padding: 0; |
|||
background-color: @component-background; |
|||
|
|||
.scrollbar__wrap { |
|||
padding: 16px !important; |
|||
margin-bottom: 0 !important; |
|||
} |
|||
|
|||
> .scrollbar > .scrollbar__bar.is-horizontal { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.@{prefix-cls-detail} { |
|||
position: absolute; |
|||
|
|||
.ant-drawer-header { |
|||
width: 100%; |
|||
height: @detail-header-height; |
|||
padding: 0; |
|||
border-top: 1px solid @border-color-base; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.ant-drawer-title { |
|||
height: 100%; |
|||
} |
|||
|
|||
.ant-drawer-close { |
|||
height: @detail-header-height; |
|||
line-height: @detail-header-height; |
|||
} |
|||
|
|||
.scrollbar__wrap { |
|||
padding: 0 !important; |
|||
} |
|||
|
|||
.ant-drawer-body { |
|||
height: calc(100% - @detail-header-height); |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,82 +0,0 @@ |
|||
<template> |
|||
<div :class="prefixCls" :style="getStyle" v-if="showFooter || $slots.footer"> |
|||
<template v-if="!$slots.footer"> |
|||
<slot name="insertFooter"></slot> |
|||
<a-button v-bind="cancelButtonProps" @click="handleClose" class="mr-2" v-if="showCancelBtn"> |
|||
{{ cancelText }} |
|||
</a-button> |
|||
<slot name="centerFooter"></slot> |
|||
<a-button |
|||
:type="okType" |
|||
@click="handleOk" |
|||
v-bind="okButtonProps" |
|||
class="mr-2" |
|||
:loading="confirmLoading" |
|||
v-if="showOkBtn" |
|||
> |
|||
{{ okText }} |
|||
</a-button> |
|||
<slot name="appendFooter"></slot> |
|||
</template> |
|||
|
|||
<template v-else> |
|||
<slot name="footer"></slot> |
|||
</template> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { CSSProperties } from 'vue'; |
|||
import { defineComponent, computed } from 'vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
|
|||
import { footerProps } from '../props'; |
|||
export default defineComponent({ |
|||
name: 'BasicDrawerFooter', |
|||
props: { |
|||
...footerProps, |
|||
height: { |
|||
type: String, |
|||
default: '60px', |
|||
}, |
|||
}, |
|||
emits: ['ok', 'close'], |
|||
setup(props, { emit }) { |
|||
const { prefixCls } = useDesign('basic-drawer-footer'); |
|||
|
|||
const getStyle = computed((): CSSProperties => { |
|||
const heightStr = `${props.height}`; |
|||
return { |
|||
height: heightStr, |
|||
lineHeight: heightStr, |
|||
}; |
|||
}); |
|||
|
|||
function handleOk() { |
|||
emit('ok'); |
|||
} |
|||
|
|||
function handleClose() { |
|||
emit('close'); |
|||
} |
|||
return { handleOk, prefixCls, handleClose, getStyle }; |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-basic-drawer-footer'; |
|||
@footer-height: 60px; |
|||
.@{prefix-cls} { |
|||
position: absolute; |
|||
bottom: 0; |
|||
width: 100%; |
|||
padding: 0 12px 0 20px; |
|||
text-align: right; |
|||
background-color: @component-background; |
|||
border-top: 1px solid @border-color-base; |
|||
|
|||
> * { |
|||
margin-right: 8px; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,194 +0,0 @@ |
|||
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes'; |
|||
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue'; |
|||
import type { ScrollContainerOptions } from '/@/components/Container/index'; |
|||
|
|||
export interface DrawerInstance { |
|||
setDrawerProps: (props: Partial<DrawerProps> | boolean) => void; |
|||
emitVisible?: (visible: boolean, uid: number) => void; |
|||
} |
|||
|
|||
export interface ReturnMethods extends DrawerInstance { |
|||
openDrawer: <T = any>(visible?: boolean, data?: T, openOnSet?: boolean) => void; |
|||
closeDrawer: () => void; |
|||
getVisible?: ComputedRef<boolean>; |
|||
} |
|||
|
|||
export type RegisterFn = (drawerInstance: DrawerInstance, uuid?: string) => void; |
|||
|
|||
export interface ReturnInnerMethods extends DrawerInstance { |
|||
closeDrawer: () => void; |
|||
changeLoading: (loading: boolean) => void; |
|||
changeOkLoading: (loading: boolean) => void; |
|||
getVisible?: ComputedRef<boolean>; |
|||
} |
|||
|
|||
export type UseDrawerReturnType = [RegisterFn, ReturnMethods]; |
|||
|
|||
export type UseDrawerInnerReturnType = [RegisterFn, ReturnInnerMethods]; |
|||
|
|||
export interface DrawerFooterProps { |
|||
showOkBtn: boolean; |
|||
showCancelBtn: boolean; |
|||
/** |
|||
* Text of the Cancel button |
|||
* @default 'cancel' |
|||
* @type string |
|||
*/ |
|||
cancelText: string; |
|||
/** |
|||
* Text of the OK button |
|||
* @default 'OK' |
|||
* @type string |
|||
*/ |
|||
okText: string; |
|||
|
|||
/** |
|||
* Button type of the OK button |
|||
* @default 'primary' |
|||
* @type string |
|||
*/ |
|||
okType: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default'; |
|||
/** |
|||
* The ok button props, follow jsx rules |
|||
* @type object |
|||
*/ |
|||
okButtonProps: { props: ButtonProps; on: {} }; |
|||
|
|||
/** |
|||
* The cancel button props, follow jsx rules |
|||
* @type object |
|||
*/ |
|||
cancelButtonProps: { props: ButtonProps; on: {} }; |
|||
/** |
|||
* Whether to apply loading visual effect for OK button or not |
|||
* @default false |
|||
* @type boolean |
|||
*/ |
|||
confirmLoading: boolean; |
|||
|
|||
showFooter: boolean; |
|||
footerHeight: string | number; |
|||
} |
|||
export interface DrawerProps extends DrawerFooterProps { |
|||
isDetail?: boolean; |
|||
loading?: boolean; |
|||
showDetailBack?: boolean; |
|||
visible?: boolean; |
|||
/** |
|||
* Built-in ScrollContainer component configuration |
|||
* @type ScrollContainerOptions |
|||
*/ |
|||
scrollOptions?: ScrollContainerOptions; |
|||
closeFunc?: () => Promise<any>; |
|||
triggerWindowResize?: boolean; |
|||
/** |
|||
* Whether a close (x) button is visible on top right of the Drawer dialog or not. |
|||
* @default true |
|||
* @type boolean |
|||
*/ |
|||
closable?: boolean; |
|||
|
|||
/** |
|||
* Whether to unmount child components on closing drawer or not. |
|||
* @default false |
|||
* @type boolean |
|||
*/ |
|||
destroyOnClose?: boolean; |
|||
|
|||
/** |
|||
* Return the mounted node for Drawer. |
|||
* @default 'body' |
|||
* @type any ( HTMLElement| () => HTMLElement | string) |
|||
*/ |
|||
getContainer?: () => HTMLElement | string; |
|||
|
|||
/** |
|||
* Whether to show mask or not. |
|||
* @default true |
|||
* @type boolean |
|||
*/ |
|||
mask?: boolean; |
|||
|
|||
/** |
|||
* Clicking on the mask (area outside the Drawer) to close the Drawer or not. |
|||
* @default true |
|||
* @type boolean |
|||
*/ |
|||
maskClosable?: boolean; |
|||
|
|||
/** |
|||
* Style for Drawer's mask element. |
|||
* @default {} |
|||
* @type object |
|||
*/ |
|||
maskStyle?: CSSProperties; |
|||
|
|||
/** |
|||
* The title for Drawer. |
|||
* @type any (string | slot) |
|||
*/ |
|||
title?: VNodeChild | JSX.Element; |
|||
|
|||
/** |
|||
* The class name of the container of the Drawer dialog. |
|||
* @type string |
|||
*/ |
|||
wrapClassName?: string; |
|||
|
|||
/** |
|||
* Style of wrapper element which **contains mask** compare to `drawerStyle` |
|||
* @type object |
|||
*/ |
|||
wrapStyle?: CSSProperties; |
|||
|
|||
/** |
|||
* Style of the popup layer element |
|||
* @type object |
|||
*/ |
|||
drawerStyle?: CSSProperties; |
|||
|
|||
/** |
|||
* Style of floating layer, typically used for adjusting its position. |
|||
* @type object |
|||
*/ |
|||
bodyStyle?: CSSProperties; |
|||
headerStyle?: CSSProperties; |
|||
|
|||
/** |
|||
* Width of the Drawer dialog. |
|||
* @default 256 |
|||
* @type string | number |
|||
*/ |
|||
width?: string | number; |
|||
|
|||
/** |
|||
* placement is top or bottom, height of the Drawer dialog. |
|||
* @type string | number |
|||
*/ |
|||
height?: string | number; |
|||
|
|||
/** |
|||
* The z-index of the Drawer. |
|||
* @default 1000 |
|||
* @type number |
|||
*/ |
|||
zIndex?: number; |
|||
|
|||
/** |
|||
* The placement of the Drawer. |
|||
* @default 'right' |
|||
* @type string |
|||
*/ |
|||
placement?: 'top' | 'right' | 'bottom' | 'left'; |
|||
afterVisibleChange?: (visible?: boolean) => void; |
|||
keyboard?: boolean; |
|||
/** |
|||
* Specify a callback that will be called when a user clicks mask, close button or Cancel button. |
|||
*/ |
|||
onClose?: (e?: Event) => void; |
|||
} |
|||
export interface DrawerActionType { |
|||
scrollBottom: () => void; |
|||
scrollTo: (to: number) => void; |
|||
getScrollWrap: () => Element | null; |
|||
} |
|||
@ -1,107 +0,0 @@ |
|||
<template> |
|||
<Dropdown :trigger="trigger" v-bind="$attrs"> |
|||
<span> |
|||
<slot></slot> |
|||
</span> |
|||
<template #overlay> |
|||
<Menu :selectedKeys="selectedKeys"> |
|||
<template v-for="item in dropMenuList" :key="`${item.event}`"> |
|||
<MenuItem |
|||
v-bind="getAttr(item.event)" |
|||
@click="handleClickMenu(item)" |
|||
:disabled="item.disabled" |
|||
> |
|||
<Popconfirm |
|||
v-if="popconfirm && item.popConfirm" |
|||
v-bind="getPopConfirmAttrs(item.popConfirm)" |
|||
> |
|||
<template #icon v-if="item.popConfirm.icon"> |
|||
<Icon :icon="item.popConfirm.icon" /> |
|||
</template> |
|||
<div> |
|||
<Icon :icon="item.icon" v-if="item.icon" /> |
|||
<span class="ml-1">{{ item.text }}</span> |
|||
</div> |
|||
</Popconfirm> |
|||
<template v-else> |
|||
<Icon :icon="item.icon" v-if="item.icon" /> |
|||
<span class="ml-1">{{ item.text }}</span> |
|||
</template> |
|||
</MenuItem> |
|||
<MenuDivider v-if="item.divider" :key="`d-${item.event}`" /> |
|||
</template> |
|||
</Menu> |
|||
</template> |
|||
</Dropdown> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { computed, PropType } from 'vue'; |
|||
import type { DropMenu } from './typing'; |
|||
|
|||
import { defineComponent } from 'vue'; |
|||
import { Dropdown, Menu, Popconfirm } from 'ant-design-vue'; |
|||
import { Icon } from '/@/components/Icon'; |
|||
import { omit } from 'lodash-es'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicDropdown', |
|||
components: { |
|||
Dropdown, |
|||
Menu, |
|||
MenuItem: Menu.Item, |
|||
MenuDivider: Menu.Divider, |
|||
Icon, |
|||
Popconfirm, |
|||
}, |
|||
props: { |
|||
popconfirm: Boolean, |
|||
/** |
|||
* the trigger mode which executes the drop-down action |
|||
* @default ['hover'] |
|||
* @type string[] |
|||
*/ |
|||
trigger: { |
|||
type: [Array] as PropType<('contextmenu' | 'click' | 'hover')[]>, |
|||
default: () => { |
|||
return ['contextmenu']; |
|||
}, |
|||
}, |
|||
dropMenuList: { |
|||
type: Array as PropType<(DropMenu & Recordable)[]>, |
|||
default: () => [], |
|||
}, |
|||
selectedKeys: { |
|||
type: Array as PropType<string[]>, |
|||
default: () => [], |
|||
}, |
|||
}, |
|||
emits: ['menuEvent'], |
|||
setup(props, { emit }) { |
|||
function handleClickMenu(item: DropMenu) { |
|||
const { event } = item; |
|||
const menu = props.dropMenuList.find((item) => `${item.event}` === `${event}`); |
|||
emit('menuEvent', menu); |
|||
item.onClick?.(); |
|||
} |
|||
|
|||
const getPopConfirmAttrs = computed(() => { |
|||
return (attrs) => { |
|||
const originAttrs = omit(attrs, ['confirm', 'cancel', 'icon']); |
|||
if (!attrs.onConfirm && attrs.confirm && isFunction(attrs.confirm)) |
|||
originAttrs['onConfirm'] = attrs.confirm; |
|||
if (!attrs.onCancel && attrs.cancel && isFunction(attrs.cancel)) |
|||
originAttrs['onCancel'] = attrs.cancel; |
|||
return originAttrs; |
|||
}; |
|||
}); |
|||
|
|||
return { |
|||
handleClickMenu, |
|||
getPopConfirmAttrs, |
|||
getAttr: (key: string | number) => ({ key }), |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,59 +0,0 @@ |
|||
import xlsx from 'xlsx'; |
|||
import type { WorkBook } from 'xlsx'; |
|||
import type { JsonToSheet, AoAToSheet } from './typing'; |
|||
|
|||
const { utils, writeFile } = xlsx; |
|||
|
|||
const DEF_FILE_NAME = 'excel-list.xlsx'; |
|||
|
|||
export function jsonToSheetXlsx<T = any>({ |
|||
data, |
|||
header, |
|||
filename = DEF_FILE_NAME, |
|||
json2sheetOpts = {}, |
|||
write2excelOpts = { bookType: 'xlsx' }, |
|||
}: JsonToSheet<T>) { |
|||
const arrData = [...data]; |
|||
if (header) { |
|||
arrData.unshift(header); |
|||
json2sheetOpts.skipHeader = true; |
|||
} |
|||
|
|||
const worksheet = utils.json_to_sheet(arrData, json2sheetOpts); |
|||
|
|||
/* add worksheet to workbook */ |
|||
const workbook: WorkBook = { |
|||
SheetNames: [filename], |
|||
Sheets: { |
|||
[filename]: worksheet, |
|||
}, |
|||
}; |
|||
/* output format determined by filename */ |
|||
writeFile(workbook, filename, write2excelOpts); |
|||
/* at this point, out.xlsb will have been downloaded */ |
|||
} |
|||
|
|||
export function aoaToSheetXlsx<T = any>({ |
|||
data, |
|||
header, |
|||
filename = DEF_FILE_NAME, |
|||
write2excelOpts = { bookType: 'xlsx' }, |
|||
}: AoAToSheet<T>) { |
|||
const arrData = [...data]; |
|||
if (header) { |
|||
arrData.unshift(header); |
|||
} |
|||
|
|||
const worksheet = utils.aoa_to_sheet(arrData); |
|||
|
|||
/* add worksheet to workbook */ |
|||
const workbook: WorkBook = { |
|||
SheetNames: [filename], |
|||
Sheets: { |
|||
[filename]: worksheet, |
|||
}, |
|||
}; |
|||
/* output format determined by filename */ |
|||
writeFile(workbook, filename, write2excelOpts); |
|||
/* at this point, out.xlsb will have been downloaded */ |
|||
} |
|||
@ -1,126 +0,0 @@ |
|||
<template> |
|||
<div> |
|||
<input |
|||
ref="inputRef" |
|||
type="file" |
|||
v-show="false" |
|||
accept=".xlsx, .xls" |
|||
@change="handleInputClick" |
|||
/> |
|||
<div @click="handleUpload"> |
|||
<slot></slot> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, ref, unref } from 'vue'; |
|||
import XLSX from 'xlsx'; |
|||
|
|||
import type { ExcelData } from './typing'; |
|||
export default defineComponent({ |
|||
name: 'ImportExcel', |
|||
emits: ['success', 'error'], |
|||
setup(_, { emit }) { |
|||
const inputRef = ref<HTMLInputElement | null>(null); |
|||
const loadingRef = ref<Boolean>(false); |
|||
|
|||
/** |
|||
* @description: 第一行作为头部 |
|||
*/ |
|||
function getHeaderRow(sheet: XLSX.WorkSheet) { |
|||
if (!sheet || !sheet['!ref']) return []; |
|||
const headers: string[] = []; |
|||
// A3:B7=>{s:{c:0, r:2}, e:{c:1, r:6}} |
|||
const range = XLSX.utils.decode_range(sheet['!ref']); |
|||
|
|||
const R = range.s.r; |
|||
/* start in the first row */ |
|||
for (let C = range.s.c; C <= range.e.c; ++C) { |
|||
/* walk every column in the range */ |
|||
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]; |
|||
/* find the cell in the first row */ |
|||
let hdr = 'UNKNOWN ' + C; // <-- replace with your desired default |
|||
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell); |
|||
headers.push(hdr); |
|||
} |
|||
return headers; |
|||
} |
|||
|
|||
/** |
|||
* @description: 获得excel数据 |
|||
*/ |
|||
function getExcelData(workbook: XLSX.WorkBook) { |
|||
const excelData: ExcelData[] = []; |
|||
for (const sheetName of workbook.SheetNames) { |
|||
const worksheet = workbook.Sheets[sheetName]; |
|||
const header: string[] = getHeaderRow(worksheet); |
|||
const results = XLSX.utils.sheet_to_json(worksheet); |
|||
excelData.push({ |
|||
header, |
|||
results, |
|||
meta: { |
|||
sheetName, |
|||
}, |
|||
}); |
|||
} |
|||
return excelData; |
|||
} |
|||
|
|||
/** |
|||
* @description: 读取excel数据 |
|||
*/ |
|||
function readerData(rawFile: File) { |
|||
loadingRef.value = true; |
|||
return new Promise((resolve, reject) => { |
|||
const reader = new FileReader(); |
|||
reader.onload = async (e) => { |
|||
try { |
|||
const data = e.target && e.target.result; |
|||
const workbook = XLSX.read(data, { type: 'array' }); |
|||
// console.log(workbook); |
|||
/* DO SOMETHING WITH workbook HERE */ |
|||
const excelData = getExcelData(workbook); |
|||
emit('success', excelData); |
|||
resolve(''); |
|||
} catch (error) { |
|||
reject(error); |
|||
emit('error'); |
|||
} finally { |
|||
loadingRef.value = false; |
|||
} |
|||
}; |
|||
reader.readAsArrayBuffer(rawFile); |
|||
}); |
|||
} |
|||
|
|||
async function upload(rawFile: File) { |
|||
const inputRefDom = unref(inputRef); |
|||
if (inputRefDom) { |
|||
// fix can't select the same excel |
|||
inputRefDom.value = ''; |
|||
} |
|||
await readerData(rawFile); |
|||
} |
|||
|
|||
/** |
|||
* @description: 触发选择文件管理器 |
|||
*/ |
|||
function handleInputClick(e: Event) { |
|||
const files = e && (e.target as HTMLInputElement).files; |
|||
const rawFile = files && files[0]; // only setting files[0] |
|||
if (!rawFile) return; |
|||
upload(rawFile); |
|||
} |
|||
|
|||
/** |
|||
* @description: 点击上传按钮 |
|||
*/ |
|||
function handleUpload() { |
|||
const inputRefDom = unref(inputRef); |
|||
inputRefDom && inputRefDom.click(); |
|||
} |
|||
|
|||
return { handleUpload, handleInputClick, inputRef }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,158 +0,0 @@ |
|||
<template> |
|||
<div class="h-full" :class="prefixCls"> |
|||
<FlowChartToolbar :prefixCls="prefixCls" v-if="toolbar" @view-data="handlePreview" /> |
|||
<div ref="lfElRef" class="h-full"></div> |
|||
<BasicModal @register="register" title="流程数据" width="50%"> |
|||
<JsonPreview :data="graphData" /> |
|||
</BasicModal> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { Ref } from 'vue'; |
|||
import type { Definition } from '@logicflow/core'; |
|||
import { defineComponent, ref, onMounted, unref, nextTick, computed, watch } from 'vue'; |
|||
import FlowChartToolbar from './FlowChartToolbar.vue'; |
|||
import LogicFlow from '@logicflow/core'; |
|||
import { Snapshot, BpmnElement, Menu, DndPanel, SelectionSelect } from '@logicflow/extension'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useAppStore } from '/@/store/modules/app'; |
|||
import { createFlowChartContext } from './useFlowContext'; |
|||
import { toLogicFlowData } from './adpterForTurbo'; |
|||
import { useModal, BasicModal } from '/@/components/Modal'; |
|||
import { JsonPreview } from '/@/components/CodeEditor'; |
|||
import { configDefaultDndPanel } from './config'; |
|||
import '@logicflow/core/dist/style/index.css'; |
|||
import '@logicflow/extension/lib/style/index.css'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'FlowChart', |
|||
components: { BasicModal, FlowChartToolbar, JsonPreview }, |
|||
props: { |
|||
flowOptions: { |
|||
type: Object as PropType<Definition>, |
|||
default: () => ({}), |
|||
}, |
|||
|
|||
data: { |
|||
type: Object as PropType<any>, |
|||
default: () => ({}), |
|||
}, |
|||
|
|||
toolbar: { |
|||
type: Boolean, |
|||
default: true, |
|||
}, |
|||
patternItems: { |
|||
type: Array, |
|||
}, |
|||
}, |
|||
setup(props) { |
|||
const lfElRef = ref(null); |
|||
const graphData = ref({}); |
|||
|
|||
const lfInstance = ref(null) as Ref<LogicFlow | null>; |
|||
|
|||
const { prefixCls } = useDesign('flow-chart'); |
|||
const appStore = useAppStore(); |
|||
const [register, { openModal }] = useModal(); |
|||
createFlowChartContext({ |
|||
logicFlow: lfInstance as unknown as LogicFlow, |
|||
}); |
|||
|
|||
const getFlowOptions = computed(() => { |
|||
const { flowOptions } = props; |
|||
|
|||
const defaultOptions: Partial<Definition> = { |
|||
grid: true, |
|||
background: { |
|||
color: appStore.getDarkMode === 'light' ? '#f7f9ff' : '#151515', |
|||
}, |
|||
keyboard: { |
|||
enabled: true, |
|||
}, |
|||
...flowOptions, |
|||
}; |
|||
return defaultOptions as Definition; |
|||
}); |
|||
|
|||
watch( |
|||
() => props.data, |
|||
() => { |
|||
onRender(); |
|||
} |
|||
); |
|||
|
|||
// TODO |
|||
// watch( |
|||
// () => appStore.getDarkMode, |
|||
// () => { |
|||
// init(); |
|||
// } |
|||
// ); |
|||
|
|||
watch( |
|||
() => unref(getFlowOptions), |
|||
(options) => { |
|||
unref(lfInstance)?.updateEditConfig(options); |
|||
} |
|||
); |
|||
|
|||
// init logicFlow |
|||
async function init() { |
|||
await nextTick(); |
|||
|
|||
const lfEl = unref(lfElRef); |
|||
if (!lfEl) { |
|||
return; |
|||
} |
|||
LogicFlow.use(DndPanel); |
|||
|
|||
// Canvas configuration |
|||
LogicFlow.use(Snapshot); |
|||
// Use the bpmn plug-in to introduce bpmn elements, which can be used after conversion in turbo |
|||
LogicFlow.use(BpmnElement); |
|||
// Start the right-click menu |
|||
LogicFlow.use(Menu); |
|||
LogicFlow.use(SelectionSelect); |
|||
|
|||
lfInstance.value = new LogicFlow({ |
|||
...unref(getFlowOptions), |
|||
container: lfEl, |
|||
}); |
|||
const lf = unref(lfInstance)!; |
|||
lf?.setDefaultEdgeType('line'); |
|||
onRender(); |
|||
lf?.setPatternItems(props.patternItems || configDefaultDndPanel(lf)); |
|||
} |
|||
|
|||
async function onRender() { |
|||
await nextTick(); |
|||
const lf = unref(lfInstance); |
|||
if (!lf) { |
|||
return; |
|||
} |
|||
const lFData = toLogicFlowData(props.data); |
|||
lf.render(lFData); |
|||
} |
|||
|
|||
function handlePreview() { |
|||
const lf = unref(lfInstance); |
|||
if (!lf) { |
|||
return; |
|||
} |
|||
graphData.value = unref(lf).getGraphData(); |
|||
openModal(); |
|||
} |
|||
|
|||
onMounted(init); |
|||
|
|||
return { |
|||
register, |
|||
prefixCls, |
|||
lfElRef, |
|||
handlePreview, |
|||
graphData, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,13 +0,0 @@ |
|||
import BasicForm from './src/BasicForm.vue'; |
|||
|
|||
export * from './src/types/form'; |
|||
export * from './src/types/formItem'; |
|||
|
|||
export { useComponentRegister } from './src/hooks/useComponentRegister'; |
|||
export { useForm } from './src/hooks/useForm'; |
|||
|
|||
export { default as ApiSelect } from './src/components/ApiSelect.vue'; |
|||
export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue'; |
|||
export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue'; |
|||
|
|||
export { BasicForm }; |
|||
@ -1,336 +0,0 @@ |
|||
<template> |
|||
<Form |
|||
v-bind="getBindValue" |
|||
:class="getFormClass" |
|||
ref="formElRef" |
|||
:model="formModel" |
|||
@keypress.enter="handleEnterPress" |
|||
> |
|||
<Row v-bind="getRow"> |
|||
<slot name="formHeader"></slot> |
|||
<template v-for="schema in getSchema" :key="schema.field"> |
|||
<FormItem |
|||
:tableAction="tableAction" |
|||
:formActionType="formActionType" |
|||
:schema="schema" |
|||
:formProps="getProps" |
|||
:allDefaultValues="defaultValueRef" |
|||
:formModel="formModel" |
|||
:setFormModel="setFormModel" |
|||
> |
|||
<template #[item]="data" v-for="item in Object.keys($slots)"> |
|||
<slot :name="item" v-bind="data || {}"></slot> |
|||
</template> |
|||
</FormItem> |
|||
</template> |
|||
|
|||
<FormAction v-bind="{ ...getProps, ...advanceState }" @toggle-advanced="handleToggleAdvanced"> |
|||
<template |
|||
#[item]="data" |
|||
v-for="item in ['resetBefore', 'submitBefore', 'advanceBefore', 'advanceAfter']" |
|||
> |
|||
<slot :name="item" v-bind="data"></slot> |
|||
</template> |
|||
</FormAction> |
|||
<slot name="formFooter"></slot> |
|||
</Row> |
|||
</Form> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { FormActionType, FormProps, FormSchema } from './types/form'; |
|||
import type { AdvanceState } from './types/hooks'; |
|||
import type { Ref } from 'vue'; |
|||
|
|||
import { defineComponent, reactive, ref, computed, unref, onMounted, watch, nextTick } from 'vue'; |
|||
import { Form, Row } from 'ant-design-vue'; |
|||
import FormItem from './components/FormItem.vue'; |
|||
import FormAction from './components/FormAction.vue'; |
|||
|
|||
import { dateItemType } from './helper'; |
|||
import { dateUtil } from '/@/utils/dateUtil'; |
|||
|
|||
// import { cloneDeep } from 'lodash-es'; |
|||
import { deepMerge } from '/@/utils'; |
|||
|
|||
import { useFormValues } from './hooks/useFormValues'; |
|||
import useAdvanced from './hooks/useAdvanced'; |
|||
import { useFormEvents } from './hooks/useFormEvents'; |
|||
import { createFormContext } from './hooks/useFormContext'; |
|||
import { useAutoFocus } from './hooks/useAutoFocus'; |
|||
import { useModalContext } from '/@/components/Modal'; |
|||
|
|||
import { basicProps } from './props'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
|
|||
import type { RowProps } from 'ant-design-vue/lib/grid/Row'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicForm', |
|||
components: { FormItem, Form, Row, FormAction }, |
|||
props: basicProps, |
|||
emits: ['advanced-change', 'reset', 'submit', 'register'], |
|||
setup(props, { emit, attrs }) { |
|||
const formModel = reactive<Recordable>({}); |
|||
const modalFn = useModalContext(); |
|||
|
|||
const advanceState = reactive<AdvanceState>({ |
|||
isAdvanced: true, |
|||
hideAdvanceBtn: false, |
|||
isLoad: false, |
|||
actionSpan: 6, |
|||
}); |
|||
|
|||
const defaultValueRef = ref<Recordable>({}); |
|||
const isInitedDefaultRef = ref(false); |
|||
const propsRef = ref<Partial<FormProps>>({}); |
|||
const schemaRef = ref<Nullable<FormSchema[]>>(null); |
|||
const formElRef = ref<Nullable<FormActionType>>(null); |
|||
|
|||
const { prefixCls } = useDesign('basic-form'); |
|||
|
|||
// Get the basic configuration of the form |
|||
const getProps = computed((): FormProps => { |
|||
return { ...props, ...unref(propsRef) } as FormProps; |
|||
}); |
|||
|
|||
const getFormClass = computed(() => { |
|||
return [ |
|||
prefixCls, |
|||
{ |
|||
[`${prefixCls}--compact`]: unref(getProps).compact, |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
// Get uniform row style and Row configuration for the entire form |
|||
const getRow = computed((): RowProps => { |
|||
const { baseRowStyle = {}, rowProps } = unref(getProps); |
|||
return { |
|||
style: baseRowStyle, |
|||
...rowProps, |
|||
}; |
|||
}); |
|||
|
|||
const getBindValue = computed( |
|||
() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable) |
|||
); |
|||
|
|||
const getSchema = computed((): FormSchema[] => { |
|||
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any); |
|||
for (const schema of schemas) { |
|||
const { defaultValue, component } = schema; |
|||
// handle date type |
|||
if (defaultValue && dateItemType.includes(component)) { |
|||
if (!Array.isArray(defaultValue)) { |
|||
schema.defaultValue = dateUtil(defaultValue); |
|||
} else { |
|||
const def: moment.Moment[] = []; |
|||
defaultValue.forEach((item) => { |
|||
def.push(dateUtil(item)); |
|||
}); |
|||
schema.defaultValue = def; |
|||
} |
|||
} |
|||
} |
|||
return schemas as FormSchema[]; |
|||
}); |
|||
|
|||
const { handleToggleAdvanced } = useAdvanced({ |
|||
advanceState, |
|||
emit, |
|||
getProps, |
|||
getSchema, |
|||
formModel, |
|||
defaultValueRef, |
|||
}); |
|||
|
|||
const { handleFormValues, initDefault } = useFormValues({ |
|||
getProps, |
|||
defaultValueRef, |
|||
getSchema, |
|||
formModel, |
|||
}); |
|||
|
|||
useAutoFocus({ |
|||
getSchema, |
|||
getProps, |
|||
isInitedDefault: isInitedDefaultRef, |
|||
formElRef: formElRef as Ref<FormActionType>, |
|||
}); |
|||
|
|||
const { |
|||
handleSubmit, |
|||
setFieldsValue, |
|||
clearValidate, |
|||
validate, |
|||
validateFields, |
|||
getFieldsValue, |
|||
updateSchema, |
|||
resetSchema, |
|||
appendSchemaByField, |
|||
removeSchemaByFiled, |
|||
resetFields, |
|||
scrollToField, |
|||
} = useFormEvents({ |
|||
emit, |
|||
getProps, |
|||
formModel, |
|||
getSchema, |
|||
defaultValueRef, |
|||
formElRef: formElRef as Ref<FormActionType>, |
|||
schemaRef: schemaRef as Ref<FormSchema[]>, |
|||
handleFormValues, |
|||
}); |
|||
|
|||
createFormContext({ |
|||
resetAction: resetFields, |
|||
submitAction: handleSubmit, |
|||
}); |
|||
|
|||
watch( |
|||
() => unref(getProps).model, |
|||
() => { |
|||
const { model } = unref(getProps); |
|||
if (!model) return; |
|||
setFieldsValue(model); |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
} |
|||
); |
|||
|
|||
watch( |
|||
() => unref(getProps).schemas, |
|||
(schemas) => { |
|||
resetSchema(schemas ?? []); |
|||
} |
|||
); |
|||
|
|||
watch( |
|||
() => getSchema.value, |
|||
(schema) => { |
|||
nextTick(() => { |
|||
// Solve the problem of modal adaptive height calculation when the form is placed in the modal |
|||
modalFn?.redoModalHeight?.(); |
|||
}); |
|||
if (unref(isInitedDefaultRef)) { |
|||
return; |
|||
} |
|||
if (schema?.length) { |
|||
initDefault(); |
|||
isInitedDefaultRef.value = true; |
|||
} |
|||
} |
|||
); |
|||
|
|||
async function setProps(formProps: Partial<FormProps>): Promise<void> { |
|||
propsRef.value = deepMerge(unref(propsRef) || {}, formProps); |
|||
} |
|||
|
|||
function setFormModel(key: string, value: any) { |
|||
formModel[key] = value; |
|||
const { validateTrigger } = unref(getBindValue); |
|||
if (!validateTrigger || validateTrigger === 'change') { |
|||
validateFields([key]).catch((_) => {}); |
|||
} |
|||
} |
|||
|
|||
function handleEnterPress(e: KeyboardEvent) { |
|||
const { autoSubmitOnEnter } = unref(getProps); |
|||
if (!autoSubmitOnEnter) return; |
|||
if (e.key === 'Enter' && e.target && e.target instanceof HTMLElement) { |
|||
const target: HTMLElement = e.target as HTMLElement; |
|||
if (target && target.tagName && target.tagName.toUpperCase() == 'INPUT') { |
|||
handleSubmit(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
const formActionType: Partial<FormActionType> = { |
|||
getFieldsValue, |
|||
setFieldsValue, |
|||
resetFields, |
|||
updateSchema, |
|||
resetSchema, |
|||
setProps, |
|||
removeSchemaByFiled, |
|||
appendSchemaByField, |
|||
clearValidate, |
|||
validateFields, |
|||
validate, |
|||
submit: handleSubmit, |
|||
scrollToField: scrollToField, |
|||
}; |
|||
|
|||
onMounted(() => { |
|||
initDefault(); |
|||
emit('register', formActionType); |
|||
}); |
|||
|
|||
return { |
|||
getBindValue, |
|||
handleToggleAdvanced, |
|||
handleEnterPress, |
|||
formModel, |
|||
defaultValueRef, |
|||
advanceState, |
|||
getRow, |
|||
getProps, |
|||
formElRef, |
|||
getSchema, |
|||
formActionType, |
|||
setFormModel, |
|||
prefixCls, |
|||
getFormClass, |
|||
...formActionType, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-basic-form'; |
|||
|
|||
.@{prefix-cls} { |
|||
.ant-form-item { |
|||
&-label label::after { |
|||
margin: 0 6px 0 2px; |
|||
} |
|||
|
|||
&-with-help { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
&:not(.ant-form-item-with-help) { |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
&.suffix-item { |
|||
.ant-form-item-children { |
|||
display: flex; |
|||
} |
|||
|
|||
.ant-form-item-control { |
|||
margin-top: 4px; |
|||
} |
|||
|
|||
.suffix { |
|||
display: inline-flex; |
|||
padding-left: 6px; |
|||
margin-top: 1px; |
|||
line-height: 1; |
|||
align-items: center; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.ant-form-explain { |
|||
font-size: 14px; |
|||
} |
|||
|
|||
&--compact { |
|||
.ant-form-item { |
|||
margin-bottom: 8px !important; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,73 +0,0 @@ |
|||
import type { Component } from 'vue'; |
|||
import type { ComponentType } from './types/index'; |
|||
|
|||
/** |
|||
* Component list, register here to setting it in the form |
|||
*/ |
|||
import { |
|||
Input, |
|||
Select, |
|||
Radio, |
|||
Checkbox, |
|||
AutoComplete, |
|||
Cascader, |
|||
DatePicker, |
|||
InputNumber, |
|||
Switch, |
|||
TimePicker, |
|||
TreeSelect, |
|||
Slider, |
|||
Rate, |
|||
} from 'ant-design-vue'; |
|||
|
|||
import RadioButtonGroup from './components/RadioButtonGroup.vue'; |
|||
import ApiSelect from './components/ApiSelect.vue'; |
|||
import ApiTreeSelect from './components/ApiTreeSelect.vue'; |
|||
import { BasicUpload } from '/@/components/Upload'; |
|||
import { StrengthMeter } from '/@/components/StrengthMeter'; |
|||
import { IconPicker } from '/@/components/Icon'; |
|||
import { CountdownInput } from '/@/components/CountDown'; |
|||
|
|||
const componentMap = new Map<ComponentType, Component>(); |
|||
|
|||
componentMap.set('Input', Input); |
|||
componentMap.set('InputGroup', Input.Group); |
|||
componentMap.set('InputPassword', Input.Password); |
|||
componentMap.set('InputSearch', Input.Search); |
|||
componentMap.set('InputTextArea', Input.TextArea); |
|||
componentMap.set('InputNumber', InputNumber); |
|||
componentMap.set('AutoComplete', AutoComplete); |
|||
|
|||
componentMap.set('Select', Select); |
|||
componentMap.set('ApiSelect', ApiSelect); |
|||
componentMap.set('TreeSelect', TreeSelect); |
|||
componentMap.set('ApiTreeSelect', ApiTreeSelect); |
|||
componentMap.set('Switch', Switch); |
|||
componentMap.set('RadioButtonGroup', RadioButtonGroup); |
|||
componentMap.set('RadioGroup', Radio.Group); |
|||
componentMap.set('Checkbox', Checkbox); |
|||
componentMap.set('CheckboxGroup', Checkbox.Group); |
|||
componentMap.set('Cascader', Cascader); |
|||
componentMap.set('Slider', Slider); |
|||
componentMap.set('Rate', Rate); |
|||
|
|||
componentMap.set('DatePicker', DatePicker); |
|||
componentMap.set('MonthPicker', DatePicker.MonthPicker); |
|||
componentMap.set('RangePicker', DatePicker.RangePicker); |
|||
componentMap.set('WeekPicker', DatePicker.WeekPicker); |
|||
componentMap.set('TimePicker', TimePicker); |
|||
componentMap.set('StrengthMeter', StrengthMeter); |
|||
componentMap.set('IconPicker', IconPicker); |
|||
componentMap.set('InputCountDown', CountdownInput); |
|||
|
|||
componentMap.set('Upload', BasicUpload); |
|||
|
|||
export function add(compName: ComponentType, component: Component) { |
|||
componentMap.set(compName, component); |
|||
} |
|||
|
|||
export function del(compName: ComponentType) { |
|||
componentMap.delete(compName); |
|||
} |
|||
|
|||
export { componentMap }; |
|||
@ -1,147 +0,0 @@ |
|||
<template> |
|||
<Select |
|||
@dropdownVisibleChange="handleFetch" |
|||
v-bind="attrs" |
|||
@change="handleChange" |
|||
:options="getOptions" |
|||
v-model:value="state" |
|||
> |
|||
<template #[item]="data" v-for="item in Object.keys($slots)"> |
|||
<slot :name="item" v-bind="data || {}"></slot> |
|||
</template> |
|||
<template #suffixIcon v-if="loading"> |
|||
<LoadingOutlined spin /> |
|||
</template> |
|||
<template #notFoundContent v-if="loading"> |
|||
<span> |
|||
<LoadingOutlined spin class="mr-1" /> |
|||
{{ t('component.form.apiSelectNotFound') }} |
|||
</span> |
|||
</template> |
|||
</Select> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue'; |
|||
import { Select } from 'ant-design-vue'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; |
|||
import { useAttrs } from '/@/hooks/core/useAttrs'; |
|||
import { get, omit } from 'lodash-es'; |
|||
import { LoadingOutlined } from '@ant-design/icons-vue'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
|
|||
type OptionsItem = { label: string; value: string; disabled?: boolean }; |
|||
|
|||
export default defineComponent({ |
|||
name: 'ApiSelect', |
|||
components: { |
|||
Select, |
|||
LoadingOutlined, |
|||
}, |
|||
inheritAttrs: false, |
|||
props: { |
|||
value: propTypes.oneOfType([ |
|||
propTypes.object, |
|||
propTypes.number, |
|||
propTypes.string, |
|||
propTypes.array, |
|||
]), |
|||
numberToString: propTypes.bool, |
|||
api: { |
|||
type: Function as PropType<(arg?: Recordable) => Promise<OptionsItem[]>>, |
|||
default: null, |
|||
}, |
|||
// api params |
|||
params: { |
|||
type: Object as PropType<Recordable>, |
|||
default: () => ({}), |
|||
}, |
|||
// support xxx.xxx.xx |
|||
resultField: propTypes.string.def(''), |
|||
labelField: propTypes.string.def('label'), |
|||
valueField: propTypes.string.def('value'), |
|||
immediate: propTypes.bool.def(true), |
|||
}, |
|||
emits: ['options-change', 'change'], |
|||
setup(props, { emit }) { |
|||
const options = ref<OptionsItem[]>([]); |
|||
const loading = ref(false); |
|||
const isFirstLoad = ref(true); |
|||
const emitData = ref<any[]>([]); |
|||
const attrs = useAttrs(); |
|||
const { t } = useI18n(); |
|||
|
|||
// Embedded in the form, just use the hook binding to perform form verification |
|||
const [state] = useRuleFormItem(props, 'value', 'change', emitData); |
|||
|
|||
const getOptions = computed(() => { |
|||
const { labelField, valueField, numberToString } = props; |
|||
|
|||
return unref(options).reduce((prev, next: Recordable) => { |
|||
if (next) { |
|||
const value = next[valueField]; |
|||
prev.push({ |
|||
label: next[labelField], |
|||
value: numberToString ? `${value}` : value, |
|||
...omit(next, [labelField, valueField]), |
|||
}); |
|||
} |
|||
return prev; |
|||
}, [] as OptionsItem[]); |
|||
}); |
|||
|
|||
watchEffect(() => { |
|||
props.immediate && fetch(); |
|||
}); |
|||
|
|||
watch( |
|||
() => props.params, |
|||
() => { |
|||
!unref(isFirstLoad) && fetch(); |
|||
}, |
|||
{ deep: true } |
|||
); |
|||
|
|||
async function fetch() { |
|||
const api = props.api; |
|||
if (!api || !isFunction(api)) return; |
|||
options.value = []; |
|||
try { |
|||
loading.value = true; |
|||
const res = await api(props.params); |
|||
if (Array.isArray(res)) { |
|||
options.value = res; |
|||
emitChange(); |
|||
return; |
|||
} |
|||
if (props.resultField) { |
|||
options.value = get(res, props.resultField) || []; |
|||
} |
|||
emitChange(); |
|||
} catch (error) { |
|||
console.warn(error); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
async function handleFetch() { |
|||
if (!props.immediate && unref(isFirstLoad)) { |
|||
await fetch(); |
|||
isFirstLoad.value = false; |
|||
} |
|||
} |
|||
|
|||
function emitChange() { |
|||
emit('options-change', unref(getOptions)); |
|||
} |
|||
|
|||
function handleChange(_, ...args) { |
|||
emitData.value = args; |
|||
} |
|||
|
|||
return { state, attrs, getOptions, loading, t, handleFetch, handleChange }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,86 +0,0 @@ |
|||
<template> |
|||
<a-tree-select v-bind="getAttrs" @change="handleChange"> |
|||
<template #[item]="data" v-for="item in Object.keys($slots)"> |
|||
<slot :name="item" v-bind="data || {}"></slot> |
|||
</template> |
|||
<template #suffixIcon v-if="loading"> |
|||
<LoadingOutlined spin /> |
|||
</template> |
|||
</a-tree-select> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue'; |
|||
import { TreeSelect } from 'ant-design-vue'; |
|||
import { isArray, isFunction } from '/@/utils/is'; |
|||
import { get } from 'lodash-es'; |
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
import { LoadingOutlined } from '@ant-design/icons-vue'; |
|||
export default defineComponent({ |
|||
name: 'ApiTreeSelect', |
|||
components: { ATreeSelect: TreeSelect, LoadingOutlined }, |
|||
props: { |
|||
api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> }, |
|||
params: { type: Object }, |
|||
immediate: { type: Boolean, default: true }, |
|||
resultField: propTypes.string.def(''), |
|||
}, |
|||
emits: ['options-change', 'change'], |
|||
setup(props, { attrs, emit }) { |
|||
const treeData = ref<Recordable[]>([]); |
|||
const isFirstLoaded = ref<Boolean>(false); |
|||
const loading = ref(false); |
|||
const getAttrs = computed(() => { |
|||
return { |
|||
...(props.api ? { treeData: unref(treeData) } : {}), |
|||
...attrs, |
|||
}; |
|||
}); |
|||
|
|||
function handleChange(...args) { |
|||
emit('change', ...args); |
|||
} |
|||
|
|||
watch( |
|||
() => props.params, |
|||
() => { |
|||
isFirstLoaded.value && fetch(); |
|||
}, |
|||
{ deep: true } |
|||
); |
|||
|
|||
watch( |
|||
() => props.immediate, |
|||
(v) => { |
|||
v && !isFirstLoaded.value && fetch(); |
|||
} |
|||
); |
|||
|
|||
onMounted(() => { |
|||
props.immediate && fetch(); |
|||
}); |
|||
|
|||
async function fetch() { |
|||
const { api } = props; |
|||
if (!api || !isFunction(api)) return; |
|||
loading.value = true; |
|||
treeData.value = []; |
|||
let result; |
|||
try { |
|||
result = await api(props.params); |
|||
} catch (e) { |
|||
console.error(e); |
|||
} |
|||
loading.value = false; |
|||
if (!result) return; |
|||
if (!isArray(result)) { |
|||
result = get(result, props.resultField); |
|||
} |
|||
treeData.value = (result as Recordable[]) || []; |
|||
isFirstLoaded.value = true; |
|||
emit('options-change', treeData.value); |
|||
} |
|||
return { getAttrs, loading, handleChange }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,135 +0,0 @@ |
|||
<template> |
|||
<a-col v-bind="actionColOpt" v-if="showActionButtonGroup"> |
|||
<div style="width: 100%" :style="{ textAlign: actionColOpt.style.textAlign }"> |
|||
<FormItem> |
|||
<slot name="resetBefore"></slot> |
|||
<Button |
|||
type="default" |
|||
class="mr-2" |
|||
v-bind="getResetBtnOptions" |
|||
@click="resetAction" |
|||
v-if="showResetButton" |
|||
> |
|||
{{ getResetBtnOptions.text }} |
|||
</Button> |
|||
<slot name="submitBefore"></slot> |
|||
|
|||
<Button |
|||
type="primary" |
|||
class="mr-2" |
|||
v-bind="getSubmitBtnOptions" |
|||
@click="submitAction" |
|||
v-if="showSubmitButton" |
|||
> |
|||
{{ getSubmitBtnOptions.text }} |
|||
</Button> |
|||
|
|||
<slot name="advanceBefore"></slot> |
|||
<Button |
|||
type="link" |
|||
size="small" |
|||
@click="toggleAdvanced" |
|||
v-if="showAdvancedButton && !hideAdvanceBtn" |
|||
> |
|||
{{ isAdvanced ? t('component.form.putAway') : t('component.form.unfold') }} |
|||
<BasicArrow class="ml-1" :expand="!isAdvanced" up /> |
|||
</Button> |
|||
<slot name="advanceAfter"></slot> |
|||
</FormItem> |
|||
</div> |
|||
</a-col> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { ColEx } from '../types/index'; |
|||
//import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; |
|||
import { defineComponent, computed, PropType } from 'vue'; |
|||
import { Form, Col } from 'ant-design-vue'; |
|||
import { Button, ButtonProps } from '/@/components/Button'; |
|||
import { BasicArrow } from '/@/components/Basic'; |
|||
import { useFormContext } from '../hooks/useFormContext'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
|
|||
type ButtonOptions = Partial<ButtonProps> & { text: string }; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicFormAction', |
|||
components: { |
|||
FormItem: Form.Item, |
|||
Button, |
|||
BasicArrow, |
|||
[Col.name]: Col, |
|||
}, |
|||
props: { |
|||
showActionButtonGroup: propTypes.bool.def(true), |
|||
showResetButton: propTypes.bool.def(true), |
|||
showSubmitButton: propTypes.bool.def(true), |
|||
showAdvancedButton: propTypes.bool.def(true), |
|||
resetButtonOptions: { |
|||
type: Object as PropType<ButtonOptions>, |
|||
default: () => ({}), |
|||
}, |
|||
submitButtonOptions: { |
|||
type: Object as PropType<ButtonOptions>, |
|||
default: () => ({}), |
|||
}, |
|||
actionColOptions: { |
|||
type: Object as PropType<Partial<ColEx>>, |
|||
default: () => ({}), |
|||
}, |
|||
actionSpan: propTypes.number.def(6), |
|||
isAdvanced: propTypes.bool, |
|||
hideAdvanceBtn: propTypes.bool, |
|||
}, |
|||
emits: ['toggle-advanced'], |
|||
setup(props, { emit }) { |
|||
const { t } = useI18n(); |
|||
|
|||
const actionColOpt = computed(() => { |
|||
const { showAdvancedButton, actionSpan: span, actionColOptions } = props; |
|||
const actionSpan = 24 - span; |
|||
const advancedSpanObj = showAdvancedButton |
|||
? { span: actionSpan < 6 ? 24 : actionSpan } |
|||
: {}; |
|||
const actionColOpt: Partial<ColEx> = { |
|||
style: { textAlign: 'right' }, |
|||
span: showAdvancedButton ? 6 : 4, |
|||
...advancedSpanObj, |
|||
...actionColOptions, |
|||
}; |
|||
return actionColOpt; |
|||
}); |
|||
|
|||
const getResetBtnOptions = computed((): ButtonOptions => { |
|||
return Object.assign( |
|||
{ |
|||
text: t('common.resetText'), |
|||
}, |
|||
props.resetButtonOptions |
|||
); |
|||
}); |
|||
|
|||
const getSubmitBtnOptions = computed(() => { |
|||
return Object.assign( |
|||
{ |
|||
text: t('common.queryText'), |
|||
}, |
|||
props.submitButtonOptions |
|||
); |
|||
}); |
|||
|
|||
function toggleAdvanced() { |
|||
emit('toggle-advanced'); |
|||
} |
|||
|
|||
return { |
|||
t, |
|||
actionColOpt, |
|||
getResetBtnOptions, |
|||
getSubmitBtnOptions, |
|||
toggleAdvanced, |
|||
...useFormContext(), |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,366 +0,0 @@ |
|||
<script lang="tsx"> |
|||
import type { PropType, Ref } from 'vue'; |
|||
import type { FormActionType, FormProps } from '../types/form'; |
|||
import type { FormSchema } from '../types/form'; |
|||
import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; |
|||
import type { TableActionType } from '/@/components/Table'; |
|||
import { defineComponent, computed, unref, toRefs } from 'vue'; |
|||
import { Form, Col } from 'ant-design-vue'; |
|||
import { componentMap } from '../componentMap'; |
|||
import { BasicHelp } from '/@/components/Basic'; |
|||
import { isBoolean, isFunction, isNull } from '/@/utils/is'; |
|||
import { getSlot } from '/@/utils/helper/tsxHelper'; |
|||
import { createPlaceholderMessage, setComponentRuleType } from '../helper'; |
|||
import { upperFirst, cloneDeep } from 'lodash-es'; |
|||
import { useItemLabelWidth } from '../hooks/useLabelWidth'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicFormItem', |
|||
inheritAttrs: false, |
|||
props: { |
|||
schema: { |
|||
type: Object as PropType<FormSchema>, |
|||
default: () => ({}), |
|||
}, |
|||
formProps: { |
|||
type: Object as PropType<FormProps>, |
|||
default: () => ({}), |
|||
}, |
|||
allDefaultValues: { |
|||
type: Object as PropType<Recordable>, |
|||
default: () => ({}), |
|||
}, |
|||
formModel: { |
|||
type: Object as PropType<Recordable>, |
|||
default: () => ({}), |
|||
}, |
|||
setFormModel: { |
|||
type: Function as PropType<(key: string, value: any) => void>, |
|||
default: null, |
|||
}, |
|||
tableAction: { |
|||
type: Object as PropType<TableActionType>, |
|||
}, |
|||
formActionType: { |
|||
type: Object as PropType<FormActionType>, |
|||
}, |
|||
}, |
|||
setup(props, { slots }) { |
|||
const { t } = useI18n(); |
|||
|
|||
const { schema, formProps } = toRefs(props) as { |
|||
schema: Ref<FormSchema>; |
|||
formProps: Ref<FormProps>; |
|||
}; |
|||
|
|||
const itemLabelWidthProp = useItemLabelWidth(schema, formProps); |
|||
|
|||
const getValues = computed(() => { |
|||
const { allDefaultValues, formModel, schema } = props; |
|||
const { mergeDynamicData } = props.formProps; |
|||
return { |
|||
field: schema.field, |
|||
model: formModel, |
|||
values: { |
|||
...mergeDynamicData, |
|||
...allDefaultValues, |
|||
...formModel, |
|||
} as Recordable, |
|||
schema: schema, |
|||
}; |
|||
}); |
|||
|
|||
const getComponentsProps = computed(() => { |
|||
const { schema, tableAction, formModel, formActionType } = props; |
|||
const { componentProps = {} } = schema; |
|||
if (!isFunction(componentProps)) { |
|||
return componentProps; |
|||
} |
|||
return componentProps({ schema, tableAction, formModel, formActionType }) ?? {}; |
|||
}); |
|||
|
|||
const getDisable = computed(() => { |
|||
const { disabled: globDisabled } = props.formProps; |
|||
const { dynamicDisabled } = props.schema; |
|||
const { disabled: itemDisabled = false } = unref(getComponentsProps); |
|||
let disabled = !!globDisabled || itemDisabled; |
|||
if (isBoolean(dynamicDisabled)) { |
|||
disabled = dynamicDisabled; |
|||
} |
|||
if (isFunction(dynamicDisabled)) { |
|||
disabled = dynamicDisabled(unref(getValues)); |
|||
} |
|||
return disabled; |
|||
}); |
|||
|
|||
function getShow(): { isShow: boolean; isIfShow: boolean } { |
|||
const { show, ifShow } = props.schema; |
|||
const { showAdvancedButton } = props.formProps; |
|||
const itemIsAdvanced = showAdvancedButton |
|||
? isBoolean(props.schema.isAdvanced) |
|||
? props.schema.isAdvanced |
|||
: true |
|||
: true; |
|||
|
|||
let isShow = true; |
|||
let isIfShow = true; |
|||
|
|||
if (isBoolean(show)) { |
|||
isShow = show; |
|||
} |
|||
if (isBoolean(ifShow)) { |
|||
isIfShow = ifShow; |
|||
} |
|||
if (isFunction(show)) { |
|||
isShow = show(unref(getValues)); |
|||
} |
|||
if (isFunction(ifShow)) { |
|||
isIfShow = ifShow(unref(getValues)); |
|||
} |
|||
isShow = isShow && itemIsAdvanced; |
|||
return { isShow, isIfShow }; |
|||
} |
|||
|
|||
function handleRules(): ValidationRule[] { |
|||
const { |
|||
rules: defRules = [], |
|||
component, |
|||
rulesMessageJoinLabel, |
|||
label, |
|||
dynamicRules, |
|||
required, |
|||
} = props.schema; |
|||
|
|||
if (isFunction(dynamicRules)) { |
|||
return dynamicRules(unref(getValues)) as ValidationRule[]; |
|||
} |
|||
|
|||
let rules: ValidationRule[] = cloneDeep(defRules) as ValidationRule[]; |
|||
const { rulesMessageJoinLabel: globalRulesMessageJoinLabel } = props.formProps; |
|||
|
|||
const joinLabel = Reflect.has(props.schema, 'rulesMessageJoinLabel') |
|||
? rulesMessageJoinLabel |
|||
: globalRulesMessageJoinLabel; |
|||
const defaultMsg = createPlaceholderMessage(component) + `${joinLabel ? label : ''}`; |
|||
|
|||
function validator(rule: any, value: any) { |
|||
const msg = rule.message || defaultMsg; |
|||
if (value === undefined || isNull(value)) { |
|||
// 空值 |
|||
return Promise.reject(msg); |
|||
} else if (Array.isArray(value) && value.length === 0) { |
|||
// 数组类型 |
|||
return Promise.reject(msg); |
|||
} else if (typeof value === 'string' && value.trim() === '') { |
|||
// 空字符串 |
|||
return Promise.reject(msg); |
|||
} else if ( |
|||
typeof value === 'object' && |
|||
Reflect.has(value, 'checked') && |
|||
Reflect.has(value, 'halfChecked') && |
|||
Array.isArray(value.checked) && |
|||
Array.isArray(value.halfChecked) && |
|||
value.checked.length === 0 && |
|||
value.halfChecked.length === 0 |
|||
) { |
|||
// 非关联选择的tree组件 |
|||
return Promise.reject(msg); |
|||
} |
|||
return Promise.resolve(); |
|||
} |
|||
|
|||
const getRequired = isFunction(required) ? required(unref(getValues)) : required; |
|||
|
|||
if ((!rules || rules.length === 0) && getRequired) { |
|||
rules = [{ required: getRequired, validator }]; |
|||
} |
|||
|
|||
const requiredRuleIndex: number = rules.findIndex( |
|||
(rule) => Reflect.has(rule, 'required') && !Reflect.has(rule, 'validator') |
|||
); |
|||
|
|||
if (requiredRuleIndex !== -1) { |
|||
const rule = rules[requiredRuleIndex]; |
|||
const { isShow } = getShow(); |
|||
if (!isShow) { |
|||
rule.required = false; |
|||
} |
|||
if (component) { |
|||
if (!Reflect.has(rule, 'type')) { |
|||
rule.type = component === 'InputNumber' ? 'number' : 'string'; |
|||
} |
|||
|
|||
rule.message = rule.message || defaultMsg; |
|||
|
|||
if (component.includes('Input') || component.includes('Textarea')) { |
|||
rule.whitespace = true; |
|||
} |
|||
const valueFormat = unref(getComponentsProps)?.valueFormat; |
|||
setComponentRuleType(rule, component, valueFormat); |
|||
} |
|||
} |
|||
|
|||
// Maximum input length rule check |
|||
const characterInx = rules.findIndex((val) => val.max); |
|||
if (characterInx !== -1 && !rules[characterInx].validator) { |
|||
rules[characterInx].message = |
|||
rules[characterInx].message || |
|||
t('component.form.maxTip', [rules[characterInx].max] as Recordable); |
|||
} |
|||
return rules; |
|||
} |
|||
|
|||
function renderComponent() { |
|||
const { |
|||
renderComponentContent, |
|||
component, |
|||
field, |
|||
changeEvent = 'change', |
|||
valueField, |
|||
} = props.schema; |
|||
|
|||
const isCheck = component && ['Switch', 'Checkbox'].includes(component); |
|||
|
|||
const eventKey = `on${upperFirst(changeEvent)}`; |
|||
|
|||
const on = { |
|||
[eventKey]: (...args: Nullable<Recordable>[]) => { |
|||
const [e] = args; |
|||
if (propsData[eventKey]) { |
|||
propsData[eventKey](...args); |
|||
} |
|||
const target = e ? e.target : null; |
|||
const value = target ? (isCheck ? target.checked : target.value) : e; |
|||
props.setFormModel(field, value); |
|||
}, |
|||
}; |
|||
const Comp = componentMap.get(component) as ReturnType<typeof defineComponent>; |
|||
|
|||
const { autoSetPlaceHolder, size } = props.formProps; |
|||
const propsData: Recordable = { |
|||
allowClear: true, |
|||
getPopupContainer: (trigger: Element) => trigger.parentNode, |
|||
size, |
|||
...unref(getComponentsProps), |
|||
disabled: unref(getDisable), |
|||
}; |
|||
|
|||
const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder; |
|||
// RangePicker place is an array |
|||
if (isCreatePlaceholder && component !== 'RangePicker' && component) { |
|||
propsData.placeholder = |
|||
unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component); |
|||
} |
|||
propsData.codeField = field; |
|||
propsData.formValues = unref(getValues); |
|||
|
|||
const bindValue: Recordable = { |
|||
[valueField || (isCheck ? 'checked' : 'value')]: props.formModel[field], |
|||
}; |
|||
|
|||
const compAttr: Recordable = { |
|||
...propsData, |
|||
...on, |
|||
...bindValue, |
|||
}; |
|||
|
|||
if (!renderComponentContent) { |
|||
return <Comp {...compAttr} />; |
|||
} |
|||
const compSlot = isFunction(renderComponentContent) |
|||
? { ...renderComponentContent(unref(getValues)) } |
|||
: { |
|||
default: () => renderComponentContent, |
|||
}; |
|||
return <Comp {...compAttr}>{compSlot}</Comp>; |
|||
} |
|||
|
|||
function renderLabelHelpMessage() { |
|||
const { label, helpMessage, helpComponentProps, subLabel } = props.schema; |
|||
const renderLabel = subLabel ? ( |
|||
<span> |
|||
{label} <span class="text-secondary">{subLabel}</span> |
|||
</span> |
|||
) : ( |
|||
label |
|||
); |
|||
const getHelpMessage = isFunction(helpMessage) |
|||
? helpMessage(unref(getValues)) |
|||
: helpMessage; |
|||
if (!getHelpMessage || (Array.isArray(getHelpMessage) && getHelpMessage.length === 0)) { |
|||
return renderLabel; |
|||
} |
|||
return ( |
|||
<span> |
|||
{renderLabel} |
|||
<BasicHelp placement="top" class="mx-1" text={getHelpMessage} {...helpComponentProps} /> |
|||
</span> |
|||
); |
|||
} |
|||
|
|||
function renderItem() { |
|||
const { itemProps, slot, render, field, suffix } = props.schema; |
|||
const { labelCol, wrapperCol } = unref(itemLabelWidthProp); |
|||
const { colon } = props.formProps; |
|||
|
|||
const getContent = () => { |
|||
return slot |
|||
? getSlot(slots, slot, unref(getValues)) |
|||
: render |
|||
? render(unref(getValues)) |
|||
: renderComponent(); |
|||
}; |
|||
|
|||
const showSuffix = !!suffix; |
|||
const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix; |
|||
|
|||
return ( |
|||
<Form.Item |
|||
name={field} |
|||
colon={colon} |
|||
class={{ 'suffix-item': showSuffix }} |
|||
{...(itemProps as Recordable)} |
|||
label={renderLabelHelpMessage()} |
|||
rules={handleRules()} |
|||
labelCol={labelCol} |
|||
wrapperCol={wrapperCol} |
|||
> |
|||
<div style="display:flex"> |
|||
<div style="flex:1">{getContent()}</div> |
|||
{showSuffix && <span class="suffix">{getSuffix}</span>} |
|||
</div> |
|||
</Form.Item> |
|||
); |
|||
} |
|||
|
|||
return () => { |
|||
const { colProps = {}, colSlot, renderColContent, component } = props.schema; |
|||
if (!componentMap.has(component)) { |
|||
return null; |
|||
} |
|||
|
|||
const { baseColProps = {} } = props.formProps; |
|||
const realColProps = { ...baseColProps, ...colProps }; |
|||
const { isIfShow, isShow } = getShow(); |
|||
const values = unref(getValues); |
|||
|
|||
const getContent = () => { |
|||
return colSlot |
|||
? getSlot(slots, colSlot, values) |
|||
: renderColContent |
|||
? renderColContent(values) |
|||
: renderItem(); |
|||
}; |
|||
|
|||
return ( |
|||
isIfShow && ( |
|||
<Col {...realColProps} v-show={isShow}> |
|||
{getContent()} |
|||
</Col> |
|||
) |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,72 +0,0 @@ |
|||
import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; |
|||
import type { ComponentType } from './types/index'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import { dateUtil } from '/@/utils/dateUtil'; |
|||
import { isNumber, isObject } from '/@/utils/is'; |
|||
|
|||
const { t } = useI18n(); |
|||
|
|||
/** |
|||
* @description: 生成placeholder |
|||
*/ |
|||
export function createPlaceholderMessage(component: ComponentType) { |
|||
if (component.includes('Input') || component.includes('Complete')) { |
|||
return t('common.inputText'); |
|||
} |
|||
if (component.includes('Picker')) { |
|||
return t('common.chooseText'); |
|||
} |
|||
if ( |
|||
component.includes('Select') || |
|||
component.includes('Cascader') || |
|||
component.includes('Checkbox') || |
|||
component.includes('Radio') || |
|||
component.includes('Switch') |
|||
) { |
|||
// return `请选择${label}`;
|
|||
return t('common.chooseText'); |
|||
} |
|||
return ''; |
|||
} |
|||
|
|||
const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker']; |
|||
|
|||
function genType() { |
|||
return [...DATE_TYPE, 'RangePicker']; |
|||
} |
|||
|
|||
export function setComponentRuleType( |
|||
rule: ValidationRule, |
|||
component: ComponentType, |
|||
valueFormat: string |
|||
) { |
|||
if (['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'].includes(component)) { |
|||
rule.type = valueFormat ? 'string' : 'object'; |
|||
} else if (['RangePicker', 'Upload', 'CheckboxGroup', 'TimePicker'].includes(component)) { |
|||
rule.type = 'array'; |
|||
} else if (['InputNumber'].includes(component)) { |
|||
rule.type = 'number'; |
|||
} |
|||
} |
|||
|
|||
export function processDateValue(attr: Recordable, component: string) { |
|||
const { valueFormat, value } = attr; |
|||
if (valueFormat) { |
|||
attr.value = isObject(value) ? dateUtil(value).format(valueFormat) : value; |
|||
} else if (DATE_TYPE.includes(component) && value) { |
|||
attr.value = dateUtil(attr.value); |
|||
} |
|||
} |
|||
|
|||
export function handleInputNumberValue(component?: ComponentType, val?: any) { |
|||
if (!component) return val; |
|||
if (['Input', 'InputPassword', 'InputSearch', 'InputTextArea'].includes(component)) { |
|||
return val && isNumber(val) ? `${val}` : val; |
|||
} |
|||
return val; |
|||
} |
|||
|
|||
/** |
|||
* 时间字段 |
|||
*/ |
|||
export const dateItemType = genType(); |
|||
@ -1,165 +0,0 @@ |
|||
import type { ColEx } from '../types'; |
|||
import type { AdvanceState } from '../types/hooks'; |
|||
import type { ComputedRef, Ref } from 'vue'; |
|||
import type { FormProps, FormSchema } from '../types/form'; |
|||
import { computed, unref, watch } from 'vue'; |
|||
import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is'; |
|||
import { useBreakpoint } from '/@/hooks/event/useBreakpoint'; |
|||
import { useDebounceFn } from '@vueuse/core'; |
|||
|
|||
const BASIC_COL_LEN = 24; |
|||
|
|||
interface UseAdvancedContext { |
|||
advanceState: AdvanceState; |
|||
emit: EmitType; |
|||
getProps: ComputedRef<FormProps>; |
|||
getSchema: ComputedRef<FormSchema[]>; |
|||
formModel: Recordable; |
|||
defaultValueRef: Ref<Recordable>; |
|||
} |
|||
|
|||
export default function ({ |
|||
advanceState, |
|||
emit, |
|||
getProps, |
|||
getSchema, |
|||
formModel, |
|||
defaultValueRef, |
|||
}: UseAdvancedContext) { |
|||
const { realWidthRef, screenEnum, screenRef } = useBreakpoint(); |
|||
|
|||
const getEmptySpan = computed((): number => { |
|||
if (!advanceState.isAdvanced) { |
|||
return 0; |
|||
} |
|||
// For some special cases, you need to manually specify additional blank lines
|
|||
const emptySpan = unref(getProps).emptySpan || 0; |
|||
|
|||
if (isNumber(emptySpan)) { |
|||
return emptySpan; |
|||
} |
|||
if (isObject(emptySpan)) { |
|||
const { span = 0 } = emptySpan; |
|||
const screen = unref(screenRef) as string; |
|||
|
|||
const screenSpan = (emptySpan as any)[screen.toLowerCase()]; |
|||
return screenSpan || span || 0; |
|||
} |
|||
return 0; |
|||
}); |
|||
|
|||
const debounceUpdateAdvanced = useDebounceFn(updateAdvanced, 30); |
|||
|
|||
watch( |
|||
[() => unref(getSchema), () => advanceState.isAdvanced, () => unref(realWidthRef)], |
|||
() => { |
|||
const { showAdvancedButton } = unref(getProps); |
|||
if (showAdvancedButton) { |
|||
debounceUpdateAdvanced(); |
|||
} |
|||
}, |
|||
{ immediate: true } |
|||
); |
|||
|
|||
function getAdvanced(itemCol: Partial<ColEx>, itemColSum = 0, isLastAction = false) { |
|||
const width = unref(realWidthRef); |
|||
|
|||
const mdWidth = |
|||
parseInt(itemCol.md as string) || |
|||
parseInt(itemCol.xs as string) || |
|||
parseInt(itemCol.sm as string) || |
|||
(itemCol.span as number) || |
|||
BASIC_COL_LEN; |
|||
|
|||
const lgWidth = parseInt(itemCol.lg as string) || mdWidth; |
|||
const xlWidth = parseInt(itemCol.xl as string) || lgWidth; |
|||
const xxlWidth = parseInt(itemCol.xxl as string) || xlWidth; |
|||
if (width <= screenEnum.LG) { |
|||
itemColSum += mdWidth; |
|||
} else if (width < screenEnum.XL) { |
|||
itemColSum += lgWidth; |
|||
} else if (width < screenEnum.XXL) { |
|||
itemColSum += xlWidth; |
|||
} else { |
|||
itemColSum += xxlWidth; |
|||
} |
|||
|
|||
if (isLastAction) { |
|||
advanceState.hideAdvanceBtn = false; |
|||
if (itemColSum <= BASIC_COL_LEN * 2) { |
|||
// When less than or equal to 2 lines, the collapse and expand buttons are not displayed
|
|||
advanceState.hideAdvanceBtn = true; |
|||
advanceState.isAdvanced = true; |
|||
} else if ( |
|||
itemColSum > BASIC_COL_LEN * 2 && |
|||
itemColSum <= BASIC_COL_LEN * (unref(getProps).autoAdvancedLine || 3) |
|||
) { |
|||
advanceState.hideAdvanceBtn = false; |
|||
|
|||
// More than 3 lines collapsed by default
|
|||
} else if (!advanceState.isLoad) { |
|||
advanceState.isLoad = true; |
|||
advanceState.isAdvanced = !advanceState.isAdvanced; |
|||
} |
|||
return { isAdvanced: advanceState.isAdvanced, itemColSum }; |
|||
} |
|||
if (itemColSum > BASIC_COL_LEN * (unref(getProps).alwaysShowLines || 1)) { |
|||
return { isAdvanced: advanceState.isAdvanced, itemColSum }; |
|||
} else { |
|||
// The first line is always displayed
|
|||
return { isAdvanced: true, itemColSum }; |
|||
} |
|||
} |
|||
|
|||
function updateAdvanced() { |
|||
let itemColSum = 0; |
|||
let realItemColSum = 0; |
|||
const { baseColProps = {} } = unref(getProps); |
|||
|
|||
for (const schema of unref(getSchema)) { |
|||
const { show, colProps } = schema; |
|||
let isShow = true; |
|||
|
|||
if (isBoolean(show)) { |
|||
isShow = show; |
|||
} |
|||
|
|||
if (isFunction(show)) { |
|||
isShow = show({ |
|||
schema: schema, |
|||
model: formModel, |
|||
field: schema.field, |
|||
values: { |
|||
...unref(defaultValueRef), |
|||
...formModel, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
if (isShow && (colProps || baseColProps)) { |
|||
const { itemColSum: sum, isAdvanced } = getAdvanced( |
|||
{ ...baseColProps, ...colProps }, |
|||
itemColSum |
|||
); |
|||
|
|||
itemColSum = sum || 0; |
|||
if (isAdvanced) { |
|||
realItemColSum = itemColSum; |
|||
} |
|||
schema.isAdvanced = isAdvanced; |
|||
} |
|||
} |
|||
|
|||
advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpan); |
|||
|
|||
getAdvanced(unref(getProps).actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true); |
|||
|
|||
emit('advanced-change'); |
|||
} |
|||
|
|||
function handleToggleAdvanced() { |
|||
advanceState.isAdvanced = !advanceState.isAdvanced; |
|||
} |
|||
|
|||
return { handleToggleAdvanced }; |
|||
} |
|||
@ -1,122 +0,0 @@ |
|||
import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form'; |
|||
import type { NamePath } from 'ant-design-vue/lib/form/interface'; |
|||
import type { DynamicProps } from '/#/utils'; |
|||
import { ref, onUnmounted, unref, nextTick, watch } from 'vue'; |
|||
import { isProdMode } from '/@/utils/env'; |
|||
import { error } from '/@/utils/log'; |
|||
import { getDynamicProps } from '/@/utils'; |
|||
|
|||
export declare type ValidateFields = (nameList?: NamePath[]) => Promise<Recordable>; |
|||
|
|||
type Props = Partial<DynamicProps<FormProps>>; |
|||
|
|||
export function useForm(props?: Props): UseFormReturnType { |
|||
const formRef = ref<Nullable<FormActionType>>(null); |
|||
const loadedRef = ref<Nullable<boolean>>(false); |
|||
|
|||
async function getForm() { |
|||
const form = unref(formRef); |
|||
if (!form) { |
|||
error( |
|||
'The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!' |
|||
); |
|||
} |
|||
await nextTick(); |
|||
return form as FormActionType; |
|||
} |
|||
|
|||
function register(instance: FormActionType) { |
|||
isProdMode() && |
|||
onUnmounted(() => { |
|||
formRef.value = null; |
|||
loadedRef.value = null; |
|||
}); |
|||
if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return; |
|||
|
|||
formRef.value = instance; |
|||
loadedRef.value = true; |
|||
|
|||
watch( |
|||
() => props, |
|||
() => { |
|||
props && instance.setProps(getDynamicProps(props)); |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
deep: true, |
|||
} |
|||
); |
|||
} |
|||
|
|||
const methods: FormActionType = { |
|||
scrollToField: async (name: NamePath, options?: ScrollOptions | undefined) => { |
|||
const form = await getForm(); |
|||
form.scrollToField(name, options); |
|||
}, |
|||
setProps: async (formProps: Partial<FormProps>) => { |
|||
const form = await getForm(); |
|||
form.setProps(formProps); |
|||
}, |
|||
|
|||
updateSchema: async (data: Partial<FormSchema> | Partial<FormSchema>[]) => { |
|||
const form = await getForm(); |
|||
form.updateSchema(data); |
|||
}, |
|||
|
|||
resetSchema: async (data: Partial<FormSchema> | Partial<FormSchema>[]) => { |
|||
const form = await getForm(); |
|||
form.resetSchema(data); |
|||
}, |
|||
|
|||
clearValidate: async (name?: string | string[]) => { |
|||
const form = await getForm(); |
|||
form.clearValidate(name); |
|||
}, |
|||
|
|||
resetFields: async () => { |
|||
getForm().then(async (form) => { |
|||
await form.resetFields(); |
|||
}); |
|||
}, |
|||
|
|||
removeSchemaByFiled: async (field: string | string[]) => { |
|||
unref(formRef)?.removeSchemaByFiled(field); |
|||
}, |
|||
|
|||
// TODO promisify
|
|||
getFieldsValue: <T>() => { |
|||
return unref(formRef)?.getFieldsValue() as T; |
|||
}, |
|||
|
|||
setFieldsValue: async <T>(values: T) => { |
|||
const form = await getForm(); |
|||
form.setFieldsValue<T>(values); |
|||
}, |
|||
|
|||
appendSchemaByField: async ( |
|||
schema: FormSchema, |
|||
prefixField: string | undefined, |
|||
first: boolean |
|||
) => { |
|||
const form = await getForm(); |
|||
form.appendSchemaByField(schema, prefixField, first); |
|||
}, |
|||
|
|||
submit: async (): Promise<any> => { |
|||
const form = await getForm(); |
|||
return form.submit(); |
|||
}, |
|||
|
|||
validate: async (nameList?: NamePath[]): Promise<Recordable> => { |
|||
const form = await getForm(); |
|||
return form.validate(nameList); |
|||
}, |
|||
|
|||
validateFields: async (nameList?: NamePath[]): Promise<Recordable> => { |
|||
const form = await getForm(); |
|||
return form.validateFields(nameList); |
|||
}, |
|||
}; |
|||
|
|||
return [register, methods]; |
|||
} |
|||
@ -1,260 +0,0 @@ |
|||
import type { ComputedRef, Ref } from 'vue'; |
|||
import type { FormProps, FormSchema, FormActionType } from '../types/form'; |
|||
import type { NamePath } from 'ant-design-vue/lib/form/interface'; |
|||
import { unref, toRaw } from 'vue'; |
|||
import { isArray, isFunction, isObject, isString } from '/@/utils/is'; |
|||
import { deepMerge } from '/@/utils'; |
|||
import { dateItemType, handleInputNumberValue } from '../helper'; |
|||
import { dateUtil } from '/@/utils/dateUtil'; |
|||
import { cloneDeep, uniqBy } from 'lodash-es'; |
|||
import { error } from '/@/utils/log'; |
|||
|
|||
interface UseFormActionContext { |
|||
emit: EmitType; |
|||
getProps: ComputedRef<FormProps>; |
|||
getSchema: ComputedRef<FormSchema[]>; |
|||
formModel: Recordable; |
|||
defaultValueRef: Ref<Recordable>; |
|||
formElRef: Ref<FormActionType>; |
|||
schemaRef: Ref<FormSchema[]>; |
|||
handleFormValues: Fn; |
|||
} |
|||
export function useFormEvents({ |
|||
emit, |
|||
getProps, |
|||
formModel, |
|||
getSchema, |
|||
defaultValueRef, |
|||
formElRef, |
|||
schemaRef, |
|||
handleFormValues, |
|||
}: UseFormActionContext) { |
|||
async function resetFields(): Promise<void> { |
|||
const { resetFunc, submitOnReset } = unref(getProps); |
|||
resetFunc && isFunction(resetFunc) && (await resetFunc()); |
|||
|
|||
const formEl = unref(formElRef); |
|||
if (!formEl) return; |
|||
|
|||
Object.keys(formModel).forEach((key) => { |
|||
formModel[key] = defaultValueRef.value[key]; |
|||
}); |
|||
clearValidate(); |
|||
emit('reset', toRaw(formModel)); |
|||
submitOnReset && handleSubmit(); |
|||
} |
|||
|
|||
/** |
|||
* @description: Set form value |
|||
*/ |
|||
async function setFieldsValue(values: Recordable): Promise<void> { |
|||
const fields = unref(getSchema) |
|||
.map((item) => item.field) |
|||
.filter(Boolean); |
|||
|
|||
const validKeys: string[] = []; |
|||
Object.keys(values).forEach((key) => { |
|||
const schema = unref(getSchema).find((item) => item.field === key); |
|||
let value = values[key]; |
|||
|
|||
const hasKey = Reflect.has(values, key); |
|||
|
|||
value = handleInputNumberValue(schema?.component, value); |
|||
// 0| '' is allow
|
|||
if (hasKey && fields.includes(key)) { |
|||
// time type
|
|||
if (itemIsDateType(key)) { |
|||
if (Array.isArray(value)) { |
|||
const arr: any[] = []; |
|||
for (const ele of value) { |
|||
arr.push(ele ? dateUtil(ele) : null); |
|||
} |
|||
formModel[key] = arr; |
|||
} else { |
|||
const { componentProps } = schema || {}; |
|||
let _props = componentProps as any; |
|||
if (typeof componentProps === 'function') { |
|||
_props = _props({ formModel }); |
|||
} |
|||
formModel[key] = value ? (_props?.valueFormat ? value : dateUtil(value)) : null; |
|||
} |
|||
} else { |
|||
formModel[key] = value; |
|||
} |
|||
validKeys.push(key); |
|||
} |
|||
}); |
|||
validateFields(validKeys).catch((_) => {}); |
|||
} |
|||
/** |
|||
* @description: Delete based on field name |
|||
*/ |
|||
async function removeSchemaByFiled(fields: string | string[]): Promise<void> { |
|||
const schemaList: FormSchema[] = cloneDeep(unref(getSchema)); |
|||
if (!fields) { |
|||
return; |
|||
} |
|||
|
|||
let fieldList: string[] = isString(fields) ? [fields] : fields; |
|||
if (isString(fields)) { |
|||
fieldList = [fields]; |
|||
} |
|||
for (const field of fieldList) { |
|||
_removeSchemaByFiled(field, schemaList); |
|||
} |
|||
schemaRef.value = schemaList; |
|||
} |
|||
|
|||
/** |
|||
* @description: Delete based on field name |
|||
*/ |
|||
function _removeSchemaByFiled(field: string, schemaList: FormSchema[]): void { |
|||
if (isString(field)) { |
|||
const index = schemaList.findIndex((schema) => schema.field === field); |
|||
if (index !== -1) { |
|||
delete formModel[field]; |
|||
schemaList.splice(index, 1); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @description: Insert after a certain field, if not insert the last |
|||
*/ |
|||
async function appendSchemaByField(schema: FormSchema, prefixField?: string, first = false) { |
|||
const schemaList: FormSchema[] = cloneDeep(unref(getSchema)); |
|||
|
|||
const index = schemaList.findIndex((schema) => schema.field === prefixField); |
|||
const hasInList = schemaList.some((item) => item.field === prefixField || schema.field); |
|||
|
|||
if (!hasInList) return; |
|||
|
|||
if (!prefixField || index === -1 || first) { |
|||
first ? schemaList.unshift(schema) : schemaList.push(schema); |
|||
schemaRef.value = schemaList; |
|||
return; |
|||
} |
|||
if (index !== -1) { |
|||
schemaList.splice(index + 1, 0, schema); |
|||
} |
|||
schemaRef.value = schemaList; |
|||
} |
|||
|
|||
async function resetSchema(data: Partial<FormSchema> | Partial<FormSchema>[]) { |
|||
let updateData: Partial<FormSchema>[] = []; |
|||
if (isObject(data)) { |
|||
updateData.push(data as FormSchema); |
|||
} |
|||
if (isArray(data)) { |
|||
updateData = [...data]; |
|||
} |
|||
|
|||
const hasField = updateData.every((item) => Reflect.has(item, 'field') && item.field); |
|||
|
|||
if (!hasField) { |
|||
error( |
|||
'All children of the form Schema array that need to be updated must contain the `field` field' |
|||
); |
|||
return; |
|||
} |
|||
schemaRef.value = updateData as FormSchema[]; |
|||
} |
|||
|
|||
async function updateSchema(data: Partial<FormSchema> | Partial<FormSchema>[]) { |
|||
let updateData: Partial<FormSchema>[] = []; |
|||
if (isObject(data)) { |
|||
updateData.push(data as FormSchema); |
|||
} |
|||
if (isArray(data)) { |
|||
updateData = [...data]; |
|||
} |
|||
|
|||
const hasField = updateData.every((item) => Reflect.has(item, 'field') && item.field); |
|||
|
|||
if (!hasField) { |
|||
error( |
|||
'All children of the form Schema array that need to be updated must contain the `field` field' |
|||
); |
|||
return; |
|||
} |
|||
const schema: FormSchema[] = []; |
|||
updateData.forEach((item) => { |
|||
unref(getSchema).forEach((val) => { |
|||
if (val.field === item.field) { |
|||
const newSchema = deepMerge(val, item); |
|||
schema.push(newSchema as FormSchema); |
|||
} else { |
|||
schema.push(val); |
|||
} |
|||
}); |
|||
}); |
|||
schemaRef.value = uniqBy(schema, 'field'); |
|||
} |
|||
|
|||
function getFieldsValue(): Recordable { |
|||
const formEl = unref(formElRef); |
|||
if (!formEl) return {}; |
|||
return handleFormValues(toRaw(unref(formModel))); |
|||
} |
|||
|
|||
/** |
|||
* @description: Is it time |
|||
*/ |
|||
function itemIsDateType(key: string) { |
|||
return unref(getSchema).some((item) => { |
|||
return item.field === key ? dateItemType.includes(item.component) : false; |
|||
}); |
|||
} |
|||
|
|||
async function validateFields(nameList?: NamePath[] | undefined) { |
|||
return unref(formElRef)?.validateFields(nameList); |
|||
} |
|||
|
|||
async function validate(nameList?: NamePath[] | undefined) { |
|||
return await unref(formElRef)?.validate(nameList); |
|||
} |
|||
|
|||
async function clearValidate(name?: string | string[]) { |
|||
await unref(formElRef)?.clearValidate(name); |
|||
} |
|||
|
|||
async function scrollToField(name: NamePath, options?: ScrollOptions | undefined) { |
|||
await unref(formElRef)?.scrollToField(name, options); |
|||
} |
|||
|
|||
/** |
|||
* @description: Form submission |
|||
*/ |
|||
async function handleSubmit(e?: Event): Promise<void> { |
|||
e && e.preventDefault(); |
|||
const { submitFunc } = unref(getProps); |
|||
if (submitFunc && isFunction(submitFunc)) { |
|||
await submitFunc(); |
|||
return; |
|||
} |
|||
const formEl = unref(formElRef); |
|||
if (!formEl) return; |
|||
try { |
|||
const values = await validate(); |
|||
const res = handleFormValues(values); |
|||
emit('submit', res); |
|||
} catch (error) { |
|||
throw new Error(error); |
|||
} |
|||
} |
|||
|
|||
return { |
|||
handleSubmit, |
|||
clearValidate, |
|||
validate, |
|||
validateFields, |
|||
getFieldsValue, |
|||
updateSchema, |
|||
resetSchema, |
|||
appendSchemaByField, |
|||
removeSchemaByFiled, |
|||
resetFields, |
|||
setFieldsValue, |
|||
scrollToField, |
|||
}; |
|||
} |
|||
@ -1,87 +0,0 @@ |
|||
import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is'; |
|||
import { dateUtil } from '/@/utils/dateUtil'; |
|||
import { unref } from 'vue'; |
|||
import type { Ref, ComputedRef } from 'vue'; |
|||
import type { FormProps, FormSchema } from '../types/form'; |
|||
import { set } from 'lodash-es'; |
|||
|
|||
interface UseFormValuesContext { |
|||
defaultValueRef: Ref<any>; |
|||
getSchema: ComputedRef<FormSchema[]>; |
|||
getProps: ComputedRef<FormProps>; |
|||
formModel: Recordable; |
|||
} |
|||
export function useFormValues({ |
|||
defaultValueRef, |
|||
getSchema, |
|||
formModel, |
|||
getProps, |
|||
}: UseFormValuesContext) { |
|||
// Processing form values
|
|||
function handleFormValues(values: Recordable) { |
|||
if (!isObject(values)) { |
|||
return {}; |
|||
} |
|||
const res: Recordable = {}; |
|||
for (const item of Object.entries(values)) { |
|||
let [, value] = item; |
|||
const [key] = item; |
|||
if (!key || (isArray(value) && value.length === 0) || isFunction(value)) { |
|||
continue; |
|||
} |
|||
const transformDateFunc = unref(getProps).transformDateFunc; |
|||
if (isObject(value)) { |
|||
value = transformDateFunc?.(value); |
|||
} |
|||
if (isArray(value) && value[0]?._isAMomentObject && value[1]?._isAMomentObject) { |
|||
value = value.map((item) => transformDateFunc?.(item)); |
|||
} |
|||
// Remove spaces
|
|||
if (isString(value)) { |
|||
value = value.trim(); |
|||
} |
|||
set(res, key, value); |
|||
} |
|||
return handleRangeTimeValue(res); |
|||
} |
|||
|
|||
/** |
|||
* @description: Processing time interval parameters |
|||
*/ |
|||
function handleRangeTimeValue(values: Recordable) { |
|||
const fieldMapToTime = unref(getProps).fieldMapToTime; |
|||
|
|||
if (!fieldMapToTime || !Array.isArray(fieldMapToTime)) { |
|||
return values; |
|||
} |
|||
|
|||
for (const [field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD'] of fieldMapToTime) { |
|||
if (!field || !startTimeKey || !endTimeKey || !values[field]) { |
|||
continue; |
|||
} |
|||
|
|||
const [startTime, endTime]: string[] = values[field]; |
|||
|
|||
values[startTimeKey] = dateUtil(startTime).format(format); |
|||
values[endTimeKey] = dateUtil(endTime).format(format); |
|||
Reflect.deleteProperty(values, field); |
|||
} |
|||
|
|||
return values; |
|||
} |
|||
|
|||
function initDefault() { |
|||
const schemas = unref(getSchema); |
|||
const obj: Recordable = {}; |
|||
schemas.forEach((item) => { |
|||
const { defaultValue } = item; |
|||
if (!isNullOrUnDef(defaultValue)) { |
|||
obj[item.field] = defaultValue; |
|||
formModel[item.field] = defaultValue; |
|||
} |
|||
}); |
|||
defaultValueRef.value = obj; |
|||
} |
|||
|
|||
return { handleFormValues, initDefault }; |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
import type { Ref } from 'vue'; |
|||
import type { FormProps, FormSchema } from '../types/form'; |
|||
|
|||
import { computed, unref } from 'vue'; |
|||
import { isNumber } from '/@/utils/is'; |
|||
|
|||
export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<FormProps>) { |
|||
return computed(() => { |
|||
const schemaItem = unref(schemaItemRef); |
|||
const { labelCol = {}, wrapperCol = {} } = schemaItem.itemProps || {}; |
|||
const { labelWidth, disabledLabelWidth } = schemaItem; |
|||
|
|||
const { |
|||
labelWidth: globalLabelWidth, |
|||
labelCol: globalLabelCol, |
|||
wrapperCol: globWrapperCol, |
|||
} = unref(propsRef); |
|||
|
|||
// If labelWidth is set globally, all items setting
|
|||
if ((!globalLabelWidth && !labelWidth && !globalLabelCol) || disabledLabelWidth) { |
|||
labelCol.style = { |
|||
textAlign: 'left', |
|||
}; |
|||
return { labelCol, wrapperCol }; |
|||
} |
|||
let width = labelWidth || globalLabelWidth; |
|||
const col = { ...globalLabelCol, ...labelCol }; |
|||
const wrapCol = { ...globWrapperCol, ...wrapperCol }; |
|||
|
|||
if (width) { |
|||
width = isNumber(width) ? `${width}px` : width; |
|||
} |
|||
|
|||
return { |
|||
labelCol: { style: { width }, ...col }, |
|||
wrapperCol: { style: { width: `calc(100% - ${width})` }, ...wrapCol }, |
|||
}; |
|||
}); |
|||
} |
|||
@ -1,102 +0,0 @@ |
|||
import type { FieldMapToTime, FormSchema } from './types/form'; |
|||
import type { CSSProperties, PropType } from 'vue'; |
|||
import type { ColEx } from './types'; |
|||
import type { TableActionType } from '/@/components/Table'; |
|||
import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; |
|||
import type { RowProps } from 'ant-design-vue/lib/grid/Row'; |
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
|
|||
export const basicProps = { |
|||
model: { |
|||
type: Object as PropType<Recordable>, |
|||
default: {}, |
|||
}, |
|||
// 标签宽度 固定宽度
|
|||
labelWidth: { |
|||
type: [Number, String] as PropType<number | string>, |
|||
default: 0, |
|||
}, |
|||
fieldMapToTime: { |
|||
type: Array as PropType<FieldMapToTime>, |
|||
default: () => [], |
|||
}, |
|||
compact: propTypes.bool, |
|||
// 表单配置规则
|
|||
schemas: { |
|||
type: [Array] as PropType<FormSchema[]>, |
|||
default: () => [], |
|||
}, |
|||
mergeDynamicData: { |
|||
type: Object as PropType<Recordable>, |
|||
default: null, |
|||
}, |
|||
baseRowStyle: { |
|||
type: Object as PropType<CSSProperties>, |
|||
}, |
|||
baseColProps: { |
|||
type: Object as PropType<Partial<ColEx>>, |
|||
}, |
|||
autoSetPlaceHolder: propTypes.bool.def(true), |
|||
// 在INPUT组件上单击回车时,是否自动提交
|
|||
autoSubmitOnEnter: propTypes.bool.def(false), |
|||
submitOnReset: propTypes.bool, |
|||
size: propTypes.oneOf(['default', 'small', 'large']).def('default'), |
|||
// 禁用表单
|
|||
disabled: propTypes.bool, |
|||
emptySpan: { |
|||
type: [Number, Object] as PropType<number>, |
|||
default: 0, |
|||
}, |
|||
// 是否显示收起展开按钮
|
|||
showAdvancedButton: propTypes.bool, |
|||
// 转化时间
|
|||
transformDateFunc: { |
|||
type: Function as PropType<Fn>, |
|||
default: (date: any) => { |
|||
return date._isAMomentObject ? date?.format('YYYY-MM-DD HH:mm:ss') : date; |
|||
}, |
|||
}, |
|||
rulesMessageJoinLabel: propTypes.bool.def(true), |
|||
// 超过3行自动折叠
|
|||
autoAdvancedLine: propTypes.number.def(3), |
|||
// 不受折叠影响的行数
|
|||
alwaysShowLines: propTypes.number.def(1), |
|||
|
|||
// 是否显示操作按钮
|
|||
showActionButtonGroup: propTypes.bool.def(true), |
|||
// 操作列Col配置
|
|||
actionColOptions: Object as PropType<Partial<ColEx>>, |
|||
// 显示重置按钮
|
|||
showResetButton: propTypes.bool.def(true), |
|||
// 是否聚焦第一个输入框,只在第一个表单项为input的时候作用
|
|||
autoFocusFirstItem: propTypes.bool, |
|||
// 重置按钮配置
|
|||
resetButtonOptions: Object as PropType<Partial<ButtonProps>>, |
|||
|
|||
// 显示确认按钮
|
|||
showSubmitButton: propTypes.bool.def(true), |
|||
// 确认按钮配置
|
|||
submitButtonOptions: Object as PropType<Partial<ButtonProps>>, |
|||
|
|||
// 自定义重置函数
|
|||
resetFunc: Function as PropType<() => Promise<void>>, |
|||
submitFunc: Function as PropType<() => Promise<void>>, |
|||
|
|||
// 以下为默认props
|
|||
hideRequiredMark: propTypes.bool, |
|||
|
|||
labelCol: Object as PropType<Partial<ColEx>>, |
|||
|
|||
layout: propTypes.oneOf(['horizontal', 'vertical', 'inline']).def('horizontal'), |
|||
tableAction: { |
|||
type: Object as PropType<TableActionType>, |
|||
}, |
|||
|
|||
wrapperCol: Object as PropType<Partial<ColEx>>, |
|||
|
|||
colon: propTypes.bool, |
|||
|
|||
labelAlign: propTypes.string, |
|||
|
|||
rowProps: Object as PropType<RowProps>, |
|||
}; |
|||
@ -1,220 +0,0 @@ |
|||
import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface'; |
|||
import type { VNode } from 'vue'; |
|||
import type { ButtonProps as AntdButtonProps } from '/@/components/Button'; |
|||
import type { FormItem } from './formItem'; |
|||
import type { ColEx, ComponentType } from './index'; |
|||
import type { TableActionType } from '/@/components/Table/src/types/table'; |
|||
import type { CSSProperties } from 'vue'; |
|||
import type { RowProps } from 'ant-design-vue/lib/grid/Row'; |
|||
|
|||
export type FieldMapToTime = [string, [string, string], string?][]; |
|||
|
|||
export type Rule = RuleObject & { |
|||
trigger?: 'blur' | 'change' | ['change', 'blur']; |
|||
}; |
|||
|
|||
export interface RenderCallbackParams { |
|||
schema: FormSchema; |
|||
values: Recordable; |
|||
model: Recordable; |
|||
field: string; |
|||
} |
|||
|
|||
export interface ButtonProps extends AntdButtonProps { |
|||
text?: string; |
|||
} |
|||
|
|||
export interface FormActionType { |
|||
submit: () => Promise<void>; |
|||
setFieldsValue: <T>(values: T) => Promise<void>; |
|||
resetFields: () => Promise<void>; |
|||
getFieldsValue: () => Recordable; |
|||
clearValidate: (name?: string | string[]) => Promise<void>; |
|||
updateSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>; |
|||
resetSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>; |
|||
setProps: (formProps: Partial<FormProps>) => Promise<void>; |
|||
removeSchemaByFiled: (field: string | string[]) => Promise<void>; |
|||
appendSchemaByField: ( |
|||
schema: FormSchema, |
|||
prefixField: string | undefined, |
|||
first?: boolean | undefined |
|||
) => Promise<void>; |
|||
validateFields: (nameList?: NamePath[]) => Promise<any>; |
|||
validate: (nameList?: NamePath[]) => Promise<any>; |
|||
scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>; |
|||
} |
|||
|
|||
export type RegisterFn = (formInstance: FormActionType) => void; |
|||
|
|||
export type UseFormReturnType = [RegisterFn, FormActionType]; |
|||
|
|||
export interface FormProps { |
|||
layout?: 'vertical' | 'inline' | 'horizontal'; |
|||
// Form value
|
|||
model?: Recordable; |
|||
// The width of all items in the entire form
|
|||
labelWidth?: number | string; |
|||
//alignment
|
|||
labelAlign?: 'left' | 'right'; |
|||
//Row configuration for the entire form
|
|||
rowProps?: RowProps; |
|||
// Submit form on reset
|
|||
submitOnReset?: boolean; |
|||
// Col configuration for the entire form
|
|||
labelCol?: Partial<ColEx>; |
|||
// Col configuration for the entire form
|
|||
wrapperCol?: Partial<ColEx>; |
|||
|
|||
// General row style
|
|||
baseRowStyle?: CSSProperties; |
|||
|
|||
// General col configuration
|
|||
baseColProps?: Partial<ColEx>; |
|||
|
|||
// Form configuration rules
|
|||
schemas?: FormSchema[]; |
|||
// Function values used to merge into dynamic control form items
|
|||
mergeDynamicData?: Recordable; |
|||
// Compact mode for search forms
|
|||
compact?: boolean; |
|||
// Blank line span
|
|||
emptySpan?: number | Partial<ColEx>; |
|||
// Internal component size of the form
|
|||
size?: 'default' | 'small' | 'large'; |
|||
// Whether to disable
|
|||
disabled?: boolean; |
|||
// Time interval fields are mapped into multiple
|
|||
fieldMapToTime?: FieldMapToTime; |
|||
// Placeholder is set automatically
|
|||
autoSetPlaceHolder?: boolean; |
|||
// Auto submit on press enter on input
|
|||
autoSubmitOnEnter?: boolean; |
|||
// Check whether the information is added to the label
|
|||
rulesMessageJoinLabel?: boolean; |
|||
// Whether to show collapse and expand buttons
|
|||
showAdvancedButton?: boolean; |
|||
// Whether to focus on the first input box, only works when the first form item is input
|
|||
autoFocusFirstItem?: boolean; |
|||
// Automatically collapse over the specified number of rows
|
|||
autoAdvancedLine?: number; |
|||
// Always show lines
|
|||
alwaysShowLines?: number; |
|||
// Whether to show the operation button
|
|||
showActionButtonGroup?: boolean; |
|||
|
|||
// Reset button configuration
|
|||
resetButtonOptions?: Partial<ButtonProps>; |
|||
|
|||
// Confirm button configuration
|
|||
submitButtonOptions?: Partial<ButtonProps>; |
|||
|
|||
// Operation column configuration
|
|||
actionColOptions?: Partial<ColEx>; |
|||
|
|||
// Show reset button
|
|||
showResetButton?: boolean; |
|||
// Show confirmation button
|
|||
showSubmitButton?: boolean; |
|||
|
|||
resetFunc?: () => Promise<void>; |
|||
submitFunc?: () => Promise<void>; |
|||
transformDateFunc?: (date: any) => string; |
|||
colon?: boolean; |
|||
} |
|||
export interface FormSchema { |
|||
// Field name
|
|||
field: string; |
|||
// Event name triggered by internal value change, default change
|
|||
changeEvent?: string; |
|||
// Variable name bound to v-model Default value
|
|||
valueField?: string; |
|||
// Label name
|
|||
label: string; |
|||
// Auxiliary text
|
|||
subLabel?: string; |
|||
// Help text on the right side of the text
|
|||
helpMessage?: |
|||
| string |
|||
| string[] |
|||
| ((renderCallbackParams: RenderCallbackParams) => string | string[]); |
|||
// BaseHelp component props
|
|||
helpComponentProps?: Partial<HelpComponentProps>; |
|||
// Label width, if it is passed, the labelCol and WrapperCol configured by itemProps will be invalid
|
|||
labelWidth?: string | number; |
|||
// Disable the adjustment of labelWidth with global settings of formModel, and manually set labelCol and wrapperCol by yourself
|
|||
disabledLabelWidth?: boolean; |
|||
// render component
|
|||
component: ComponentType; |
|||
// Component parameters
|
|||
componentProps?: |
|||
| ((opt: { |
|||
schema: FormSchema; |
|||
tableAction: TableActionType; |
|||
formActionType: FormActionType; |
|||
formModel: Recordable; |
|||
}) => Recordable) |
|||
| object; |
|||
// Required
|
|||
required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); |
|||
|
|||
suffix?: string | number | ((values: RenderCallbackParams) => string | number); |
|||
|
|||
// Validation rules
|
|||
rules?: Rule[]; |
|||
// Check whether the information is added to the label
|
|||
rulesMessageJoinLabel?: boolean; |
|||
|
|||
// Reference formModelItem
|
|||
itemProps?: Partial<FormItem>; |
|||
|
|||
// col configuration outside formModelItem
|
|||
colProps?: Partial<ColEx>; |
|||
|
|||
// 默认值
|
|||
defaultValue?: any; |
|||
isAdvanced?: boolean; |
|||
|
|||
// Matching details components
|
|||
span?: number; |
|||
|
|||
ifShow?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); |
|||
|
|||
show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); |
|||
|
|||
// Render the content in the form-item tag
|
|||
render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string; |
|||
|
|||
// Rendering col content requires outer wrapper form-item
|
|||
renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string; |
|||
|
|||
renderComponentContent?: |
|||
| ((renderCallbackParams: RenderCallbackParams) => any) |
|||
| VNode |
|||
| VNode[] |
|||
| string; |
|||
|
|||
// Custom slot, in from-item
|
|||
slot?: string; |
|||
|
|||
// Custom slot, similar to renderColContent
|
|||
colSlot?: string; |
|||
|
|||
dynamicDisabled?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); |
|||
|
|||
dynamicRules?: (renderCallbackParams: RenderCallbackParams) => Rule[]; |
|||
} |
|||
export interface HelpComponentProps { |
|||
maxWidth: string; |
|||
// Whether to display the serial number
|
|||
showIndex: boolean; |
|||
// Text list
|
|||
text: any; |
|||
// colour
|
|||
color: string; |
|||
// font size
|
|||
fontSize: string; |
|||
icon: string; |
|||
absolute: boolean; |
|||
// Positioning
|
|||
position: any; |
|||
} |
|||
@ -1,112 +0,0 @@ |
|||
type ColSpanType = number | string; |
|||
export interface ColEx { |
|||
style?: any; |
|||
/** |
|||
* raster number of cells to occupy, 0 corresponds to display: none |
|||
* @default none (0) |
|||
* @type ColSpanType |
|||
*/ |
|||
span?: ColSpanType; |
|||
|
|||
/** |
|||
* raster order, used in flex layout mode |
|||
* @default 0 |
|||
* @type ColSpanType |
|||
*/ |
|||
order?: ColSpanType; |
|||
|
|||
/** |
|||
* the layout fill of flex |
|||
* @default none |
|||
* @type ColSpanType |
|||
*/ |
|||
flex?: ColSpanType; |
|||
|
|||
/** |
|||
* the number of cells to offset Col from the left |
|||
* @default 0 |
|||
* @type ColSpanType |
|||
*/ |
|||
offset?: ColSpanType; |
|||
|
|||
/** |
|||
* the number of cells that raster is moved to the right |
|||
* @default 0 |
|||
* @type ColSpanType |
|||
*/ |
|||
push?: ColSpanType; |
|||
|
|||
/** |
|||
* the number of cells that raster is moved to the left |
|||
* @default 0 |
|||
* @type ColSpanType |
|||
*/ |
|||
pull?: ColSpanType; |
|||
|
|||
/** |
|||
* <576px and also default setting, could be a span value or an object containing above props |
|||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType |
|||
*/ |
|||
xs?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; |
|||
|
|||
/** |
|||
* ≥576px, could be a span value or an object containing above props |
|||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType |
|||
*/ |
|||
sm?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; |
|||
|
|||
/** |
|||
* ≥768px, could be a span value or an object containing above props |
|||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType |
|||
*/ |
|||
md?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; |
|||
|
|||
/** |
|||
* ≥992px, could be a span value or an object containing above props |
|||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType |
|||
*/ |
|||
lg?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; |
|||
|
|||
/** |
|||
* ≥1200px, could be a span value or an object containing above props |
|||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType |
|||
*/ |
|||
xl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; |
|||
|
|||
/** |
|||
* ≥1600px, could be a span value or an object containing above props |
|||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType |
|||
*/ |
|||
xxl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; |
|||
} |
|||
|
|||
export type ComponentType = |
|||
| 'Input' |
|||
| 'InputGroup' |
|||
| 'InputPassword' |
|||
| 'InputSearch' |
|||
| 'InputTextArea' |
|||
| 'InputNumber' |
|||
| 'InputCountDown' |
|||
| 'Select' |
|||
| 'ApiSelect' |
|||
| 'TreeSelect' |
|||
| 'ApiTreeSelect' |
|||
| 'RadioButtonGroup' |
|||
| 'RadioGroup' |
|||
| 'Checkbox' |
|||
| 'CheckboxGroup' |
|||
| 'AutoComplete' |
|||
| 'Cascader' |
|||
| 'DatePicker' |
|||
| 'MonthPicker' |
|||
| 'RangePicker' |
|||
| 'WeekPicker' |
|||
| 'TimePicker' |
|||
| 'Switch' |
|||
| 'StrengthMeter' |
|||
| 'Upload' |
|||
| 'IconPicker' |
|||
| 'Render' |
|||
| 'Slider' |
|||
| 'Rate'; |
|||
@ -1,216 +0,0 @@ |
|||
<template> |
|||
<a-input |
|||
disabled |
|||
:style="{ width }" |
|||
:placeholder="t('component.icon.placeholder')" |
|||
:class="prefixCls" |
|||
v-model:value="currentSelect" |
|||
> |
|||
<template #addonAfter> |
|||
<Popover |
|||
placement="bottomLeft" |
|||
trigger="click" |
|||
v-model="visible" |
|||
:overlayClassName="`${prefixCls}-popover`" |
|||
> |
|||
<template #title> |
|||
<div class="flex justify-between"> |
|||
<a-input |
|||
:placeholder="t('component.icon.search')" |
|||
@change="handleSearchChange" |
|||
allowClear |
|||
/> |
|||
</div> |
|||
</template> |
|||
|
|||
<template #content> |
|||
<div v-if="getPaginationList.length"> |
|||
<ScrollContainer class="border border-solid border-t-0"> |
|||
<ul class="flex flex-wrap px-2"> |
|||
<li |
|||
v-for="icon in getPaginationList" |
|||
:key="icon" |
|||
:class="currentSelect === icon ? 'border border-primary' : ''" |
|||
class=" |
|||
p-2 |
|||
w-1/8 |
|||
cursor-pointer |
|||
mr-1 |
|||
mt-1 |
|||
flex |
|||
justify-center |
|||
items-center |
|||
border border-solid |
|||
hover:border-primary |
|||
" |
|||
@click="handleClick(icon)" |
|||
:title="icon" |
|||
> |
|||
<!-- <Icon :icon="icon" :prefix="prefix" /> --> |
|||
<SvgIcon v-if="isSvgMode" :name="icon" /> |
|||
<Icon :icon="icon" v-else /> |
|||
</li> |
|||
</ul> |
|||
</ScrollContainer> |
|||
<div class="flex py-2 items-center justify-center" v-if="getTotal >= pageSize"> |
|||
<Pagination |
|||
showLessItems |
|||
size="small" |
|||
:pageSize="pageSize" |
|||
:total="getTotal" |
|||
@change="handlePageChange" |
|||
/> |
|||
</div> |
|||
</div> |
|||
<template v-else |
|||
><div class="p-5"> <Empty /></div> |
|||
</template> |
|||
</template> |
|||
|
|||
<span class="cursor-pointer px-2 py-1 flex items-center" v-if="isSvgMode && currentSelect"> |
|||
<SvgIcon :name="currentSelect" /> |
|||
</span> |
|||
<Icon :icon="currentSelect || 'ion:apps-outline'" class="cursor-pointer px-2 py-1" v-else /> |
|||
</Popover> |
|||
</template> |
|||
</a-input> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, ref, watchEffect, watch, unref } from 'vue'; |
|||
|
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { ScrollContainer } from '/@/components/Container'; |
|||
|
|||
import { Input, Popover, Pagination, Empty } from 'ant-design-vue'; |
|||
import Icon from './Icon.vue'; |
|||
import SvgIcon from './SvgIcon.vue'; |
|||
|
|||
import iconsData from '../data/icons.data'; |
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
import { usePagination } from '/@/hooks/web/usePagination'; |
|||
import { useDebounceFn } from '@vueuse/core'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard'; |
|||
import { useMessage } from '/@/hooks/web/useMessage'; |
|||
import svgIcons from 'virtual:svg-icons-names'; |
|||
|
|||
function getIcons() { |
|||
const data = iconsData as any; |
|||
const prefix: string = data?.prefix ?? ''; |
|||
let result: string[] = []; |
|||
if (prefix) { |
|||
result = (data?.icons ?? []).map((item) => `${prefix}:${item}`); |
|||
} else if (Array.isArray(iconsData)) { |
|||
result = iconsData as string[]; |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
function getSvgIcons() { |
|||
return svgIcons.map((icon) => icon.replace('icon-', '')); |
|||
} |
|||
|
|||
export default defineComponent({ |
|||
name: 'IconPicker', |
|||
components: { [Input.name]: Input, Icon, Popover, ScrollContainer, Pagination, Empty, SvgIcon }, |
|||
inheritAttrs: false, |
|||
props: { |
|||
value: propTypes.string, |
|||
width: propTypes.string.def('100%'), |
|||
pageSize: propTypes.number.def(140), |
|||
copy: propTypes.bool.def(false), |
|||
mode: propTypes.oneOf<('svg' | 'iconify')[]>(['svg', 'iconify']).def('iconify'), |
|||
}, |
|||
emits: ['change', 'update:value'], |
|||
setup(props, { emit }) { |
|||
const isSvgMode = props.mode === 'svg'; |
|||
const icons = isSvgMode ? getSvgIcons() : getIcons(); |
|||
|
|||
const currentSelect = ref(''); |
|||
const visible = ref(false); |
|||
const currentList = ref(icons); |
|||
|
|||
const { t } = useI18n(); |
|||
const { prefixCls } = useDesign('icon-picker'); |
|||
|
|||
const debounceHandleSearchChange = useDebounceFn(handleSearchChange, 100); |
|||
const { clipboardRef, isSuccessRef } = useCopyToClipboard(props.value); |
|||
const { createMessage } = useMessage(); |
|||
|
|||
const { getPaginationList, getTotal, setCurrentPage } = usePagination( |
|||
currentList, |
|||
props.pageSize |
|||
); |
|||
|
|||
watchEffect(() => { |
|||
currentSelect.value = props.value; |
|||
}); |
|||
|
|||
watch( |
|||
() => currentSelect.value, |
|||
(v) => { |
|||
emit('update:value', v); |
|||
return emit('change', v); |
|||
} |
|||
); |
|||
|
|||
function handlePageChange(page: number) { |
|||
setCurrentPage(page); |
|||
} |
|||
|
|||
function handleClick(icon: string) { |
|||
currentSelect.value = icon; |
|||
if (props.copy) { |
|||
clipboardRef.value = icon; |
|||
if (unref(isSuccessRef)) { |
|||
createMessage.success(t('component.icon.copy')); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function handleSearchChange(e: ChangeEvent) { |
|||
const value = e.target.value; |
|||
if (!value) { |
|||
setCurrentPage(1); |
|||
currentList.value = icons; |
|||
return; |
|||
} |
|||
currentList.value = icons.filter((item) => item.includes(value)); |
|||
} |
|||
|
|||
return { |
|||
t, |
|||
prefixCls, |
|||
visible, |
|||
isSvgMode, |
|||
getTotal, |
|||
getPaginationList, |
|||
handlePageChange, |
|||
handleClick, |
|||
currentSelect, |
|||
handleSearchChange: debounceHandleSearchChange, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-icon-picker'; |
|||
|
|||
.@{prefix-cls} { |
|||
.ant-input-group-addon { |
|||
padding: 0; |
|||
} |
|||
|
|||
&-popover { |
|||
width: 300px; |
|||
|
|||
.ant-popover-inner-content { |
|||
padding: 0; |
|||
} |
|||
|
|||
.scrollbar { |
|||
height: 220px; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,67 +0,0 @@ |
|||
<template> |
|||
<section class="full-loading" :class="{ absolute }" v-show="loading"> |
|||
<Spin v-bind="$attrs" :tip="tip" :size="size" :spinning="loading" /> |
|||
</section> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { PropType } from 'vue'; |
|||
import { defineComponent } from 'vue'; |
|||
import { Spin } from 'ant-design-vue'; |
|||
import { SizeEnum } from '/@/enums/sizeEnum'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'Loading', |
|||
components: { Spin }, |
|||
props: { |
|||
tip: { |
|||
type: String as PropType<string>, |
|||
default: '', |
|||
}, |
|||
size: { |
|||
type: String as PropType<SizeEnum>, |
|||
default: SizeEnum.LARGE, |
|||
validator: (v: SizeEnum): boolean => { |
|||
return [SizeEnum.DEFAULT, SizeEnum.SMALL, SizeEnum.LARGE].includes(v); |
|||
}, |
|||
}, |
|||
absolute: { |
|||
type: Boolean as PropType<boolean>, |
|||
default: false, |
|||
}, |
|||
loading: { |
|||
type: Boolean as PropType<boolean>, |
|||
default: false, |
|||
}, |
|||
background: { |
|||
type: String as PropType<string>, |
|||
}, |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less" scoped> |
|||
.full-loading { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 200; |
|||
display: flex; |
|||
width: 100%; |
|||
height: 100%; |
|||
justify-content: center; |
|||
align-items: center; |
|||
background-color: rgba(240, 242, 245, 0.4); |
|||
|
|||
&.absolute { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 300; |
|||
} |
|||
} |
|||
|
|||
html[data-theme='dark'] { |
|||
.full-loading { |
|||
background-color: @modal-mask-bg; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,49 +0,0 @@ |
|||
import { unref } from 'vue'; |
|||
import { createLoading } from './createLoading'; |
|||
import type { LoadingProps } from './typing'; |
|||
import type { Ref } from 'vue'; |
|||
|
|||
export interface UseLoadingOptions { |
|||
target?: HTMLElement | Ref<ElRef>; |
|||
props?: Partial<LoadingProps>; |
|||
} |
|||
|
|||
interface Fn { |
|||
(): void; |
|||
} |
|||
|
|||
export function useLoading(props: Partial<LoadingProps>): [Fn, Fn, (string) => void]; |
|||
export function useLoading(opt: Partial<UseLoadingOptions>): [Fn, Fn, (string) => void]; |
|||
|
|||
export function useLoading( |
|||
opt: Partial<LoadingProps> | Partial<UseLoadingOptions> |
|||
): [Fn, Fn, (string) => void] { |
|||
let props: Partial<LoadingProps>; |
|||
let target: HTMLElement | Ref<ElRef> = document.body; |
|||
|
|||
if (Reflect.has(opt, 'target') || Reflect.has(opt, 'props')) { |
|||
const options = opt as Partial<UseLoadingOptions>; |
|||
props = options.props || {}; |
|||
target = options.target || document.body; |
|||
} else { |
|||
props = opt as Partial<LoadingProps>; |
|||
} |
|||
|
|||
const instance = createLoading(props, undefined, true); |
|||
|
|||
const open = (): void => { |
|||
const t = unref(target as Ref<ElRef>); |
|||
if (!t) return; |
|||
instance.open(t); |
|||
}; |
|||
|
|||
const close = (): void => { |
|||
instance.close(); |
|||
}; |
|||
|
|||
const setTip = (tip: string) => { |
|||
instance.setTip(tip); |
|||
}; |
|||
|
|||
return [open, close, setTip]; |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import markDown from './src/Markdown.vue'; |
|||
|
|||
export const MarkDown = withInstall(markDown); |
|||
export * from './src/typing'; |
|||
@ -1,143 +0,0 @@ |
|||
<template> |
|||
<div ref="wrapRef"></div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { |
|||
defineComponent, |
|||
ref, |
|||
unref, |
|||
nextTick, |
|||
computed, |
|||
watch, |
|||
onBeforeUnmount, |
|||
onDeactivated, |
|||
} from 'vue'; |
|||
import Vditor from 'vditor'; |
|||
import 'vditor/dist/index.css'; |
|||
import { useLocale } from '/@/locales/useLocale'; |
|||
import { useModalContext } from '../../Modal'; |
|||
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; |
|||
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated'; |
|||
|
|||
type Lang = 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' | undefined; |
|||
|
|||
export default defineComponent({ |
|||
inheritAttrs: false, |
|||
props: { |
|||
height: { type: Number, default: 360 }, |
|||
value: { type: String, default: '' }, |
|||
}, |
|||
emits: ['change', 'get', 'update:value'], |
|||
setup(props, { attrs, emit }) { |
|||
const wrapRef = ref<ElRef>(null); |
|||
const vditorRef = ref<Nullable<Vditor>>(null); |
|||
const initedRef = ref(false); |
|||
|
|||
const modalFn = useModalContext(); |
|||
|
|||
const { getLocale } = useLocale(); |
|||
const { getDarkMode } = useRootSetting(); |
|||
const valueRef = ref(''); |
|||
|
|||
watch( |
|||
[() => getDarkMode.value, () => initedRef.value], |
|||
([val, inited]) => { |
|||
if (!inited) { |
|||
return; |
|||
} |
|||
const theme = val === 'dark' ? 'dark' : 'classic'; |
|||
instance.getVditor()?.setTheme(theme); |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
flush: 'post', |
|||
} |
|||
); |
|||
|
|||
watch( |
|||
() => props.value, |
|||
(v) => { |
|||
if (v !== valueRef.value) { |
|||
instance.getVditor()?.setValue(v); |
|||
} |
|||
valueRef.value = v; |
|||
} |
|||
); |
|||
|
|||
const getCurrentLang = computed((): 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' => { |
|||
let lang: Lang; |
|||
switch (unref(getLocale)) { |
|||
case 'en': |
|||
lang = 'en_US'; |
|||
break; |
|||
case 'ja': |
|||
lang = 'ja_JP'; |
|||
break; |
|||
case 'ko': |
|||
lang = 'ko_KR'; |
|||
break; |
|||
default: |
|||
lang = 'zh_CN'; |
|||
} |
|||
return lang; |
|||
}); |
|||
function init() { |
|||
const wrapEl = unref(wrapRef) as HTMLElement; |
|||
if (!wrapEl) return; |
|||
const bindValue = { ...attrs, ...props }; |
|||
const insEditor = new Vditor(wrapEl, { |
|||
theme: getDarkMode.value === 'dark' ? 'dark' : 'classic', |
|||
lang: unref(getCurrentLang), |
|||
mode: 'sv', |
|||
preview: { |
|||
actions: [], |
|||
}, |
|||
input: (v) => { |
|||
valueRef.value = v; |
|||
emit('update:value', v); |
|||
emit('change', v); |
|||
}, |
|||
after: () => { |
|||
nextTick(() => { |
|||
modalFn?.redoModalHeight?.(); |
|||
insEditor.setValue(valueRef.value); |
|||
vditorRef.value = insEditor; |
|||
initedRef.value = true; |
|||
emit('get', instance); |
|||
}); |
|||
}, |
|||
blur: () => { |
|||
//unref(vditorRef)?.setValue(props.value); |
|||
}, |
|||
...bindValue, |
|||
cache: { |
|||
enable: false, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
const instance = { |
|||
getVditor: (): Vditor => vditorRef.value!, |
|||
}; |
|||
|
|||
function destroy() { |
|||
const vditorInstance = unref(vditorRef); |
|||
if (!vditorInstance) return; |
|||
try { |
|||
vditorInstance?.destroy?.(); |
|||
} catch (error) {} |
|||
vditorRef.value = null; |
|||
initedRef.value = false; |
|||
} |
|||
|
|||
onMountedOrActivated(init); |
|||
|
|||
onBeforeUnmount(destroy); |
|||
onDeactivated(destroy); |
|||
return { |
|||
wrapRef, |
|||
...instance, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,167 +0,0 @@ |
|||
<template> |
|||
<Menu |
|||
:selectedKeys="selectedKeys" |
|||
:defaultSelectedKeys="defaultSelectedKeys" |
|||
:mode="mode" |
|||
:openKeys="getOpenKeys" |
|||
:inlineIndent="inlineIndent" |
|||
:theme="theme" |
|||
@openChange="handleOpenChange" |
|||
:class="getMenuClass" |
|||
@click="handleMenuClick" |
|||
:subMenuOpenDelay="0.2" |
|||
v-bind="getInlineCollapseOptions" |
|||
> |
|||
<template v-for="item in items" :key="item.path"> |
|||
<BasicSubMenuItem :item="item" :theme="theme" :isHorizontal="isHorizontal" /> |
|||
</template> |
|||
</Menu> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { MenuState } from './types'; |
|||
import { computed, defineComponent, unref, reactive, watch, toRefs, ref } from 'vue'; |
|||
import { Menu } from 'ant-design-vue'; |
|||
import BasicSubMenuItem from './components/BasicSubMenuItem.vue'; |
|||
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; |
|||
import { useOpenKeys } from './useOpenKeys'; |
|||
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
import { basicProps } from './props'; |
|||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; |
|||
import { REDIRECT_NAME } from '/@/router/constant'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { getCurrentParentPath } from '/@/router/menus'; |
|||
import { listenerRouteChange } from '/@/logics/mitt/routeChange'; |
|||
import { getAllParentPath } from '/@/router/helper/menuHelper'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicMenu', |
|||
components: { |
|||
Menu, |
|||
BasicSubMenuItem, |
|||
}, |
|||
props: basicProps, |
|||
emits: ['menuClick'], |
|||
setup(props, { emit }) { |
|||
const isClickGo = ref(false); |
|||
const currentActiveMenu = ref(''); |
|||
|
|||
const menuState = reactive<MenuState>({ |
|||
defaultSelectedKeys: [], |
|||
openKeys: [], |
|||
selectedKeys: [], |
|||
collapsedOpenKeys: [], |
|||
}); |
|||
|
|||
const { prefixCls } = useDesign('basic-menu'); |
|||
const { items, mode, accordion } = toRefs(props); |
|||
|
|||
const { getCollapsed, getIsHorizontal, getTopMenuAlign, getSplit } = useMenuSetting(); |
|||
|
|||
const { currentRoute } = useRouter(); |
|||
|
|||
const { handleOpenChange, setOpenKeys, getOpenKeys } = useOpenKeys( |
|||
menuState, |
|||
items, |
|||
mode, |
|||
accordion |
|||
); |
|||
|
|||
const getIsTopMenu = computed(() => { |
|||
const { type, mode } = props; |
|||
|
|||
return ( |
|||
(type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL) || |
|||
(props.isHorizontal && unref(getSplit)) |
|||
); |
|||
}); |
|||
|
|||
const getMenuClass = computed(() => { |
|||
const align = props.isHorizontal && unref(getSplit) ? 'start' : unref(getTopMenuAlign); |
|||
return [ |
|||
prefixCls, |
|||
`justify-${align}`, |
|||
{ |
|||
[`${prefixCls}__second`]: !props.isHorizontal && unref(getSplit), |
|||
[`${prefixCls}__sidebar-hor`]: unref(getIsTopMenu), |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
const getInlineCollapseOptions = computed(() => { |
|||
const isInline = props.mode === MenuModeEnum.INLINE; |
|||
|
|||
const inlineCollapseOptions: { inlineCollapsed?: boolean } = {}; |
|||
if (isInline) { |
|||
inlineCollapseOptions.inlineCollapsed = props.mixSider ? false : unref(getCollapsed); |
|||
} |
|||
return inlineCollapseOptions; |
|||
}); |
|||
|
|||
listenerRouteChange((route) => { |
|||
if (route.name === REDIRECT_NAME) return; |
|||
handleMenuChange(route); |
|||
currentActiveMenu.value = route.meta?.currentActiveMenu as string; |
|||
|
|||
if (unref(currentActiveMenu)) { |
|||
menuState.selectedKeys = [unref(currentActiveMenu)]; |
|||
setOpenKeys(unref(currentActiveMenu)); |
|||
} |
|||
}); |
|||
|
|||
!props.mixSider && |
|||
watch( |
|||
() => props.items, |
|||
() => { |
|||
handleMenuChange(); |
|||
} |
|||
); |
|||
|
|||
async function handleMenuClick({ key }: { key: string; keyPath: string[] }) { |
|||
const { beforeClickFn } = props; |
|||
if (beforeClickFn && isFunction(beforeClickFn)) { |
|||
const flag = await beforeClickFn(key); |
|||
if (!flag) return; |
|||
} |
|||
emit('menuClick', key); |
|||
|
|||
isClickGo.value = true; |
|||
// const parentPath = await getCurrentParentPath(key); |
|||
|
|||
// menuState.openKeys = [parentPath]; |
|||
menuState.selectedKeys = [key]; |
|||
} |
|||
|
|||
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) { |
|||
if (unref(isClickGo)) { |
|||
isClickGo.value = false; |
|||
return; |
|||
} |
|||
const path = (route || unref(currentRoute)).path; |
|||
setOpenKeys(path); |
|||
if (unref(currentActiveMenu)) return; |
|||
if (props.isHorizontal && unref(getSplit)) { |
|||
const parentPath = await getCurrentParentPath(path); |
|||
menuState.selectedKeys = [parentPath]; |
|||
} else { |
|||
const parentPaths = await getAllParentPath(props.items, path); |
|||
menuState.selectedKeys = parentPaths; |
|||
} |
|||
} |
|||
|
|||
return { |
|||
prefixCls, |
|||
getIsHorizontal, |
|||
handleMenuClick, |
|||
getInlineCollapseOptions, |
|||
getMenuClass, |
|||
handleOpenChange, |
|||
getOpenKeys, |
|||
...toRefs(menuState), |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@import './index.less'; |
|||
</style> |
|||
@ -1,34 +0,0 @@ |
|||
<template> |
|||
<MenuItem :key="item.path"> |
|||
<!-- <MenuItem :class="getLevelClass"> --> |
|||
<MenuItemContent v-bind="$props" :item="item" /> |
|||
</MenuItem> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
import { Menu } from 'ant-design-vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { itemProps } from '../props'; |
|||
|
|||
import MenuItemContent from './MenuItemContent.vue'; |
|||
export default defineComponent({ |
|||
name: 'BasicMenuItem', |
|||
components: { MenuItem: Menu.Item, MenuItemContent }, |
|||
props: itemProps, |
|||
setup() // props |
|||
{ |
|||
const { prefixCls } = useDesign('basic-menu-item'); |
|||
|
|||
// const getLevelClass = computed(() => { |
|||
// const { level, theme } = props; |
|||
|
|||
// const levelCls = [`${prefixCls}__level${level}`, theme]; |
|||
// return levelCls; |
|||
// }); |
|||
return { |
|||
prefixCls, |
|||
// getLevelClass, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,55 +0,0 @@ |
|||
import type { Menu } from '/@/router/types'; |
|||
import type { PropType } from 'vue'; |
|||
|
|||
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; |
|||
import { ThemeEnum } from '/@/enums/appEnum'; |
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
export const basicProps = { |
|||
items: { |
|||
type: Array as PropType<Menu[]>, |
|||
default: () => [], |
|||
}, |
|||
collapsedShowTitle: propTypes.bool, |
|||
// 最好是4 倍数
|
|||
inlineIndent: propTypes.number.def(20), |
|||
// 菜单组件的mode属性
|
|||
mode: { |
|||
type: String as PropType<MenuModeEnum>, |
|||
default: MenuModeEnum.INLINE, |
|||
}, |
|||
|
|||
type: { |
|||
type: String as PropType<MenuTypeEnum>, |
|||
default: MenuTypeEnum.MIX, |
|||
}, |
|||
theme: propTypes.string.def(ThemeEnum.DARK), |
|||
inlineCollapsed: propTypes.bool, |
|||
mixSider: propTypes.bool, |
|||
|
|||
isHorizontal: propTypes.bool, |
|||
accordion: propTypes.bool.def(true), |
|||
beforeClickFn: { |
|||
type: Function as PropType<(key: string) => Promise<boolean>>, |
|||
}, |
|||
}; |
|||
|
|||
export const itemProps = { |
|||
item: { |
|||
type: Object as PropType<Menu>, |
|||
default: {}, |
|||
}, |
|||
level: propTypes.number, |
|||
theme: propTypes.oneOf(['dark', 'light']), |
|||
showTitle: propTypes.bool, |
|||
isHorizontal: propTypes.bool, |
|||
}; |
|||
|
|||
export const contentProps = { |
|||
item: { |
|||
type: Object as PropType<Menu>, |
|||
default: null, |
|||
}, |
|||
showTitle: propTypes.bool.def(true), |
|||
level: propTypes.number.def(0), |
|||
isHorizontal: propTypes.bool.def(true), |
|||
}; |
|||
@ -1,83 +0,0 @@ |
|||
import { MenuModeEnum } from '/@/enums/menuEnum'; |
|||
import type { Menu as MenuType } from '/@/router/types'; |
|||
import type { MenuState } from './types'; |
|||
|
|||
import { computed, Ref, toRaw } from 'vue'; |
|||
|
|||
import { unref } from 'vue'; |
|||
import { uniq } from 'lodash-es'; |
|||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; |
|||
import { getAllParentPath } from '/@/router/helper/menuHelper'; |
|||
import { useTimeoutFn } from '/@/hooks/core/useTimeout'; |
|||
|
|||
export function useOpenKeys( |
|||
menuState: MenuState, |
|||
menus: Ref<MenuType[]>, |
|||
mode: Ref<MenuModeEnum>, |
|||
accordion: Ref<boolean> |
|||
) { |
|||
const { getCollapsed, getIsMixSidebar } = useMenuSetting(); |
|||
|
|||
async function setOpenKeys(path: string) { |
|||
if (mode.value === MenuModeEnum.HORIZONTAL) { |
|||
return; |
|||
} |
|||
const native = unref(getIsMixSidebar); |
|||
useTimeoutFn( |
|||
() => { |
|||
const menuList = toRaw(menus.value); |
|||
if (menuList?.length === 0) { |
|||
menuState.openKeys = []; |
|||
return; |
|||
} |
|||
if (!unref(accordion)) { |
|||
menuState.openKeys = uniq([...menuState.openKeys, ...getAllParentPath(menuList, path)]); |
|||
} else { |
|||
menuState.openKeys = getAllParentPath(menuList, path); |
|||
} |
|||
}, |
|||
16, |
|||
!native |
|||
); |
|||
} |
|||
|
|||
const getOpenKeys = computed(() => { |
|||
const collapse = unref(getIsMixSidebar) ? false : unref(getCollapsed); |
|||
|
|||
return collapse ? menuState.collapsedOpenKeys : menuState.openKeys; |
|||
}); |
|||
|
|||
/** |
|||
* @description: 重置值 |
|||
*/ |
|||
function resetKeys() { |
|||
menuState.selectedKeys = []; |
|||
menuState.openKeys = []; |
|||
} |
|||
|
|||
function handleOpenChange(openKeys: string[]) { |
|||
if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion) || unref(getIsMixSidebar)) { |
|||
menuState.openKeys = openKeys; |
|||
} else { |
|||
// const menuList = toRaw(menus.value);
|
|||
// getAllParentPath(menuList, path);
|
|||
const rootSubMenuKeys: string[] = []; |
|||
for (const { children, path } of unref(menus)) { |
|||
if (children && children.length > 0) { |
|||
rootSubMenuKeys.push(path); |
|||
} |
|||
} |
|||
if (!unref(getCollapsed)) { |
|||
const latestOpenKey = openKeys.find((key) => menuState.openKeys.indexOf(key) === -1); |
|||
if (rootSubMenuKeys.indexOf(latestOpenKey as string) === -1) { |
|||
menuState.openKeys = openKeys; |
|||
} else { |
|||
menuState.openKeys = latestOpenKey ? [latestOpenKey] : []; |
|||
} |
|||
} else { |
|||
menuState.collapsedOpenKeys = openKeys; |
|||
} |
|||
} |
|||
} |
|||
return { setOpenKeys, resetKeys, getOpenKeys, handleOpenChange }; |
|||
} |
|||
@ -1,239 +0,0 @@ |
|||
<template> |
|||
<Modal v-bind="getBindValue" @cancel="handleCancel"> |
|||
<template #closeIcon v-if="!$slots.closeIcon"> |
|||
<ModalClose |
|||
:canFullscreen="getProps.canFullscreen" |
|||
:fullScreen="fullScreenRef" |
|||
@cancel="handleCancel" |
|||
@fullscreen="handleFullScreen" |
|||
/> |
|||
</template> |
|||
|
|||
<template #title v-if="!$slots.title"> |
|||
<ModalHeader |
|||
:helpMessage="getProps.helpMessage" |
|||
:title="getMergeProps.title" |
|||
@dblclick="handleTitleDbClick" |
|||
/> |
|||
</template> |
|||
|
|||
<template #footer v-if="!$slots.footer"> |
|||
<ModalFooter v-bind="getBindValue" @ok="handleOk" @cancel="handleCancel"> |
|||
<template #[item]="data" v-for="item in Object.keys($slots)"> |
|||
<slot :name="item" v-bind="data || {}"></slot> |
|||
</template> |
|||
</ModalFooter> |
|||
</template> |
|||
|
|||
<ModalWrapper |
|||
:useWrapper="getProps.useWrapper" |
|||
:footerOffset="wrapperFooterOffset" |
|||
:fullScreen="fullScreenRef" |
|||
ref="modalWrapperRef" |
|||
:loading="getProps.loading" |
|||
:loading-tip="getProps.loadingTip" |
|||
:minHeight="getProps.minHeight" |
|||
:height="getWrapperHeight" |
|||
:visible="visibleRef" |
|||
:modalFooterHeight="footer !== undefined && !footer ? 0 : undefined" |
|||
v-bind="omit(getProps.wrapperProps, 'visible', 'height', 'modalFooterHeight')" |
|||
@ext-height="handleExtHeight" |
|||
@height-change="handleHeightChange" |
|||
> |
|||
<slot></slot> |
|||
</ModalWrapper> |
|||
|
|||
<template #[item]="data" v-for="item in Object.keys(omit($slots, 'default'))"> |
|||
<slot :name="item" v-bind="data"></slot> |
|||
</template> |
|||
</Modal> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { ModalProps, ModalMethods } from './typing'; |
|||
|
|||
import { |
|||
defineComponent, |
|||
computed, |
|||
ref, |
|||
watch, |
|||
unref, |
|||
watchEffect, |
|||
toRef, |
|||
getCurrentInstance, |
|||
nextTick, |
|||
} from 'vue'; |
|||
import Modal from './components/Modal'; |
|||
import ModalWrapper from './components/ModalWrapper.vue'; |
|||
import ModalClose from './components/ModalClose.vue'; |
|||
import ModalFooter from './components/ModalFooter.vue'; |
|||
import ModalHeader from './components/ModalHeader.vue'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
import { deepMerge } from '/@/utils'; |
|||
import { basicProps } from './props'; |
|||
import { useFullScreen } from './hooks/useModalFullScreen'; |
|||
import { omit } from 'lodash-es'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicModal', |
|||
components: { Modal, ModalWrapper, ModalClose, ModalFooter, ModalHeader }, |
|||
inheritAttrs: false, |
|||
props: basicProps, |
|||
emits: ['visible-change', 'height-change', 'cancel', 'ok', 'register', 'update:visible'], |
|||
setup(props, { emit, attrs }) { |
|||
const visibleRef = ref(false); |
|||
const propsRef = ref<Partial<ModalProps> | null>(null); |
|||
const modalWrapperRef = ref<any>(null); |
|||
|
|||
// modal Bottom and top height |
|||
const extHeightRef = ref(0); |
|||
const modalMethods: ModalMethods = { |
|||
setModalProps, |
|||
emitVisible: undefined, |
|||
redoModalHeight: () => { |
|||
nextTick(() => { |
|||
if (unref(modalWrapperRef)) { |
|||
(unref(modalWrapperRef) as any).setModalHeight(); |
|||
} |
|||
}); |
|||
}, |
|||
}; |
|||
|
|||
const instance = getCurrentInstance(); |
|||
if (instance) { |
|||
emit('register', modalMethods, instance.uid); |
|||
} |
|||
|
|||
// Custom title component: get title |
|||
const getMergeProps = computed((): ModalProps => { |
|||
return { |
|||
...props, |
|||
...(unref(propsRef) as any), |
|||
}; |
|||
}); |
|||
|
|||
const { handleFullScreen, getWrapClassName, fullScreenRef } = useFullScreen({ |
|||
modalWrapperRef, |
|||
extHeightRef, |
|||
wrapClassName: toRef(getMergeProps.value, 'wrapClassName'), |
|||
}); |
|||
|
|||
// modal component does not need title and origin buttons |
|||
const getProps = computed((): ModalProps => { |
|||
const opt = { |
|||
...unref(getMergeProps), |
|||
visible: unref(visibleRef), |
|||
okButtonProps: undefined, |
|||
cancelButtonProps: undefined, |
|||
title: undefined, |
|||
}; |
|||
return { |
|||
...opt, |
|||
wrapClassName: unref(getWrapClassName), |
|||
}; |
|||
}); |
|||
|
|||
const getBindValue = computed((): Recordable => { |
|||
const attr = { |
|||
...attrs, |
|||
...unref(getMergeProps), |
|||
visible: unref(visibleRef), |
|||
wrapClassName: unref(getWrapClassName), |
|||
}; |
|||
if (unref(fullScreenRef)) { |
|||
return omit(attr, ['height', 'title']); |
|||
} |
|||
return omit(attr, 'title'); |
|||
}); |
|||
|
|||
const getWrapperHeight = computed(() => { |
|||
if (unref(fullScreenRef)) return undefined; |
|||
return unref(getProps).height; |
|||
}); |
|||
|
|||
watchEffect(() => { |
|||
visibleRef.value = !!props.visible; |
|||
fullScreenRef.value = !!props.defaultFullscreen; |
|||
}); |
|||
|
|||
watch( |
|||
() => unref(visibleRef), |
|||
(v) => { |
|||
emit('visible-change', v); |
|||
emit('update:visible', v); |
|||
instance && modalMethods.emitVisible?.(v, instance.uid); |
|||
nextTick(() => { |
|||
if (props.scrollTop && v && unref(modalWrapperRef)) { |
|||
(unref(modalWrapperRef) as any).scrollTop(); |
|||
} |
|||
}); |
|||
}, |
|||
{ |
|||
immediate: false, |
|||
} |
|||
); |
|||
|
|||
// 取消事件 |
|||
async function handleCancel(e: Event) { |
|||
e?.stopPropagation(); |
|||
|
|||
if (props.closeFunc && isFunction(props.closeFunc)) { |
|||
const isClose: boolean = await props.closeFunc(); |
|||
visibleRef.value = !isClose; |
|||
return; |
|||
} |
|||
|
|||
visibleRef.value = false; |
|||
emit('cancel', e); |
|||
} |
|||
|
|||
/** |
|||
* @description: 设置modal参数 |
|||
*/ |
|||
function setModalProps(props: Partial<ModalProps>): void { |
|||
// Keep the last setModalProps |
|||
propsRef.value = deepMerge(unref(propsRef) || ({} as any), props); |
|||
if (Reflect.has(props, 'visible')) { |
|||
visibleRef.value = !!props.visible; |
|||
} |
|||
if (Reflect.has(props, 'defaultFullscreen')) { |
|||
fullScreenRef.value = !!props.defaultFullscreen; |
|||
} |
|||
} |
|||
|
|||
function handleOk(e: Event) { |
|||
emit('ok', e); |
|||
} |
|||
|
|||
function handleHeightChange(height: string) { |
|||
emit('height-change', height); |
|||
} |
|||
|
|||
function handleExtHeight(height: number) { |
|||
extHeightRef.value = height; |
|||
} |
|||
|
|||
function handleTitleDbClick(e: ChangeEvent) { |
|||
if (!props.canFullscreen) return; |
|||
e.stopPropagation(); |
|||
handleFullScreen(e); |
|||
} |
|||
|
|||
return { |
|||
handleCancel, |
|||
getBindValue, |
|||
getProps, |
|||
handleFullScreen, |
|||
fullScreenRef, |
|||
getMergeProps, |
|||
handleOk, |
|||
visibleRef, |
|||
omit, |
|||
modalWrapperRef, |
|||
handleExtHeight, |
|||
handleHeightChange, |
|||
handleTitleDbClick, |
|||
getWrapperHeight, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,26 +0,0 @@ |
|||
import { Modal } from 'ant-design-vue'; |
|||
import { defineComponent, toRefs, unref } from 'vue'; |
|||
import { basicProps } from '../props'; |
|||
import { useModalDragMove } from '../hooks/useModalDrag'; |
|||
import { useAttrs } from '/@/hooks/core/useAttrs'; |
|||
import { extendSlots } from '/@/utils/helper/tsxHelper'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'Modal', |
|||
inheritAttrs: false, |
|||
props: basicProps, |
|||
setup(props, { slots }) { |
|||
const { visible, draggable, destroyOnClose } = toRefs(props); |
|||
const attrs = useAttrs(); |
|||
useModalDragMove({ |
|||
visible, |
|||
destroyOnClose, |
|||
draggable, |
|||
}); |
|||
|
|||
return () => { |
|||
const propsData = { ...unref(attrs), ...props } as Recordable; |
|||
return <Modal {...propsData}>{extendSlots(slots)}</Modal>; |
|||
}; |
|||
}, |
|||
}); |
|||
@ -1,106 +0,0 @@ |
|||
<template> |
|||
<div :class="getClass"> |
|||
<template v-if="canFullscreen"> |
|||
<Tooltip :title="t('component.modal.restore')" placement="bottom" v-if="fullScreen"> |
|||
<FullscreenExitOutlined role="full" @click="handleFullScreen" /> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.modal.maximize')" placement="bottom" v-else> |
|||
<FullscreenOutlined role="close" @click="handleFullScreen" /> |
|||
</Tooltip> |
|||
</template> |
|||
<Tooltip :title="t('component.modal.close')" placement="bottom"> |
|||
<CloseOutlined @click="handleCancel" /> |
|||
</Tooltip> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, computed } from 'vue'; |
|||
import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant-design/icons-vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { Tooltip } from 'ant-design-vue'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'ModalClose', |
|||
components: { Tooltip, FullscreenExitOutlined, FullscreenOutlined, CloseOutlined }, |
|||
props: { |
|||
canFullscreen: { type: Boolean, default: true }, |
|||
fullScreen: { type: Boolean }, |
|||
}, |
|||
emits: ['cancel', 'fullscreen'], |
|||
setup(props, { emit }) { |
|||
const { prefixCls } = useDesign('basic-modal-close'); |
|||
const { t } = useI18n(); |
|||
|
|||
const getClass = computed(() => { |
|||
return [ |
|||
prefixCls, |
|||
`${prefixCls}--custom`, |
|||
{ |
|||
[`${prefixCls}--can-full`]: props.canFullscreen, |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
function handleCancel(e: Event) { |
|||
emit('cancel', e); |
|||
} |
|||
|
|||
function handleFullScreen(e: Event) { |
|||
e?.stopPropagation(); |
|||
e?.preventDefault(); |
|||
emit('fullscreen'); |
|||
} |
|||
|
|||
return { |
|||
t, |
|||
getClass, |
|||
prefixCls, |
|||
handleCancel, |
|||
handleFullScreen, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-basic-modal-close'; |
|||
.@{prefix-cls} { |
|||
display: flex; |
|||
height: 95%; |
|||
align-items: center; |
|||
|
|||
> span { |
|||
margin-left: 48px; |
|||
font-size: 16px; |
|||
} |
|||
|
|||
&--can-full { |
|||
> span { |
|||
margin-left: 12px; |
|||
} |
|||
} |
|||
|
|||
&:not(&--can-full) { |
|||
> span:nth-child(1) { |
|||
&:hover { |
|||
font-weight: 700; |
|||
} |
|||
} |
|||
} |
|||
|
|||
& span:nth-child(1) { |
|||
display: inline-block; |
|||
padding: 10px; |
|||
|
|||
&:hover { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
|
|||
& span:nth-child(2) { |
|||
&:hover { |
|||
color: @error-color; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,21 +0,0 @@ |
|||
<template> |
|||
<BasicTitle :helpMessage="helpMessage"> |
|||
{{ title }} |
|||
</BasicTitle> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { PropType } from 'vue'; |
|||
import { defineComponent } from 'vue'; |
|||
import { BasicTitle } from '/@/components/Basic'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicModalHeader', |
|||
components: { BasicTitle }, |
|||
props: { |
|||
helpMessage: { |
|||
type: [String, Array] as PropType<string | string[]>, |
|||
}, |
|||
title: { type: String }, |
|||
}, |
|||
}); |
|||
</script> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue