This commit is contained in:
Bunny 2024-05-11 23:05:59 +08:00
commit c2e3bbe97b
292 changed files with 50920 additions and 0 deletions

11
.env Normal file
View File

@ -0,0 +1,11 @@
# title
VITE_GLOB_APP_TITLE = 'GuYue-Admin'
# 本地运行端口号
VITE_PORT = 8088
# open 运行 npm run dev 时自动打开浏览器
VITE_OPEN = false
# 打包后是否生成包分析文件
VITE_REPORT = true

19
.env.development Normal file
View File

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

18
.env.production Normal file
View File

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

18
.env.test Normal file
View File

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

20
.eslintignore Normal file
View File

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

68
.eslintrc.js Normal file
View File

@ -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" // 要求组件名称始终为 “-” 链接的单词
}
};

28
.gitignore vendored Normal file
View File

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

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint:lint-staged

9
.prettierignore Normal file
View File

@ -0,0 +1,9 @@
/dist/*
/html/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*

39
.prettierrc.js Normal file
View File

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

15
.stylelintignore Normal file
View File

@ -0,0 +1,15 @@
# .stylelintignore
# 旧的不需打包的样式库
*.min.css
# 其他类型文件
*.js
*.jpg
*.woff
# 测试和打包目录
/test/
/dist/*
/public/*
public/*
/node_modules/

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"antfu.unocss",
"Vue.volar",
"Vue.vscode-typescript-vue-plugin"
]
}

21
LICENSE Normal file
View File

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

60
build/info.ts Normal file
View File

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

47
build/plugins.ts Normal file
View File

@ -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 // 动态主题需配置
})
]
})
];
};

30
build/proxy.ts Normal file
View File

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

110
build/utils.ts Normal file
View File

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

73
commitlint.config.js Normal file
View File

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

100
components.d.ts vendored Normal file
View File

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

104
index.html Normal file
View File

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

8
lint-staged.config.js Normal file
View File

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

141
package.json Normal file
View File

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

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

55
src/App.vue Normal file
View File

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

View File

@ -0,0 +1,2 @@
// * 后端微服务端口名
export const PORT = "/guyue";

View File

@ -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("请求失败!");
}
};

117
src/api/index.ts Normal file
View File

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

View File

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

27
src/api/modules/login.ts Normal file
View File

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

11
src/api/modules/role.ts Normal file
View File

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

36
src/api/modules/user.ts Normal file
View File

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

BIN
src/assets/fonts/DIN.otf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
src/assets/images/403.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/assets/images/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
src/assets/images/500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
src/assets/images/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
src/assets/images/msg01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
src/assets/images/msg02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
src/assets/images/msg03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
src/assets/images/msg04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
src/assets/images/msg05.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

View File

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

View File

@ -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);
// keycheckedList
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(() => {
// columnschecked
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

13
src/config/config.ts Normal file
View File

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

13
src/config/index.ts Normal file
View File

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

12
src/config/nprogress.ts Normal file
View File

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

View File

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

28
src/directives/index.ts Normal file
View File

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

View File

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

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