Compare commits

...

6 Commits

Author SHA1 Message Date
bunny bca8b5ca6f Merge branch 'dev' 2025-04-29 23:39:02 +08:00
bunny 798893c078 feat: 显示页面调整 2025-04-29 23:31:45 +08:00
bunny d2341b1556 feat: 显示页面调整 2025-04-29 23:21:40 +08:00
bunny edf8d86656 feat: 更新文档 2025-04-29 18:36:50 +08:00
bunny ab2588de54 feat: 与上游合并 2025-04-29 18:18:07 +08:00
bunny c9d9bcd76d feat: 与上游合并 2025-04-29 18:15:27 +08:00
272 changed files with 13457 additions and 5557 deletions

35
.env
View File

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

View File

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

View File

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

16
.env.staging Normal file
View File

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

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
public/wasm/capture.worker.js linguist-language=Vue
public/wasm/index.js linguist-language=Vue

8
.gitignore vendored
View File

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

2
.nvmrc
View File

@ -1 +1 @@
v20.15.0
v22.14.0

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
src/views/system/menu/README.md

View File

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

View File

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

View File

@ -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.logterser打包慢但能去除 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;
};

View File

@ -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';
* 使jscss文件cdn
*/
export const cdn = importToCDN({
//prodUrl解释 name: 对应下面modules的nameversion: 自动读取本地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-demihttps://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的nameversion: 自动读取本地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-demihttps://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"
}
]
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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: {} } : {})
}
};

View File

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

View File

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

View File

@ -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) => {

View File

@ -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',
});
};
/** 系统文件管理---获取所有文件类型 */

View File

@ -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,
});
};

View File

@ -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,
});
};
/** 菜单管理-更新菜单 */

View File

@ -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,
});
};

View File

@ -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,
});
};
/** 用户系统消息---用户获取系统消息列表 */

View File

@ -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,
});
};
/** 任务调度分组---获取所有任务调度分组 */

View File

@ -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,
});
};
// -----------------------------------------

View File

@ -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更新角色列表 */

BIN
src/assets/car.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
</>
);
},
});

View File

@ -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;
},
},
};

View File

@ -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>
</>
);
},
});

View File

@ -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);
},
},
};

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
import reCropperPreview from './src/index.vue';
import { withInstall } from '@pureadmin/utils';
/** 图片裁剪预览组件 */
export const ReCropperPreview = withInstall(reCropperPreview);
export default ReCropperPreview;

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import { withInstall } from '@pureadmin/utils';
import reBarcode from './src/index.vue';
/** 条形码组件 */
export const ReBarcode = withInstall(reBarcode);
export default ReBarcode;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
normal 普通数字动画组件
rebound 回弹式数字动画组件

View File

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

View File

@ -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>>([]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 050%
* @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: () => [],
}
);
},
});
}

View File

@ -0,0 +1,7 @@
import reFlop from './src/index.vue';
import { withInstall } from '@pureadmin/utils';
/** 时间翻牌组件 */
export const ReFlop = withInstall(reFlop);
export default ReFlop;

View File

@ -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';
}

View File

@ -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>
);
},
});

View File

@ -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 => 08h => 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>

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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';
}

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -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',
},
];

View File

@ -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.jsonhttps://icones.js.org/collections/ri-meta.json 取icons字段可获得当前图标集的所有图标
*/
export const IconJson = {
// https://icones.js.org/collections/ep-meta.json
'ep:': [

View File

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

View File

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

View File

@ -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,
});
},

View File

@ -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: () => [

View File

@ -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: () => [],
}
);
}
},
});

View File

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

View File

@ -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));
});

View File

@ -0,0 +1,7 @@
import reImageVerify from './src/index.vue';
import { withInstall } from '@pureadmin/utils';
/** 图形验证码组件 */
export const ReImageVerify = withInstall(reImageVerify);
export default ReImageVerify;

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import perms from './src/perms';
const Perms = perms;
export { Perms };

View File

@ -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;
};
},
});

View File

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

View File

@ -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>
</>
);
},
});

View File

@ -0,0 +1,7 @@
import reQrcode from './src/index';
import { withInstall } from '@pureadmin/utils';
/** 二维码组件 */
export const ReQrcode = withInstall(reQrcode);
export default ReQrcode;

View File

@ -0,0 +1,9 @@
.qrcode {
&--disabled {
background: rgb(255 255 255 / 95%);
& > div {
transform: translate(-50%, -50%);
}
}
}

View File

@ -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>
</>
);
},
});

Some files were not shown because too many files have changed in this diff Show More