feat: initial commit
@@ -0,0 +1,21 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.eslintcache
|
||||
report.html
|
||||
|
||||
yarn.lock
|
||||
npm-debug.log*
|
||||
.pnpm-error.log*
|
||||
.pnpm-debug.log
|
||||
tests/**/coverage/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -0,0 +1,4 @@
|
||||
# Web default environment
|
||||
VITE_PORT = 8848
|
||||
VITE_HIDE_HOME = false
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Web development environment
|
||||
VITE_PORT = 80
|
||||
VITE_PUBLIC_PATH = ./
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Web production environment
|
||||
VITE_PUBLIC_PATH = ./
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
VITE_CDN = false
|
||||
VITE_COMPRESSION = "none"
|
||||
VITE_APP_BASE_API = '/prod-api'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Web staging environment
|
||||
VITE_PUBLIC_PATH = ./
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
VITE_CDN = true
|
||||
VITE_COMPRESSION = "none"
|
||||
VITE_APP_BASE_API = '/stage-api'
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
globals: {
|
||||
// Ref sugar (take 2)
|
||||
$: "readonly",
|
||||
$$: "readonly",
|
||||
$ref: "readonly",
|
||||
$shallowRef: "readonly",
|
||||
$computed: "readonly",
|
||||
|
||||
// index.d.ts
|
||||
// global.d.ts
|
||||
Fn: "readonly",
|
||||
PromiseFn: "readonly",
|
||||
RefType: "readonly",
|
||||
LabelValueOptions: "readonly",
|
||||
EmitType: "readonly",
|
||||
TargetContext: "readonly",
|
||||
ComponentElRef: "readonly",
|
||||
ComponentRef: "readonly",
|
||||
ElRef: "readonly",
|
||||
global: "readonly",
|
||||
ForDataType: "readonly",
|
||||
ComponentRoutes: "readonly",
|
||||
|
||||
// script setup
|
||||
defineProps: "readonly",
|
||||
defineEmits: "readonly",
|
||||
defineExpose: "readonly",
|
||||
withDefaults: "readonly"
|
||||
},
|
||||
extends: [
|
||||
"../eslint.base.cjs",
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/typescript/recommended",
|
||||
"@vue/prettier",
|
||||
"@vue/eslint-config-typescript"
|
||||
],
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
jsxPragma: "React",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.ts", "*.vue"],
|
||||
rules: {
|
||||
"no-undef": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["*.vue"],
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
extraFileExtensions: [".vue"],
|
||||
ecmaVersion: "latest",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
"vue/no-v-html": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/require-explicit-emits": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off", // any
|
||||
"no-debugger": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off", // setup()
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"vue/html-self-closing": [
|
||||
"error",
|
||||
{
|
||||
html: {
|
||||
void: "always",
|
||||
normal: "always",
|
||||
component: "always"
|
||||
},
|
||||
svg: "always",
|
||||
math: "always"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
endOfLine: "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.eslintcache
|
||||
report.html
|
||||
|
||||
yarn.lock
|
||||
npm-debug.log*
|
||||
.pnpm-error.log*
|
||||
.pnpm-debug.log
|
||||
tests/**/coverage/
|
||||
|
||||
# 本机调试debug配置文件
|
||||
.vscode/launch.json
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"akamud.vscode-theme-onedark",
|
||||
"antfu.iconify",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"christian-kohler.npm-intellisense",
|
||||
"christian-kohler.path-intellisense",
|
||||
"Codeium.codeium",
|
||||
"csstools.postcss",
|
||||
"DavidAnson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"donjayamanne.githistory",
|
||||
"dsznajder.es7-react-js-snippets",
|
||||
"eamodio.gitlens",
|
||||
"ecmel.vscode-html-css",
|
||||
"esbenp.prettier-vscode",
|
||||
"genieai.chatgpt-vscode",
|
||||
"hollowtree.vue-snippets",
|
||||
"lokalise.i18n-ally",
|
||||
"mhutchie.git-graph",
|
||||
"mikestead.dotenv",
|
||||
"pmneo.tsimporter",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"stylelint.vscode-stylelint",
|
||||
"syler.sass-indented",
|
||||
"sysoev.language-stylus",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"Vue.volar",
|
||||
"xabikos.JavaScriptSnippets"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"editor.formatOnType": true,
|
||||
"editor.formatOnSave": true,
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.guides.bracketPairs": "active",
|
||||
"files.autoSave": "afterDelay",
|
||||
"git.confirmSync": false,
|
||||
"workbench.startupEditor": "newUntitledFile",
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.acceptSuggestionOnCommitCharacter": false,
|
||||
"css.lint.propertyIgnoredDueToDisplay": "ignore",
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
"comments": true,
|
||||
"strings": true
|
||||
},
|
||||
"files.associations": {
|
||||
"editor.snippetSuggestions": "top"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"iconify.excludes": ["el"],
|
||||
"cSpell.words": ["iconify", "Qrcode"]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"Vue3.0快速生成模板": {
|
||||
"scope": "vue",
|
||||
"prefix": "Vue3.0",
|
||||
"body": [
|
||||
"<template>",
|
||||
"\t<div>test</div>",
|
||||
"</template>\n",
|
||||
"<script lang='ts'>",
|
||||
"export default {",
|
||||
"\tsetup() {",
|
||||
"\t\treturn {}",
|
||||
"\t}",
|
||||
"}",
|
||||
"</script>\n",
|
||||
"<style lang='scss' scoped>\n",
|
||||
"</style>",
|
||||
"$2"
|
||||
],
|
||||
"description": "Vue3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"Vue3.2+快速生成模板": {
|
||||
"scope": "vue",
|
||||
"prefix": "Vue3.2+",
|
||||
"body": [
|
||||
"<script setup lang='ts'>",
|
||||
"</script>\n",
|
||||
"<template>",
|
||||
"\t<div>test</div>",
|
||||
"</template>\n",
|
||||
"<style lang='scss' scoped>\n",
|
||||
"</style>",
|
||||
"$2"
|
||||
],
|
||||
"description": "Vue3.2+"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Vue3.3+defineOptions快速生成模板": {
|
||||
"scope": "vue",
|
||||
"prefix": "Vue3.3+",
|
||||
"body": [
|
||||
"<script setup lang='ts'>",
|
||||
"defineOptions({",
|
||||
"\tname: ''",
|
||||
"})",
|
||||
"</script>\n",
|
||||
"<template>",
|
||||
"\t<div>test</div>",
|
||||
"</template>\n",
|
||||
"<style lang='scss' scoped>\n",
|
||||
"</style>",
|
||||
"$2"
|
||||
],
|
||||
"description": "Vue3.3+defineOptions快速生成模板"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM node:16-alpine as build-stage
|
||||
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@7.32.1 --activate
|
||||
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
COPY .npmrc package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-present, pure-admin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,36 @@
|
||||
<h1>vue-pure-admin Lite Edition(no i18n version)</h1>
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
**English** | [中文](./README.md)
|
||||
|
||||
## Introduce
|
||||
|
||||
The simplified version is based on the shelf extracted from [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin), which contains main functions and is more suitable for actual project development. The packaged size is introduced globally [element-plus](https://element-plus.org) is still below `2.3MB`, and the full version of the code will be permanently synchronized. After enabling `brotli` compression and `cdn` to replace the local library mode, the package size is less than `350kb`
|
||||
|
||||
## Supporting Video
|
||||
|
||||
- [Click Watch Tutorial](https://www.bilibili.com/video/BV1kg411v7QT)
|
||||
- [Click Watch UI Design](https://www.bilibili.com/video/BV17g411T7rq)
|
||||
|
||||
## Docs
|
||||
|
||||
- [documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
|
||||
|
||||
## Preview
|
||||
|
||||
- [Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
|
||||
|
||||
## Maintainer
|
||||
|
||||
[xiaoxian521](https://github.com/xiaoxian521)
|
||||
|
||||
## ⚠️ Attention
|
||||
|
||||
- The Lite version does not accept any issues and prs. If you have any questions, please go to the full version [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) to mention, thank you!
|
||||
|
||||
## License
|
||||
|
||||
In principle, no fees and copyrights are charged, and it is commercially available, but if you need secondary open source (such as using this platform for secondary development and open source, the front-end code must be open source and free), please contact the author for permission! (Free, just take a record)
|
||||
|
||||
[MIT © 2020-present, pure-admin](./LICENSE)
|
||||
@@ -0,0 +1,145 @@
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Release-V1.8.0-green.svg" alt="Downloads">
|
||||
<img src="https://img.shields.io/badge/JDK-1.8+-green.svg" alt="Build Status">
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Build Status">
|
||||
<img src="https://img.shields.io/badge/Spring%20Boot-2.7.1-blue.svg" alt="Downloads">
|
||||
<a target="_blank" href="https://bladex.vip">
|
||||
<img src="https://img.shields.io/badge/Author-valarchie-ff69b4.svg" alt="Downloads">
|
||||
</a>
|
||||
<a target="_blank" href="https://bladex.vip">
|
||||
<img src="https://img.shields.io/badge/Copyright%20-@Agileboot-%23ff3f59.svg" alt="Downloads">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
|
||||
<img alt="logo" height="200" src="https://oscimg.oschina.net/oscnet/up-eda2a402cc061f1f5f40d9ac4c084f4c98c.png">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">AgileBoot v2.0.0</h1>
|
||||
|
||||
<h4 align="center">基于SpringBoot+Vue3前后端分离的Java快速开发框架</h4>
|
||||
<p align="center">
|
||||
</p>
|
||||
|
||||
## ⚡ 平台简介 ⚡
|
||||
|
||||
- 本仓库是 Agilboot 快速开发脚手架的配套前端项目。前端是基于优秀的开源项目[Pure-Admin](https://github.com/pure-admin/vue-pure-admin)开发而成。在此感谢 Pure-Admin 作者。
|
||||
- 本仓库前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
|
||||
- 配套后端代码仓库地址[AgileBoot-Back-End](https://github.com/valarchie/AgileBoot-Back-End) 版本。
|
||||
|
||||
- 规范请参照该文档 [前端规范](https://gitee.com/MinJieLiu/web-standard#/MinJieLiu/web-standard)
|
||||
|
||||
### 前端配套资料
|
||||
|
||||
#### 配套视频
|
||||
|
||||
- [点我查看教程](https://www.bilibili.com/video/BV1kg411v7QT)
|
||||
- [点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
|
||||
|
||||
#### 配套保姆级文档
|
||||
|
||||
- [查看文档](https://yiming_chang.gitee.io/pure-admin-doc)
|
||||
|
||||
#### Pure-Admin 框架预览
|
||||
|
||||
- [查看预览](https://pure-admin-thin.netlify.app/#/login)
|
||||
|
||||
#### Pure-Admin 维护者
|
||||
|
||||
- [xiaoxian521](https://github.com/xiaoxian521)
|
||||
|
||||
## ✨ 使用 ✨
|
||||
|
||||
### 开发环境
|
||||
|
||||
Node.js 版本要求:16.0+
|
||||
pnpm 版本要求:6.0+
|
||||
|
||||
优先选择 node=16, pnpm=7.30.5的环境.
|
||||
|
||||
如果您还没安装 pnpm,请执行下面命令进行安装(mac 用户遇到安装报错请在命令前加上 sudo) 如果是 windows 用户,使用 power shell 管理员权限执行
|
||||
|
||||
```
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
安装依赖
|
||||
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
启动平台
|
||||
|
||||
```
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
不管是什么源,我们都可以不用管,直接执行下面命令即可
|
||||
|
||||
npm config set registry https://registry.npmmirror.com
|
||||
|
||||
上面的命令是将本地的源换成国内源 npmmirror
|
||||
(opens new window),经过几轮测试,发现它的下载速度快且同步率高,同步频率 10 分钟一次,如果您之前的源是这个 http://registry.npm.taobao.org ,那您必须换成 npmmirror 啦,因为原淘宝 npm 域名即将停止解析
|
||||
|
||||
## 🙊 系统内置功能 🙊
|
||||
|
||||
🙂 大部分功能,均有通过 **单元测试** **集成测试** 保证质量。
|
||||
|
||||
| | 功能 | 描述 |
|
||||
| --- | ---------- | ------------------------------------------------------------- |
|
||||
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
|
||||
| ⭐ | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
|
||||
| ⭐ | 岗位管理 | 配置系统用户所属担任职务 |
|
||||
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 |
|
||||
| ⭐ | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
|
||||
| | 参数管理 | 对系统动态配置常用参数 |
|
||||
| | 通知公告 | 系统通知公告信息发布维护 |
|
||||
| 🚀 | 操作日志 | 系统正常操作日志记录和查询;系统异常信息日志记录和查询 |
|
||||
| | 登录日志 | 系统登录日志记录查询包含登录异常 |
|
||||
| | 在线用户 | 当前系统中活跃用户状态监控 |
|
||||
| | 系统接口 | 根据业务代码自动生成相关的 api 接口文档 |
|
||||
| | 服务监控 | 监视当前系统 CPU、内存、磁盘、堆栈等相关信息 |
|
||||
| | 缓存监控 | 对系统的缓存信息查询,命令统计等 |
|
||||
| | 连接池监视 | 监视当前系统数据库连接池状态,可进行分析 SQL 找出系统性能瓶颈 |
|
||||
|
||||
## 💥 在线体验 💥
|
||||
|
||||
演示地址:
|
||||
|
||||
- <www.agileboot.vip>
|
||||
- <www.agileboot.cc>
|
||||
> 账号密码:admin/admin123
|
||||
|
||||
[项目文档](https://juejin.cn/column/7159946528827080734)
|
||||
|
||||
## 🎬 AgileBoot 全栈交流群 🎬
|
||||
|
||||
QQ 群: [](https://qm.qq.com/cgi-bin/qm/qr?k=TR5guoXS0HssErVWefmdFRirJvfpEvp1&jump_from=webapi&authKey=VkWMmVhp/pNdWuRD8sqgM+Sv2+Vy2qCJQSeLmeXlLtfER2RJBi6zL56PdcRlCmTs) 点击按钮入群。
|
||||
|
||||
如果觉得该项目对您有帮助,可以小额捐赠支持本项目演示网站服务器等费用~
|
||||
|
||||
<img alt="logo" height="200" src="https://oscimg.oschina.net/oscnet/up-28b63fdd7b3ce003bd30c25883f2276212b.png">
|
||||
|
||||
### 用法
|
||||
|
||||
#### 安装依赖
|
||||
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
#### 安装一个包
|
||||
|
||||
```
|
||||
pnpm add 包名
|
||||
```
|
||||
|
||||
#### 卸载一个包
|
||||
|
||||
```
|
||||
pnpm remove 包名
|
||||
```
|
||||
|
||||
### 许可证
|
||||
|
||||
原则上不收取任何费用及版权,可商用,不过如需二次开源(比如用此平台二次开发并开源,要求前端代码必须开源免费)请联系作者获取许可!(免费,走个记录而已)
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Plugin as importToCDN } from "vite-plugin-cdn-import";
|
||||
|
||||
/**
|
||||
* @description 打包时采用`cdn`模式,仅限外网使用(默认不采用,如果需要采用cdn模式,请在 .env.production 文件,将 VITE_CDN 设置成true)
|
||||
* 平台采用国内cdn:https://www.bootcdn.cn,当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com
|
||||
* 提醒:mockjs不能用cdn模式引入,会报错。正确的方式是,生产环境删除mockjs,使用真实的后端请求
|
||||
* 注意:上面提到的仅限外网使用也不是完全肯定的,如果你们公司内网部署的有相关js、css文件,也可以将下面配置对应改一下,整一套内网版cdn
|
||||
*/
|
||||
export const getCdnPlugin = () =>
|
||||
importToCDN({
|
||||
//(prodUrl解释: name: 对应下面modules的name,version: 自动读取本地package.json中dependencies依赖中对应包的版本号,path: 对应下面modules的path,当然也可写完整路径,会替换prodUrl)
|
||||
prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}",
|
||||
modules: [
|
||||
{
|
||||
name: "vue",
|
||||
var: "Vue",
|
||||
path: "vue.global.prod.min.js"
|
||||
},
|
||||
{
|
||||
name: "vue-router",
|
||||
var: "VueRouter",
|
||||
path: "vue-router.global.min.js"
|
||||
},
|
||||
// 项目中没有直接安装vue-demi,但是pinia用到了,所以需要在引入pinia前引入vue-demi(https://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77)
|
||||
{
|
||||
name: "vue-demi",
|
||||
var: "VueDemi",
|
||||
path: "index.iife.min.js"
|
||||
},
|
||||
{
|
||||
name: "pinia",
|
||||
var: "Pinia",
|
||||
path: "pinia.iife.min.js"
|
||||
},
|
||||
{
|
||||
name: "element-plus",
|
||||
var: "ElementPlus",
|
||||
path: "index.full.min.js",
|
||||
css: "index.min.css"
|
||||
},
|
||||
{
|
||||
name: "axios",
|
||||
var: "axios",
|
||||
path: "axios.min.js"
|
||||
},
|
||||
{
|
||||
name: "dayjs",
|
||||
var: "dayjs",
|
||||
path: "dayjs.min.js"
|
||||
},
|
||||
{
|
||||
name: "echarts",
|
||||
var: "echarts",
|
||||
path: "echarts.min.js"
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Plugin } from "vite";
|
||||
import { isArray } from "@pureadmin/utils";
|
||||
import compressPlugin from "vite-plugin-compression";
|
||||
|
||||
export const configCompressPlugin = (
|
||||
compress: ViteCompression
|
||||
): Plugin | Plugin[] => {
|
||||
if (compress === "none") return null;
|
||||
|
||||
const gz = {
|
||||
// 生成的压缩包后缀
|
||||
ext: ".gz",
|
||||
// 体积大于threshold才会被压缩
|
||||
threshold: 0,
|
||||
// 默认压缩.js|mjs|json|css|html后缀文件,设置成true,压缩全部文件
|
||||
filter: () => true,
|
||||
// 压缩后是否删除原始文件
|
||||
deleteOriginFile: false
|
||||
};
|
||||
const br = {
|
||||
ext: ".br",
|
||||
algorithm: "brotliCompress",
|
||||
threshold: 0,
|
||||
filter: () => true,
|
||||
deleteOriginFile: false
|
||||
};
|
||||
|
||||
const codeList = [
|
||||
{ k: "gzip", v: gz },
|
||||
{ k: "brotli", v: br },
|
||||
{ k: "both", v: [gz, br] }
|
||||
];
|
||||
|
||||
const plugins: Plugin[] = [];
|
||||
|
||||
codeList.forEach(item => {
|
||||
if (compress.includes(item.k)) {
|
||||
if (compress.includes("clear")) {
|
||||
if (isArray(item.v)) {
|
||||
item.v.forEach(vItem => {
|
||||
plugins.push(
|
||||
compressPlugin(Object.assign(vItem, { deleteOriginFile: true }))
|
||||
);
|
||||
});
|
||||
} else {
|
||||
plugins.push(
|
||||
compressPlugin(Object.assign(item.v, { deleteOriginFile: true }))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isArray(item.v)) {
|
||||
item.v.forEach(vItem => {
|
||||
plugins.push(compressPlugin(vItem));
|
||||
});
|
||||
} else {
|
||||
plugins.push(compressPlugin(item.v));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return plugins;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/** 处理环境变量 */
|
||||
const wrapperEnv = (envConfigs: Recordable): ViteEnv => {
|
||||
/** 此处为默认值 */
|
||||
const defaultEnvConfigs: ViteEnv = {
|
||||
VITE_PORT: 8848,
|
||||
VITE_PUBLIC_PATH: "",
|
||||
VITE_ROUTER_HISTORY: "",
|
||||
VITE_CDN: false,
|
||||
VITE_HIDE_HOME: "false",
|
||||
VITE_COMPRESSION: "none",
|
||||
VITE_APP_BASE_API: ""
|
||||
};
|
||||
|
||||
for (const configName of Object.keys(envConfigs)) {
|
||||
let realConfigValue = envConfigs[configName].replace(/\\n/g, "\n");
|
||||
realConfigValue =
|
||||
realConfigValue === "true"
|
||||
? true
|
||||
: realConfigValue === "false"
|
||||
? false
|
||||
: realConfigValue;
|
||||
|
||||
if (configName === "VITE_PORT") {
|
||||
realConfigValue = Number(realConfigValue);
|
||||
}
|
||||
|
||||
defaultEnvConfigs[configName] = realConfigValue;
|
||||
if (typeof realConfigValue === "string") {
|
||||
process.env[configName] = realConfigValue;
|
||||
} else if (typeof realConfigValue === "object") {
|
||||
process.env[configName] = JSON.stringify(realConfigValue);
|
||||
}
|
||||
}
|
||||
return defaultEnvConfigs;
|
||||
};
|
||||
|
||||
export { wrapperEnv };
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Plugin } from "vite";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import utils from "@pureadmin/utils";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import { green, blue, bold } from "picocolors";
|
||||
dayjs.extend(duration);
|
||||
|
||||
export function viteBuildInfo(): Plugin {
|
||||
let config: { command: string };
|
||||
let startTime: Dayjs;
|
||||
let endTime: Dayjs;
|
||||
let outDir: string;
|
||||
return {
|
||||
name: "vite:buildInfo",
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
outDir = resolvedConfig.build?.outDir ?? "dist";
|
||||
},
|
||||
buildStart() {
|
||||
console.log(
|
||||
bold(
|
||||
green(
|
||||
`👏欢迎使用${blue(
|
||||
"[Agileboot全栈项目]"
|
||||
)},如果您感觉不错,记得点击后面链接给个star哦💖 https://github.com/valarchie/agileboot-back-end`
|
||||
)
|
||||
)
|
||||
);
|
||||
if (config.command === "build") {
|
||||
startTime = dayjs(new Date());
|
||||
}
|
||||
},
|
||||
closeBundle() {
|
||||
if (config.command === "build") {
|
||||
endTime = dayjs(new Date());
|
||||
utils.getPackageSize({
|
||||
folder: outDir,
|
||||
callback: (size: string) => {
|
||||
console.log(
|
||||
bold(
|
||||
green(
|
||||
`🎉恭喜打包完成(总用时${dayjs
|
||||
.duration(endTime.diff(startTime))
|
||||
.format("mm分ss秒")},打包后的大小为${size})`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 此文件作用于 `vite.config.ts` 的 `optimizeDeps.include` 依赖预构建配置项
|
||||
* 依赖预构建,`vite` 启动时会将下面 include 里的模块,编译成 esm 格式并缓存到 node_modules/.vite 文件夹,页面加载到对应模块时如果浏览器有缓存就读取浏览器缓存,如果没有会读取本地缓存并按需加载
|
||||
* 尤其当您禁用浏览器缓存时(这种情况只应该发生在调试阶段)必须将对应模块加入到 include里,否则会遇到开发环境切换页面卡顿的问题(vite 会认为它是一个新的依赖包会重新加载并强制刷新页面),因为它既无法使用浏览器缓存,又没有在本地 node_modules/.vite 里缓存
|
||||
* 温馨提示:如果您使用的第三方库是全局引入,也就是引入到 src/main.ts 文件里,就不需要再添加到 include 里了,因为 vite 会自动将它们缓存到 node_modules/.vite
|
||||
*/
|
||||
const include = [
|
||||
"qs",
|
||||
"mitt",
|
||||
"xlsx",
|
||||
"dayjs",
|
||||
"axios",
|
||||
"pinia",
|
||||
"js-cookie",
|
||||
"sortablejs",
|
||||
"pinyin-pro",
|
||||
"@vueuse/core",
|
||||
"@pureadmin/utils",
|
||||
"responsive-storage"
|
||||
];
|
||||
|
||||
/**
|
||||
* 在预构建中强制排除的依赖项
|
||||
* 温馨提示:所有以 `@iconify-icons/` 开头引入的的本地图标模块,都应该加入到下面的 `exclude` 里,因为平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好
|
||||
*/
|
||||
const exclude = [
|
||||
"@iconify-icons/ep",
|
||||
"@iconify-icons/ri",
|
||||
"@pureadmin/theme/dist/browser-utils"
|
||||
];
|
||||
|
||||
export { include, exclude };
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getCdnPlugin } from "./cdn";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { viteBuildInfo } from "./info";
|
||||
import svgLoader from "vite-svg-loader";
|
||||
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||
import { configCompressPlugin } from "./compress";
|
||||
// import ElementPlus from "unplugin-element-plus/vite";
|
||||
import { visualizer } from "rollup-plugin-visualizer";
|
||||
import removeConsole from "vite-plugin-remove-console";
|
||||
import { themePreprocessorPlugin } from "@pureadmin/theme";
|
||||
import { genScssMultipleScopeVars } from "../src/layout/theme";
|
||||
|
||||
export function getPluginsList(
|
||||
VITE_CDN: boolean,
|
||||
VITE_COMPRESSION: ViteCompression
|
||||
) {
|
||||
const lifecycle = process.env.npm_lifecycle_event;
|
||||
return [
|
||||
vue(),
|
||||
// jsx、tsx语法支持
|
||||
vueJsx(),
|
||||
VITE_CDN ? getCdnPlugin() : null,
|
||||
configCompressPlugin(VITE_COMPRESSION),
|
||||
// 线上环境删除console
|
||||
removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
|
||||
viteBuildInfo(),
|
||||
// 自定义主题
|
||||
themePreprocessorPlugin({
|
||||
scss: {
|
||||
multipleScopeVars: genScssMultipleScopeVars(),
|
||||
extract: true
|
||||
}
|
||||
}),
|
||||
// svg组件化支持
|
||||
svgLoader(),
|
||||
// ElementPlus({}),
|
||||
// mock支持
|
||||
// 打包分析
|
||||
lifecycle === "report"
|
||||
? visualizer({ open: true, brotliSize: true, filename: "report.html" })
|
||||
: null
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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"
|
||||
/>
|
||||
<title>Agileboot管理系统</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script>
|
||||
window.process = {};
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loader,
|
||||
.loader::before,
|
||||
.loader::after {
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
border-radius: 50%;
|
||||
animation: load-animation 1.8s infinite ease-in-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin: 80px auto;
|
||||
font-size: 10px;
|
||||
color: #406eeb;
|
||||
text-indent: -9999em;
|
||||
transform: translateZ(0);
|
||||
transform: translate(-50%, 0);
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.loader::before,
|
||||
.loader::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.loader::before {
|
||||
left: -3.5em;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.loader::after {
|
||||
left: 3.5em;
|
||||
}
|
||||
|
||||
@keyframes load-animation {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"name": "@simple-template/web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
|
||||
"serve": "pnpm dev",
|
||||
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build",
|
||||
"build:staging": "rimraf dist && vite build --mode staging",
|
||||
"report": "rimraf dist && vite build",
|
||||
"preview": "vite preview",
|
||||
"preview:build": "pnpm build && vite preview",
|
||||
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
|
||||
"svgo": "svgo -f src/assets/svg -o src/assets/svg",
|
||||
"clean:cache": "rimraf node_modules && pnpm install",
|
||||
"lint:eslint": "eslint --ignore-path ../.eslintignore --cache --cache-location node_modules/.cache/eslint/.eslintcache --max-warnings 0 \"{src,build}/**/*.{vue,js,ts,tsx}\"",
|
||||
"lint:prettier": "prettier --check \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "pnpm --dir .. exec stylelint \"web/**/*.{html,vue,css,scss}\" --config ./stylelint.config.cjs --ignore-path ./.stylelintignore --cache --cache-location web/node_modules/.cache/stylelint/",
|
||||
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
|
||||
"prepare": "echo \"Git hooks are managed by ../../.githooks\"",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"not ie 11",
|
||||
"not op_mini all"
|
||||
],
|
||||
"dependencies": {
|
||||
"@pureadmin/descriptions": "^1.1.1",
|
||||
"@pureadmin/table": "^2.3.2",
|
||||
"@pureadmin/utils": "^1.9.6",
|
||||
"@vueuse/core": "^10.2.0",
|
||||
"@vueuse/motion": "^2.0.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.4.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.8",
|
||||
"echarts": "^5.4.2",
|
||||
"element-plus": "2.3.6",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"mitt": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.4",
|
||||
"pinyin-pro": "^3.15.2",
|
||||
"cropperjs": "^1.5.13",
|
||||
"vue-tippy": "^6.2.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"qs": "^6.11.2",
|
||||
"responsive-storage": "^2.2.0",
|
||||
"sortablejs": "^1.15.0",
|
||||
"typeit": "^8.7.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.2",
|
||||
"vue-types": "^5.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-icons/ep": "^1.2.12",
|
||||
"@iconify-icons/ri": "^1.2.9",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@pureadmin/theme": "^3.1.0",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/nprogress": "0.2.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/sortablejs": "^1.15.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||
"@typescript-eslint/parser": "^5.60.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"cssnano": "^6.0.1",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-import": "^15.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.1",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"sass": "^1.63.6",
|
||||
"svgo": "^3.0.2",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "5.0.4",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-cdn-import": "^0.3.5",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-remove-console": "^2.1.1",
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
"vue-eslint-parser": "^9.3.1",
|
||||
"vue-tsc": "^1.8.1"
|
||||
},
|
||||
"repository": "git@github.com:valarchie/agileboot-front-end-pure.git",
|
||||
"author": "valarchie",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-import": {},
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
|
||||
}
|
||||
};
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109z"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665z"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.114 323.114 0 0 1-107.769-242.852z"/></svg>
|
||||
|
After Width: | Height: | Size: 712 B |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"Version": "4.4.0",
|
||||
"Title": "Agileboot",
|
||||
"FixedHeader": true,
|
||||
"HiddenSideBar": false,
|
||||
"MultiTagsCache": false,
|
||||
"KeepAlive": true,
|
||||
"Layout": "vertical",
|
||||
"Theme": "default",
|
||||
"DarkMode": false,
|
||||
"Grey": false,
|
||||
"Weak": false,
|
||||
"HideTabs": false,
|
||||
"SidebarStatus": true,
|
||||
"EpThemeColor": "#409EFF",
|
||||
"ShowLogo": true,
|
||||
"ShowModel": "smart",
|
||||
"MenuArrowIconNoTransition": true,
|
||||
"CachingAsyncRoutes": false,
|
||||
"TooltipEffect": "light",
|
||||
"ResponsiveStorageNameSpace": "responsive-"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<el-config-provider :locale="currentLocale">
|
||||
<router-view />
|
||||
<ReDialog />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { ElConfigProvider } from "element-plus";
|
||||
import zhCn from "element-plus/lib/locale/lang/zh-cn";
|
||||
import { ReDialog } from "@/components/ReDialog";
|
||||
export default defineComponent({
|
||||
name: "app",
|
||||
components: {
|
||||
[ElConfigProvider.name]: ElConfigProvider,
|
||||
ReDialog
|
||||
},
|
||||
computed: {
|
||||
currentLocale() {
|
||||
return zhCn;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,164 @@
|
||||
import { http } from "@/utils/http";
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
|
||||
export type CaptchaDTO = {
|
||||
/** 验证码的base64图片 */
|
||||
captchaCodeImg: string;
|
||||
/** 验证码对应的缓存key */
|
||||
captchaCodeKey: string;
|
||||
};
|
||||
|
||||
export type ConfigDTO = {
|
||||
/** 验证码开关 */
|
||||
isCaptchaOn: boolean;
|
||||
/** 系统字典配置(下拉选项之类的) */
|
||||
dictionary: Record<string, Array<DictionaryData>>;
|
||||
};
|
||||
|
||||
export type LoginByPasswordDTO = {
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 密码 */
|
||||
password: string;
|
||||
/** 验证码 */
|
||||
captchaCode: string;
|
||||
/** 验证码对应的缓存key */
|
||||
captchaCodeKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 后端token实现
|
||||
*/
|
||||
export type TokenDTO = {
|
||||
/** token */
|
||||
token: string;
|
||||
/** 当前登录的用户 */
|
||||
currentUser: CurrentLoginUserDTO;
|
||||
};
|
||||
|
||||
export type CurrentLoginUserDTO = {
|
||||
userInfo: CurrentUserInfoDTO;
|
||||
roleKey: string;
|
||||
permissions: Set<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 当前User
|
||||
*/
|
||||
export interface CurrentUserInfoDTO {
|
||||
avatar?: string;
|
||||
createTime?: Date;
|
||||
creatorId?: number;
|
||||
creatorName?: string;
|
||||
deptId?: number;
|
||||
deptName?: string;
|
||||
email?: string;
|
||||
loginDate?: Date;
|
||||
loginIp?: string;
|
||||
nickName?: string;
|
||||
phoneNumber?: string;
|
||||
postId?: number;
|
||||
postName?: string;
|
||||
remark?: string;
|
||||
roleId?: number;
|
||||
roleName?: string;
|
||||
sex?: number;
|
||||
status?: number;
|
||||
updaterId?: number;
|
||||
updaterName?: string;
|
||||
updateTime?: Date;
|
||||
userId?: number;
|
||||
username?: string;
|
||||
userType?: number;
|
||||
}
|
||||
|
||||
export type DictionaryData = {
|
||||
label: string;
|
||||
value: number;
|
||||
cssTag: string;
|
||||
};
|
||||
|
||||
/** 获取系统配置接口 */
|
||||
export const getConfig = () => {
|
||||
return http.request<ResponseData<ConfigDTO>>("get", "/getConfig");
|
||||
};
|
||||
|
||||
/** 验证码接口 */
|
||||
export const getCaptchaCode = () => {
|
||||
return http.request<ResponseData<CaptchaDTO>>("get", "/captchaImage");
|
||||
};
|
||||
|
||||
/** 登录接口 */
|
||||
export const loginByPassword = (data: LoginByPasswordDTO) => {
|
||||
return http.request<ResponseData<TokenDTO>>("post", "/login", { data });
|
||||
};
|
||||
|
||||
/** 获取当前登录用户接口 */
|
||||
export const getLoginUserInfo = () => {
|
||||
return http.request<ResponseData<TokenDTO>>("get", "/getLoginUserInfo");
|
||||
};
|
||||
|
||||
export interface RouteMeta {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
showLink?: boolean;
|
||||
showParent?: boolean;
|
||||
auths?: string[];
|
||||
rank?: number;
|
||||
frameSrc?: string;
|
||||
isFrameSrcInternal?: boolean;
|
||||
}
|
||||
|
||||
export type RouteItem = RouteRecordRaw & {
|
||||
name?: string;
|
||||
path: string;
|
||||
meta: RouteMeta;
|
||||
children?: RouteItem[];
|
||||
};
|
||||
|
||||
type AsyncRoutesResponse = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: RouteItem[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 为后端返回的路由添加唯一id,后面我们在构建菜单树的层级结构时需要用到
|
||||
* 这里我们假设 name + path 是唯一的,若日后有 name + path 不唯一的情况,
|
||||
* 则需要修改此处的逻辑
|
||||
*/
|
||||
const addUniqueId = (routes: RouteItem[]): RouteItem[] => {
|
||||
return routes.map(route => {
|
||||
const id = `${route.name || ""}${route.path}`;
|
||||
|
||||
if (route.children && route.children.length > 0) {
|
||||
route.children = addUniqueId(route.children);
|
||||
}
|
||||
|
||||
return {
|
||||
...route,
|
||||
meta: {
|
||||
...route.meta,
|
||||
id
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function withId(result: AsyncRoutesResponse) {
|
||||
if (result.data) {
|
||||
result.data = addUniqueId(result.data);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动态菜单
|
||||
* TODO:对于开发环境下此处可以对路由数据做一些校验,比如说 name 是否重复,name+path 是否重复等等
|
||||
*/
|
||||
export const getAsyncRoutes = async () => {
|
||||
const result = await http.request<AsyncRoutesResponse>("get", "/getRouters");
|
||||
return withId(result);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
export interface ConfigQuery extends BasePageQuery {
|
||||
/**
|
||||
* 配置key
|
||||
*/
|
||||
configKey?: string;
|
||||
/**
|
||||
* 配置名称
|
||||
*/
|
||||
configName?: string;
|
||||
/**
|
||||
* 是否允许更改配置
|
||||
*/
|
||||
isAllowChange?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConfigDTO, 配置信息
|
||||
*/
|
||||
export interface ConfigDTO {
|
||||
configId?: string;
|
||||
configKey?: string;
|
||||
configName?: string;
|
||||
configOptions?: string[];
|
||||
configValue?: string;
|
||||
createTime?: Date;
|
||||
isAllowChange?: string;
|
||||
isAllowChangeStr?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConfigUpdateCommand
|
||||
*/
|
||||
export interface UpdateConfigRequest {
|
||||
configValue: string;
|
||||
}
|
||||
|
||||
/** 获取配置列表 */
|
||||
export const getConfigListApi = (params?: ConfigQuery) => {
|
||||
return http.request<ResponseData<PageDTO<ConfigDTO>>>(
|
||||
"get",
|
||||
"/system/configs",
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** 获取配置信息 */
|
||||
export const getConfigInfoApi = (configId: string) => {
|
||||
return http.request<ResponseData<ConfigDTO>>(
|
||||
"get",
|
||||
`/system/config/${configId}`
|
||||
);
|
||||
};
|
||||
|
||||
/** 刷新配置缓存 */
|
||||
export const updateConfigApi = (
|
||||
configId: number,
|
||||
data: UpdateConfigRequest
|
||||
) => {
|
||||
return http.request<ResponseData<PageDTO<ConfigDTO>>>(
|
||||
"put",
|
||||
`/system/config/${configId}`,
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** 刷新配置缓存 */
|
||||
export const refreshConfigCacheApi = () => {
|
||||
return http.request<ResponseData<void>>("delete", "/system/configs/cache");
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
export interface DeptQuery extends BaseQuery {
|
||||
// TODO 目前不需要这个参数
|
||||
deptId?: number;
|
||||
parentId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* DeptDTO
|
||||
*/
|
||||
export interface DeptDTO {
|
||||
createTime?: Date;
|
||||
id?: number;
|
||||
deptName?: string;
|
||||
email?: string;
|
||||
leaderName?: string;
|
||||
orderNum?: number;
|
||||
parentId?: number;
|
||||
phone?: string;
|
||||
status?: number;
|
||||
statusStr?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AddDeptCommand
|
||||
*/
|
||||
export interface DeptRequest {
|
||||
deptName: string;
|
||||
email?: string;
|
||||
leaderName?: string;
|
||||
orderNum: number;
|
||||
parentId: number;
|
||||
phone?: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface DeptTreeDTO {
|
||||
id: number;
|
||||
parentId: number;
|
||||
label: string;
|
||||
children: [DeptTreeDTO];
|
||||
}
|
||||
|
||||
/** 获取部门列表 */
|
||||
export const getDeptListApi = (params?: DeptQuery) => {
|
||||
return http.request<ResponseData<Array<DeptDTO>>>("get", "/system/depts", {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
/** 新增部门 */
|
||||
export const addDeptApi = (data: DeptRequest) => {
|
||||
console.log(data);
|
||||
return http.request<ResponseData<void>>("post", "/system/dept", {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
/** 部门详情 */
|
||||
export const getDeptInfoApi = (deptId: string) => {
|
||||
return http.request<ResponseData<DeptDTO>>("get", `/system/dept/${deptId}`);
|
||||
};
|
||||
|
||||
/** 修改部门 */
|
||||
export const updateDeptApi = (deptId: string, data: DeptRequest) => {
|
||||
return http.request<ResponseData<void>>("put", `/system/dept/${deptId}`, {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除部门 */
|
||||
export const deleteDeptApi = (deptId: string) => {
|
||||
return http.request<ResponseData<void>>("delete", `/system/dept/${deptId}`);
|
||||
};
|
||||
|
||||
/** 获取部门树级结构 */
|
||||
export const getDeptTree = () => {
|
||||
return http.request<ResponseData<DeptTreeDTO>>(
|
||||
"get",
|
||||
"/system/depts/dropdown"
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
export interface OperationLogsQuery extends BasePageQuery {
|
||||
businessType?: string;
|
||||
requestModule?: string;
|
||||
status?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface OperationLogDTO {
|
||||
businessType?: number;
|
||||
businessTypeStr?: string;
|
||||
calledMethod?: string;
|
||||
deptId?: number;
|
||||
deptName?: string;
|
||||
errorStack?: string;
|
||||
operationId?: number;
|
||||
operationParam?: string;
|
||||
operationResult?: string;
|
||||
operationTime?: Date;
|
||||
operatorIp?: string;
|
||||
operatorLocation?: string;
|
||||
operatorType?: number;
|
||||
operatorTypeStr?: string;
|
||||
requestMethod?: string;
|
||||
requestModule?: string;
|
||||
requestUrl?: string;
|
||||
status?: number;
|
||||
statusStr?: string;
|
||||
userId?: number;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 获取操作日志列表 */
|
||||
export const getOperationLogListApi = (params?: OperationLogsQuery) => {
|
||||
return http.request<ResponseData<PageDTO<OperationLogDTO>>>(
|
||||
"get",
|
||||
"/logs/operationLogs",
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const exportOperationLogExcelApi = (
|
||||
params: OperationLogsQuery,
|
||||
fileName: string
|
||||
) => {
|
||||
return http.download("/logs/operationLogs/excel", fileName, {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteOperationLogApi = (data: Array<number>) => {
|
||||
return http.request<ResponseData<void>>("delete", "/logs/operationLogs", {
|
||||
params: {
|
||||
// 需要将数组转换为字符串 否则Axios会将参数变成 noticeIds[0]:1 noticeIds[1]:2 这种格式,后端接收参数不成功
|
||||
operationIds: data.toString()
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 登录日志查询类 */
|
||||
export interface LoginLogQuery extends BasePageQuery {
|
||||
beginTime?: string;
|
||||
endTime?: string;
|
||||
ipAddress?: string;
|
||||
status?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录日志信息
|
||||
*/
|
||||
export interface LoginLogsDTO {
|
||||
browser?: string;
|
||||
infoId?: string;
|
||||
ipAddress?: string;
|
||||
loginLocation?: string;
|
||||
loginTime?: Date;
|
||||
msg?: string;
|
||||
operationSystem?: string;
|
||||
/** TODO 这个登录状态的设计很奇怪 需要重构掉 */
|
||||
status?: number;
|
||||
statusStr?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 获取操作日志列表 */
|
||||
export const getLoginLogListApi = (params?: LoginLogQuery) => {
|
||||
return http.request<ResponseData<PageDTO<LoginLogsDTO>>>(
|
||||
"get",
|
||||
"/logs/loginLogs",
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const exportLoginLogExcelApi = (
|
||||
params: LoginLogQuery,
|
||||
fileName: string
|
||||
) => {
|
||||
return http.download("/logs/loginLogs/excel", fileName, {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteLoginLogApi = (data: Array<number>) => {
|
||||
return http.request<ResponseData<void>>("delete", "/logs/loginLogs", {
|
||||
params: {
|
||||
// 需要将数组转换为字符串 否则Axios会将参数变成 noticeIds[0]:1 noticeIds[1]:2 这种格式,后端接收参数不成功
|
||||
ids: data.toString()
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import { http } from "@/utils/http";
|
||||
import { Tree } from "@/utils/tree";
|
||||
|
||||
export interface MenuQuery {
|
||||
isButton: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* MenuDTO
|
||||
*/
|
||||
export interface MenuDTO extends Tree {
|
||||
createTime?: Date;
|
||||
isButton?: number;
|
||||
id?: number;
|
||||
menuName?: string;
|
||||
parentId?: number;
|
||||
menuType: number;
|
||||
menuTypeStr: string;
|
||||
path?: string;
|
||||
permission?: string;
|
||||
routerName?: string;
|
||||
status?: number;
|
||||
statusStr?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* MenuDetailDTO
|
||||
*/
|
||||
export interface MenuDetailDTO extends MenuDTO {
|
||||
meta: MetaDTO;
|
||||
permission?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AddMenuCommand
|
||||
*/
|
||||
export interface MenuRequest {
|
||||
id: number;
|
||||
parentId: number;
|
||||
menuName: string;
|
||||
routerName?: string;
|
||||
path?: string;
|
||||
permission?: string;
|
||||
status: number;
|
||||
isButton: boolean;
|
||||
menuType: number;
|
||||
meta: MetaDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* MetaDTO
|
||||
*/
|
||||
export interface MetaDTO {
|
||||
auths?: string[];
|
||||
dynamicLevel?: number;
|
||||
extraIcon?: ExtraIconDTO;
|
||||
frameLoading?: boolean;
|
||||
frameSrc?: string;
|
||||
hiddenTag?: boolean;
|
||||
icon?: string;
|
||||
isFrameSrcInternal?: boolean;
|
||||
keepAlive?: boolean;
|
||||
rank?: number;
|
||||
roles?: string[];
|
||||
showLink?: boolean;
|
||||
showParent?: boolean;
|
||||
title?: string;
|
||||
transition?: TransitionDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExtraIconDTO
|
||||
*/
|
||||
export interface ExtraIconDTO {
|
||||
name?: string;
|
||||
svg?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* TransitionDTO
|
||||
*/
|
||||
export interface TransitionDTO {
|
||||
enterTransition?: string;
|
||||
leaveTransition?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/** 获取菜单列表 */
|
||||
export const getMenuListApi = (params: MenuQuery) => {
|
||||
return http.request<ResponseData<Array<MenuDTO>>>("get", "/system/menus", {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
/** 添加菜单 */
|
||||
export const addMenuApi = (data: MenuRequest) => {
|
||||
return http.request<ResponseData<void>>("post", "/system/menus", { data });
|
||||
};
|
||||
|
||||
/** 修改菜单 */
|
||||
export const updateMenuApi = (menuId: string, data: MenuRequest) => {
|
||||
return http.request<ResponseData<void>>("put", `/system/menus/${menuId}`, {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除菜单 */
|
||||
export const deleteMenuApi = (menuId: string) => {
|
||||
return http.request<ResponseData<void>>("delete", `/system/menus/${menuId}`);
|
||||
};
|
||||
|
||||
/** 菜单详情 */
|
||||
export const getMenuInfoApi = (menuId: string) => {
|
||||
return http.request<ResponseData<MenuDetailDTO>>(
|
||||
"get",
|
||||
`/system/menus/${menuId}`
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
export interface OnlineUserQuery {
|
||||
ipAddress: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface OnlineUserInfo {
|
||||
browser?: string;
|
||||
deptName?: string;
|
||||
ipAddress?: string;
|
||||
loginLocation?: string;
|
||||
loginTime?: number;
|
||||
operationSystem?: string;
|
||||
tokenId?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 获取操作日志列表 */
|
||||
export const getOnlineUserListApi = (params?: OnlineUserQuery) => {
|
||||
return http.request<ResponseData<PageDTO<OnlineUserInfo>>>(
|
||||
"get",
|
||||
"/monitor/onlineUsers",
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** 强制登出用户 */
|
||||
export const logoutOnlineUserApi = (tokenId: string) => {
|
||||
return http.request<ResponseData<void>>(
|
||||
"delete",
|
||||
`/monitor/onlineUser/${tokenId}`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* ServerInfo
|
||||
*/
|
||||
export interface ServerInfo {
|
||||
cpuInfo?: CpuInfo;
|
||||
diskInfos?: DiskInfo[];
|
||||
jvmInfo?: JvmInfo;
|
||||
memoryInfo?: MemoryInfo;
|
||||
systemInfo?: SystemInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* CpuInfo
|
||||
*/
|
||||
export interface CpuInfo {
|
||||
cpuNum?: number;
|
||||
free?: number;
|
||||
sys?: number;
|
||||
total?: number;
|
||||
used?: number;
|
||||
wait?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* DiskInfo
|
||||
*/
|
||||
export interface DiskInfo {
|
||||
dirName?: string;
|
||||
free?: string;
|
||||
sysTypeName?: string;
|
||||
total?: string;
|
||||
typeName?: string;
|
||||
usage?: number;
|
||||
used?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* JvmInfo
|
||||
*/
|
||||
export interface JvmInfo {
|
||||
free?: number;
|
||||
home?: string;
|
||||
inputArgs?: string;
|
||||
max?: number;
|
||||
name?: string;
|
||||
runTime?: string;
|
||||
startTime?: string;
|
||||
total?: number;
|
||||
usage?: number;
|
||||
used?: number;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* MemoryInfo
|
||||
*/
|
||||
export interface MemoryInfo {
|
||||
free?: number;
|
||||
total?: number;
|
||||
usage?: number;
|
||||
used?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemInfo
|
||||
*/
|
||||
export interface SystemInfo {
|
||||
computerIp?: string;
|
||||
computerName?: string;
|
||||
osArch?: string;
|
||||
osName?: string;
|
||||
userDir?: string;
|
||||
}
|
||||
|
||||
/** 获取服务器信息 */
|
||||
export const getServerInfoApi = () => {
|
||||
return http.request<ResponseData<ServerInfo>>("get", "/monitor/serverInfo");
|
||||
};
|
||||
|
||||
/**
|
||||
* RedisCacheInfoDTO
|
||||
*/
|
||||
export interface RedisCacheInfoDTO {
|
||||
commandStats?: CommandStatusDTO[];
|
||||
dbSize?: number;
|
||||
info?: { [key: string]: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* CommandStatusDTO
|
||||
*/
|
||||
export interface CommandStatusDTO {
|
||||
name?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
/** 获取Redis信息 */
|
||||
export const getCacheInfoApi = () => {
|
||||
return http.request<ResponseData<ServerInfo>>("get", "/monitor/cacheInfo");
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
export interface SystemNoticeQuery extends BasePageQuery {
|
||||
noticeType: string;
|
||||
noticeTitle: string;
|
||||
creatorName: string;
|
||||
}
|
||||
|
||||
type SystemNoticeDTO = {
|
||||
noticeId: string;
|
||||
noticeTitle: string;
|
||||
noticeType: number;
|
||||
noticeContent: string;
|
||||
status: number;
|
||||
createTime: Date;
|
||||
creatorName: string;
|
||||
};
|
||||
|
||||
export type SystemNoticeRequest = {
|
||||
noticeId?: number;
|
||||
noticeTitle: string;
|
||||
noticeType: number;
|
||||
noticeContent: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
/** 获取系统通知列表 */
|
||||
export const getSystemNoticeListApi = (params?: SystemNoticeQuery) => {
|
||||
return http.request<ResponseData<PageDTO<SystemNoticeDTO>>>(
|
||||
"get",
|
||||
"/system/notices",
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** 添加系统通知 */
|
||||
export const addSystemNoticeApi = (data: SystemNoticeRequest) => {
|
||||
return http.request<ResponseData<void>>("post", "/system/notices", {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
/** 修改系统通知 */
|
||||
export const updateSystemNoticeApi = (data: SystemNoticeRequest) => {
|
||||
return http.request<ResponseData<void>>(
|
||||
"put",
|
||||
`/system/notices/${data.noticeId}`,
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** 删除系统通知 */
|
||||
export const deleteSystemNoticeApi = (data: Array<number>) => {
|
||||
return http.request<ResponseData<void>>("delete", "/system/notices", {
|
||||
params: {
|
||||
// 需要将数组转换为字符串 否则Axios会将参数变成 noticeIds[0]:1 noticeIds[1]:2 这种格式,后端接收参数不成功
|
||||
noticeIds: data.toString()
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
export interface PostListCommand extends BasePageQuery {
|
||||
postCode?: string;
|
||||
postName?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface PostPageResponse {
|
||||
createTime: string;
|
||||
postCode: string;
|
||||
postId: number;
|
||||
postName: string;
|
||||
postSort: number;
|
||||
remark: string;
|
||||
status: number;
|
||||
statusStr: string;
|
||||
}
|
||||
|
||||
export function getPostListApi(params: PostListCommand) {
|
||||
return http.request<ResponseData<PageDTO<PostPageResponse>>>(
|
||||
"get",
|
||||
"/system/post/list",
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const exportPostExcelApi = (
|
||||
params: PostListCommand,
|
||||
fileName: string
|
||||
) => {
|
||||
return http.download("/system/post/excel", fileName, {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
export const deletePostApi = (data: Array<number>) => {
|
||||
return http.request<ResponseData<void>>("delete", "/system/post", {
|
||||
params: {
|
||||
// 需要将数组转换为字符串 否则Axios会将参数变成 noticeIds[0]:1 noticeIds[1]:2 这种格式,后端接收参数不成功
|
||||
ids: data.toString()
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export interface AddPostCommand {
|
||||
postCode: string;
|
||||
postName: string;
|
||||
postSort: number;
|
||||
remark?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export const addPostApi = (data: AddPostCommand) => {
|
||||
return http.request<ResponseData<void>>("post", "/system/post", {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
export interface UpdatePostCommand extends AddPostCommand {
|
||||
postId: number;
|
||||
}
|
||||
|
||||
export const updatePostApi = (data: UpdatePostCommand) => {
|
||||
return http.request<ResponseData<void>>("put", "/system/post", {
|
||||
data
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
export interface RoleQuery extends BasePageQuery {
|
||||
roleKey?: string;
|
||||
roleName?: string;
|
||||
status?: string;
|
||||
timeRangeColumn?: string;
|
||||
}
|
||||
|
||||
export interface RoleDTO {
|
||||
createTime: Date;
|
||||
dataScope: number;
|
||||
remark: string;
|
||||
roleId: number;
|
||||
roleKey: string;
|
||||
roleName: string;
|
||||
roleSort: number;
|
||||
selectedDeptList: number[];
|
||||
selectedMenuList: number[];
|
||||
status: number;
|
||||
}
|
||||
|
||||
export function getRoleListApi(params: RoleQuery) {
|
||||
return http.request<ResponseData<PageDTO<RoleDTO>>>(
|
||||
"get",
|
||||
"/system/role/list",
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getRoleInfoApi(roleId: number) {
|
||||
return http.request<ResponseData<RoleDTO>>("get", "/system/role/" + roleId);
|
||||
}
|
||||
|
||||
export interface AddRoleCommand {
|
||||
dataScope?: string;
|
||||
menuIds: number[];
|
||||
remark?: string;
|
||||
roleKey: string;
|
||||
roleName: string;
|
||||
roleSort: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export function addRoleApi(data: AddRoleCommand) {
|
||||
return http.request<void>("post", "/system/role", {
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export interface UpdateRoleCommand extends AddRoleCommand {
|
||||
roleId: number;
|
||||
}
|
||||
|
||||
export function updateRoleApi(data: UpdateRoleCommand) {
|
||||
return http.request<void>("put", "/system/role", {
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteRoleApi(roleId: number) {
|
||||
return http.request<void>("delete", "/system/role/" + roleId);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
export interface UserQuery extends BasePageQuery {
|
||||
deptId?: number;
|
||||
phoneNumber?: string;
|
||||
status?: number;
|
||||
userId?: number;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserDTO
|
||||
*/
|
||||
export interface UserDTO {
|
||||
avatar?: string;
|
||||
createTime?: Date;
|
||||
creatorId?: number;
|
||||
creatorName?: string;
|
||||
deptId?: number;
|
||||
deptName?: string;
|
||||
email?: string;
|
||||
loginDate?: Date;
|
||||
loginIp?: string;
|
||||
nickname?: string;
|
||||
phoneNumber?: string;
|
||||
postId?: number;
|
||||
remark?: string;
|
||||
roleId?: number;
|
||||
roleName?: string;
|
||||
sex?: number;
|
||||
status?: number;
|
||||
updaterId?: number;
|
||||
updaterName?: string;
|
||||
updateTime?: Date;
|
||||
userId?: number;
|
||||
username?: string;
|
||||
userType?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* AddUserCommand
|
||||
*/
|
||||
export interface UserRequest {
|
||||
userId: number;
|
||||
avatar?: string;
|
||||
deptId?: number;
|
||||
email?: string;
|
||||
nickname?: string;
|
||||
phoneNumber?: string;
|
||||
password: string;
|
||||
postId?: number;
|
||||
remark?: string;
|
||||
roleId?: number;
|
||||
sex?: number;
|
||||
status?: number;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateProfileCommand
|
||||
*/
|
||||
export interface UserProfileRequest {
|
||||
email?: string;
|
||||
nickName?: string;
|
||||
phoneNumber?: string;
|
||||
sex?: number;
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetPasswordCommand
|
||||
*/
|
||||
export interface ResetPasswordRequest {
|
||||
newPassword?: string;
|
||||
oldPassword?: string;
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
export interface PasswordRequest {
|
||||
userId: number;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/** 获取用户列表 */
|
||||
export const getUserListApi = (params?: UserQuery) => {
|
||||
return http.request<ResponseData<PageDTO<UserDTO>>>("get", "/system/users", {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
/** 新增用户 */
|
||||
export const addUserApi = (data?: UserRequest) => {
|
||||
return http.request<ResponseData<void>>("post", "/system/users", {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
/** 编辑用户 */
|
||||
export const updateUserApi = (userId: number, data?: UserRequest) => {
|
||||
return http.request<ResponseData<void>>("put", `/system/users/${userId}`, {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
/** 更改用户密码 */
|
||||
export const updateUserPasswordApi = (data?: PasswordRequest) => {
|
||||
return http.request<ResponseData<void>>(
|
||||
"put",
|
||||
`/system/users/${data.userId}/password`,
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** 删除用户 */
|
||||
export const deleteUserApi = (userId: number) => {
|
||||
return http.request<ResponseData<void>>("delete", `/system/users/${userId}`);
|
||||
};
|
||||
|
||||
/** 修改用户状态 */
|
||||
export const updateUserStatusApi = (userId: number, status: number) => {
|
||||
return http.request<ResponseData<PageDTO<UserDTO>>>(
|
||||
"put",
|
||||
`/system/users/${userId}/status`,
|
||||
{
|
||||
data: {
|
||||
status: status
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** 批量导出用户 */
|
||||
export const exportUserExcelApi = (params: UserQuery, fileName: string) => {
|
||||
return http.download("/system/users/excel", fileName, {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
/** 用户头像上传 */
|
||||
export const uploadUserAvatarApi = data => {
|
||||
return http.request<ResponseData<void>>(
|
||||
"post",
|
||||
"/system/user/profile/avatar",
|
||||
{
|
||||
data
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** 更改用户资料 */
|
||||
export const updateUserProfileApi = (data?: UserProfileRequest) => {
|
||||
return http.request<ResponseData<void>>("put", "/system/user/profile", {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
/** 更改当前用户密码 */
|
||||
export const updateCurrentUserPasswordApi = (data?: ResetPasswordRequest) => {
|
||||
return http.request<ResponseData<void>>(
|
||||
"put",
|
||||
"/system/user/profile/password",
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 2208059 */
|
||||
src: url("iconfont.woff2?t=1671895108120") format("woff2"),
|
||||
url("iconfont.woff?t=1671895108120") format("woff"),
|
||||
url("iconfont.ttf?t=1671895108120") format("truetype");
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.pure-iconfont-tabs:before {
|
||||
content: "\e63e";
|
||||
}
|
||||
|
||||
.pure-iconfont-logo:before {
|
||||
content: "\e620";
|
||||
}
|
||||
|
||||
.pure-iconfont-new:before {
|
||||
content: "\e615";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "2208059",
|
||||
"name": "pure-admin",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "pure-iconfont-",
|
||||
"description": "pure-admin-iconfont",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "20594647",
|
||||
"name": "Tabs",
|
||||
"font_class": "tabs",
|
||||
"unicode": "e63e",
|
||||
"unicode_decimal": 58942
|
||||
},
|
||||
{
|
||||
"icon_id": "22129506",
|
||||
"name": "PureLogo",
|
||||
"font_class": "logo",
|
||||
"unicode": "e620",
|
||||
"unicode_decimal": 58912
|
||||
},
|
||||
{
|
||||
"icon_id": "7795615",
|
||||
"name": "New",
|
||||
"font_class": "new",
|
||||
"unicode": "e615",
|
||||
"unicode_decimal": 58901
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109z"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665z"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.114 323.114 0 0 1-107.769-242.852z"/></svg>
|
||||
|
After Width: | Height: | Size: 712 B |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M3 5a2 2 0 0 1 2-2h2a.5.5 0 0 1 0 1H5a1 1 0 0 0-1 1v2a.5.5 0 0 1-1 0V5zm9.5-1.5A.5.5 0 0 1 13 3h2a2 2 0 0 1 2 2v2a.5.5 0 0 1-1 0V5a1 1 0 0 0-1-1h-2a.5.5 0 0 1-.5-.5zm-9 9a.5.5 0 0 1 .5.5v2a1 1 0 0 0 1 1h2a.5.5 0 0 1 0 1H5a2 2 0 0 1-2-2v-2a.5.5 0 0 1 .5-.5zm13 0a.5.5 0 0 1 .5.5v2a2 2 0 0 1-2 2h-2a.5.5 0 0 1 0-1h2a1 1 0 0 0 1-1v-2a.5.5 0 0 1 .5-.5z" fill="currentColor"/></g></svg>
|
||||
|
After Width: | Height: | Size: 508 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24"><g fill="none"><path d="M8.5 3.75a.75.75 0 0 0-1.5 0v2.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 0 0 1.5h2.5A2.25 2.25 0 0 0 8.5 6.25v-2.5zm0 16.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 0-.75-.75h-2.5a.75.75 0 0 1 0-1.5h2.5a2.25 2.25 0 0 1 2.25 2.25v2.5zM16.25 3a.75.75 0 0 0-.75.75v2.5a2.25 2.25 0 0 0 2.25 2.25h2.5a.75.75 0 0 0 0-1.5h-2.5a.75.75 0 0 1-.75-.75v-2.5a.75.75 0 0 0-.75-.75zm-.75 17.25a.75.75 0 0 0 1.5 0v-2.5a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 0 0-1.5h-2.5a2.25 2.25 0 0 0-2.25 2.25v2.5z" fill="currentColor"/></g></svg>
|
||||
|
After Width: | Height: | Size: 631 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.897 35.897 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0c.166.18.304.332.413.455a35.897 35.897 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44.019 44.019 0 0 1-6.584-.874zm6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42.137 42.137 0 0 0 4.227-.454A33.913 33.913 0 0 0 12 4.09a33.913 33.913 0 0 0-6.649 12.387c1.395.222 2.805.374 4.227.454zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></svg>
|
||||
|
After Width: | Height: | Size: 588 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981z"/></svg>
|
||||
|
After Width: | Height: | Size: 263 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></svg>
|
||||
|
After Width: | Height: | Size: 480 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--ant-design" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8z"/></svg>
|
||||
|
After Width: | Height: | Size: 352 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5l-.5.5zM13 3V1h-1v2.5l.5.5H15V3h-2zm-1 9.5V15h1v-2h2v-1h-2.5l-.5.5zM1 12v1h2v2h1v-2.5l-.5-.5H1zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5v5zM10 7H6v2h4V7z"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3v8zm2-6h6v4H5V6zM2 6H1V2.5l.5-.5H5v1H2v3zm13-3.5V6h-1V3h-3V2h3.5l.5.5zM14 10h1v3.5l-.5.5H11v-1h3v-3zM2 13h3v1H1.5l-.5-.5V10h1v3z"/></svg>
|
||||
|
After Width: | Height: | Size: 318 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1V7m10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 381 B |
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,5 @@
|
||||
import auth from "./src/auth";
|
||||
|
||||
const Auth = auth;
|
||||
|
||||
export { Auth };
|
||||
@@ -0,0 +1,20 @@
|
||||
import { defineComponent, Fragment } from "vue";
|
||||
import { hasAuth } from "@/router/utils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Auth",
|
||||
props: {
|
||||
value: {
|
||||
type: undefined,
|
||||
default: []
|
||||
}
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => {
|
||||
if (!slots) return null;
|
||||
return hasAuth(props.value) ? (
|
||||
<Fragment>{slots.default?.()}</Fragment>
|
||||
) : null;
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ElCol } from "element-plus";
|
||||
import { h, defineComponent } from "vue";
|
||||
|
||||
// 封装element-plus的el-col组件
|
||||
export default defineComponent({
|
||||
name: "ReCol",
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: 24
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const attrs = this.$attrs;
|
||||
const val = this.value;
|
||||
return h(
|
||||
ElCol,
|
||||
{
|
||||
xs: val,
|
||||
sm: val,
|
||||
md: val,
|
||||
lg: val,
|
||||
xl: val,
|
||||
...attrs
|
||||
},
|
||||
{ default: () => this.$slots.default() }
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import reCropper from "./src";
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
|
||||
/** 图片裁剪组件 */
|
||||
export const ReCropper = withInstall(reCropper);
|
||||
|
||||
export default ReCropper;
|
||||
@@ -0,0 +1,11 @@
|
||||
@import "cropperjs/dist/cropper.css";
|
||||
@import "tippy.js/dist/tippy.css";
|
||||
@import "tippy.js/themes/light.css";
|
||||
@import "tippy.js/animations/perspective.css";
|
||||
|
||||
.re-circled {
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
import "./circled.css";
|
||||
import Cropper from "cropperjs";
|
||||
import { ElUpload } from "element-plus";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { useResizeObserver } from "@vueuse/core";
|
||||
import { longpress } from "@/directives/longpress";
|
||||
import { useTippy, directive as tippy } from "vue-tippy";
|
||||
import { delay, debounce, isArray, downloadByBase64 } from "@pureadmin/utils";
|
||||
import {
|
||||
ref,
|
||||
unref,
|
||||
computed,
|
||||
PropType,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
defineComponent
|
||||
} from "vue";
|
||||
import {
|
||||
Reload,
|
||||
Upload,
|
||||
ArrowH,
|
||||
ArrowV,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ChangeIcon,
|
||||
ArrowRight,
|
||||
RotateLeft,
|
||||
SearchPlus,
|
||||
RotateRight,
|
||||
SearchMinus,
|
||||
DownloadIcon
|
||||
} from "./svg";
|
||||
|
||||
type Options = Cropper.Options;
|
||||
|
||||
const defaultOptions: Options = {
|
||||
aspectRatio: 1,
|
||||
zoomable: true,
|
||||
zoomOnTouch: true,
|
||||
zoomOnWheel: true,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
toggleDragModeOnDblclick: true,
|
||||
autoCrop: true,
|
||||
background: true,
|
||||
highlight: true,
|
||||
center: true,
|
||||
responsive: true,
|
||||
restore: true,
|
||||
checkCrossOrigin: true,
|
||||
checkOrientation: true,
|
||||
scalable: true,
|
||||
modal: true,
|
||||
guides: true,
|
||||
movable: true,
|
||||
rotatable: true
|
||||
};
|
||||
|
||||
const props = {
|
||||
src: { type: String, required: true },
|
||||
alt: { type: String },
|
||||
circled: { type: Boolean, default: false },
|
||||
realTimePreview: { type: Boolean, default: true },
|
||||
height: { type: [String, Number], default: "360px" },
|
||||
crossorigin: {
|
||||
type: String as PropType<"" | "anonymous" | "use-credentials" | undefined>,
|
||||
default: undefined
|
||||
},
|
||||
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
|
||||
options: { type: Object as PropType<Options>, default: () => ({}) }
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "ReCropper",
|
||||
props,
|
||||
setup(props, { attrs, emit }) {
|
||||
const tippyElRef = ref<ElRef<HTMLImageElement>>();
|
||||
const imgElRef = ref<ElRef<HTMLImageElement>>();
|
||||
const cropper = ref<Nullable<Cropper>>();
|
||||
const isReady = ref(false);
|
||||
const imgBase64 = ref();
|
||||
const inCircled = ref(props.circled);
|
||||
const inSrc = ref(props.src);
|
||||
let scaleX = 1;
|
||||
let scaleY = 1;
|
||||
|
||||
const debounceRealTimeCroppered = debounce(realTimeCroppered, 80);
|
||||
|
||||
const getImageStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
height: props.height,
|
||||
maxWidth: "100%",
|
||||
...props.imageStyle
|
||||
};
|
||||
});
|
||||
|
||||
const getClass = computed(() => {
|
||||
return [
|
||||
attrs.class,
|
||||
{
|
||||
["re-circled"]: inCircled.value
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return [
|
||||
"p-[6px]",
|
||||
"h-[30px]",
|
||||
"w-[30px]",
|
||||
"outline-none",
|
||||
"rounded-[4px]",
|
||||
"cursor-pointer",
|
||||
"hover:bg-[rgba(0,0,0,0.06)]"
|
||||
];
|
||||
});
|
||||
|
||||
const getWrapperStyle = computed((): CSSProperties => {
|
||||
return { height: `${props.height}`.replace(/px/, "") + "px" };
|
||||
});
|
||||
|
||||
onMounted(init);
|
||||
|
||||
onUnmounted(() => {
|
||||
cropper.value?.destroy();
|
||||
});
|
||||
|
||||
useResizeObserver(tippyElRef, () => {
|
||||
handCropper("reset");
|
||||
});
|
||||
|
||||
async function init() {
|
||||
const imgEl = unref(imgElRef);
|
||||
if (!imgEl) return;
|
||||
cropper.value = new Cropper(imgEl, {
|
||||
...defaultOptions,
|
||||
ready: () => {
|
||||
isReady.value = true;
|
||||
realTimeCroppered();
|
||||
delay(400).then(() => emit("readied", cropper.value));
|
||||
},
|
||||
crop() {
|
||||
debounceRealTimeCroppered();
|
||||
},
|
||||
zoom() {
|
||||
debounceRealTimeCroppered();
|
||||
},
|
||||
cropmove() {
|
||||
debounceRealTimeCroppered();
|
||||
},
|
||||
...props.options
|
||||
});
|
||||
}
|
||||
|
||||
function realTimeCroppered() {
|
||||
props.realTimePreview && croppered();
|
||||
}
|
||||
|
||||
function croppered() {
|
||||
if (!cropper.value) return;
|
||||
const canvas = inCircled.value
|
||||
? getRoundedCanvas()
|
||||
: cropper.value.getCroppedCanvas();
|
||||
// https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toBlob
|
||||
canvas.toBlob(blob => {
|
||||
if (!blob) return;
|
||||
const fileReader: FileReader = new FileReader();
|
||||
fileReader.readAsDataURL(blob);
|
||||
fileReader.onloadend = e => {
|
||||
if (!e.target?.result || !blob) return;
|
||||
imgBase64.value = e.target.result;
|
||||
emit("cropper", {
|
||||
base64: e.target.result,
|
||||
blob,
|
||||
info: { size: blob.size, ...cropper.value.getData() }
|
||||
});
|
||||
};
|
||||
fileReader.onerror = () => {
|
||||
emit("error");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getRoundedCanvas() {
|
||||
const sourceCanvas = cropper.value!.getCroppedCanvas();
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d")!;
|
||||
const width = sourceCanvas.width;
|
||||
const height = sourceCanvas.height;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.drawImage(sourceCanvas, 0, 0, width, height);
|
||||
context.globalCompositeOperation = "destination-in";
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
width / 2,
|
||||
height / 2,
|
||||
Math.min(width, height) / 2,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
true
|
||||
);
|
||||
context.fill();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function handCropper(event: string, arg?: number | Array<number>) {
|
||||
if (event === "scaleX") {
|
||||
scaleX = arg = scaleX === -1 ? 1 : -1;
|
||||
}
|
||||
if (event === "scaleY") {
|
||||
scaleY = arg = scaleY === -1 ? 1 : -1;
|
||||
}
|
||||
arg && isArray(arg)
|
||||
? cropper.value?.[event]?.(...arg)
|
||||
: cropper.value?.[event]?.(arg);
|
||||
}
|
||||
|
||||
function beforeUpload(file) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
inSrc.value = "";
|
||||
reader.onload = e => {
|
||||
inSrc.value = e.target?.result as string;
|
||||
};
|
||||
reader.onloadend = () => {
|
||||
init();
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
const menuContent = defineComponent({
|
||||
directives: {
|
||||
tippy,
|
||||
longpress
|
||||
},
|
||||
setup() {
|
||||
return () => (
|
||||
<div class="flex flex-wrap w-[60px] justify-between">
|
||||
<ElUpload
|
||||
accept="image/*"
|
||||
show-file-list={false}
|
||||
before-upload={beforeUpload}
|
||||
>
|
||||
<Upload
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "上传",
|
||||
placement: "left-start"
|
||||
}}
|
||||
/>
|
||||
</ElUpload>
|
||||
<DownloadIcon
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "下载",
|
||||
placement: "right-start"
|
||||
}}
|
||||
onClick={() => downloadByBase64(imgBase64.value, "cropping.png")}
|
||||
/>
|
||||
<ChangeIcon
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "圆形、矩形裁剪",
|
||||
placement: "left-start"
|
||||
}}
|
||||
onClick={() => {
|
||||
inCircled.value = !inCircled.value;
|
||||
realTimeCroppered();
|
||||
}}
|
||||
/>
|
||||
<Reload
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "重置",
|
||||
placement: "right-start"
|
||||
}}
|
||||
onClick={() => handCropper("reset")}
|
||||
/>
|
||||
<ArrowUp
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "上移(可长按)",
|
||||
placement: "left-start"
|
||||
}}
|
||||
v-longpress={[() => handCropper("move", [0, -10]), "0:100"]}
|
||||
/>
|
||||
<ArrowDown
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "下移(可长按)",
|
||||
placement: "right-start"
|
||||
}}
|
||||
v-longpress={[() => handCropper("move", [0, 10]), "0:100"]}
|
||||
/>
|
||||
<ArrowLeft
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "左移(可长按)",
|
||||
placement: "left-start"
|
||||
}}
|
||||
v-longpress={[() => handCropper("move", [-10, 0]), "0:100"]}
|
||||
/>
|
||||
<ArrowRight
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "右移(可长按)",
|
||||
placement: "right-start"
|
||||
}}
|
||||
v-longpress={[() => handCropper("move", [10, 0]), "0:100"]}
|
||||
/>
|
||||
<ArrowH
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "水平翻转",
|
||||
placement: "left-start"
|
||||
}}
|
||||
onClick={() => handCropper("scaleX", -1)}
|
||||
/>
|
||||
<ArrowV
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "垂直翻转",
|
||||
placement: "right-start"
|
||||
}}
|
||||
onClick={() => handCropper("scaleY", -1)}
|
||||
/>
|
||||
<RotateLeft
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "逆时针旋转",
|
||||
placement: "left-start"
|
||||
}}
|
||||
onClick={() => handCropper("rotate", -45)}
|
||||
/>
|
||||
<RotateRight
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "顺时针旋转",
|
||||
placement: "right-start"
|
||||
}}
|
||||
onClick={() => handCropper("rotate", 45)}
|
||||
/>
|
||||
<SearchPlus
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "放大(可长按)",
|
||||
placement: "left-start"
|
||||
}}
|
||||
v-longpress={[() => handCropper("zoom", 0.1), "0:100"]}
|
||||
/>
|
||||
<SearchMinus
|
||||
class={iconClass.value}
|
||||
v-tippy={{
|
||||
content: "缩小(可长按)",
|
||||
placement: "right-start"
|
||||
}}
|
||||
v-longpress={[() => handCropper("zoom", -0.1), "0:100"]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function onContextmenu(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const { show, setProps } = useTippy(tippyElRef, {
|
||||
content: menuContent,
|
||||
arrow: false,
|
||||
theme: "light",
|
||||
trigger: "manual",
|
||||
interactive: true,
|
||||
appendTo: "parent",
|
||||
// hideOnClick: false,
|
||||
animation: "perspective",
|
||||
placement: "bottom-start"
|
||||
});
|
||||
|
||||
setProps({
|
||||
getReferenceClientRect: () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: event.clientY,
|
||||
bottom: event.clientY,
|
||||
left: event.clientX,
|
||||
right: event.clientX
|
||||
})
|
||||
});
|
||||
|
||||
show();
|
||||
}
|
||||
|
||||
return {
|
||||
inSrc,
|
||||
props,
|
||||
imgElRef,
|
||||
tippyElRef,
|
||||
getClass,
|
||||
getWrapperStyle,
|
||||
getImageStyle,
|
||||
isReady,
|
||||
croppered,
|
||||
onContextmenu
|
||||
};
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
inSrc,
|
||||
isReady,
|
||||
getClass,
|
||||
getImageStyle,
|
||||
onContextmenu,
|
||||
getWrapperStyle
|
||||
} = this;
|
||||
const { alt, crossorigin } = this.props;
|
||||
|
||||
return inSrc ? (
|
||||
<div
|
||||
ref="tippyElRef"
|
||||
class={getClass}
|
||||
style={getWrapperStyle}
|
||||
onContextmenu={event => onContextmenu(event)}
|
||||
>
|
||||
<img
|
||||
v-show={isReady}
|
||||
ref="imgElRef"
|
||||
style={getImageStyle}
|
||||
src={inSrc}
|
||||
alt={alt}
|
||||
crossorigin={crossorigin}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0 0 48.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 347 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m296.992 216.992-272 272L3.008 512l21.984 23.008 272 272 46.016-46.016L126.016 544h772L680.992 760.992l46.016 46.016 272-272L1020.992 512l-21.984-23.008-272-272-46.048 46.048L898.016 480h-772l216.96-216.992z"/></svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M872 474H286.9l350.2-304c5.6-4.9 2.2-14-5.2-14h-88.5c-3.9 0-7.6 1.4-10.5 3.9L155 487.8a31.96 31.96 0 0 0 0 48.3L535.1 866c1.5 1.3 3.3 2 5.2 2h91.5c7.4 0 10.8-9.2 5.2-14L286.9 550H872c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"/></svg>
|
||||
|
After Width: | Height: | Size: 344 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M869 487.8 491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-.7 5.2-2L869 536.2a32.07 32.07 0 0 0 0-48.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 351 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M868 545.5 536.1 163a31.96 31.96 0 0 0-48.3 0L156 545.5a7.97 7.97 0 0 0 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 339 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m512 67.008-23.008 21.984-256 256 46.048 46.048L480 190.016v644L279.008 632.96l-46.048 46.08 256 256 23.008 21.984 23.008-21.984 256-256-46.016-46.016L544 834.016v-644l200.992 200.96 46.016-45.984-256-256z"/></svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M956.8 988.8H585.6c-16 0-25.6-9.6-25.6-28.8V576c0-16 9.6-28.8 25.6-28.8h371.2c16 0 25.6 9.6 25.6 28.8v384c0 16-9.6 28.8-25.6 28.8zM608 937.6h326.4V598.4H608v339.2zm-121.6 44.8C262.4 982.4 144 848 144 595.2c0-19.2 9.6-28.8 25.6-28.8s25.6 12.8 25.6 28.8c0 220.8 96 326.4 288 326.4 16 0 25.6 12.8 25.6 28.8s-6.4 32-22.4 32z"/><path d="M262.4 694.4c-6.4 0-9.6-3.2-16-6.4L160 601.6c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8-3.2 3.2-6.4 6.4-12.8 6.4z"/><path d="M86.4 694.4c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0 9.6 9.6 9.6 22.4 0 28.8L99.2 688c-3.2 3.2-6.4 6.4-12.8 6.4zm790.4-249.6c-16 0-28.8-12.8-28.8-32 0-224-99.2-336-300.8-336-16 0-28.8-12.8-28.8-32s9.6-32 28.8-32c233.6 0 355.2 137.6 355.2 396.8 0 22.4-9.6 35.2-25.6 35.2z"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4l-86.4-86.4c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8 0 3.2-6.4 6.4-12.8 6.4z"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0s9.6 22.4 0 28.8l-86.4 86.4c-3.2 3.2-6.4 6.4-12.8 6.4zM288 524.8C156.8 524.8 48 416 48 278.4S156.8 35.2 288 35.2 528 144 528 281.6 419.2 524.8 288 524.8zm-3.2-432c-99.2 0-179.2 83.2-179.2 185.6S185.6 464 284.8 464 464 380.8 464 278.4 384 92.8 284.8 92.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M505.7 661a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"/></svg>
|
||||
|
After Width: | Height: | Size: 428 B |
@@ -0,0 +1,31 @@
|
||||
import Reload from "./reload.svg?component";
|
||||
import Upload from "./upload.svg?component";
|
||||
import ArrowH from "./arrow-h.svg?component";
|
||||
import ArrowV from "./arrow-v.svg?component";
|
||||
import ArrowUp from "./arrow-up.svg?component";
|
||||
import ChangeIcon from "./change.svg?component";
|
||||
import ArrowDown from "./arrow-down.svg?component";
|
||||
import ArrowLeft from "./arrow-left.svg?component";
|
||||
import DownloadIcon from "./download.svg?component";
|
||||
import ArrowRight from "./arrow-right.svg?component";
|
||||
import RotateLeft from "./rotate-left.svg?component";
|
||||
import SearchPlus from "./search-plus.svg?component";
|
||||
import RotateRight from "./rotate-right.svg?component";
|
||||
import SearchMinus from "./search-minus.svg?component";
|
||||
|
||||
export {
|
||||
Reload,
|
||||
Upload,
|
||||
ArrowH,
|
||||
ArrowV,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ChangeIcon,
|
||||
ArrowRight,
|
||||
RotateLeft,
|
||||
SearchPlus,
|
||||
RotateRight,
|
||||
SearchMinus,
|
||||
DownloadIcon
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 0 1 755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 0 0 3 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8zm756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 0 1 512.1 856a342.24 342.24 0 0 1-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 0 0-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 0 0-8-8.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 865 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H188V494h440v326z"/><path fill="currentColor" d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 636 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8z"/><path fill="currentColor" d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H396V494h440v326z"/></svg>
|
||||
|
After Width: | Height: | Size: 639 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"/></svg>
|
||||
|
After Width: | Height: | Size: 535 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"/></svg>
|
||||
|
After Width: | Height: | Size: 631 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 0 0-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"/></svg>
|
||||
|
After Width: | Height: | Size: 423 B |
@@ -0,0 +1,64 @@
|
||||
import { ref } from "vue";
|
||||
import reDialog from "./index.vue";
|
||||
import { useTimeoutFn } from "@vueuse/core";
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
import type {
|
||||
EventType,
|
||||
ArgsType,
|
||||
DialogProps,
|
||||
ButtonProps,
|
||||
DialogOptions
|
||||
} from "./type";
|
||||
|
||||
const dialogStore = ref<Array<DialogOptions>>([]);
|
||||
|
||||
/** 打开弹框 */
|
||||
const addDialog = (options: DialogOptions) => {
|
||||
const open = () =>
|
||||
dialogStore.value.push(Object.assign(options, { visible: true }));
|
||||
if (options?.openDelay) {
|
||||
useTimeoutFn(() => {
|
||||
open();
|
||||
}, options.openDelay);
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
};
|
||||
|
||||
/** 关闭弹框 */
|
||||
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
|
||||
dialogStore.value.splice(index, 1);
|
||||
options.closeCallBack && options.closeCallBack({ options, index, args });
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 更改弹框自身属性值
|
||||
* @param value 属性值
|
||||
* @param key 属性,默认`title`
|
||||
* @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`)
|
||||
*/
|
||||
const updateDialog = (value: any, key = "title", index = 0) => {
|
||||
dialogStore.value[index][key] = value;
|
||||
};
|
||||
|
||||
/** 关闭所有弹框 */
|
||||
const closeAllDialog = () => {
|
||||
dialogStore.value = [];
|
||||
};
|
||||
|
||||
/** 千万别忘了在下面这三处引入并注册下,放心注册,不使用`addDialog`调用就不会被挂载
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L13
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L18
|
||||
*/
|
||||
const ReDialog = withInstall(reDialog);
|
||||
|
||||
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
|
||||
export {
|
||||
ReDialog,
|
||||
dialogStore,
|
||||
addDialog,
|
||||
closeDialog,
|
||||
updateDialog,
|
||||
closeAllDialog
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
closeDialog,
|
||||
dialogStore,
|
||||
type EventType,
|
||||
type ButtonProps,
|
||||
type DialogOptions
|
||||
} from "./index";
|
||||
import { ref, computed } from "vue";
|
||||
import { isFunction } from "@pureadmin/utils";
|
||||
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
|
||||
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
|
||||
|
||||
const fullscreen = ref(false);
|
||||
|
||||
const footerButtons = computed(() => {
|
||||
return (options: DialogOptions) => {
|
||||
return options?.footerButtons?.length > 0
|
||||
? options.footerButtons
|
||||
: ([
|
||||
{
|
||||
label: "取消",
|
||||
text: true,
|
||||
bg: true,
|
||||
btnClick: ({ dialog: { options, index } }) => {
|
||||
const done = () =>
|
||||
closeDialog(options, index, { command: "cancel" });
|
||||
if (options?.beforeCancel && isFunction(options?.beforeCancel)) {
|
||||
options.beforeCancel(done, { options, index });
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "确定",
|
||||
type: "primary",
|
||||
text: true,
|
||||
bg: true,
|
||||
btnClick: ({ dialog: { options, index } }) => {
|
||||
const done = () =>
|
||||
closeDialog(options, index, { command: "sure" });
|
||||
if (options?.beforeSure && isFunction(options?.beforeSure)) {
|
||||
options.beforeSure(done, { options, index });
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
}
|
||||
] as Array<ButtonProps>);
|
||||
};
|
||||
});
|
||||
|
||||
const fullscreenClass = computed(() => {
|
||||
return [
|
||||
"el-icon",
|
||||
"el-dialog__close",
|
||||
"-translate-x-2",
|
||||
"cursor-pointer",
|
||||
"hover:!text-[red]"
|
||||
];
|
||||
});
|
||||
|
||||
function eventsCallBack(
|
||||
event: EventType,
|
||||
options: DialogOptions,
|
||||
index: number
|
||||
) {
|
||||
fullscreen.value = options?.fullscreen ?? false;
|
||||
if (options?.[event] && isFunction(options?.[event])) {
|
||||
return options?.[event]({ options, index });
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose(
|
||||
options: DialogOptions,
|
||||
index: number,
|
||||
args = { command: "close" }
|
||||
) {
|
||||
closeDialog(options, index, args);
|
||||
eventsCallBack("close", options, index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
class="pure-dialog"
|
||||
v-for="(options, index) in dialogStore"
|
||||
:key="index"
|
||||
v-bind="options"
|
||||
v-model="options.visible"
|
||||
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
|
||||
@close="handleClose(options, index)"
|
||||
@opened="eventsCallBack('open', options, index)"
|
||||
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
|
||||
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
|
||||
>
|
||||
<!-- header -->
|
||||
<template
|
||||
v-if="options?.fullscreenIcon || options?.headerRenderer"
|
||||
#header="{ close, titleId, titleClass }"
|
||||
>
|
||||
<div
|
||||
v-if="options?.fullscreenIcon"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<span :id="titleId" :class="titleClass">{{ options?.title }}</span>
|
||||
<i
|
||||
v-if="!options?.fullscreen"
|
||||
:class="fullscreenClass"
|
||||
@click="fullscreen = !fullscreen"
|
||||
>
|
||||
<IconifyIconOffline
|
||||
class="pure-dialog-svg"
|
||||
:icon="
|
||||
options?.fullscreen
|
||||
? ExitFullscreen
|
||||
: fullscreen
|
||||
? ExitFullscreen
|
||||
: Fullscreen
|
||||
"
|
||||
/>
|
||||
</i>
|
||||
</div>
|
||||
<component
|
||||
v-else
|
||||
:is="options?.headerRenderer({ close, titleId, titleClass })"
|
||||
/>
|
||||
</template>
|
||||
<component
|
||||
v-bind="options?.props"
|
||||
:is="options.contentRenderer({ options, index })"
|
||||
@close="args => handleClose(options, index, args)"
|
||||
/>
|
||||
<!-- footer -->
|
||||
<template v-if="!options?.hideFooter" #footer>
|
||||
<template v-if="options?.footerRenderer">
|
||||
<component :is="options?.footerRenderer({ options, index })" />
|
||||
</template>
|
||||
<span v-else>
|
||||
<el-button
|
||||
v-for="(btn, key) in footerButtons(options)"
|
||||
:key="key"
|
||||
v-bind="btn"
|
||||
@click="
|
||||
btn.btnClick({
|
||||
dialog: { options, index },
|
||||
button: { btn, index: key }
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ btn?.label }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,218 @@
|
||||
import type { CSSProperties, VNode, Component } from "vue";
|
||||
|
||||
type DoneFn = (cancel?: boolean) => void;
|
||||
type EventType = "open" | "close" | "openAutoFocus" | "closeAutoFocus";
|
||||
type ArgsType = {
|
||||
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或者空白页 */
|
||||
command: "cancel" | "sure" | "close";
|
||||
};
|
||||
|
||||
/** https://element-plus.org/zh-CN/component/dialog.html#attributes */
|
||||
type DialogProps = {
|
||||
/** `Dialog` 的显示与隐藏 */
|
||||
visible?: boolean;
|
||||
/** `Dialog` 的标题 */
|
||||
title?: string;
|
||||
/** `Dialog` 的宽度,默认 `50%` */
|
||||
width?: string | number;
|
||||
/** 是否为全屏 `Dialog`(会一直处于全屏状态,除非弹框关闭),默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
|
||||
fullscreen?: boolean;
|
||||
/** 是否显示全屏操作图标,默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
|
||||
fullscreenIcon?: boolean;
|
||||
/** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */
|
||||
top?: string;
|
||||
/** 是否需要遮罩层,默认 `true` */
|
||||
modal?: boolean;
|
||||
/** `Dialog` 自身是否插入至 `body` 元素上。嵌套的 `Dialog` 必须指定该属性并赋值为 `true`,默认 `false` */
|
||||
appendToBody?: boolean;
|
||||
/** 是否在 `Dialog` 出现时将 `body` 滚动锁定,默认 `true` */
|
||||
lockScroll?: boolean;
|
||||
/** `Dialog` 的自定义类名 */
|
||||
class?: string;
|
||||
/** `Dialog` 的自定义样式 */
|
||||
style?: CSSProperties;
|
||||
/** `Dialog` 打开的延时时间,单位毫秒,默认 `0` */
|
||||
openDelay?: number;
|
||||
/** `Dialog` 关闭的延时时间,单位毫秒,默认 `0` */
|
||||
closeDelay?: number;
|
||||
/** 是否可以通过点击 `modal` 关闭 `Dialog`,默认 `true` */
|
||||
closeOnClickModal?: boolean;
|
||||
/** 是否可以通过按下 `ESC` 关闭 `Dialog`,默认 `true` */
|
||||
closeOnPressEscape?: boolean;
|
||||
/** 是否显示关闭按钮,默认 `true` */
|
||||
showClose?: boolean;
|
||||
/** 关闭前的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
|
||||
beforeClose?: (done: DoneFn) => void;
|
||||
/** 为 `Dialog` 启用可拖拽功能,默认 `false` */
|
||||
draggable?: boolean;
|
||||
/** 是否让 `Dialog` 的 `header` 和 `footer` 部分居中排列,默认 `false` */
|
||||
center?: boolean;
|
||||
/** 是否水平垂直对齐对话框,默认 `false` */
|
||||
alignCenter?: boolean;
|
||||
/** 当关闭 `Dialog` 时,销毁其中的元素,默认 `false` */
|
||||
destroyOnClose?: boolean;
|
||||
};
|
||||
|
||||
type BtnClickDialog = {
|
||||
options?: DialogOptions;
|
||||
index?: number;
|
||||
};
|
||||
type BtnClickButton = {
|
||||
btn?: ButtonProps;
|
||||
index?: number;
|
||||
};
|
||||
/** https://element-plus.org/zh-CN/component/button.html#button-attributes */
|
||||
type ButtonProps = {
|
||||
/** 按钮文字 */
|
||||
label: string;
|
||||
/** 按钮尺寸 */
|
||||
size?: "large" | "default" | "small";
|
||||
/** 按钮类型 */
|
||||
type?: "primary" | "success" | "warning" | "danger" | "info";
|
||||
/** 是否为朴素按钮,默认 `false` */
|
||||
plain?: boolean;
|
||||
/** 是否为文字按钮,默认 `false` */
|
||||
text?: boolean;
|
||||
/** 是否显示文字按钮背景颜色,默认 `false` */
|
||||
bg?: boolean;
|
||||
/** 是否为链接按钮,默认 `false` */
|
||||
link?: boolean;
|
||||
/** 是否为圆角按钮,默认 `false` */
|
||||
round?: boolean;
|
||||
/** 是否为圆形按钮,默认 `false` */
|
||||
circle?: boolean;
|
||||
/** 是否为加载中状态,默认 `false` */
|
||||
loading?: boolean;
|
||||
/** 自定义加载中状态图标组件 */
|
||||
loadingIcon?: string | Component;
|
||||
/** 按钮是否为禁用状态,默认 `false` */
|
||||
disabled?: boolean;
|
||||
/** 图标组件 */
|
||||
icon?: string | Component;
|
||||
/** 是否开启原生 `autofocus` 属性,默认 `false` */
|
||||
autofocus?: boolean;
|
||||
/** 原生 `type` 属性,默认 `button` */
|
||||
nativeType?: "button" | "submit" | "reset";
|
||||
/** 自动在两个中文字符之间插入空格 */
|
||||
autoInsertSpace?: boolean;
|
||||
/** 自定义按钮颜色, 并自动计算 `hover` 和 `active` 触发后的颜色 */
|
||||
color?: string;
|
||||
/** `dark` 模式, 意味着自动设置 `color` 为 `dark` 模式的颜色,默认 `false` */
|
||||
dark?: boolean;
|
||||
/** 自定义元素标签 */
|
||||
tag?: string | Component;
|
||||
/** 点击按钮后触发的回调 */
|
||||
btnClick?: ({
|
||||
dialog,
|
||||
button
|
||||
}: {
|
||||
/** 当前 `Dialog` 信息 */
|
||||
dialog: BtnClickDialog;
|
||||
/** 当前 `button` 信息 */
|
||||
button: BtnClickButton;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
interface DialogOptions extends DialogProps {
|
||||
/** 内容区组件的 `props`,可通过 `defineProps` 接收 */
|
||||
props?: any;
|
||||
/** 是否隐藏 `Dialog` 按钮操作区的内容 */
|
||||
hideFooter?: boolean;
|
||||
/**
|
||||
* @description 自定义对话框标题的内容渲染器
|
||||
* @see {@link https://element-plus.org/zh-CN/component/dialog.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%A4%B4%E9%83%A8}
|
||||
*/
|
||||
headerRenderer?: ({
|
||||
close,
|
||||
titleId,
|
||||
titleClass
|
||||
}: {
|
||||
close: Function;
|
||||
titleId: string;
|
||||
titleClass: string;
|
||||
}) => VNode | Component;
|
||||
/** 自定义内容渲染器 */
|
||||
contentRenderer?: ({
|
||||
options,
|
||||
index
|
||||
}: {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}) => VNode | Component;
|
||||
/** 自定义按钮操作区的内容渲染器,会覆盖`footerButtons`以及默认的 `取消` 和 `确定` 按钮 */
|
||||
footerRenderer?: ({
|
||||
options,
|
||||
index
|
||||
}: {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}) => VNode | Component;
|
||||
/** 自定义底部按钮操作 */
|
||||
footerButtons?: Array<ButtonProps>;
|
||||
/** `Dialog` 打开后的回调 */
|
||||
open?: ({
|
||||
options,
|
||||
index
|
||||
}: {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}) => void;
|
||||
/** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或者空白页关闭页面时才会触发) */
|
||||
close?: ({
|
||||
options,
|
||||
index
|
||||
}: {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}) => void;
|
||||
/** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或者空白页 */
|
||||
closeCallBack?: ({
|
||||
options,
|
||||
index,
|
||||
args
|
||||
}: {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
args: any;
|
||||
}) => void;
|
||||
/** 输入焦点聚焦在 `Dialog` 内容时的回调 */
|
||||
openAutoFocus?: ({
|
||||
options,
|
||||
index
|
||||
}: {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}) => void;
|
||||
/** 输入焦点从 `Dialog` 内容失焦时的回调 */
|
||||
closeAutoFocus?: ({
|
||||
options,
|
||||
index
|
||||
}: {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}) => void;
|
||||
/** 点击底部取消按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
|
||||
beforeCancel?: (
|
||||
done: Function,
|
||||
{
|
||||
options,
|
||||
index
|
||||
}: {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}
|
||||
) => void;
|
||||
/** 点击底部确定按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
|
||||
beforeSure?: (
|
||||
done: Function,
|
||||
{
|
||||
options,
|
||||
index
|
||||
}: {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
|
||||
@@ -0,0 +1,12 @@
|
||||
import iconifyIconOffline from "./src/iconifyIconOffline";
|
||||
import iconifyIconOnline from "./src/iconifyIconOnline";
|
||||
import fontIcon from "./src/iconfont";
|
||||
|
||||
/** 本地图标组件 */
|
||||
const IconifyIconOffline = iconifyIconOffline;
|
||||
/** 在线图标组件 */
|
||||
const IconifyIconOnline = iconifyIconOnline;
|
||||
/** iconfont组件 */
|
||||
const FontIcon = fontIcon;
|
||||
|
||||
export { IconifyIconOffline, IconifyIconOnline, FontIcon };
|
||||
@@ -0,0 +1,237 @@
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep } from "@pureadmin/utils";
|
||||
import { IconJson } from "@/components/ReIcon/data";
|
||||
import { ref, computed, CSSProperties, toRef, watch } from "vue";
|
||||
type ParameterCSSProperties = (item?: string) => CSSProperties | undefined;
|
||||
|
||||
defineOptions({
|
||||
name: "IconSelect"
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
require: false,
|
||||
type: String
|
||||
}
|
||||
});
|
||||
const emit = defineEmits<{ (e: "update:modelValue", v: string) }>();
|
||||
|
||||
const visible = ref(false);
|
||||
const inputValue = toRef(props, "modelValue");
|
||||
const iconList = ref(IconJson);
|
||||
const icon = ref("add-location");
|
||||
const currentActiveType = ref("ep:");
|
||||
// 深拷贝图标数据,前端做搜索
|
||||
const copyIconList = cloneDeep(iconList.value);
|
||||
|
||||
const pageSize = ref(96);
|
||||
const currentPage = ref(1);
|
||||
|
||||
// 搜索条件
|
||||
const filterValue = ref("");
|
||||
|
||||
const tabsList = [
|
||||
{
|
||||
label: "Element Plus",
|
||||
name: "ep:"
|
||||
},
|
||||
{
|
||||
label: "Font Awesome 4",
|
||||
name: "fa:"
|
||||
},
|
||||
{
|
||||
label: "Font Awesome 5 Solid",
|
||||
name: "fa-solid:"
|
||||
}
|
||||
];
|
||||
|
||||
const pageList = computed(() => {
|
||||
if (currentPage.value === 1) {
|
||||
return copyIconList[currentActiveType.value]
|
||||
.filter(v => v.includes(filterValue.value))
|
||||
.slice(currentPage.value - 1, pageSize.value);
|
||||
} else {
|
||||
return copyIconList[currentActiveType.value]
|
||||
.filter(v => v.includes(filterValue.value))
|
||||
.slice(
|
||||
pageSize.value * (currentPage.value - 1),
|
||||
pageSize.value * (currentPage.value - 1) + pageSize.value
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const iconItemStyle = computed((): ParameterCSSProperties => {
|
||||
return item => {
|
||||
if (inputValue.value === currentActiveType.value + item) {
|
||||
return {
|
||||
borderColor: "var(--el-color-primary)",
|
||||
color: "var(--el-color-primary)"
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function handleClick({ props }) {
|
||||
currentPage.value = 1;
|
||||
currentActiveType.value = props.name;
|
||||
emit(
|
||||
"update:modelValue",
|
||||
currentActiveType.value + iconList.value[currentActiveType.value][0]
|
||||
);
|
||||
icon.value = iconList.value[currentActiveType.value][0];
|
||||
}
|
||||
|
||||
function onChangeIcon(item) {
|
||||
icon.value = item;
|
||||
emit("update:modelValue", currentActiveType.value + item);
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function onCurrentChange(page) {
|
||||
currentPage.value = page;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return props.modelValue;
|
||||
},
|
||||
() => {
|
||||
if (props.modelValue) {
|
||||
currentActiveType.value = props.modelValue.substring(
|
||||
0,
|
||||
props.modelValue.indexOf(":") + 1
|
||||
);
|
||||
icon.value = props.modelValue.substring(
|
||||
props.modelValue.indexOf(":") + 1
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(
|
||||
() => {
|
||||
return filterValue.value;
|
||||
},
|
||||
() => {
|
||||
currentPage.value = 1;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="selector w-[350px]">
|
||||
<el-input v-model="inputValue" disabled>
|
||||
<template #append>
|
||||
<el-popover
|
||||
:width="350"
|
||||
trigger="click"
|
||||
popper-class="pure-popper"
|
||||
:popper-options="{
|
||||
placement: 'auto'
|
||||
}"
|
||||
:visible="visible"
|
||||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="w-[40px] h-[32px] cursor-pointer flex justify-center items-center"
|
||||
@click="visible = !visible"
|
||||
>
|
||||
<IconifyIconOnline :icon="currentActiveType + icon" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-input
|
||||
class="px-2 pt-2"
|
||||
v-model="filterValue"
|
||||
placeholder="搜索图标"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<el-tabs v-model="currentActiveType" @tab-click="handleClick">
|
||||
<el-tab-pane
|
||||
v-for="(pane, index) in tabsList"
|
||||
:key="index"
|
||||
:label="pane.label"
|
||||
:name="pane.name"
|
||||
>
|
||||
<el-scrollbar height="220px">
|
||||
<ul class="flex flex-wrap px-2 ml-2">
|
||||
<li
|
||||
v-for="(item, key) in pageList"
|
||||
:key="key"
|
||||
:title="item"
|
||||
class="icon-item p-2 cursor-pointer mr-2 mt-1 flex justify-center items-center border border-solid"
|
||||
:style="iconItemStyle(item)"
|
||||
@click="onChangeIcon(item)"
|
||||
>
|
||||
<IconifyIconOnline
|
||||
:icon="currentActiveType + item"
|
||||
width="20px"
|
||||
height="20px"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-pagination
|
||||
small
|
||||
:total="copyIconList[currentActiveType].length"
|
||||
:page-size="pageSize"
|
||||
:current-page="currentPage"
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
class="flex items-center justify-center h-10"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-item {
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
transition: all 0.4s;
|
||||
transform: scaleX(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-next) {
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
box-shadow: -5px 0 5px -6px #ccc;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-prev) {
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
box-shadow: 5px 0 5px -6px #ccc;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
height: 30px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header),
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
position: static;
|
||||
margin: 0;
|
||||
box-shadow: 0 2px 5px rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
import { iconType } from "./types";
|
||||
import { h, defineComponent, Component } from "vue";
|
||||
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
|
||||
|
||||
/**
|
||||
* 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标
|
||||
* @see 点击查看文档图标篇 {@link https://yiming_chang.gitee.io/pure-admin-doc/pages/icon/}
|
||||
* @param icon 必传 图标
|
||||
* @param attrs 可选 iconType 属性
|
||||
* @returns Component
|
||||
*/
|
||||
export function useRenderIcon(icon: any, attrs?: iconType): Component {
|
||||
// iconfont
|
||||
const ifReg = /^IF-/;
|
||||
// typeof icon === "function" 属于SVG
|
||||
if (ifReg.test(icon)) {
|
||||
// iconfont
|
||||
const name = icon.split(ifReg)[1];
|
||||
const iconName = name.slice(
|
||||
0,
|
||||
name.indexOf(" ") == -1 ? name.length : name.indexOf(" ")
|
||||
);
|
||||
const iconType = name.slice(name.indexOf(" ") + 1, name.length);
|
||||
return defineComponent({
|
||||
name: "FontIcon",
|
||||
render() {
|
||||
return h(FontIcon, {
|
||||
icon: iconName,
|
||||
iconType,
|
||||
...attrs
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (typeof icon === "function" || typeof icon?.render === "function") {
|
||||
// svg
|
||||
return icon;
|
||||
} else if (typeof icon === "object") {
|
||||
return defineComponent({
|
||||
name: "OfflineIcon",
|
||||
render() {
|
||||
return h(IconifyIconOffline, {
|
||||
icon: icon,
|
||||
...attrs
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
|
||||
return defineComponent({
|
||||
name: "Icon",
|
||||
render() {
|
||||
const IconifyIcon =
|
||||
icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline;
|
||||
return h(IconifyIcon, {
|
||||
icon: icon,
|
||||
...attrs
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { h, defineComponent } from "vue";
|
||||
|
||||
// 封装iconfont组件,默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code)
|
||||
export default defineComponent({
|
||||
name: "FontIcon",
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const attrs = this.$attrs;
|
||||
if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") {
|
||||
return h(
|
||||
"i",
|
||||
{
|
||||
class: "iconfont",
|
||||
...attrs
|
||||
},
|
||||
this.icon
|
||||
);
|
||||
} else if (
|
||||
Object.keys(attrs).includes("svg") ||
|
||||
attrs?.iconType === "svg"
|
||||
) {
|
||||
return h(
|
||||
"svg",
|
||||
{
|
||||
class: "icon-svg",
|
||||
"aria-hidden": true
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h("use", {
|
||||
"xlink:href": `#${this.icon}`
|
||||
})
|
||||
]
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return h("i", {
|
||||
class: `iconfont ${this.icon}`,
|
||||
...attrs
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { h, defineComponent } from "vue";
|
||||
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
|
||||
|
||||
// Iconify Icon在Vue里本地使用(用于内网环境)https://docs.iconify.design/icon-components/vue/offline.html
|
||||
export default defineComponent({
|
||||
name: "IconifyIconOffline",
|
||||
components: { IconifyIcon },
|
||||
props: {
|
||||
icon: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
render() {
|
||||
if (typeof this.icon === "object") addIcon(this.icon, this.icon);
|
||||
const attrs = this.$attrs;
|
||||
return h(
|
||||
IconifyIcon,
|
||||
{
|
||||
icon: this.icon,
|
||||
style: attrs?.style
|
||||
? Object.assign(attrs.style, { outline: "none" })
|
||||
: { outline: "none" },
|
||||
...attrs
|
||||
},
|
||||
{
|
||||
default: () => []
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { h, defineComponent } from "vue";
|
||||
import { Icon as IconifyIcon } from "@iconify/vue";
|
||||
|
||||
// Iconify Icon在Vue里在线使用(用于外网环境)
|
||||
export default defineComponent({
|
||||
name: "IconifyIconOnline",
|
||||
components: { IconifyIcon },
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const attrs = this.$attrs;
|
||||
return h(
|
||||
IconifyIcon,
|
||||
{
|
||||
icon: `${this.icon}`,
|
||||
style: attrs?.style
|
||||
? Object.assign(attrs.style, { outline: "none" })
|
||||
: { outline: "none" },
|
||||
...attrs
|
||||
},
|
||||
{
|
||||
default: () => []
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { addIcon } from "@iconify/vue/dist/offline";
|
||||
|
||||
/**
|
||||
* 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
|
||||
*/
|
||||
|
||||
// 本地菜单图标,后端在路由的icon中返回对应的图标字符串并且前端在此处使用addIcon添加即可渲染菜单图标
|
||||
import UbuntuFill from "@iconify-icons/ri/ubuntu-fill";
|
||||
import Menu from "@iconify-icons/ep/menu";
|
||||
import Edit from "@iconify-icons/ep/edit";
|
||||
import InformationLine from "@iconify-icons/ri/information-line";
|
||||
import SetUp from "@iconify-icons/ep/set-up";
|
||||
import TerminalWindowLine from "@iconify-icons/ri/terminal-window-line";
|
||||
import Guide from "@iconify-icons/ep/guide";
|
||||
import HomeFilled from "@iconify-icons/ep/home-filled";
|
||||
import Card from "@iconify-icons/ri/bank-card-line";
|
||||
import ListCheck from "@iconify-icons/ri/list-check";
|
||||
import Histogram from "@iconify-icons/ep/histogram";
|
||||
import Ppt from "@iconify-icons/ri/file-ppt-2-line";
|
||||
import CheckboxCircleLine from "@iconify-icons/ri/checkbox-circle-line";
|
||||
import FlUser from "@iconify-icons/ri/admin-line";
|
||||
import Role from "@iconify-icons/ri/admin-fill";
|
||||
import Setting from "@iconify-icons/ri/settings-3-line";
|
||||
import Dept from "@iconify-icons/ri/git-branch-line";
|
||||
import Lollipop from "@iconify-icons/ep/lollipop";
|
||||
import Monitor from "@iconify-icons/ep/monitor";
|
||||
addIcon("ubuntuFill", UbuntuFill);
|
||||
addIcon("menu", Menu);
|
||||
addIcon("edit", Edit);
|
||||
addIcon("informationLine", InformationLine);
|
||||
addIcon("setUp", SetUp);
|
||||
addIcon("terminalWindowLine", TerminalWindowLine);
|
||||
addIcon("guide", Guide);
|
||||
addIcon("homeFilled", HomeFilled);
|
||||
addIcon("card", Card);
|
||||
addIcon("listCheck", ListCheck);
|
||||
addIcon("histogram", Histogram);
|
||||
addIcon("ppt", Ppt);
|
||||
addIcon("checkboxCircleLine", CheckboxCircleLine);
|
||||
addIcon("flUser", FlUser);
|
||||
addIcon("role", Role);
|
||||
addIcon("setting", Setting);
|
||||
addIcon("dept", Dept);
|
||||
addIcon("lollipop", Lollipop);
|
||||
addIcon("monitor", Monitor);
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface iconType {
|
||||
// iconify (https://docs.iconify.design/icon-components/vue/#properties)
|
||||
inline?: boolean;
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
horizontalFlip?: boolean;
|
||||
verticalFlip?: boolean;
|
||||
flip?: string;
|
||||
rotate?: number | string;
|
||||
color?: string;
|
||||
horizontalAlign?: boolean;
|
||||
verticalAlign?: boolean;
|
||||
align?: string;
|
||||
onLoad?: Function;
|
||||
includes?: Function;
|
||||
|
||||
// all icon
|
||||
style?: object;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import reImageVerify from "./src/index.vue";
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
|
||||
/** 图形验证码组件 */
|
||||
export const ReImageVerify = withInstall(reImageVerify);
|
||||
|
||||
export default ReImageVerify;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
/**
|
||||
* 绘制图形验证码
|
||||
* @param width - 图形宽度
|
||||
* @param height - 图形高度
|
||||
*/
|
||||
export const useImageVerify = (width = 120, height = 40) => {
|
||||
const domRef = ref<HTMLCanvasElement>();
|
||||
const imgCode = ref("");
|
||||
|
||||
function setImgCode(code: string) {
|
||||
imgCode.value = code;
|
||||
}
|
||||
|
||||
function getImgCode() {
|
||||
if (!domRef.value) return;
|
||||
imgCode.value = draw(domRef.value, width, height);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getImgCode();
|
||||
});
|
||||
|
||||
return {
|
||||
domRef,
|
||||
imgCode,
|
||||
setImgCode,
|
||||
getImgCode
|
||||
};
|
||||
};
|
||||
|
||||
/** 前端自己生成验证码 */
|
||||
function randomNum(min: number, max: number) {
|
||||
const num = Math.floor(Math.random() * (max - min) + min);
|
||||
return num;
|
||||
}
|
||||
|
||||
function randomColor(min: number, max: number) {
|
||||
const r = randomNum(min, max);
|
||||
const g = randomNum(min, max);
|
||||
const b = randomNum(min, max);
|
||||
return `rgb(${r},${g},${b})`;
|
||||
}
|
||||
|
||||
function draw(dom: HTMLCanvasElement, width: number, height: number) {
|
||||
let imgCode = "";
|
||||
|
||||
const NUMBER_STRING = "0123456789";
|
||||
|
||||
const ctx = dom.getContext("2d");
|
||||
if (!ctx) return imgCode;
|
||||
|
||||
ctx.fillStyle = randomColor(180, 230);
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
|
||||
imgCode += text;
|
||||
const fontSize = randomNum(18, 41);
|
||||
const deg = randomNum(-30, 30);
|
||||
ctx.font = `${fontSize}px Simhei`;
|
||||
ctx.textBaseline = "top";
|
||||
ctx.fillStyle = randomColor(80, 150);
|
||||
ctx.save();
|
||||
ctx.translate(30 * i + 15, 15);
|
||||
ctx.rotate((deg * Math.PI) / 180);
|
||||
ctx.fillText(text, -15 + 5, -15);
|
||||
ctx.restore();
|
||||
}
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(randomNum(0, width), randomNum(0, height));
|
||||
ctx.lineTo(randomNum(0, width), randomNum(0, height));
|
||||
ctx.strokeStyle = randomColor(180, 230);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let i = 0; i < 41; i += 1) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = randomColor(150, 200);
|
||||
ctx.fill();
|
||||
}
|
||||
return imgCode;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from "vue";
|
||||
import { useImageVerify } from "./hooks";
|
||||
|
||||
defineOptions({
|
||||
name: "ReImageVerify"
|
||||
});
|
||||
|
||||
interface Props {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:code", code: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
code: ""
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify();
|
||||
|
||||
watch(
|
||||
() => props.code,
|
||||
newValue => {
|
||||
setImgCode(newValue);
|
||||
}
|
||||
);
|
||||
watch(imgCode, newValue => {
|
||||
emit("update:code", newValue);
|
||||
});
|
||||
|
||||
defineExpose({ getImgCode });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<canvas
|
||||
ref="domRef"
|
||||
width="120"
|
||||
height="40"
|
||||
class="cursor-pointer"
|
||||
@click="getImgCode"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import pureTableBar from "./src/bar";
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
|
||||
/** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */
|
||||
export const PureTableBar = withInstall(pureTableBar);
|
||||
@@ -0,0 +1,355 @@
|
||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
||||
import { defineComponent, ref, computed, type PropType, nextTick } from "vue";
|
||||
import {
|
||||
delay,
|
||||
cloneDeep,
|
||||
isBoolean,
|
||||
isFunction,
|
||||
getKeyList
|
||||
} from "@pureadmin/utils";
|
||||
|
||||
import Sortable from "sortablejs";
|
||||
import DragIcon from "./svg/drag.svg?component";
|
||||
import ExpandIcon from "./svg/expand.svg?component";
|
||||
import RefreshIcon from "./svg/refresh.svg?component";
|
||||
import SettingIcon from "./svg/settings.svg?component";
|
||||
import CollapseIcon from "./svg/collapse.svg?component";
|
||||
|
||||
const props = {
|
||||
/** 头部最左边的标题 */
|
||||
title: {
|
||||
type: String,
|
||||
default: "列表"
|
||||
},
|
||||
/** 对于树形表格,如果想启用展开和折叠功能,传入当前表格的ref即可 */
|
||||
tableRef: {
|
||||
type: Object as PropType<any>
|
||||
},
|
||||
/** 需要展示的列 */
|
||||
columns: {
|
||||
type: Array as PropType<TableColumnList>,
|
||||
default: () => []
|
||||
}
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "PureTableBar",
|
||||
props,
|
||||
emits: ["refresh"],
|
||||
setup(props, { emit, slots, attrs }) {
|
||||
const buttonRef = ref();
|
||||
const size = ref("default");
|
||||
const isExpandAll = ref(true);
|
||||
const loading = ref(false);
|
||||
const checkAll = ref(true);
|
||||
const isIndeterminate = ref(false);
|
||||
const filterColumns = cloneDeep(props?.columns).filter(column =>
|
||||
isBoolean(column?.hide)
|
||||
? !column.hide
|
||||
: !(isFunction(column?.hide) && column?.hide())
|
||||
);
|
||||
let checkColumnList = getKeyList(cloneDeep(props?.columns), "label");
|
||||
const checkedColumns = ref(getKeyList(cloneDeep(filterColumns), "label"));
|
||||
const dynamicColumns = ref(cloneDeep(props?.columns));
|
||||
|
||||
const getDropdownItemStyle = computed(() => {
|
||||
return s => {
|
||||
return {
|
||||
background:
|
||||
s === size.value ? useEpThemeStoreHook().epThemeColor : "",
|
||||
color: s === size.value ? "#fff" : "var(--el-text-color-primary)"
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return [
|
||||
"text-black",
|
||||
"dark:text-white",
|
||||
"duration-100",
|
||||
"hover:!text-primary",
|
||||
"cursor-pointer",
|
||||
"outline-none"
|
||||
];
|
||||
});
|
||||
|
||||
const topClass = computed(() => {
|
||||
return [
|
||||
"flex",
|
||||
"justify-between",
|
||||
"pt-[3px]",
|
||||
"px-[11px]",
|
||||
"border-b-[1px]",
|
||||
"border-solid",
|
||||
"border-[#dcdfe6]",
|
||||
"dark:border-[#303030]"
|
||||
];
|
||||
});
|
||||
|
||||
function onReFresh() {
|
||||
loading.value = true;
|
||||
emit("refresh");
|
||||
delay(500).then(() => (loading.value = false));
|
||||
}
|
||||
|
||||
function onExpand() {
|
||||
isExpandAll.value = !isExpandAll.value;
|
||||
toggleRowExpansionAll(props.tableRef.data, isExpandAll.value);
|
||||
}
|
||||
|
||||
function toggleRowExpansionAll(data, isExpansion) {
|
||||
data.forEach(item => {
|
||||
props.tableRef.toggleRowExpansion(item, isExpansion);
|
||||
if (item.children !== undefined && item.children !== null) {
|
||||
toggleRowExpansionAll(item.children, isExpansion);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleCheckAllChange(val: boolean) {
|
||||
checkedColumns.value = val ? checkColumnList : [];
|
||||
isIndeterminate.value = false;
|
||||
dynamicColumns.value.map(column =>
|
||||
val ? (column.hide = false) : (column.hide = true)
|
||||
);
|
||||
}
|
||||
|
||||
function handleCheckedColumnsChange(value: string[]) {
|
||||
const checkedCount = value.length;
|
||||
checkAll.value = checkedCount === checkColumnList.length;
|
||||
isIndeterminate.value =
|
||||
checkedCount > 0 && checkedCount < checkColumnList.length;
|
||||
}
|
||||
|
||||
function handleCheckColumnListChange(val: boolean, label: string) {
|
||||
dynamicColumns.value.filter(item => item.label === label)[0].hide = !val;
|
||||
}
|
||||
|
||||
async function onReset() {
|
||||
checkAll.value = true;
|
||||
isIndeterminate.value = false;
|
||||
dynamicColumns.value = cloneDeep(props?.columns);
|
||||
checkColumnList = [];
|
||||
checkColumnList = await getKeyList(cloneDeep(props?.columns), "label");
|
||||
checkedColumns.value = getKeyList(cloneDeep(filterColumns), "label");
|
||||
}
|
||||
|
||||
const dropdown = {
|
||||
dropdown: () => (
|
||||
<el-dropdown-menu class="translation">
|
||||
<el-dropdown-item
|
||||
style={getDropdownItemStyle.value("large")}
|
||||
onClick={() => (size.value = "large")}
|
||||
>
|
||||
宽松
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
style={getDropdownItemStyle.value("default")}
|
||||
onClick={() => (size.value = "default")}
|
||||
>
|
||||
默认
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
style={getDropdownItemStyle.value("small")}
|
||||
onClick={() => (size.value = "small")}
|
||||
>
|
||||
紧凑
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
)
|
||||
};
|
||||
|
||||
/** 列展示拖拽排序 */
|
||||
const rowDrop = (event: { preventDefault: () => void }) => {
|
||||
event.preventDefault();
|
||||
nextTick(() => {
|
||||
const wrapper: HTMLElement = document.querySelector(
|
||||
".el-checkbox-group>div"
|
||||
);
|
||||
Sortable.create(wrapper, {
|
||||
animation: 300,
|
||||
handle: ".drag-btn",
|
||||
onEnd: ({ newIndex, oldIndex, item }) => {
|
||||
const targetThElem = item;
|
||||
const wrapperElem = targetThElem.parentNode as HTMLElement;
|
||||
const oldColumn = dynamicColumns.value[oldIndex];
|
||||
const newColumn = dynamicColumns.value[newIndex];
|
||||
if (oldColumn?.fixed || newColumn?.fixed) {
|
||||
// 当前列存在fixed属性 则不可拖拽
|
||||
const oldThElem = wrapperElem.children[oldIndex] as HTMLElement;
|
||||
if (newIndex > oldIndex) {
|
||||
wrapperElem.insertBefore(targetThElem, oldThElem);
|
||||
} else {
|
||||
wrapperElem.insertBefore(
|
||||
targetThElem,
|
||||
oldThElem ? oldThElem.nextElementSibling : oldThElem
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const currentRow = dynamicColumns.value.splice(oldIndex, 1)[0];
|
||||
dynamicColumns.value.splice(newIndex, 0, currentRow);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const isFixedColumn = (label: string) => {
|
||||
return dynamicColumns.value.filter(item => item.label === label)[0].fixed
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
|
||||
const reference = {
|
||||
reference: () => (
|
||||
<SettingIcon
|
||||
class={["w-[16px]", iconClass.value]}
|
||||
onMouseover={e => (buttonRef.value = e.currentTarget)}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div {...attrs} class="w-[99/100] mt-2 px-2 pb-2 bg-bg_color">
|
||||
<div class="flex justify-between w-full h-[60px] p-4">
|
||||
{slots?.title ? (
|
||||
slots.title()
|
||||
) : (
|
||||
<p class="font-bold truncate">{props.title}</p>
|
||||
)}
|
||||
<div class="flex items-center justify-around">
|
||||
{slots?.buttons ? (
|
||||
<div class="flex mr-4">{slots.buttons()}</div>
|
||||
) : null}
|
||||
{props.tableRef?.size ? (
|
||||
<>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content={isExpandAll.value ? "折叠" : "展开"}
|
||||
placement="top"
|
||||
>
|
||||
<ExpandIcon
|
||||
class={["w-[16px]", iconClass.value]}
|
||||
style={{
|
||||
transform: isExpandAll.value ? "none" : "rotate(-90deg)"
|
||||
}}
|
||||
onClick={() => onExpand()}
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
</>
|
||||
) : null}
|
||||
<el-tooltip effect="dark" content="刷新" placement="top">
|
||||
<RefreshIcon
|
||||
class={[
|
||||
"w-[16px]",
|
||||
iconClass.value,
|
||||
loading.value ? "animate-spin" : ""
|
||||
]}
|
||||
onClick={() => onReFresh()}
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
<el-tooltip effect="dark" content="密度" placement="top">
|
||||
<el-dropdown v-slots={dropdown} trigger="click">
|
||||
<CollapseIcon class={["w-[16px]", iconClass.value]} />
|
||||
</el-dropdown>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
|
||||
<el-popover
|
||||
v-slots={reference}
|
||||
placement="bottom-start"
|
||||
popper-style={{ padding: 0 }}
|
||||
width="160"
|
||||
trigger="click"
|
||||
>
|
||||
<div class={[topClass.value]}>
|
||||
<el-checkbox
|
||||
class="!-mr-1"
|
||||
label="列展示"
|
||||
v-model={checkAll.value}
|
||||
indeterminate={isIndeterminate.value}
|
||||
onChange={value => handleCheckAllChange(value)}
|
||||
/>
|
||||
<el-button type="primary" link onClick={() => onReset()}>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="pt-[6px] pl-[11px]">
|
||||
<el-checkbox-group
|
||||
v-model={checkedColumns.value}
|
||||
onChange={value => handleCheckedColumnsChange(value)}
|
||||
>
|
||||
<el-space
|
||||
direction="vertical"
|
||||
alignment="flex-start"
|
||||
size={0}
|
||||
>
|
||||
{checkColumnList.map(item => {
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
<DragIcon
|
||||
class={[
|
||||
"drag-btn w-[16px] mr-2",
|
||||
isFixedColumn(item)
|
||||
? "!cursor-no-drop"
|
||||
: "!cursor-grab"
|
||||
]}
|
||||
onMouseenter={(event: {
|
||||
preventDefault: () => void;
|
||||
}) => rowDrop(event)}
|
||||
/>
|
||||
<el-checkbox
|
||||
key={item}
|
||||
label={item}
|
||||
onChange={value =>
|
||||
handleCheckColumnListChange(value, item)
|
||||
}
|
||||
>
|
||||
<span
|
||||
title={item}
|
||||
class="inline-block w-[120px] truncate hover:text-text_color_primary"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</el-space>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<el-tooltip
|
||||
popper-options={{
|
||||
modifiers: [
|
||||
{
|
||||
name: "computeStyles",
|
||||
options: {
|
||||
adaptive: false,
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
placement="top"
|
||||
virtual-ref={buttonRef.value}
|
||||
virtual-triggering
|
||||
trigger="hover"
|
||||
content="列设置"
|
||||
/>
|
||||
</div>
|
||||
{slots.default({
|
||||
size: size.value,
|
||||
dynamicColumns: dynamicColumns.value
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M13.79 10.21a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42l-2.5-2.5a1 1 0 0 0-.33-.21 1 1 0 0 0-.76 0 1 1 0 0 0-.33.21l-2.5 2.5a1 1 0 0 0 1.42 1.42l.79-.8v5.18l-.79-.8a1 1 0 0 0-1.42 1.42l2.5 2.5a1 1 0 0 0 .33.21.94.94 0 0 0 .76 0 1 1 0 0 0 .33-.21l2.5-2.5a1 1 0 0 0-1.42-1.42l-.79.8V9.41ZM7 4h10a1 1 0 0 0 0-2H7a1 1 0 0 0 0 2Zm10 16H7a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 441 B |