init
|
@ -0,0 +1,11 @@
|
|||
# title
|
||||
VITE_GLOB_APP_TITLE = 'GuYue-Admin'
|
||||
|
||||
# 本地运行端口号
|
||||
VITE_PORT = 8088
|
||||
|
||||
# open 运行 npm run dev 时自动打开浏览器
|
||||
VITE_OPEN = false
|
||||
|
||||
# 打包后是否生成包分析文件
|
||||
VITE_REPORT = true
|
|
@ -0,0 +1,19 @@
|
|||
# 本地环境
|
||||
VITE_USER_NODE_ENV = development
|
||||
|
||||
# 公共基础路径
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 是否开启 VitePWA
|
||||
VITE_PWA = false
|
||||
|
||||
# 打包时是否删除 console
|
||||
VITE_DROP_CONSOLE = true
|
||||
|
||||
# 开发环境接口地址
|
||||
VITE_API_URL = /api
|
||||
|
||||
# 开发环境跨域代理,可配置多个
|
||||
VITE_PROXY = [["/api","https://mock.mengxuegu.com/mock/64112a1afe77f949bc0d6ec6/antd"]]
|
||||
# VITE_PROXY = [["/api","https://mock.mengxuegu.com/mock/64112a1afe77f949bc0d6ec6"]]
|
||||
# VITE_PROXY = [["/api-easymock","https://mock.mengxuegu.com"],["/api-fastmock","https://www.fastmock.site"]]
|
|
@ -0,0 +1,18 @@
|
|||
# 线上环境
|
||||
VITE_USER_NODE_ENV = production
|
||||
|
||||
# 公共基础路径
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 是否启用 gzip 或 brotli 压缩打包,如果需要多个压缩规则,可以使用 “,” 分隔
|
||||
# Optional: gzip | brotli | none
|
||||
VITE_BUILD_COMPRESS = none
|
||||
|
||||
# 打包压缩后是否删除源文件
|
||||
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
|
||||
|
||||
# 打包时是否删除 console
|
||||
VITE_DROP_CONSOLE = true
|
||||
|
||||
# 线上环境接口地址
|
||||
VITE_API_URL = " https://mock.mengxuegu.com/mock/64112a1afe77f949bc0d6ec6/antd"
|
|
@ -0,0 +1,18 @@
|
|||
# 测试环境
|
||||
VITE_USER_NODE_ENV = test
|
||||
|
||||
# 公共基础路径
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 是否启用 gzip 或 brotli 压缩打包,如果需要多个压缩规则,可以使用 “,” 分隔
|
||||
# Optional: gzip | brotli | none
|
||||
VITE_BUILD_COMPRESS = none
|
||||
|
||||
# 打包压缩后是否删除源文件
|
||||
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
|
||||
|
||||
# 打包时是否删除 console
|
||||
VITE_DROP_CONSOLE = true
|
||||
|
||||
# 测试环境接口地址
|
||||
VITE_API_URL = " https://mock.mengxuegu.com/mock/64112a1afe77f949bc0d6ec6/antd"
|
|
@ -0,0 +1,20 @@
|
|||
# eslint 忽略检查 (根据项目需要自行添加)
|
||||
*.sh
|
||||
node_modules
|
||||
*.md
|
||||
*.woff
|
||||
*.ttf
|
||||
.vscode
|
||||
.idea
|
||||
dist
|
||||
html
|
||||
/public
|
||||
/docs
|
||||
.husky
|
||||
.local
|
||||
/bin
|
||||
.eslintrc.js
|
||||
.prettierrc.js
|
||||
/src/mock/*
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// @see: http://eslint.cn
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true
|
||||
},
|
||||
/* 指定如何解析语法 */
|
||||
parser: "vue-eslint-parser",
|
||||
/* 优先级低于 parse 的语法解析配置 */
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
jsxPragma: "React",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
/* 继承某些已有的规则 */
|
||||
extends: ["plugin:vue/vue3-recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:prettier/recommended"],
|
||||
/*
|
||||
* "off" 或 0 ==> 关闭规则
|
||||
* "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行)
|
||||
* "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错)
|
||||
*/
|
||||
rules: {
|
||||
// eslint (http://eslint.cn/docs/rules)
|
||||
"no-var": "error", // 要求使用 let 或 const 而不是 var
|
||||
"no-multiple-empty-lines": ["error", { max: 1 }], // 不允许多个空行
|
||||
"no-use-before-define": "off", // 禁止在 函数/类/变量 定义之前使用它们
|
||||
"prefer-const": "off", // 此规则旨在标记使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const
|
||||
"no-irregular-whitespace": "off", // 禁止不规则的空白
|
||||
|
||||
// typeScript (https://typescript-eslint.io/rules)
|
||||
"@typescript-eslint/no-unused-vars": "error", // 禁止定义未使用的变量
|
||||
"@typescript-eslint/prefer-ts-expect-error": "error", // 禁止使用 @ts-ignore
|
||||
"@typescript-eslint/no-inferrable-types": "off", // 可以轻松推断的显式类型可能会增加不必要的冗长
|
||||
"@typescript-eslint/no-namespace": "off", // 禁止使用自定义 TypeScript 模块和命名空间。
|
||||
"@typescript-eslint/no-explicit-any": "off", // 禁止使用 any 类型
|
||||
"@typescript-eslint/ban-types": "off", // 禁止使用特定类型
|
||||
"@typescript-eslint/explicit-function-return-type": "off", // 不允许对初始化为数字、字符串或布尔值的变量或参数进行显式类型声明
|
||||
"@typescript-eslint/no-var-requires": "off", // 不允许在 import 语句中使用 require 语句
|
||||
"@typescript-eslint/no-empty-function": "off", // 禁止空函数
|
||||
"@typescript-eslint/no-use-before-define": "off", // 禁止在变量定义之前使用它们
|
||||
"@typescript-eslint/ban-ts-comment": "off", // 禁止 @ts-<directive> 使用注释或要求在指令后进行描述
|
||||
"@typescript-eslint/no-non-null-assertion": "off", // 不允许使用后缀运算符的非空断言(!)
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off", // 要求导出函数和类的公共类方法的显式返回和参数类型
|
||||
|
||||
// vue (https://eslint.vuejs.org/rules)
|
||||
"vue/no-v-html": "off", // 禁止使用 v-html
|
||||
"vue/script-setup-uses-vars": "error", // 防止<script setup>使用的变量<template>被标记为未使用,此规则仅在启用该no-unused-vars规则时有效。
|
||||
"vue/v-slot-style": "error", // 强制执行 v-slot 指令样式
|
||||
"vue/no-mutating-props": "off", // 不允许组件 prop的改变
|
||||
"vue/custom-event-name-casing": "off", // 为自定义事件名称强制使用特定大小写
|
||||
"vue/attributes-order": "off", // vue api使用顺序,强制执行属性顺序
|
||||
"vue/one-component-per-file": "off", // 强制每个组件都应该在自己的文件中
|
||||
"vue/html-closing-bracket-newline": "off", // 在标签的右括号之前要求或禁止换行
|
||||
"vue/max-attributes-per-line": "off", // 强制每行的最大属性数
|
||||
"vue/multiline-html-element-content-newline": "off", // 在多行元素的内容之前和之后需要换行符
|
||||
"vue/singleline-html-element-content-newline": "off", // 在单行元素的内容之前和之后需要换行符
|
||||
"vue/attribute-hyphenation": "off", // 对模板中的自定义组件强制执行属性命名样式
|
||||
"vue/require-default-prop": "off", // 此规则要求为每个 prop 为必填时,必须提供默认值
|
||||
"vue/multi-word-component-names": "off" // 要求组件名称始终为 “-” 链接的单词
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
html
|
||||
dist-ssr
|
||||
*.local
|
||||
stats.html
|
||||
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:lint-staged
|
|
@ -0,0 +1,9 @@
|
|||
/dist/*
|
||||
/html/*
|
||||
.local
|
||||
/node_modules/**
|
||||
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
|
||||
/public/*
|
|
@ -0,0 +1,39 @@
|
|||
// @see: https://www.prettier.cn
|
||||
|
||||
module.exports = {
|
||||
// 超过最大值换行
|
||||
printWidth: 130,
|
||||
// 缩进字节数
|
||||
tabWidth: 2,
|
||||
// 使用制表符而不是空格缩进行
|
||||
useTabs: true,
|
||||
// 结尾不用分号(true有,false没有)
|
||||
semi: true,
|
||||
// 使用单引号(true单双引号,false双引号)
|
||||
singleQuote: false,
|
||||
// 更改引用对象属性的时间 可选值 "<as-needed|consistent|preserve>"
|
||||
quoteProps: "as-needed",
|
||||
// 在对象,数组括号与文字之间加空格 "{ foo: bar }"
|
||||
bracketSpacing: true,
|
||||
// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
|
||||
trailingComma: "none",
|
||||
// 在JSX中使用单引号而不是双引号
|
||||
jsxSingleQuote: false,
|
||||
// (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号 ,always:不省略括号
|
||||
arrowParens: "avoid",
|
||||
// 如果文件顶部已经有一个 doclock,这个选项将新建一行注释,并打上@format标记。
|
||||
insertPragma: false,
|
||||
// 指定要使用的解析器,不需要写文件开头的 @prettier
|
||||
requirePragma: false,
|
||||
// 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
|
||||
proseWrap: "preserve",
|
||||
// 在html中空格是否是敏感的 "css" - 遵守 CSS 显示属性的默认值, "strict" - 空格被认为是敏感的 ,"ignore" - 空格被认为是不敏感的
|
||||
htmlWhitespaceSensitivity: "css",
|
||||
// 换行符使用 lf 结尾是 可选值 "<auto|lf|crlf|cr>"
|
||||
endOfLine: "auto",
|
||||
// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
|
||||
rangeStart: 0,
|
||||
rangeEnd: Infinity,
|
||||
// Vue文件脚本和样式标签缩进
|
||||
vueIndentScriptAndStyle: false
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
# .stylelintignore
|
||||
# 旧的不需打包的样式库
|
||||
*.min.css
|
||||
|
||||
# 其他类型文件
|
||||
*.js
|
||||
*.jpg
|
||||
*.woff
|
||||
|
||||
# 测试和打包目录
|
||||
/test/
|
||||
/dist/*
|
||||
/public/*
|
||||
public/*
|
||||
/node_modules/
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"antfu.unocss",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Bunny
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,60 @@
|
|||
import type { Plugin } from "vite";
|
||||
import { getPackageSize } from "./utils";
|
||||
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";
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
const welcomeMessage = gradientString("cyan", "magenta").multiline(
|
||||
`您好! 欢迎使用 bunny-admin 后台管理项目
|
||||
项目访问地址如下:
|
||||
http://localhost:8088/`
|
||||
);
|
||||
|
||||
const boxenOptions: BoxenOptions = {
|
||||
padding: 0.5,
|
||||
borderColor: "cyan",
|
||||
borderStyle: "round"
|
||||
};
|
||||
|
||||
export function viteBuildInfo(): Plugin {
|
||||
let config: { command: string };
|
||||
let startTime: Dayjs;
|
||||
let endTime: Dayjs;
|
||||
let outDir: string;
|
||||
return {
|
||||
name: "vite:buildInfo",
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
outDir = resolvedConfig.build?.outDir ?? "dist";
|
||||
},
|
||||
buildStart() {
|
||||
console.log(boxen(welcomeMessage, boxenOptions));
|
||||
if (config.command === "build") {
|
||||
startTime = dayjs(new Date());
|
||||
}
|
||||
},
|
||||
closeBundle() {
|
||||
if (config.command === "build") {
|
||||
endTime = dayjs(new Date());
|
||||
getPackageSize({
|
||||
folder: outDir,
|
||||
callback: (size: string) => {
|
||||
console.log(
|
||||
boxen(
|
||||
gradientString("cyan", "magenta").multiline(
|
||||
`🎉 恭喜打包完成(总用时${dayjs
|
||||
.duration(endTime.diff(startTime))
|
||||
.format("mm分ss秒")},打包后的大小为${size})`
|
||||
),
|
||||
boxenOptions
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { resolve } from "path";
|
||||
import { PluginOption } from "vite";
|
||||
import { createHtmlPlugin } from "vite-plugin-html";
|
||||
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||
// 按需引入antdV
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
|
||||
import unocss from "@unocss/vite";
|
||||
import { viteBuildInfo } from "./info";
|
||||
|
||||
/**
|
||||
* 创建 vite 插件
|
||||
* @param viteEnv
|
||||
*/
|
||||
export const createVitePlugins = (viteEnv: ViteEnv): (PluginOption | PluginOption[])[] => {
|
||||
return [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
unocss(),
|
||||
viteBuildInfo(),
|
||||
// 标题设置
|
||||
createHtmlPlugin({
|
||||
inject: {
|
||||
data: {
|
||||
title: viteEnv.VITE_GLOB_APP_TITLE
|
||||
}
|
||||
}
|
||||
}),
|
||||
// * 使用 svg 图标
|
||||
createSvgIconsPlugin({
|
||||
// 指定需要缓存的图标文件夹
|
||||
iconDirs: [resolve(process.cwd(), "src/assets/icons")],
|
||||
// 指定symbolId格式
|
||||
symbolId: "icon-[dir]-[name]"
|
||||
}),
|
||||
// UI组件库按需引入
|
||||
Components({
|
||||
resolvers: [
|
||||
AntDesignVueResolver({
|
||||
importStyle: false // 动态主题需配置
|
||||
})
|
||||
]
|
||||
})
|
||||
];
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import type { ProxyOptions } from "vite";
|
||||
|
||||
type ProxyItem = [string, string];
|
||||
|
||||
type ProxyList = ProxyItem[];
|
||||
|
||||
type ProxyTargetList = Record<string, ProxyOptions>;
|
||||
|
||||
/**
|
||||
* 创建代理,用于解析 .env.development 代理配置
|
||||
* @param list
|
||||
*/
|
||||
export function createProxy(list: ProxyList = []) {
|
||||
const ret: ProxyTargetList = {};
|
||||
for (const [prefix, target] of list) {
|
||||
const httpsRE = /^https:\/\//;
|
||||
const isHttps = httpsRE.test(target);
|
||||
|
||||
// https://github.com/http-party/node-http-proxy#options
|
||||
ret[prefix] = {
|
||||
target: target,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewrite: path => path.replace(new RegExp(`^${prefix}`), ""),
|
||||
// https is require secure=false
|
||||
...(isHttps ? { secure: false } : {})
|
||||
};
|
||||
}
|
||||
return ret;
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
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();
|
||||
|
||||
/**
|
||||
* @description 根据可选的路径片段生成一个新的绝对路径
|
||||
* @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 alias: Record<string, string> = {
|
||||
"@": 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")
|
||||
};
|
||||
|
||||
/** 处理环境变量 */
|
||||
const wrapperEnv = (envConf: Recordable): ViteEnv => {
|
||||
// 默认值
|
||||
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;
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
export { root, pathResolve, alias, __APP_INFO__, wrapperEnv, getPackageSize };
|
|
@ -0,0 +1,73 @@
|
|||
// @see: https://cz-git.qbenben.com/zh/guide
|
||||
/** @type {import('cz-git').UserConfig} */
|
||||
|
||||
export default {
|
||||
ignores: [commit => commit.includes("init")],
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
rules: {
|
||||
// @see: https://commitlint.js.org/#/reference-rules
|
||||
"body-leading-blank": [2, "always"],
|
||||
"footer-leading-blank": [1, "always"],
|
||||
"header-max-length": [2, "always", 108],
|
||||
"subject-empty": [2, "never"],
|
||||
"type-empty": [2, "never"],
|
||||
"subject-case": [0],
|
||||
"type-enum": [2, "always", ["feat", "fix", "media", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert", "wip", "workflow", "types", "release"]]
|
||||
},
|
||||
prompt: {
|
||||
messages: {
|
||||
type: "选择你要提交的类型 :",
|
||||
scope: "选择一个提交范围(可选):",
|
||||
customScope: "请输入自定义的提交范围 :",
|
||||
subject: "填写简短精炼的变更描述 :\n",
|
||||
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
|
||||
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
|
||||
footerPrefixsSelect: "选择关联issue前缀(可选):",
|
||||
customFooterPrefixs: "输入自定义issue前缀 :",
|
||||
footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
|
||||
confirmCommit: "是否提交或修改commit ?"
|
||||
},
|
||||
types: [
|
||||
{ value: "feat", name: "特性: 🚀 新增功能", emoji: "🚀" },
|
||||
{ value: "media", name: "媒体: 🎁 新增媒体资源", emoji: "🎁" },
|
||||
{ value: "fix", name: "修复: 🧩 修复缺陷", emoji: "🧩" },
|
||||
{ value: "docs", name: "文档: 📚 文档变更", emoji: "📚" },
|
||||
{ value: "style", name: "格式: 🎨 代码格式(不影响功能,例如空格、分号等格式修正)", emoji: "🎨" },
|
||||
{ value: "refactor", name: "重构: ♻️ 代码重构(不包括 bug 修复、功能新增)", emoji: "♻️" },
|
||||
{ value: "perf", name: "性能: ⚡️ 性能优化", emoji: "⚡️" },
|
||||
{ value: "test", name: "测试: ✅ 添加疏漏测试或已有测试改动", emoji: "✅" },
|
||||
{ value: "build", name: "构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)", emoji: "📦️" },
|
||||
{ value: "ci", name: "集成: 🎡 修改 CI 配置、脚本", emoji: "🎡" },
|
||||
{ value: "revert", name: "回退: ⏪️ 回滚 commit", emoji: "⏪️" },
|
||||
{ value: "chore", name: "其他: 🔨 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)", emoji: "🔨" }
|
||||
],
|
||||
useEmoji: true,
|
||||
themeColorCode: "",
|
||||
scopes: [],
|
||||
allowCustomScopes: true,
|
||||
allowEmptyScopes: true,
|
||||
customScopesAlign: "bottom",
|
||||
customScopesAlias: "custom",
|
||||
emptyScopesAlias: "empty",
|
||||
upperCaseSubject: false,
|
||||
allowBreakingChanges: ["feat", "fix"],
|
||||
breaklineNumber: 100,
|
||||
breaklineChar: "|",
|
||||
skipQuestions: [],
|
||||
issuePrefixs: [{ value: "closed", name: "closed: ISSUES has been processed" }],
|
||||
customIssuePrefixsAlign: "top",
|
||||
emptyIssuePrefixsAlias: "skip",
|
||||
customIssuePrefixsAlias: "custom",
|
||||
allowCustomIssuePrefixs: true,
|
||||
allowEmptyIssuePrefixs: true,
|
||||
confirmColorize: true,
|
||||
maxHeaderLength: Infinity,
|
||||
maxSubjectLength: Infinity,
|
||||
minSubjectLength: 0,
|
||||
scopeOverrides: undefined,
|
||||
defaultBody: "",
|
||||
defaultIssues: "",
|
||||
defaultScope: "",
|
||||
defaultSubject: ""
|
||||
}
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {};
|
||||
|
||||
declare module "@vue/runtime-core" {
|
||||
export interface GlobalComponents {
|
||||
403: typeof import("./src/components/ErrorMessage/403.vue")["default"];
|
||||
404: typeof import("./src/components/ErrorMessage/404.vue")["default"];
|
||||
500: typeof import("./src/components/ErrorMessage/500.vue")["default"];
|
||||
AAlert: typeof import("ant-design-vue/es")["Alert"];
|
||||
AAnchorLink: typeof import("ant-design-vue/es")["AnchorLink"];
|
||||
AAutoComplete: typeof import("ant-design-vue/es")["AutoComplete"];
|
||||
AAvatar: typeof import("ant-design-vue/es")["Avatar"];
|
||||
ABadge: typeof import("ant-design-vue/es")["Badge"];
|
||||
ABreadcrumb: typeof import("ant-design-vue/es")["Breadcrumb"];
|
||||
ABreadcrumbItem: typeof import("ant-design-vue/es")["BreadcrumbItem"];
|
||||
AButton: typeof import("ant-design-vue/es")["Button"];
|
||||
ACard: typeof import("ant-design-vue/es")["Card"];
|
||||
ACardGrid: typeof import("ant-design-vue/es")["CardGrid"];
|
||||
ACheckbox: typeof import("ant-design-vue/es")["Checkbox"];
|
||||
ACheckboxGroup: typeof import("ant-design-vue/es")["CheckboxGroup"];
|
||||
ACol: typeof import("ant-design-vue/es")["Col"];
|
||||
AConfigProvider: typeof import("ant-design-vue/es")["ConfigProvider"];
|
||||
ADatePicker: typeof import("ant-design-vue/es")["DatePicker"];
|
||||
ADescriptions: typeof import("ant-design-vue/es")["Descriptions"];
|
||||
ADescriptionsItem: typeof import("ant-design-vue/es")["DescriptionsItem"];
|
||||
ADivider: typeof import("ant-design-vue/es")["Divider"];
|
||||
ADrawer: typeof import("ant-design-vue/es")["Drawer"];
|
||||
ADropdown: typeof import("ant-design-vue/es")["Dropdown"];
|
||||
AForm: typeof import("ant-design-vue/es")["Form"];
|
||||
AFormItem: typeof import("ant-design-vue/es")["FormItem"];
|
||||
AImage: typeof import("ant-design-vue/es")["Image"];
|
||||
AImagePreviewGroup: typeof import("ant-design-vue/es")["ImagePreviewGroup"];
|
||||
AInput: typeof import("ant-design-vue/es")["Input"];
|
||||
AInputGroup: typeof import("ant-design-vue/es")["InputGroup"];
|
||||
AInputPassword: typeof import("ant-design-vue/es")["InputPassword"];
|
||||
AInputSearch: typeof import("ant-design-vue/es")["InputSearch"];
|
||||
ALayout: typeof import("ant-design-vue/es")["Layout"];
|
||||
ALayoutContent: typeof import("ant-design-vue/es")["LayoutContent"];
|
||||
ALayoutFooter: typeof import("ant-design-vue/es")["LayoutFooter"];
|
||||
ALayoutHeader: typeof import("ant-design-vue/es")["LayoutHeader"];
|
||||
ALayoutSider: typeof import("ant-design-vue/es")["LayoutSider"];
|
||||
AList: typeof import("ant-design-vue/es")["List"];
|
||||
AListItem: typeof import("ant-design-vue/es")["ListItem"];
|
||||
AListItemMeta: typeof import("ant-design-vue/es")["ListItemMeta"];
|
||||
AMenu: typeof import("ant-design-vue/es")["Menu"];
|
||||
AMenuDivider: typeof import("ant-design-vue/es")["MenuDivider"];
|
||||
AMenuItem: typeof import("ant-design-vue/es")["MenuItem"];
|
||||
AModal: typeof import("ant-design-vue/es")["Modal"];
|
||||
APagination: typeof import("ant-design-vue/es")["Pagination"];
|
||||
APopover: typeof import("ant-design-vue/es")["Popover"];
|
||||
ARadio: typeof import("ant-design-vue/es")["Radio"];
|
||||
ARadioGroup: typeof import("ant-design-vue/es")["RadioGroup"];
|
||||
ARangePicker: typeof import("ant-design-vue/es")["RangePicker"];
|
||||
AResult: typeof import("ant-design-vue/es")["Result"];
|
||||
ARow: typeof import("ant-design-vue/es")["Row"];
|
||||
ASelect: typeof import("ant-design-vue/es")["Select"];
|
||||
ASelectOption: typeof import("ant-design-vue/es")["SelectOption"];
|
||||
ASkeleton: typeof import("ant-design-vue/es")["Skeleton"];
|
||||
ASpace: typeof import("ant-design-vue/es")["Space"];
|
||||
AStep: typeof import("ant-design-vue/es")["Step"];
|
||||
ASteps: typeof import("ant-design-vue/es")["Steps"];
|
||||
ASubMenu: typeof import("ant-design-vue/es")["SubMenu"];
|
||||
ASwitch: typeof import("ant-design-vue/es")["Switch"];
|
||||
ATable: typeof import("ant-design-vue/es")["Table"];
|
||||
ATabPane: typeof import("ant-design-vue/es")["TabPane"];
|
||||
ATabs: typeof import("ant-design-vue/es")["Tabs"];
|
||||
ATag: typeof import("ant-design-vue/es")["Tag"];
|
||||
ATextarea: typeof import("ant-design-vue/es")["Textarea"];
|
||||
ATooltip: typeof import("ant-design-vue/es")["Tooltip"];
|
||||
ATypographyText: typeof import("ant-design-vue/es")["TypographyText"];
|
||||
ATypographyTitle: typeof import("ant-design-vue/es")["TypographyTitle"];
|
||||
AUploadDragger: typeof import("ant-design-vue/es")["UploadDragger"];
|
||||
CompactHeaders: typeof import("./src/components/CompactHeaders/index.vue")["default"];
|
||||
CopyOptBtn: typeof import("./src/components/CopyOptBtn/index.vue")["default"];
|
||||
CountUp: typeof import("./src/components/CountUp/index.vue")["default"];
|
||||
Empty: typeof import("./src/components/Empty/index.vue")["default"];
|
||||
GRoleSelect: typeof import("./src/components/GSelect/GRoleSelect.vue")["default"];
|
||||
ImportExcel: typeof import("./src/components/ImportExcel/index.vue")["default"];
|
||||
Pagination: typeof import("./src/components/ProTable/components/Pagination.vue")["default"];
|
||||
ParticleClock: typeof import("./src/components/ParticleClock/index.vue")["default"];
|
||||
ProTable: typeof import("./src/components/ProTable/index.vue")["default"];
|
||||
RouterLink: typeof import("vue-router")["RouterLink"];
|
||||
RouterView: typeof import("vue-router")["RouterView"];
|
||||
SearchForm: typeof import("./src/components/SearchForm/index.vue")["default"];
|
||||
SelectFilter: typeof import("./src/components/SelectFilter/index.vue")["default"];
|
||||
SvgIcon: typeof import("./src/components/SvgIcon/index.vue")["default"];
|
||||
SwitchDark: typeof import("./src/components/SwitchDark/index.vue")["default"];
|
||||
TableFilter: typeof import("./src/components/TableFilter/index.vue")["default"];
|
||||
TablePreview: typeof import("./src/components/TablePreview/index.vue")["default"];
|
||||
TableTooltip: typeof import("./src/components/TableTooltip/index.vue")["default"];
|
||||
TabRightMenu: typeof import("./src/components/ContextMenu/tabRightMenu.vue")["default"];
|
||||
ThemeColor: typeof import("./src/components/ThemeColor/index.vue")["default"];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%- title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.loading-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.loading-box .loading-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 98px;
|
||||
}
|
||||
.dot {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 32px;
|
||||
transform: rotate(45deg);
|
||||
animation: ant-rotate 1.2s infinite linear;
|
||||
}
|
||||
.dot i {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: #409eff;
|
||||
border-radius: 100%;
|
||||
opacity: 0.3;
|
||||
transform: scale(0.75);
|
||||
transform-origin: 50% 50%;
|
||||
animation: ant-spin-move 1s infinite linear alternate;
|
||||
}
|
||||
.dot i:nth-child(1) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.dot i:nth-child(2) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.dot i:nth-child(3) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
.dot i:nth-child(4) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
@keyframes ant-rotate {
|
||||
to {
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
@keyframes ant-spin-move {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- 项目加载中Loading -->
|
||||
<div class="loading-box">
|
||||
<div class="loading-wrap">
|
||||
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const globalState = JSON.parse(window.localStorage.getItem("guyue-global"));
|
||||
if (globalState) {
|
||||
const dot = document.querySelectorAll(".dot i");
|
||||
const html = document.querySelector("html");
|
||||
dot.forEach(item => (item.style.background = globalState.primary));
|
||||
if (globalState.styleSetting === "realDark") html.style.background = "#141414";
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
|
||||
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": ["prettier --write--parser json"],
|
||||
"package.json": ["prettier --write"],
|
||||
"*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"],
|
||||
"*.{scss,less,styl,html}": ["stylelint --fix", "prettier --write"],
|
||||
"*.md": ["prettier --write"]
|
||||
};
|
|
@ -0,0 +1,141 @@
|
|||
{
|
||||
"name": "bunny_admin",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"bunny-admin",
|
||||
"bunny-cli",
|
||||
"ant-design",
|
||||
"typescript",
|
||||
"pinia",
|
||||
"vue3",
|
||||
"vite",
|
||||
"esm"
|
||||
],
|
||||
"description": "Bunny-Admin后台管理系统",
|
||||
"homepage": "https://gitee.com/BunnyBoss/bunny-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitee.com/BunnyBoss/bunny-admin"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://gitee.com/BunnyBoss/bunny-admin/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Bunny0212",
|
||||
"email": "1319900154@qq.com",
|
||||
"url": "https://gitee.com/BunnyBoss"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"serve": "vite",
|
||||
"start": "vite",
|
||||
"build:dev": "vue-tsc --noEmit && vite build --mode development",
|
||||
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
||||
"build:pro": "vue-tsc --noEmit && vite build --mode production",
|
||||
"preview": "vite preview",
|
||||
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
|
||||
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
|
||||
"lint:lint-staged": "lint-staged",
|
||||
"prepare": "husky install",
|
||||
"commit": "git pull && git add -A && git-cz && git push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
"@pureadmin/utils": "^2.4.7",
|
||||
"@types/node": "^18.15.3",
|
||||
"@vueuse/core": "^8.0.1",
|
||||
"ant-design-vue": "^3.2.15",
|
||||
"axios": "^1.2.1",
|
||||
"boxen": "^7.1.1",
|
||||
"countup.js": "^2.6.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"driver.js": "^0.9.8",
|
||||
"echarts": "^5.4.1",
|
||||
"echarts-liquidfill": "^3.1.0",
|
||||
"gradient-string": "^2.0.2",
|
||||
"js-md5": "^0.7.3",
|
||||
"mitt": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.28",
|
||||
"pinia-plugin-persistedstate": "^3.0.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vue-router": "^4.1.6",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.2.2",
|
||||
"@commitlint/config-conventional": "^17.0.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/gradient-string": "^1.1.6",
|
||||
"@types/js-md5": "^0.7.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/sortablejs": "^1.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"@unocss/preset-uno": "0.54.1",
|
||||
"@unocss/transformer-directives": "0.54.1",
|
||||
"@unocss/vite": "0.54.1",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"commitizen": "^4.2.4",
|
||||
"commitlint": "^17.0.1",
|
||||
"cz-git": "^1.3.2",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"husky": "^8.0.1",
|
||||
"less": "^4.1.3",
|
||||
"lint-staged": "^12.4.2",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-less": "^6.0.0",
|
||||
"prettier": "^2.8.4",
|
||||
"stylelint": "^15.2.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-prettier": "^9.0.5",
|
||||
"stylelint-config-recess-order": "^4.0.0",
|
||||
"stylelint-config-standard": "^30.0.1",
|
||||
"stylelint-less": "^1.0.6",
|
||||
"stylelint-order": "^6.0.3",
|
||||
"typescript": "^4.5.4",
|
||||
"unocss": "^0.55.0",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^3.2.5",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "^1.0.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
|
||||
"yarn": ">=1.22.20"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"yarn": {
|
||||
"allowedDeprecatedVersions": {
|
||||
"sourcemap-codec": "*",
|
||||
"domexception": "*",
|
||||
"w3c-hr-time": "*",
|
||||
"stable": "*",
|
||||
"abab": "*"
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"eslint": "9"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-git"
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 5.4 KiB |
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<a-config-provider :componentSize="assemblySize" :locale="i18nLocale">
|
||||
<!-- <a-spin :spinning="loading" :delay="500" :tip="$t('tip.loading')"> -->
|
||||
<router-view></router-view>
|
||||
<!-- </a-spin> -->
|
||||
<template #renderEmpty>
|
||||
<Empty />
|
||||
</template>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useGlobalStore } from "@/stores/modules/global";
|
||||
import { getBrowserLang } from "@/utils/util";
|
||||
import enUS from "ant-design-vue/es/locale/en_US";
|
||||
import zhCN from "ant-design-vue/es/locale/zh_CN";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
// 组件picker时间国际化
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
// 自定义空状态
|
||||
import Empty from "@/components/Empty/index.vue";
|
||||
|
||||
// 初始化主题配置
|
||||
const { initTheme } = useTheme();
|
||||
initTheme();
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
// antd 语言配置
|
||||
const i18nLocale = computed(() => {
|
||||
if (globalStore.language && globalStore.language == "zh_CN") {
|
||||
dayjs.locale("zh-cn");
|
||||
return zhCN;
|
||||
}
|
||||
if (globalStore.language == "en") {
|
||||
return enUS;
|
||||
}
|
||||
if (getBrowserLang() == "zh_CN") {
|
||||
dayjs.locale("zh-cn");
|
||||
return zhCN;
|
||||
} else {
|
||||
return enUS;
|
||||
}
|
||||
});
|
||||
|
||||
// 配置组件全局大小
|
||||
const assemblySize = computed(() => globalStore.assemblySize);
|
||||
|
||||
// 全局loading
|
||||
// const loading = computed(() => globalStore.loading);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,2 @@
|
|||
// * 后端微服务端口名
|
||||
export const PORT = "/guyue";
|
|
@ -0,0 +1,38 @@
|
|||
import { message } from "ant-design-vue";
|
||||
|
||||
export const checkStatus = (status: number): void => {
|
||||
switch (status) {
|
||||
case 400:
|
||||
message.error("请求失败!请您稍后重试");
|
||||
break;
|
||||
case 401:
|
||||
message.error("登录失效!请您重新登录");
|
||||
break;
|
||||
case 403:
|
||||
message.error("当前账号无权限访问!");
|
||||
break;
|
||||
case 404:
|
||||
message.error("你所访问的资源不存在!");
|
||||
break;
|
||||
case 405:
|
||||
message.error("请求方式错误!请您稍后重试");
|
||||
break;
|
||||
case 408:
|
||||
message.error("请求超时!请您稍后重试");
|
||||
break;
|
||||
case 500:
|
||||
message.error("服务异常!");
|
||||
break;
|
||||
case 502:
|
||||
message.error("网关错误!");
|
||||
break;
|
||||
case 503:
|
||||
message.error("服务不可用!");
|
||||
break;
|
||||
case 504:
|
||||
message.error("网关超时!");
|
||||
break;
|
||||
default:
|
||||
message.error("请求失败!");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
import axios from "axios";
|
||||
import type { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { message } from "ant-design-vue";
|
||||
import { ResultData } from "@/api/interface";
|
||||
import { ResultEnum } from "@/enums/httpEnum";
|
||||
import { checkStatus } from "./helper/checkStatus";
|
||||
import { useGlobalStore } from "@/stores/modules/global";
|
||||
import { LOGIN_URL } from "@/config";
|
||||
import { useUserStore } from "@/stores/modules/user";
|
||||
import router from "@/routers";
|
||||
|
||||
export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||
noLoading?: boolean;
|
||||
}
|
||||
|
||||
// 配置
|
||||
const config = {
|
||||
// 默认地址请求地址,可在 .env.*** 文件中修改
|
||||
baseURL: import.meta.env.VITE_API_URL as string,
|
||||
// 设置超时时间(10s)
|
||||
timeout: ResultEnum.TIMEOUT as number,
|
||||
// 跨域时候允许携带凭证
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
class RequestHttp {
|
||||
service: AxiosInstance;
|
||||
public constructor(config: AxiosRequestConfig) {
|
||||
// 实例化 axios
|
||||
this.service = axios.create(config);
|
||||
|
||||
/**
|
||||
* @description 请求拦截器
|
||||
* 客户端发送请求 -> [请求拦截器] -> 服务器
|
||||
* token校验(JWT) : 接受服务器返回的token,存储到vuex/pinia/本地储存当中
|
||||
*/
|
||||
this.service.interceptors.request.use(
|
||||
(config: CustomAxiosRequestConfig) => {
|
||||
const globalState = useGlobalStore();
|
||||
const userStore = useUserStore();
|
||||
// * 如果当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { noLoading: true }来控制不显示loading,参见loginApi
|
||||
config.noLoading || globalState.setGlobalState("loading", true);
|
||||
if (config.headers && typeof config.headers.set === "function") {
|
||||
config.headers.set("x-access-token", userStore.token);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @description 响应拦截器
|
||||
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
|
||||
*/
|
||||
this.service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { data } = response;
|
||||
const globalState = useGlobalStore();
|
||||
const userStore = useUserStore();
|
||||
// * 在请求结束后,并关闭请求 loading
|
||||
globalState.setGlobalState("loading", false);
|
||||
// * 登录失效(code == 401)
|
||||
if (data.code == ResultEnum.OVERDUE) {
|
||||
message.error(data.msg);
|
||||
userStore.setToken("");
|
||||
// 跳转到登录页
|
||||
router.replace(LOGIN_URL);
|
||||
return Promise.reject(data);
|
||||
}
|
||||
// * 全局错误信息拦截(防止下载文件得时候返回数据流,没有code,直接报错)
|
||||
if (data.code && data.code !== ResultEnum.SUCCESS) {
|
||||
message.error(data.msg);
|
||||
return Promise.reject(data);
|
||||
}
|
||||
// * 成功请求(在页面上除非特殊情况,否则不用在页面处理失败逻辑)
|
||||
return data;
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
const { response } = error;
|
||||
// 关闭请求 loading
|
||||
const globalState = useGlobalStore();
|
||||
globalState.setGlobalState("loading", false);
|
||||
// 请求超时 && 网络错误单独判断,没有 response
|
||||
if (error.message.indexOf("timeout") !== -1) message.error("请求超时!请您稍后重试");
|
||||
if (error.message.indexOf("Network Error") !== -1) message.error("网络错误!请您稍后重试");
|
||||
// 根据响应的错误状态码,做不同的处理
|
||||
if (response) checkStatus(response.status);
|
||||
// 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面
|
||||
if (!window.navigator.onLine) {
|
||||
// 跳转500显示页
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// * 常用请求方法封装
|
||||
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.get(url, { params, ..._object });
|
||||
}
|
||||
post<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.post(url, params, _object);
|
||||
}
|
||||
put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.put(url, params, _object);
|
||||
}
|
||||
delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.delete(url, { params, ..._object });
|
||||
}
|
||||
download(url: string, params?: object, _object = {}): Promise<BlobPart> {
|
||||
return this.service.post(url, params, { ..._object, responseType: "blob" });
|
||||
}
|
||||
}
|
||||
|
||||
export default new RequestHttp(config);
|
|
@ -0,0 +1,77 @@
|
|||
// * 请求响应参数(不包含data)
|
||||
export interface Result {
|
||||
code: string;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
// * 请求响应参数(包含data)
|
||||
export interface ResultData<T = any> extends Result {
|
||||
data: T;
|
||||
}
|
||||
|
||||
// * 分页响应参数
|
||||
export interface ResPage<T> {
|
||||
list: T[];
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// * 分页请求参数
|
||||
export interface ReqPage {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// * 登录模块
|
||||
export namespace Login {
|
||||
export interface ReqLoginForm {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
export interface ResLogin {
|
||||
access_token: string;
|
||||
}
|
||||
export interface ResAuthButtons {
|
||||
[key: string]: string[];
|
||||
}
|
||||
}
|
||||
|
||||
// * 用户管理模块
|
||||
export namespace User {
|
||||
export interface ReqUserParams extends ReqPage {
|
||||
username: string;
|
||||
gender: number;
|
||||
idCard: string;
|
||||
email: string;
|
||||
address: string;
|
||||
createTime: string[];
|
||||
status: number;
|
||||
}
|
||||
export interface ResUserList {
|
||||
id: string;
|
||||
username: string;
|
||||
gender: number;
|
||||
user: { detail: { age: number } };
|
||||
idCard: string;
|
||||
email: string;
|
||||
address: string;
|
||||
createTime: string;
|
||||
status: number;
|
||||
avatar: string;
|
||||
photo: any[];
|
||||
children?: ResUserList[];
|
||||
}
|
||||
}
|
||||
|
||||
// * 角色列表模块
|
||||
export namespace Role {
|
||||
export interface RoleList {
|
||||
id: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
name: string;
|
||||
status: number;
|
||||
type: number;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { Login } from "@/api/interface/index";
|
||||
import { PORT } from "@/api/config/servicePort";
|
||||
import http from "@/api";
|
||||
|
||||
/**
|
||||
* @name 登录模块
|
||||
*/
|
||||
// * 用户登录
|
||||
export const loginApi = (params: Login.ReqLoginForm) => {
|
||||
// 正常 post json 请求
|
||||
return http.post<Login.ResLogin>(PORT + `/login`, params, { noLoading: true });
|
||||
};
|
||||
|
||||
// * 获取菜单列表
|
||||
export const getAuthMenuListApi = () => {
|
||||
return http.get<Menu.MenuOptions[]>(PORT + `/menu/list`, {}, { noLoading: true });
|
||||
};
|
||||
|
||||
// * 获取按钮权限
|
||||
export const getAuthButtonListApi = () => {
|
||||
return http.get<Login.ResAuthButtons>(PORT + `/auth/buttons`, {}, { noLoading: true });
|
||||
};
|
||||
|
||||
// * 用户退出登录
|
||||
export const logoutApi = () => {
|
||||
return http.post(PORT + `/logout`);
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
import { Role } from "@/api/interface/index";
|
||||
import { PORT } from "@/api/config/servicePort";
|
||||
import http from "@/api";
|
||||
|
||||
/**
|
||||
* @name 角色管理模块
|
||||
*/
|
||||
// 角色列表
|
||||
export const getRolesListApi = () => {
|
||||
return http.get<Role.RoleList[]>(PORT + `/basicRole/list`);
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
import { ResPage, User } from "@/api/interface/index";
|
||||
import { PORT } from "@/api/config/servicePort";
|
||||
import http from "@/api";
|
||||
|
||||
/**
|
||||
* @name 用户管理模块
|
||||
*/
|
||||
// 获取用户列表
|
||||
export const getUserList = (params: User.ReqUserParams) => {
|
||||
return http.post<ResPage<User.ResUserList>>(PORT + `/user/list`, params);
|
||||
};
|
||||
|
||||
// 批量添加用户
|
||||
export const BatchAddUser = (params: FormData) => {
|
||||
return http.post(PORT + `/user/import`, params);
|
||||
};
|
||||
|
||||
// 导出用户数据
|
||||
export const exportUserInfo = (params: User.ReqUserParams) => {
|
||||
return http.download(PORT + `/user/export`, params);
|
||||
};
|
||||
|
||||
// 重置用户密码
|
||||
export const resetUserPassWord = (params: { id: string }) => {
|
||||
return http.post(PORT + `/user/rest_password`, params);
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
export const deleteUser = (params: { id: string[] }) => {
|
||||
return http.post(PORT + `/user/delete`, params);
|
||||
};
|
||||
|
||||
// 切换用户状态
|
||||
export const changeUserStatus = (params: { id: string; status: number }) => {
|
||||
return http.post(PORT + `/user/change`, params);
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
@font-face {
|
||||
font-family: YouSheBiaoTiHei;
|
||||
src: url("./YouSheBiaoTiHei.ttf");
|
||||
}
|
||||
@font-face {
|
||||
font-family: MetroDF;
|
||||
src: url("./MetroDF.ttf");
|
||||
}
|
||||
@font-face {
|
||||
font-family: DIN;
|
||||
src: url("./DIN.Otf");
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
@font-face {
|
||||
font-family: iconfont; /* Project id 2667653 */
|
||||
src: url("iconfont.ttf?t=1663324025864") format("truetype");
|
||||
}
|
||||
.iconfont {
|
||||
font-family: iconfont !important;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
cursor: pointer;
|
||||
}
|
||||
.icon-xiaoxi::before {
|
||||
font-size: 21.2px;
|
||||
content: "\e61f";
|
||||
}
|
||||
.icon-zhuti::before {
|
||||
font-size: 22.4px;
|
||||
content: "\e638";
|
||||
}
|
||||
.icon-sousuo::before {
|
||||
content: "\e611";
|
||||
}
|
||||
.icon-contentright::before {
|
||||
content: "\e8c9";
|
||||
}
|
||||
.icon-contentleft::before {
|
||||
content: "\e8ca";
|
||||
}
|
||||
.icon-fangda::before {
|
||||
content: "\e826";
|
||||
}
|
||||
.icon-suoxiao::before {
|
||||
content: "\e641";
|
||||
}
|
||||
.icon-zhongyingwen::before {
|
||||
content: "\e8cb";
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.5303 6.53033C18.8232 6.23744 18.8232 5.76256 18.5303 5.46967C18.2374 5.17678 17.7626 5.17678 17.4697 5.46967L12 10.9393L6.53033 5.46967C6.23744 5.17678 5.76256 5.17678 5.46967 5.46967C5.17678 5.76256 5.17678 6.23744 5.46967 6.53033L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0607L17.4697 18.5303C17.7626 18.8232 18.2374 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53033Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 645 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1680886419224" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6634" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M768 64q59.008 0.992 106.016 32t70.016 85.504 12.512 110.016-51.488 98.016-96.512 54.496-110.496-8.992l-218.016 290.016v171.008h96q14.016 0 23.008 8.992t8.992 23.008-8.992 23.008-23.008 8.992h-256q-14.016 0-23.008-8.992t-8.992-23.008 8.992-23.008 23.008-8.992h96v-171.008L77.024 274.048q-12.992-18.016-12.992-39.008v-11.008q0-12.992 8.992-22.016T96.032 192h72L102.016 108.992q-8-11.008-6.496-24T108.032 64t23.488-6.496 21.504 11.488l96 123.008h338.016q20.992-58.016 70.016-92.512T768.064 64z m-111.008 128H800q14.016 0.992 23.008 10.016t8.992 22.016v11.008q0 20.992-12.992 38.016l-80 108q48.992 10.016 91.008-12.992t58.016-71.008-3.008-92-64.992-64.512-91.488-6.016-71.488 58.496v-0.992zM299.008 256l128.992 166.016q8 11.008 6.496 23.488t-12 20.512-23.008 7.008-21.504-11.008L217.984 256H143.968l304 404.992L751.968 256H298.976z" p-id="6635"></path></svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1681139654009" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2528" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M433.834667 337.706667a21.333333 21.333333 0 0 1 1.493333-32.768 29.397333 29.397333 0 0 1 37.461333 0.938666l202.709334 190.037334a21.077333 21.077333 0 0 1 1.365333 30.421333c-0.341333 0.426667-204.117333 191.573333-204.117333 191.573333a29.354667 29.354667 0 0 1-37.632 1.152 21.333333 21.333333 0 0 1-1.28-32.938666l185.813333-174.293334-185.813333-174.122666z" fill="#707070" p-id="2529"></path></svg>
|
After Width: | Height: | Size: 737 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683095054996" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14355" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M258.048 568.832l253.952 279.04 253.952-279.04c22.016-24.064 13.312-43.52-18.944-43.52l-117.248 0 0-322.56c0-32.76800001-26.112-58.88000001-58.368-58.88l-117.248 0c-32.25599999 0-58.36799999 26.112-58.88 58.368l0 323.072-117.248 0c-32.76800001 0-41.47199999 19.456-19.968 43.52z" p-id="14356" fill="#D03050"></path></svg>
|
After Width: | Height: | Size: 653 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1680624798338" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2733" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M392.533333 806.4L85.333333 503.466667l59.733334-59.733334 247.466666 247.466667L866.133333 213.333333l59.733334 59.733334L392.533333 806.4z" fill="#ffffff" p-id="2734"></path></svg>
|
After Width: | Height: | Size: 513 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1681015819371" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="30341" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M626.504285 375.563329a26.383253 26.383253 0 0 1-25.98523-26.724415c0-73.918596-58.338831-134.020101-129.983009-134.020101a26.383253 26.383253 0 0 1-25.985229-26.724416c0-14.783719 11.656394-26.838136 25.985229-26.838136 71.644178 0 129.983008-60.101505 129.983009-133.963241 0-14.783719 11.656394-26.781276 25.98523-26.781276 14.385696 0 25.98523 11.997557 25.985229 26.781276 0 73.861736 58.338831 133.963241 129.983009 133.963241 14.328836 0 25.98523 11.997557 25.985229 26.781276s-11.656394 26.781276-25.985229 26.781276c-71.644178 0-129.983008 60.158365-129.983009 133.96324 0 14.783719-11.599534 26.781276-25.985229 26.781276zM564.185222 188.037537c25.473485 15.920928 46.966739 37.925926 62.319063 64.252318 15.352324-26.269532 36.845577-48.33139 62.375923-64.252318a186.729746 186.729746 0 0 1-62.375923-64.252318c-15.352324 26.269532-36.788717 48.38825-62.319063 64.252318zM106.629111 536.307846a26.383253 26.383253 0 0 1-25.98523-26.724415 26.383253 26.383253 0 0 0-25.985229-26.838137A26.383253 26.383253 0 0 1 28.673422 455.964018c0-14.783719 11.656394-26.781276 25.98523-26.781276a26.383253 26.383253 0 0 0 25.985229-26.838137c0-14.783719 11.656394-26.724416 25.98523-26.724415 14.385696 0 26.04209 11.940696 26.04209 26.724415s11.599534 26.838136 25.98523 26.838137c14.328836 0 25.98523 11.940696 25.985229 26.724416s-11.656394 26.838136-25.985229 26.838136a26.383253 26.383253 0 0 0-25.98523 26.781276c0 14.783719-11.656394 26.781276-26.04209 26.781276zM972.102152 854.555833l-550.40924-567.239935a76.420456 76.420456 0 0 0-110.309289 0l-30.420346 31.443833c-14.726859 15.124882-22.744183 35.253484-22.744183 56.860459 0 21.493253 8.074185 41.621856 22.744183 56.860459l550.35238 567.183075a76.420456 76.420456 0 0 0 110.309289 0l30.477206-31.443834c14.669998-15.124882 22.744183-35.253484 22.744184-56.860459 0-21.493253-8.131046-41.678716-22.744184-56.860458zM342.656875 334.73752a25.587206 25.587206 0 0 1 36.788717 0l74.771503 77.102782-53.27825 50.605808L326.053621 385.400189a27.406741 27.406741 0 0 1 0-37.869066l16.489533-12.736742z m567.012494 617.618302a25.416625 25.416625 0 0 1-36.674996 0L434.259074 500.201455l54.017436-50.890111 438.735299 452.211228a27.406741 27.406741 0 0 1 0 37.869066l-17.34244 12.964184zM210.62689 268.438225a26.383253 26.383253 0 0 1-25.98523-26.838136c0-44.294297-34.969182-80.343828-78.012549-80.343828a26.383253 26.383253 0 0 1-25.98523-26.781276c0-14.783719 11.656394-26.781276 25.98523-26.781276 43.043367 0 78.012549-36.106391 78.012549-80.400689 0-14.783719 11.656394-26.781276 25.98523-26.781276s25.98523 11.997557 25.98523 26.781276c0 44.351158 34.969182 80.400689 78.012549 80.400689 14.328836 0 25.98523 11.940696 25.985229 26.724415s-11.656394 26.838136-25.985229 26.838137c-43.043367 0-78.012549 36.049531-78.012549 80.343828 0 14.783719-11.656394 26.838136-25.98523 26.838136z m-26.098951-133.96324c9.89372 7.676162 18.65023 16.716975 26.098951 26.894997a134.190682 134.190682 0 0 1 26.09895-26.894997 134.190682 134.190682 0 0 1-26.09895-26.894997 134.190682 134.190682 0 0 1-26.098951 26.894997zM210.62689 804.234327a26.383253 26.383253 0 0 1-25.98523-26.781276c0-44.351158-34.969182-80.400689-78.012549-80.400688a26.383253 26.383253 0 0 1-25.98523-26.724416c0-14.783719 11.656394-26.838136 25.98523-26.838136 43.043367 0 78.012549-36.049531 78.012549-80.400689 0-14.783719 11.656394-26.724416 25.98523-26.724415s25.98523 11.940696 25.98523 26.724415c0 44.351158 34.969182 80.400689 78.012549 80.400689 14.328836 0 25.98523 11.997557 25.985229 26.781276s-11.656394 26.781276-25.985229 26.781276c-43.043367 0-78.012549 36.049531-78.012549 80.400688 0 14.783719-11.656394 26.781276-25.98523 26.781276z m-26.098951-133.96324c9.89372 7.676162 18.65023 16.716975 26.098951 26.894997a134.190682 134.190682 0 0 1 26.09895-26.894997 134.190682 134.190682 0 0 1-26.09895-26.894997 134.190682 134.190682 0 0 1-26.098951 26.894997z" fill="#001529" p-id="30342"></path></svg>
|
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679578965190" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15232" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M335.22 240.91c0-57.08 10.68-111.66 30.15-161.87-167.51 64.86-286.3 227.51-286.3 417.92 0 247.42 200.58 448 448 448 190.34 0 352.95-118.71 417.85-286.13-50.16 19.42-104.69 30.08-161.71 30.08-247.41 0-447.99-200.57-447.99-448z" fill="#FFD500" p-id="15233"></path></svg>
|
After Width: | Height: | Size: 600 B |
|
@ -0,0 +1,8 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.08337 6.66699C7.77373 6.66699 8.33337 6.10735 8.33337 5.41699C8.33337 4.72664 7.77373 4.16699 7.08337 4.16699C6.39302 4.16699 5.83337 4.72664 5.83337 5.41699C5.83337 6.10735 6.39302 6.66699 7.08337 6.66699Z" fill="#666666"/>
|
||||
<path d="M12.9167 6.66699C13.6071 6.66699 14.1667 6.10735 14.1667 5.41699C14.1667 4.72664 13.6071 4.16699 12.9167 4.16699C12.2264 4.16699 11.6667 4.72664 11.6667 5.41699C11.6667 6.10735 12.2264 6.66699 12.9167 6.66699Z" fill="#666666"/>
|
||||
<path d="M8.33337 10.0003C8.33337 10.6907 7.77373 11.2503 7.08337 11.2503C6.39302 11.2503 5.83337 10.6907 5.83337 10.0003C5.83337 9.30997 6.39302 8.75033 7.08337 8.75033C7.77373 8.75033 8.33337 9.30997 8.33337 10.0003Z" fill="#666666"/>
|
||||
<path d="M12.9167 11.2503C13.6071 11.2503 14.1667 10.6907 14.1667 10.0003C14.1667 9.30997 13.6071 8.75033 12.9167 8.75033C12.2264 8.75033 11.6667 9.30997 11.6667 10.0003C11.6667 10.6907 12.2264 11.2503 12.9167 11.2503Z" fill="#666666"/>
|
||||
<path d="M8.33337 14.5837C8.33337 15.274 7.77373 15.8337 7.08337 15.8337C6.39302 15.8337 5.83337 15.274 5.83337 14.5837C5.83337 13.8933 6.39302 13.3337 7.08337 13.3337C7.77373 13.3337 8.33337 13.8933 8.33337 14.5837Z" fill="#666666"/>
|
||||
<path d="M12.9167 15.8337C13.6071 15.8337 14.1667 15.274 14.1667 14.5837C14.1667 13.8933 13.6071 13.3337 12.9167 13.3337C12.2264 13.3337 11.6667 13.8933 11.6667 14.5837C11.6667 15.274 12.2264 15.8337 12.9167 15.8337Z" fill="#666666"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1631454216260" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2219" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><defs><style type="text/css"></style></defs><path d="M697.652 256c-23.565 0-42.667-19.103-42.667-42.667s19.102-42.666 42.667-42.666h124.651c40.288 0 73.697 31.956 73.697 72.347v85.783c0 23.564-19.103 42.667-42.667 42.667s-42.666-19.103-42.666-42.667V256H697.652z m113.015 597.333V496.565c0-23.564 19.102-42.666 42.666-42.666C876.897 453.899 896 473 896 496.565V866.32c0 40.391-33.41 72.348-73.697 72.348H201.697c-40.288 0-73.697-31.957-73.697-72.348V243.014c0-40.39 33.41-72.347 73.697-72.347h124.727c23.564 0 42.667 19.102 42.667 42.666 0 23.564-19.103 42.667-42.667 42.667h-113.09v597.333h597.333zM368.485 541.418c-23.564 0-42.667-19.102-42.667-42.666 0-23.564 19.103-42.667 42.667-42.667h320c23.564 0 42.667 19.103 42.667 42.667s-19.103 42.666-42.667 42.666h-320z m58.182-370.751c-23.564 0-42.667 19.102-42.667 42.666C384 236.897 403.103 256 426.667 256h170.666C620.897 256 640 236.897 640 213.333s-19.103-42.666-42.667-42.666H426.667z m0-85.334h170.666c70.693 0 128 57.308 128 128 0 70.693-57.307 128-128 128H426.667c-70.693 0-128-57.307-128-128 0-70.692 57.307-128 128-128zM368.485 696.57c-23.564 0-42.667-19.103-42.667-42.667s19.103-42.666 42.667-42.666h320c23.564 0 42.667 19.102 42.667 42.666 0 23.564-19.103 42.667-42.667 42.667h-320z" p-id="2220" fill="#f49776"></path></svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683094815057" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14060" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M765.792 455.168L512.032 176l-253.76 279.168c-21.856 24.064-13.312 43.52 19.04 43.52H394.72V821.28c0 32.544 26.24 58.752 58.624 58.752h117.44a58.56 58.56 0 0 0 58.624-58.752V498.656h117.408c32.192 0 40.832-19.456 18.976-43.488z" p-id="14061" fill="#18A058"></path></svg>
|
After Width: | Height: | Size: 602 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1631454148586" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1906" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M826.666667 644.266667A341.333333 341.333333 0 0 0 342.613333 215.594667l-42.325333-74.112A426.666667 426.666667 0 0 1 900.992 687.36l57.258667 33.024-177.706667 94.549333-7.04-201.130666 53.162667 30.677333zM197.333333 379.733333A341.333333 341.333333 0 0 0 681.386667 808.405333l42.325333 74.112A426.666667 426.666667 0 0 1 123.008 336.64L65.706667 303.658667 243.2 209.066667l7.253333 201.258666L197.290667 379.733333zM554.666667 577.536h128v85.333333h-128v85.333334h-85.333334v-85.333334H341.333333v-85.333333h128v-42.666667H341.333333v-85.333333h110.336L361.130667 358.997333 421.546667 298.666667 512 389.162667 602.496 298.666667l60.373333 60.330666-90.538666 90.538667H682.666667v85.333333h-128v42.666667z" p-id="1907" fill="#b37feb"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679577781791" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2924" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M501.48 493.55m-233.03 0a233.03 233.03 0 1 0 466.06 0 233.03 233.03 0 1 0-466.06 0Z" fill="#F9C626" p-id="2925"></path><path d="M501.52 185.35H478.9c-8.28 0-15-6.72-15-15V87.59c0-8.28 6.72-15 15-15h22.62c8.28 0 15 6.72 15 15v82.76c0 8.28-6.72 15-15 15zM281.37 262.76l-16 16c-5.86 5.86-15.36 5.86-21.21 0l-58.52-58.52c-5.86-5.86-5.86-15.36 0-21.21l16-16c5.86-5.86 15.36-5.86 21.21 0l58.52 58.52c5.86 5.86 5.86 15.35 0 21.21zM185.76 478.48v22.62c0 8.28-6.72 15-15 15H88c-8.28 0-15-6.72-15-15v-22.62c0-8.28 6.72-15 15-15h82.76c8.28 0 15 6.72 15 15zM270.69 698.63l16 16c5.86 5.86 5.86 15.36 0 21.21l-58.52 58.52c-5.86 5.86-15.36 5.86-21.21 0l-16-16c-5.86-5.86-5.86-15.36 0-21.21l58.52-58.52c5.85-5.86 15.35-5.86 21.21 0zM486.41 794.24h22.62c8.28 0 15 6.72 15 15V892c0 8.28-6.72 15-15 15h-22.62c-8.28 0-15-6.72-15-15v-82.76c0-8.28 6.72-15 15-15zM706.56 709.31l16-16c5.86-5.86 15.36-5.86 21.21 0l58.52 58.52c5.86 5.86 5.86 15.36 0 21.21l-16 16c-5.86 5.86-15.36 5.86-21.21 0l-58.52-58.52c-5.86-5.85-5.86-15.35 0-21.21zM802.17 493.59v-22.62c0-8.28 6.72-15 15-15h82.76c8.28 0 15 6.72 15 15v22.62c0 8.28-6.72 15-15 15h-82.76c-8.28 0-15-6.72-15-15zM717.24 273.44l-16-16c-5.86-5.86-5.86-15.36 0-21.21l58.52-58.52c5.86-5.86 15.36-5.86 21.21 0l16 16c5.86 5.86 5.86 15.36 0 21.21l-58.52 58.52c-5.86 5.86-15.35 5.86-21.21 0z" fill="#F9C626" p-id="2926"></path></svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1631453917190" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1531" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M300.032 558.08V427.52c0-19.968 13.312-35.84 29.696-35.84s29.696 15.872 29.696 35.84V558.08c0 19.968-13.312 35.84-29.696 35.84s-29.696-15.872-29.696-35.84zM480.256 558.08V363.52c0-19.968 13.312-35.84 29.696-35.84 16.384 0 29.696 15.872 29.696 35.84v194.56c0 19.968-13.312 35.84-29.696 35.84-16.384 0-29.696-15.872-29.696-35.84zM660.48 558.08V299.52c0-19.968 13.312-35.84 29.696-35.84s29.696 15.872 29.696 35.84V558.08c0 19.968-13.312 35.84-29.696 35.84s-29.696-15.872-29.696-35.84z" p-id="1532" fill="#2d8cf0"></path><path d="M861.696 781.312H568.32v117.248h146.944c16.384 0 29.184 13.312 29.184 29.184 0 16.384-13.312 29.184-29.184 29.184H362.496c-16.384-1.024-28.672-14.848-27.648-30.72 1.024-14.848 12.8-27.136 27.648-27.648h146.944v-117.248H157.184c-65.024 0-117.248-52.736-117.248-117.248V194.048C39.936 129.024 92.16 76.8 157.184 76.8h704.512c65.024 0 117.248 52.736 117.248 117.248v470.016c0.512 64.512-52.224 117.248-117.248 117.248z m58.88-587.264c0-32.256-26.112-58.88-58.88-58.88H157.184c-32.256 0-58.88 26.112-58.88 58.88v470.016c0 32.256 26.112 58.88 58.88 58.88h704.512c32.256 0 58.88-26.112 58.88-58.88V194.048z" p-id="1533" fill="#2d8cf0"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1631454355958" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2654" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><defs><style type="text/css"></style></defs><path d="M931.707 868.351H853.75v-36.85c0-9.742-3.977-18.707-10.501-25.23-6.528-6.521-15.485-10.609-25.271-10.609h-15.913v-36.704c0-9.818-4.163-18.921-10.576-25.237-6.452-6.522-15.521-10.533-25.373-10.533h-309.94c-9.931 0-18.711 4.011-25.309 10.533-6.412 6.453-10.571 15.419-10.571 25.237v36.704h-15.914c-9.855 0-18.924 4.088-25.271 10.609l-1.613 1.721c-5.483 6.308-8.89 14.551-8.89 23.51v36.85h-77.959c-18.604 0-33.62 14.985-33.62 33.624 0 18.638 15.017 33.623 33.62 33.623h641.058c18.536 0 33.549-14.985 33.549-33.623 0-18.64-15.013-33.625-33.549-33.625z m-118.206 0H408.827v-32.328h31.687c11.111 0 20.179-8.89 20.179-20.071V763.47h300.904v52.481c0 11.182 9.144 20.071 20.248 20.071h31.656v32.329zM443.2 512.495a65.26 65.26 0 0 0-4.518 23.806c0 17.2 6.599 34.408 19.643 47.596l22.113 22.156 1.26 1.072c12.9 12.397 29.604 18.493 46.307 18.493 16.99 0 34.338-6.453 47.207-19.565h0.18L706.29 475.081l1.221-1.073c12.33-13.052 18.35-29.758 18.35-46.455 0-17.206-6.521-34.193-19.57-47.245h-0.072l0.072-0.359-22.078-22.01c-13.049-12.975-30.252-19.498-47.566-19.498-8.136 0-16.306 1.432-24.012 4.376L471.446 201.664c3.082-7.74 4.548-15.915 4.548-24.012 0-17.207-6.702-34.415-19.746-47.604l-22.116-22.01c-13.048-13.044-30.214-19.636-47.387-19.636-17.204 0-34.518 6.592-47.563 19.636L208.28 239.009l-1.107 1.155c-12.261 12.9-18.46 29.751-18.46 46.091 0 17.345 6.523 34.552 19.567 47.673l22.082 22.078 1.29 1.08c13.046 12.467 29.641 18.563 46.275 18.563 8.133 0 16.237-1.582 24.013-4.444l14.015 14.117L82.044 619.385c-15.522 15.48-23.3 35.916-23.3 56.272 0 20.647 7.777 41.075 23.3 56.563 15.662 15.556 36.093 23.364 56.562 23.364 20.499 0 40.86-7.809 56.451-23.364L428.9 498.308l14.3 14.187z m174.484-126.024c5.271-5.159 12.116-7.808 18.962-7.808 6.991 0 13.836 2.648 19.072 7.808l22.112 22.155c5.16 5.159 7.778 12.253 7.778 18.927 0 6.66-2.364 13.183-6.987 18.274l-0.791 0.79-130.901 130.979a26.943 26.943 0 0 1-18.924 7.809c-6.522 0-13.227-2.29-18.136-7.093l-0.898-0.716-22.185-22.153c-5.088-5.092-7.778-12.117-7.778-19.142 0-6.386 2.367-12.839 6.99-17.998l0.788-0.931 130.898-130.901z m-37.668-19.209L467.467 479.883 334.559 346.835 447.18 234.282l132.836 132.98z m-283.167-39.72c-5.088 5.161-11.896 7.748-18.921 7.748-6.525 0-13.192-2.223-18.14-7.032l-0.859-0.716-22.186-22.291c-5.091-4.946-7.743-11.972-7.743-18.996 0-6.453 2.329-12.9 6.953-18.06l0.79-0.867 130.899-130.827c5.266-5.166 12.078-7.739 19.103-7.739 6.812 0 13.767 2.573 18.927 7.739l22.112 22.079c5.165 5.16 7.782 12.191 7.782 19.072 0 6.453-2.295 13.044-6.846 18.204l-0.937 0.716-130.934 130.97zM166.597 703.765c-7.633 7.595-17.885 11.461-27.991 11.461-10.183 0-20.469-3.866-27.996-11.461-7.637-7.679-11.545-17.93-11.545-28.107 0-10.105 3.908-20.22 11.545-27.815l233.987-233.98 55.806 55.84-233.806 234.062zM466 685.625v9.75c0 8.076 5.373 14.625 12 14.625h272c6.627 0 12-6.549 12-14.625v-9.75c0-8.076-5.373-14.625-12-14.625H478c-6.627 0-12 6.549-12 14.625z" p-id="2655" fill="#2ccb98"></path></svg>
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 44 KiB |
|
@ -0,0 +1,33 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" width="100%" height="100%" viewBox="0 0 1400 800">
|
||||
|
||||
<rect x="1300" y="400" rx="40" ry="40" width="150" height="150" stroke="rgb(129, 201, 149)" fill="rgb(129, 201, 149)">
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="0 1450 550" to="360 1450 550" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
|
||||
<path d="M 100 350 A 150 150 0 1 1 400 350 Q400 370 380 370 L 250 370 L 120 370 Q100 370 100 350" fill="#a2b3ff">
|
||||
<animateMotion path="M 800 -200 L 800 -300 L 800 -200" dur="20s" begin="0s" repeatCount="indefinite"/>
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="30s" type="rotate" values="0 210 530 ; -30 210 530 ; 0 210 530" keyTimes="0 ; 0.5 ; 1" repeatCount="indefinite"/>
|
||||
</path>
|
||||
|
||||
<circle cx="150" cy="150" r="180" stroke="#85FFBD" fill="#85FFBD">
|
||||
<animateMotion path="M 0 0 L 40 20 Z" dur="5s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
|
||||
<!-- 三角形 -->
|
||||
<path d="M 165 580 L 270 580 Q275 578 270 570 L 223 483 Q220 480 217 483 L 165 570 Q160 578 165 580" fill="#a2b3ff">
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="0 210 530" to="360 210 530" repeatCount="indefinite"/>
|
||||
</path>
|
||||
|
||||
<!-- <circle cx="1200" cy="600" r="30" stroke="rgb(241, 243, 244)" fill="rgb(241, 243, 244)">-->
|
||||
<!-- <animateMotion path="M 0 0 L -20 40 Z" dur="9s" repeatCount="indefinite"/>-->
|
||||
<!-- </circle>-->
|
||||
|
||||
<path d="M 100 350 A 40 40 0 1 1 180 350 L 180 430 A 40 40 0 1 1 100 430 Z" fill="#3054EB">
|
||||
<animateMotion path="M 140 390 L 180 360 L 140 390" dur="20s" begin="0s" repeatCount="indefinite"/>
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="30s" type="rotate" values="0 140 390; -60 140 390; 0 140 390" keyTimes="0 ; 0.5 ; 1" repeatCount="indefinite"/>
|
||||
</path>
|
||||
|
||||
<rect x="400" y="600" rx="40" ry="40" width="100" height="100" stroke="rgb(129, 201, 149)" fill="#3054EB">
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="-30 550 750" to="330 550 750" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,84 @@
|
|||
.table-head-modal {
|
||||
.Custom-list-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
.left {
|
||||
width: 66%;
|
||||
padding: 10px 10px 10px 20px;
|
||||
border: 1px solid rgb(239 239 245);
|
||||
.choosableField {
|
||||
padding-bottom: 10px;
|
||||
.optionalField {
|
||||
padding: 5px 0;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
.amount {
|
||||
color: var(--ant-primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.check-all {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.check-ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
.check-li {
|
||||
padding: 2px 8px;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 12px;
|
||||
white-space: nowrap;
|
||||
.ant-checkbox-wrapper {
|
||||
width: 106px;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--ant-primary-2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
width: 34%;
|
||||
padding: 10px 10px 10px 20px;
|
||||
border: 1px solid rgb(239 239 245);
|
||||
.checkedField {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 10px;
|
||||
.optionalField {
|
||||
padding: 5px 0;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
.amount {
|
||||
color: var(--ant-primary-color);
|
||||
}
|
||||
}
|
||||
.ant-btn-link {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.drag-container {
|
||||
height: 296px;
|
||||
overflow-y: auto;
|
||||
.draggable-item-container {
|
||||
padding-right: 3px;
|
||||
padding-bottom: 5px;
|
||||
.drag-item {
|
||||
padding: 3px 8px;
|
||||
cursor: move;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
|
@ -0,0 +1,316 @@
|
|||
<template>
|
||||
<a-modal
|
||||
class="table-head-modal"
|
||||
:visible="tableHeadPop"
|
||||
:bodyStyle="{ padding: '8px' }"
|
||||
title="列表视图设置"
|
||||
:width="900"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="Custom-list-container">
|
||||
<!-- 可选字段 -->
|
||||
<div class="left">
|
||||
<div class="choosableField">
|
||||
<p class="optionalField">
|
||||
可选字段
|
||||
<span class="amount">{{ columnsAll.length }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="check-all">
|
||||
<a-checkbox v-model:checked="state.checkAll" :indeterminate="state.indeterminate" @change="onCheckAllChange">
|
||||
全选
|
||||
</a-checkbox>
|
||||
</div>
|
||||
<a-checkbox-group v-model:value="checkedList" @change="onChange" style="height: 265px; overflow-y: auto">
|
||||
<ul class="check-ul">
|
||||
<li v-for="(item, index) in columnsAll" :key="index" class="check-li">
|
||||
<a-checkbox :value="item.key" @change="getCheckOne" :disabled="item?.disabled ?? false">
|
||||
<span class="commonCode">{{ item.title }}</span>
|
||||
</a-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
<!-- 已选字段 -->
|
||||
<div class="right">
|
||||
<div class="checkedField">
|
||||
<p class="optionalField">
|
||||
已选字段
|
||||
<span class="amount">{{ sort.length + fixedColumns.fixedLeft.length + fixedColumns.fixedRight.length }}</span>
|
||||
</p>
|
||||
<a-button v-show="sort.length" type="link" @click="clearAll">清空</a-button>
|
||||
</div>
|
||||
<Draggable v-model="sort" item-key="key" :animation="100" :sort="true" class="drag-container">
|
||||
<template #item="{ element }">
|
||||
<div class="draggable-item-container">
|
||||
<div class="drag-item">
|
||||
<a-space style="justify-content: space-between; width: 100%">
|
||||
<div>
|
||||
<a-space>
|
||||
<SvgIcon name="move" />
|
||||
<span>{{ element.title }}</span>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-button type="text" size="small" @click="sortDeleteOne(element.key)">
|
||||
<template #icon>
|
||||
<SvgIcon name="close" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部 -->
|
||||
<template #footer>
|
||||
<a-button @click="handleCancel">返回</a-button>
|
||||
<a-button type="primary" class="search-btn" @click="handleOk">保存到本地</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="CompactHeaders">
|
||||
import { onMounted, ref, toRaw, watch, reactive } from "vue";
|
||||
import type { TableColumnType } from "ant-design-vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { getJsonArrEqual } from "@/utils/table";
|
||||
import Draggable from "vuedraggable";
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
import { createCacheStorage } from "@/utils/cache/storageCache";
|
||||
import { StorageType } from "@/enums/cache";
|
||||
|
||||
/* ts */
|
||||
interface CompactHeadersProps {
|
||||
columns: TableColumnType[]; // 必传
|
||||
tableKey: string; // 缓存自定义列标识 --> 必传
|
||||
}
|
||||
interface ColumnProps<T = any> extends TableColumnType<T> {
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
declare type Key = string | number;
|
||||
|
||||
/* Props */
|
||||
const props = withDefaults(defineProps<CompactHeadersProps>(), {});
|
||||
/* 触发自定义事件定义 */
|
||||
const emits = defineEmits<{
|
||||
(e: "update:columns", columns: any): void;
|
||||
}>();
|
||||
/* 选中key */
|
||||
const checkedList = ref<Key[]>([]);
|
||||
/* 全部列表项 */
|
||||
const columnsAll = ref<ColumnProps[]>([]);
|
||||
/* 固定列 */
|
||||
const fixedColumns = reactive<{
|
||||
indexColumn: ColumnProps[];
|
||||
fixedLeft: ColumnProps[];
|
||||
fixedRight: ColumnProps[];
|
||||
}>({
|
||||
indexColumn: [],
|
||||
fixedLeft: [],
|
||||
fixedRight: []
|
||||
});
|
||||
/* 排序数据 */
|
||||
const sort = ref<ColumnProps[]>([]);
|
||||
/* 全选状态与indeterminate状态 */
|
||||
const state = reactive({
|
||||
indeterminate: false,
|
||||
checkAll: false
|
||||
});
|
||||
/* 自定义列表弹窗状态 */
|
||||
const tableHeadPop = ref(false);
|
||||
/* 接受父组件参数 */
|
||||
const acceptParams = () => {
|
||||
tableHeadPop.value = true;
|
||||
setValues();
|
||||
};
|
||||
/* 关闭弹窗 */
|
||||
const handleCancel = () => {
|
||||
tableHeadPop.value = false;
|
||||
};
|
||||
/* 缓存自定义列表 */
|
||||
const storageConfig = {
|
||||
key: "sortTable-" + props.tableKey,
|
||||
type: StorageType.LOCAL,
|
||||
hasEncrypt: false
|
||||
};
|
||||
const sortStorage = createCacheStorage(storageConfig);
|
||||
/* 初始化 */
|
||||
const setValues = () => {
|
||||
checkedList.value = [];
|
||||
sort.value = [];
|
||||
// 获取相同项
|
||||
let newColumns = getJsonArrEqual(toRaw(props.columns), columnsAll.value);
|
||||
// 初始化checked属性
|
||||
columnsAll.value.forEach(i => {
|
||||
i.checked = false;
|
||||
// 是否禁用(禁用fixed--> left/right)
|
||||
if (i.fixed === "left" || i.fixed === "right") {
|
||||
i.disabled = true;
|
||||
}
|
||||
});
|
||||
const keySet = new Set(newColumns.map(i => i.key));
|
||||
columnsAll.value.forEach(item => {
|
||||
if (keySet.has(item.key)) {
|
||||
item.checked = true;
|
||||
item.key && checkedList.value.push(item.key);
|
||||
}
|
||||
});
|
||||
// 初始化排序数据
|
||||
props.columns.forEach(item => {
|
||||
// 排序数据(剔除固定列)
|
||||
if (item.fixed !== "left" && item.fixed !== "right") {
|
||||
sort.value.push(item);
|
||||
}
|
||||
});
|
||||
};
|
||||
/* 选中 */
|
||||
const onChange = (checkedValue: any) => {
|
||||
checkedList.value = checkedValue;
|
||||
};
|
||||
/* 单个选中 */
|
||||
const getCheckOne = (e: any) => {
|
||||
columnsAll.value.forEach((item, index) => {
|
||||
if (item.key === e.target.value) {
|
||||
// 根据选中与否赋值
|
||||
columnsAll.value[index].checked = e.target.checked;
|
||||
// 判断是否选中还是取消选中
|
||||
if (e.target.checked) {
|
||||
// 选中添加一个排序数据
|
||||
sort.value.push(item);
|
||||
} else {
|
||||
let index = sort.value.findIndex(i => i.key === item.key);
|
||||
if (index !== -1) sort.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
/* 点击全选 */
|
||||
const onCheckAllChange = (e: any) => {
|
||||
sort.value = [];
|
||||
// 判断是否全选
|
||||
if (e.target.checked) {
|
||||
if (columnsAll.value.length) {
|
||||
// 防止叠加
|
||||
checkedList.value = [];
|
||||
columnsAll.value.map(i => {
|
||||
i.key && checkedList.value.push(i.key);
|
||||
return (i.checked = true);
|
||||
});
|
||||
// 排序数据为全部
|
||||
columnsAll.value.forEach(item => {
|
||||
if (!item.disabled) {
|
||||
sort.value.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let newCheckedList: Key[] = [];
|
||||
// 固定列
|
||||
fixedColumns.fixedLeft.forEach(item => {
|
||||
item.key && newCheckedList.push(item.key);
|
||||
});
|
||||
fixedColumns.fixedRight.forEach(item => {
|
||||
item.key && newCheckedList.push(item.key);
|
||||
});
|
||||
checkedList.value = newCheckedList;
|
||||
columnsAll.value.map(i => {
|
||||
// 判断是否是固定列
|
||||
if (i.fixed === "left" || i.fixed === "right") {
|
||||
return (i.checked = true);
|
||||
} else {
|
||||
return (i.checked = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
/* 清空排序数据 */
|
||||
const clearAll = () => {
|
||||
sort.value = [];
|
||||
let newCheckedList: Key[] = [];
|
||||
// 固定列
|
||||
fixedColumns.fixedLeft.forEach(item => {
|
||||
item.key && newCheckedList.push(item.key);
|
||||
});
|
||||
fixedColumns.fixedRight.forEach(item => {
|
||||
item.key && newCheckedList.push(item.key);
|
||||
});
|
||||
checkedList.value = newCheckedList;
|
||||
};
|
||||
/* 单个排序数据删除 */
|
||||
const sortDeleteOne = (key: number | string) => {
|
||||
// 删除数据
|
||||
const sortIndex = sort.value.findIndex(i => i.key === key);
|
||||
if (sortIndex !== -1) sort.value.splice(sortIndex, 1);
|
||||
// 删除该key的checkedList
|
||||
const checkIndex = checkedList.value.findIndex(i => i === key);
|
||||
if (checkIndex !== -1) checkedList.value.splice(checkIndex, 1);
|
||||
};
|
||||
/* 获取选中 */
|
||||
const getValues = (): Promise<any[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 必须选择三项或者三项以上
|
||||
if (checkedList.value.length >= 3) {
|
||||
resolve(sort.value);
|
||||
} else {
|
||||
message.warning("必须选中三项或者三项以上");
|
||||
reject([]);
|
||||
}
|
||||
});
|
||||
};
|
||||
/* 确定 */
|
||||
const handleOk = async () => {
|
||||
let newColumns = await getValues();
|
||||
let _totalCols = [...fixedColumns.indexColumn, ...fixedColumns.fixedLeft, ...newColumns, ...fixedColumns.fixedRight];
|
||||
if (_totalCols.length) {
|
||||
let keys: Key[] = [];
|
||||
_totalCols.forEach(item => keys.push(item.key));
|
||||
// 缓存自定义列表
|
||||
sortStorage.set(keys);
|
||||
tableHeadPop.value = false;
|
||||
emits("update:columns", _totalCols);
|
||||
}
|
||||
};
|
||||
/* 监听全选按钮 */
|
||||
watch(
|
||||
() => [...checkedList.value],
|
||||
newVal => {
|
||||
state.indeterminate = !!newVal.length && newVal.length < columnsAll.value.length;
|
||||
state.checkAll = newVal.length === columnsAll.value.length;
|
||||
}
|
||||
);
|
||||
/* 组件挂载 */
|
||||
onMounted(() => {
|
||||
// 初始化columns与checked属性
|
||||
let newColumns = props.columns.map(item => ({ ...item, checked: false }));
|
||||
// 判断是否存在序号列并缓存
|
||||
newColumns.forEach(item => {
|
||||
if (item.key === "index") {
|
||||
fixedColumns.indexColumn.push(item);
|
||||
}
|
||||
});
|
||||
// 过滤掉不需要设置的列
|
||||
let filtrationKeys: (string | number)[] = ["index"];
|
||||
newColumns = newColumns!.filter(item => !filtrationKeys.includes(item.key!));
|
||||
columnsAll.value = newColumns;
|
||||
// 筛选固定左右列
|
||||
columnsAll.value.forEach(item => {
|
||||
if (item.fixed === "left") {
|
||||
fixedColumns.fixedLeft.push(item);
|
||||
}
|
||||
if (item.fixed === "right") {
|
||||
fixedColumns.fixedRight.push(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
acceptParams
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import url("./index.less");
|
||||
</style>
|
|
@ -0,0 +1,79 @@
|
|||
import { Component, h, render } from "vue";
|
||||
|
||||
interface Instance {
|
||||
id: number;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
// 定义一个变量,用于记录当前菜单实例
|
||||
let curInstance: Instance | null = null;
|
||||
// 定义一个变量,用于记录菜单的id
|
||||
let seed = 1;
|
||||
// 定义一个函数,用于展示右键菜单
|
||||
const contextMenu = (e: MouseEvent, data: any, component: Component) => {
|
||||
// 如果当前已经存在菜单实例,就销毁它
|
||||
if (curInstance) {
|
||||
curInstance.destroy();
|
||||
}
|
||||
// 将当前菜单实例置为null
|
||||
curInstance = null;
|
||||
// 生成一个新的菜单id
|
||||
let id = seed++;
|
||||
// 创建一个临时的div,用于挂载我们的菜单
|
||||
const container = document.createElement("div");
|
||||
// 获取body标签,用于挂载整个菜单
|
||||
const appendTo = document.body;
|
||||
// 传给menu组件的props
|
||||
const props = {
|
||||
data,
|
||||
onClose: () => {
|
||||
if (curInstance) {
|
||||
curInstance.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
// 渲染虚拟节点
|
||||
const vnode = h(component, props);
|
||||
// vnode为需要渲染的虚拟节点,container为渲染的容器
|
||||
render(vnode, container);
|
||||
// 首先需要先把菜单真正渲染到页面,才能拿到它的宽度和高度
|
||||
appendTo.appendChild(container.firstElementChild as HTMLElement);
|
||||
// 当前真正的菜单节点,上面输出的vnode中可以看到,el就是我们的菜单节点
|
||||
const curMenu = vnode.el as HTMLElement;
|
||||
// 获取curMenu的高度和宽度,用于临界的计算
|
||||
const { offsetWidth, offsetHeight } = curMenu;
|
||||
// 获取body的可视区域的宽度
|
||||
const { clientWidth } = appendTo;
|
||||
// 取出右键点击时的坐标,clientX是距离左侧的位置,clientY是距离顶部的位置
|
||||
const { clientX, clientY } = e;
|
||||
|
||||
// 当前可视区域的宽度 - 当前鼠标距离浏览器左边的距离
|
||||
// 如果 大于菜单的宽度,说明正常设置菜单距离左边界的距离,即设置style.left
|
||||
// 否则菜单需要在鼠标左侧展示,即需要设置style.right组件距离可视区域右侧的距离
|
||||
const leftOrRight = clientWidth - clientX > offsetWidth ? "left" : "right";
|
||||
|
||||
// 当前浏览器的高度(不包含滚动条) - 当前鼠标距离浏览器上边的距离
|
||||
// 如果 大于菜单的高度,说明可以正常设置菜单距离上边界的距离,即设置style.top
|
||||
// 否则需要设置菜单距离底部边界的位置,即style.bottom
|
||||
const topOrBottom = window.innerHeight - clientY > offsetHeight ? "top" : "bottom";
|
||||
const offsetLeft = Math.abs(clientWidth - clientX);
|
||||
// 设置left或者right的style
|
||||
curMenu.style[leftOrRight] = leftOrRight === "left" ? `${clientX + 20}px` : `${offsetLeft}px`;
|
||||
// 设置top或者bottom的style
|
||||
curMenu.style[topOrBottom] = topOrBottom === "bottom" ? "2px" : `${clientY}px`;
|
||||
|
||||
// 定义一个对象,用于记录当前菜单实例的id和销毁函数
|
||||
const instance: Instance = {
|
||||
id,
|
||||
destroy: () => {
|
||||
curInstance = null;
|
||||
render(null, container);
|
||||
}
|
||||
};
|
||||
// 将当前菜单实例置为instance
|
||||
curInstance = instance;
|
||||
// 返回当前菜单实例
|
||||
return instance;
|
||||
};
|
||||
|
||||
export default contextMenu;
|
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<div class="context-menu" ref="contextMenu" @blur="() => onClose()" tabindex="-1">
|
||||
<div class="context-menu-item-container">
|
||||
<vertical-right-outlined />
|
||||
<div class="context-menu-item" @click="clickMenu('closeLeft')">关闭左侧</div>
|
||||
</div>
|
||||
<div class="context-menu-item-container">
|
||||
<vertical-left-outlined />
|
||||
<div class="context-menu-item" @click="clickMenu('closeRight')">关闭右侧</div>
|
||||
</div>
|
||||
<div class="context-menu-item-container">
|
||||
<close-circle-outlined />
|
||||
<div class="context-menu-item" @click="clickMenu('closeOther')">关闭其他</div>
|
||||
</div>
|
||||
<div class="context-menu-item-container">
|
||||
<sync-outlined />
|
||||
<div class="context-menu-item" @click="clickMenu('refreshPage')">刷新页面</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, nextTick } from "vue";
|
||||
import { VerticalRightOutlined, VerticalLeftOutlined, CloseCircleOutlined, SyncOutlined } from "@ant-design/icons-vue";
|
||||
import { useGlobalStore } from "@/stores/modules/global";
|
||||
import { useTabsStore } from "@/stores/modules/tabs";
|
||||
import { useKeepAliveStore } from "@/stores/modules/keepAlive";
|
||||
|
||||
interface MenuValue {
|
||||
path: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const tabsStore = useTabsStore();
|
||||
const keepAliveStore = useKeepAliveStore();
|
||||
|
||||
const props = defineProps<{
|
||||
data: MenuValue;
|
||||
onClose: Function;
|
||||
}>();
|
||||
|
||||
// 创建组件ref
|
||||
const contextMenu = ref();
|
||||
onMounted(async () => {
|
||||
// 确保组件已经渲染
|
||||
await nextTick();
|
||||
// 触发组件focus
|
||||
contextMenu.value.focus();
|
||||
});
|
||||
|
||||
const clickMenu = (type: string) => {
|
||||
if (type === "closeLeft") closeLeftTab(props.data);
|
||||
if (type === "closeRight") closeRightTab(props.data);
|
||||
if (type === "closeOther") closeOtherTab(props.data);
|
||||
if (type === "refreshPage") refresh(props.data);
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
// 关闭左侧
|
||||
const closeLeftTab = (menu: MenuValue) => {
|
||||
tabsStore.closeLeftTab(menu.path);
|
||||
const names = tabsStore.tabsMenuList.map(item => item.name);
|
||||
keepAliveStore.setKeepAliveName([...names] as string[]);
|
||||
};
|
||||
|
||||
// 关闭右侧
|
||||
const closeRightTab = (menu: MenuValue) => {
|
||||
tabsStore.closeRightTab(menu.path);
|
||||
const names = tabsStore.tabsMenuList.map(item => item.name);
|
||||
keepAliveStore.setKeepAliveName([...names] as string[]);
|
||||
};
|
||||
|
||||
// 关闭其他
|
||||
const closeOtherTab = (menu: MenuValue) => {
|
||||
tabsStore.rightCloseMultipleTab(menu.path);
|
||||
keepAliveStore.setKeepAliveName([menu.name] as string[]);
|
||||
};
|
||||
|
||||
// 刷新当前页面
|
||||
const refresh = (menu: MenuValue) => {
|
||||
if (globalStore.routeName !== menu.name) return;
|
||||
setTimeout(() => {
|
||||
keepAliveStore.removeKeepAliveName(menu.name as string);
|
||||
globalStore.setGlobalState("refreshPage", false);
|
||||
nextTick(() => {
|
||||
keepAliveStore.addKeepAliveName(menu.name as string);
|
||||
globalStore.setGlobalState("refreshPage", true);
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
padding: 6px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
background-color: @bg-color;
|
||||
border: 1px solid rgb(222 222 222 / 50%);
|
||||
border-radius: 2px;
|
||||
.context-menu-item-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 14px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--ant-primary-color);
|
||||
background-color: var(--ant-primary-color-active-deprecated-f-30);
|
||||
}
|
||||
.context-menu-item {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>{{ value }}</span>
|
||||
</template>
|
||||
<a-button :type="type" :size="size" :ghost="isGhost" v-copy="value">{{ label }}</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="copyOptBtn">
|
||||
import { toRefs } from "vue";
|
||||
|
||||
interface copyOptBtnProps {
|
||||
value: string; // 复制的值 --> 必传
|
||||
label?: string; // 按钮的label值 --> 非必传,默认值为 -- 复制
|
||||
type?: "primary" | "ghost" | "dashed" | "link" | "text" | "default"; // 按钮类型 --> 非必传,默认值为 primary
|
||||
size?: "large" | "middle" | "small"; // 按钮类型大小 --> 非必传,默认值为 small
|
||||
isGhost?: boolean; // 幽灵属性,使按钮背景透明 --> 非必传,默认值为 true
|
||||
}
|
||||
/* Props */
|
||||
const props = withDefaults(defineProps<copyOptBtnProps>(), {
|
||||
label: "复制",
|
||||
type: "primary",
|
||||
size: "small",
|
||||
isGhost: true
|
||||
});
|
||||
const { value, label, type, size, isGhost } = toRefs(props);
|
||||
</script>
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<span ref="countupRef"></span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CountUp } from "countup.js";
|
||||
import type { CountUpOptions } from "countup.js";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
let numAnim = ref(null) as any;
|
||||
const countupRef = ref();
|
||||
const props = defineProps({
|
||||
end: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
validator(option: Object) {
|
||||
let keys = [
|
||||
"startVal",
|
||||
"decimalPlaces",
|
||||
"duration",
|
||||
"useGrouping",
|
||||
"useEasing",
|
||||
"smartEasingThreshold",
|
||||
"smartEasingAmount",
|
||||
"separator",
|
||||
"decimal",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"numerals"
|
||||
];
|
||||
for (const key in option) {
|
||||
if (!keys.includes(key)) {
|
||||
console.error(" CountUp 传入的 options 值不符合 CountUpOptions");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
default() {
|
||||
let options: CountUpOptions = {
|
||||
startVal: 0, // 开始的数字 一般设置0开始
|
||||
decimalPlaces: 2, // number类型 小数位,整数自动添.00
|
||||
duration: 2, // number类型 动画延迟秒数,默认值是2
|
||||
useGrouping: true, // boolean类型 是否开启逗号,默认true(1,000)false(1000)
|
||||
useEasing: true, // booleanl类型 动画缓动效果(ease),默认true
|
||||
smartEasingThreshold: 500, // numberl类型 大于这个数值的值开启平滑缓动
|
||||
smartEasingAmount: 300, // numberl类型
|
||||
separator: ",", // string 类型 分割用的符号
|
||||
decimal: ".", // string 类型 小数分割符合
|
||||
prefix: "", // sttring 类型 数字开头添加固定字符
|
||||
suffix: "", // sttring类型 数字末尾添加固定字符
|
||||
numerals: [] // Array类型 替换从0到9对应的字,也就是自定数字字符了,数组存储
|
||||
};
|
||||
return options;
|
||||
}
|
||||
}
|
||||
});
|
||||
onMounted(() => {
|
||||
initCount();
|
||||
});
|
||||
const initCount = () => {
|
||||
numAnim = new CountUp(countupRef.value, props.end, props.options);
|
||||
numAnim.start();
|
||||
};
|
||||
|
||||
const updateCount = (num: number) => {
|
||||
numAnim.update(num);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
initCount,
|
||||
updateCount
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div style="text-align: center" class="empty-wrapper">
|
||||
<img :src="EmptySrc" alt="" />
|
||||
<p>暂无数据</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import EmptySrc from "@/assets/images/empty.png";
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.empty-wrapper {
|
||||
img {
|
||||
height: 60px;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="not-container">
|
||||
<img src="@/assets/images/403.png" class="not-img" alt="403" />
|
||||
<div class="not-detail">
|
||||
<h2>403</h2>
|
||||
<h4>抱歉,您无权访问该页面~🙅♂️🙅♀️</h4>
|
||||
<a-button type="primary" @click="router.push(HOME_URL)">返回首页</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="403">
|
||||
import { useRouter } from "vue-router";
|
||||
import { HOME_URL } from "@/config";
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import url("./index.less");
|
||||
</style>
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="not-container">
|
||||
<img src="@/assets/images/404.png" class="not-img" alt="404" />
|
||||
<div class="not-detail">
|
||||
<h2>404</h2>
|
||||
<h4>抱歉,您访问的页面不存在~🤷♂️🤷♀️</h4>
|
||||
<a-button type="primary" @click="router.push(HOME_URL)">返回首页</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="404">
|
||||
import { useRouter } from "vue-router";
|
||||
import { HOME_URL } from "@/config";
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import url("./index.less");
|
||||
</style>
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="not-container">
|
||||
<img src="@/assets/images/500.png" class="not-img" alt="500" />
|
||||
<div class="not-detail">
|
||||
<h2>500</h2>
|
||||
<h4>抱歉,您的网络不见了~🤦♂️🤦♀️</h4>
|
||||
<a-button type="primary" @click="router.push(HOME_URL)">返回首页</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="500">
|
||||
import { useRouter } from "vue-router";
|
||||
import { HOME_URL } from "@/config";
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import url("./index.less");
|
||||
</style>
|
|
@ -0,0 +1,33 @@
|
|||
.not-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.not-img {
|
||||
max-width: 500px;
|
||||
margin-right: 120px;
|
||||
}
|
||||
.not-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
h2,
|
||||
h4 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 60px;
|
||||
color: var(--ant-primary-color);
|
||||
}
|
||||
h4 {
|
||||
margin: 30px 0 20px;
|
||||
font-size: 19px;
|
||||
font-weight: normal;
|
||||
color: var(--ant-primary-color);
|
||||
}
|
||||
.a-button {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<a-select
|
||||
v-bind="$attrs"
|
||||
:loading="isLoading"
|
||||
:options="roleList"
|
||||
allowClear
|
||||
@focus="getRoleList"
|
||||
placeholder="请选择角色类型"
|
||||
class="roleSelect"
|
||||
></a-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect } from "vue";
|
||||
import { getRolesListApi } from "@/api/modules/role";
|
||||
import { createCacheStorage } from "@/utils/cache/storageCache";
|
||||
import { CacheConfig } from "@/utils/cache/config";
|
||||
import { message } from "ant-design-vue";
|
||||
import type { SelectProps } from "ant-design-vue";
|
||||
import type { Role } from "@/api/interface/index";
|
||||
|
||||
/* Props */
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isImmediately?: boolean;
|
||||
}>(),
|
||||
{
|
||||
isImmediately: false
|
||||
}
|
||||
);
|
||||
|
||||
/* 是否加载中 */
|
||||
const isLoading = ref(false);
|
||||
/* 角色列表 */
|
||||
const roleList = ref<SelectProps["options"]>([]);
|
||||
/* 筛选、转化{label: '', value: ''} */
|
||||
const handleRoleData = (filterData: Role.RoleList[]) => {
|
||||
return filterData.map(item => {
|
||||
return { label: item.name, value: item.id };
|
||||
});
|
||||
};
|
||||
/* 获取角色列表 */
|
||||
const getRoleList = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const useRoleList = createCacheStorage(CacheConfig.RoleSelect);
|
||||
const oldRoleListData = useRoleList.get();
|
||||
if (!oldRoleListData) {
|
||||
let { data } = await getRolesListApi();
|
||||
if (data && data.length > 0) {
|
||||
roleList.value = handleRoleData(data);
|
||||
useRoleList.set(data);
|
||||
}
|
||||
} else {
|
||||
roleList.value = handleRoleData(oldRoleListData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
message.error("获取角色列表失败");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
/* 刷新数据 */
|
||||
const refresh = () => {
|
||||
const useRoleList = createCacheStorage(CacheConfig.RoleSelect);
|
||||
useRoleList.remove();
|
||||
getRoleList();
|
||||
};
|
||||
/* 获取数据源 */
|
||||
const getData = () => {
|
||||
return roleList.value;
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.isImmediately) {
|
||||
getRoleList();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
getData
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.roleSelect {
|
||||
min-width: 200px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,20 @@
|
|||
.ant-upload.ant-upload-drag {
|
||||
width: 80% !important;
|
||||
p.ant-upload-text {
|
||||
margin-bottom: 20px !important;
|
||||
font-size: 14px !important;
|
||||
color: rgba(#000000, 0.85) !important;
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--ant-primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-upload.ant-upload-drag p.ant-upload-drag-icon {
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
.a-upload__tip {
|
||||
margin-top: 7px;
|
||||
font-size: 14px;
|
||||
color: @info-text-color;
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
<template>
|
||||
<a-modal v-model:visible="visible" :title="`批量添加${parameter.title}`" :footer="null" width="580px">
|
||||
<a-form>
|
||||
<!-- 模板下载 -->
|
||||
<a-form-item label="模板下载 :">
|
||||
<a-button type="primary" @click="downloadTemp">
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
点击下载
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
<!-- 文件上传 -->
|
||||
<a-form-item label="文件上传 :">
|
||||
<a-upload-dragger
|
||||
name="file"
|
||||
:multiple="true"
|
||||
class="upload"
|
||||
:maxCount="excelLimit"
|
||||
@change="handleChange"
|
||||
:showUploadList="false"
|
||||
:customRequest="uploadExcel"
|
||||
:beforeUpload="beforeExcelUpload"
|
||||
:accept="parameter.fileType!.join(',')"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<cloud-upload-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">将文件拖到此处,或<em>点击上传</em></p>
|
||||
</a-upload-dragger>
|
||||
<div class="a-upload__tip">请上传 .xls , .xlsx 标准格式文件,文件最大为 {{ parameter.fileSize }}M</div>
|
||||
</a-form-item>
|
||||
<!-- 数据覆盖 -->
|
||||
<a-form-item label="数据覆盖 :">
|
||||
<a-switch v-model:checked="isCover" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="importExcel">
|
||||
import { ref } from "vue";
|
||||
import { useDownload } from "@/hooks/useDownload";
|
||||
import { UploadChangeParam, notification } from "ant-design-vue";
|
||||
|
||||
/* 接口 */
|
||||
export interface ExcelParameterProps {
|
||||
title: string; // 标题
|
||||
fileSize?: number; // 上传文件的大小
|
||||
fileType?: File.ExcelMimeType[]; // 上传文件的类型
|
||||
tempApi?: (params: any) => Promise<any>; // 下载模板的Api
|
||||
importApi?: (params: any) => Promise<any>; // 批量导入的Api
|
||||
getTableList?: () => void; // 获取表格数据的Api
|
||||
}
|
||||
/* 是否覆盖数据 */
|
||||
const isCover = ref(false);
|
||||
/* 最大文件上传数 */
|
||||
const excelLimit = ref(1);
|
||||
/* 弹窗状态 */
|
||||
const visible = ref(false);
|
||||
/* 限制弹框 M4-38464 */
|
||||
let limitMessage: boolean = true;
|
||||
/* 父组件传过来的参数 */
|
||||
const parameter = ref<ExcelParameterProps>({
|
||||
title: "",
|
||||
fileSize: 5,
|
||||
fileType: ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]
|
||||
});
|
||||
/* 接受父组件参数 */
|
||||
const acceptParams = (params: ExcelParameterProps) => {
|
||||
parameter.value = { ...parameter.value, ...params };
|
||||
visible.value = true;
|
||||
};
|
||||
/* Excel 导入模板下载 */
|
||||
const downloadTemp = () => {
|
||||
if (!parameter.value.tempApi) return;
|
||||
useDownload(parameter.value.tempApi, `${parameter.value.title}模板`);
|
||||
};
|
||||
/* 文件上传 */
|
||||
const uploadExcel = async (e: any) => {
|
||||
try {
|
||||
let excelFormData = new FormData();
|
||||
excelFormData.append("file", e.file);
|
||||
excelFormData.append("isCover", isCover.value as unknown as Blob);
|
||||
let res = await parameter.value.importApi!(excelFormData);
|
||||
// 调用实例的成功方法通知组件该文件上传成功
|
||||
e.onSuccess(res.data, e);
|
||||
// 重新刷新表格
|
||||
parameter.value.getTableList && parameter.value.getTableList();
|
||||
visible.value = false;
|
||||
} catch (error) {
|
||||
// 调用实例的失败方法通知组件该文件上传失败
|
||||
e.onError(error);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @description 文件上传之前判断
|
||||
* @param file 上传的文件
|
||||
*/
|
||||
const beforeExcelUpload = async (file: any, fileList: any) => {
|
||||
const isExcel = parameter.value.fileType!.includes(file.type as File.ExcelMimeType);
|
||||
const fileSize = file.size / 1024 / 1024 < parameter.value.fileSize!;
|
||||
const fileAmount = fileList.length >= 2 ? false : true;
|
||||
if (!isExcel) {
|
||||
notification["warning"]({
|
||||
message: "温馨提示",
|
||||
description: "上传文件只能是 xls / xlsx 格式!",
|
||||
style: { borderRadius: "8px" },
|
||||
duration: 3
|
||||
});
|
||||
}
|
||||
if (!fileSize) {
|
||||
setTimeout(() => {
|
||||
notification["warning"]({
|
||||
message: "温馨提示",
|
||||
description: `上传文件大小不能超过 ${parameter.value.fileSize}MB!`,
|
||||
style: { borderRadius: "8px" },
|
||||
duration: 3
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
if (!fileAmount) {
|
||||
if (limitMessage) {
|
||||
notification["warning"]({
|
||||
message: "温馨提示",
|
||||
description: "最多只能上传一个文件!",
|
||||
style: { borderRadius: "8px" },
|
||||
duration: 3
|
||||
});
|
||||
limitMessage = false;
|
||||
setTimeout(() => {
|
||||
limitMessage = true;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
return isExcel && fileSize && fileAmount;
|
||||
};
|
||||
/* 上传文件改变时的状态 */
|
||||
const handleChange = (info: UploadChangeParam) => {
|
||||
const { status } = info.file;
|
||||
if (status === "done") {
|
||||
excelUploadSuccess();
|
||||
} else if (status === "error") {
|
||||
excelUploadError();
|
||||
}
|
||||
};
|
||||
/* 上传错误提示 */
|
||||
const excelUploadError = (): void => {
|
||||
notification["error"]({
|
||||
message: "温馨提示",
|
||||
description: `批量添加${parameter.value.title}失败,请您重新上传!`
|
||||
});
|
||||
};
|
||||
/* 上传成功提示 */
|
||||
const excelUploadSuccess = (): void => {
|
||||
notification["success"]({
|
||||
message: "温馨提示",
|
||||
description: `批量添加${parameter.value.title}成功!`
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import url("./index.less");
|
||||
</style>
|
|
@ -0,0 +1,193 @@
|
|||
<template>
|
||||
<div class="particle-clock-container">
|
||||
<canvas ref="canvas"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ParticleClock">
|
||||
import { ref, onMounted, onUnmounted, computed } from "vue";
|
||||
import { useGlobalStore } from "@/stores/modules/global";
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const primary = computed(() => globalStore.primary);
|
||||
|
||||
onMounted(() => {
|
||||
// 当组件挂载时
|
||||
ctx = canvas.value.getContext("2d", {
|
||||
// 获取 2D 上下文
|
||||
willReadFrequently: true
|
||||
});
|
||||
|
||||
initCanvasSize(); // 初始化画布尺寸
|
||||
draw(); // 开始绘制
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 当组件卸载时
|
||||
cancelAnimationFrame(animationFrameId); // 取消动画帧
|
||||
});
|
||||
|
||||
/* 创建一个画布引用 */
|
||||
const canvas = ref();
|
||||
/* 定义画布上下文变量 */
|
||||
let ctx: any;
|
||||
/* 定义动画帧 ID 变量 */
|
||||
let animationFrameId: any;
|
||||
/* 初始化画布尺寸函数 */
|
||||
const initCanvasSize = () => {
|
||||
// 设置画布宽度
|
||||
canvas.value.width = window.innerWidth * devicePixelRatio;
|
||||
// 设置画布高度
|
||||
canvas.value.height = window.innerHeight * devicePixelRatio;
|
||||
};
|
||||
/* 获取 [min, max] 范围内的随机整数 */
|
||||
const getRandom = (min: number, max: number) => {
|
||||
// 返回随机整数
|
||||
return Math.floor(Math.random() * (max + 1 - min) + min);
|
||||
};
|
||||
/* 粒子类 */
|
||||
class Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
// 构造函数
|
||||
constructor() {
|
||||
const r = Math.min(canvas.value.width, canvas.value.height) / 2; // 计算半径
|
||||
const cx = canvas.value.width / 2; // 计算中心点 X 坐标
|
||||
const cy = canvas.value.height / 2; // 计算中心点 Y 坐标
|
||||
const rad = (getRandom(0, 360) * Math.PI) / 180; // 计算随机弧度
|
||||
this.x = cx + r * Math.cos(rad); // 设置粒子 X 坐标
|
||||
this.y = cy + r * Math.sin(rad); // 设置粒子 Y 坐标
|
||||
this.size = 12; // 设置粒子大小
|
||||
}
|
||||
// 绘制粒子方法
|
||||
draw() {
|
||||
ctx.beginPath(); // 开始路径
|
||||
ctx.fillStyle = primary.value; // 设置填充样式
|
||||
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI); // 绘制圆形
|
||||
ctx.fill(); // 填充路径
|
||||
}
|
||||
// 移动粒子方法
|
||||
moveTo(tx: number, ty: number) {
|
||||
const duration = 500; // 500ms的运动时间
|
||||
const sx = this.x, // 起始 X 坐标
|
||||
sy = this.y; // 起始 Y 坐标
|
||||
const xSpeed = (tx - sx) / duration; // 计算 X 方向速度
|
||||
const ySpeed = (ty - sy) / duration; // 计算 Y 方向速度
|
||||
const startTime = Date.now(); // 记录开始时间
|
||||
const _move = () => {
|
||||
// 移动函数
|
||||
const t = Date.now() - startTime; // 计算已过时间
|
||||
const x = sx + xSpeed * t; // 计算当前 X 坐标
|
||||
const y = sy + ySpeed * t; // 计算当前 Y 坐标
|
||||
this.x = x; // 更新粒子 X 坐标
|
||||
this.y = y; // 更新粒子 Y 坐标
|
||||
if (t >= duration) {
|
||||
// 如果已达到运动时间
|
||||
this.x = tx; // 设置粒子 X 坐标为目标 X 坐标
|
||||
this.y = ty; // 设置粒子 Y 坐标为目标 Y 坐标
|
||||
return; // 结束函数
|
||||
}
|
||||
// x,y改动一点
|
||||
requestAnimationFrame(_move); // 请求下一帧动画
|
||||
};
|
||||
_move(); // 开始移动
|
||||
}
|
||||
}
|
||||
/* 粒子数组 */
|
||||
const partciles: any[] = [];
|
||||
/* 文本变量 */
|
||||
let text: any = null;
|
||||
/* 清除画布函数 */
|
||||
const clear = () => {
|
||||
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height); // 清除画布内容
|
||||
};
|
||||
/* 绘制函数 */
|
||||
const draw = () => {
|
||||
clear(); // 清除画布
|
||||
update(); // 更新状态
|
||||
partciles.forEach(p => p.draw()); // 绘制每个粒子
|
||||
animationFrameId = requestAnimationFrame(draw); // 请求下一帧动画
|
||||
};
|
||||
/* 获取文本函数 */
|
||||
const getText = () => {
|
||||
// 返回当前时间字符串
|
||||
return new Date().toTimeString().substring(0, 8);
|
||||
};
|
||||
/* 更新状态函数 */
|
||||
const update = () => {
|
||||
const newText = getText(); // 获取新文本
|
||||
if (newText === text) {
|
||||
// 如果文本没有变化
|
||||
return; // 结束函数
|
||||
}
|
||||
clear(); // 清除画布
|
||||
text = newText; // 更新文本
|
||||
// 画文本
|
||||
const { width, height } = canvas.value; // 获取画布宽高
|
||||
ctx.fillStyle = "#000"; // 设置填充样式
|
||||
ctx.textBaseline = "middle"; // 设置文本基线
|
||||
ctx.font = `${240 * devicePixelRatio}px 'DS-Digital', sans-serif`; // 设置字体样式
|
||||
ctx.fillText(text, (width - ctx.measureText(text).width) / 2, height / 2); // 绘制文本
|
||||
const points = getPoints(); // 获取点集
|
||||
clear(); // 清除画布
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
// 遍历点集
|
||||
let p = partciles[i]; // 获取粒子
|
||||
if (!p) {
|
||||
// 如果粒子不存在
|
||||
p = new Particle(); // 创建新粒子
|
||||
partciles.push(p); // 将粒子添加到数组
|
||||
}
|
||||
const [x, y] = points[i]; // 获取点坐标
|
||||
p.moveTo(x, y); // 移动粒子到目标位置
|
||||
}
|
||||
if (points.length < partciles.length) {
|
||||
// 如果点集数量小于粒子数量
|
||||
partciles.splice(points.length); // 删除多余粒子
|
||||
}
|
||||
};
|
||||
/* 获取点集函数 */
|
||||
const getPoints = () => {
|
||||
const { width, height, data } = ctx.getImageData(
|
||||
// 获取画布图像数据
|
||||
0,
|
||||
0,
|
||||
canvas.value.width,
|
||||
canvas.value.height
|
||||
);
|
||||
const points = []; // 点集数组
|
||||
const gap = 6; // 间隔
|
||||
for (let i = 0; i < width; i += gap) {
|
||||
// 遍历宽度
|
||||
for (let j = 0; j < height; j += gap) {
|
||||
// 遍历高度
|
||||
const index = (i + j * width) * 4; // 计算像素索引
|
||||
const r = data[index]; // 获取红色通道值
|
||||
const g = data[index + 1]; // 获取绿色通道值
|
||||
const b = data[index + 2]; // 获取蓝色通道值
|
||||
const a = data[index + 3]; // 获取透明度通道值
|
||||
if (r === 0 && g === 0 && b === 0 && a === 255) {
|
||||
// 如果像素为黑色
|
||||
points.push([i, j]); // 将坐标添加到点集
|
||||
}
|
||||
}
|
||||
}
|
||||
return points; // 返回点集
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.particle-clock-container {
|
||||
width: 120px;
|
||||
height: 50px;
|
||||
margin-top: 4px;
|
||||
margin-right: 6px;
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div class="antd-custom-pagination">
|
||||
<a-pagination
|
||||
v-model:current="pageabale.pageNum"
|
||||
v-model:pageSize="pageabale.pageSize"
|
||||
show-size-changer
|
||||
show-quick-jumper
|
||||
:page-size-options="pageSizeOptions"
|
||||
:total="pageabale.total"
|
||||
:show-total="(total: number) => `共 ${total} 条`"
|
||||
@change="handlePageAndPageSize"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="pagination">
|
||||
import { ref } from "vue";
|
||||
|
||||
interface Pageable {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface PaginationProps {
|
||||
pageabale: Pageable;
|
||||
handlePageAndPageSize: (page: number, pageSize: number) => void;
|
||||
}
|
||||
|
||||
const pageSizeOptions = ref<string[]>(["10", "25", "50", "100"]);
|
||||
|
||||
defineProps<PaginationProps>();
|
||||
</script>
|
|
@ -0,0 +1,326 @@
|
|||
<template>
|
||||
<!-- 查询表单 -->
|
||||
<SearchForm
|
||||
ref="searchForm"
|
||||
:getScrollY="getScrollY"
|
||||
:search="search"
|
||||
:reset="reset"
|
||||
:search-param="searchParam"
|
||||
v-show="isShowSearch"
|
||||
>
|
||||
<template #searchItem="scope">
|
||||
<slot name="searchForm" :expand="scope.expand" :formState="scope.formState"></slot>
|
||||
</template>
|
||||
</SearchForm>
|
||||
<!-- 表格内容 card -->
|
||||
<div class="card table-main">
|
||||
<!-- 表格头部 操作按钮 -->
|
||||
<div class="table-header">
|
||||
<div class="header-button-lf">
|
||||
<slot name="tableHeader" :selectedListIds="selectedListIds" :selectedList="selectedList" :isSelected="isSelected"></slot>
|
||||
</div>
|
||||
<div class="header-button-ri">
|
||||
<slot name="toolButton">
|
||||
<!-- 刷新 -->
|
||||
<a-button shape="circle" class="tool-btn" @click="getTableList">
|
||||
<template #icon>
|
||||
<sync-outlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 打印 -->
|
||||
<a-tooltip placement="top">
|
||||
<template #title>
|
||||
<span>暂不支持表格打印功能</span>
|
||||
</template>
|
||||
<a-button shape="circle" class="tool-btn">
|
||||
<template #icon>
|
||||
<printer-outlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<!-- 表格配置 -->
|
||||
<a-button shape="circle" class="tool-btn" @click="openCompactHeaders">
|
||||
<template #icon>
|
||||
<setting-outlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 搜索栏显隐 -->
|
||||
<a-button shape="circle" class="tool-btn" @click="isShowSearch = !isShowSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表格主体 -->
|
||||
<a-table
|
||||
ref="tableRef"
|
||||
:loading="isLoading"
|
||||
:columns="tableColumns"
|
||||
v-bind="$attrs"
|
||||
:dataSource="data ?? tableData"
|
||||
:bordered="border"
|
||||
:rowKey="rowKey"
|
||||
:row-selection="multiple ? rowSelection : false"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 2000, y: scrollY }"
|
||||
@resizeColumn="handleResizeColumn"
|
||||
>
|
||||
<!-- 个性化头部单元格 -->
|
||||
<template #headerCell="{ title, column }">
|
||||
<slot name="headerCell" :title="title" :column="column"></slot>
|
||||
</template>
|
||||
<!-- 个性化单元格 -->
|
||||
<template #bodyCell="{ text, record, index, column }">
|
||||
<slot name="bodyCell" :text="text" :record="record" :index="index" :column="column"></slot>
|
||||
<template v-if="column.ellipsis">
|
||||
<a-tooltip placement="top">
|
||||
<template #title>
|
||||
<span>{{ text }}</span>
|
||||
</template>
|
||||
<span>{{ text }}</span>
|
||||
</a-tooltip>
|
||||
<!-- <TableTooltip :content="text">{{ text }}</TableTooltip> -->
|
||||
</template>
|
||||
<!-- 序号 -->
|
||||
<template v-if="column.key === 'index'">
|
||||
<span>{{ parseInt(index) + 1 }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 额外的展开行 -->
|
||||
<template #expandedRowRender="scope">
|
||||
<slot name="expandedRowRender" v-bind="scope"></slot>
|
||||
</template>
|
||||
<!-- 总结栏 -->
|
||||
<!-- <template #summary>
|
||||
<slot name="summary"></slot>
|
||||
</template> -->
|
||||
<!-- 无数据 -->
|
||||
<template #emptyText>
|
||||
<div class="table-empty" :style="{ height: noDataHeight }">
|
||||
<slot name="emptyText">
|
||||
<div class="notData-container">
|
||||
<img src="@/assets/images/emptyData.png" alt="notData" />
|
||||
<div class="notice">暂无数据</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
<!-- antd vue table 插槽 -->
|
||||
<!-- <template v-for="slot in Object.keys($slots)" #[slot]="scope">
|
||||
<slot :name="slot" v-bind="scope"></slot>
|
||||
</template> -->
|
||||
</a-table>
|
||||
<!-- 分页组件 -->
|
||||
<Pagination :pageabale="pageable" :handle-page-and-page-size="handlePageAndPageSize" />
|
||||
<!-- 底部操作按钮 -->
|
||||
<transition appear name="fade-transform" mode="out-in">
|
||||
<div class="footer-slot-wrapper" v-show="selectedList.length">
|
||||
<a-space>
|
||||
<a-checkbox v-model:checked="state.checkAll" @change="onCheckAllChange" :indeterminate="state.indeterminate">
|
||||
已选择
|
||||
<span class="footer-selected-count">{{ selectedList.length }}</span>
|
||||
项
|
||||
</a-checkbox>
|
||||
<!-- 按钮插槽 -->
|
||||
<slot name="footer-btn" :selectedListIds="selectedListIds" :selectedList="selectedList" :isSelected="isSelected"></slot>
|
||||
</a-space>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 列表设置 -->
|
||||
<CompactHeaders ref="CompactHeadersRef" :table-key="props.tableKey" v-model:columns="tableColumns" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx" name="ProTable">
|
||||
import { ref, reactive, onMounted, nextTick, watch } from "vue";
|
||||
import type { TableColumnType } from "ant-design-vue";
|
||||
import { Table } from "ant-design-vue";
|
||||
import { useTable } from "@/hooks/useTable";
|
||||
import { useSelection } from "@/hooks/useSelection";
|
||||
import Pagination from "./components/Pagination.vue";
|
||||
import SearchForm from "@/components/SearchForm/index.vue";
|
||||
import { getTableScroll } from "@/utils/table";
|
||||
import CompactHeaders from "@/components/CompactHeaders/index.vue";
|
||||
// import TableTooltip from "@/components/TableTooltip/index.vue";
|
||||
import { createCacheStorage } from "@/utils/cache/storageCache";
|
||||
import { StorageType } from "@/enums/cache";
|
||||
import { isArray } from "@/utils/is";
|
||||
|
||||
interface ProTableProps {
|
||||
columns: TableColumnType[];
|
||||
tableKey: string; // table 唯一标识,缓存自定义列使用 --> 必传
|
||||
data?: any[]; // 静态 table data 数据,若存在则不会使用 requestApi 返回的 data ---> 非必传
|
||||
requestApi?: (params: any) => Promise<any>; // 请求表格数据的 api ---> 非必传
|
||||
requestAuto?: boolean; // 是否自动执行请求 api ---> 非必传(默认为true)
|
||||
requestError?: (params: any) => void; // 表格 api 请求错误监听 ---> 非必传
|
||||
dataCallback?: (data: any) => any; // 返回数据的回调函数,可以对数据进行处理 ---> 非必传
|
||||
isPagination?: boolean; // 是否需要分页组件 ---> 非必传(默认为true)
|
||||
initParam?: any; // 初始化请求参数 ---> 非必传(默认为{})
|
||||
multiple?: boolean; //表格是否多选 ---> 非必传(默认为false)
|
||||
border?: boolean; // 是否带有纵向边框 ---> 非必传(默认值为true)
|
||||
toolButton?: boolean; // 是否显示表格功能按钮 ---> 非必传(默认值为true)
|
||||
rowKey?: string; // 行数据的 Key,用来优化 Table 的渲染,当表格数据多选时,所指定的 id ---> 非必传(默认值为id)
|
||||
isSummary?: boolean; // 表格总结栏,用来优化 Table 滚动条的渲染 ---> 非必传(默认值为false)
|
||||
}
|
||||
/* 接受父组件参数,配置默认值 */
|
||||
const props = withDefaults(defineProps<ProTableProps>(), {
|
||||
requestAuto: true,
|
||||
isPagination: true,
|
||||
initParam: {},
|
||||
multiple: false,
|
||||
border: true,
|
||||
toolButton: true,
|
||||
rowKey: "id",
|
||||
isSummary: false
|
||||
});
|
||||
/* 是否显示搜索模块 */
|
||||
const isShowSearch = ref(true);
|
||||
/* 表格 DOM 元素 */
|
||||
const tableRef = ref();
|
||||
/* 底部全选按钮状态 */
|
||||
const state = reactive({
|
||||
indeterminate: false,
|
||||
checkAll: false
|
||||
});
|
||||
/* 底部全选 */
|
||||
const onCheckAllChange = (e: any) => {
|
||||
if (e.target.checked) selectionChange(tableData.value);
|
||||
else selectionChange([]);
|
||||
};
|
||||
/* 表格多选 Hooks */
|
||||
const { selectionChange, selectedList, selectedListIds, isSelected } = useSelection(props.rowKey);
|
||||
/* 表格操作 Hooks */
|
||||
const { tableData, isLoading, searchParam, pageable, getTableList, search, reset, handlePageAndPageSize } = useTable(
|
||||
props.requestApi,
|
||||
props.initParam,
|
||||
props.isPagination,
|
||||
props.dataCallback,
|
||||
props.requestError
|
||||
);
|
||||
/* 表格选择操作 */
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedListIds,
|
||||
onChange: (selectedRowKeys: Key[], selectedRows: DefaultRecordType[]) => {
|
||||
// 表格行选中操作
|
||||
selectionChange(selectedRows);
|
||||
},
|
||||
selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE]
|
||||
};
|
||||
/* 清空选中数据列表 */
|
||||
const clearSelection = () => selectionChange([]);
|
||||
/* 接受 columns 并设置为响应式 */
|
||||
const tableColumns = ref<TableColumnType[]>(props.columns);
|
||||
/* 搜索表单实例 */
|
||||
const scrollY = ref("0");
|
||||
const searchForm = ref();
|
||||
/* 空状态高度 */
|
||||
const noDataHeight = ref("0");
|
||||
/* 获取table scrollY */
|
||||
const getScrollY = () => {
|
||||
nextTick(() => {
|
||||
let scrol_Y = getTableScroll({ isSummary: props.isSummary });
|
||||
let height = getTableScroll({ extraHeight: 108 });
|
||||
scrollY.value = scrol_Y;
|
||||
noDataHeight.value = height;
|
||||
});
|
||||
};
|
||||
/* 可伸缩列,伸缩距离(100 - 400) --> 单页数据量大,拖拽表头与表体不同步(待优化) */
|
||||
const handleResizeColumn = (w: string | number, col: TableColumnType) => {
|
||||
if (100 > Number(w) || Number(w) > 400) return;
|
||||
if (col.width) col.width = w;
|
||||
};
|
||||
/* 打开列表设置 */
|
||||
const CompactHeadersRef = ref<InstanceType<typeof CompactHeaders> | null>(null);
|
||||
const openCompactHeaders = () => {
|
||||
CompactHeadersRef.value?.acceptParams();
|
||||
};
|
||||
/* 自定义列缓存 */
|
||||
const storageConfig = {
|
||||
key: "sortTable-" + props.tableKey,
|
||||
type: StorageType.LOCAL,
|
||||
hasEncrypt: false
|
||||
};
|
||||
const sortStorage = createCacheStorage(storageConfig);
|
||||
/* 初始化请求 */
|
||||
onMounted(() => {
|
||||
props.requestAuto && getTableList();
|
||||
scrollY.value = getTableScroll({ isSummary: props.isSummary });
|
||||
noDataHeight.value = getTableScroll({ extraHeight: 108 });
|
||||
// 判断是否存在自定义列缓存
|
||||
let _cache = sortStorage.get();
|
||||
if (_cache && isArray(_cache)) {
|
||||
let newColumns: TableColumnType[] = [];
|
||||
_cache.forEach(item => {
|
||||
tableColumns.value.forEach(column => {
|
||||
if (item === column.key) {
|
||||
newColumns.push(column);
|
||||
}
|
||||
});
|
||||
});
|
||||
tableColumns.value = [...newColumns];
|
||||
}
|
||||
});
|
||||
/* 监听搜索栏显隐 */
|
||||
watch(
|
||||
() => isShowSearch.value,
|
||||
() => {
|
||||
getScrollY();
|
||||
}
|
||||
);
|
||||
/* 监听 selectedList */
|
||||
watch(
|
||||
() => selectedList.value,
|
||||
newVal => {
|
||||
state.indeterminate = !!newVal.length && newVal.length < tableData.value.length;
|
||||
state.checkAll = newVal.length === tableData.value.length;
|
||||
}
|
||||
);
|
||||
/* 暴露给父组件的参数和方法(外部需要什么,都可以从这里暴露出去) */
|
||||
defineExpose({
|
||||
tableData,
|
||||
searchParam,
|
||||
pageable,
|
||||
getTableList,
|
||||
reset,
|
||||
isSelected,
|
||||
selectedList,
|
||||
selectedListIds,
|
||||
clearSelection
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.notData-container {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 70px;
|
||||
text-align: center;
|
||||
img {
|
||||
height: 210px;
|
||||
}
|
||||
.notice {
|
||||
font-size: 16px;
|
||||
line-height: 29px;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
.footer-slot-wrapper {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 30px);
|
||||
height: 32px;
|
||||
background: #ffffff;
|
||||
.footer-selected-count {
|
||||
font-weight: 600;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
:deep(.ant-table-summary) {
|
||||
height: 48px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="card table-search">
|
||||
<a-form ref="formRef" name="advanced_search" class="ant-advanced-search-form" :model="searchParam">
|
||||
<a-row :gutter="24">
|
||||
<slot name="searchItem" :expand="expand" :formState="searchParam"></slot>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-col :span="24" style="text-align: right">
|
||||
<!-- 搜索 -->
|
||||
<a-button type="primary" @click="search">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<!-- 重置 -->
|
||||
<a-button style="margin: 0 8px" @click="reset">
|
||||
<template #icon>
|
||||
<rest-outlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
<!-- 展开、合并 -->
|
||||
<a-button type="link" @click="expand = !expand">
|
||||
<template #icon>
|
||||
<component :is="expand ? UpOutlined : DownOutlined"></component>
|
||||
</template>
|
||||
{{ expand ? "合并" : "展开" }}
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="SearchForm">
|
||||
import { ref, watch } from "vue";
|
||||
import { UpOutlined, DownOutlined } from "@ant-design/icons-vue";
|
||||
import type { FormInstance } from "ant-design-vue";
|
||||
/* 接口 */
|
||||
interface SearchFormProps {
|
||||
searchParam?: { [key: string]: any }; // 搜索参数
|
||||
search: (params?: any) => void; // 搜索方法
|
||||
reset: (params: any) => void; // 重置方法
|
||||
getScrollY: () => void; // 获取表格高度
|
||||
}
|
||||
/* 默认值 */
|
||||
const props = withDefaults(defineProps<SearchFormProps>(), {
|
||||
searchParam: () => ({})
|
||||
});
|
||||
/* 展开、合并 */
|
||||
const expand = ref(false);
|
||||
/* 表单实例 */
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
watch(
|
||||
() => expand.value,
|
||||
() => {
|
||||
props.getScrollY();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
|
@ -0,0 +1,66 @@
|
|||
.select-filter {
|
||||
width: 100%;
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px dashed var(--ant-primary-2);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
&-title {
|
||||
margin-top: -2px;
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: @primary-text-color;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-notData {
|
||||
margin: 18px 0;
|
||||
font-size: 14px;
|
||||
color: @primary-text-color;
|
||||
}
|
||||
&-list {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
margin: 13px 0;
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 15px;
|
||||
margin-right: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--ant-primary-color);
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
background: var(--ant-primary-1);
|
||||
border: 1px solid var(--ant-primary-3);
|
||||
border-radius: 32px;
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background: var(--ant-primary-color);
|
||||
border-color: var(--ant-primary-color);
|
||||
transition: 0.1s;
|
||||
}
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
background: var(--ant-primary-color);
|
||||
border-color: var(--ant-primary-color);
|
||||
}
|
||||
.el-icon {
|
||||
margin-right: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.select-filter-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="select-filter">
|
||||
<div v-for="item in data" :key="item.key" class="select-filter-item">
|
||||
<div class="select-filter-title">
|
||||
<span>{{ item.title }} :</span>
|
||||
</div>
|
||||
<span v-if="!item.options.length" class="select-filter-notData">暂无数据 ~</span>
|
||||
<ul class="select-filter-list">
|
||||
<li
|
||||
v-for="option in item.options"
|
||||
:key="option.value"
|
||||
:class="{
|
||||
active:
|
||||
option.value === selected[item.key] ||
|
||||
(Array.isArray(selected[item.key]) && selected[item.key].includes(option.value))
|
||||
}"
|
||||
@click="select(item, option)"
|
||||
>
|
||||
<slot :row="option">
|
||||
<component v-if="option.icon" :is="option.icon" class="select-filter-icon" />
|
||||
<span>{{ option.label }}</span>
|
||||
</slot>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="selectFilter">
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
interface OptionsProps {
|
||||
value: string | number;
|
||||
label: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface SelectDataProps {
|
||||
title: string; // 列表标题
|
||||
key: string; // 当前筛选项 key 值
|
||||
multiple?: boolean; // 是否为多选
|
||||
options: OptionsProps[]; // 筛选数据
|
||||
}
|
||||
|
||||
interface SelectFilterProps {
|
||||
data?: SelectDataProps[]; // 选择的列表数据
|
||||
defaultValues?: { [key: string]: any }; // 默认值
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SelectFilterProps>(), {
|
||||
data: () => [],
|
||||
defaultValues: () => ({})
|
||||
});
|
||||
|
||||
// 重新接收默认值
|
||||
const selected = ref<{ [key: string]: any }>({});
|
||||
watch(
|
||||
() => props.defaultValues,
|
||||
() => {
|
||||
props.data.forEach(item => {
|
||||
if (item.multiple) selected.value[item.key] = props.defaultValues[item.key] ?? [""];
|
||||
else selected.value[item.key] = props.defaultValues[item.key] ?? "";
|
||||
});
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
interface FilterEmits {
|
||||
(e: "change", value: any): void;
|
||||
}
|
||||
const emit = defineEmits<FilterEmits>();
|
||||
|
||||
/**
|
||||
* @description 选择筛选项
|
||||
* @param {Object} item 选中的哪项列表
|
||||
* @param {Object} option 选中的值
|
||||
* @return void
|
||||
* */
|
||||
const select = (item: SelectDataProps, option: OptionsProps) => {
|
||||
if (!item.multiple) {
|
||||
// * 单选
|
||||
if (selected.value[item.key] !== option.value) selected.value[item.key] = option.value;
|
||||
} else {
|
||||
// * 多选
|
||||
// 如果选中的是第一个值,则直接设置
|
||||
if (item.options[0].value === option.value) selected.value[item.key] = [option.value];
|
||||
// 如果选择的值已经选中了,则删除选中的值
|
||||
if (selected.value[item.key].includes(option.value)) {
|
||||
let currentIndex = selected.value[item.key].findIndex((s: any) => s === option.value);
|
||||
selected.value[item.key].splice(currentIndex, 1);
|
||||
// 当全部删光时,把第第一个值选中
|
||||
if (selected.value[item.key].length == 0) selected.value[item.key] = [item.options[0].value];
|
||||
} else {
|
||||
// 未选中点击值的时候,追加选中值
|
||||
selected.value[item.key].push(option.value);
|
||||
// 单选中全部并且点击到了未选中的值,把第一个值删除掉
|
||||
if (selected.value[item.key].includes(item.options[0].value)) selected.value[item.key].splice(0, 1);
|
||||
}
|
||||
}
|
||||
emit("change", selected.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import url("./index.less");
|
||||
</style>
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<svg :style="iconStyle" aria-hidden="true">
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="SvgIcon">
|
||||
import { CSSProperties, computed } from "vue";
|
||||
|
||||
interface SvgProps {
|
||||
name: string; // 图标的名称 --> 必传
|
||||
prefix?: string; // 图标的前缀 --> 非必传(默认为"icon")
|
||||
iconStyle?: CSSProperties; // 图标的样式 --> 非必传
|
||||
}
|
||||
|
||||
// 接受父组件参数并设置默认值
|
||||
const props = withDefaults(defineProps<SvgProps>(), {
|
||||
prefix: "icon",
|
||||
iconStyle: () => ({ width: "16px", height: "16px" })
|
||||
});
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
|
||||
</script>
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<a-switch v-model:checked="checked1" class="switch-dark" v-bind="$attrs">
|
||||
<template #checkedChildren><SvgIcon name="sunny" :iconStyle="{ width: '12px', height: '12px' }" /></template>
|
||||
<template #unCheckedChildren><SvgIcon name="moon" :iconStyle="{ width: '12px', height: '12px' }" /></template>
|
||||
</a-switch>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="SwitchDark">
|
||||
import { ref } from "vue";
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
|
||||
const checked1 = ref(false);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.switch-dark {
|
||||
:global(.ant-switch-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<a-dropdown :trigger="[trigger]" @visibleChange="PopoverShow">
|
||||
<div class="table-filter-title-container">
|
||||
<p class="table-filter-title">
|
||||
<span class="mr-12">{{ title }}</span>
|
||||
<component
|
||||
:is="iconUpOrDowm ? UpOutlined : DownOutlined"
|
||||
class="title-icon"
|
||||
:style="{ color: iconUpOrDowm ? primary : '' }"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu @click="onClick" v-bind="$attrs">
|
||||
<a-menu-item v-for="item in filterOptions" :key="item.value">
|
||||
<span>{{ item.label }}</span>
|
||||
<span class="select-icon" v-show="checkedValue.includes(item.value)"><check-outlined /></span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="TableFilter">
|
||||
import { ref, toRefs, computed, watch } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { DownOutlined, UpOutlined } from "@ant-design/icons-vue";
|
||||
import type { MenuProps } from "ant-design-vue";
|
||||
import { isArray } from "@/utils/is";
|
||||
import { useGlobalStore } from "@/stores/modules/global";
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const { primary } = storeToRefs(globalStore);
|
||||
/* 选项 */
|
||||
type filterOptions = {
|
||||
label: string;
|
||||
value: string | number | null;
|
||||
};
|
||||
/* Props */
|
||||
type EventType = "click" | "hover" | "contextmenu";
|
||||
interface TableFilterProps {
|
||||
title: string; // 表头标题 ---> 必传
|
||||
trigger?: EventType; // 触发下拉的行为 ---> 非必传(默认为click)
|
||||
options: Array<filterOptions>; // 下拉框值 ---> 必传
|
||||
isMultiple?: boolean; // 是否多选 ---> 非必传(默认为false)
|
||||
titleChange?: boolean; // 是否改变标题 ---> 非必传(默认为false)
|
||||
emptyValue?: string | number | null; // 全部值 ---> 非必传(默认为null)
|
||||
}
|
||||
const props = withDefaults(defineProps<TableFilterProps>(), {
|
||||
title: "Table Filter",
|
||||
trigger: "click",
|
||||
isMultiple: false,
|
||||
titleChange: false,
|
||||
emptyValue: null
|
||||
});
|
||||
const emits = defineEmits<{
|
||||
(e: "update:filterValue", value: any): void;
|
||||
}>();
|
||||
const { options, isMultiple, emptyValue } = toRefs(props);
|
||||
const iconUpOrDowm = ref(false);
|
||||
const title = ref(props.title);
|
||||
/* 选中值 */
|
||||
const checkedValue = ref<Array<string | number | null>>([emptyValue.value]);
|
||||
/* 选项 0ptions */
|
||||
const filterOptions = computed(() => {
|
||||
const data: Array<filterOptions> = [{ label: "全部", value: emptyValue.value }];
|
||||
if (isArray(options.value)) {
|
||||
return data.concat(options.value);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
/* 点击选项 */
|
||||
const onClick: MenuProps["onClick"] = ({ key }) => {
|
||||
iconUpOrDowm.value = false;
|
||||
let value: string | number = key;
|
||||
if (isMultiple.value && value !== emptyValue.value) {
|
||||
let newCheckedValue: Array<string | number | null> = checkedValue.value.filter(item => item !== emptyValue.value);
|
||||
if (checkedValue.value.includes(value)) {
|
||||
newCheckedValue = checkedValue.value.filter(item => item !== value);
|
||||
if (!newCheckedValue.length) {
|
||||
checkedValue.value = [emptyValue.value];
|
||||
} else {
|
||||
checkedValue.value = [...newCheckedValue];
|
||||
}
|
||||
} else {
|
||||
checkedValue.value = [...newCheckedValue, value];
|
||||
}
|
||||
} else {
|
||||
checkedValue.value = [value];
|
||||
}
|
||||
// 判断是否多选
|
||||
if (isMultiple.value) {
|
||||
emits("update:filterValue", [...checkedValue.value]);
|
||||
} else {
|
||||
emits("update:filterValue", checkedValue.value[0]);
|
||||
}
|
||||
};
|
||||
/* 点击标题 */
|
||||
const PopoverShow = (value: boolean) => {
|
||||
iconUpOrDowm.value = value;
|
||||
};
|
||||
/* 改变标题 */
|
||||
watch(
|
||||
() => checkedValue.value,
|
||||
() => {
|
||||
if (!isMultiple.value) {
|
||||
const data = options.value.find(option => option.value === checkedValue.value[0]);
|
||||
title.value = data?.label || props.title;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.table-filter-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.table-filter-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.title-icon {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.select-icon {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
bottom: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--ant-primary-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<a-popover v-model:visible="visible" placement="bottom" trigger="click">
|
||||
<template #content>
|
||||
<!-- 图片 -->
|
||||
<a-image-preview-group v-if="props.type === 'image'">
|
||||
<a-image v-for="src in srcList" @click="hide" :key="src" :width="80" :height="80" :src="src" />
|
||||
</a-image-preview-group>
|
||||
<!-- 视频 -->
|
||||
<template v-if="props.type === 'video'">
|
||||
<video v-for="src in srcList" :key="src" :src="src" controls width="260"></video>
|
||||
</template>
|
||||
</template>
|
||||
<a-tag :color="primary" style="cursor: pointer">查看{{ srcList.length }}个文件</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="TablePreview">
|
||||
import { ref, computed, watchEffect } from "vue";
|
||||
import { useGlobalStore } from "@/stores/modules/global";
|
||||
import { isArray, isString } from "@/utils/is";
|
||||
|
||||
/* 接口 */
|
||||
export interface TablePreviewProps {
|
||||
src: string[] | string; // 图片路径 --> 必传
|
||||
type?: "image" | "video"; // 展示类型 --> 不必传,默认image
|
||||
}
|
||||
/* 主题颜色 */
|
||||
const globalStore = useGlobalStore();
|
||||
const primary = computed(() => globalStore.primary);
|
||||
/* Props */
|
||||
const props = withDefaults(defineProps<TablePreviewProps>(), {
|
||||
type: "image"
|
||||
});
|
||||
/* 气泡卡片状态 */
|
||||
const visible = ref<boolean>(false);
|
||||
/* 隐藏 */
|
||||
const hide = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
/* srcList */
|
||||
const srcList = ref<string[]>([]);
|
||||
/* 监听 */
|
||||
watchEffect(() => {
|
||||
if (isArray(props.src)) srcList.value = props.src;
|
||||
if (isString(props.src)) srcList.value = [props.src];
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,59 @@
|
|||
<!--
|
||||
placement 气泡框位置,可选 top left right bottom topLeft topRight bottomLeft bottomRight leftTop leftBottom rightTop rightBottom
|
||||
overlayClassName 卡片类名
|
||||
trigger 触发行为,可选 hover/focus/click/contextmenu
|
||||
-->
|
||||
<template>
|
||||
<a-tooltip v-if="isShowTooltip" placement="top" trigger="hover" overlayClassName="table-tooltip" v-model:visible="isShow">
|
||||
<template #title>
|
||||
<span>{{ content }}</span>
|
||||
</template>
|
||||
<div class="content" @mouseleave="mouseleave">
|
||||
<span ref="contentRef">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<div v-else class="content" @mouseenter="mouseenter">
|
||||
<span ref="contentRef">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="tableTooltip">
|
||||
import { ref } from "vue";
|
||||
/* Props接口 */
|
||||
interface TableTooltipProps {
|
||||
content: string;
|
||||
}
|
||||
/* 接受父组件参数,配置默认值 */
|
||||
withDefaults(defineProps<TableTooltipProps>(), {
|
||||
content: ""
|
||||
});
|
||||
|
||||
const isShow = ref(true);
|
||||
/* 内容实例 */
|
||||
const contentRef = ref();
|
||||
/* tooltip显隐 */
|
||||
const isShowTooltip = ref(false);
|
||||
/* 鼠标移出 */
|
||||
const mouseleave = () => {
|
||||
// isShowTooltip.value = false;
|
||||
};
|
||||
/* 鼠标移入 */
|
||||
const mouseenter = () => {
|
||||
// 关键代码逻辑
|
||||
const tooltipWidth = contentRef.value.parentNode.offsetWidth;
|
||||
const contentWidth = contentRef.value.offsetWidth;
|
||||
isShowTooltip.value = tooltipWidth > contentWidth ? false : true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div class="color-picker">
|
||||
<a-tooltip placement="top" v-for="(color, index) in colors" :key="index" :title="colorNames[index]">
|
||||
<div class="color-option" :style="{ backgroundColor: color }" @click="selectColor(index)">
|
||||
<SvgIcon v-if="selectedColorIndex === index" class="checkIcon" name="duihao" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ColorPicker">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useGlobalStore } from "@/stores/modules/global";
|
||||
import { ConfigProvider } from "ant-design-vue";
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
|
||||
interface ColorPickerProps {
|
||||
colors: Array<string>; // 主题色 --> 必传
|
||||
colorNames: Array<string>; // 主题色提示语 --> 必传
|
||||
defaultColor: string; // 默认颜色 --> 必传
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ColorPickerProps>(), {});
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const selectedColorIndex = ref(0);
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
let index = props.colors.indexOf(props.defaultColor);
|
||||
if (index !== -1) selectedColorIndex.value = index;
|
||||
});
|
||||
|
||||
const selectColor = (index: number) => {
|
||||
selectedColorIndex.value = index;
|
||||
const selectedColor = props.colors[selectedColorIndex.value];
|
||||
globalStore.setGlobalState("primary", selectedColor);
|
||||
ConfigProvider.config({
|
||||
theme: {
|
||||
primaryColor: selectedColor
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.color-picker {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.color-option {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkIcon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,10 @@
|
|||
import type { App } from "vue";
|
||||
import SvgIcon from "./SvgIcon/index.vue";
|
||||
|
||||
const compList = [SvgIcon];
|
||||
|
||||
export function registerGlobComp(app: App) {
|
||||
compList.forEach(comp => {
|
||||
app.component(comp.name || comp.displayName, comp);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// ? 全局不动配置项 只做导出不做修改
|
||||
|
||||
// * 首页地址(默认)
|
||||
export const HOME_URL: string = "/home/index";
|
||||
|
||||
// * 登录页地址(默认)
|
||||
export const LOGIN_URL: string = "/login";
|
||||
|
||||
// * 默认主题颜色
|
||||
export const DEFAULT_PRIMARY: string = "#2449ff";
|
||||
|
||||
// * 路由白名单地址(必须是本地存在的路由 staticRouter.ts)
|
||||
export const ROUTER_WHITE_LIST: string[] = ["/500"];
|
|
@ -0,0 +1,13 @@
|
|||
// ? 全局不动配置项 只做导出不做修改
|
||||
|
||||
// * 首页地址(默认)
|
||||
export const HOME_URL: string = "/home/index";
|
||||
|
||||
// * 登录页地址(默认)
|
||||
export const LOGIN_URL: string = "/login";
|
||||
|
||||
// * 默认主题颜色
|
||||
export const DEFAULT_PRIMARY: string = "#2449ff";
|
||||
|
||||
// * 路由白名单地址(必须是本地存在的路由 staticRouter.ts)
|
||||
export const ROUTER_WHITE_LIST: string[] = ["/500"];
|
|
@ -0,0 +1,12 @@
|
|||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
|
||||
NProgress.configure({
|
||||
easing: "ease", // 动画方式
|
||||
speed: 500, // 递增进度条的速度
|
||||
showSpinner: true, // 是否显示加载ico
|
||||
trickleSpeed: 200, // 自动递增间隔
|
||||
minimum: 0.3 // 初始化时的最小百分比
|
||||
});
|
||||
|
||||
export default NProgress;
|
|
@ -0,0 +1,19 @@
|
|||
import { PersistedStateOptions } from "pinia-plugin-persistedstate";
|
||||
|
||||
/**
|
||||
* @description pinia持久化参数配置
|
||||
* @param {String} key 存储到持久化的 name
|
||||
* @param {Array} paths 需要持久化的 state name
|
||||
* @return persist
|
||||
* */
|
||||
const piniaPersistConfig = (key: string, paths?: string[]) => {
|
||||
const persist: PersistedStateOptions = {
|
||||
key,
|
||||
storage: localStorage,
|
||||
// storage: sessionStorage,
|
||||
paths
|
||||
};
|
||||
return persist;
|
||||
};
|
||||
|
||||
export default piniaPersistConfig;
|
|
@ -0,0 +1,28 @@
|
|||
import { App, Directive } from "vue";
|
||||
import auth from "./modules/auth";
|
||||
import copy from "./modules/copy";
|
||||
import draggable from "./modules/draggable";
|
||||
import debounce from "./modules/debounce";
|
||||
import throttle from "./modules/throttle";
|
||||
import longpress from "./modules/longpress";
|
||||
import waterMarker from "./modules/waterMarker";
|
||||
|
||||
const directivesList: { [key: string]: Directive } = {
|
||||
auth,
|
||||
copy,
|
||||
draggable,
|
||||
debounce,
|
||||
throttle,
|
||||
longpress,
|
||||
waterMarker
|
||||
};
|
||||
|
||||
const directives = {
|
||||
install: function (app: App<Element>) {
|
||||
Object.keys(directivesList).forEach(key => {
|
||||
app.directive(key, directivesList[key]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default directives;
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* description v-auth 按钮权限指令
|
||||
*/
|
||||
import { useAuthStore } from "@/stores/modules/auth";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
const auth: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const { value } = binding;
|
||||
const authStore = useAuthStore();
|
||||
const currentPageRoles = authStore.authButtonListGet[authStore.routeName] ?? [];
|
||||
// 判断v-auth是否绑定多个权限
|
||||
if (value instanceof Array && value.length) {
|
||||
const hasPermission = value.every(item => currentPageRoles.includes(item));
|
||||
if (!hasPermission) el.remove();
|
||||
} else {
|
||||
if (!currentPageRoles.includes(value)) el.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default auth;
|