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