Compare commits
6 Commits
bea581772f
...
bca8b5ca6f
Author | SHA1 | Date |
---|---|---|
|
bca8b5ca6f | |
|
798893c078 | |
|
d2341b1556 | |
|
edf8d86656 | |
|
ab2588de54 | |
|
c9d9bcd76d |
35
.env
|
@ -1,34 +1,5 @@
|
|||
# 平台本地运行端口号
|
||||
VITE_PORT=7000
|
||||
VITE_PORT = 8848
|
||||
|
||||
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY="hash"
|
||||
|
||||
# 基础请求路径
|
||||
VITE_BASE_API=/api
|
||||
|
||||
# 跨域代理地址
|
||||
VITE_APP_URL=http://localhost:8801
|
||||
|
||||
# mock地址
|
||||
VITE_MOCK_BASE_API=/mock
|
||||
|
||||
# 网络请求延迟时间
|
||||
VITE_BASE_API_TIMEOUT=60000
|
||||
|
||||
# 失败重试次数
|
||||
VITE_BASE_API_RETRY=5
|
||||
|
||||
# 失败重试时间
|
||||
VITE_BASE_API_RETRY_DELAY=3000
|
||||
|
||||
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
|
||||
VITE_CDN=false
|
||||
|
||||
# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
|
||||
# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
VITE_COMPRESSION="none"
|
||||
|
||||
# 开发环境读取配置文件路径
|
||||
VITE_PUBLIC_PATH=/
|
||||
# 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置)
|
||||
VITE_HIDE_HOME = false
|
||||
|
|
|
@ -1,26 +1,8 @@
|
|||
# 平台本地运行端口号
|
||||
VITE_PORT=7000
|
||||
|
||||
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY="hash"
|
||||
|
||||
# 基础请求路径
|
||||
VITE_BASE_API=/api
|
||||
|
||||
# 跨域代理地址
|
||||
VITE_APP_URL=http://localhost:7070
|
||||
|
||||
# mock地址
|
||||
VITE_MOCK_BASE_API=/mock
|
||||
|
||||
# 网络请求延迟时间
|
||||
VITE_BASE_API_TIMEOUT=60000
|
||||
|
||||
# 失败重试次数
|
||||
VITE_BASE_API_RETRY=5
|
||||
|
||||
# 失败重试时间
|
||||
VITE_BASE_API_RETRY_DELAY=3000
|
||||
VITE_PORT = 8848
|
||||
|
||||
# 开发环境读取配置文件路径
|
||||
VITE_PUBLIC_PATH=/
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
|
|
@ -1,31 +1,13 @@
|
|||
# 平台本地运行端口号
|
||||
VITE_PORT=80
|
||||
# 线上环境平台打包路径
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY="hash"
|
||||
|
||||
# 基础请求路径
|
||||
VITE_BASE_API=/api
|
||||
|
||||
# mock地址
|
||||
VITE_MOCK_BASE_API=/mock
|
||||
|
||||
# 网络请求延迟时间
|
||||
VITE_BASE_API_TIMEOUT=60000
|
||||
|
||||
# 失败重试次数
|
||||
VITE_BASE_API_RETRY=5
|
||||
|
||||
# 失败重试时间
|
||||
VITE_BASE_API_RETRY_DELAY=3000
|
||||
# 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
||||
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
|
||||
VITE_CDN=true
|
||||
VITE_CDN = false
|
||||
|
||||
# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
|
||||
# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
VITE_COMPRESSION="gzip"
|
||||
|
||||
# 开发环境读取配置文件路径
|
||||
VITE_PUBLIC_PATH=/
|
||||
VITE_COMPRESSION = "none"
|
|
@ -0,0 +1,16 @@
|
|||
# 预发布也需要生产环境的行为
|
||||
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
|
||||
# NODE_ENV = development
|
||||
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
||||
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
|
||||
VITE_CDN = true
|
||||
|
||||
# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
|
||||
# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
VITE_COMPRESSION = "none"
|
|
@ -0,0 +1,2 @@
|
|||
public/wasm/capture.worker.js linguist-language=Vue
|
||||
public/wasm/index.js linguist-language=Vue
|
|
@ -7,11 +7,6 @@ dist-ssr
|
|||
report.html
|
||||
vite.config.*.timestamp*
|
||||
|
||||
bunny-web.site.csr
|
||||
bunny-web.site.key
|
||||
bunny-web.site_bundle.crt
|
||||
bunny-web.site_bundle.pem
|
||||
|
||||
yarn.lock
|
||||
npm-debug.log*
|
||||
.pnpm-error.log*
|
||||
|
@ -24,5 +19,4 @@ tests/**/coverage/
|
|||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
tsconfig.tsbuildinfo
|
|
@ -0,0 +1 @@
|
|||
src/views/system/menu/README.md
|
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Bunny
|
||||
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
|
||||
|
|
47
ReadMe.md
|
@ -38,7 +38,7 @@
|
|||
|
||||
## ✨ v4.0.0 重大更新
|
||||
|
||||
新分支` sysn_6.0.0`与上游【Pure Admin】合并
|
||||
新分支`sysn_6.0.0`与上游【Pure Admin】合并,旧版放在`master-v1`中,最新的`sysn_6.0.0`放在`dev`中。
|
||||
|
||||
### 核心改进
|
||||
|
||||
|
@ -77,10 +77,10 @@
|
|||
|
||||
通过 `WebSecurityConfig` 配置
|
||||
|
||||
| 路径类型 | 示例 | 访问要求 | 配置方式 |
|
||||
| -------- | ----------------- | -------- | ------------------------- |
|
||||
| 路径类型 | 示例 | 访问要求 | 配置方式 |
|
||||
|------|-------------------|------|--------------------|
|
||||
| 公开接口 | `/api/public/**` | 无需认证 | 路径包含 `public` 关键字 |
|
||||
| 私有接口 | `/api/private/**` | 需登录 | 路径包含 `private` 关键字 |
|
||||
| 私有接口 | `/api/private/**` | 需登录 | 路径包含 `private` 关键字 |
|
||||
|
||||
### 路径匹配策略
|
||||
|
||||
|
@ -144,10 +144,10 @@ http.authorizeHttpRequests(auth -> auth
|
|||
|
||||
AntPath详情:https://juejin.cn/spost/7498247273660743732
|
||||
|
||||
| 模式 | 示例 | 说明 |
|
||||
| -------- | --------------- | ---------------- |
|
||||
| 模式 | 示例 | 说明 |
|
||||
|------|-----------------|------------|
|
||||
| 精确匹配 | `/api/user` | 完全匹配路径 |
|
||||
| 通配符 | `/api/user/*` | 匹配单级路径 |
|
||||
| 通配符 | `/api/user/*` | 匹配单级路径 |
|
||||
| 多级通配 | `/api/user/**` | 匹配多级路径 |
|
||||
| 方法限定 | `GET /api/user` | 匹配特定HTTP方法 |
|
||||
|
||||
|
@ -188,12 +188,13 @@ docker compose up -d
|
|||
```
|
||||
2. **权限码设计**:
|
||||
|
||||
- 模块::操作 (如 `user::create`)
|
||||
- 分层级设计 (如 `system:user:update`)
|
||||
- 模块::操作 (如 `user::create`)
|
||||
- 分层级设计 (如 `system:user:update`)
|
||||
|
||||
3. **批量操作**:
|
||||
- 使用 Excel/JSON 管理大量权限配置
|
||||
- 定期备份权限配置
|
||||
|
||||
- 使用 Excel/JSON 管理大量权限配置
|
||||
- 定期备份权限配置
|
||||
|
||||
## 🌟 项目优势
|
||||
|
||||
|
@ -222,24 +223,24 @@ docker compose up -d
|
|||
|
||||
### 前端示例规范
|
||||
|
||||
| **操作** | **API 层** | **Pinia 层** |
|
||||
| :------- | :------------ | :-------------- |
|
||||
| 查询单个 | `getUser` | `loadUser` |
|
||||
| 查询列表 | `getUserList` | `loadUserList` |
|
||||
| 分页查询 | `getUserPage` | `fetchUserPage` |
|
||||
| 新增数据 | `createUser` | `addUser` |
|
||||
| 更新数据 | `updateUser` | `editUser` |
|
||||
| 删除数据 | `deleteUser` | `removeUser` |
|
||||
| **操作** | **API 层** | **Pinia 层** |
|
||||
|:-------|:--------------|:----------------|
|
||||
| 查询单个 | `getUser` | `loadUser` |
|
||||
| 查询列表 | `getUserList` | `loadUserList` |
|
||||
| 分页查询 | `getUserPage` | `fetchUserPage` |
|
||||
| 新增数据 | `createUser` | `addUser` |
|
||||
| 更新数据 | `updateUser` | `editUser` |
|
||||
| 删除数据 | `deleteUser` | `removeUser` |
|
||||
|
||||
### 后端接口示例规范
|
||||
|
||||
遵循Restful
|
||||
|
||||
| **操作** | **RESTful** |
|
||||
| :------- | :-------------------------- |
|
||||
| 查询列表 | `GET /users` |
|
||||
| 分页查询 | `GET /users/{page}/{limit}` |
|
||||
| 查询单个 | `GET /users/{id}` |
|
||||
|:-------|:----------------------------|
|
||||
| 查询列表 | `GET /users` |
|
||||
| 分页查询 | `GET /users/{page}/{limit}` |
|
||||
| 查询单个 | `GET /users/{id}` |
|
||||
| 新增 | `POST /users` |
|
||||
| 更新 | `PUT /users/{id}` |
|
||||
| 删除 | `DELETE /users/{id}` |
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import { pathResolve } from './utils';
|
||||
import type { BuildOptions } from 'vite';
|
||||
|
||||
export const buildEnvironment = () => {
|
||||
const environment: BuildOptions = {
|
||||
target: 'es2015',
|
||||
assetsInlineLimit: 20000,
|
||||
// 构建输出的目录,默认值为"dist"
|
||||
outDir: 'docker/dist',
|
||||
// 用于指定使用的代码压缩工具。在这里,minify 被设置为 'terser',表示使用 Terser 进行代码压缩。默认值terser
|
||||
// esbuild 打包更快,但是不能去除 console.log,terser打包慢,但能去除 console.log
|
||||
minify: 'terser',
|
||||
// 用于配置 Terser 的选项
|
||||
terserOptions: {
|
||||
// 用于配置压缩选项
|
||||
compress: {
|
||||
drop_console: true, // 是否删除代码中的 console 语句, 默认值false
|
||||
drop_debugger: true, // 是否删除代码中的 debugger 语句, 默认值false
|
||||
},
|
||||
},
|
||||
// 禁用 gzip 压缩大小报告,可略微减少打包时间
|
||||
reportCompressedSize: false,
|
||||
// 用于指定是否生成源映射文件。源映射文件可以帮助调试和定位源代码中的错误。当设置为false时,构建过程不会生成源映射文件
|
||||
sourcemap: false,
|
||||
// 用于配置 CommonJS 模块的选项
|
||||
commonjsOptions: {
|
||||
// 用于指定是否忽略 CommonJS 模块中的 try-catch 语句。当设置为false时,构建过程会保留 CommonJS 模块中的 try-catch 语句
|
||||
ignoreTryCatch: false,
|
||||
},
|
||||
// 规定触发警告的 chunk 大小, 当某个代码分块的大小超过该限制时,Vite 会发出警告
|
||||
chunkSizeWarningLimit: 2000,
|
||||
rollupOptions: {
|
||||
external: ['md-editor-v3', 'echarts'],
|
||||
input: {
|
||||
index: pathResolve('../index.html', import.meta.url),
|
||||
},
|
||||
// 静态资源分类打包
|
||||
output: {
|
||||
chunkFileNames: 'static/js/[name]-[hash].js',
|
||||
entryFileNames: 'static/js/[name]-[hash].js',
|
||||
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
|
||||
manualChunks: id => {
|
||||
// 如果是包含在包中则打包成 vendor
|
||||
if (id.includes('node_modules')) {
|
||||
return `vendor`;
|
||||
// return id.toString().split('node_modules/')[1].split('/')[1].toString();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return environment;
|
||||
};
|
116
build/cdn.ts
|
@ -1,4 +1,4 @@
|
|||
import { Plugin as importToCDN } from 'vite-plugin-cdn-import';
|
||||
import { Plugin as importToCDN } from "vite-plugin-cdn-import";
|
||||
|
||||
/**
|
||||
* @description 打包时采用`cdn`模式,仅限外网使用(默认不采用,如果需要采用cdn模式,请在 .env.production 文件,将 VITE_CDN 设置成true)
|
||||
|
@ -6,67 +6,55 @@ import { Plugin as importToCDN } from 'vite-plugin-cdn-import';
|
|||
* 注意:上面提到的仅限外网使用也不是完全肯定的,如果你们公司内网部署的有相关js、css文件,也可以将下面配置对应改一下,整一套内网版cdn
|
||||
*/
|
||||
export const cdn = importToCDN({
|
||||
//(prodUrl解释: name: 对应下面modules的name,version: 自动读取本地package.json中dependencies依赖中对应包的版本号,path: 对应下面modules的path,当然也可写完整路径,会替换prodUrl)
|
||||
// prodUrl: 'https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}',
|
||||
prodUrl: 'https://unpkg.com/{name}@{version}/{path}',
|
||||
modules: [
|
||||
{
|
||||
name: 'vue',
|
||||
var: 'Vue',
|
||||
path: 'dist/vue.global.prod.js',
|
||||
},
|
||||
{
|
||||
name: 'vue-router',
|
||||
var: 'VueRouter',
|
||||
path: 'dist/vue-router.global.js',
|
||||
},
|
||||
{
|
||||
name: 'vue-i18n',
|
||||
var: 'VueI18n',
|
||||
path: 'dist/vue-i18n.global.prod.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: 'lib/index.iife.js',
|
||||
},
|
||||
{
|
||||
name: 'pinia',
|
||||
var: 'Pinia',
|
||||
path: 'dist/pinia.iife.js',
|
||||
},
|
||||
{
|
||||
name: 'element-plus',
|
||||
var: 'ElementPlus',
|
||||
path: 'dist/index.full.js',
|
||||
css: 'dist/index.css',
|
||||
},
|
||||
{
|
||||
name: 'axios',
|
||||
var: 'axios',
|
||||
path: 'dist/axios.min.js',
|
||||
},
|
||||
{
|
||||
name: 'dayjs',
|
||||
var: 'dayjs',
|
||||
path: 'dayjs.min.js',
|
||||
},
|
||||
{
|
||||
name: 'echarts',
|
||||
var: 'echarts',
|
||||
path: 'dist/echarts.min.js',
|
||||
},
|
||||
{
|
||||
name: 'md-editor-v3',
|
||||
var: 'MdEditorV3',
|
||||
path: 'lib/umd/index.js',
|
||||
css: 'lib/style.css',
|
||||
},
|
||||
{
|
||||
name: 'pinyin-pro',
|
||||
var: 'pinyinPro',
|
||||
path: 'dist/index.mjs',
|
||||
},
|
||||
],
|
||||
//(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"
|
||||
},
|
||||
{
|
||||
name: "vue-i18n",
|
||||
var: "VueI18n",
|
||||
path: "vue-i18n.runtime.global.prod.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"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -1,57 +1,63 @@
|
|||
import type { Plugin } from 'vite';
|
||||
import { isArray } from '@pureadmin/utils';
|
||||
import compressPlugin from 'vite-plugin-compression';
|
||||
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;
|
||||
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 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 codeList = [
|
||||
{ k: "gzip", v: gz },
|
||||
{ k: "brotli", v: br },
|
||||
{ k: "both", v: [gz, br] }
|
||||
];
|
||||
|
||||
const plugins: Plugin[] = [];
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
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;
|
||||
return plugins;
|
||||
};
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
import type { Plugin } from 'vite';
|
||||
import { getPackageSize } from './utils';
|
||||
import boxen, { type Options as BoxenOptions } from 'boxen';
|
||||
import dayjs, { type Dayjs } from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import gradientString from 'gradient-string';
|
||||
import boxen, { type Options as BoxenOptions } from 'boxen';
|
||||
|
||||
import gradient from 'gradient-string';
|
||||
import type { Plugin } from 'vite';
|
||||
import { getPackageSize } from './utils';
|
||||
dayjs.extend(duration);
|
||||
|
||||
const welcomeMessage = (VITE_PORT: number) => {
|
||||
return gradientString('cyan', 'magenta').multiline(
|
||||
`您好! 欢迎使用 bunny 系列开发模板项目访问地址如下:
|
||||
http://localhost:${VITE_PORT}`
|
||||
const welcomeMessage = (VITE_PORT: number) =>
|
||||
gradient(['cyan', 'magenta']).multiline(
|
||||
`您好! 欢迎使用 bunny 系列开发模板项目访问地址如下:\nhttp://localhost:${VITE_PORT}
|
||||
pure-admin 开源项目保姆级文档:\nhttps://pure-admin.cn`
|
||||
);
|
||||
};
|
||||
|
||||
const boxenOptions: BoxenOptions = {
|
||||
padding: 0.5,
|
||||
|
@ -45,7 +43,7 @@ export function viteBuildInfo(VITE_PORT: number): Plugin {
|
|||
callback: (size: string) => {
|
||||
console.log(
|
||||
boxen(
|
||||
gradientString('cyan', 'magenta').multiline(
|
||||
gradient(['cyan', 'magenta']).multiline(
|
||||
`🎉 恭喜打包完成(总用时${dayjs.duration(endTime.diff(startTime)).format('mm分ss秒')},打包后的大小为${size})`
|
||||
),
|
||||
boxenOptions
|
||||
|
|
|
@ -4,12 +4,61 @@
|
|||
* 尤其当您禁用浏览器缓存时(这种情况只应该发生在调试阶段)必须将对应模块加入到 include里,否则会遇到开发环境切换页面卡顿的问题(vite 会认为它是一个新的依赖包会重新加载并强制刷新页面),因为它既无法使用浏览器缓存,又没有在本地 node_modules/.vite 里缓存
|
||||
* 温馨提示:如果您使用的第三方库是全局引入,也就是引入到 src/main.ts 文件里,就不需要再添加到 include 里了,因为 vite 会自动将它们缓存到 node_modules/.vite
|
||||
*/
|
||||
const include = ['qs', 'mitt', 'dayjs', 'axios', 'pinia', 'vue-i18n', 'vue-types', 'js-cookie', 'vue-tippy', 'pinyin-pro', 'sortablejs', '@vueuse/core', '@pureadmin/utils', 'responsive-storage'];
|
||||
const include = [
|
||||
"qs",
|
||||
"mitt",
|
||||
"xlsx",
|
||||
"dayjs",
|
||||
"axios",
|
||||
"pinia",
|
||||
"vditor",
|
||||
"typeit",
|
||||
"swiper",
|
||||
"qrcode",
|
||||
"intro.js",
|
||||
"vue-i18n",
|
||||
"deep-chat",
|
||||
"vxe-table",
|
||||
"vue-types",
|
||||
"js-cookie",
|
||||
"vue-tippy",
|
||||
"cropperjs",
|
||||
"jsbarcode",
|
||||
"codemirror",
|
||||
"pinyin-pro",
|
||||
"sortablejs",
|
||||
"swiper/vue",
|
||||
"mint-filter",
|
||||
"highlight.js",
|
||||
"@vueuse/core",
|
||||
"vue3-danmaku",
|
||||
"v-contextmenu",
|
||||
"vue-pdf-embed",
|
||||
"wavesurfer.js",
|
||||
"swiper/modules",
|
||||
"china-area-data",
|
||||
"vue-json-pretty",
|
||||
"@logicflow/core",
|
||||
"@pureadmin/utils",
|
||||
"@wangeditor/editor",
|
||||
"responsive-storage",
|
||||
"plus-pro-components",
|
||||
"@howdyjs/mouse-menu",
|
||||
"@logicflow/extension",
|
||||
"vue-virtual-scroller",
|
||||
"codemirror-editor-vue3",
|
||||
"@amap/amap-jsapi-loader",
|
||||
"el-table-infinite-scroll",
|
||||
"vue-waterfall-plugin-next",
|
||||
"@infectoone/vue-ganttastic",
|
||||
"@wangeditor/editor-for-vue",
|
||||
"vuedraggable/src/vuedraggable"
|
||||
];
|
||||
|
||||
/**
|
||||
* 在预构建中强制排除的依赖项
|
||||
* 温馨提示:所有以 `@iconify-icons/` 开头引入的的本地图标模块,都应该加入到下面的 `exclude` 里,因为平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好
|
||||
* 温馨提示:平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好
|
||||
*/
|
||||
const exclude = ['@iconify-icons/ep', '@iconify-icons/ri', '@pureadmin/theme/dist/browser-utils'];
|
||||
const exclude = ["@iconify/json"];
|
||||
|
||||
export { include, exclude };
|
||||
|
|
|
@ -1,37 +1,44 @@
|
|||
import { cdn } from './cdn';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { pathResolve } from './utils';
|
||||
import { viteBuildInfo } from './info';
|
||||
import svgLoader from 'vite-svg-loader';
|
||||
import type { PluginOption } from 'vite';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import Inspector from 'vite-plugin-vue-inspector';
|
||||
import { configCompressPlugin } from './compress';
|
||||
import removeNoMatch from 'vite-plugin-router-warn';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import removeConsole from 'vite-plugin-remove-console';
|
||||
import { themePreprocessorPlugin } from '@pureadmin/theme';
|
||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
|
||||
import { genScssMultipleScopeVars } from '../src/layout/theme';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import { codeInspectorPlugin } from 'code-inspector-plugin';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import Icons from 'unplugin-icons/vite';
|
||||
import type { PluginOption } from 'vite';
|
||||
import removeConsole from 'vite-plugin-remove-console';
|
||||
import removeNoMatch from 'vite-plugin-router-warn';
|
||||
import svgLoader from 'vite-svg-loader';
|
||||
import { cdn } from './cdn';
|
||||
import { configCompressPlugin } from './compress';
|
||||
import { viteBuildInfo } from './info';
|
||||
|
||||
// import { vitePluginFakeServer } from 'vite-plugin-fake-server';
|
||||
|
||||
export function getPluginsList(
|
||||
VITE_CDN: boolean,
|
||||
VITE_COMPRESSION: ViteCompression,
|
||||
VITE_PORT: number
|
||||
): PluginOption[] {
|
||||
export function getPluginsList(VITE_CDN: boolean, VITE_COMPRESSION: ViteCompression, VITE_PORT: number): PluginOption[] {
|
||||
const lifecycle = process.env.npm_lifecycle_event;
|
||||
return [
|
||||
vue(),
|
||||
tailwindcss(),
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag === 'deep-chat',
|
||||
},
|
||||
},
|
||||
}),
|
||||
// jsx、tsx语法支持
|
||||
vueJsx(),
|
||||
VueI18nPlugin({
|
||||
jitCompilation: false,
|
||||
// include: [pathResolve('../locales/**')],
|
||||
// include: [pathResolve("../locales/**")]
|
||||
}),
|
||||
/**
|
||||
* 在页面上按住组合键时,鼠标在页面移动即会在 DOM 上出现遮罩层并显示相关信息,点击一下将自动打开 IDE 并将光标定位到元素对应的代码位置
|
||||
* Mac 默认组合键 Option + Shift
|
||||
* Windows 默认组合键 Alt + Shift
|
||||
* 更多用法看 https://inspector.fe-dev.cn/guide/start.html
|
||||
*/
|
||||
codeInspectorPlugin({
|
||||
bundler: 'vite',
|
||||
hideConsole: true,
|
||||
}),
|
||||
// 按下Command(⌘)+Shift(⇧),然后点击页面元素会自动打开本地IDE并跳转到对应的代码位置
|
||||
Inspector(),
|
||||
viteBuildInfo(VITE_PORT),
|
||||
/**
|
||||
* 开发环境下移除非必要的vue-router动态路由警告No match found for location with path
|
||||
|
@ -39,22 +46,20 @@ export function getPluginsList(
|
|||
* vite-plugin-router-warn只在开发环境下启用,只处理vue-router文件并且只在服务启动或重启时运行一次,性能消耗可忽略不计
|
||||
*/
|
||||
removeNoMatch(),
|
||||
// // mock支持
|
||||
// mock支持
|
||||
// vitePluginFakeServer({
|
||||
// logger: false,
|
||||
// include: 'mock',
|
||||
// infixName: false,
|
||||
// enableProd: true,// 线上支持mock
|
||||
// logger: false,
|
||||
// include: "mock",
|
||||
// infixName: false,
|
||||
// enableProd: true
|
||||
// }),
|
||||
// 自定义主题
|
||||
themePreprocessorPlugin({
|
||||
scss: {
|
||||
multipleScopeVars: genScssMultipleScopeVars(),
|
||||
extract: true,
|
||||
},
|
||||
}),
|
||||
// svg组件化支持
|
||||
svgLoader(),
|
||||
// 自动按需加载图标
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
scale: 1,
|
||||
}),
|
||||
VITE_CDN ? cdn : null,
|
||||
configCompressPlugin(VITE_COMPRESSION),
|
||||
// 线上环境删除console
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import { loadEnv, type ServerOptions } from 'vite';
|
||||
import { root, wrapperEnv } from './utils';
|
||||
|
||||
export const serverOptions = (mode: string) => {
|
||||
const { VITE_PORT, VITE_APP_URL } = wrapperEnv(loadEnv(mode, root));
|
||||
|
||||
const options: ServerOptions = {
|
||||
port: VITE_PORT, // ? 端口号
|
||||
host: '0.0.0.0',
|
||||
open: true,
|
||||
cors: true,
|
||||
proxy: {
|
||||
'/api/v1/': {
|
||||
target: 'http://129.211.31.58:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/api': {
|
||||
target: VITE_APP_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/mock': {
|
||||
target: VITE_APP_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/mock/, '/mock'),
|
||||
},
|
||||
},
|
||||
// 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布
|
||||
warmup: {
|
||||
clientFiles: ['./index.html', './src/{views,components}/*'],
|
||||
},
|
||||
};
|
||||
|
||||
return options;
|
||||
};
|
149
build/utils.ts
|
@ -1,9 +1,15 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { readdir, stat } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { formatBytes, sum } from '@pureadmin/utils';
|
||||
import { dependencies, devDependencies, engines, name, version } from '../package.json';
|
||||
import dayjs from "dayjs";
|
||||
import { readdir, stat } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { sum, formatBytes } from "@pureadmin/utils";
|
||||
import {
|
||||
name,
|
||||
version,
|
||||
engines,
|
||||
dependencies,
|
||||
devDependencies
|
||||
} from "../package.json";
|
||||
|
||||
/** 启动`node`进程时所在工作目录的绝对路径 */
|
||||
const root: string = process.cwd();
|
||||
|
@ -13,91 +19,92 @@ const root: string = process.cwd();
|
|||
* @param dir 路径片段,默认`build`
|
||||
* @param metaUrl 模块的完整`url`,如果在`build`目录外调用必传`import.meta.url`
|
||||
*/
|
||||
const pathResolve = (dir = '.', metaUrl = import.meta.url) => {
|
||||
// 当前文件目录的绝对路径
|
||||
const currentFileDir = dirname(fileURLToPath(metaUrl));
|
||||
// build 目录的绝对路径
|
||||
const buildDir = resolve(currentFileDir, 'build');
|
||||
// 解析的绝对路径
|
||||
const resolvedPath = resolve(currentFileDir, dir);
|
||||
// 检查解析的绝对路径是否在 build 目录内
|
||||
if (resolvedPath.startsWith(buildDir)) {
|
||||
// 在 build 目录内,返回当前文件路径
|
||||
return fileURLToPath(metaUrl);
|
||||
}
|
||||
// 不在 build 目录内,返回解析后的绝对路径
|
||||
return resolvedPath;
|
||||
const pathResolve = (dir = ".", metaUrl = import.meta.url) => {
|
||||
// 当前文件目录的绝对路径
|
||||
const currentFileDir = dirname(fileURLToPath(metaUrl));
|
||||
// build 目录的绝对路径
|
||||
const buildDir = resolve(currentFileDir, "build");
|
||||
// 解析的绝对路径
|
||||
const resolvedPath = resolve(currentFileDir, dir);
|
||||
// 检查解析的绝对路径是否在 build 目录内
|
||||
if (resolvedPath.startsWith(buildDir)) {
|
||||
// 在 build 目录内,返回当前文件路径
|
||||
return fileURLToPath(metaUrl);
|
||||
}
|
||||
// 不在 build 目录内,返回解析后的绝对路径
|
||||
return resolvedPath;
|
||||
};
|
||||
|
||||
/** 设置别名 */
|
||||
const alias: Record<string, string> = {
|
||||
'@': pathResolve('../src'),
|
||||
'@build': pathResolve(),
|
||||
"@": pathResolve("../src"),
|
||||
"@build": pathResolve()
|
||||
};
|
||||
|
||||
/** 平台的名称、版本、运行所需的`node`和`pnpm`版本、依赖、最后构建时间的类型提示 */
|
||||
const __APP_INFO__ = {
|
||||
pkg: { name, version, engines, dependencies, devDependencies },
|
||||
lastBuildTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'),
|
||||
pkg: { name, version, engines, dependencies, devDependencies },
|
||||
lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")
|
||||
};
|
||||
|
||||
/** 处理环境变量 */
|
||||
const wrapperEnv = (envConf: Recordable): ViteEnv => {
|
||||
// 默认值
|
||||
const ret: ViteEnv = {
|
||||
VITE_PORT: 8848,
|
||||
VITE_PUBLIC_PATH: '',
|
||||
VITE_ROUTER_HISTORY: '',
|
||||
VITE_APP_URL: '',
|
||||
VITE_CDN: false,
|
||||
VITE_HIDE_HOME: 'false',
|
||||
VITE_COMPRESSION: 'none',
|
||||
};
|
||||
// 默认值
|
||||
const ret: ViteEnv = {
|
||||
VITE_PORT: 8848,
|
||||
VITE_PUBLIC_PATH: "",
|
||||
VITE_ROUTER_HISTORY: "",
|
||||
VITE_CDN: false,
|
||||
VITE_HIDE_HOME: "false",
|
||||
VITE_COMPRESSION: "none"
|
||||
};
|
||||
|
||||
for (const envName of Object.keys(envConf)) {
|
||||
let realName = envConf[envName].replace(/\\n/g, '\n');
|
||||
realName = realName === 'true' ? true : realName === 'false' ? false : realName;
|
||||
for (const envName of Object.keys(envConf)) {
|
||||
let realName = envConf[envName].replace(/\\n/g, "\n");
|
||||
realName =
|
||||
realName === "true" ? true : realName === "false" ? false : realName;
|
||||
|
||||
if (envName === 'VITE_PORT') {
|
||||
realName = Number(realName);
|
||||
}
|
||||
ret[envName] = realName;
|
||||
if (typeof realName === 'string') {
|
||||
process.env[envName] = realName;
|
||||
} else if (typeof realName === 'object') {
|
||||
process.env[envName] = JSON.stringify(realName);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
if (envName === "VITE_PORT") {
|
||||
realName = Number(realName);
|
||||
}
|
||||
ret[envName] = realName;
|
||||
if (typeof realName === "string") {
|
||||
process.env[envName] = realName;
|
||||
} else if (typeof realName === "object") {
|
||||
process.env[envName] = JSON.stringify(realName);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
const fileListTotal: number[] = [];
|
||||
|
||||
/** 获取指定文件夹中所有文件的总大小 */
|
||||
const getPackageSize = options => {
|
||||
const { folder = 'dist', callback, format = true } = options;
|
||||
readdir(folder, (err, files: string[]) => {
|
||||
if (err) throw err;
|
||||
let count = 0;
|
||||
const checkEnd = () => {
|
||||
++count == files.length && callback(format ? formatBytes(sum(fileListTotal)) : sum(fileListTotal));
|
||||
};
|
||||
files.forEach((item: string) => {
|
||||
stat(`${folder}/${item}`, async (err, stats) => {
|
||||
if (err) throw err;
|
||||
if (stats.isFile()) {
|
||||
fileListTotal.push(stats.size);
|
||||
checkEnd();
|
||||
} else if (stats.isDirectory()) {
|
||||
getPackageSize({
|
||||
folder: `${folder}/${item}/`,
|
||||
callback: checkEnd,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
files.length === 0 && callback(0);
|
||||
});
|
||||
const { folder = "dist", callback, format = true } = options;
|
||||
readdir(folder, (err, files: string[]) => {
|
||||
if (err) throw err;
|
||||
let count = 0;
|
||||
const checkEnd = () => {
|
||||
++count == files.length &&
|
||||
callback(format ? formatBytes(sum(fileListTotal)) : sum(fileListTotal));
|
||||
};
|
||||
files.forEach((item: string) => {
|
||||
stat(`${folder}/${item}`, async (err, stats) => {
|
||||
if (err) throw err;
|
||||
if (stats.isFile()) {
|
||||
fileListTotal.push(stats.size);
|
||||
checkEnd();
|
||||
} else if (stats.isDirectory()) {
|
||||
getPackageSize({
|
||||
folder: `${folder}/${item}/`,
|
||||
callback: checkEnd
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
files.length === 0 && callback(0);
|
||||
});
|
||||
};
|
||||
|
||||
export { root, pathResolve, alias, __APP_INFO__, wrapperEnv, getPackageSize };
|
||||
|
|
23
index.html
|
@ -2,23 +2,14 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
|
||||
<meta content="webkit" name="renderer" />
|
||||
<!--
|
||||
如果遇到http和https混用问题,可以使将全部的htp都转成https 但是如果都不支持
|
||||
如果对方服务器可以支持https可以试下这个方案
|
||||
如果不支持需要修改代码,将请求修改成/api/xxx的形式之后使用NGINX做反向代理
|
||||
-->
|
||||
<!--<meta content="upgrade-insecure-requests" http-equiv="Content-Security-Policy" />-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<meta
|
||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||
/>
|
||||
<title>bunny-admin</title>
|
||||
<link href="/favicon.ico" rel="icon" />
|
||||
<script>
|
||||
window.process = {};
|
||||
</script>
|
||||
<title>vue-pure-admin</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -62,7 +53,7 @@
|
|||
.loader::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
content: '';
|
||||
content: "";
|
||||
}
|
||||
|
||||
.loader::before {
|
||||
|
@ -88,6 +79,6 @@
|
|||
</style>
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
<script src="/src/main.ts" type="module"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
222
package.json
|
@ -3,7 +3,23 @@
|
|||
"version": "4.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"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 && generate-version-file",
|
||||
"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 . -r",
|
||||
"clean:cache": "rimraf .eslintcache && rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install",
|
||||
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
|
||||
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
|
||||
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"keywords": [
|
||||
"bunny-auth-admin",
|
||||
"element-plus",
|
||||
|
@ -27,161 +43,159 @@
|
|||
"email": "1319900154@qq.com",
|
||||
"url": "https://github.com/BunnyMaster"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS=--max-old-space-size=1024 vite",
|
||||
"serve": "pnpm vite",
|
||||
"start": "vite",
|
||||
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build && generate-version-file",
|
||||
"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 . -r",
|
||||
"clean:cache": "rimraf .eslintcache && rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install",
|
||||
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
|
||||
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
|
||||
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@howdyjs/mouse-menu": "^2.1.3",
|
||||
"@howdyjs/mouse-menu": "^2.1.6",
|
||||
"@infectoone/vue-ganttastic": "^2.3.2",
|
||||
"@logicflow/core": "^1.2.28",
|
||||
"@logicflow/extension": "^1.2.28",
|
||||
"@pureadmin/descriptions": "^1.2.1",
|
||||
"@pureadmin/table": "^3.2.0",
|
||||
"@pureadmin/utils": "^2.4.8",
|
||||
"@vue-flow/background": "^1.3.0",
|
||||
"@vue-flow/core": "^1.41.0",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/motion": "^2.2.3",
|
||||
"@pureadmin/table": "^3.2.1",
|
||||
"@pureadmin/utils": "^2.6.0",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/core": "^1.42.5",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "1.8.2",
|
||||
"axios": "^1.8.4",
|
||||
"china-area-data": "^5.0.1",
|
||||
"codemirror": "^5.65.19",
|
||||
"codemirror-editor-vue3": "^2.8.0",
|
||||
"cropperjs": "^1.6.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"deep-chat": "^2.1.1",
|
||||
"echarts": "^5.6.0",
|
||||
"el-table-infinite-scroll": "^3.0.6",
|
||||
"element-plus": "2.7.1",
|
||||
"element-plus": "2.9.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"intro.js": "^7.2.0",
|
||||
"js-base64": "^3.7.7",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsbarcode": "^3.11.6",
|
||||
"localforage": "^1.10.0",
|
||||
"md-editor-v3": "^4.21.2",
|
||||
"md-editor-v3": "^5.5.0",
|
||||
"mint-filter": "^4.0.3",
|
||||
"mitt": "^3.0.1",
|
||||
"mqtt": "4.3.7",
|
||||
"nprogress": "^0.2.0",
|
||||
"path": "^0.12.7",
|
||||
"pinia": "^2.2.2",
|
||||
"pinia-plugin-persistedstate": "^3.2.3",
|
||||
"pinyin-pro": "^3.24.2",
|
||||
"plus-pro-components": "^0.1.14",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"plus-pro-components": "^0.1.22",
|
||||
"qrcode": "^1.5.4",
|
||||
"qs": "^6.13.0",
|
||||
"qs": "^6.14.0",
|
||||
"responsive-storage": "^2.2.0",
|
||||
"sortablejs": "^1.15.2",
|
||||
"swiper": "^11.1.11",
|
||||
"terser": "^5.31.6",
|
||||
"typeit": "^8.8.4",
|
||||
"sortablejs": "^1.15.6",
|
||||
"swiper": "^11.2.6",
|
||||
"typeit": "^8.8.7",
|
||||
"v-contextmenu": "^3.2.0",
|
||||
"v3-infinite-loading": "^1.3.2",
|
||||
"version-rocket": "^1.7.2",
|
||||
"vite-plugin-vue-inspector": "^5.1.3",
|
||||
"vue": "^3.4.38",
|
||||
"vue-i18n": "11.1.2",
|
||||
"vditor": "^3.11.0",
|
||||
"version-rocket": "^1.7.4",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-json-pretty": "^2.4.0",
|
||||
"vue-pdf-embed": "^2.1.0",
|
||||
"vue-router": "^4.4.3",
|
||||
"vue-tippy": "^6.4.4",
|
||||
"vue-types": "^5.1.3",
|
||||
"vue-pdf-embed": "^2.1.2",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-tippy": "^6.7.0",
|
||||
"vue-types": "^6.0.0",
|
||||
"vue-virtual-scroller": "2.0.0-beta.8",
|
||||
"vue-waterfall-plugin-next": "^2.6.0",
|
||||
"vue-waterfall-plugin-next": "^2.6.5",
|
||||
"vue3-danmaku": "^1.6.1",
|
||||
"vue3-puzzle-vcode": "^1.1.7",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vxe-table": "^4.6.18",
|
||||
"wavesurfer.js": "^7.8.4",
|
||||
"xgplayer": "^3.0.20",
|
||||
"vxe-table": "4.6.25",
|
||||
"wavesurfer.js": "^7.9.4",
|
||||
"xgplayer": "^3.0.21",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@iconify-icons/ep": "^1.2.12",
|
||||
"@iconify-icons/ri": "^1.2.10",
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@pureadmin/theme": "^3.2.0",
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@faker-js/faker": "^9.7.0",
|
||||
"@iconify/json": "^2.2.329",
|
||||
"@iconify/vue": "4.2.0",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.5",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@types/codemirror": "^5.60.15",
|
||||
"@types/dagre": "^0.7.52",
|
||||
"@types/gradient-string": "^1.1.6",
|
||||
"@types/intro.js": "^5.1.5",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^20.16.2",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/qs": "^6.9.15",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"boxen": "^7.1.1",
|
||||
"cssnano": "^7.0.5",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||
"boxen": "^8.0.1",
|
||||
"code-inspector-plugin": "^0.20.10",
|
||||
"cssnano": "^7.0.6",
|
||||
"dagre": "^0.8.5",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-define-config": "^2.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"gradient-string": "^2.0.2",
|
||||
"lint-staged": "^15.2.9",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss-html": "^1.7.0",
|
||||
"postcss-import": "^16.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"gradient-string": "^3.0.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-html": "^1.8.0",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"prettier": "^3.3.3",
|
||||
"rimraf": "^5.0.10",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.77.8",
|
||||
"stylelint": "^16.8.2",
|
||||
"stylelint-config-recess-order": "^5.1.0",
|
||||
"stylelint-config-recommended-vue": "^1.5.0",
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"stylelint-prettier": "^5.0.2",
|
||||
"prettier": "^3.5.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.86.3",
|
||||
"stylelint": "^16.18.0",
|
||||
"stylelint-config-recess-order": "^6.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.6.0",
|
||||
"stylelint-config-standard-scss": "^14.0.0",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"svgo": "^3.3.2",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.2",
|
||||
"vite-plugin-cdn-import": "^0.3.5",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"vite": "^6.3.2",
|
||||
"vite-plugin-cdn-import": "^1.0.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-fake-server": "^2.2.0",
|
||||
"vite-plugin-remove-console": "^2.2.0",
|
||||
"vite-plugin-router-warn": "^1.0.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue-eslint-parser": "^9.4.3",
|
||||
"vue-tsc": "^1.8.27"
|
||||
"vue-eslint-parser": "^10.1.3",
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
|
||||
"pnpm": ">=8.6.10"
|
||||
"node": "^18.18.0 || ^20.9.0 || >=22.0.0",
|
||||
"pnpm": ">=9"
|
||||
},
|
||||
"pnpm": {
|
||||
"allowedDeprecatedVersions": {
|
||||
"are-we-there-yet": "*",
|
||||
"sourcemap-codec": "*",
|
||||
"lodash.isequal": "*",
|
||||
"domexception": "*",
|
||||
"w3c-hr-time": "*",
|
||||
"inflight": "*",
|
||||
"npmlog": "*",
|
||||
"rimraf": "*",
|
||||
"stable": "*",
|
||||
"abab": "*"
|
||||
"gauge": "*",
|
||||
"abab": "*",
|
||||
"glob": "*"
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"eslint": "9"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677"
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"core-js",
|
||||
"es5-ext",
|
||||
"esbuild",
|
||||
"typeit",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
9439
pnpm-lock.yaml
|
@ -1,12 +1,8 @@
|
|||
// @ts-check
|
||||
|
||||
/** @type {import("postcss-load-config").Config} */
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
export default {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
|
||||
},
|
||||
plugins: {
|
||||
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
|
||||
}
|
||||
};
|
||||
|
|
55
src/App.vue
|
@ -2,20 +2,23 @@
|
|||
<el-config-provider :locale="currentLocale">
|
||||
<router-view />
|
||||
<ReDialog />
|
||||
<ReDrawer />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeMount, onMounted } from 'vue';
|
||||
import { checkVersion } from 'version-rocket';
|
||||
import { ElConfigProvider } from 'element-plus';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { ReDialog } from '@/components/ReDialog';
|
||||
import { ReDrawer } from '@/components/ReDrawer';
|
||||
import en from 'element-plus/es/locale/lang/en';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import plusEn from 'plus-pro-components/es/locale/lang/en';
|
||||
import plusZhCn from 'plus-pro-components/es/locale/lang/zh-cn';
|
||||
import { userI18nStore } from '@/store/i18n/i18n';
|
||||
import { useNav } from '@/layout/hooks/useNav';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { userI18nStore } from '@/store/i18n/i18n';
|
||||
|
||||
const i18nStore = userI18nStore();
|
||||
const i18n = useI18n();
|
||||
|
@ -50,30 +53,30 @@ const currentLocale = computed(() => {
|
|||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 设置多语言
|
||||
setI18n();
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
const { version, name: title } = __APP_INFO__.pkg;
|
||||
const { VITE_PUBLIC_PATH, MODE } = import.meta.env;
|
||||
// https://github.com/guMcrey/version-rocket/blob/main/README.zh-CN.md#api
|
||||
if (MODE === 'production') {
|
||||
// 版本实时更新检测,只作用于线上环境
|
||||
checkVersion(
|
||||
// config
|
||||
{
|
||||
// 5分钟检测一次版本
|
||||
pollingTime: 300000,
|
||||
localPackageVersion: version,
|
||||
originVersionFileUrl: `${location.origin}${VITE_PUBLIC_PATH}version.json`,
|
||||
},
|
||||
// options
|
||||
{
|
||||
title,
|
||||
description: '检测到新版本',
|
||||
buttonText: '立即更新',
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 定义滚动条高宽及背景高宽分别对应横竖滚动条的尺寸 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
/* 定义滚动条轨道内阴影+圆角 */
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #ebecef;
|
||||
border-radius: 5px;
|
||||
box-shadow: inset 0 0 6px #ebecef;
|
||||
}
|
||||
|
||||
/* 定义滑块内阴影+圆角 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #d0d2d6;
|
||||
border-radius: 5px;
|
||||
box-shadow: inset 0 0 6px #d0d2d6;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,12 +6,12 @@ export const whiteList = ['/refresh-token', '/login'];
|
|||
// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
|
||||
export const defaultConfig: AxiosRequestConfig = {
|
||||
// 默认请求地址
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
baseURL: '/api',
|
||||
// 设置超时时间
|
||||
timeout: import.meta.env.VITE_BASE_API_TIMEOUT,
|
||||
timeout: 6000,
|
||||
// @ts-expect-error
|
||||
retry: import.meta.env.VITE_BASE_API_RETRY, //设置全局重试请求次数(最多重试几次请求)
|
||||
retryDelay: import.meta.env.VITE_BASE_API_RETRY_DELAY, //设置全局请求间隔
|
||||
retry: 3, //设置全局重试请求次数(最多重试几次请求)
|
||||
retryDelay: 3000, //设置全局请求间隔
|
||||
// 跨域允许携带凭证
|
||||
// withCredentials: true,
|
||||
headers: {
|
||||
|
|
|
@ -34,7 +34,12 @@ class PureHttp {
|
|||
|
||||
/** 通用请求工具函数 */
|
||||
public request<T>(method: RequestMethods, url: string, param?: AxiosRequestConfig, axiosConfig?: PureHttpRequestConfig): Promise<T> {
|
||||
const config = { method, url, ...param, ...axiosConfig } as PureHttpRequestConfig;
|
||||
const config = {
|
||||
method,
|
||||
url,
|
||||
...param,
|
||||
...axiosConfig,
|
||||
} as PureHttpRequestConfig;
|
||||
|
||||
// 单独处理自定义请求/响应回调
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
@ -24,7 +24,9 @@ export const deleteFiles = (data: any) => {
|
|||
|
||||
/** 系统文件管理---根据文件id下载文件 */
|
||||
export const downloadFilesByFileId = (data: any) => {
|
||||
return http.request<any>('get', `files/file/${data.id}`, { responseType: 'blob' });
|
||||
return http.request<any>('get', `files/file/${data.id}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
};
|
||||
|
||||
/** 系统文件管理---获取所有文件类型 */
|
||||
|
|
|
@ -10,5 +10,7 @@ export const getScheduleExecuteLogPage = (data: any) => {
|
|||
|
||||
/** 调度任务执行日志---删除调度任务执行日志 */
|
||||
export const deleteScheduleExecuteLog = (data: any) => {
|
||||
return http.request<BaseResult<object>>('delete', 'scheduleExecuteLog', { data });
|
||||
return http.request<BaseResult<object>>('delete', 'scheduleExecuteLog', {
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -18,7 +18,9 @@ export const createRouter = (data?: any) => {
|
|||
|
||||
/** 菜单管理-清除选中菜单所有角色 */
|
||||
export const clearRouterRole = (data: any) => {
|
||||
return http.request<BaseResult<any>>('delete', `routerRole/clearRouterRole`, { data });
|
||||
return http.request<BaseResult<any>>('delete', `routerRole/clearRouterRole`, {
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
/** 菜单管理-更新菜单 */
|
||||
|
|
|
@ -25,5 +25,7 @@ export const deleteMenuIcon = (data: any) => {
|
|||
|
||||
/** 系统菜单图标---根据名称搜索图标 */
|
||||
export const getIconNameListByIconName = (data: any) => {
|
||||
return http.request<BaseResult<object>>('get', 'menuIcon/public', { params: data });
|
||||
return http.request<BaseResult<object>>('get', 'menuIcon/public', {
|
||||
params: data,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -15,7 +15,9 @@ export const updateMessageReceivedByAdmin = (data: any) => {
|
|||
|
||||
/** 管理员操作用户消息---管理删除用户消息 */
|
||||
export const deleteMessageReceivedByAdmin = (data: any) => {
|
||||
return http.request<BaseResult<object>>('delete', 'messageReceived', { data });
|
||||
return http.request<BaseResult<object>>('delete', 'messageReceived', {
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
/** 用户系统消息---用户获取系统消息列表 */
|
||||
|
|
|
@ -20,7 +20,9 @@ export const updateSchedulersGroup = (data: any) => {
|
|||
|
||||
/** 任务调度分组---删除任务调度分组 */
|
||||
export const deleteSchedulersGroup = (data: any) => {
|
||||
return http.request<BaseResult<object>>('delete', 'schedulersGroup', { data });
|
||||
return http.request<BaseResult<object>>('delete', 'schedulersGroup', {
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
/** 任务调度分组---获取所有任务调度分组 */
|
||||
|
|
|
@ -67,7 +67,9 @@ export const getUserListByKeyword = (data: any) => {
|
|||
|
||||
/** 用户管理---强制用户下线 */
|
||||
export const forcedOfflineByAdmin = (data: any) => {
|
||||
return http.request<BaseResult<UserResult>>('put', 'user/forcedOffline', { data });
|
||||
return http.request<BaseResult<UserResult>>('put', 'user/forcedOffline', {
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
// -----------------------------------------
|
||||
|
|
|
@ -29,7 +29,9 @@ export const getRoleList = () => {
|
|||
|
||||
/** 角色---使用Excel导出导出角色列表 */
|
||||
export const exportRoleList = () => {
|
||||
return http.request<BaseResult<any>>('get', `role/file/export`, { responseType: 'blob' });
|
||||
return http.request<BaseResult<any>>('get', `role/file/export`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
};
|
||||
|
||||
/* 角色---使用Excel更新角色列表 */
|
||||
|
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -26,8 +26,7 @@
|
|||
c = document.createElement('div');
|
||||
(c.innerHTML = e._iconfont_svg_string_2208059),
|
||||
(c = c.getElementsByTagName('svg')[0]) &&
|
||||
(c.setAttribute('aria-hidden', 'false'),
|
||||
(c.style.position = 'absolute'),
|
||||
((c.style.position = 'absolute'),
|
||||
(c.style.width = 0),
|
||||
(c.style.height = 0),
|
||||
(c.style.overflow = 'hidden'),
|
||||
|
|
|
@ -1,4 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="false" 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-8"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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-8"/></svg>
|
Before Width: | Height: | Size: 365 B After Width: | Height: | Size: 332 B |
|
@ -1,4 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="false" class="re-screen" color="#00000073" viewBox="0 0 16 16">
|
||||
<path fill="currentColor"
|
||||
d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/></svg>
|
Before Width: | Height: | Size: 341 B After Width: | Height: | Size: 308 B |
|
@ -1,3 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="false" class="re-screen" color="#00000073" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/></svg>
|
Before Width: | Height: | Size: 308 B After Width: | Height: | Size: 283 B |
|
@ -1,4 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="false" class="globalization" viewBox="0 0 512 512">
|
||||
<path fill="currentColor"
|
||||
d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" class="globalization" viewBox="0 0 512 512"><path fill="currentColor" d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"/></svg>
|
Before Width: | Height: | Size: 840 B After Width: | Height: | Size: 807 B |
|
@ -1,4 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="false" class="iconify iconify--mdi" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 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-2"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 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-2"/></svg>
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 360 B |
|
@ -1,4 +1 @@
|
|||
<svg width="32" height="32" fill="currentColor" aria-hidden="false" data-icon="holder" viewBox="64 64 896 896">
|
||||
<path
|
||||
d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/>
|
||||
</svg>
|
||||
<svg width="32" height="32" fill="currentColor" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/></svg>
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 373 B |
|
@ -0,0 +1,11 @@
|
|||
import reNormalCountTo from './src/normal';
|
||||
import reboundCountTo from './src/rebound';
|
||||
import { withInstall } from '@pureadmin/utils';
|
||||
|
||||
/** 普通数字动画组件 */
|
||||
const ReNormalCountTo = withInstall(reNormalCountTo);
|
||||
|
||||
/** 回弹式数字动画组件 */
|
||||
const ReboundCountTo = withInstall(reboundCountTo);
|
||||
|
||||
export { ReNormalCountTo, ReboundCountTo };
|
|
@ -0,0 +1,152 @@
|
|||
import { computed, defineComponent, onMounted, reactive, unref, watch } from 'vue';
|
||||
import { countToProps } from './props';
|
||||
import { isNumber } from '@pureadmin/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ReNormalCountTo',
|
||||
props: countToProps,
|
||||
emits: ['mounted', 'callback'],
|
||||
setup(props, { emit }) {
|
||||
const state = reactive<{
|
||||
localStartVal: number;
|
||||
printVal: number | null;
|
||||
displayValue: string;
|
||||
paused: boolean;
|
||||
localDuration: number | null;
|
||||
startTime: number | null;
|
||||
timestamp: number | null;
|
||||
rAF: any;
|
||||
remaining: number | null;
|
||||
color: string;
|
||||
fontSize: string;
|
||||
}>({
|
||||
localStartVal: props.startVal,
|
||||
displayValue: formatNumber(props.startVal),
|
||||
printVal: null,
|
||||
paused: false,
|
||||
localDuration: props.duration,
|
||||
startTime: null,
|
||||
timestamp: null,
|
||||
remaining: null,
|
||||
rAF: null,
|
||||
color: null,
|
||||
fontSize: '16px',
|
||||
});
|
||||
|
||||
const getCountDown = computed(() => {
|
||||
return props.startVal > props.endVal;
|
||||
});
|
||||
|
||||
watch([() => props.startVal, () => props.endVal], () => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
|
||||
function start() {
|
||||
const { startVal, duration, color, fontSize } = props;
|
||||
state.localStartVal = startVal;
|
||||
state.startTime = null;
|
||||
state.localDuration = duration;
|
||||
state.paused = false;
|
||||
state.color = color;
|
||||
state.fontSize = fontSize;
|
||||
state.rAF = requestAnimationFrame(count);
|
||||
}
|
||||
|
||||
function pauseResume() {
|
||||
if (state.paused) {
|
||||
resume();
|
||||
state.paused = false;
|
||||
} else {
|
||||
pause();
|
||||
state.paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
function pause() {
|
||||
cancelAnimationFrame(state.rAF);
|
||||
}
|
||||
|
||||
function resume() {
|
||||
state.startTime = null;
|
||||
state.localDuration = +(state.remaining as number);
|
||||
state.localStartVal = +(state.printVal as number);
|
||||
requestAnimationFrame(count);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
state.startTime = null;
|
||||
cancelAnimationFrame(state.rAF);
|
||||
state.displayValue = formatNumber(props.startVal);
|
||||
}
|
||||
|
||||
function count(timestamp: number) {
|
||||
const { useEasing, easingFn, endVal } = props;
|
||||
if (!state.startTime) state.startTime = timestamp;
|
||||
state.timestamp = timestamp;
|
||||
const progress = timestamp - state.startTime;
|
||||
state.remaining = (state.localDuration as number) - progress;
|
||||
if (useEasing) {
|
||||
if (unref(getCountDown)) {
|
||||
state.printVal = state.localStartVal - easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number);
|
||||
} else {
|
||||
state.printVal = easingFn(progress, state.localStartVal, endVal - state.localStartVal, state.localDuration as number);
|
||||
}
|
||||
} else {
|
||||
if (unref(getCountDown)) {
|
||||
state.printVal = state.localStartVal - (state.localStartVal - endVal) * (progress / (state.localDuration as number));
|
||||
} else {
|
||||
state.printVal = state.localStartVal + (endVal - state.localStartVal) * (progress / (state.localDuration as number));
|
||||
}
|
||||
}
|
||||
if (unref(getCountDown)) {
|
||||
state.printVal = state.printVal < endVal ? endVal : state.printVal;
|
||||
} else {
|
||||
state.printVal = state.printVal > endVal ? endVal : state.printVal;
|
||||
}
|
||||
state.displayValue = formatNumber(state.printVal);
|
||||
if (progress < (state.localDuration as number)) {
|
||||
state.rAF = requestAnimationFrame(count);
|
||||
} else {
|
||||
emit('callback');
|
||||
}
|
||||
}
|
||||
|
||||
function formatNumber(num: number | string) {
|
||||
const { decimals, decimal, separator, suffix, prefix } = props;
|
||||
num = Number(num).toFixed(decimals);
|
||||
num += '';
|
||||
const x = num.split('.');
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? decimal + x[1] : '';
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
if (separator && !isNumber(separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + separator + '$2');
|
||||
}
|
||||
}
|
||||
return prefix + x1 + x2 + suffix;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
emit('mounted');
|
||||
});
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
color: props.color,
|
||||
fontSize: props.fontSize,
|
||||
}}
|
||||
>
|
||||
{state.displayValue}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import type { PropType } from 'vue';
|
||||
import propTypes from '@/utils/propTypes';
|
||||
|
||||
export const countToProps = {
|
||||
startVal: propTypes.number.def(0),
|
||||
endVal: propTypes.number.def(2020),
|
||||
duration: propTypes.number.def(1300),
|
||||
autoplay: propTypes.bool.def(true),
|
||||
decimals: {
|
||||
type: Number as PropType<number>,
|
||||
required: false,
|
||||
default: 0,
|
||||
validator(value: number) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
color: propTypes.string.def(),
|
||||
fontSize: propTypes.string.def(),
|
||||
decimal: propTypes.string.def('.'),
|
||||
separator: propTypes.string.def(','),
|
||||
prefix: propTypes.string.def(''),
|
||||
suffix: propTypes.string.def(''),
|
||||
useEasing: propTypes.bool.def(true),
|
||||
easingFn: {
|
||||
type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
|
||||
default(t: number, b: number, c: number, d: number) {
|
||||
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
import './rebound.css';
|
||||
import { defineComponent, onBeforeMount, onBeforeUnmount, ref, unref } from 'vue';
|
||||
import { reboundProps } from './props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ReboundCountTo',
|
||||
props: reboundProps,
|
||||
setup(props) {
|
||||
const ulRef = ref();
|
||||
const timer = ref(null);
|
||||
|
||||
onBeforeMount(() => {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const testUA = (regexp) => regexp.test(ua);
|
||||
const isSafari = testUA(/safari/g) && !testUA(/chrome/g);
|
||||
|
||||
// Safari浏览器的兼容代码
|
||||
isSafari &&
|
||||
(timer.value = setTimeout(() => {
|
||||
ulRef.value.setAttribute(
|
||||
'style',
|
||||
`
|
||||
animation: none;
|
||||
transform: translateY(calc(var(--i) * -9.09%))
|
||||
`
|
||||
);
|
||||
}, props.delay * 1000));
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(unref(timer));
|
||||
});
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div class="scroll-num" style={{ '--i': props.i, '--delay': props.delay }}>
|
||||
<ul ref="ulRef" style={{ fontSize: '32px' }}>
|
||||
<li>0</li>
|
||||
<li>1</li>
|
||||
<li>2</li>
|
||||
<li>3</li>
|
||||
<li>4</li>
|
||||
<li>5</li>
|
||||
<li>6</li>
|
||||
<li>7</li>
|
||||
<li>8</li>
|
||||
<li>9</li>
|
||||
<li>0</li>
|
||||
</ul>
|
||||
|
||||
<svg width="0" height="0">
|
||||
<filter id="blur">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation={`0 ${props.blur}`} />
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import type { PropType } from 'vue';
|
||||
import propTypes from '@/utils/propTypes';
|
||||
|
||||
export const reboundProps = {
|
||||
delay: propTypes.number.def(1),
|
||||
blur: propTypes.number.def(2),
|
||||
i: {
|
||||
type: Number as PropType<number>,
|
||||
required: false,
|
||||
default: 0,
|
||||
validator(value: number) {
|
||||
return value < 10 && value >= 0 && Number.isInteger(value);
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
.scroll-num {
|
||||
animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards;
|
||||
color: var(--color, #333);
|
||||
font-size: var(--height, calc(var(--width, 20px) * 1.1));
|
||||
height: var(--height, calc(var(--width, 20px) * 1.8));
|
||||
line-height: var(--height, calc(var(--width, 20px) * 1.8));
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
width: var(--width, 20px);
|
||||
}
|
||||
|
||||
ul {
|
||||
animation:
|
||||
move 0.3s linear infinite,
|
||||
bounce-in-down 1s calc(var(--delay) * 1s) forwards;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
from {
|
||||
transform: translateY(-90%);
|
||||
filter: url(#blur);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(1%);
|
||||
filter: url(#blur);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce-in-down {
|
||||
from {
|
||||
transform: translateY(calc(var(--i) * -9.09% - 7%));
|
||||
filter: none;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateY(calc(var(--i) * -9.09% + 3%));
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(calc(var(--i) * -9.09% - 1%));
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translateY(calc(var(--i) * -9.09% + 0.6%));
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: translateY(calc(var(--i) * -9.09% - 0.3%));
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(calc(var(--i) * -9.09%));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes enhance-bounce-in-down {
|
||||
25% {
|
||||
transform: translateY(8%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-4%);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translateY(2%);
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: translateY(-1%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import reCropperPreview from './src/index.vue';
|
||||
import { withInstall } from '@pureadmin/utils';
|
||||
|
||||
/** 图片裁剪预览组件 */
|
||||
export const ReCropperPreview = withInstall(reCropperPreview);
|
||||
|
||||
export default ReCropperPreview;
|
|
@ -0,0 +1,65 @@
|
|||
<script lang="tsx" setup>
|
||||
import { ref } from 'vue';
|
||||
import ReCropper from '@/components/ReCropper';
|
||||
import { formatBytes } from '@pureadmin/utils';
|
||||
import { $t } from '@/plugins/i18n';
|
||||
|
||||
defineProps({
|
||||
imgSrc: String,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['cropper']);
|
||||
|
||||
const infos = ref();
|
||||
const popoverRef = ref();
|
||||
const refCropper = ref();
|
||||
const showPopover = ref(false);
|
||||
const cropperImg = ref<string>('');
|
||||
|
||||
function onCropper({ base64, blob, info }) {
|
||||
infos.value = info;
|
||||
cropperImg.value = base64;
|
||||
emit('cropper', { base64, blob, info });
|
||||
}
|
||||
|
||||
function hidePopover() {
|
||||
popoverRef.value.hide();
|
||||
}
|
||||
|
||||
defineExpose({ hidePopover });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="!showPopover" element-loading-background="transparent">
|
||||
<el-popover ref="popoverRef" :visible="showPopover" placement="right" popper-style="top:260px" width="18vw">
|
||||
<template #reference>
|
||||
<div class="w-[18vw]">
|
||||
<ReCropper ref="refCropper" :src="imgSrc" circled @cropper="onCropper" @readied="showPopover = true" />
|
||||
<p v-show="showPopover" class="mt-1 text-center">
|
||||
{{ $t('cropper_preview_tips') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-wrap justify-center items-center text-center">
|
||||
<el-image v-if="cropperImg" :preview-src-list="Array.of(cropperImg)" :src="cropperImg" class="cropper-img-preview" fit="contain" />
|
||||
<div v-if="infos" class="mt-1">
|
||||
<p>{{ $t('image_size') }}:{{ parseInt(infos.width) }} × {{ parseInt(infos.height) }}{{ $t('pixel') }}</p>
|
||||
<p>
|
||||
{{ $t('file_size') }}:{{ formatBytes(infos.size) }}({{ infos.size }}
|
||||
{{ $t('bytes') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cropper-img-preview {
|
||||
height: 200px;
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
max-height: 310px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { animates } from './animate';
|
||||
import { cloneDeep } from '@pureadmin/utils';
|
||||
import { $t } from '@/plugins/i18n';
|
||||
|
||||
defineOptions({
|
||||
name: 'ReAnimateSelector',
|
||||
|
@ -11,7 +10,7 @@ defineOptions({
|
|||
defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: $t('pleaseSelectAnimation'),
|
||||
default: '请选择动画',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -62,7 +61,6 @@ function filterMethod(value: any) {
|
|||
}
|
||||
|
||||
const animateMap = ref({});
|
||||
|
||||
function onMouseEnter(index: string | number) {
|
||||
animateMap.value[index] = animateMap.value[index]?.loading
|
||||
? Object.assign({}, animateMap.value[index], {
|
||||
|
@ -72,7 +70,6 @@ function onMouseEnter(index: string | number) {
|
|||
loading: true,
|
||||
});
|
||||
}
|
||||
|
||||
function onMouseleave() {
|
||||
animateMap.value = {};
|
||||
}
|
||||
|
@ -80,33 +77,33 @@ function onMouseleave() {
|
|||
|
||||
<template>
|
||||
<el-select
|
||||
:filter-method="filterMethod"
|
||||
:model-value="inputValue"
|
||||
:placeholder="placeholder"
|
||||
clearable
|
||||
filterable
|
||||
:placeholder="placeholder"
|
||||
popper-class="pure-animate-popper"
|
||||
:model-value="inputValue"
|
||||
:filter-method="filterMethod"
|
||||
@clear="onClear"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="w-[280px]">
|
||||
<el-scrollbar :view-style="{ overflow: 'hidden' }" class="border-t border-[#e5e7eb]" height="212px" noresize>
|
||||
<ul class="flex flex-wrap justify-around mb-1">
|
||||
<el-scrollbar noresize height="212px" :view-style="{ overflow: 'hidden' }" class="border-t border-[#e5e7eb]">
|
||||
<ul class="flex flex-wrap justify-around mb-1!">
|
||||
<li
|
||||
v-for="(animate, index) in animatesList"
|
||||
:key="index"
|
||||
:class="animateClass"
|
||||
:style="animateStyle(animate)"
|
||||
@click="onChangeIcon(animate)"
|
||||
@mouseenter.prevent="onMouseEnter(index)"
|
||||
@mouseleave.prevent="onMouseleave"
|
||||
@click="onChangeIcon(animate)"
|
||||
>
|
||||
<h4 :class="[`animate__animated animate__${animateMap[index]?.loading ? animate + ' animate__infinite' : ''} `]">
|
||||
{{ animate }}
|
||||
</h4>
|
||||
</li>
|
||||
</ul>
|
||||
<el-empty v-show="animatesList.length === 0" :description="`${searchVal} ${$t('animationNotExist')}`" :image-size="60" />
|
||||
<el-empty v-show="animatesList.length === 0" :description="`${searchVal} 动画不存在`" :image-size="60" />
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { withInstall } from '@pureadmin/utils';
|
||||
import reBarcode from './src/index.vue';
|
||||
|
||||
/** 条形码组件 */
|
||||
export const ReBarcode = withInstall(reBarcode);
|
||||
|
||||
export default ReBarcode;
|
|
@ -0,0 +1,42 @@
|
|||
<script setup lang="ts">
|
||||
import JsBarcode from 'jsbarcode';
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ReBarcode',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'canvas',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
// 完整配置 https://github.com/lindell/JsBarcode/wiki/Options
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
// type 相当于 options.format,如果 type 和 options.format 同时存在,type 值优先;
|
||||
type: {
|
||||
type: String,
|
||||
default: 'CODE128',
|
||||
},
|
||||
});
|
||||
|
||||
const wrapEl = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
const opt = { ...props.options, format: props.type };
|
||||
JsBarcode(wrapEl.value, props.text, opt);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="tag" ref="wrapEl" />
|
||||
</template>
|
|
@ -21,7 +21,9 @@ defineProps({
|
|||
<slot name="icon" />
|
||||
</div>
|
||||
</el-row>
|
||||
<p class="list-card-item_detail--name text-text_color_primary">{{ title }}</p>
|
||||
<p class="list-card-item_detail--name text-text_color_primary">
|
||||
{{ title }}
|
||||
</p>
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -78,10 +80,10 @@ defineProps({
|
|||
height: 40px;
|
||||
margin-bottom: 24px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ElCol } from 'element-plus';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { h, defineComponent } from 'vue';
|
||||
|
||||
// 封装element-plus的el-col组件
|
||||
export default defineComponent({
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
normal 普通数字动画组件
|
||||
rebound 回弹式数字动画组件
|
|
@ -54,6 +54,7 @@ export default defineComponent({
|
|||
state.rAF = requestAnimationFrame(count);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function pauseResume() {
|
||||
if (state.paused) {
|
||||
resume();
|
||||
|
@ -75,6 +76,7 @@ export default defineComponent({
|
|||
requestAnimationFrame(count);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function reset() {
|
||||
state.startTime = null;
|
||||
cancelAnimationFrame(state.rAF);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ref } from 'vue';
|
|||
import reDialog from './index.vue';
|
||||
import { useTimeoutFn } from '@vueuse/core';
|
||||
import { withInstall } from '@pureadmin/utils';
|
||||
import type { ArgsType, ButtonProps, DialogOptions, DialogProps, EventType } from './type';
|
||||
import type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions } from './type';
|
||||
|
||||
const dialogStore = ref<Array<DialogOptions>>([]);
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
import { type ButtonProps, closeDialog, type DialogOptions, dialogStore, type EventType } from './index';
|
||||
import { computed, ref } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { type EventType, type ButtonProps, type DialogOptions, closeDialog, dialogStore } 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';
|
||||
import { $t } from '@/plugins/i18n';
|
||||
import Fullscreen from '~icons/ri/fullscreen-fill';
|
||||
import ExitFullscreen from '~icons/ri/fullscreen-exit-fill';
|
||||
|
||||
defineOptions({
|
||||
name: 'ReDialog',
|
||||
|
@ -19,7 +18,7 @@ const footerButtons = computed(() => {
|
|||
? options.footerButtons
|
||||
: ([
|
||||
{
|
||||
label: $t('cancel'),
|
||||
label: '取消',
|
||||
text: true,
|
||||
bg: true,
|
||||
btnClick: ({ dialog: { options, index } }) => {
|
||||
|
@ -32,7 +31,7 @@ const footerButtons = computed(() => {
|
|||
},
|
||||
},
|
||||
{
|
||||
label: $t('confirm'),
|
||||
label: '确定',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
bg: true,
|
||||
|
@ -64,7 +63,7 @@ const footerButtons = computed(() => {
|
|||
});
|
||||
|
||||
const fullscreenClass = computed(() => {
|
||||
return ['el-icon', 'el-dialog__close', '-translate-x-2', 'cursor-pointer', 'hover:!text-[red]'];
|
||||
return ['el-icon', 'el-dialog__close', '-translate-x-2', 'cursor-pointer', 'hover:text-[red]!'];
|
||||
});
|
||||
|
||||
function eventsCallBack(event: EventType, options: DialogOptions, index: number, isClickFullScreen = false) {
|
||||
|
@ -84,14 +83,14 @@ function handleClose(options: DialogOptions, index: number, args = { command: 'c
|
|||
<el-dialog
|
||||
v-for="(options, index) in dialogStore"
|
||||
:key="index"
|
||||
v-model="options.visible"
|
||||
:fullscreen="fullscreen ? true : !!options?.fullscreen"
|
||||
class="pure-dialog"
|
||||
v-bind="options"
|
||||
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
|
||||
v-model="options.visible"
|
||||
class="pure-dialog"
|
||||
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
|
||||
@closed="handleClose(options, index)"
|
||||
@openAutoFocus="eventsCallBack('openAutoFocus', 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 }">
|
||||
|
@ -107,12 +106,12 @@ function handleClose(options: DialogOptions, index: number, args = { command: 'c
|
|||
}
|
||||
"
|
||||
>
|
||||
<IconifyIconOffline :icon="options?.fullscreen ? ExitFullscreen : fullscreen ? ExitFullscreen : Fullscreen" class="pure-dialog-svg" />
|
||||
<IconifyIconOffline class="pure-dialog-svg" :icon="options?.fullscreen ? ExitFullscreen : fullscreen ? ExitFullscreen : Fullscreen" />
|
||||
</i>
|
||||
</div>
|
||||
<component :is="options?.headerRenderer({ close, titleId, titleClass })" v-else />
|
||||
</template>
|
||||
<component :is="options.contentRenderer({ options, index })" v-bind="options?.props" @close="(args) => handleClose(options, index, args)" />
|
||||
<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">
|
||||
|
@ -136,8 +135,8 @@ function handleClose(options: DialogOptions, index: number, args = { command: 'c
|
|||
</el-popconfirm>
|
||||
<el-button
|
||||
v-else
|
||||
:loading="key === 1 && sureBtnMap[index]?.loading"
|
||||
v-bind="btn"
|
||||
:loading="key === 1 && sureBtnMap[index]?.loading"
|
||||
@click="
|
||||
btn.btnClick({
|
||||
dialog: { options, index },
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Component, CSSProperties, VNode } from 'vue';
|
||||
import type { CSSProperties, VNode, Component } from 'vue';
|
||||
|
||||
type DoneFn = (cancel?: boolean) => void;
|
||||
type EventType = 'open' | 'close' | 'openAutoFocus' | 'closeAutoFocus' | 'fullscreenCallBack';
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { ref } from 'vue';
|
||||
import reDrawer from './index.vue';
|
||||
import { useTimeoutFn } from '@vueuse/core';
|
||||
import { withInstall } from '@pureadmin/utils';
|
||||
import type { EventType, ArgsType, DrawerProps, DrawerOptions, ButtonProps } from './type';
|
||||
|
||||
const drawerStore = ref<Array<DrawerOptions>>([]);
|
||||
|
||||
/** 打开抽屉 */
|
||||
const addDrawer = (options: DrawerOptions) => {
|
||||
const open = () => drawerStore.value.push(Object.assign(options, { visible: true }));
|
||||
if (options?.openDelay) {
|
||||
useTimeoutFn(() => {
|
||||
open();
|
||||
}, options.openDelay);
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
};
|
||||
|
||||
/** 关闭抽屉 */
|
||||
const closeDrawer = (options: DrawerOptions, index: number, args?: any) => {
|
||||
drawerStore.value[index].visible = false;
|
||||
options.closeCallBack && options.closeCallBack({ options, index, args });
|
||||
|
||||
const closeDelay = options?.closeDelay ?? 200;
|
||||
useTimeoutFn(() => {
|
||||
drawerStore.value.splice(index, 1);
|
||||
}, closeDelay);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 更改抽屉自身属性值
|
||||
* @param value 属性值
|
||||
* @param key 属性,默认`title`
|
||||
* @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`)
|
||||
*/
|
||||
const updateDrawer = (value: any, key = 'title', index = 0) => {
|
||||
drawerStore.value[index][key] = value;
|
||||
};
|
||||
|
||||
/** 关闭所有弹框 */
|
||||
const closeAllDrawer = () => {
|
||||
drawerStore.value = [];
|
||||
};
|
||||
|
||||
const ReDrawer = withInstall(reDrawer);
|
||||
|
||||
export type { EventType, ArgsType, DrawerOptions, DrawerProps, ButtonProps };
|
||||
export { ReDrawer, drawerStore, addDrawer, closeDrawer, updateDrawer, closeAllDrawer };
|
|
@ -0,0 +1,141 @@
|
|||
<script setup lang="ts">
|
||||
import { type EventType, type ButtonProps, type DrawerOptions, closeDrawer, drawerStore } from './index';
|
||||
import { computed, ref } from 'vue';
|
||||
import { isFunction } from '@pureadmin/utils';
|
||||
|
||||
defineOptions({
|
||||
name: 'ReDrawer',
|
||||
});
|
||||
|
||||
const sureBtnMap = ref({});
|
||||
|
||||
const footerButtons = computed(() => {
|
||||
return (options: DrawerOptions) => {
|
||||
return options?.footerButtons?.length > 0
|
||||
? options.footerButtons
|
||||
: ([
|
||||
{
|
||||
label: '取消',
|
||||
text: true,
|
||||
bg: true,
|
||||
btnClick: ({ drawer: { options, index } }) => {
|
||||
const done = () => closeDrawer(options, index, { command: 'cancel' });
|
||||
if (options?.beforeCancel && isFunction(options?.beforeCancel)) {
|
||||
options.beforeCancel(done, { options, index });
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '确定',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
bg: true,
|
||||
popConfirm: options?.popConfirm,
|
||||
btnClick: ({ drawer: { options, index } }) => {
|
||||
if (options?.sureBtnLoading) {
|
||||
sureBtnMap.value[index] = Object.assign({}, sureBtnMap.value[index], {
|
||||
loading: true,
|
||||
});
|
||||
}
|
||||
const closeLoading = () => {
|
||||
if (options?.sureBtnLoading) {
|
||||
sureBtnMap.value[index].loading = false;
|
||||
}
|
||||
};
|
||||
const done = () => {
|
||||
closeLoading();
|
||||
closeDrawer(options, index, { command: 'sure' });
|
||||
};
|
||||
if (options?.beforeSure && isFunction(options?.beforeSure)) {
|
||||
options.beforeSure(done, { options, index, closeLoading });
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
},
|
||||
] as Array<ButtonProps>);
|
||||
};
|
||||
});
|
||||
|
||||
function eventsCallBack(event: EventType, options: DrawerOptions, index: number) {
|
||||
if (options?.[event] && isFunction(options?.[event])) {
|
||||
return options?.[event]({ options, index });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DrawerOptions} options - 包含抽屉相关配置的对象
|
||||
* @param {number} index - 抽屉的索引
|
||||
* @param {Object} args - 传递给关闭抽屉操作的参数对象,默认为 { command: 'close' }
|
||||
* @returns {void} 这个函数不返回任何值
|
||||
*/
|
||||
function handleClose(options: DrawerOptions, index: number, args = { command: 'close' }) {
|
||||
closeDrawer(options, index, args);
|
||||
eventsCallBack('close', options, index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-drawer
|
||||
v-for="(options, index) in drawerStore"
|
||||
:key="index"
|
||||
v-bind="options"
|
||||
v-model="options.visible"
|
||||
class="pure-drawer"
|
||||
:append-to-body="!!options?.appendToBody"
|
||||
:append-to="options?.appendTo ? options.appendTo : 'body'"
|
||||
:destroy-on-close="!!options?.destroyOnClose"
|
||||
:lock-scroll="!!options?.lockScroll"
|
||||
@closed="handleClose(options, index)"
|
||||
@opened="eventsCallBack('open', options, index)"
|
||||
@open-auto-focus="eventsCallBack('openAutoFocus', options, index)"
|
||||
@close-auto-focus="eventsCallBack('closeAutoFocus', options, index)"
|
||||
>
|
||||
<!-- header -->
|
||||
<template v-if="options?.headerRenderer" #header="{ close, titleId, titleClass }">
|
||||
<component :is="options?.headerRenderer({ close, titleId, titleClass })" />
|
||||
</template>
|
||||
<!-- body -->
|
||||
<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>
|
||||
<template v-for="(btn, key) in footerButtons(options)" :key="key">
|
||||
<el-popconfirm
|
||||
v-if="btn.popConfirm"
|
||||
v-bind="btn.popConfirm"
|
||||
@confirm="
|
||||
btn.btnClick({
|
||||
drawer: { options, index },
|
||||
button: { btn, index: key },
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button v-bind="btn">{{ btn?.label }}</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-button
|
||||
v-else
|
||||
v-bind="btn"
|
||||
:loading="key === 1 && sureBtnMap[index]?.loading"
|
||||
@click="
|
||||
btn.btnClick({
|
||||
drawer: { options, index },
|
||||
button: { btn, index: key },
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ btn?.label }}
|
||||
</el-button>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
|
@ -0,0 +1,204 @@
|
|||
import type { CSSProperties, VNode, Component } from 'vue';
|
||||
|
||||
type DoneFn = (cancel?: boolean) => void;
|
||||
type EventType = 'open' | 'close' | 'openAutoFocus' | 'closeAutoFocus';
|
||||
type ArgsType = {
|
||||
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了 `esc` 键 */
|
||||
command: 'cancel' | 'sure' | 'close';
|
||||
};
|
||||
|
||||
type ButtonType = 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text';
|
||||
|
||||
type DrawerProps = {
|
||||
/** `Drawer` 的显示与隐藏 */
|
||||
visible?: boolean;
|
||||
/** `Drawer` 自身是否插入至 `body` 元素上。嵌套的 `Drawer` 必须指定该属性并赋值为 `true`,默认 `false` */
|
||||
appendToBody?: boolean;
|
||||
/** 挂载到哪个 `DOM` 元素 将覆盖 `appendToBody` */
|
||||
appendTo?: string;
|
||||
/** 是否在 `Drawer` 出现时将 `body` 滚动锁定,默认 `true` */
|
||||
lockScroll?: boolean;
|
||||
/** 关闭前的回调,会暂停 `Drawer` 的关闭 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
|
||||
beforeClose?: (done: DoneFn) => void;
|
||||
/** 是否可以通过点击 `modal` 关闭 `Drawer` ,默认 `true` */
|
||||
closeOnClickModal?: boolean;
|
||||
/** 是否可以通过按下 `ESC` 关闭 `Drawer` ,默认 `true` */
|
||||
closeOnPressEscape?: boolean;
|
||||
/** 是否显示关闭按钮,默认 `true` */
|
||||
showClose?: boolean;
|
||||
/** `Drawer` 打开的延时时间,单位毫秒,默认 `0` */
|
||||
openDelay?: number;
|
||||
/** `Drawer` 关闭的延时时间,单位毫秒,默认 `0` */
|
||||
closeDelay?: number;
|
||||
/** `Drawer` 自定义类名 */
|
||||
class?: string;
|
||||
/** `Drawer` 的自定义样式 */
|
||||
style?: CSSProperties;
|
||||
/** 控制是否在关闭 `Drawer` 之后将子元素全部销毁,默认 `false` */
|
||||
destroyOnClose?: boolean;
|
||||
/** 是否需要遮罩层,默认 `true` */
|
||||
modal?: boolean;
|
||||
/** `Drawer` 打开的方向,默认 `rtl` */
|
||||
direction?: 'rtl' | 'ltr' | 'ttb' | 'btt';
|
||||
/** `Drawer` 窗体的大小, 当使用 `number` 类型时, 以像素为单位, 当使用 `string` 类型时, 请传入 `'x%'`, 否则便会以 `number` 类型解释 */
|
||||
size?: string | number;
|
||||
/** `Drawer` 的标题 */
|
||||
title?: string;
|
||||
/** 控制是否显示 `header` 栏, 默认为 `true`, 当此项为 `false` 时, `title attribute` 和 `title slot` 均不生效 */
|
||||
withHeader?: boolean;
|
||||
/** 遮罩层的自定义类名 */
|
||||
modalClass?: string;
|
||||
/** 设置 `z-index` */
|
||||
zIndex?: number;
|
||||
/** `header` 的 `aria-level` 属性,默认 `2` */
|
||||
headerAriaLevel?: string;
|
||||
};
|
||||
|
||||
//element-plus.org/zh-CN/component/popConfirm.html#attributes
|
||||
type PopConfirm = {
|
||||
/** 标题 */
|
||||
title?: string;
|
||||
/** 确认按钮文字 */
|
||||
confirmButtonText?: string;
|
||||
/** 取消按钮文字 */
|
||||
cancelButtonText?: string;
|
||||
/** 确认按钮类型,默认 `primary` */
|
||||
confirmButtonType?: ButtonType;
|
||||
/** 取消按钮类型,默认 `text` */
|
||||
cancelButtonType?: ButtonType;
|
||||
/** 自定义图标,默认 `QuestionFilled` */
|
||||
icon?: string | Component;
|
||||
/** `Icon` 颜色,默认 `#f90` */
|
||||
iconColor?: string;
|
||||
/** 是否隐藏 `Icon`,默认 `false` */
|
||||
hideIcon?: boolean;
|
||||
/** 关闭时的延迟,默认 `200` */
|
||||
hideAfter?: number;
|
||||
/** 是否将 `popover` 的下拉列表插入至 `body` 元素,默认 `true` */
|
||||
teleported?: boolean;
|
||||
/** 当 `popover` 组件长时间不触发且 `persistent` 属性设置为 `false` 时, `popover` 将会被删除,默认 `false` */
|
||||
persistent?: boolean;
|
||||
/** 弹层宽度,最小宽度 `150px`,默认 `150` */
|
||||
width?: string | number;
|
||||
};
|
||||
|
||||
type BtnClickDrawer = {
|
||||
options?: DrawerOptions;
|
||||
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;
|
||||
/** 确认按钮的 `PopConfirm` 气泡确认框相关配置 */
|
||||
popConfirm?: PopConfirm;
|
||||
/** 是否为加载中状态,默认 `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?: ({
|
||||
drawer,
|
||||
button,
|
||||
}: {
|
||||
/** 当前 `Drawer` 信息 */
|
||||
drawer: BtnClickDrawer;
|
||||
/** 当前 `button` 信息 */
|
||||
button: BtnClickButton;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
interface DrawerOptions extends DrawerProps {
|
||||
/** 内容区组件的 `props`,可通过 `defineProps` 接收 */
|
||||
props?: any;
|
||||
/** 是否隐藏 `Drawer` 按钮操作区的内容 */
|
||||
hideFooter?: boolean;
|
||||
/** 确认按钮的 `PopConfirm` 气泡确认框相关配置 */
|
||||
popConfirm?: PopConfirm;
|
||||
/** 点击确定按钮后是否开启 `loading` 加载动画 */
|
||||
sureBtnLoading?: boolean;
|
||||
/**
|
||||
* @description 自定义抽屉标题的内容渲染器
|
||||
* @see {@link https://element-plus.org/zh-CN/component/drawer.html#%E6%8F%92%E6%A7%BD}
|
||||
*/
|
||||
headerRenderer?: ({ close, titleId, titleClass }: { close: Function; titleId: string; titleClass: string }) => VNode | Component;
|
||||
/** 自定义内容渲染器 */
|
||||
contentRenderer?: ({ options, index }: { options: DrawerOptions; index: number }) => VNode | Component;
|
||||
/** 自定义按钮操作区的内容渲染器,会覆盖`footerButtons`以及默认的 `取消` 和 `确定` 按钮 */
|
||||
footerRenderer?: ({ options, index }: { options: DrawerOptions; index: number }) => VNode | Component;
|
||||
/** 自定义底部按钮操作 */
|
||||
footerButtons?: Array<ButtonProps>;
|
||||
/** `Drawer` 打开后的回调 */
|
||||
open?: ({ options, index }: { options: DrawerOptions; index: number }) => void;
|
||||
/** `Drawer` 关闭后的回调(只有点击右上角关闭按钮或空白页或按下了esc键关闭页面时才会触发) */
|
||||
close?: ({ options, index }: { options: DrawerOptions; index: number }) => void;
|
||||
/** `Drawer` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
|
||||
closeCallBack?: ({ options, index, args }: { options: DrawerOptions; index: number; args: any }) => void;
|
||||
/** 输入焦点聚焦在 `Drawer` 内容时的回调 */
|
||||
openAutoFocus?: ({ options, index }: { options: DrawerOptions; index: number }) => void;
|
||||
/** 输入焦点从 `Drawer` 内容失焦时的回调 */
|
||||
closeAutoFocus?: ({ options, index }: { options: DrawerOptions; index: number }) => void;
|
||||
|
||||
/** 点击底部取消按钮的回调,会暂停 `Drawer` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
|
||||
beforeCancel?: (
|
||||
done: Function,
|
||||
{
|
||||
options,
|
||||
index,
|
||||
}: {
|
||||
options: DrawerOptions;
|
||||
index: number;
|
||||
}
|
||||
) => void;
|
||||
/** 点击底部确定按钮的回调,会暂停 `Drawer` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
|
||||
beforeSure?: (
|
||||
done: Function,
|
||||
{
|
||||
options,
|
||||
index,
|
||||
closeLoading,
|
||||
}: {
|
||||
options: DrawerOptions;
|
||||
index: number;
|
||||
closeLoading: Function;
|
||||
}
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type { ButtonProps, DrawerOptions, ArgsType, DrawerProps, EventType };
|
|
@ -0,0 +1,39 @@
|
|||
.point {
|
||||
width: var(--point-width);
|
||||
height: var(--point-height);
|
||||
background: var(--point-background);
|
||||
position: relative;
|
||||
border-radius: var(--point-border-radius);
|
||||
}
|
||||
|
||||
.point-flicker:after {
|
||||
background: var(--point-background);
|
||||
}
|
||||
|
||||
.point-flicker:before,
|
||||
.point-flicker:after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
border-radius: var(--point-border-radius);
|
||||
animation: flicker 1.2s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(var(--point-scale));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import './index.css';
|
||||
import { type Component, h, defineComponent } from 'vue';
|
||||
|
||||
export interface attrsType {
|
||||
width?: string;
|
||||
height?: string;
|
||||
borderRadius?: number | string;
|
||||
background?: string;
|
||||
scale?: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 圆点、方形闪烁动画组件
|
||||
* @param width 可选 string 宽
|
||||
* @param height 可选 string 高
|
||||
* @param borderRadius 可选 number | string 传0为方形、传50%或者不传为圆形
|
||||
* @param background 可选 string 闪烁颜色
|
||||
* @param scale 可选 number | string 闪烁范围,默认2,值越大闪烁范围越大
|
||||
* @returns Component
|
||||
*/
|
||||
export function useRenderFlicker(attrs?: attrsType): Component {
|
||||
return defineComponent({
|
||||
name: 'ReFlicker',
|
||||
render() {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: 'point point-flicker',
|
||||
style: {
|
||||
'--point-width': attrs?.width ?? '12px',
|
||||
'--point-height': attrs?.height ?? '12px',
|
||||
'--point-background': attrs?.background ?? 'var(--el-color-primary)',
|
||||
'--point-border-radius': attrs?.borderRadius ?? '50%',
|
||||
'--point-scale': attrs?.scale ?? '2',
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import reFlop from './src/index.vue';
|
||||
import { withInstall } from '@pureadmin/utils';
|
||||
|
||||
/** 时间翻牌组件 */
|
||||
export const ReFlop = withInstall(reFlop);
|
||||
|
||||
export default ReFlop;
|
|
@ -0,0 +1,184 @@
|
|||
.m-flipper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
border: solid 1px #000;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
font-size: 66px;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 6px rgb(0 0 0 / 50%);
|
||||
text-align: center;
|
||||
font-family: 'Helvetica Neue';
|
||||
}
|
||||
|
||||
.m-flipper .digital::before,
|
||||
.m-flipper .digital::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.m-flipper .digital::before {
|
||||
top: 0;
|
||||
bottom: 50%;
|
||||
border-radius: 10px 10px 0 0;
|
||||
border-bottom: solid 1px #666;
|
||||
}
|
||||
|
||||
.m-flipper .digital::after {
|
||||
top: 50%;
|
||||
bottom: 0;
|
||||
border-radius: 0 0 10px 10px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
/* 向下翻 */
|
||||
.m-flipper.down .front::before {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.m-flipper.down .back::after {
|
||||
z-index: 2;
|
||||
transform-origin: 50% 0%;
|
||||
transform: perspective(160px) rotateX(180deg);
|
||||
}
|
||||
|
||||
.m-flipper.down .front::after,
|
||||
.m-flipper.down .back::before {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.m-flipper.down.go .front::before {
|
||||
transform-origin: 50% 100%;
|
||||
animation: frontFlipDown 0.6s ease-in-out both;
|
||||
box-shadow: 0 -2px 6px rgb(255 255 255 / 30%);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.m-flipper.down.go .back::after {
|
||||
animation: backFlipDown 0.6s ease-in-out both;
|
||||
}
|
||||
|
||||
/* 向上翻 */
|
||||
.m-flipper.up .front::after {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.m-flipper.up .back::before {
|
||||
z-index: 2;
|
||||
transform-origin: 50% 100%;
|
||||
transform: perspective(160px) rotateX(-180deg);
|
||||
}
|
||||
|
||||
.m-flipper.up .front::before,
|
||||
.m-flipper.up .back::after {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.m-flipper.up.go .front::after {
|
||||
transform-origin: 50% 0;
|
||||
animation: frontFlipUp 0.6s ease-in-out both;
|
||||
box-shadow: 0 2px 6px rgb(255 255 255 / 30%);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.m-flipper.up.go .back::before {
|
||||
animation: backFlipUp 0.6s ease-in-out both;
|
||||
}
|
||||
|
||||
@keyframes frontFlipDown {
|
||||
0% {
|
||||
transform: perspective(160px) rotateX(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: perspective(160px) rotateX(-180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes backFlipDown {
|
||||
0% {
|
||||
transform: perspective(160px) rotateX(180deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: perspective(160px) rotateX(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes frontFlipUp {
|
||||
0% {
|
||||
transform: perspective(160px) rotateX(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: perspective(160px) rotateX(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes backFlipUp {
|
||||
0% {
|
||||
transform: perspective(160px) rotateX(-180deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: perspective(160px) rotateX(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.m-flipper .number0::before,
|
||||
.m-flipper .number0::after {
|
||||
content: '0';
|
||||
}
|
||||
|
||||
.m-flipper .number1::before,
|
||||
.m-flipper .number1::after {
|
||||
content: '1';
|
||||
}
|
||||
|
||||
.m-flipper .number2::before,
|
||||
.m-flipper .number2::after {
|
||||
content: '2';
|
||||
}
|
||||
|
||||
.m-flipper .number3::before,
|
||||
.m-flipper .number3::after {
|
||||
content: '3';
|
||||
}
|
||||
|
||||
.m-flipper .number4::before,
|
||||
.m-flipper .number4::after {
|
||||
content: '4';
|
||||
}
|
||||
|
||||
.m-flipper .number5::before,
|
||||
.m-flipper .number5::after {
|
||||
content: '5';
|
||||
}
|
||||
|
||||
.m-flipper .number6::before,
|
||||
.m-flipper .number6::after {
|
||||
content: '6';
|
||||
}
|
||||
|
||||
.m-flipper .number7::before,
|
||||
.m-flipper .number7::after {
|
||||
content: '7';
|
||||
}
|
||||
|
||||
.m-flipper .number8::before,
|
||||
.m-flipper .number8::after {
|
||||
content: '8';
|
||||
}
|
||||
|
||||
.m-flipper .number9::before,
|
||||
.m-flipper .number9::after {
|
||||
content: '9';
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import './filpper.css';
|
||||
import propTypes from '@/utils/propTypes';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
const props = {
|
||||
// front paper text
|
||||
// 前牌文字
|
||||
frontText: propTypes.number.def(0),
|
||||
// back paper text
|
||||
// 后牌文字
|
||||
backText: propTypes.number.def(1),
|
||||
// flipping duration, please be consistent with the CSS animation-duration value.
|
||||
// 翻牌动画时间,与CSS中设置的animation-duration保持一致
|
||||
duration: propTypes.number.def(600),
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ReFlop',
|
||||
props,
|
||||
setup(props) {
|
||||
const { frontText, backText, duration } = props;
|
||||
const isFlipping = ref(false);
|
||||
const flipType = ref('down');
|
||||
const frontTextFromData = ref(frontText);
|
||||
const backTextFromData = ref(backText);
|
||||
|
||||
const textClass = (number: number) => {
|
||||
return 'number' + number;
|
||||
};
|
||||
|
||||
const flip = (type: string, front: number, back: number) => {
|
||||
// 如果处于翻转中,则不执行
|
||||
if (isFlipping.value) return false;
|
||||
frontTextFromData.value = front;
|
||||
backTextFromData.value = back;
|
||||
// 根据传递过来的type设置翻转方向
|
||||
flipType.value = type;
|
||||
// 设置翻转状态为true
|
||||
isFlipping.value = true;
|
||||
|
||||
setTimeout(() => {
|
||||
// 设置翻转状态为false
|
||||
isFlipping.value = false;
|
||||
frontTextFromData.value = back;
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// 下翻牌
|
||||
const flipDown = (front: any, back: any): void => {
|
||||
flip('down', front, back);
|
||||
};
|
||||
|
||||
// 上翻牌
|
||||
const flipUp = (front: any, back: any): void => {
|
||||
flip('up', front, back);
|
||||
};
|
||||
|
||||
// 设置前牌文字
|
||||
function setFront(text: number): void {
|
||||
frontTextFromData.value = text;
|
||||
}
|
||||
|
||||
// 设置后牌文字
|
||||
const setBack = (text: number): void => {
|
||||
backTextFromData.value = text;
|
||||
};
|
||||
|
||||
return {
|
||||
flipType,
|
||||
isFlipping,
|
||||
frontTextFromData,
|
||||
backTextFromData,
|
||||
textClass,
|
||||
flipDown,
|
||||
flipUp,
|
||||
setFront,
|
||||
setBack,
|
||||
};
|
||||
},
|
||||
|
||||
render() {
|
||||
const main = `m-flipper ${this.flipType} ${this.isFlipping ? 'go' : ''}`;
|
||||
const front = `digital front ${this.textClass(this.frontTextFromData)}`;
|
||||
const back = `digital back ${this.textClass(this.backTextFromData)}`;
|
||||
return (
|
||||
<div class={main}>
|
||||
<div class={front} />
|
||||
<div class={back} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,122 @@
|
|||
<script setup lang="ts">
|
||||
import flippers from './filpper';
|
||||
import { ref, unref, nextTick, onUnmounted } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ReFlop',
|
||||
});
|
||||
|
||||
const timer = ref(null);
|
||||
const flipObjs = ref([]);
|
||||
|
||||
const flipperHour1 = ref();
|
||||
const flipperHour2 = ref();
|
||||
const flipperMinute1 = ref();
|
||||
const flipperMinute2 = ref();
|
||||
const flipperSecond1 = ref();
|
||||
const flipperSecond2 = ref();
|
||||
|
||||
// 初始化数字
|
||||
const init = () => {
|
||||
const now = new Date();
|
||||
const nowTimeStr = formatDate(new Date(now.getTime()), 'hhiiss');
|
||||
for (let i = 0; i < flipObjs.value.length; i++) {
|
||||
flipObjs?.value[i]?.setFront(nowTimeStr[i]);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始计时
|
||||
const run = () => {
|
||||
timer.value = setInterval(() => {
|
||||
// 获取当前时间
|
||||
const now = new Date();
|
||||
const nowTimeStr = formatDate(new Date(now.getTime() - 1000), 'hhiiss');
|
||||
const nextTimeStr = formatDate(now, 'hhiiss');
|
||||
for (let i = 0; i < flipObjs.value.length; i++) {
|
||||
if (nowTimeStr[i] === nextTimeStr[i]) {
|
||||
continue;
|
||||
}
|
||||
flipObjs?.value[i]?.flipDown(nowTimeStr[i], nextTimeStr[i]);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 正则格式化日期
|
||||
const formatDate = (date: Date, dateFormat: string) => {
|
||||
/* 单独格式化年份,根据y的字符数量输出年份
|
||||
* 例如:yyyy => 2019
|
||||
yy => 19
|
||||
y => 9
|
||||
*/
|
||||
if (/(y+)/.test(dateFormat)) {
|
||||
dateFormat = dateFormat.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
|
||||
}
|
||||
// 格式化月、日、时、分、秒
|
||||
const o = {
|
||||
'm+': date.getMonth() + 1,
|
||||
'd+': date.getDate(),
|
||||
'h+': date.getHours(),
|
||||
'i+': date.getMinutes(),
|
||||
's+': date.getSeconds(),
|
||||
};
|
||||
for (const k in o) {
|
||||
if (new RegExp(`(${k})`).test(dateFormat)) {
|
||||
// 取出对应的值
|
||||
const str = o[k] + '';
|
||||
/* 根据设置的格式,输出对应的字符
|
||||
* 例如: 早上8时,hh => 08,h => 8
|
||||
* 但是,当数字>=10时,无论格式为一位还是多位,不做截取,这是与年份格式化不一致的地方
|
||||
* 例如: 下午15时,hh => 15, h => 15
|
||||
*/
|
||||
dateFormat = dateFormat.replace(RegExp.$1, RegExp.$1.length === 1 ? str : padLeftZero(str));
|
||||
}
|
||||
}
|
||||
return dateFormat;
|
||||
};
|
||||
|
||||
// 日期时间补零
|
||||
const padLeftZero = (str: string | any[]) => {
|
||||
return ('00' + str).substr(str.length);
|
||||
};
|
||||
|
||||
nextTick(() => {
|
||||
flipObjs.value = [unref(flipperHour1), unref(flipperHour2), unref(flipperMinute1), unref(flipperMinute2), unref(flipperSecond1), unref(flipperSecond2)];
|
||||
|
||||
init();
|
||||
run();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flip-clock">
|
||||
<flippers ref="flipperHour1" />
|
||||
<flippers ref="flipperHour2" />
|
||||
<em>:</em>
|
||||
<flippers ref="flipperMinute1" />
|
||||
<flippers ref="flipperMinute2" />
|
||||
<em>:</em>
|
||||
<flippers ref="flipperSecond1" />
|
||||
<flippers ref="flipperSecond2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.flip-clock .m-flipper {
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.flip-clock em {
|
||||
display: inline-block;
|
||||
font-size: 66px;
|
||||
font-style: normal;
|
||||
line-height: 102px;
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,17 @@
|
|||
import control from './src/Control.vue';
|
||||
import nodePanel from './src/NodePanel.vue';
|
||||
import dataDialog from './src/DataDialog.vue';
|
||||
import { withInstall } from '@pureadmin/utils';
|
||||
|
||||
/** LogicFlow流程图-控制面板 */
|
||||
const Control = withInstall(control);
|
||||
|
||||
/** LogicFlow流程图-拖拽面板 */
|
||||
const NodePanel = withInstall(nodePanel);
|
||||
|
||||
/** LogicFlow流程图-查看数据 */
|
||||
const DataDialog = withInstall(dataDialog);
|
||||
|
||||
export { Control, NodePanel, DataDialog };
|
||||
|
||||
// LogicFlow流程图文档:http://logic-flow.org/
|
|
@ -0,0 +1,143 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, unref, onMounted } from 'vue';
|
||||
import { LogicFlow } from '@logicflow/core';
|
||||
|
||||
interface Props {
|
||||
lf?: LogicFlow;
|
||||
catTurboData?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
lf: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'catData'): void;
|
||||
}>();
|
||||
|
||||
const controlButton3 = ref();
|
||||
const controlButton4 = ref();
|
||||
|
||||
const focusIndex = ref<Number>(-1);
|
||||
const titleLists = ref([
|
||||
{
|
||||
icon: 'icon-zoom-out-hs',
|
||||
text: '缩小',
|
||||
size: '18',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'icon-enlarge-hs',
|
||||
text: '放大',
|
||||
size: '18',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'icon-full-screen-hs',
|
||||
text: '适应',
|
||||
size: '15',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'icon-previous-hs',
|
||||
text: '上一步',
|
||||
size: '15',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
icon: 'icon-next-step-hs',
|
||||
text: '下一步',
|
||||
size: '17',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
icon: 'icon-download-hs',
|
||||
text: '下载图片',
|
||||
size: '17',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'icon-watch-hs',
|
||||
text: '查看数据',
|
||||
size: '17',
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const onControl = (item, key) => {
|
||||
['zoom', 'zoom', 'resetZoom', 'undo', 'redo', 'getSnapshot'].forEach((v, i) => {
|
||||
const domControl = props.lf;
|
||||
if (key === 1) {
|
||||
domControl.zoom(true);
|
||||
}
|
||||
if (key === 6) {
|
||||
emit('catData');
|
||||
}
|
||||
if (key === i) {
|
||||
domControl[v]();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onEnter = (key) => {
|
||||
focusIndex.value = key;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
props.lf.on('history:change', ({ data: { undoAble, redoAble } }) => {
|
||||
unref(titleLists)[3].disabled = unref(controlButton3).disabled = !undoAble;
|
||||
unref(titleLists)[4].disabled = unref(controlButton4).disabled = !redoAble;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="control-container">
|
||||
<!-- 功能按钮 -->
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item, key) in titleLists"
|
||||
:key="key"
|
||||
:title="item.text"
|
||||
class="dark:text-bg_color"
|
||||
@mouseenter.prevent="onEnter(key)"
|
||||
@mouseleave.prevent="focusIndex = -1"
|
||||
>
|
||||
<button
|
||||
:ref="'controlButton' + key"
|
||||
v-tippy="{
|
||||
content: item.text,
|
||||
}"
|
||||
:disabled="item.disabled"
|
||||
:style="{
|
||||
cursor: item.disabled === false ? 'pointer' : 'not-allowed',
|
||||
color: item.disabled === false ? '' : '#00000040',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
}"
|
||||
@click="onControl(item, key)"
|
||||
>
|
||||
<span :class="'iconfont ' + item.icon" :style="{ fontSize: `${item.size}px` }" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url('./assets/iconfont/iconfont.css');
|
||||
|
||||
.control-container {
|
||||
background: hsl(0deg 0% 100% / 80%);
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.control-container ul li {
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.control-container ul li span:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
|
||||
defineProps({
|
||||
graphData: Object,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vue-json-pretty :path="'res'" :deep="3" :showLength="true" :data="graphData" />
|
||||
</template>
|
|
@ -0,0 +1,150 @@
|
|||
const TurboType = {
|
||||
SEQUENCE_FLOW: 1,
|
||||
START_EVENT: 2,
|
||||
END_EVENT: 3,
|
||||
USER_TASK: 4,
|
||||
SERVICE_TASK: 5,
|
||||
EXCLUSIVE_GATEWAY: 6,
|
||||
};
|
||||
|
||||
function getTurboType(type) {
|
||||
switch (type) {
|
||||
case 'bpmn:sequenceFlow':
|
||||
return TurboType.SEQUENCE_FLOW;
|
||||
case 'bpmn:startEvent':
|
||||
return TurboType.START_EVENT;
|
||||
case 'bpmn:endEvent':
|
||||
return TurboType.END_EVENT;
|
||||
case 'bpmn:userTask':
|
||||
return TurboType.USER_TASK;
|
||||
case 'bpmn:serviceTask':
|
||||
return TurboType.SERVICE_TASK;
|
||||
case 'bpmn:exclusiveGateway':
|
||||
return TurboType.EXCLUSIVE_GATEWAY;
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
function convertNodeToTurboElement(node) {
|
||||
const { id, type, x, y, text = '', properties } = node;
|
||||
return {
|
||||
incoming: [],
|
||||
outgoing: [],
|
||||
dockers: [],
|
||||
type: getTurboType(node.type),
|
||||
properties: {
|
||||
...properties,
|
||||
name: (text && text.value) || '',
|
||||
x: x,
|
||||
y: y,
|
||||
text,
|
||||
logicFlowType: type,
|
||||
},
|
||||
key: id,
|
||||
};
|
||||
}
|
||||
|
||||
function convertEdgeToTurboElement(edge) {
|
||||
const { id, type, sourceNodeId, targetNodeId, startPoint, endPoint, pointsList, text = '', properties } = edge;
|
||||
return {
|
||||
incoming: [sourceNodeId],
|
||||
outgoing: [targetNodeId],
|
||||
type: getTurboType(type),
|
||||
dockers: [],
|
||||
properties: {
|
||||
...properties,
|
||||
name: (text && text.value) || '',
|
||||
text,
|
||||
startPoint,
|
||||
endPoint,
|
||||
pointsList,
|
||||
logicFlowType: type,
|
||||
},
|
||||
key: id,
|
||||
};
|
||||
}
|
||||
|
||||
export function toTurboData(data) {
|
||||
const nodeMap = new Map();
|
||||
const turboData = {
|
||||
flowElementList: [],
|
||||
};
|
||||
data.nodes.forEach((node) => {
|
||||
const flowElement = convertNodeToTurboElement(node);
|
||||
turboData.flowElementList.push(flowElement);
|
||||
nodeMap.set(node.id, flowElement);
|
||||
});
|
||||
data.edges.forEach((edge) => {
|
||||
const flowElement = convertEdgeToTurboElement(edge);
|
||||
const sourceElement = nodeMap.get(edge.sourceNodeId);
|
||||
sourceElement.outgoing.push(flowElement.key);
|
||||
const targetElement = nodeMap.get(edge.targetNodeId);
|
||||
targetElement.incoming.push(flowElement.key);
|
||||
turboData.flowElementList.push(flowElement);
|
||||
});
|
||||
return turboData;
|
||||
}
|
||||
|
||||
function convertFlowElementToEdge(element) {
|
||||
const { incoming, outgoing, properties, key } = element;
|
||||
const { text, startPoint, endPoint, pointsList, logicFlowType } = properties;
|
||||
const edge = {
|
||||
id: key,
|
||||
type: logicFlowType,
|
||||
sourceNodeId: incoming[0],
|
||||
targetNodeId: outgoing[0],
|
||||
text,
|
||||
startPoint,
|
||||
endPoint,
|
||||
pointsList,
|
||||
properties: {},
|
||||
};
|
||||
const excludeProperties = ['startPoint', 'endPoint', 'pointsList', 'text', 'logicFlowType'];
|
||||
Object.keys(element.properties).forEach((property) => {
|
||||
if (excludeProperties.indexOf(property) === -1) {
|
||||
edge.properties[property] = element.properties[property];
|
||||
}
|
||||
});
|
||||
return edge;
|
||||
}
|
||||
|
||||
function convertFlowElementToNode(element) {
|
||||
const { properties, key } = element;
|
||||
const { x, y, text, logicFlowType } = properties;
|
||||
const node = {
|
||||
id: key,
|
||||
type: logicFlowType,
|
||||
x,
|
||||
y,
|
||||
text,
|
||||
properties: {},
|
||||
};
|
||||
const excludeProperties = ['x', 'y', 'text', 'logicFlowType'];
|
||||
Object.keys(element.properties).forEach((property) => {
|
||||
if (excludeProperties.indexOf(property) === -1) {
|
||||
node.properties[property] = element.properties[property];
|
||||
}
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
export function toLogicflowData(data) {
|
||||
const lfData = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
const list = data.flowElementList;
|
||||
list &&
|
||||
list.length > 0 &&
|
||||
list.forEach((element) => {
|
||||
if (element.type === TurboType.SEQUENCE_FLOW) {
|
||||
const edge = convertFlowElementToEdge(element);
|
||||
lfData.edges.push(edge);
|
||||
} else {
|
||||
const node = convertFlowElementToNode(element);
|
||||
lfData.nodes.push(node);
|
||||
}
|
||||
});
|
||||
return lfData;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.eot?t=1618544337340'); /* IE9 */
|
||||
src:
|
||||
url('iconfont.eot?t=1618544337340#iefix') format('embedded-opentype'),
|
||||
/* IE6-IE8 */
|
||||
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAZ0AAsAAAAADKgAAAYmAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDZAqLQIldATYCJAMgCxIABCAFhG0HgQkb6ApRlA9Sk+xngd1wXQyjTXRCW7pkEvLB0N9/pZhyo7nvIIK1Nisnipg3omjUREiURDXNNEL/jDRCI5H/riTu/9q0D5OakT05VaM3E4kMJI2QhanZillesmYnVT0pD5+399suTrCEkjDhqLtAxyURhIU6Ser/1tp8aDPgI2g7ex2ah+Q7i0rI+Gy9rSNYOtEEdPFQVkrlj/1c3oZFk6Sv/bYQqWUunsgkk8QRkrgkCJEKpUcO8zx0cFLQr+x6CEiNi0BN2YWV4MwJhmDEqhdU4BwR8oIOEXPCjGMzcoKDuLmnLwLw6vy9vMCFM6ggIW50umRpIbVW14U29L/QmIZgqDs5cD0JDKwCHFIylReQ51yFpO+XKBwDcjHltbq9801mxdeFzX8inbguoAq1yCWzpH95JuRUJIC0EDPH5nNGtIkkA4GgvROBocpEEKLCCBwVj0BRF/CJHFYhEo9WCbF1TCdgEEgF0A0Ee8NxioIeN97QzQqFMd2tdfIJC3KeK0T3eJYu0J07g6BVbCB0IiDVDNsQ1mFcbNxDCTk6IWEb2ShHfHxUlvAjkfj0mHDhC56GAL4CWMUgQXgEywDxuH0TBAD7gDZuRqtx7KWpnyTbushlJUpytdfnUvoS/pXG880npIYe3wueUdIJoa9HlRgdsYiF5QJv8C2zjIbzXERGQmwH0QylmjJfC4evBB8UUKQZMsAMG2aWMU6nc6s9m7X4Thn0gTfomgnm5d0qwX4v0rQH3GZn4Ajp8F2VeUcTTARpA+FfyLcpc+T05bOemT2fny8EH8Vn4LPFh3htyOtB3jDSJj34IpEQ3HNboUdasWNDQifcA8BfPPkTe6YaWp0nF/IrhQHGW2D5HTO7O2zfTH3+gxip/NioTs9VwUXL7T3AbzTxHa3qSu1e4EZTfZl/QiC2c7UI5jZ/ET938pSH8Z8IPBwU0NopeLgB7h6Kvp0GVCOw72KAjKFA71sPKX7/9g+Js/AmNfj8/o28sqNVdSTVI93p08F3v/75zqw8W79vb0RVaCTrw6aNntrQwCtbzzDKosTRFMjp/WFqtpZUEGxsi6P8L09byvlyrrvUJ6/ZFJR/X32mbUmndlduWjbdnwnY2ZBHo8OIKIVDUJah62hi4aKdSoqZsWypN7d0w6nsAzb12tWrqZOl12+W/W7YyLFxDy/7U369cgFF85PUVevYahz8y/HS9ZGrbv7saR0sn5MfEzhinC2Dizcv5xHycyChG33pcskigbRkvXnDaurRjRuIeDdu4rnSgPQ/L196FHQg6FGs7266c82aTtDT1jU0CqzWoG2Ndf91wRo1g/0wo9b4VPtV+2iwl/fjvxq4f83CBZeYgx6njp8mb7jzou9FfPdwBBpffvyUx6XARoc/1umGwtrl034lryLH/YCEwly/XrrckYHsd+/YWY/u3EGI085rV6RD5+Bw7dqnoAvBjzifw3S3zdaNZL/dRnfz7XZup232DX4VtD6Cn+AzkqFgBq6unr/gwtCDuydN51fk76ocHS/nN25Y/WqMe1fzBRgEQHPEjqE0gIbkR1CKM/zYUukn9ItRVMHwLfuO1kaP2mlUivpAUpbb8f5wZS1eib+cs3/qlD9r8DU2NEccqhPVFos3SRGSKtb4hyJEcX6VZhArj8Y+edgVpHICKD9tt8ddsvuYpNLZfQGoyBiY2CzKm1chkFmHUGwbUityTs70kCCSE2DZZADRaSeo0heYTpdQ3vwIAv0+QagzEKTOQnnOzHzoXTMkrCJYy6q7Wb1GNPO6hLi6keVYaDeqpDDFGarGkqy3sLFRMXFPDjZjqYsD5A6BI4RneUk0sdlwM2w0iqxFEtuwhkTpCLHER0fzWQ+I0ogmcLVPgqkQmBZLrdvC1tMQmfGTE66J3y+HCdoZqUgFBd/Y1TCJTL92VqwoMRVQOUxzpYJTiZd1EHAIyXmskS4RmbCySY4ZpVPEsmRv1QbTIKLoGtgt4kVTI74qM2p4tulMzwFS4qPiUDFxCSSUSGJJKJd2ozFS1kgYmyN1snOnimh0brybVuw0G0WV9iF3xeYjFAg4LcEi4Q692C7TUI8omiJRZAN3M+4ikTLBlosAAAA=')
|
||||
format('woff2'),
|
||||
url('iconfont.woff?t=1618544337340') format('woff'),
|
||||
url('iconfont.ttf?t=1618544337340') format('truetype'),
|
||||
/* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ url('iconfont.svg?t=1618544337340#iconfont') format('svg'); /* iOS 4.1- */
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-full-screen-hs::before {
|
||||
content: '\e656';
|
||||
}
|
||||
|
||||
.icon-watch-hs::before {
|
||||
content: '\e766';
|
||||
}
|
||||
|
||||
.icon-download-hs::before {
|
||||
content: '\e6af';
|
||||
}
|
||||
|
||||
.icon-enlarge-hs::before {
|
||||
content: '\e765';
|
||||
}
|
||||
|
||||
.icon-previous-hs::before {
|
||||
content: '\e84c';
|
||||
}
|
||||
|
||||
.icon-zoom-out-hs::before {
|
||||
content: '\e744';
|
||||
}
|
||||
|
||||
.icon-next-step-hs::before {
|
||||
content: '\e84b';
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"id": "2491438",
|
||||
"name": "liu'c'tu",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "755619",
|
||||
"name": "自适应图标",
|
||||
"font_class": "full-screen-hs",
|
||||
"unicode": "e656",
|
||||
"unicode_decimal": 58966
|
||||
},
|
||||
{
|
||||
"icon_id": "14445801",
|
||||
"name": "查看",
|
||||
"font_class": "watch-hs",
|
||||
"unicode": "e766",
|
||||
"unicode_decimal": 59238
|
||||
},
|
||||
{
|
||||
"icon_id": "9712640",
|
||||
"name": "下载",
|
||||
"font_class": "download-hs",
|
||||
"unicode": "e6af",
|
||||
"unicode_decimal": 59055
|
||||
},
|
||||
{
|
||||
"icon_id": "1029099",
|
||||
"name": "放大",
|
||||
"font_class": "enlarge-hs",
|
||||
"unicode": "e765",
|
||||
"unicode_decimal": 59237
|
||||
},
|
||||
{
|
||||
"icon_id": "20017362",
|
||||
"name": "上一步",
|
||||
"font_class": "previous-hs",
|
||||
"unicode": "e84c",
|
||||
"unicode_decimal": 59468
|
||||
},
|
||||
{
|
||||
"icon_id": "1010015",
|
||||
"name": "缩小",
|
||||
"font_class": "zoom-out-hs",
|
||||
"unicode": "e744",
|
||||
"unicode_decimal": 59204
|
||||
},
|
||||
{
|
||||
"icon_id": "20017363",
|
||||
"name": "下一步",
|
||||
"font_class": "next-step-hs",
|
||||
"unicode": "e84b",
|
||||
"unicode_decimal": 59467
|
||||
}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 8.1 KiB |
|
@ -0,0 +1,55 @@
|
|||
export const nodeList = [
|
||||
{
|
||||
text: '开始',
|
||||
type: 'start',
|
||||
class: 'node-start',
|
||||
},
|
||||
{
|
||||
text: '矩形',
|
||||
type: 'rect',
|
||||
class: 'node-rect',
|
||||
},
|
||||
{
|
||||
type: 'user',
|
||||
text: '用户',
|
||||
class: 'node-user',
|
||||
},
|
||||
{
|
||||
type: 'push',
|
||||
text: '推送',
|
||||
class: 'node-push',
|
||||
},
|
||||
{
|
||||
type: 'download',
|
||||
text: '位置',
|
||||
class: 'node-download',
|
||||
},
|
||||
{
|
||||
type: 'end',
|
||||
text: '结束',
|
||||
class: 'node-end',
|
||||
},
|
||||
];
|
||||
|
||||
export const BpmnNode = [
|
||||
{
|
||||
type: 'bpmn:startEvent',
|
||||
text: '开始',
|
||||
class: 'bpmn-start',
|
||||
},
|
||||
{
|
||||
type: 'bpmn:endEvent',
|
||||
text: '结束',
|
||||
class: 'bpmn-end',
|
||||
},
|
||||
{
|
||||
type: 'bpmn:exclusiveGateway',
|
||||
text: '网关',
|
||||
class: 'bpmn-exclusiveGateway',
|
||||
},
|
||||
{
|
||||
type: 'bpmn:userTask',
|
||||
text: '用户',
|
||||
class: 'bpmn-user',
|
||||
},
|
||||
];
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* 想要哪个图标集 自行添加即可 请注意此处添加的图标集均为在线图标(https://iconify.design/docs/api/#public-api)
|
||||
* 如果项目在内网环境下 参考 https://www.bilibili.com/video/BV17S4y1J79d?p=4&vd_source=5a992808de6229d78e7810536c5f9ab3 教程自行离线部署图标
|
||||
* https://icones.js.org/collections/图标集前缀名-meta.json(如:https://icones.js.org/collections/ri-meta.json 取icons字段,可获得当前图标集的所有图标)
|
||||
*/
|
||||
export const IconJson = {
|
||||
// https://icones.js.org/collections/ep-meta.json
|
||||
'ep:': [
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import iconifyIconOffline from './src/iconifyIconOffline';
|
||||
import iconifyIconOnline from './src/iconifyIconOnline';
|
||||
import iconSelect from './src/Select.vue';
|
||||
import fontIcon from './src/iconfont';
|
||||
|
||||
/** 本地图标组件 */
|
||||
const IconifyIconOffline = iconifyIconOffline;
|
||||
/** 在线图标组件 */
|
||||
const IconifyIconOnline = iconifyIconOnline;
|
||||
/** `IconSelect`图标选择器组件 */
|
||||
const IconSelect = iconSelect;
|
||||
/** `iconfont`组件 */
|
||||
const FontIcon = fontIcon;
|
||||
|
||||
export { IconifyIconOffline, IconifyIconOnline, FontIcon };
|
||||
export { IconifyIconOffline, IconifyIconOnline, IconSelect, FontIcon };
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { IconJson } from '@/components/ReIcon/data';
|
||||
import { cloneDeep, isAllEmpty } from '@pureadmin/utils';
|
||||
import { computed, CSSProperties, ref, watch } from 'vue';
|
||||
import Search from '@iconify-icons/ri/search-eye-line';
|
||||
import { ref, computed, CSSProperties, watch } from 'vue';
|
||||
import Search from '~icons/ri/search-eye-line';
|
||||
|
||||
type ParameterCSSProperties = (item?: string) => CSSProperties | undefined;
|
||||
|
||||
|
@ -40,12 +40,10 @@ const tabsList = [
|
|||
},
|
||||
];
|
||||
|
||||
const pageList = computed(
|
||||
() =>
|
||||
currentActiveType.value !== 'web' &&
|
||||
copyIconList[currentActiveType.value]
|
||||
.filter((i) => i.includes(filterValue.value))
|
||||
.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)
|
||||
const pageList = computed(() =>
|
||||
copyIconList[currentActiveType.value]
|
||||
.filter((i) => i.includes(filterValue.value))
|
||||
.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)
|
||||
);
|
||||
|
||||
const iconItemStyle = computed((): ParameterCSSProperties => {
|
||||
|
@ -97,7 +95,7 @@ function onClear() {
|
|||
|
||||
watch(
|
||||
() => pageList.value,
|
||||
() => currentActiveType.value !== 'web' && (totalPage.value = copyIconList[currentActiveType.value].filter((i) => i.includes(filterValue.value)).length),
|
||||
() => (totalPage.value = copyIconList[currentActiveType.value].filter((i) => i.includes(filterValue.value)).length),
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(
|
||||
|
@ -116,12 +114,12 @@ watch(
|
|||
<el-input v-model="inputValue" disabled>
|
||||
<template #append>
|
||||
<el-popover
|
||||
:width="350"
|
||||
trigger="click"
|
||||
popper-class="pure-popper"
|
||||
:popper-options="{
|
||||
placement: 'auto',
|
||||
}"
|
||||
:width="350"
|
||||
popper-class="pure-popper"
|
||||
trigger="click"
|
||||
@before-enter="onBeforeEnter"
|
||||
@after-leave="onAfterLeave"
|
||||
>
|
||||
|
@ -132,41 +130,42 @@ watch(
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<el-input v-model="filterValue" class="px-2 pt-2" clearable placeholder="搜索图标" />
|
||||
<el-input v-model="filterValue" class="px-2 pt-2" 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">
|
||||
<ul class="flex flex-wrap px-2! ml-2!">
|
||||
<li
|
||||
v-for="(item, key) in pageList"
|
||||
:key="key"
|
||||
:style="iconItemStyle(item)"
|
||||
:title="item"
|
||||
class="icon-item p-2 cursor-pointer mr-2 mt-1 flex justify-center items-center border border-[#e5e7eb]"
|
||||
:style="iconItemStyle(item)"
|
||||
@click="onChangeIcon(item)"
|
||||
>
|
||||
<IconifyIconOnline :icon="currentActiveType + item" height="20px" width="20px" />
|
||||
<IconifyIconOnline :icon="currentActiveType + item" width="20px" height="20px" />
|
||||
</li>
|
||||
</ul>
|
||||
<el-empty v-show="pageList.length === 0" :description="`${filterValue} 图标不存在`" :image-size="60" />
|
||||
</el-scrollbar>
|
||||
<div class="w-full h-9 flex items-center overflow-auto border-t border-[#e5e7eb]">
|
||||
<el-pagination
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:pager-count="5"
|
||||
:total="totalPage"
|
||||
background
|
||||
class="flex-auto ml-2"
|
||||
layout="pager"
|
||||
size="small"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
<el-button bg class="justify-end mr-2 ml-2" size="small" text type="danger" @click="onClear">清空</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="w-full h-9 flex items-center overflow-auto border-t border-[#e5e7eb]">
|
||||
<el-pagination
|
||||
class="flex-auto ml-2"
|
||||
:total="totalPage"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:pager-count="5"
|
||||
layout="pager"
|
||||
background
|
||||
size="small"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
<el-button class="justify-end mx-2!" type="danger" size="small" text bg @click="onClear">清空</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-input>
|
||||
|
@ -178,8 +177,8 @@ watch(
|
|||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
transition: all 0.4s;
|
||||
transform: scaleX(1.05);
|
||||
transition: all 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { iconType } from './types';
|
||||
import { type Component, defineComponent, h } from 'vue';
|
||||
import { FontIcon, IconifyIconOffline, IconifyIconOnline } from '../index';
|
||||
import { h, defineComponent, type Component } from 'vue';
|
||||
import { FontIcon, IconifyIconOnline, IconifyIconOffline } from '../index';
|
||||
|
||||
/**
|
||||
* 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标
|
||||
* @see 点击查看文档图标篇 {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/}
|
||||
* @see 点击查看文档图标篇 {@link https://pure-admin.cn/pages/icon/}
|
||||
* @param icon 必传 图标
|
||||
* @param attrs 可选 iconType 属性
|
||||
* @returns Component
|
||||
|
@ -46,9 +46,10 @@ export function useRenderIcon(icon: any, attrs?: iconType): Component {
|
|||
return defineComponent({
|
||||
name: 'Icon',
|
||||
render() {
|
||||
const IconifyIcon = icon && icon.includes(':') ? IconifyIconOnline : IconifyIconOffline;
|
||||
if (!icon) return;
|
||||
const IconifyIcon = icon.includes(':') ? IconifyIconOnline : IconifyIconOffline;
|
||||
return h(IconifyIcon, {
|
||||
icon: icon,
|
||||
icon,
|
||||
...attrs,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { defineComponent, h } from 'vue';
|
||||
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({
|
||||
|
@ -25,7 +25,6 @@ export default defineComponent({
|
|||
'svg',
|
||||
{
|
||||
class: 'icon-svg',
|
||||
'aria-hidden': false,
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { defineComponent, h } from 'vue';
|
||||
import { addIcon, Icon as IconifyIcon } from '@iconify/vue/dist/offline';
|
||||
import { h, defineComponent } from 'vue';
|
||||
import { Icon as IconifyIcon, addIcon } from '@iconify/vue/dist/offline';
|
||||
|
||||
// Iconify Icon在Vue里本地使用(用于内网环境)
|
||||
export default defineComponent({
|
||||
|
@ -13,16 +13,31 @@ export default defineComponent({
|
|||
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: () => [],
|
||||
}
|
||||
);
|
||||
if (typeof this.icon === 'string') {
|
||||
return h(
|
||||
IconifyIcon,
|
||||
{
|
||||
icon: this.icon,
|
||||
'aria-hidden': false,
|
||||
style: attrs?.style ? Object.assign(attrs.style, { outline: 'none' }) : { outline: 'none' },
|
||||
...attrs,
|
||||
},
|
||||
{
|
||||
default: () => [],
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return h(
|
||||
this.icon,
|
||||
{
|
||||
'aria-hidden': false,
|
||||
style: attrs?.style ? Object.assign(attrs.style, { outline: 'none' }) : { outline: 'none' },
|
||||
...attrs,
|
||||
},
|
||||
{
|
||||
default: () => [],
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { defineComponent, h } from 'vue';
|
||||
import { h, defineComponent } from 'vue';
|
||||
import { Icon as IconifyIcon } from '@iconify/vue';
|
||||
|
||||
// Iconify Icon在Vue里在线使用(用于外网环境)
|
||||
|
@ -17,6 +17,7 @@ export default defineComponent({
|
|||
IconifyIcon,
|
||||
{
|
||||
icon: `${this.icon}`,
|
||||
'aria-hidden': false,
|
||||
style: attrs?.style ? Object.assign(attrs.style, { outline: 'none' }) : { outline: 'none' },
|
||||
...attrs,
|
||||
},
|
||||
|
|
|
@ -1,16 +1,87 @@
|
|||
// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
|
||||
import { getSvgInfo } from '@pureadmin/utils';
|
||||
import { addIcon } from '@iconify/vue/dist/offline';
|
||||
|
||||
// https://icon-sets.iconify.design/ep/?keyword=ep
|
||||
import EpMenu from '~icons/ep/menu?raw';
|
||||
import EpEdit from '~icons/ep/edit?raw';
|
||||
import EpGuide from '~icons/ep/guide?raw';
|
||||
import EpSetUp from '~icons/ep/set-up?raw';
|
||||
import EpMonitor from '~icons/ep/monitor?raw';
|
||||
import EpLollipop from '~icons/ep/lollipop?raw';
|
||||
import EpHistogram from '~icons/ep/histogram?raw';
|
||||
import EpHomeFilled from '~icons/ep/home-filled?raw';
|
||||
|
||||
// https://icon-sets.iconify.design/ri/?keyword=ri
|
||||
import RiMindMap from '~icons/ri/mind-map?raw';
|
||||
import RiAdminFill from '~icons/ri/admin-fill?raw';
|
||||
import RiTableLine from '~icons/ri/table-line?raw';
|
||||
import RiLinksFill from '~icons/ri/links-fill?raw';
|
||||
import RiAdminLine from '~icons/ri/admin-line?raw';
|
||||
import RiListCheck from '~icons/ri/list-check?raw';
|
||||
import RiSearchLine from '~icons/ri/search-line?raw';
|
||||
import RiWindowLine from '~icons/ri/window-line?raw';
|
||||
import RiUbuntuFill from '~icons/ri/ubuntu-fill?raw';
|
||||
import RiHistoryFill from '~icons/ri/history-fill?raw';
|
||||
import RiEditBoxLine from '~icons/ri/edit-box-line?raw';
|
||||
import RiCodeBoxLine from '~icons/ri/code-box-line?raw';
|
||||
import RiArtboardLine from '~icons/ri/artboard-line?raw';
|
||||
import RiMarkdownLine from '~icons/ri/markdown-line?raw';
|
||||
import RiFileInfoLine from '~icons/ri/file-info-line?raw';
|
||||
import RiBankCardLine from '~icons/ri/bank-card-line?raw';
|
||||
import RiFilePpt2Line from '~icons/ri/file-ppt-2-line?raw';
|
||||
import RiGitBranchLine from '~icons/ri/git-branch-line?raw';
|
||||
import RiSettings3Line from '~icons/ri/settings-3-line?raw';
|
||||
import RiUserVoiceLine from '~icons/ri/user-voice-line?raw';
|
||||
import RiBookmark2Line from '~icons/ri/bookmark-2-line?raw';
|
||||
import RiFileSearchLine from '~icons/ri/file-search-line?raw';
|
||||
import RiChatSearchLine from '~icons/ri/chat-search-line?raw';
|
||||
import RiInformationLine from '~icons/ri/information-line?raw';
|
||||
import RiTerminalWindowLine from '~icons/ri/terminal-window-line?raw';
|
||||
import RiCheckboxCircleLine from '~icons/ri/checkbox-circle-line?raw';
|
||||
import RiBarChartHorizontalLine from '~icons/ri/bar-chart-horizontal-line?raw';
|
||||
|
||||
const icons = [
|
||||
// Element Plus Icon: https://github.com/element-plus/element-plus-icons
|
||||
['ep/menu', EpMenu],
|
||||
['ep/edit', EpEdit],
|
||||
['ep/guide', EpGuide],
|
||||
['ep/set-up', EpSetUp],
|
||||
['ep/monitor', EpMonitor],
|
||||
['ep/lollipop', EpLollipop],
|
||||
['ep/histogram', EpHistogram],
|
||||
['ep/home-filled', EpHomeFilled],
|
||||
// Remix Icon: https://github.com/Remix-Design/RemixIcon
|
||||
['ri/mind-map', RiMindMap],
|
||||
['ri/admin-fill', RiAdminFill],
|
||||
['ri/table-line', RiTableLine],
|
||||
['ri/links-fill', RiLinksFill],
|
||||
['ri/admin-line', RiAdminLine],
|
||||
['ri/list-check', RiListCheck],
|
||||
['ri/search-line', RiSearchLine],
|
||||
['ri/window-line', RiWindowLine],
|
||||
['ri/ubuntu-fill', RiUbuntuFill],
|
||||
['ri/history-fill', RiHistoryFill],
|
||||
['ri/edit-box-line', RiEditBoxLine],
|
||||
['ri/code-box-line', RiCodeBoxLine],
|
||||
['ri/artboard-line', RiArtboardLine],
|
||||
['ri/markdown-line', RiMarkdownLine],
|
||||
['ri/file-info-line', RiFileInfoLine],
|
||||
['ri/bank-card-line', RiBankCardLine],
|
||||
['ri/file-ppt-2-line', RiFilePpt2Line],
|
||||
['ri/git-branch-line', RiGitBranchLine],
|
||||
['ri/settings-3-line', RiSettings3Line],
|
||||
['ri/user-voice-line', RiUserVoiceLine],
|
||||
['ri/bookmark-2-line', RiBookmark2Line],
|
||||
['ri/file-search-line', RiFileSearchLine],
|
||||
['ri/chat-search-line', RiChatSearchLine],
|
||||
['ri/information-line', RiInformationLine],
|
||||
['ri/terminal-window-line', RiTerminalWindowLine],
|
||||
['ri/checkbox-circle-line', RiCheckboxCircleLine],
|
||||
['ri/bar-chart-horizontal-line', RiBarChartHorizontalLine],
|
||||
];
|
||||
|
||||
// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标
|
||||
// @iconify-icons/ep
|
||||
import Lollipop from '@iconify-icons/ep/lollipop';
|
||||
import HomeFilled from '@iconify-icons/ep/home-filled';
|
||||
// @iconify-icons/ri
|
||||
import Search from '@iconify-icons/ri/search-line';
|
||||
import InformationLine from '@iconify-icons/ri/information-line';
|
||||
|
||||
addIcon('ep:lollipop', Lollipop);
|
||||
addIcon('ep:home-filled', HomeFilled);
|
||||
|
||||
addIcon('ri:search-line', Search);
|
||||
addIcon('ri:information-line', InformationLine);
|
||||
icons.forEach(([name, icon]) => {
|
||||
addIcon(name as string, getSvgInfo(icon as string));
|
||||
});
|
||||
|
|
|
@ -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,85 @@
|
|||
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,40 @@
|
|||
<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 perms from './src/perms';
|
||||
|
||||
const Perms = perms;
|
||||
|
||||
export { Perms };
|
|
@ -0,0 +1,18 @@
|
|||
import { defineComponent, Fragment } from 'vue';
|
||||
import { hasPerms } from '@/utils/auth';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Perms',
|
||||
props: {
|
||||
value: {
|
||||
type: undefined,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => {
|
||||
if (!slots) return null;
|
||||
return hasPerms(props.value) ? <Fragment>{slots.default?.()}</Fragment> : null;
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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,283 @@
|
|||
import { $t } from '@/plugins/i18n';
|
||||
import { useEpThemeStoreHook } from '@/store/modules/epTheme';
|
||||
import { cloneDeep, delay, getKeyList, isBoolean, isFunction } from '@pureadmin/utils';
|
||||
import Sortable from 'sortablejs';
|
||||
import { computed, defineComponent, getCurrentInstance, nextTick, type PropType, ref, unref } from 'vue';
|
||||
|
||||
import CollapseIcon from '@/assets/table-bar/collapse.svg?component';
|
||||
import DragIcon from '@/assets/table-bar/drag.svg?component';
|
||||
import ExpandIcon from '@/assets/table-bar/expand.svg?component';
|
||||
import RefreshIcon from '@/assets/table-bar/refresh.svg?component';
|
||||
import SettingIcon from '@/assets/table-bar/settings.svg?component';
|
||||
import ExitFullscreen from '~icons/ri/fullscreen-exit-fill';
|
||||
import Fullscreen from '~icons/ri/fullscreen-fill';
|
||||
|
||||
const props = {
|
||||
/** 头部最左边的标题 */
|
||||
title: {
|
||||
type: String,
|
||||
default: '列表',
|
||||
},
|
||||
/** 对于树形表格,如果想启用展开和折叠功能,传入当前表格的ref即可 */
|
||||
tableRef: {
|
||||
type: Object as PropType<any>,
|
||||
},
|
||||
/** 需要展示的列 */
|
||||
columns: {
|
||||
type: Array as PropType<TableColumnList>,
|
||||
default: () => [],
|
||||
},
|
||||
isExpandAll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
tableKey: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: '0',
|
||||
},
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PureTableBar',
|
||||
props,
|
||||
emits: ['refresh', 'fullscreen'],
|
||||
setup(props, { emit, slots, attrs }) {
|
||||
const size = ref('default');
|
||||
const loading = ref(false);
|
||||
const checkAll = ref(true);
|
||||
const isFullscreen = ref(false);
|
||||
const isIndeterminate = ref(false);
|
||||
const instance = getCurrentInstance()!;
|
||||
const isExpandAll = ref(props.isExpandAll);
|
||||
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-hidden'];
|
||||
});
|
||||
|
||||
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 onFullscreen() {
|
||||
isFullscreen.value = !isFullscreen.value;
|
||||
emit('fullscreen', isFullscreen.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[]) {
|
||||
checkedColumns.value = value;
|
||||
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 = (instance?.proxy?.$refs[`GroupRef${unref(props.tableKey)}`] as any).$el.firstElementChild;
|
||||
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;
|
||||
};
|
||||
|
||||
const rendTippyProps = (content: string) => {
|
||||
// https://vue-tippy.netlify.app/props
|
||||
return {
|
||||
content,
|
||||
offset: [0, 18],
|
||||
duration: [300, 0],
|
||||
followCursor: true,
|
||||
hideOnClick: 'toggle',
|
||||
};
|
||||
};
|
||||
|
||||
const reference = {
|
||||
reference: () => <SettingIcon class={['w-[16px]', iconClass.value]} v-tippy={rendTippyProps('列设置')} />,
|
||||
};
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div {...attrs} class={['w-full', 'px-2', 'pb-2', 'bg-bg_color', isFullscreen.value ? ['h-full!', 'z-2002', 'fixed', 'inset-0'] : 'mt-2']}>
|
||||
<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 ? (
|
||||
<>
|
||||
<ExpandIcon
|
||||
class={['w-[16px]', iconClass.value]}
|
||||
style={{
|
||||
transform: isExpandAll.value ? 'none' : 'rotate(-90deg)',
|
||||
}}
|
||||
v-tippy={rendTippyProps(isExpandAll.value ? '折叠' : '展开')}
|
||||
onClick={() => onExpand()}
|
||||
/>
|
||||
<el-divider direction="vertical" />
|
||||
</>
|
||||
) : null}
|
||||
<RefreshIcon
|
||||
class={['w-[16px]', iconClass.value, loading.value ? 'animate-spin' : '']}
|
||||
v-tippy={rendTippyProps('刷新')}
|
||||
onClick={() => onReFresh()}
|
||||
/>
|
||||
<el-divider direction="vertical" />
|
||||
<el-dropdown v-slots={dropdown} trigger="click" v-tippy={rendTippyProps('密度')}>
|
||||
<CollapseIcon class={['w-[16px]', iconClass.value]} />
|
||||
</el-dropdown>
|
||||
<el-divider direction="vertical" />
|
||||
|
||||
<el-popover v-slots={reference} placement="bottom-start" popper-style={{ padding: 0 }} width="200" 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()}>
|
||||
{$t('buttons.reset')}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="pt-[6px] pl-[11px]">
|
||||
<el-scrollbar max-height="36vh">
|
||||
<el-checkbox-group
|
||||
ref={`GroupRef${unref(props.tableKey)}`}
|
||||
modelValue={checkedColumns.value}
|
||||
onChange={(value) => handleCheckedColumnsChange(value)}
|
||||
>
|
||||
<el-space direction="vertical" alignment="flex-start" size={0}>
|
||||
{checkColumnList.map((item, index) => {
|
||||
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={index} label={item} value={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>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-popover>
|
||||
<el-divider direction="vertical" />
|
||||
|
||||
<iconifyIconOffline
|
||||
class={['w-[16px]', iconClass.value]}
|
||||
icon={isFullscreen.value ? ExitFullscreen : Fullscreen}
|
||||
v-tippy={isFullscreen.value ? '退出全屏' : '全屏'}
|
||||
onClick={() => onFullscreen()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{slots.default({
|
||||
size: size.value,
|
||||
dynamicColumns: dynamicColumns.value,
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import reQrcode from './src/index';
|
||||
import { withInstall } from '@pureadmin/utils';
|
||||
|
||||
/** 二维码组件 */
|
||||
export const ReQrcode = withInstall(reQrcode);
|
||||
|
||||
export default ReQrcode;
|
|
@ -0,0 +1,9 @@
|
|||
.qrcode {
|
||||
&--disabled {
|
||||
background: rgb(255 255 255 / 95%);
|
||||
|
||||
& > div {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
import { type PropType, ref, unref, watch, nextTick, computed, defineComponent } from 'vue';
|
||||
import './index.scss';
|
||||
import propTypes from '@/utils/propTypes';
|
||||
import { isString, cloneDeep } from '@pureadmin/utils';
|
||||
import QRCode, { type QRCodeRenderersOptions } from 'qrcode';
|
||||
import RefreshRight from '~icons/ep/refresh-right';
|
||||
|
||||
interface QrcodeLogo {
|
||||
src?: string;
|
||||
logoSize?: number;
|
||||
bgColor?: string;
|
||||
borderSize?: number;
|
||||
crossOrigin?: string;
|
||||
borderRadius?: number;
|
||||
logoRadius?: number;
|
||||
}
|
||||
|
||||
const props = {
|
||||
// img 或者 canvas,img不支持logo嵌套
|
||||
tag: propTypes.string.validate((v: string) => ['canvas', 'img'].includes(v)).def('canvas'),
|
||||
// 二维码内容
|
||||
text: {
|
||||
type: [String, Array] as PropType<string | Recordable[]>,
|
||||
default: null,
|
||||
},
|
||||
// qrcode.js配置项
|
||||
options: {
|
||||
type: Object as PropType<QRCodeRenderersOptions>,
|
||||
default: (): QRCodeRenderersOptions => ({}),
|
||||
},
|
||||
// 宽度
|
||||
width: propTypes.number.def(200),
|
||||
// logo
|
||||
logo: {
|
||||
type: [String, Object] as PropType<Partial<QrcodeLogo> | string>,
|
||||
default: (): QrcodeLogo | string => '',
|
||||
},
|
||||
// 是否过期
|
||||
disabled: propTypes.bool.def(false),
|
||||
// 过期提示内容
|
||||
disabledText: propTypes.string.def(''),
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ReQrcode',
|
||||
props,
|
||||
emits: ['done', 'click', 'disabled-click'],
|
||||
setup(props, { emit }) {
|
||||
const { toCanvas, toDataURL } = QRCode;
|
||||
const loading = ref(true);
|
||||
const wrapRef = ref<Nullable<HTMLCanvasElement | HTMLImageElement>>(null);
|
||||
const renderText = computed(() => String(props.text));
|
||||
const wrapStyle = computed(() => {
|
||||
return {
|
||||
width: props.width + 'px',
|
||||
height: props.width + 'px',
|
||||
};
|
||||
});
|
||||
const initQrcode = async () => {
|
||||
await nextTick();
|
||||
const options = cloneDeep(props.options || {});
|
||||
if (props.tag === 'canvas') {
|
||||
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
|
||||
options.errorCorrectionLevel = options.errorCorrectionLevel || getErrorCorrectionLevel(unref(renderText));
|
||||
const _width: number = await getOriginWidth(unref(renderText), options);
|
||||
options.scale = props.width === 0 ? undefined : (props.width / _width) * 4;
|
||||
const canvasRef: any = await toCanvas(unref(wrapRef) as HTMLCanvasElement, unref(renderText), options);
|
||||
if (props.logo) {
|
||||
const url = await createLogoCode(canvasRef);
|
||||
emit('done', url);
|
||||
loading.value = false;
|
||||
} else {
|
||||
emit('done', canvasRef.toDataURL());
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
const url = await toDataURL(renderText.value, {
|
||||
errorCorrectionLevel: 'H',
|
||||
width: props.width,
|
||||
...options,
|
||||
});
|
||||
(unref(wrapRef) as any).src = url;
|
||||
emit('done', url);
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
watch(
|
||||
() => renderText.value,
|
||||
(val) => {
|
||||
if (!val) return;
|
||||
initQrcode();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
const createLogoCode = (canvasRef: HTMLCanvasElement) => {
|
||||
const canvasWidth = canvasRef.width;
|
||||
const logoOptions: QrcodeLogo = Object.assign(
|
||||
{
|
||||
logoSize: 0.15,
|
||||
bgColor: '#ffffff',
|
||||
borderSize: 0.05,
|
||||
crossOrigin: 'anonymous',
|
||||
borderRadius: 8,
|
||||
logoRadius: 0,
|
||||
},
|
||||
isString(props.logo) ? {} : props.logo
|
||||
);
|
||||
const { logoSize = 0.15, bgColor = '#ffffff', borderSize = 0.05, crossOrigin = 'anonymous', borderRadius = 8, logoRadius = 0 } = logoOptions;
|
||||
const logoSrc = isString(props.logo) ? props.logo : props.logo.src;
|
||||
const logoWidth = canvasWidth * logoSize;
|
||||
const logoXY = (canvasWidth * (1 - logoSize)) / 2;
|
||||
const logoBgWidth = canvasWidth * (logoSize + borderSize);
|
||||
const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2;
|
||||
const ctx = canvasRef.getContext('2d');
|
||||
if (!ctx) return;
|
||||
// logo 底色
|
||||
canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius);
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fill();
|
||||
// logo
|
||||
const image = new Image();
|
||||
if (crossOrigin || logoRadius) {
|
||||
image.setAttribute('crossOrigin', crossOrigin);
|
||||
}
|
||||
(image as any).src = logoSrc;
|
||||
// 使用image绘制可以避免某些跨域情况
|
||||
const drawLogoWithImage = (image: HTMLImageElement) => {
|
||||
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
|
||||
};
|
||||
// 使用canvas绘制以获得更多的功能
|
||||
const drawLogoWithCanvas = (image: HTMLImageElement) => {
|
||||
const canvasImage = document.createElement('canvas');
|
||||
canvasImage.width = logoXY + logoWidth;
|
||||
canvasImage.height = logoXY + logoWidth;
|
||||
const imageCanvas = canvasImage.getContext('2d');
|
||||
if (!imageCanvas || !ctx) return;
|
||||
imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
|
||||
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius);
|
||||
if (!ctx) return;
|
||||
const fillStyle = ctx.createPattern(canvasImage, 'no-repeat');
|
||||
if (fillStyle) {
|
||||
ctx.fillStyle = fillStyle;
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
// 将 logo绘制到 canvas上
|
||||
return new Promise((resolve: any) => {
|
||||
image.onload = () => {
|
||||
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image);
|
||||
resolve(canvasRef.toDataURL());
|
||||
};
|
||||
});
|
||||
};
|
||||
// 得到原QrCode的大小,以便缩放得到正确的QrCode大小
|
||||
const getOriginWidth = async (content: string, options: QRCodeRenderersOptions) => {
|
||||
const _canvas = document.createElement('canvas');
|
||||
await toCanvas(_canvas, content, options);
|
||||
return _canvas.width;
|
||||
};
|
||||
// 对于内容少的QrCode,增大容错率
|
||||
const getErrorCorrectionLevel = (content: string) => {
|
||||
if (content.length > 36) {
|
||||
return 'M';
|
||||
} else if (content.length > 16) {
|
||||
return 'Q';
|
||||
} else {
|
||||
return 'H';
|
||||
}
|
||||
};
|
||||
// 用于绘制圆角
|
||||
const canvasRoundRect = (ctx: CanvasRenderingContext2D) => {
|
||||
return (x: number, y: number, w: number, h: number, r: number) => {
|
||||
const minSize = Math.min(w, h);
|
||||
if (r > minSize / 2) {
|
||||
r = minSize / 2;
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
return ctx;
|
||||
};
|
||||
};
|
||||
const clickCode = () => {
|
||||
emit('click');
|
||||
};
|
||||
const disabledClick = () => {
|
||||
emit('disabled-click');
|
||||
};
|
||||
return () => (
|
||||
<>
|
||||
<div v-loading={unref(loading)} class="qrcode relative inline-block" style={unref(wrapStyle)}>
|
||||
{props.tag === 'canvas' ? <canvas ref={wrapRef} onClick={clickCode}></canvas> : <img ref={wrapRef} onClick={clickCode}></img>}
|
||||
{props.disabled && (
|
||||
<div class="qrcode--disabled absolute top-0 left-0 flex w-full h-full items-center justify-center" onClick={disabledClick}>
|
||||
<div class="absolute top-[50%] left-[50%] font-bold">
|
||||
<iconify-icon-offline class="cursor-pointer" icon={RefreshRight} width="30" color="var(--el-color-primary)" />
|
||||
<div>{props.disabledText}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|