1
15
.editorconfig
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# @see: http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*] # 表示所有文件适用
|
||||||
|
charset = utf-8 # 设置文件字符集为 utf-8
|
||||||
|
end_of_line = lf # 控制换行类型(lf | cr | crlf)
|
||||||
|
insert_final_newline = true # 始终在文件末尾插入一个新行
|
||||||
|
indent_style = space # 缩进风格[tab | space]
|
||||||
|
indent_size = 2 # 缩进大小
|
||||||
|
max_line_length = 130 # 最大行长度
|
||||||
|
|
||||||
|
[*.md] # 表示仅对 md 文件适用以下规则
|
||||||
|
max_line_length = off # 关闭最大行长度限制
|
||||||
|
trim_trailing_whitespace = false # 关闭末尾空格修剪
|
13
.env.development
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||||
|
VITE_ENV = 'development'
|
||||||
|
VITE_WEB_TITLE = '固始电子报'
|
||||||
|
VITE_WEB_EN_TITLE = 'GUSHI-NEWSPAPER'
|
||||||
|
VITE_LOGIN_TITLE = '固始电子报 管理平台'
|
||||||
|
VITE_LOGIN_EN_TITLE = 'GuShi Platform'
|
||||||
|
VITE_WEB_BASE_API = '/api'
|
||||||
|
# 本地Mock地址
|
||||||
|
VITE_SERVER = 'http://csdzb.hschool.com.cn/'
|
||||||
|
# 路由模式[哈希模式 AND WEB模式 [hash | history, 这两个模式是固定死的,不能乱改值]
|
||||||
|
VITE_ROUTER_MODE = hash
|
||||||
|
# 是否使用全部去除console和debugger
|
||||||
|
VITE_DROP_CONSOLE = false
|
13
.env.production
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||||
|
VITE_ENV = 'production'
|
||||||
|
VITE_WEB_TITLE = '固始电子报'
|
||||||
|
VITE_WEB_EN_TITLE = 'GUSHI-NEWSPAPER'
|
||||||
|
VITE_LOGIN_TITLE = '固始电子报 管理平台'
|
||||||
|
VITE_LOGIN_EN_TITLE = 'GuShi Platform'
|
||||||
|
VITE_WEB_BASE_API = '/api'
|
||||||
|
# 后端接口地址
|
||||||
|
VITE_SERVER = 'http://csdzb.hschool.com.cn/'
|
||||||
|
# 路由模式[哈希模式 AND WEB模式 [hash | history, 这两个模式是固定死的,不能乱改值]
|
||||||
|
VITE_ROUTER_MODE = hash
|
||||||
|
# 是否使用全部去除console和debugger
|
||||||
|
VITE_DROP_CONSOLE = true
|
15
.eslintignore
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
*.sh
|
||||||
|
node_modules
|
||||||
|
*.md
|
||||||
|
*.woff
|
||||||
|
*.ttf
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
dist
|
||||||
|
/public
|
||||||
|
/docs
|
||||||
|
.husky
|
||||||
|
.local
|
||||||
|
/bin
|
||||||
|
/src/mock/*
|
||||||
|
stats.html
|
61
.eslintrc.cjs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// @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", "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 }], // 不允许多个空行
|
||||||
|
"prefer-const": "off", // 使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const
|
||||||
|
"no-use-before-define": "off", // 禁止在 函数/类/变量 定义之前使用它们
|
||||||
|
|
||||||
|
// typeScript (https://typescript-eslint.io/rules)
|
||||||
|
"@typescript-eslint/no-unused-vars": "error", // 禁止定义未使用的变量
|
||||||
|
"@typescript-eslint/no-empty-function": "error", // 禁止空函数
|
||||||
|
"@typescript-eslint/prefer-ts-expect-error": "error", // 禁止使用 @ts-ignore
|
||||||
|
"@typescript-eslint/ban-ts-comment": "error", // 禁止 @ts-<directive> 使用注释或要求在指令后进行描述
|
||||||
|
"@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/no-var-requires": "off", // 允许使用 require() 函数导入模块
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off", // 不允许使用后缀运算符的非空断言(!)
|
||||||
|
|
||||||
|
// vue (https://eslint.vuejs.org/rules)
|
||||||
|
"vue/script-setup-uses-vars": "error", // 防止<script setup>使用的变量<template>被标记为未使用,此规则仅在启用该 no-unused-vars 规则时有效
|
||||||
|
"vue/v-slot-style": "error", // 强制执行 v-slot 指令样式
|
||||||
|
"vue/no-mutating-props": "error", // 不允许改变组件 prop
|
||||||
|
"vue/custom-event-name-casing": "error", // 为自定义事件名称强制使用特定大小写
|
||||||
|
"vue/html-closing-bracket-newline": "error", // 在标签的右括号之前要求或禁止换行
|
||||||
|
"vue/attribute-hyphenation": "error", // 对模板中的自定义组件强制执行属性命名样式:my-prop="prop"
|
||||||
|
"vue/attributes-order": "off", // vue api使用顺序,强制执行属性顺序
|
||||||
|
"vue/no-v-html": "off", // 禁止使用 v-html
|
||||||
|
"vue/require-default-prop": "off", // 此规则要求为每个 prop 为必填时,必须提供默认值
|
||||||
|
"vue/multi-word-component-names": "off", // 要求组件名称始终为 “-” 链接的单词
|
||||||
|
"vue/no-setup-props-destructure": "off" // 禁止解构 props 传递给 setup
|
||||||
|
}
|
||||||
|
};
|
14
.eslintrc.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:vue/vue3-essential"],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint", "vue"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
41
.prettierrc.cjs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// @see: https://www.prettier.cn
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// 指定最大换行长度
|
||||||
|
printWidth: 130,
|
||||||
|
// 缩进制表符宽度 | 空格数
|
||||||
|
tabWidth: 2,
|
||||||
|
// 使用制表符而不是空格缩进行 (true:制表符,false:空格)
|
||||||
|
useTabs: false,
|
||||||
|
// 结尾不用分号 (true:有,false:没有)
|
||||||
|
semi: true,
|
||||||
|
// 使用单引号 (true:单引号,false:双引号)
|
||||||
|
singleQuote: false,
|
||||||
|
// 在对象字面量中决定是否将属性名用引号括起来 可选值 "<as-needed|consistent|preserve>"
|
||||||
|
quoteProps: "as-needed",
|
||||||
|
// 在JSX中使用单引号而不是双引号 (true:单引号,false:双引号)
|
||||||
|
jsxSingleQuote: false,
|
||||||
|
// 多行时尽可能打印尾随逗号 可选值"<none|es5|all>"
|
||||||
|
trailingComma: "none",
|
||||||
|
// 在对象,数组括号与文字之间加空格 "{ foo: bar }" (true:有,false:没有)
|
||||||
|
bracketSpacing: true,
|
||||||
|
// 将 > 多行元素放在最后一行的末尾,而不是单独放在下一行 (true:放末尾,false:单独一行)
|
||||||
|
bracketSameLine: false,
|
||||||
|
// (x) => {} 箭头函数参数只有一个时是否要有小括号 (avoid:省略括号,always:不省略括号)
|
||||||
|
arrowParens: "avoid",
|
||||||
|
// 指定要使用的解析器,不需要写文件开头的 @prettier
|
||||||
|
requirePragma: false,
|
||||||
|
// 可以在文件顶部插入一个特殊标记,指定该文件已使用 Prettier 格式化
|
||||||
|
insertPragma: false,
|
||||||
|
// 用于控制文本是否应该被换行以及如何进行换行
|
||||||
|
proseWrap: "preserve",
|
||||||
|
// 在html中空格是否是敏感的 "css" - 遵守 CSS 显示属性的默认值, "strict" - 空格被认为是敏感的 ,"ignore" - 空格被认为是不敏感的
|
||||||
|
htmlWhitespaceSensitivity: "css",
|
||||||
|
// 控制在 Vue 单文件组件中 <script> 和 <style> 标签内的代码缩进方式
|
||||||
|
vueIndentScriptAndStyle: false,
|
||||||
|
// 换行符使用 lf 结尾是 可选值 "<auto|lf|crlf|cr>"
|
||||||
|
endOfLine: "auto",
|
||||||
|
// 这两个选项可用于格式化以给定字符偏移量[分别包括和不包括]开始和结束的代码 (rangeStart:开始,rangeEnd:结束)
|
||||||
|
rangeStart: 0,
|
||||||
|
rangeEnd: Infinity
|
||||||
|
};
|
4
.stylelintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/node_modules/*
|
||||||
|
/dist/*
|
||||||
|
/html/*
|
||||||
|
/public/*
|
40
.stylelintrc.cjs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// @see: https://stylelint.io
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
// 继承某些已有的规则
|
||||||
|
extends: [
|
||||||
|
"stylelint-config-standard", // 配置 stylelint 拓展插件
|
||||||
|
"stylelint-config-html/vue", // 配置 vue 中 template 样式格式化
|
||||||
|
"stylelint-config-standard-scss", // 配置 stylelint scss 插件
|
||||||
|
"stylelint-config-recommended-vue/scss", // 配置 vue 中 scss 样式格式化
|
||||||
|
"stylelint-config-recess-order" // 配置 stylelint css 属性书写顺序插件,
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
// 扫描 .vue/html 文件中的 <style> 标签内的样式
|
||||||
|
{
|
||||||
|
files: ["**/*.{vue,html}"],
|
||||||
|
customSyntax: "postcss-html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"function-url-quotes": "always", // URL 的引号 "always(必须加上引号)"|"never(没有引号)"
|
||||||
|
"color-hex-length": "long", // 指定 16 进制颜色的简写或扩写 "short(16进制简写)"|"long(16进制扩写)"
|
||||||
|
"rule-empty-line-before": "never", // 要求或禁止在规则之前的空行 "always(规则之前必须始终有一个空行)"|"never(规则前绝不能有空行)"|"always-multi-line(多行规则之前必须始终有一个空行)"|"never-multi-line(多行规则之前绝不能有空行)"
|
||||||
|
"font-family-no-missing-generic-family-keyword": null, // 禁止在字体族名称列表中缺少通用字体族关键字
|
||||||
|
"scss/at-import-partial-extension": null, // 解决不能使用 @import 引入 scss 文件
|
||||||
|
"property-no-unknown": null, // 禁止未知的属性
|
||||||
|
"no-empty-source": null, // 禁止空源码
|
||||||
|
"selector-class-pattern": null, // 强制选择器类名的格式
|
||||||
|
"value-no-vendor-prefix": null, // 关闭 vendor-prefix (为了解决多行省略 -webkit-box)
|
||||||
|
"no-descending-specificity": null, // 不允许较低特异性的选择器出现在覆盖较高特异性的选择器
|
||||||
|
"value-keyword-case": null, // 解决在 scss 中使用 v-bind 大写单词报错
|
||||||
|
"selector-pseudo-class-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
ignorePseudoClasses: ["global", "v-deep", "deep"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
ignoreFiles: ["**/*.js", "**/*.jsx", "**/*.tsx", "**/*.ts"]
|
||||||
|
};
|
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
174
README.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="https://pic4.zhimg.com/v2-702a23ebb518199355099df77a3cfe07_b.webp" width="200" height="200" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">KOI-UI🌻</h1>
|
||||||
|
|
||||||
|
<p align="center">一款<b>开箱即用</b>的 Vue3 中后台管理系统框架[纯前端]</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<span> [ </span>
|
||||||
|
纯前端演示[开源]
|
||||||
|
<a href="http://39.107.143.109/login" target="_blank">点击这里</a>
|
||||||
|
<span> ] </span>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<span> [ </span>
|
||||||
|
前后端演示[129元]
|
||||||
|
<a href="http://39.107.143.109/login" target="_blank">点击这里</a>
|
||||||
|
<span> ] </span>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<span> [ </span>
|
||||||
|
博客演示[269元]
|
||||||
|
<a href="http://39.107.143.109:8188/home/index" target="_blank">点击这里</a>
|
||||||
|
<span> ] </span>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
前后端版本模版[129元]:SpringBoot3、JDK17、Sa-Token等技术;
|
||||||
|
博客版本[269元]:博客 + 管理平台;
|
||||||
|
|
||||||
|
## 1、简介
|
||||||
|
|
||||||
|
KOI-UI🌻 是一款开源企业级别的中后台管理平台模板,基于 Vue3、Vite、TypeScript、Pinia、Pinia持久化插件、Unocss 和 ElementPlus等前端最新技术栈。相较于其他比较流行的后台管理模板,更加简洁、快捷和容易理解,对萌新小白十分友好。此项目学习成本非常低含有相关代码注释以及大量的案例,非常适合企业级项目、中小型项目、个人项目以及毕业设计。后续将用户、角色、菜单、字典管理和通用管理平台页面依次编写,做到直接对接后端接口即可,使之快速开发。常见的组件有小伙伴提供可提issus会依次封装进去展示。
|
||||||
|
|
||||||
|
## 2、特点
|
||||||
|
|
||||||
|
- 🎯 使用 Element Plus + Vite + Vue3 + TypeScript + Uncoss + Pinia 等主流技术。
|
||||||
|
- 🍊 多种布局和丰富的主题适配移动端、IPad和PC端。
|
||||||
|
- 🐼 内置权限管理页面,进行二次开发可直接对接后端接口即可。
|
||||||
|
- 🌸 集成登陆、注销及权限验证。
|
||||||
|
- 🎃 封装按钮和Input框的防抖、限流和背景水印以及左侧无限递归菜单。
|
||||||
|
- 🍀 集成 `pinia`,vuex 的替代方案,轻量、简单、易用,并且配置pinia持久化插件。
|
||||||
|
- 😍 二次封装Dialog对话框、Drawer抽屉、Notification通知、Message消息提示和Popconfirm确认框,操作更加方便快捷。
|
||||||
|
- 🍓 二次封装axios,方便接口更好的统一管理。
|
||||||
|
- 🌍 集成Echarts图表。
|
||||||
|
- 🌈 集成 `unocss`,antfu 开源的原子 css 解决方案,非常轻量。
|
||||||
|
- 🐟 集成多环境配置,dev、测试、生产环境。
|
||||||
|
- 🌼 集成 `eslint + prettier`,代码约束和格式化统一。
|
||||||
|
- 🌻 集成 `stylelint`,代码约束scss、less、css规范化。
|
||||||
|
- 👻 集成 `mock` 接口服务。
|
||||||
|
- 🏡 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon。
|
||||||
|
|
||||||
|
## 3、无需递归路由
|
||||||
|
|
||||||
|
众所周知,在实现左侧无限递归路由的时候,后端需要进行树形递归,前端再二次进行递归处理,这样增加了前后端开发难度,如何解决如下?
|
||||||
|
|
||||||
|
此项目,前端使用扁平化路由,实现三级及以上页面依旧存在路由缓存效果。
|
||||||
|
|
||||||
|
后端提供接口菜单不需要再进行递归,直接根据提供的json格式,进行提供接口[数据如下],简化后端难度,实现无限递归路由。
|
||||||
|
|
||||||
|
```properties
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"msg": "SUCCESS",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"menuId": 1,
|
||||||
|
"menuName": "系统管理",
|
||||||
|
"parentId": 0,
|
||||||
|
"menuType": "1",
|
||||||
|
"path": "/system",
|
||||||
|
"name": "systemPage",
|
||||||
|
"component": "",
|
||||||
|
"icon": "Tools",
|
||||||
|
"isHide": "1",
|
||||||
|
"isLink": "",
|
||||||
|
"isKeepAlive": "0",
|
||||||
|
"isFull": "1",
|
||||||
|
"isAffix": "1",
|
||||||
|
"redirect": "/system/user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuId": 11,
|
||||||
|
"menuName": "用户管理",
|
||||||
|
"parentId": 1,
|
||||||
|
"menuType": "2",
|
||||||
|
"path": "/system/user",
|
||||||
|
"name": "userPage",
|
||||||
|
"component": "system/user/index",
|
||||||
|
"icon": "UserFilled",
|
||||||
|
"isHide": "1",
|
||||||
|
"isLink": "",
|
||||||
|
"isKeepAlive": "0",
|
||||||
|
"isFull": "1",
|
||||||
|
"isAffix": "1",
|
||||||
|
"redirect": ""
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4、预览
|
||||||
|
|
||||||
|
> 预览截图
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/KOI1.png" /></td>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/KOI2.png" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/KOI3.png" /></td>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/KOI4.png" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/KOI5.png" /></td>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/KOI6.png" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/KOI7.png" /></td>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/KOI8.png" /></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 5、支持
|
||||||
|
|
||||||
|
如果觉得 KOI-ADMIN🌻这个框架不错,或者已经在使用了,希望你可以在 **Github** 或者 **Gitee(码云)** 帮我点个 ⭐ ,这将对我是极大的鼓励。
|
||||||
|
|
||||||
|
## 6、快速开始
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 若未配置pnpm,请先下载并配置镜像
|
||||||
|
npm install pnpm -g --registry=https://registry.npmmirror.com
|
||||||
|
# 下载依赖
|
||||||
|
pnpm install
|
||||||
|
# 启动
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7、打包发布
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 构建环境
|
||||||
|
pnpm build
|
||||||
|
# 生产环境
|
||||||
|
pnpm build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8、源码
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<span> [ </span>
|
||||||
|
Gitee仓库
|
||||||
|
<a href="https://gitee.com/BigCatHome/koi-ui.git" target="_blank">点击这里</a>
|
||||||
|
<span> ] </span>
|
||||||
|
<p>
|
||||||
|
<p align="left">
|
||||||
|
<span> [ </span>
|
||||||
|
GitHub仓库
|
||||||
|
<a href="https://github.com/yuxintao6/koi-ui.git" target="_blank">点击这里</a>
|
||||||
|
<span> ] </span>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
## 9、入群交流
|
||||||
|
|
||||||
|
> 注意:加微信方式时记得添加备注:KOI-UI,支持知识付费。
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/WeChat.png"/></td>
|
||||||
|
<td><img src="https://gitee.com/BigCatHome/koi-photo/raw/master/photos/KOI-ADMIN/WeChatPay.png"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
164
commitlint.config.cjs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// @see: https://cz-git.qbenben.com/zh/guide
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const scopes = fs
|
||||||
|
.readdirSync(path.resolve(__dirname, "src"), { withFileTypes: true })
|
||||||
|
.filter(dirent => dirent.isDirectory())
|
||||||
|
.map(dirent => dirent.name.replace(/s$/, ""));
|
||||||
|
|
||||||
|
/** @type {import('cz-git').UserConfig} */
|
||||||
|
module.exports = {
|
||||||
|
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",
|
||||||
|
"docs",
|
||||||
|
"style",
|
||||||
|
"refactor",
|
||||||
|
"perf",
|
||||||
|
"test",
|
||||||
|
"build",
|
||||||
|
"ci",
|
||||||
|
"chore",
|
||||||
|
"revert",
|
||||||
|
"wip",
|
||||||
|
"workflow",
|
||||||
|
"types",
|
||||||
|
"release"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
messages: {
|
||||||
|
// 英文版
|
||||||
|
// type: "Select the type of change that you're committing:",
|
||||||
|
// scope: "Denote the SCOPE of this change (optional):",
|
||||||
|
// customScope: "Denote the SCOPE of this change:",
|
||||||
|
// subject: "Write a SHORT, IMPERATIVE tense description of the change:\n",
|
||||||
|
// body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n',
|
||||||
|
// breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n',
|
||||||
|
// footerPrefixsSelect: "Select the ISSUES type of changeList by this change (optional):",
|
||||||
|
// customFooterPrefixs: "Input ISSUES prefix:",
|
||||||
|
// footer: "List any ISSUES by this change. E.g.: #31, #34:\n",
|
||||||
|
// confirmCommit: "Are you sure you want to proceed with the commit above?"
|
||||||
|
// 中文版
|
||||||
|
type: "选择你要提交的类型 :",
|
||||||
|
scope: "选择一个提交范围[可选]:",
|
||||||
|
customScope: "请输入自定义的提交范围 :",
|
||||||
|
subject: "填写简短精炼的变更描述 :\n",
|
||||||
|
body: '填写更加详细的变更描述[可选]。使用 "|" 换行 :\n',
|
||||||
|
breaking: '列举非兼容性重大的变更[可选]。使用 "|" 换行 :\n',
|
||||||
|
footerPrefixsSelect: "选择关联issue前缀[可选]:",
|
||||||
|
customFooterPrefixs: "输入自定义issue前缀 :",
|
||||||
|
footer: "列举关联issue [可选] 例如: #31, #I3244 :\n",
|
||||||
|
confirmCommit: "是否提交或修改commit ?"
|
||||||
|
},
|
||||||
|
types: [
|
||||||
|
// 英文版
|
||||||
|
// {
|
||||||
|
// value: "feat",
|
||||||
|
// name: "feat: 👻 A new feature",
|
||||||
|
// emoji: "👻"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "fix",
|
||||||
|
// name: "fix: 🌈 A bug fix",
|
||||||
|
// emoji: "🌈"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "docs",
|
||||||
|
// name: "docs: 🍊 Documentation only changes",
|
||||||
|
// emoji: "🍊"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "style",
|
||||||
|
// name: "style: 🌻 Changes that do not affect the meaning of the code",
|
||||||
|
// emoji: "🌻"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "refactor",
|
||||||
|
// name: "refactor: 🎃 A code change that neither fixes a bug nor adds a feature",
|
||||||
|
// emoji: "🎃"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "perf",
|
||||||
|
// name: "perf: ⚡️ A code change that improves performance",
|
||||||
|
// emoji: "⚡️"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "test",
|
||||||
|
// name: "test: ✅ Adding missing tests or correcting existing tests",
|
||||||
|
// emoji: "✅"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "build",
|
||||||
|
// name: "build: 📦️ Changes that affect the build system or external dependencies",
|
||||||
|
// emoji: "📦️"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "ci",
|
||||||
|
// name: "ci: 🍀 Changes to our CI configuration files and scripts",
|
||||||
|
// emoji: "🍀"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "chore",
|
||||||
|
// name: "chore: 🔨 Other changes that don't modify src or test files",
|
||||||
|
// emoji: "🔨"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "revert",
|
||||||
|
// name: "revert: 🌍 Reverts a previous commit",
|
||||||
|
// emoji: "🌍"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "wip",
|
||||||
|
// name: "wip: 🕔 work in process",
|
||||||
|
// emoji: "🕔"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "workflow",
|
||||||
|
// name: "workflow: 🎯 workflow improvements",
|
||||||
|
// emoji: "🎯"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: "type",
|
||||||
|
// name: "type: 🏡 type definition file changes",
|
||||||
|
// emoji: "🏡"
|
||||||
|
// }
|
||||||
|
// 中文版
|
||||||
|
{ value: "feat", 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: "🔨" },
|
||||||
|
{ value: "wip", name: "开发: 🕔 正在开发中", emoji: "🕔" },
|
||||||
|
{ value: "workflow", name: "工作流: 🎯 工作流程改进", emoji: "🎯" },
|
||||||
|
{ value: "types", name: "类型: 🏡 类型定义文件修改", emoji: "🏡" }
|
||||||
|
],
|
||||||
|
useEmoji: true,
|
||||||
|
scopes: [...scopes],
|
||||||
|
customScopesAlign: "bottom",
|
||||||
|
emptyScopesAlias: "empty",
|
||||||
|
customScopesAlias: "custom",
|
||||||
|
allowBreakingChanges: ["feat", "fix"]
|
||||||
|
}
|
||||||
|
};
|
210
index.html
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/public/vite.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>%VITE_WEB_TITLE%</title>
|
||||||
|
<!-- 加载第一步开始 -->
|
||||||
|
<style>
|
||||||
|
.preloader {
|
||||||
|
background: #5838fc;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-inner {
|
||||||
|
width: 70px;
|
||||||
|
height: 60px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-perspective: 1000;
|
||||||
|
perspective: 1000;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plane {
|
||||||
|
background: #fff;
|
||||||
|
width: 400%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
-webkit-transform: translate3d(0px, 0, 0);
|
||||||
|
transform: translate3d(0px, 0, 0);
|
||||||
|
/*transition: all 0.8s ease; */
|
||||||
|
z-index: 100;
|
||||||
|
-webkit-perspective: 1000;
|
||||||
|
perspective: 1000;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top .plane {
|
||||||
|
z-index: 2000;
|
||||||
|
-webkit-animation: trans1 1.3s ease-in infinite 0s backwards;
|
||||||
|
animation: trans1 1.3s ease-in infinite 0s backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
#middle .plane {
|
||||||
|
-webkit-transform: translate3d(0px, 0, 0);
|
||||||
|
transform: translate3d(0px, 0, 0);
|
||||||
|
background: #fff;
|
||||||
|
-webkit-animation: trans2 1.3s linear infinite 0.3s backwards;
|
||||||
|
animation: trans2 1.3s linear infinite 0.3s backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottom .plane {
|
||||||
|
z-index: 2000;
|
||||||
|
-webkit-animation: trans3 1.3s ease-out infinite 0.7s backwards;
|
||||||
|
animation: trans3 1.3s ease-out infinite 0.7s backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top {
|
||||||
|
width: 53px;
|
||||||
|
height: 20px;
|
||||||
|
left: 20px;
|
||||||
|
-webkit-transform: skew(-15deg, 0);
|
||||||
|
transform: skew(-15deg, 0);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
#middle {
|
||||||
|
width: 33px;
|
||||||
|
height: 20px;
|
||||||
|
left: 20px;
|
||||||
|
top: 15px;
|
||||||
|
-webkit-transform: skew(-15deg, 40deg);
|
||||||
|
transform: skew(-15deg, 40deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottom {
|
||||||
|
width: 53px;
|
||||||
|
height: 20px;
|
||||||
|
top: 30px;
|
||||||
|
-webkit-transform: skew(-15deg, 0);
|
||||||
|
transform: skew(-15deg, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preloader p {
|
||||||
|
color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
left: -50px;
|
||||||
|
top: 60px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes trans1 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate3d(53px, 0, 0);
|
||||||
|
transform: translate3d(53px, 0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate3d(-250px, 0, 0);
|
||||||
|
transform: translate3d(-250px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes trans1 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate3d(53px, 0, 0);
|
||||||
|
transform: translate3d(53px, 0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate3d(-250px, 0, 0);
|
||||||
|
transform: translate3d(-250px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes trans2 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate3d(-160px, 0, 0);
|
||||||
|
transform: translate3d(-160px, 0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate3d(53px, 0, 0);
|
||||||
|
transform: translate3d(53px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes trans2 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate3d(-160px, 0, 0);
|
||||||
|
transform: translate3d(-160px, 0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate3d(53px, 0, 0);
|
||||||
|
transform: translate3d(53px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes trans3 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate3d(53px, 0, 0);
|
||||||
|
transform: translate3d(53px, 0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate3d(-220px, 0, 0);
|
||||||
|
transform: translate3d(-220px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes trans3 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate3d(53px, 0, 0);
|
||||||
|
transform: translate3d(53px, 0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate3d(-220px, 0, 0);
|
||||||
|
transform: translate3d(-220px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- 加载第一步结束 -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<!-- 第二步开始 -->
|
||||||
|
<div class="preloader">
|
||||||
|
<div class="loader-inner">
|
||||||
|
<div id="top" class="mask">
|
||||||
|
<div class="plane"></div>
|
||||||
|
</div>
|
||||||
|
<div id="middle" class="mask">
|
||||||
|
<div class="plane"></div>
|
||||||
|
</div>
|
||||||
|
<div id="bottom" class="mask">
|
||||||
|
<div class="plane"></div>
|
||||||
|
</div>
|
||||||
|
<p>页面加载中,请耐心等待...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 第二步结束 -->
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
lint-staged.config.cjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
"*.{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"]
|
||||||
|
};
|
99
package.json
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"name": "gushi",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:test": "vue-tsc && vite build --mode test",
|
||||||
|
"build:prod": "vue-tsc && vite build --mode production",
|
||||||
|
"type:check": "vue-tsc --noEmit --skipLibCheck",
|
||||||
|
"preview": "npm run build:dev && vite preview",
|
||||||
|
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
|
||||||
|
"fix": "eslint src --fix",
|
||||||
|
"lint:prettier": "prettier --write \"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",
|
||||||
|
"release": "standard-version",
|
||||||
|
"commit": "git add -A && czg && git push",
|
||||||
|
"commitlint": "commitlint --config commitlint.config.cjs -e -V"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@vueuse/core": "^11.2.0",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
|
"animate.css": "^4.1.1",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"default-passive-events": "^2.0.0",
|
||||||
|
"driver.js": "^1.3.1",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
|
"element-plus": "^2.8.7",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"pinia": "^2.2.6",
|
||||||
|
"pinia-plugin-persistedstate": "^4.1.2",
|
||||||
|
"sortablejs": "^1.15.3",
|
||||||
|
"vue": "^3.5.12",
|
||||||
|
"vue-router": "^4.4.5",
|
||||||
|
"vue3-count-to": "^1.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/eslint-parser": "^7.25.9",
|
||||||
|
"@commitlint/cli": "^19.5.0",
|
||||||
|
"@commitlint/config-conventional": "^19.5.0",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/nprogress": "^0.2.3",
|
||||||
|
"@types/sortablejs": "^1.15.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
||||||
|
"@typescript-eslint/parser": "^8.13.0",
|
||||||
|
"@unocss/reset": "^0.64.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"cz-git": "^1.10.1",
|
||||||
|
"czg": "^1.10.1",
|
||||||
|
"eslint": "^9.14.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"eslint-plugin-vue": "^9.30.0",
|
||||||
|
"husky": "^9.1.6",
|
||||||
|
"lint-staged": "^15.2.10",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"postcss-html": "^1.7.0",
|
||||||
|
"postcss-scss": "^4.0.9",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"sass": "^1.80.6",
|
||||||
|
"sass-loader": "^16.0.3",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
|
"stylelint": "^16.10.0",
|
||||||
|
"stylelint-config-html": "^1.1.0",
|
||||||
|
"stylelint-config-prettier": "^9.0.5",
|
||||||
|
"stylelint-config-recess-order": "^5.1.1",
|
||||||
|
"stylelint-config-recommended-scss": "^14.1.0",
|
||||||
|
"stylelint-config-recommended-vue": "^1.5.0",
|
||||||
|
"stylelint-config-standard": "^36.0.1",
|
||||||
|
"stylelint-config-standard-scss": "^13.1.0",
|
||||||
|
"stylelint-config-standard-vue": "^1.0.0",
|
||||||
|
"stylelint-order": "^6.0.4",
|
||||||
|
"stylelint-scss": "^6.8.1",
|
||||||
|
"svgo": "^3.3.2",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"unocss": "^0.64.0",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-image-optimizer": "^1.1.8",
|
||||||
|
"vite-plugin-mock": "^3.0.2",
|
||||||
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
|
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||||
|
"vue-i18n": "^10.0.4",
|
||||||
|
"vue-tsc": "^2.1.10"
|
||||||
|
},
|
||||||
|
"commitizen": {
|
||||||
|
"path": "node_modules/cz-git"
|
||||||
|
}
|
||||||
|
}
|
8556
pnpm-lock.yaml
generated
Normal file
5
postcss.config.cjs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
BIN
public/vite.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
6
scripts/preinstall.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
if (!/pnpm/.test(process.env.npm_execpath || "")) {
|
||||||
|
console.warn(
|
||||||
|
`\u001b[33mThis repository must using pnpm as the package manager ` + ` for scripts to work properly.\u001b[39m\n`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
68
src/App.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<el-config-provider :locale="locale" :size="dimension">
|
||||||
|
<router-view></router-view>
|
||||||
|
</el-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, nextTick, computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { getBrowserLang } from "@/utils/index.ts";
|
||||||
|
import { useTheme } from "@/utils/theme.ts";
|
||||||
|
import en from "element-plus/es/locale/lang/en";
|
||||||
|
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||||
|
import { autoRefresh } from "@/utils/autoUpdate.ts";
|
||||||
|
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
|
||||||
|
const dimension = computed(() => globalStore.dimension);
|
||||||
|
const { initThemeConfig } = useTheme();
|
||||||
|
|
||||||
|
// 初始化语言
|
||||||
|
const i18n = useI18n();
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化主题配置
|
||||||
|
handleThemeConfig();
|
||||||
|
// 初始化语言配置
|
||||||
|
//handleI18nConfig();
|
||||||
|
// 自动检测更新
|
||||||
|
//handleAutoUpdate();
|
||||||
|
// 开发环境打印项目名称
|
||||||
|
// console.log(
|
||||||
|
// `%c KOI-ADMIN %c V1.0.0 `,
|
||||||
|
// "padding: 2px 1px; border-radius: 3px 0 0 3px; color: #fff; background: #6169FF; font-weight: bold;",
|
||||||
|
// "padding: 2px 1px; border-radius: 0 3px 3px 0; color: #fff; background: #42c02e; font-weight: bold;"
|
||||||
|
// );
|
||||||
|
});
|
||||||
|
|
||||||
|
// 语言配置
|
||||||
|
// const locale = computed(() => {
|
||||||
|
// if (globalStore.language == "zh") return zhCn;
|
||||||
|
// if (globalStore.language == "en") return en;
|
||||||
|
// return getBrowserLang() == "zh" ? zhCn : en;
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// // 初始化语言配置
|
||||||
|
// const handleI18nConfig = () => {
|
||||||
|
// const language = globalStore.language ?? getBrowserLang();
|
||||||
|
// i18n.locale.value = language;
|
||||||
|
// globalStore.setGlobalState("language", language);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 初始化主题配置
|
||||||
|
const handleThemeConfig = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
initThemeConfig();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自动检测更新
|
||||||
|
// const handleAutoUpdate = () => {
|
||||||
|
// nextTick(() => {
|
||||||
|
// if (import.meta.env.VITE_ENV === "production") autoRefresh();
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
81
src/api/system/post/index.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// 导入二次封装axios
|
||||||
|
import koi from "@/utils/axios.ts";
|
||||||
|
|
||||||
|
// 统一管理接口
|
||||||
|
enum API {
|
||||||
|
//类型接口
|
||||||
|
LIST = "/api/admin/type/index ",
|
||||||
|
ADD = "/api/admin/type/add",
|
||||||
|
UPDATE = "/api/admin/type/update",
|
||||||
|
DEL = "/api/admin/type/del",
|
||||||
|
//新增报纸
|
||||||
|
BMADD = "/api/admin/bm/add",
|
||||||
|
//报纸列表
|
||||||
|
BMList = "/api/admin/bm/index",
|
||||||
|
//报纸 下 的版面
|
||||||
|
BMListNEXT = "/api/admin/bm/bm",
|
||||||
|
//版面下面的文章
|
||||||
|
BMLISTNEWS = "/api/admin/bm/news",
|
||||||
|
DATEUPDATE = "/api/admin/date/update",
|
||||||
|
BMUPDATE = "/api/admin/bm/update",
|
||||||
|
DATEDEL = "/api/admin/date/del",
|
||||||
|
BMDEL = "/api/admin/bm/del",
|
||||||
|
NEWSDEL= "/api/admin/news/del",
|
||||||
|
BMINFO="/api/admin/bm/find",
|
||||||
|
NEWSADD="/api/admin/news/add",
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据ID进行查询
|
||||||
|
export const getList = (data: any) => {
|
||||||
|
return koi.post(API.LIST, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据ID进行修改
|
||||||
|
export const add = (data: any) => {
|
||||||
|
return koi.post(API.ADD, data);
|
||||||
|
};
|
||||||
|
// 根据ID进行修改
|
||||||
|
export const update = (data: any) => {
|
||||||
|
return koi.post(API.UPDATE, data);
|
||||||
|
};
|
||||||
|
// 根据ID进行修改
|
||||||
|
export const del = (data: any) => {
|
||||||
|
return koi.post(API.DEL, data);
|
||||||
|
};
|
||||||
|
export const bmAdd = (data: any) => {
|
||||||
|
return koi.post(API.BMADD, data);
|
||||||
|
};
|
||||||
|
export const bmList = (data: any) => {
|
||||||
|
return koi.post(API.BMList, data);
|
||||||
|
};
|
||||||
|
export const bmListNext = (data: any) => {
|
||||||
|
return koi.post(API.BMListNEXT, data);
|
||||||
|
};
|
||||||
|
export const bmListNews = (data: any) => {
|
||||||
|
return koi.post(API.BMLISTNEWS, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dateUpdate = (data: any) => {
|
||||||
|
return koi.post(API.DATEUPDATE, data);
|
||||||
|
};
|
||||||
|
export const bmUpdate = (data: any) => {
|
||||||
|
return koi.post(API.BMUPDATE, data);
|
||||||
|
};
|
||||||
|
export const dateDel = (data: any) => {
|
||||||
|
return koi.post(API.DATEDEL, data);
|
||||||
|
};
|
||||||
|
export const bmDel = (data: any) => {
|
||||||
|
return koi.post(API.BMDEL, data);
|
||||||
|
};
|
||||||
|
export const newsDel = (data: any) => {
|
||||||
|
return koi.post(API.NEWSDEL, data);
|
||||||
|
};
|
||||||
|
export const bmInfo = (data: any) => {
|
||||||
|
return koi.post(API.BMINFO, data);
|
||||||
|
};
|
||||||
|
export const newsAdd = (data: any) => {
|
||||||
|
return koi.post(API.NEWSADD, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
BIN
src/assets/fonts/KoiFont.woff2
Normal file
1
src/assets/icons/koi-menu-earth.svg
Normal file
After Width: | Height: | Size: 7.9 KiB |
1
src/assets/icons/koi-menu-left.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11,13H21V11H11M11,9H21V7H11M3,3V5H21V3M3,21H21V19H3M3,12L7,16V8M11,17H21V15H11V17Z" /></svg>
|
After Width: | Height: | Size: 162 B |
1
src/assets/icons/koi-menu-moon.svg
Normal 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="1710850353810" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8349" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M551.41 326.72h-74.14c-10.24 0-18.56-8.3-18.56-18.54 0-10.24 8.31-18.54 18.56-18.54h74.14c10.24 0 18.56 8.3 18.56 18.54 0 10.24-8.32 18.54-18.56 18.54z" fill="#F6BB42" p-id="8350"></path><path d="M773.93 697.54c-245.78 0-445.01-199.23-445.01-445 0-55.23 10.08-108.13 28.48-156.94C189.08 159.05 69.34 321.61 69.34 512.14c0 245.76 199.23 445 445.01 445 190.51 0 353.07-119.74 416.51-288.07-48.8 18.4-101.7 28.47-156.93 28.47z" fill="#FFCE54" p-id="8351"></path><path d="M551.41 920.05c-245.75 0-444.98-199.23-444.98-445 0-113.41 42.44-216.91 112.26-295.48-91.61 81.5-149.35 200.28-149.35 332.57 0 245.76 199.23 445 445.01 445 132.26 0 251.05-57.74 332.55-149.37-78.57 69.85-182.08 112.28-295.49 112.28z" fill="#F6BB42" p-id="8352"></path><path d="M736.84 308.17c0 10.24-8.31 18.54-18.55 18.54-10.22 0-18.53-8.3-18.53-18.54 0-10.24 8.32-18.54 18.53-18.54 10.24 0 18.55 8.3 18.55 18.54z" fill="#4A89DC" p-id="8353"></path><path d="M644.14 493.59c0 10.23-8.31 18.54-18.56 18.54-10.24 0-18.53-8.32-18.53-18.54 0-10.24 8.29-18.54 18.53-18.54 10.25 0 18.56 8.3 18.56 18.54z" fill="#48CFAD" p-id="8354"></path><path d="M959.33 270.79c0 10.24-8.31 18.54-18.53 18.54-10.24 0-18.56-8.3-18.56-18.54 0-10.24 8.31-18.54 18.56-18.54 10.22 0 18.53 8.29 18.53 18.54z" fill="#ED5564" p-id="8355"></path><path d="M625.58 85.37c0 10.24-8.29 18.54-18.53 18.54-10.24 0-18.56-8.3-18.56-18.54 0-10.23 8.31-18.53 18.56-18.53 10.25 0 18.53 8.3 18.53 18.53z" fill="#AC92EB" p-id="8356"></path><path d="M514.35 234c-10.24 0-18.56 8.31-18.56 18.54V363.8c0 10.23 8.31 18.53 18.56 18.53 10.22 0 18.53-8.3 18.53-18.53V252.54c0-10.23-8.31-18.54-18.53-18.54z" fill="#FFCE54" p-id="8357"></path><path d="M829.55 437.96c-10.24 0-18.53 8.3-18.53 18.54v111.24c0 10.24 8.29 18.54 18.53 18.54 10.24 0 18.53-8.3 18.53-18.54V456.51c-0.01-10.25-8.29-18.55-18.53-18.55z" fill="#F6BB42" p-id="8358"></path><path d="M866.63 530.67h-74.17c-10.24 0-18.53-8.3-18.53-18.53 0-10.24 8.29-18.54 18.53-18.54h74.17c10.24 0 18.53 8.3 18.53 18.54 0 10.22-8.28 18.53-18.53 18.53z" fill="#FFCE54" p-id="8359"></path></svg>
|
After Width: | Height: | Size: 2.3 KiB |
1
src/assets/icons/koi-menu-right.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11,13H21V11H11M11,9H21V7H11M3,3V5H21V3M11,17H21V15H11M3,8V16L7,12M3,21H21V19H3V21Z" /></svg>
|
After Width: | Height: | Size: 162 B |
1
src/assets/icons/koi-menu-sun.svg
Normal 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="1710850426744" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="20657" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M752.64 887.552a64 64 0 1 1-115.008 56.128 64 64 0 0 1 115.008-56.128z m-418.176-29.44a64 64 0 1 1-56.128 115.072 64 64 0 0 1 56.128-115.072z m617.472-308.8a64 64 0 1 1-28.8 124.736 64 64 0 0 1 28.8-124.736z m-825.6 48a64 64 0 1 1-124.672 28.8 64 64 0 0 1 124.672-28.8z m774.4-404.928a64 64 0 1 1-99.456 80.576 64 64 0 0 1 99.456-80.576zM190.784 182.912a64 64 0 1 1-80.576 99.52 64 64 0 0 1 80.576-99.52zM500.736 0a64 64 0 1 1 0 128 64 64 0 0 1 0-128zM500.736 192a320 320 0 1 1 0 640 320 320 0 0 1 0-640z" fill="#F5A623" p-id="20658"></path><path d="M500.736 256a256 256 0 1 0 0 512 256 256 0 0 0 0-512z" fill="#F8E71C" p-id="20659"></path></svg>
|
After Width: | Height: | Size: 980 B |
1
src/assets/icons/koi-mobile-menu.svg
Normal 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="1700662317665" class="icon" viewBox="0 0 1034 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17831" xmlns:xlink="http://www.w3.org/1999/xlink" width="129.25" height="128"><path d="M735.857778 759.523556H153.543111a153.6 153.6 0 0 1-153.6-153.6v-374.044445a153.6 153.6 0 0 1 153.6-153.6h582.314667a154.908444 154.908444 0 0 1 137.557333 85.333333 30.72 30.72 0 0 1-55.011555 27.363556 93.013333 93.013333 0 0 0-82.545778-51.2H153.543111a92.216889 92.216889 0 0 0-92.103111 92.103111v374.044445a92.216889 92.216889 0 0 0 92.103111 92.103111h582.314667a92.216889 92.216889 0 0 0 84.878222-56.433778 30.72 30.72 0 0 1 56.547556 24.007111 153.6 153.6 0 0 1-141.425778 93.923556z" fill="#0A71EF" p-id="17832"></path><path d="M413.980444 731.761778m30.72 0l-0.056888 0q30.72 0 30.72 30.72l0 144.668444q0 30.72-30.72 30.72l0.056888 0q-30.72 0-30.72-30.72l0-144.668444q0-30.72 30.72-30.72Z" fill="#0A71EF" p-id="17833"></path><path d="M199.054222 884.110222m30.72 0l429.852445 0q30.72 0 30.72 30.72l0-0.056889q0 30.72-30.72 30.72l-429.852445 0q-30.72 0-30.72-30.72l0 0.056889q0-30.72 30.72-30.72Z" fill="#0A71EF" p-id="17834"></path><path d="M199.054222 299.576889m30.72 0l240.355556 0q30.72 0 30.72 30.72l0-0.056889q0 30.72-30.72 30.72l-240.355556 0q-30.72 0-30.72-30.72l0 0.056889q0-30.72 30.72-30.72Z" fill="#89BAF7" p-id="17835"></path><path d="M199.054222 484.124444m30.72 0l119.068445 0q30.72 0 30.72 30.72l0-0.056888q0 30.72-30.72 30.72l-119.068445 0q-30.72 0-30.72-30.72l0 0.056888q0-30.72 30.72-30.72Z" fill="#89BAF7" p-id="17836"></path><path d="M1018.709333 428.088889l-83.285333 150.983111a33.678222 33.678222 0 0 1-29.468444 17.521778h-165.205334a33.678222 33.678222 0 0 1-29.468444-17.521778l-83.285334-150.983111a34.133333 34.133333 0 0 1 0-33.166222l83.285334-150.983111a33.678222 33.678222 0 0 1 29.468444-17.521778h165.376a33.678222 33.678222 0 0 1 29.468445 17.521778l83.285333 150.983111a34.133333 34.133333 0 0 1-0.170667 33.166222z m-25.543111-14.222222a4.892444 4.892444 0 0 0 0-4.721778l-83.114666-150.983111a4.778667 4.778667 0 0 0-4.209778-2.503111h-164.977778a4.778667 4.778667 0 0 0-4.209778 2.503111l-83.114666 151.210666a4.892444 4.892444 0 0 0 0 4.721778l83.114666 150.983111a4.778667 4.778667 0 0 0 4.209778 2.503111h164.977778a4.778667 4.778667 0 0 0 4.209778-2.503111z m-171.121778 73.955555a77.937778 77.937778 0 1 1 77.937778-77.937778 77.937778 77.937778 0 0 1-77.824 77.710223z m0-29.240889a48.696889 48.696889 0 1 0-48.696888-48.696889 48.696889 48.696889 0 0 0 48.810666 48.469334z" fill="#FF7733" p-id="17837"></path></svg>
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/images/632-01.jpg
Normal file
After Width: | Height: | Size: 721 KiB |
BIN
src/assets/images/error/403.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/images/error/404.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
src/assets/images/error/500.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
src/assets/images/login/bg.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
src/assets/images/login/waoku.jpg
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
src/assets/images/login/wuwu.jpg
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
src/assets/images/logo/logo.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
8
src/assets/json/authLogin.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"msg": "SUCCESS",
|
||||||
|
"data": {
|
||||||
|
"tokenName": "Authorization",
|
||||||
|
"tokenValue": "yB_CH5MUwIuTV9d7a_XnSXCBrQVL63PmfA__"
|
||||||
|
}
|
||||||
|
}
|
125
src/assets/json/authMenu.json
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"msg": "SUCCESS",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"menuId": 1,
|
||||||
|
"menuName": "电子报管理",
|
||||||
|
"enName": "DZBguanli",
|
||||||
|
"parentId": 0,
|
||||||
|
"menuType": "1",
|
||||||
|
"path": "/paper",
|
||||||
|
"name": "paperPageCenter",
|
||||||
|
"component": "",
|
||||||
|
"icon": "Tools",
|
||||||
|
"isHide": "1",
|
||||||
|
"isLink": "",
|
||||||
|
"isKeepAlive": "0",
|
||||||
|
"isFull": "1",
|
||||||
|
"isAffix": "1",
|
||||||
|
"redirect": "/system"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuId": 11,
|
||||||
|
"menuName": "新增期刊",
|
||||||
|
"enName": "User Manage",
|
||||||
|
"parentId": 1,
|
||||||
|
"menuType": "2",
|
||||||
|
"path": "/paper/add",
|
||||||
|
"name": "userPageAdd",
|
||||||
|
"component": "paper/add/index",
|
||||||
|
"icon": "CirclePlus",
|
||||||
|
"isHide": "1",
|
||||||
|
"isLink": "",
|
||||||
|
"isKeepAlive": "0",
|
||||||
|
"isFull": "1",
|
||||||
|
"isAffix": "1",
|
||||||
|
"redirect": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuId": 12,
|
||||||
|
"menuName": "编辑期刊",
|
||||||
|
"enName": "User Manage",
|
||||||
|
"parentId": 1,
|
||||||
|
"menuType": "2",
|
||||||
|
"path": "/paper/edit",
|
||||||
|
"name": "userPageEdit",
|
||||||
|
"component": "paper/add/edit",
|
||||||
|
"icon": "CirclePlus",
|
||||||
|
"isHide": "0",
|
||||||
|
"isLink": "",
|
||||||
|
"isKeepAlive": "0",
|
||||||
|
"isFull": "1",
|
||||||
|
"isAffix": "1",
|
||||||
|
"redirect": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuId": 13,
|
||||||
|
"menuName": "期刊列表",
|
||||||
|
"enName": "User Manage",
|
||||||
|
"parentId": 1,
|
||||||
|
"menuType": "2",
|
||||||
|
"path": "/paper/list",
|
||||||
|
"name": "PaperList",
|
||||||
|
"component": "paper/list/index",
|
||||||
|
"icon": "Tickets",
|
||||||
|
"isHide": "1",
|
||||||
|
"isLink": "",
|
||||||
|
"isKeepAlive": "1",
|
||||||
|
"isFull": "1",
|
||||||
|
"isAffix": "1",
|
||||||
|
"redirect": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuId": 14,
|
||||||
|
"menuName": "添加新闻",
|
||||||
|
"enName": "User Manage",
|
||||||
|
"parentId": 1,
|
||||||
|
"menuType": "2",
|
||||||
|
"path": "/paper/article/index/:id",
|
||||||
|
"name": "userPageArticle",
|
||||||
|
"component": "paper/article/index",
|
||||||
|
"icon": "Notebook",
|
||||||
|
"isHide": "0",
|
||||||
|
"isLink": "",
|
||||||
|
"isKeepAlive": "1",
|
||||||
|
"isFull": "1",
|
||||||
|
"isAffix": "1",
|
||||||
|
"redirect": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuId": 15,
|
||||||
|
"menuName": "期刊类型",
|
||||||
|
"enName": "User Manage",
|
||||||
|
"parentId": 1,
|
||||||
|
"menuType": "2",
|
||||||
|
"path": "/paper/type",
|
||||||
|
"name": "PaperType",
|
||||||
|
"component": "paper/type/index",
|
||||||
|
"icon": "Files",
|
||||||
|
"isHide": "1",
|
||||||
|
"isLink": "",
|
||||||
|
"isKeepAlive": "0",
|
||||||
|
"isFull": "1",
|
||||||
|
"isAffix": "1",
|
||||||
|
"redirect": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuId": 2,
|
||||||
|
"menuName": "知识库",
|
||||||
|
"enName": "User Manage",
|
||||||
|
"parentId": 0,
|
||||||
|
"menuType": "1",
|
||||||
|
"path": "/knowledge",
|
||||||
|
"name": "Knowledge",
|
||||||
|
"component": "knowledge/list/index",
|
||||||
|
"icon": "Files",
|
||||||
|
"isHide": "1",
|
||||||
|
"isLink": "",
|
||||||
|
"isKeepAlive": "0",
|
||||||
|
"isFull": "1",
|
||||||
|
"isAffix": "1",
|
||||||
|
"redirect": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
22
src/assets/json/authUser.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"msg": "SUCCESS",
|
||||||
|
"data": {
|
||||||
|
"loginUser": {
|
||||||
|
"userId": 1,
|
||||||
|
"loginName": "YU-ADMIN",
|
||||||
|
"sex": "1",
|
||||||
|
"avatar": "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2Fae90b4c7-98b6-4a47-b1b3-9ee8bc71acf6%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1692146441&t=6fca60f3a0d323869b81d8fb53b5dd1b"
|
||||||
|
},
|
||||||
|
"roles": ["YU-ADMIN", "SUPER-ADMIN"],
|
||||||
|
"buttons": [
|
||||||
|
"system:role:search",
|
||||||
|
"system:role:list",
|
||||||
|
"system:role:add",
|
||||||
|
"system:role:delete",
|
||||||
|
"system:role:update",
|
||||||
|
"system:role:import",
|
||||||
|
"system:role:export"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
src/assets/mouse/a.cur
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/assets/mouse/b.cur
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
src/assets/mouse/c.cur
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/mouse/index.cur
Normal file
After Width: | Height: | Size: 4.2 KiB |
19
src/components/KoiCard/Index.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div class="koi-card">
|
||||||
|
<div class="p-b-8px">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
<div class="p-t-12px">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.koi-card {
|
||||||
|
@apply overflow-x-hidden p-x-20px p-t-10px p-b-0px bg-#fff text-#303133 dark:bg-#1d1e1f dark:text-#cfd3dc dark:b-#414243 border-1px border-solid border-#e4e7ed rounded-6px h-100% flex flex-col flex-1;
|
||||||
|
}
|
||||||
|
</style>
|
164
src/components/KoiDialog/Index.vue
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<!-- append-to-body 点击空白处不关闭弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
:title="title"
|
||||||
|
:width="width"
|
||||||
|
:center="center"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
append-to-body
|
||||||
|
draggable
|
||||||
|
:destroy-on-close="destroyOnClose"
|
||||||
|
:before-close="koiClose"
|
||||||
|
:fullscreen="fullscreen"
|
||||||
|
:loading="loading"
|
||||||
|
:footerHidden="footerHidden"
|
||||||
|
>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
<div class="container" :style="{ height: height + 'px' }">
|
||||||
|
<!-- 具名插槽 -->
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
<template #footer v-if="!footerHidden">
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button type="primary" loading-icon="Eleme" :loading="confirmLoading" v-throttle="koiConfirm">{{
|
||||||
|
confirmText
|
||||||
|
}}</el-button>
|
||||||
|
<el-button type="danger" @click="koiCancel">{{ cancelText }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 此弹窗封装将使用 defineExpose,通过ref进行使用 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, toRefs } from "vue";
|
||||||
|
// @ts-ignore
|
||||||
|
import { koiMsgWarning, koiMsgBox } from "@/utils/koi.ts";
|
||||||
|
// 定义参数的类型
|
||||||
|
interface IDialogProps {
|
||||||
|
title?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
width?: number;
|
||||||
|
center?: boolean;
|
||||||
|
height?: number;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
destroyOnClose?: boolean;
|
||||||
|
fullscreen?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
footerHidden?: boolean; // 是否隐藏确认和取消按钮部分
|
||||||
|
}
|
||||||
|
// 子组件接收父组件的值
|
||||||
|
// withDefaults:设置默认值 defineProps:接收父组件的参数
|
||||||
|
// @ts-ignore
|
||||||
|
const props = withDefaults(defineProps<IDialogProps>(), {
|
||||||
|
title: "朕很中意你KoiDialog",
|
||||||
|
height: 300,
|
||||||
|
width: 650,
|
||||||
|
center: true,
|
||||||
|
visible: false,
|
||||||
|
confirmText: "确定",
|
||||||
|
cancelText: "取消",
|
||||||
|
destroyOnClose: false,
|
||||||
|
fullscreen: false,
|
||||||
|
loading: false,
|
||||||
|
footerHidden: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开关变量
|
||||||
|
const visible = ref(false);
|
||||||
|
|
||||||
|
// 确定的loading,此处必须用toRefs,否则将失去响应式
|
||||||
|
const { loading } = toRefs(props);
|
||||||
|
const confirmLoading = ref(loading);
|
||||||
|
|
||||||
|
// 打开弹窗
|
||||||
|
const koiOpen = () => {
|
||||||
|
visible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const koiClose = () => {
|
||||||
|
visible.value = false;
|
||||||
|
// koiMsgBox("您确认进行关闭么?")
|
||||||
|
// .then(() => {
|
||||||
|
// visible.value = false;
|
||||||
|
// koiMsgWarning("已关闭🌻");
|
||||||
|
// })
|
||||||
|
// .catch(() => {
|
||||||
|
// // 用户点击了取消按钮或关闭确认框
|
||||||
|
// // 执行取消操作或不做任何操作
|
||||||
|
// koiMsgWarning("已取消🌻");
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认提交后关闭弹窗
|
||||||
|
const koiQuickClose = () => {
|
||||||
|
visible.value = false;
|
||||||
|
// koiMsgWarning("已提交🌻");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前组件获取父组件传递的事件方法
|
||||||
|
const emits = defineEmits(["koiConfirm", "koiCancel"]);
|
||||||
|
// 弹框的确定事件
|
||||||
|
const koiConfirm = () => {
|
||||||
|
emits("koiConfirm");
|
||||||
|
};
|
||||||
|
// 弹框的取消事件
|
||||||
|
const koiCancel = () => {
|
||||||
|
emits("koiCancel");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露给父组件方法
|
||||||
|
defineExpose({
|
||||||
|
koiOpen,
|
||||||
|
koiClose,
|
||||||
|
koiQuickClose
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
overflow-x: initial;
|
||||||
|
overflow-y: auto; // 超出部分则滚动
|
||||||
|
}
|
||||||
|
.el-dialog {
|
||||||
|
border-top-left-radius: 8px !important;
|
||||||
|
border-top-right-radius: 8px !important;
|
||||||
|
padding-top: 0px;
|
||||||
|
|
||||||
|
// 标题头部
|
||||||
|
.el-dialog__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 50px !important;
|
||||||
|
padding: 0 0 6px 0;
|
||||||
|
|
||||||
|
// background: #1e71ee;
|
||||||
|
@apply dark:bg-#141414;
|
||||||
|
.el-dialog__title {
|
||||||
|
font-family: YouYuan;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
.el-dialog__footer {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-dialog__body {
|
||||||
|
// 内容区域内边距
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.el-dialog__headerbtn {
|
||||||
|
padding-bottom: 10px !important;
|
||||||
|
.el-dialog__close {
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
152
src/components/KoiDrawer/Index.vue
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer
|
||||||
|
v-model="visible"
|
||||||
|
:title="title"
|
||||||
|
:size="size"
|
||||||
|
:direction="direction"
|
||||||
|
:close-on-click-modal="closeOnClickModel"
|
||||||
|
:destroy-on-close="destroyOnClose"
|
||||||
|
:before-close="koiClose"
|
||||||
|
:loading="loading"
|
||||||
|
:footerHidden="footerHidden"
|
||||||
|
>
|
||||||
|
<div class="formDrawer">
|
||||||
|
<div class="body">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="footer" v-if="!footerHidden">
|
||||||
|
<el-button type="primary" loading-icon="Eleme" :loading="confirmLoading" v-throttle="koiConfirm">{{
|
||||||
|
confirmText
|
||||||
|
}}</el-button>
|
||||||
|
<el-button type="danger" @click="koiCancel">{{ cancelText }}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, toRefs } from "vue";
|
||||||
|
// @ts-ignore
|
||||||
|
import { koiMsgWarning, koiMsgBox } from "@/utils/koi.ts";
|
||||||
|
// 定义参数的类型
|
||||||
|
interface IDrawerProps {
|
||||||
|
title?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
size?: string;
|
||||||
|
destroyOnClose?: boolean;
|
||||||
|
closeOnClickModel?: boolean;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
direction?: any;
|
||||||
|
loading?: boolean;
|
||||||
|
footerHidden?: boolean; // 是否隐藏确认和取消按钮部分
|
||||||
|
}
|
||||||
|
// 子组件接收父组件的值
|
||||||
|
// withDefaults:设置默认值 defineProps:接收父组件的参数
|
||||||
|
const props = withDefaults(defineProps<IDrawerProps>(), {
|
||||||
|
title: "朕很中意你KoiDrawer",
|
||||||
|
visible: false,
|
||||||
|
size: "450",
|
||||||
|
closeOnClickModel: false,
|
||||||
|
destroyOnClose: false,
|
||||||
|
confirmText: "确定",
|
||||||
|
cancelText: "取消",
|
||||||
|
direction: "rtl",
|
||||||
|
loading: false,
|
||||||
|
footerHidden: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开关变量
|
||||||
|
const visible = ref(false);
|
||||||
|
// 确定的loading,此处必须用toRefs,否则将失去响应式
|
||||||
|
const { loading } = toRefs(props);
|
||||||
|
const confirmLoading = ref(loading);
|
||||||
|
|
||||||
|
// 打开抽屉
|
||||||
|
const koiOpen = () => {
|
||||||
|
visible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭抽屉
|
||||||
|
const koiClose = () => {
|
||||||
|
koiMsgBox("您确认进行关闭么?")
|
||||||
|
.then(() => {
|
||||||
|
visible.value = false;
|
||||||
|
koiMsgWarning("已关闭🌻");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户点击了取消按钮或关闭确认框
|
||||||
|
// 执行取消操作或不做任何操作
|
||||||
|
koiMsgWarning("已取消🌻");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 关闭抽屉
|
||||||
|
const koiCloseNoMsg = () => {
|
||||||
|
visible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认提交后关闭弹窗
|
||||||
|
const koiQuickClose = () => {
|
||||||
|
visible.value = false;
|
||||||
|
koiMsgWarning("已提交🌻");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认
|
||||||
|
const koiConfirm = () => {
|
||||||
|
emits("koiConfirm");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭抽屉
|
||||||
|
const koiCancel = () => {
|
||||||
|
emits("koiCancel");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前组件获取父组件传递的事件方法,然后点击确认和提交是触发父组件传递过来的事件
|
||||||
|
const emits = defineEmits(["koiConfirm", "koiCancel"]);
|
||||||
|
|
||||||
|
// defineExpose是vue3新增的一个api,放在<script setup>下使用的,
|
||||||
|
// 目的是把属性和方法暴露出去,可以用于父子组件通信,子组件把属性暴露出去,
|
||||||
|
// 父组件用ref获取子组件DOM,子组件暴露的方法或属性可以用dom获取。
|
||||||
|
defineExpose({
|
||||||
|
koiOpen,
|
||||||
|
koiClose,
|
||||||
|
koiCloseNoMsg,
|
||||||
|
koiQuickClose
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.formDrawer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
.body {
|
||||||
|
bottom: 50px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto; // 超出部分则滚动
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 50px;
|
||||||
|
margin-top: auto;
|
||||||
|
// justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-drawer__title) {
|
||||||
|
font-weight: 600;
|
||||||
|
@apply dark:c-#CFD3DC;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-drawer__body) {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-drawer__header) {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
165
src/components/KoiExcel/Index.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" :title="koiParams.title" :destroy-on-close="true" width="580px" draggable>
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="模板下载:">
|
||||||
|
<el-button type="primary" :icon="Download" @click="handleTemplateExcel">点击下载</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="文件上传:">
|
||||||
|
<el-upload
|
||||||
|
action="#"
|
||||||
|
class="upload"
|
||||||
|
:drag="true"
|
||||||
|
:limit="excelLimit"
|
||||||
|
:multiple="false"
|
||||||
|
:show-file-list="true"
|
||||||
|
:http-request="handleHttpUpload"
|
||||||
|
:before-upload="beforeExcelUpload"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
:on-success="handleUploadSuccess"
|
||||||
|
:on-error="handleUploadError"
|
||||||
|
:accept="koiParams.fileType"
|
||||||
|
>
|
||||||
|
<slot name="content">
|
||||||
|
<el-icon class="el-icon--upload">
|
||||||
|
<upload-filled />
|
||||||
|
</el-icon>
|
||||||
|
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||||
|
</slot>
|
||||||
|
<template #tip>
|
||||||
|
<slot name="tip">
|
||||||
|
<div class="el-upload__tip">请上传 .xls, .xlsx 标准格式文件,文件最大为 {{ koiParams.fileSize }}M</div>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="importExcelPage">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { Download } from "@element-plus/icons-vue";
|
||||||
|
import { ElNotification, UploadRawFile } from "element-plus";
|
||||||
|
import koi from "@/utils/axios.ts";
|
||||||
|
|
||||||
|
export interface IExcelParamsProps {
|
||||||
|
title: string; // 标题
|
||||||
|
fileType: string; // 类型
|
||||||
|
fileSize?: number; // 上传文件的大小
|
||||||
|
templeApi?: any; // 下载模板的Api
|
||||||
|
importApi?: any; // 批量导入的Api
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最大文件上传数
|
||||||
|
const excelLimit = ref(1);
|
||||||
|
// dialog状态
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
//限制.xls文件
|
||||||
|
const xlsFile = "application/vnd.ms-excel";
|
||||||
|
//限制.xlsx文件
|
||||||
|
const xlsxFile = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||||
|
|
||||||
|
// 父组件传过来的参数
|
||||||
|
const koiParams = ref<IExcelParamsProps>({
|
||||||
|
title: "上传Excel",
|
||||||
|
fileType: xlsFile + "," + xlsxFile,
|
||||||
|
fileSize: 5,
|
||||||
|
templeApi: "",
|
||||||
|
importApi: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// 接收父组件参数
|
||||||
|
const excelParams = (params: IExcelParamsProps) => {
|
||||||
|
koiParams.value = { ...koiParams.value, ...params };
|
||||||
|
dialogVisible.value = true;
|
||||||
|
// alert(params.value.importApi);
|
||||||
|
};
|
||||||
|
// 当前组件获取父组件传递的事件方法,然后点击确认和提交是触发父组件传递过来的事件
|
||||||
|
const emits = defineEmits(["handleTemplateExcel"]);
|
||||||
|
// 下载文件
|
||||||
|
const handleTemplateExcel = () => {
|
||||||
|
emits("handleTemplateExcel");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件上传
|
||||||
|
const handleHttpUpload = async (param: any) => {
|
||||||
|
// alert(koiParams.value.importApi);
|
||||||
|
let fileFormData = new FormData();
|
||||||
|
fileFormData.append("file", param.file, param.file.name);
|
||||||
|
const res: any = await koi.post(koiParams.value.importApi, fileFormData);
|
||||||
|
if (res?.status != 200 && res?.code != 200) {
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: "上传失败,请重试!",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 文件上传之前判断
|
||||||
|
* @param file 上传的文件
|
||||||
|
* */
|
||||||
|
const beforeExcelUpload = (file: UploadRawFile) => {
|
||||||
|
const isExcel = koiParams.value.fileType!.includes(file.type);
|
||||||
|
const fileSize = file.size / 1024 / 1024 < koiParams.value.fileSize!;
|
||||||
|
if (!isExcel)
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: "上传文件只能是xls / xlsx格式!",
|
||||||
|
type: "warning"
|
||||||
|
});
|
||||||
|
if (!fileSize)
|
||||||
|
setTimeout(() => {
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: `上传文件大小不能超过 ${koiParams.value.fileSize}MB!`,
|
||||||
|
type: "warning"
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
return isExcel && fileSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件数超出提示
|
||||||
|
const handleExceed = () => {
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: "最多只能上传一个文件!",
|
||||||
|
type: "warning"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传错误提示
|
||||||
|
const handleUploadError = () => {
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: `${koiParams.value.title}上传失败,请您重新上传!`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传成功提示
|
||||||
|
const handleUploadSuccess = () => {
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: `${koiParams.value.title}上传成功!`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
excelParams
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-upload-dragger) {
|
||||||
|
border: 2px dashed var(--el-color-primary-light-6);
|
||||||
|
&:hover {
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.upload {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
</style>
|
26
src/components/KoiGlobalIcon/Index.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<el-icon :size="props.size">
|
||||||
|
<component v-if="!props.name.startsWith(SVG_PREFIX)" :is="props.name"></component>
|
||||||
|
<component v-if="props.name.startsWith(SVG_PREFIX)" is="KoiSvgIcon" :name="props.name" :width="props.size" :height="props.size"></component>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SVG_PREFIX } from '@/config/index.ts';
|
||||||
|
|
||||||
|
// 定义参数的类型
|
||||||
|
interface IGlobalIconProps {
|
||||||
|
name?: string;
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
// 子组件接收父组件的值
|
||||||
|
// withDefaults:设置默认值 defineProps:接收父组件的参数
|
||||||
|
const props = withDefaults(defineProps<IGlobalIconProps>(), {
|
||||||
|
name: "Sunny",
|
||||||
|
size: "18"
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
110
src/components/KoiLoading/Index.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<transition name="el-fade-in-linear">
|
||||||
|
<div class="loading-box" v-if="props.loading">
|
||||||
|
<div class="loading">
|
||||||
|
<span :style="`--i: ${i}`" v-for="i in 7"></span>
|
||||||
|
</div>
|
||||||
|
<svg>
|
||||||
|
<filter id="gooey">
|
||||||
|
<feGaussianBlur in="SourceGraphic" stdDeviation="10" />
|
||||||
|
<feColorMatrix
|
||||||
|
values="1 0 0 0 0
|
||||||
|
0 1 0 0 0
|
||||||
|
0 0 1 0 0
|
||||||
|
0 0 0 20 -10"
|
||||||
|
/>
|
||||||
|
</filter>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface ILoadingProps {
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ILoadingProps>(), {
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading-box {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
will-change: clip-path;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
filter: url("#gooey");
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading span {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: loading 3s ease-in-out infinite;
|
||||||
|
animation-delay: calc(0.2s * var(--i));
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading span::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: calc(50% - 28px);
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
content: "";
|
||||||
|
background-image: linear-gradient(to right, #92fe9d 0%, #00c9ff 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 30px #00c9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.out-circle {
|
||||||
|
animation: circle-out-center 2.5s cubic-bezier(0.25, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$circle-center-center-in: circle(125% at 50% calc(50vh - 150px + 28px));
|
||||||
|
$circle-center-center-out: circle(0% at 50% calc(50vh - 150px + 28px));
|
||||||
|
|
||||||
|
@keyframes circle-out-center {
|
||||||
|
0% {
|
||||||
|
clip-path: var($circle-center-center-in);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
clip-path: var($circle-center-center-out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
71
src/components/KoiMobileDrawer/Index.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="modelValue" class="fixed inset-0 z-40" @click="close">
|
||||||
|
<div class="absolute inset-0 bg-gray-500 opacity-75" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="fixed z-50 overflow-y-auto bg-white transition-all duration-200 dark:bg-#141414"
|
||||||
|
:class="[
|
||||||
|
`duration-${duration}`,
|
||||||
|
{
|
||||||
|
[`h-screen w-${width}`]: placement === 'right' || placement === 'left',
|
||||||
|
[`w-full h-${width}`]: placement === 'top' || placement === 'bottom',
|
||||||
|
|
||||||
|
'-left-full top-0': placement === 'left' && !modelValue,
|
||||||
|
'left-0 top-0': placement === 'left' && modelValue,
|
||||||
|
|
||||||
|
'-right-full top-0': placement === 'right' && !modelValue,
|
||||||
|
'right-0 top-0': placement === 'right' && modelValue,
|
||||||
|
|
||||||
|
'-top-full left-0': placement === 'top' && !modelValue,
|
||||||
|
'top-0 left-0 ': placement === 'top' && modelValue,
|
||||||
|
|
||||||
|
'-bottom-full left-0': placement === 'bottom' && !modelValue,
|
||||||
|
'bottom-0 left-0': placement === 'bottom' && modelValue
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="mobileDrawer">
|
||||||
|
import { onUnmounted, watch } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: boolean;
|
||||||
|
placement?: "top" | "right" | "bottom" | "left";
|
||||||
|
width?: "sm" | "md" | "lg" | "1/2" | "1/3" | "1/4" | "full";
|
||||||
|
duration?: "100" | "200" | "300" | "400" | "500" | "600";
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: false,
|
||||||
|
placement: "left",
|
||||||
|
duration: "200",
|
||||||
|
width: "md",
|
||||||
|
height: "md"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "close"]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
val => {
|
||||||
|
val && emit("update:modelValue", val);
|
||||||
|
document.body.style.overflow = val ? "hidden" : "auto";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
});
|
||||||
|
</script>
|
111
src/components/KoiSelectIcon/Index.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<el-popover
|
||||||
|
placement="bottom-start"
|
||||||
|
v-model:visible="visible"
|
||||||
|
:disabled="disabled"
|
||||||
|
:width="widthPopover"
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-input
|
||||||
|
:style="{ width: width ? width + 'px' : '100%' }"
|
||||||
|
v-model="inputValue"
|
||||||
|
placeholder="请选择图标"
|
||||||
|
:disabled="disabled"
|
||||||
|
:autofocus="false"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<KoiGlobalIcon :name="modelValue" v-if="modelValue" />
|
||||||
|
<span v-else></span>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-tabs>
|
||||||
|
<el-tab-pane :label="item.name" v-for="(item, index) in iconTabs" :key="index" class="h-70">
|
||||||
|
<el-scrollbar>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
<div v-for="(iconItem, iconIndex) in item.icons" :key="iconIndex" class="m-1">
|
||||||
|
<el-button @click="handleIconSelect(iconItem)">
|
||||||
|
<KoiGlobalIcon :name="iconItem" size="18" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
// 引入ElementPlus所有图标
|
||||||
|
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
interface ISelectIconProps {
|
||||||
|
modelValue?: string;
|
||||||
|
width?: string;
|
||||||
|
widthPopover?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<ISelectIconProps>(), {
|
||||||
|
widthPopover: '380',
|
||||||
|
modelValue: '',
|
||||||
|
disabled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// ElementPlus图标库
|
||||||
|
const elementPlusIconList = [] as string[];
|
||||||
|
|
||||||
|
// 获取ElementPlus所有图标
|
||||||
|
const getElementPlusIcon = () => {
|
||||||
|
for (const name in ElementPlusIconsVue) {
|
||||||
|
elementPlusIconList.push(name);
|
||||||
|
}
|
||||||
|
return elementPlusIconList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const localIconList = [] as string[];
|
||||||
|
const modules = import.meta.glob("../../assets/icons/*.svg");
|
||||||
|
|
||||||
|
|
||||||
|
// 获取本地所有图标
|
||||||
|
const getLocalIcon = () => {
|
||||||
|
// 获取本地所有图标
|
||||||
|
for (const path in modules) {
|
||||||
|
const icon = path.split("assets/icons/")[1].split(".svg")[0];
|
||||||
|
localIconList.push(icon);
|
||||||
|
}
|
||||||
|
return localIconList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const visible = ref(false);
|
||||||
|
const inputValue = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
!value && emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconTabs = [
|
||||||
|
{
|
||||||
|
name: "ele图标",
|
||||||
|
icons: getElementPlusIcon()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '本地图标',
|
||||||
|
icons: getLocalIcon()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const handleIconSelect = (iconItem: string) => {
|
||||||
|
visible.value = false;
|
||||||
|
emit('update:modelValue', iconItem);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
35
src/components/KoiSvgIcon/Index.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<svg :style="{ width: width + 'px', height: height + 'px' }">
|
||||||
|
<use :xlink:href="prefix + name" :fill="color"></use>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 组件使用,图标需要放置@/assets/icons文件夹下,图标名字需要跟name="图标名称"保持一致。
|
||||||
|
// <KoiSvgIcon name="koi-mobile-menu" width="30" height="30"></KoiSvgIcon>
|
||||||
|
defineProps({
|
||||||
|
// xlink:href属性值的前缀
|
||||||
|
prefix: {
|
||||||
|
type: String,
|
||||||
|
default: "#icon-"
|
||||||
|
},
|
||||||
|
// svg矢量图的名字
|
||||||
|
name: String,
|
||||||
|
// svg图标的颜色
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
// svg宽度
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: "18"
|
||||||
|
},
|
||||||
|
// svg高度
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: "18"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
45
src/components/KoiTag/Index.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-for="item in tagOptions">
|
||||||
|
<el-tag
|
||||||
|
v-if="value != null && value.toString() === item.dictValue.toString()"
|
||||||
|
:disable-transitions="true"
|
||||||
|
:key="item.dictLabel"
|
||||||
|
:type="item.dictTag"
|
||||||
|
:index="item.dictLabel + item.dictValue.toString()"
|
||||||
|
:effect="item.dictColor.length === 0 ? effect : 'dark'"
|
||||||
|
:size="size"
|
||||||
|
:style="{
|
||||||
|
'border-color': item.dictColor,
|
||||||
|
background: item.dictColor
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ item.dictLabel }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 定义参数的类型
|
||||||
|
interface ITagProps {
|
||||||
|
tagOptions?: any;
|
||||||
|
value?: any;
|
||||||
|
size?: any;
|
||||||
|
effect?: any;
|
||||||
|
}
|
||||||
|
// 子组件接收父组件的值
|
||||||
|
// withDefaults:设置默认值 defineProps:接收父组件的参数
|
||||||
|
withDefaults(defineProps<ITagProps>(), {
|
||||||
|
tagOptions: [],
|
||||||
|
value: "",
|
||||||
|
size: "default",
|
||||||
|
effect: "light"
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-tag {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
172
src/components/KoiTagFilter/Index.vue
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<div class="select-filter">
|
||||||
|
<div v-for="item in data" :key="item.key" class="select-filter-item">
|
||||||
|
<div class="select-filter-item-title">
|
||||||
|
<span>{{ item.title }}:</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="!item.options.length" class="select-filter-notData">暂无数据 ~</span>
|
||||||
|
<el-scrollbar>
|
||||||
|
<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">
|
||||||
|
<el-icon v-if="option.icon">
|
||||||
|
<component :is="option.icon" />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ option.label }}</span>
|
||||||
|
</slot>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="selectFilter">
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
interface IOptionsProps {
|
||||||
|
value: string | number;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISelectDataProps {
|
||||||
|
title: string; // 列表标题
|
||||||
|
key: string; // 当前筛选项 key 值
|
||||||
|
multiple?: boolean; // 是否为多选
|
||||||
|
options: IOptionsProps[]; // 筛选数据
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISelectFilterProps {
|
||||||
|
data?: ISelectDataProps[]; // 选择的列表数据
|
||||||
|
defaultValues?: { [key: string]: any }; // 默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ISelectFilterProps>(), {
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// emit
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: [value: any];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 选择筛选项
|
||||||
|
* @param {Object} item 选中的哪项列表
|
||||||
|
* @param {Object} option 选中的值
|
||||||
|
* @return void
|
||||||
|
* */
|
||||||
|
const select = (item: ISelectDataProps, option: IOptionsProps) => {
|
||||||
|
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="scss">
|
||||||
|
.select-filter {
|
||||||
|
width: 100%;
|
||||||
|
.select-filter-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px dashed var(--el-border-color-light);
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.select-filter-item-title {
|
||||||
|
margin-top: -2px;
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.select-filter-notData {
|
||||||
|
margin: 18px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
.select-filter-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(--el-color-primary);
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
border: 1px solid var(--el-color-primary-light-5);
|
||||||
|
border-radius: 32px;
|
||||||
|
&:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
transition: 0.1s;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.el-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
36
src/components/KoiToolbar/Index.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 使用方式:<KoiToolbar v-model:showSearch="showSearch" @refreshTable="handleTableData"></KoiToolbar> -->
|
||||||
|
<div class="koi-toolbar">
|
||||||
|
<el-row>
|
||||||
|
<el-tooltip :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top">
|
||||||
|
<el-button circle icon="search" @click="toggleSearch()" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="刷新" placement="top">
|
||||||
|
<el-button circle icon="refresh" @click="handleRefresh()" />
|
||||||
|
</el-tooltip>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps(["showSearch"]);
|
||||||
|
const emits = defineEmits(["update:showSearch", "refreshTable"]);
|
||||||
|
|
||||||
|
// 点击子组件,调用父组件方法
|
||||||
|
const toggleSearch = () => {
|
||||||
|
// 同步修改父子组件的值,但是父组件需要使用v-model:showSearch="showSearch"
|
||||||
|
// @ts-ignore
|
||||||
|
emits("update:showSearch", !props.showSearch);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击子组件,调用父组件方法
|
||||||
|
const handleRefresh = () => {
|
||||||
|
emits("refreshTable");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.koi-toolbar {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
352
src/components/KoiUpload/Files.vue
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 注意:只能通过 on-change 钩子函数来对上传文件的列表进行控制。 -->
|
||||||
|
<el-upload
|
||||||
|
:file-list="fileList"
|
||||||
|
:multiple="props.isMultiple"
|
||||||
|
:limit="props.limit"
|
||||||
|
:accept="props.acceptType"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:disabled="disabled"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
:on-change="handleChange"
|
||||||
|
:folderName="folderName"
|
||||||
|
:fileParam="fileParam"
|
||||||
|
>
|
||||||
|
<div class="el-upload-text hover:bg-[--el-color-primary-light-9]">
|
||||||
|
<el-icon size="16"><Upload /></el-icon>
|
||||||
|
<span>上传文件</span>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
<div style="margin-top: 6px">
|
||||||
|
<div
|
||||||
|
class="template-file text-#555 m-t-2px rounded-6px dark:text-#CFD3DC hover:bg-[--el-color-primary-light-9]"
|
||||||
|
v-for="item in fileList"
|
||||||
|
:key="item.url"
|
||||||
|
>
|
||||||
|
<el-icon size="16" style="margin-right: 5px"><Link /></el-icon>
|
||||||
|
<el-tooltip :content="item.name" placement="top">
|
||||||
|
<div class="document-name hover:text-[--el-color-primary]">{{ item.name }}</div>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-icon class="hover:text-[--el-color-primary]" v-if="!props.disabled" size="16" @click="handleRemove(item.url)">
|
||||||
|
<Close />
|
||||||
|
</el-icon>
|
||||||
|
<!-- 默认不显示下载 -->
|
||||||
|
<el-icon
|
||||||
|
v-if="isDownload"
|
||||||
|
class="p-l-5px hover:text-[--el-color-primary]"
|
||||||
|
size="16"
|
||||||
|
@click="handleDownLoad(item.url, item.name)"
|
||||||
|
><Download
|
||||||
|
/></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="file-tips">
|
||||||
|
<slot name="tip">
|
||||||
|
支持{{ acceptTypes }};
|
||||||
|
<div class="h-20px"></div>
|
||||||
|
文件大小不能超过{{ props.fileSize }}M;最多上传{{ props.limit }}个;
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch, inject } from "vue";
|
||||||
|
import { ElLoading, formContextKey, formItemContextKey } from "element-plus";
|
||||||
|
import { koiNoticeSuccess, koiNoticeError, koiMsgWarning, koiMsgError } from "@/utils/koi.ts";
|
||||||
|
import koi from "@/utils/axios.ts";
|
||||||
|
|
||||||
|
const emits = defineEmits(["fileSuccess", "fileRemove", "update:fileList"]);
|
||||||
|
interface IUploadFilesProps {
|
||||||
|
acceptType?: string; // 上传文件类型
|
||||||
|
acceptTypes?: string; // 描述 - 上传文件类型
|
||||||
|
isMultiple?: boolean; // 是否可批量上传
|
||||||
|
limit?: number; // 允许上传文件的最大数量
|
||||||
|
disabled?: boolean; // 是否禁用上传
|
||||||
|
fileSize?: number; // 文件大小
|
||||||
|
action?: string;
|
||||||
|
fileList?: any; // 回显的文件
|
||||||
|
isDownload?: boolean; // 是否可以下载
|
||||||
|
folderName?: string; // 后端文件夹名称
|
||||||
|
fileParam?: string; // 文件类型[可向后端传递参数]
|
||||||
|
}
|
||||||
|
// 接收父组件传递过来的参数
|
||||||
|
const props = withDefaults(defineProps<IUploadFilesProps>(), {
|
||||||
|
acceptType: ".png,.jpg,.jpeg,.webp,.gif,.mp3,.mp4,.xls,.xlsx,.pdf,.log,.doc,.docx,.txt,.jar,.zip",
|
||||||
|
acceptTypes: "图片[png/jpg/webp/gif]、文件[txt/xls/xlsx]",
|
||||||
|
isMultiple: true,
|
||||||
|
limit: 1,
|
||||||
|
disabled: false,
|
||||||
|
fileSize: 10,
|
||||||
|
action: "/api/common/upload",
|
||||||
|
fileList: [],
|
||||||
|
isDownload: false,
|
||||||
|
folderName: "files",
|
||||||
|
fileParam: "-1"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取 el-form 组件上下文
|
||||||
|
const formContext = inject(formContextKey, void 0);
|
||||||
|
// 获取 el-form-item 组件上下文
|
||||||
|
const formItemContext = inject(formItemContextKey, void 0);
|
||||||
|
// 判断是否禁用上传和删除
|
||||||
|
const disabled = computed(() => {
|
||||||
|
return props.disabled || formContext?.disabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
let fileList = ref<any>([]);
|
||||||
|
// 父组件传递回显数据
|
||||||
|
fileList.value = props.fileList;
|
||||||
|
// 修改进行回显的时候使用
|
||||||
|
watch(
|
||||||
|
() => [props.fileList],
|
||||||
|
() => {
|
||||||
|
// 父组件传递回显数据
|
||||||
|
fileList.value = props.fileList;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleExceed = () => {
|
||||||
|
koiMsgWarning(`当前最多只能上传 ${props.limit} 个,请移除后上传!`);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 文件变化handleChange 这里监听上传文件的变化上传一个,执行一下后端上传单个文件请求方法。
|
||||||
|
*/
|
||||||
|
const handleChange = async (file: any) => {
|
||||||
|
// 防止多次执行change
|
||||||
|
const rawFile = file.raw;
|
||||||
|
const list = props.acceptTypes.split("/");
|
||||||
|
let acceptTypeList = list.map((item: string) => {
|
||||||
|
return getType(item);
|
||||||
|
});
|
||||||
|
// 如果要检索的字符串值没有出现,则该方法返回 -1
|
||||||
|
const isString = acceptTypeList.filter((item: string) => {
|
||||||
|
return rawFile.type.indexOf(item) > -1;
|
||||||
|
});
|
||||||
|
// 用于校验是否符合上传条件
|
||||||
|
const type = props.acceptTypes.replace("/", ", ");
|
||||||
|
if (isString.length < 1) {
|
||||||
|
koiMsgWarning(`仅支持格式为${type}的文件`);
|
||||||
|
return false;
|
||||||
|
} else if (rawFile.size / 1024 / 1024 > props.fileSize) {
|
||||||
|
koiMsgWarning(`文件大小不能超过${props.fileSize}MB!`);
|
||||||
|
const arr = [...fileList.value];
|
||||||
|
fileList.value = arr.filter((item: any) => {
|
||||||
|
return item.uid != rawFile.uid;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("file", rawFile);
|
||||||
|
formData.append("fileType", "2");
|
||||||
|
const loadingInstance = ElLoading.service({
|
||||||
|
text: "正在上传",
|
||||||
|
background: "rgba(0,0,0,.2)"
|
||||||
|
});
|
||||||
|
// 上传到服务器上面
|
||||||
|
const requestURL: string = props.action;
|
||||||
|
if (props.fileParam == "-1" || props.fileParam == "") {
|
||||||
|
props.fileParam === "-1";
|
||||||
|
}
|
||||||
|
// 文件上传
|
||||||
|
koi
|
||||||
|
.post(requestURL + "/" + props.fileSize + "/" + props.folderName + "/" + props.fileParam, formData)
|
||||||
|
.then((res: any) => {
|
||||||
|
loadingInstance.close();
|
||||||
|
let fileMap = res.data;
|
||||||
|
fileList.value.push({
|
||||||
|
name: rawFile.name,
|
||||||
|
url: fileMap.fullurl
|
||||||
|
});
|
||||||
|
emits("update:fileList", fileList.value);
|
||||||
|
emits("fileSuccess", fileMap);
|
||||||
|
// 调用 el-form 内部的校验方法[可自动校验]
|
||||||
|
formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
|
||||||
|
koiNoticeSuccess("文件上传成功🌻");
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("文件上传", error);
|
||||||
|
// 移除失败的文件
|
||||||
|
const arr = [...fileList.value];
|
||||||
|
fileList.value = arr.filter((item: any) => {
|
||||||
|
return item.uid != rawFile.uid;
|
||||||
|
});
|
||||||
|
emits("update:fileList", fileList.value);
|
||||||
|
loadingInstance.close();
|
||||||
|
koiNoticeError("上传失败,亲,您的文件不支持上传🌻");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 校验上传文件格式
|
||||||
|
// const getType = (acceptType: string) => {
|
||||||
|
// let val = "";
|
||||||
|
// switch (acceptType) {
|
||||||
|
// case "xls":
|
||||||
|
// val = "excel";
|
||||||
|
// break;
|
||||||
|
// case "doc":
|
||||||
|
// val = "word";
|
||||||
|
// break;
|
||||||
|
// case "pdf":
|
||||||
|
// val = "pdf";
|
||||||
|
// break;
|
||||||
|
// case "zip":
|
||||||
|
// val = "zip";
|
||||||
|
// break;
|
||||||
|
// case "xlsx":
|
||||||
|
// val = "sheet";
|
||||||
|
// break;
|
||||||
|
// case "pptx":
|
||||||
|
// val = "presentation";
|
||||||
|
// break;
|
||||||
|
// case "docx":
|
||||||
|
// val = "document";
|
||||||
|
// break;
|
||||||
|
// case "text":
|
||||||
|
// val = "text";
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// return val;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 文件类型映射表
|
||||||
|
const fileTypeMap: any = {
|
||||||
|
xls: "excel",
|
||||||
|
xlsx: "sheet",
|
||||||
|
doc: "word",
|
||||||
|
docx: "document",
|
||||||
|
pdf: "pdf",
|
||||||
|
zip: "zip",
|
||||||
|
pptx: "presentation",
|
||||||
|
text: "text",
|
||||||
|
log: "text",
|
||||||
|
png: "image/png",
|
||||||
|
jpg: "image/jpeg",
|
||||||
|
jpeg: "image/jpeg",
|
||||||
|
gif: "image/gif",
|
||||||
|
svg: "image/svg+xml",
|
||||||
|
mp3: "audio/mpeg",
|
||||||
|
wav: "audio/wav",
|
||||||
|
ogg: "audio/ogg",
|
||||||
|
mp4: "video/mp4",
|
||||||
|
avi: "video/x-msvideo",
|
||||||
|
mov: "video/quicktime",
|
||||||
|
webm: "video/webm",
|
||||||
|
json: "application/json",
|
||||||
|
xml: "application/xml",
|
||||||
|
yaml: "application/yaml",
|
||||||
|
js: "application/javascript",
|
||||||
|
css: "text/css",
|
||||||
|
html: "text/html",
|
||||||
|
txt: "text/plain",
|
||||||
|
csv: "text/csv",
|
||||||
|
md: "text/markdown",
|
||||||
|
sql: "application/sql",
|
||||||
|
sh: "application/x-sh",
|
||||||
|
py: "text/x-python",
|
||||||
|
rb: "text/x-ruby",
|
||||||
|
java: "text/x-java",
|
||||||
|
c: "text/x-csrc",
|
||||||
|
h: "text/x-chdr",
|
||||||
|
cpp: "text/x-c++src",
|
||||||
|
hpp: "text/x-c++hdr",
|
||||||
|
ts: "application/typescript",
|
||||||
|
sass: "text/x-sass",
|
||||||
|
scss: "text/x-scss",
|
||||||
|
less: "text/x-less"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 校验上传文件格式
|
||||||
|
const getType = (acceptType: string) => {
|
||||||
|
const lowerCaseExt = acceptType.toLowerCase();
|
||||||
|
return fileTypeMap[lowerCaseExt] || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除文件
|
||||||
|
const handleRemove = (url: string) => {
|
||||||
|
fileList.value = fileList.value.filter((item: any) => item.url !== url);
|
||||||
|
emits("update:fileList", fileList.value);
|
||||||
|
emits("fileRemove", url);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
const handleDownLoad = async (url: string, name: string) => {
|
||||||
|
if (!url && !name) {
|
||||||
|
koiMsgError("文件获取失败,请刷新重试🌻");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
koiMsgError("网络异常,请刷新重试🌻");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 创建 Blob 对象
|
||||||
|
const blob = await response.blob();
|
||||||
|
// 创建对象 URL
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob);
|
||||||
|
// 创建一个隐藏的下载链接
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = downloadUrl;
|
||||||
|
link.download = name; // 设置下载文件名
|
||||||
|
link.style.display = "none";
|
||||||
|
// 添加到 DOM 中
|
||||||
|
document.body.appendChild(link);
|
||||||
|
// 触发点击事件
|
||||||
|
link.click();
|
||||||
|
// 清理
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(downloadUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("下载失败:", error);
|
||||||
|
koiNoticeError("下载失败,请刷新重试🌻");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-upload-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 106px;
|
||||||
|
height: 32px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
|
||||||
|
/* 设置用户禁止选中 */
|
||||||
|
user-select: none;
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
span {
|
||||||
|
padding-left: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.template-file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
max-width: 360px;
|
||||||
|
.document-name {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 16px;
|
||||||
|
height: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
line-clamp: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file-tips {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
334
src/components/KoiUpload/Image.vue
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
<template>
|
||||||
|
<div class="upload-box">
|
||||||
|
<el-upload
|
||||||
|
:id="uuid"
|
||||||
|
action="#"
|
||||||
|
:class="['upload', imageDisabled ? 'disabled' : '', drag ? 'no-border' : '']"
|
||||||
|
:multiple="false"
|
||||||
|
:disabled="imageDisabled"
|
||||||
|
:show-file-list="false"
|
||||||
|
:http-request="handleHttpUpload"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:on-success="uploadSuccess"
|
||||||
|
:on-error="uploadError"
|
||||||
|
:drag="drag"
|
||||||
|
:accept="fileType.join(',')"
|
||||||
|
:folderName="folderName"
|
||||||
|
:fileParam="fileParam"
|
||||||
|
>
|
||||||
|
<template v-if="imageUrl">
|
||||||
|
<img :src="imageUrl" class="upload-image" />
|
||||||
|
<div class="upload-operate" @click.stop>
|
||||||
|
<div v-if="!imageDisabled" class="upload-icon" @click="handleEditImage">
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
<span>编辑</span>
|
||||||
|
</div>
|
||||||
|
<div class="upload-icon" @click="imageViewShow = true">
|
||||||
|
<el-icon><ZoomIn /></el-icon>
|
||||||
|
<span>查看</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!imageDisabled" class="upload-icon" @click="handleDeleteImage">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
<span>删除</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="upload-content">
|
||||||
|
<slot name="content">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
<!-- <span>请上传图片</span> -->
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<div class="upload-tip">
|
||||||
|
<slot name="tip"></slot>
|
||||||
|
</div>
|
||||||
|
<el-image-viewer v-if="imageViewShow" :url-list="[imageUrl]" @close="imageViewShow = false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="KoiUploadImage">
|
||||||
|
import {ref, computed, inject, watch} from "vue";
|
||||||
|
import { ElLoading } from "element-plus";
|
||||||
|
import { generateUUID } from "@/utils";
|
||||||
|
import koi from "@/utils/axios.ts";
|
||||||
|
import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
|
||||||
|
import type { UploadProps, UploadRequestOptions } from "element-plus";
|
||||||
|
|
||||||
|
interface IUploadImageProps {
|
||||||
|
imageUrl: string; // 图片地址 ==> 必传
|
||||||
|
action?: string; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
|
||||||
|
drag?: boolean; // 是否支持拖拽上传 ==> 非必传[默认为 true]
|
||||||
|
disabled?: boolean; // 是否禁用上传组件 ==> 非必传[默认为 false]
|
||||||
|
fileSize?: number; // 图片大小限制 ==> 非必传[默认为 3M]
|
||||||
|
fileType?: any; // 图片类型限制 ==> 非必传[默认为 ["image/webp","image/jpg", "image/jpeg", "image/png", "image/gif"]]
|
||||||
|
height?: string; // 组件高度 ==> 非必传[默认为 120px]
|
||||||
|
width?: string; // 组件宽度 ==> 非必传[默认为 120px]
|
||||||
|
borderRadius?: string; // 组件边框圆角 ==> 非必传[默认为 6px]
|
||||||
|
folderName?: string; // 后端文件夹名称
|
||||||
|
fileParam?: string; // 文件类型[可向后端传递参数]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收父组件参数
|
||||||
|
const props = withDefaults(defineProps<IUploadImageProps>(), {
|
||||||
|
imageUrl: "",
|
||||||
|
action: "/api/common/upload",
|
||||||
|
drag: true,
|
||||||
|
disabled: false,
|
||||||
|
fileSize: 3,
|
||||||
|
fileType: () => ["image/webp", "image/jpg", "image/jpeg", "image/png", "image/gif"],
|
||||||
|
height: "120px",
|
||||||
|
width: "120px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
folderName: "files",
|
||||||
|
fileParam: "-1"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成组件唯一id
|
||||||
|
const uuid = ref("id-" + generateUUID());
|
||||||
|
|
||||||
|
// 查看图片
|
||||||
|
const imageViewShow = ref(false);
|
||||||
|
// 获取 el-form 组件上下文
|
||||||
|
const formContext = inject(formContextKey, void 0);
|
||||||
|
// 获取 el-form-item 组件上下文
|
||||||
|
const formItemContext = inject(formItemContextKey, void 0);
|
||||||
|
// 判断是否禁用上传和删除
|
||||||
|
const imageDisabled = computed(() => {
|
||||||
|
return props.disabled || formContext?.disabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
let imageUrl = ref();
|
||||||
|
// 父组件传递回显数据
|
||||||
|
imageUrl.value = props.imageUrl;
|
||||||
|
// 修改进行回显的时候使用
|
||||||
|
watch(
|
||||||
|
() => [props.imageUrl],
|
||||||
|
() => {
|
||||||
|
// 父组件传递回显数据
|
||||||
|
imageUrl.value = props.imageUrl;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
/**
|
||||||
|
* @description 图片上传
|
||||||
|
* @param options upload 所有配置项
|
||||||
|
* */
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:imageUrl": [value: string];
|
||||||
|
}>();
|
||||||
|
const handleHttpUpload = async (options: UploadRequestOptions) => {
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("file", options.file);
|
||||||
|
const loadingInstance = ElLoading.service({
|
||||||
|
text: "正在上传",
|
||||||
|
background: "rgba(0,0,0,.2)"
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
if (props.fileParam == "-1" || props.fileParam == "") {
|
||||||
|
props.fileParam === "-1";
|
||||||
|
}
|
||||||
|
const res: any = await koi.post(props.action + "/" + props.fileSize + "/" + props.folderName + "/" + props.fileParam, formData);
|
||||||
|
console.log(res);
|
||||||
|
imageUrl.value=res.data.fullurl;
|
||||||
|
emit("update:imageUrl", res.data.fullurl);
|
||||||
|
loadingInstance.close();
|
||||||
|
// 调用 el-form 内部的校验方法[可自动校验]
|
||||||
|
formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
|
||||||
|
} catch (error) {
|
||||||
|
loadingInstance.close();
|
||||||
|
options.onError(error as any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 删除图片
|
||||||
|
* */
|
||||||
|
const handleDeleteImage = () => {
|
||||||
|
imageUrl.value = "";
|
||||||
|
emit("update:imageUrl", "");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 编辑图片
|
||||||
|
* */
|
||||||
|
const handleEditImage = () => {
|
||||||
|
const dom = document.querySelector(`#${uuid.value} .el-upload__input`);
|
||||||
|
dom && dom.dispatchEvent(new MouseEvent("click"));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 文件上传之前判断
|
||||||
|
* @param rawFile 选择的文件
|
||||||
|
* */
|
||||||
|
const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
|
||||||
|
const imgSize = rawFile.size / 1024 / 1024 < props.fileSize;
|
||||||
|
const imgType = props.fileType.includes(rawFile.type);
|
||||||
|
if (!imgType)
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: "上传图片不符合所需的格式!",
|
||||||
|
type: "warning"
|
||||||
|
});
|
||||||
|
if (!imgSize)
|
||||||
|
setTimeout(() => {
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: `上传图片大小不能超过 ${props.fileSize}M!`,
|
||||||
|
type: "warning"
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
return imgType && imgSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 图片上传成功
|
||||||
|
* */
|
||||||
|
const uploadSuccess = () => {
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: "图片上传成功!",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 图片上传错误
|
||||||
|
* */
|
||||||
|
const uploadError = () => {
|
||||||
|
ElNotification({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: "图片上传失败,请您重新上传!",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.is-error {
|
||||||
|
.upload {
|
||||||
|
:deep(.el-upload),
|
||||||
|
:deep(.el-upload-dragger) {
|
||||||
|
border: 2px dashed var(--el-color-danger) !important;
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--el-color-primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.disabled) {
|
||||||
|
.el-upload,
|
||||||
|
.el-upload-dragger {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
border: 2px dashed var(--el-color-primary) !important;
|
||||||
|
&:hover {
|
||||||
|
border: 2px dashed var(--el-color-primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.upload-box {
|
||||||
|
.no-border {
|
||||||
|
:deep(.el-upload) {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.upload) {
|
||||||
|
.el-upload {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: v-bind(width);
|
||||||
|
height: v-bind(height);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
border-radius: v-bind(borderRadius);
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
.upload-operate {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-upload-dragger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
border-radius: v-bind(borderRadius);
|
||||||
|
&:hover {
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-upload-dragger.is-dragover {
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
border: 2px dashed var(--el-color-primary) !important;
|
||||||
|
}
|
||||||
|
.upload-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.upload-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 30px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
.el-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.upload-operate {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgb(0 0 0 / 50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
.upload-icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 6%;
|
||||||
|
color: var(--el-color-primary-light-2);
|
||||||
|
.el-icon {
|
||||||
|
margin-bottom: 40%;
|
||||||
|
font-size: 130%;
|
||||||
|
line-height: 130%;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: 85%;
|
||||||
|
line-height: 85%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.upload-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 26px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
321
src/components/KoiUpload/Images.vue
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
<template>
|
||||||
|
<div class="upload-box">
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="_fileList"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
:class="['upload', imageDisabled ? 'disabled' : '', drag ? 'no-border' : '']"
|
||||||
|
:multiple="true"
|
||||||
|
:disabled="imageDisabled"
|
||||||
|
:limit="limit"
|
||||||
|
:http-request="handleHttpUpload"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
:on-success="uploadSuccess"
|
||||||
|
:on-error="uploadError"
|
||||||
|
:drag="drag"
|
||||||
|
:accept="fileType.join(',')"
|
||||||
|
:folderName="folderName"
|
||||||
|
:fileParam="fileParam"
|
||||||
|
>
|
||||||
|
<div class="upload-content">
|
||||||
|
<slot name="content">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
<!-- <span>请上传图片</span> -->
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<template #file="{ file }">
|
||||||
|
<img :src="file.url" class="upload-image" />
|
||||||
|
<div class="upload-operator" @click.stop>
|
||||||
|
<div class="upload-icon" @click="handlePictureCardPreview(file)">
|
||||||
|
<el-icon><ZoomIn /></el-icon>
|
||||||
|
<span>查看</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!imageDisabled" class="upload-icon" @click="handleRemove(file)">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
<span>删除</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<div class="el-upload-tip">
|
||||||
|
<slot name="tip"></slot>
|
||||||
|
</div>
|
||||||
|
<el-image-viewer v-if="imgViewVisible" :url-list="[viewImageUrl]" @close="imgViewVisible = false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="KoiUploadImages">
|
||||||
|
import { ref, computed, inject, watch } from "vue";
|
||||||
|
import { ElLoading } from "element-plus";
|
||||||
|
import koi from "@/utils/axios.ts";
|
||||||
|
import { koiNoticeSuccess, koiNoticeWarning, koiNoticeError } from "@/utils/koi.ts";
|
||||||
|
import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus";
|
||||||
|
import { formContextKey, formItemContextKey } from "element-plus";
|
||||||
|
|
||||||
|
interface IUploadImagesProps {
|
||||||
|
fileList: UploadUserFile[]; // 图片回显,这个名称不能进行修改。
|
||||||
|
action?: any; // 上传图片的 action 方法,一般项目上传都是同一个 action 方法,在组件里直接引入即可 ==> 非必传
|
||||||
|
drag?: boolean; // 是否支持拖拽上传 ==> 非必传[默认为 true]
|
||||||
|
disabled?: boolean; // 是否禁用上传组件 ==> 非必传[默认为 false]
|
||||||
|
limit?: number; // 最大图片上传数 ==> 非必传[默认为 5张]
|
||||||
|
fileSize?: number; // 图片大小限制 ==> 非必传[默认为 3M]
|
||||||
|
fileType?: any; // 图片类型限制 ==> 非必传[默认为 ["image/webp", "image/jpg", "image/jpeg", "image/png", "image/gif"]]
|
||||||
|
height?: string; // 组件高度 ==> 非必传[默认为 120px]
|
||||||
|
width?: string; // 组件宽度 ==> 非必传[默认为 120px]
|
||||||
|
borderRadius?: string; // 组件边框圆角 ==> 非必传[默认为 6px]
|
||||||
|
folderName?: string; // 文件夹名称
|
||||||
|
fileParam?: string; // 文件类型[可向后端传递参数]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<IUploadImagesProps>(), {
|
||||||
|
fileList: () => [],
|
||||||
|
action: "/koi/file/uploadFile",
|
||||||
|
drag: true,
|
||||||
|
disabled: false,
|
||||||
|
limit: 5,
|
||||||
|
fileSize: 3,
|
||||||
|
fileType: ["image/webp", "image/jpg", "image/jpeg", "image/png", "image/gif"],
|
||||||
|
height: "120px",
|
||||||
|
width: "120px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
folderName: "files",
|
||||||
|
fileParam: "-1"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取 el-form 组件上下文
|
||||||
|
const formContext = inject(formContextKey, void 0);
|
||||||
|
// 获取 el-form-item 组件上下文
|
||||||
|
const formItemContext = inject(formItemContextKey, void 0);
|
||||||
|
// 判断是否禁用上传和删除
|
||||||
|
const imageDisabled = computed(() => {
|
||||||
|
return props.disabled || formContext?.disabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
const _fileList = ref<UploadUserFile[]>(props.fileList);
|
||||||
|
|
||||||
|
// 监听 props.fileList 列表默认值改变
|
||||||
|
watch(
|
||||||
|
() => props.fileList,
|
||||||
|
(n: UploadUserFile[]) => {
|
||||||
|
console.log("props.fileList", props.fileList);
|
||||||
|
_fileList.value = n;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 文件上传之前判断
|
||||||
|
* @param rawFile 选择的文件
|
||||||
|
* */
|
||||||
|
const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
|
||||||
|
const imgSize = rawFile.size / 1024 / 1024 < props.fileSize;
|
||||||
|
const imgType = props.fileType.includes(rawFile.type);
|
||||||
|
if (!imgType) koiNoticeWarning("上传图片不符合所需的格式🌻");
|
||||||
|
if (!imgSize)
|
||||||
|
setTimeout(() => {
|
||||||
|
koiNoticeWarning(`上传图片大小不能超过 ${props.fileSize}M!`);
|
||||||
|
}, 0);
|
||||||
|
return imgType && imgSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 图片上传
|
||||||
|
* @param options upload 所有配置项
|
||||||
|
* */
|
||||||
|
const handleHttpUpload = async (options: UploadRequestOptions) => {
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("file", options.file);
|
||||||
|
const loadingInstance = ElLoading.service({
|
||||||
|
text: "正在上传",
|
||||||
|
background: "rgba(0,0,0,.2)"
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
if (props.fileParam == "-1" || props.fileParam == "") {
|
||||||
|
props.fileParam === "-1";
|
||||||
|
}
|
||||||
|
const res: any = await koi.post(props.action + "/" + props.fileSize + "/" + props.folderName + "/" + props.fileParam, formData);
|
||||||
|
options.onSuccess(import.meta.env.VITE_SERVER + res.data.fileUploadPath);
|
||||||
|
loadingInstance.close();
|
||||||
|
} catch (error) {
|
||||||
|
loadingInstance.close();
|
||||||
|
options.onError(error as any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 图片上传成功
|
||||||
|
* @param response 上传响应结果
|
||||||
|
* @param uploadFile 上传的文件
|
||||||
|
* */
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:fileList": [value: UploadUserFile[]];
|
||||||
|
}>();
|
||||||
|
const uploadSuccess = (response: string | undefined, uploadFile: UploadFile) => {
|
||||||
|
if (!response) return;
|
||||||
|
uploadFile.url = response;
|
||||||
|
emit("update:fileList", _fileList.value);
|
||||||
|
// 调用 el-form 内部的校验方法[可自动校验]
|
||||||
|
formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
|
||||||
|
koiNoticeSuccess("图片上传成功🌻");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 删除图片
|
||||||
|
* @param file 删除的文件
|
||||||
|
* */
|
||||||
|
const handleRemove = (file: UploadFile) => {
|
||||||
|
_fileList.value = _fileList.value.filter(item => item.url !== file.url || item.name !== file.name);
|
||||||
|
emit("update:fileList", _fileList.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 图片上传错误
|
||||||
|
* */
|
||||||
|
const uploadError = () => {
|
||||||
|
koiNoticeError("图片上传失败,请您重新上传🌻");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 文件数超出
|
||||||
|
* */
|
||||||
|
const handleExceed = () => {
|
||||||
|
koiNoticeWarning(`当前最多只能上传 ${props.limit} 张图片,请移除后上传!`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 图片预览
|
||||||
|
* @param file 预览的文件
|
||||||
|
* */
|
||||||
|
const viewImageUrl = ref("");
|
||||||
|
const imgViewVisible = ref(false);
|
||||||
|
const handlePictureCardPreview: UploadProps["onPreview"] = file => {
|
||||||
|
viewImageUrl.value = file.url!;
|
||||||
|
imgViewVisible.value = true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.is-error {
|
||||||
|
.upload {
|
||||||
|
:deep(.el-upload--picture-card),
|
||||||
|
:deep(.el-upload-dragger) {
|
||||||
|
border: 2px dashed var(--el-color-danger) !important;
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--el-color-primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.disabled) {
|
||||||
|
.el-upload--picture-card,
|
||||||
|
.el-upload-dragger {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: var(--el-color-primary-light-9) !important;
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--el-color-primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.upload-box {
|
||||||
|
.no-border {
|
||||||
|
:deep(.el-upload--picture-card) {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.upload) {
|
||||||
|
.el-upload-dragger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
border-radius: v-bind(borderRadius);
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-upload-dragger.is-dragover {
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
border: 2px dashed var(--el-color-primary) !important;
|
||||||
|
}
|
||||||
|
.el-upload-list__item,
|
||||||
|
.el-upload--picture-card {
|
||||||
|
width: v-bind(width);
|
||||||
|
height: v-bind(height);
|
||||||
|
background-color: transparent;
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
border-radius: v-bind(borderRadius);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.upload-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.upload-operator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgb(0 0 0 / 50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
.upload-icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 6%;
|
||||||
|
color: var(--el-color-primary-light-2);
|
||||||
|
.el-icon {
|
||||||
|
margin-bottom: 15%;
|
||||||
|
font-size: 140%;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-upload-list__item {
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
&:hover {
|
||||||
|
.upload-operator {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.upload-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 30px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
.el-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-upload-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
60
src/components/MyComponent.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
<button @click="changeMessage">改变内容</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { changeContent } from '../utils/changeContent.js'; // 引入 JavaScript 文件
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
message: "初始内容"
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeMessage() {
|
||||||
|
this.message = "内容已被改变!"; // 直接在这里改变内容
|
||||||
|
},
|
||||||
|
handleChange() {
|
||||||
|
changeContent(this.changeMessage.bind(this)); // 传递处理函数
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadScripts([ // 加载外部脚本
|
||||||
|
'/public/static/js/jquery.min.js',
|
||||||
|
'/public/static/js/jquery.mobile.custom.min.js',
|
||||||
|
'/public/static/js/swiper.min.js',
|
||||||
|
'/public/static/js/jquery.rwdImageMaps.min.js',
|
||||||
|
'/public/static/js/bootstrap.min.js',
|
||||||
|
'/public/static/js/bootstrap-datetimepicker.min.js',
|
||||||
|
'/public/static/js/bootstrap-datetimepicker.zh-CN.js',
|
||||||
|
'/public/static/js/jquery.qrcode.min.js',
|
||||||
|
'/public/static/js/pinchzoom.min.js',
|
||||||
|
'/public/static/js/html2canvas.min.js',
|
||||||
|
'/public/static/js/index.js',
|
||||||
|
'/public/static/js/qfstatH5.js',
|
||||||
|
'/public/static/js/posterShare.js',
|
||||||
|
'/public/static/js/bigHaiBao.js'
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
loadScripts(urls) {
|
||||||
|
const loadScript = (url) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = url;
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.onload = resolve; // 确保脚本加载完成后再继续
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 依次加载所有脚本
|
||||||
|
urls.reduce((promise, url) => {
|
||||||
|
return promise.then(() => loadScript(url));
|
||||||
|
}, Promise.resolve());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
41
src/components/index.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import KoiSvgIcon from "./KoiSvgIcon/Index.vue";
|
||||||
|
import KoiDialog from "./KoiDialog/Index.vue";
|
||||||
|
import KoiDrawer from "./KoiDrawer/Index.vue";
|
||||||
|
import KoiToolbar from "./KoiToolbar/Index.vue";
|
||||||
|
import KoiTag from "./KoiTag/Index.vue";
|
||||||
|
import KoiSelectIcon from "./KoiSelectIcon/Index.vue";
|
||||||
|
import KoiUploadFiles from "./KoiUpload/Files.vue";
|
||||||
|
import KoiUploadImage from "./KoiUpload/Image.vue";
|
||||||
|
import KoiUploadImages from "./KoiUpload/Images.vue";
|
||||||
|
import KoiExcel from "./KoiExcel/Index.vue";
|
||||||
|
import KoiTagFilter from "./KoiTagFilter/Index.vue";
|
||||||
|
import KoiCard from "./KoiCard/Index.vue";
|
||||||
|
import KoiGlobalIcon from "./KoiGlobalIcon/Index.vue";
|
||||||
|
import KoiLoading from "./KoiLoading/Index.vue";
|
||||||
|
|
||||||
|
import type { App, Component } from "vue";
|
||||||
|
// 对外暴露插件对象,注册全局组件
|
||||||
|
const components: { [name: string]: Component } = {
|
||||||
|
KoiSvgIcon,
|
||||||
|
KoiDialog,
|
||||||
|
KoiDrawer,
|
||||||
|
KoiToolbar,
|
||||||
|
KoiTag,
|
||||||
|
KoiSelectIcon,
|
||||||
|
KoiUploadFiles,
|
||||||
|
KoiUploadImage,
|
||||||
|
KoiUploadImages,
|
||||||
|
KoiExcel,
|
||||||
|
KoiTagFilter,
|
||||||
|
KoiCard,
|
||||||
|
KoiGlobalIcon,
|
||||||
|
KoiLoading
|
||||||
|
};
|
||||||
|
export default {
|
||||||
|
// install方法, Object.keys()得到对象所有的key
|
||||||
|
install(app: App) {
|
||||||
|
Object.keys(components).forEach((key: string) => {
|
||||||
|
app.component(key, components[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
21
src/config/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// 全局默认配置项
|
||||||
|
// 首页地址[默认]
|
||||||
|
export const HOME_URL: string = "/home/index";
|
||||||
|
|
||||||
|
// 跳转子页面静态路由父级节点
|
||||||
|
export const STATIC_URL: string = "/system/static";
|
||||||
|
|
||||||
|
// 登录页地址[默认]
|
||||||
|
export const LOGIN_URL: string = "/login";
|
||||||
|
|
||||||
|
// pinia仓库前缀
|
||||||
|
export const PINIA_PREFIX: string = "koi-";
|
||||||
|
|
||||||
|
// Svg本地图片使用 koi- 开头才会生效
|
||||||
|
export const SVG_PREFIX: string = "koi-";
|
||||||
|
|
||||||
|
// 默认主题颜色
|
||||||
|
export const DEFAULT_THEME: string = "#2992FF";
|
||||||
|
|
||||||
|
// 路由白名单地址[本地存在的路由 staticRouter.ts 中]
|
||||||
|
export const ROUTER_WHITE_LIST: string[] = ["/500"];
|
83
src/config/theme.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
export const optimumHeaderTheme: any = {
|
||||||
|
light: {
|
||||||
|
"--el-header-optimum-hover-color": "var(--el-color-primary)",
|
||||||
|
"--el-header-optimum-active-color": "var(--el-color-primary)",
|
||||||
|
"--el-header-optimum-hover-bg-color": "#f4f4f5",
|
||||||
|
"--el-header-optimum-active-bg-color": "var(--el-color-primary-light-8)",
|
||||||
|
"--el-header-optimum-border-color": "var(--el-color-primary)"
|
||||||
|
},
|
||||||
|
inverted: {
|
||||||
|
"--el-header-optimum-hover-color": "var(--el-color-primary)",
|
||||||
|
"--el-header-optimum-active-color": "#ffffff",
|
||||||
|
"--el-header-optimum-hover-bg-color": "transparent",
|
||||||
|
"--el-header-optimum-active-bg-color": "var(--el-color-primary)",
|
||||||
|
"--el-header-optimum-border-color": "var(--el-color-primary)"
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
"--el-header-optimum-hover-color": "var(--el-color-primary)",
|
||||||
|
"--el-header-optimum-active-color": "var(--el-color-primary)",
|
||||||
|
"--el-header-optimum-hover-bg-color": "var(--el-color-primary-light-9)",
|
||||||
|
"--el-header-optimum-active-bg-color": "var(--el-color-primary-light-8)",
|
||||||
|
"--el-header-optimum-border-color": "var(--el-color-primary)"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const headerTheme: any = {
|
||||||
|
light: {
|
||||||
|
"--el-header-bg-color": "#ffffff",
|
||||||
|
"--el-header-text-color": "#303133",
|
||||||
|
"--el-header-text-color-regular": "#606266"
|
||||||
|
},
|
||||||
|
inverted: {
|
||||||
|
"--el-header-bg-color": "#141414",
|
||||||
|
"--el-header-text-color": "#E5EAF3",
|
||||||
|
"--el-header-text-color-regular": "#CFD3DC"
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
"--el-header-bg-color": "#141414",
|
||||||
|
"--el-header-text-color": "#E5EAF3",
|
||||||
|
"--el-header-text-color-regular": "#CFD3DC"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const asideTheme: any = {
|
||||||
|
light: {
|
||||||
|
"--el-aside-logo-text-color": "#303133"
|
||||||
|
},
|
||||||
|
inverted: {
|
||||||
|
"--el-aside-logo-text-color": "#E5EAF3"
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
"--el-aside-logo-text-color": "#E5EAF3"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const menuTheme: any = {
|
||||||
|
light: {
|
||||||
|
"--el-menu-bg-color": "#ffffff",
|
||||||
|
"--el-menu-hover-bg-color": "#f4f4f5",
|
||||||
|
"--el-menu-active-bg-color": "var(--el-color-primary-light-8)",
|
||||||
|
"--el-menu-text-color": "#333639",
|
||||||
|
"--el-menu-hover-text-color": "var(--el-color-primary)",
|
||||||
|
"--el-menu-active-text-color": "var(--el-color-primary)",
|
||||||
|
"--el-menu-border-left-color": "var(--el-color-primary)"
|
||||||
|
},
|
||||||
|
inverted: {
|
||||||
|
"--el-menu-bg-color": "#191A20",
|
||||||
|
"--el-menu-hover-bg-color": "#000000",
|
||||||
|
"--el-menu-active-bg-color": "var(--el-color-primary)",
|
||||||
|
"--el-menu-text-color": "#dBd8d8",
|
||||||
|
"--el-menu-hover-text-color": "#ffffff",
|
||||||
|
"--el-menu-active-text-color": "#ffffff",
|
||||||
|
"--el-menu-border-left-color": "transparent"
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
"--el-menu-bg-color": "#141414",
|
||||||
|
"--el-menu-hover-bg-color": "var(--el-color-primary-light-9)",
|
||||||
|
"--el-menu-active-bg-color": "var(--el-color-primary-light-8)",
|
||||||
|
"--el-menu-text-color": "#e5eAf3",
|
||||||
|
"--el-menu-hover-text-color": "var(--el-color-primary)",
|
||||||
|
"--el-menu-active-text-color": "var(--el-color-primary)",
|
||||||
|
"--el-menu-border-left-color": "var(--el-color-primary)"
|
||||||
|
}
|
||||||
|
};
|
35
src/directives/index.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { App } from "vue";
|
||||||
|
import auth from "./modules/auth";
|
||||||
|
import waterMarker from "./modules/waterMarker";
|
||||||
|
import draggable from "./modules/draggable";
|
||||||
|
import debounce from "./modules/debounce";
|
||||||
|
import debounceInput from "./modules/debounceInput";
|
||||||
|
import throttle from "./modules/throttle";
|
||||||
|
import throttleInput from "./modules/throttleInput";
|
||||||
|
import adaptive from "./modules/adaptive";
|
||||||
|
import adaptiveTree from "./modules/adaptiveTree";
|
||||||
|
import copy from "./modules/copy";
|
||||||
|
|
||||||
|
const directivesList: any = {
|
||||||
|
auth,
|
||||||
|
waterMarker,
|
||||||
|
draggable,
|
||||||
|
debounce,
|
||||||
|
debounceInput,
|
||||||
|
throttle,
|
||||||
|
throttleInput,
|
||||||
|
adaptive,
|
||||||
|
adaptiveTree,
|
||||||
|
copy
|
||||||
|
};
|
||||||
|
|
||||||
|
const directives = {
|
||||||
|
install: function (app: App<Element>) {
|
||||||
|
Object.keys(directivesList).forEach(key => {
|
||||||
|
// 注册所有自定义指令
|
||||||
|
app.directive(key, directivesList[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default directives;
|
39
src/directives/modules/adaptive.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Directive, DirectiveBinding } from "vue";
|
||||||
|
|
||||||
|
// 自适应表格高度
|
||||||
|
interface ExHTMLElement extends HTMLElement {
|
||||||
|
resizeListener: EventListener;
|
||||||
|
}
|
||||||
|
// 初始设置表格高度
|
||||||
|
const setTableHeight = (el: ExHTMLElement, binding: DirectiveBinding) => {
|
||||||
|
const top = el.offsetTop;
|
||||||
|
const bottom = binding.value && typeof binding.value.bottom !== "undefined" ? binding.value.bottom : 80;
|
||||||
|
const pageHeight = window.innerHeight;
|
||||||
|
el.style.height = `${pageHeight - top - bottom}px`;
|
||||||
|
el.style.overflowY = "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeDirective: Directive<ExHTMLElement> = {
|
||||||
|
mounted(el, binding) {
|
||||||
|
el.resizeListener = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTableHeight(el, binding);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setTableHeight(el, binding);
|
||||||
|
// 监听窗口大小变化事件
|
||||||
|
window.addEventListener("resize", el.resizeListener);
|
||||||
|
},
|
||||||
|
unmounted(el) {
|
||||||
|
window.removeEventListener("resize", el.resizeListener);
|
||||||
|
},
|
||||||
|
updated(el, binding) {
|
||||||
|
// 确保更新后重新设置表格高度
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTableHeight(el, binding);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resizeDirective;
|
38
src/directives/modules/adaptiveTree.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Directive, DirectiveBinding } from "vue";
|
||||||
|
|
||||||
|
// 自适应树形表格高度
|
||||||
|
interface ExHTMLElement extends HTMLElement {
|
||||||
|
resizeListener: EventListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTableHeight = (el: ExHTMLElement, binding: DirectiveBinding) => {
|
||||||
|
const top = el.offsetTop;
|
||||||
|
const bottom = binding?.value?.bottom || 28;
|
||||||
|
const pageHeight = window.innerHeight;
|
||||||
|
el.style.height = `${pageHeight - top - bottom}px`;
|
||||||
|
el.style.overflowY = "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeDirective: Directive<ExHTMLElement> = {
|
||||||
|
mounted(el, binding) {
|
||||||
|
el.resizeListener = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTableHeight(el, binding);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setTableHeight(el, binding);
|
||||||
|
|
||||||
|
window.addEventListener("resize", el.resizeListener);
|
||||||
|
},
|
||||||
|
unmounted(el) {
|
||||||
|
window.removeEventListener("resize", el.resizeListener);
|
||||||
|
},
|
||||||
|
updated(el, binding) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTableHeight(el, binding);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resizeDirective;
|
24
src/directives/modules/auth.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* v-auth
|
||||||
|
* 按钮权限指令
|
||||||
|
*/
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
|
||||||
|
const auth: Directive = {
|
||||||
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
const { value } = binding;
|
||||||
|
const userStore = useAuthStore();
|
||||||
|
const adminButtons = ["*"];
|
||||||
|
if (
|
||||||
|
(Array.isArray(value) && value.some((permission: string) => userStore.buttonList.includes(permission))) ||
|
||||||
|
JSON.stringify(userStore.buttonList) === JSON.stringify(adminButtons)
|
||||||
|
) {
|
||||||
|
// 如果用户拥有指定权限中的任何一个或者是管理员,则显示元素
|
||||||
|
} else {
|
||||||
|
el.parentNode?.removeChild(el); // 如果用户不拥有所有权限,则移除元素
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default auth;
|
31
src/directives/modules/copy.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Directive, DirectiveBinding } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
interface HTMLElementWithCopyData extends HTMLElement {
|
||||||
|
copyData: string | number;
|
||||||
|
handleClickEl: EventListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy: Directive = {
|
||||||
|
mounted(el: HTMLElementWithCopyData, binding: DirectiveBinding) {
|
||||||
|
el.copyData = binding.value as string | number;
|
||||||
|
el.handleClickEl = async function () {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(el.copyData.toString());
|
||||||
|
ElMessage.success("复制成功🌻");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("复制操作不被支持或失败: ", error);
|
||||||
|
ElMessage.error("复制失败🌻");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
el.addEventListener("click", el.handleClickEl);
|
||||||
|
},
|
||||||
|
updated(el: HTMLElementWithCopyData, binding: DirectiveBinding) {
|
||||||
|
el.copyData = binding.value as string | number;
|
||||||
|
},
|
||||||
|
beforeUnmount(el: HTMLElementWithCopyData) {
|
||||||
|
el.removeEventListener("click", el.handleClickEl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default copy;
|
35
src/directives/modules/debounce.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* v-debounce
|
||||||
|
* 按钮防抖指令
|
||||||
|
* 防抖就是在行为触发执行一次后,需静默等待N秒后才能再次执行,在此期间不能再触发行为,如果重复触发就计时归零后再次计时,直到用户静默到设定的阈值才再次执行。
|
||||||
|
* 简而言之就是触发后N秒内不许触发,直到N秒后才可以执行。
|
||||||
|
* <el-button type="primary" icon="search" plain v-debounce:500="handleSearch">搜索</el-button>
|
||||||
|
*/
|
||||||
|
// @ts-nocheck
|
||||||
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
interface ElType extends HTMLElement {
|
||||||
|
handleClick: () => any;
|
||||||
|
}
|
||||||
|
const debounce: Directive = {
|
||||||
|
mounted(el: ElType, binding: DirectiveBinding) {
|
||||||
|
if (typeof binding.value !== "function") {
|
||||||
|
throw "callback must be a function";
|
||||||
|
}
|
||||||
|
let timer = null;
|
||||||
|
const delay = parseInt(binding.arg) || 500;
|
||||||
|
const handler = binding.value;
|
||||||
|
|
||||||
|
el.addEventListener(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
handler();
|
||||||
|
}, delay);
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default debounce;
|
36
src/directives/modules/debounceInput.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* v-debounce
|
||||||
|
* 按钮防抖指令
|
||||||
|
* 防抖就是在行为触发执行一次后,需静默等待N秒后才能再次执行,在此期间不能再触发行为,如果重复触发就计时归零后再次计时,直到用户静默到设定的阈值才再次执行。
|
||||||
|
* 简而言之就是触发后N秒内不许触发,直到N秒后才可以执行。
|
||||||
|
* 接收参数:function类型
|
||||||
|
* <input v-debounceInput:500="debounceInput" />
|
||||||
|
*/
|
||||||
|
// @ts-nocheck
|
||||||
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
interface ElType extends HTMLElement {
|
||||||
|
handleClick: () => any;
|
||||||
|
}
|
||||||
|
const debounce: Directive = {
|
||||||
|
mounted(el: ElType, binding: DirectiveBinding) {
|
||||||
|
if (typeof binding.value !== "function") {
|
||||||
|
throw "callback must be a function";
|
||||||
|
}
|
||||||
|
let timer = null;
|
||||||
|
const delay = parseInt(binding.arg) || 500;
|
||||||
|
const handler = binding.value;
|
||||||
|
|
||||||
|
el.addEventListener(
|
||||||
|
"input",
|
||||||
|
() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
handler();
|
||||||
|
}, delay);
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default debounce;
|
49
src/directives/modules/draggable.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。
|
||||||
|
|
||||||
|
思路:
|
||||||
|
1、设置需要拖拽的元素为absolute,其父元素为relative。
|
||||||
|
2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
|
||||||
|
3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
|
||||||
|
4、鼠标松开(onmouseup)时完成一次拖拽
|
||||||
|
|
||||||
|
使用:在 Dom 上加上 v-draggable 即可
|
||||||
|
<div class="dialog-model" v-draggable></div>
|
||||||
|
*/
|
||||||
|
import type { Directive } from "vue";
|
||||||
|
interface ElType extends HTMLElement {
|
||||||
|
parentNode: any;
|
||||||
|
}
|
||||||
|
const draggable: Directive = {
|
||||||
|
mounted: function (el: ElType) {
|
||||||
|
el.style.cursor = "move";
|
||||||
|
el.style.position = "absolute";
|
||||||
|
el.onmousedown = function (e) {
|
||||||
|
let disX = e.pageX - el.offsetLeft;
|
||||||
|
let disY = e.pageY - el.offsetTop;
|
||||||
|
document.onmousemove = function (e) {
|
||||||
|
let x = e.pageX - disX;
|
||||||
|
let y = e.pageY - disY;
|
||||||
|
let maxX = el.parentNode.offsetWidth - el.offsetWidth;
|
||||||
|
let maxY = el.parentNode.offsetHeight - el.offsetHeight;
|
||||||
|
if (x < 0) {
|
||||||
|
x = 0;
|
||||||
|
} else if (x > maxX) {
|
||||||
|
x = maxX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y < 0) {
|
||||||
|
y = 0;
|
||||||
|
} else if (y > maxY) {
|
||||||
|
y = maxY;
|
||||||
|
}
|
||||||
|
el.style.left = x + "px";
|
||||||
|
el.style.top = y + "px";
|
||||||
|
};
|
||||||
|
document.onmouseup = function () {
|
||||||
|
document.onmousemove = document.onmouseup = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default draggable;
|
38
src/directives/modules/throttle.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。
|
||||||
|
节流就是N秒内只执行一次,不管在N秒内重复调用了多少次,也只执行一次。
|
||||||
|
思路:
|
||||||
|
1、第一次点击,立即调用方法并禁用按钮,等延迟结束再次激活按钮
|
||||||
|
2、将需要触发的方法绑定在指令上
|
||||||
|
|
||||||
|
使用:给 Dom 加上 v-throttle 及回调函数即可
|
||||||
|
<el-button type="danger" icon="refresh" plain v-throttle="resetSearch">重置</el-button>
|
||||||
|
*/
|
||||||
|
// @ts-nocheck
|
||||||
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
interface ElType extends HTMLElement {
|
||||||
|
handleClick: () => any;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
const throttle: Directive = {
|
||||||
|
mounted(el: ElType, binding: DirectiveBinding) {
|
||||||
|
let timer = null;
|
||||||
|
const delay = parseInt(binding.arg) || 500;
|
||||||
|
const handler = binding.value;
|
||||||
|
let lastExecTime = 0;
|
||||||
|
|
||||||
|
el.addEventListener(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastExecTime > delay) {
|
||||||
|
handler();
|
||||||
|
lastExecTime = now;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default throttle;
|
32
src/directives/modules/throttleInput.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
节流就是N秒内只执行一次,不管在N秒内重复调用了多少次,也只执行一次。
|
||||||
|
<input v-throttleInput="throttleClick">节流提交</input>
|
||||||
|
*/
|
||||||
|
// @ts-nocheck
|
||||||
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
interface ElType extends HTMLElement {
|
||||||
|
handleClick: () => any;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
const throttle: Directive = {
|
||||||
|
mounted(el: ElType, binding: DirectiveBinding) {
|
||||||
|
let timer = null;
|
||||||
|
const delay = parseInt(binding.arg) || 500;
|
||||||
|
const handler = binding.value;
|
||||||
|
let lastExecTime = 0;
|
||||||
|
|
||||||
|
el.addEventListener(
|
||||||
|
"input",
|
||||||
|
() => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastExecTime > delay) {
|
||||||
|
handler();
|
||||||
|
lastExecTime = now;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default throttle;
|
36
src/directives/modules/waterMarker.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
需求:给整个页面添加背景水印。
|
||||||
|
|
||||||
|
思路:
|
||||||
|
1、使用 canvas 特性生成 base64 格式的图片文件,设置其字体大小,颜色等。
|
||||||
|
2、将其设置为背景图片,从而实现页面或组件水印效果
|
||||||
|
|
||||||
|
使用:设置水印文案,颜色,字体大小即可
|
||||||
|
<div v-waterMarker="{text:'YU-ADMIN',textColor: '#D9D9D9'}"></div>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
const addWaterMarker: Directive = (str: string, parentNode: any, font: any, textColor: string) => {
|
||||||
|
// 水印文字,父元素,字体,文字颜色
|
||||||
|
let can: HTMLCanvasElement = document.createElement("canvas");
|
||||||
|
parentNode.appendChild(can);
|
||||||
|
can.width = 205;
|
||||||
|
can.height = 140;
|
||||||
|
can.style.display = "none";
|
||||||
|
let cans = can.getContext("2d") as CanvasRenderingContext2D;
|
||||||
|
cans.rotate((-20 * Math.PI) / 180);
|
||||||
|
cans.font = font || "20px 宋体";
|
||||||
|
cans.fillStyle = textColor || "rgba(180, 180, 180, 0.3)";
|
||||||
|
cans.textAlign = "left";
|
||||||
|
cans.textBaseline = "Middle" as CanvasTextBaseline;
|
||||||
|
cans.fillText(str, can.width / 10, can.height / 2);
|
||||||
|
parentNode.style.backgroundImage = "url(" + can.toDataURL("image/png") + ")";
|
||||||
|
};
|
||||||
|
|
||||||
|
const waterMarker = {
|
||||||
|
mounted(el: DirectiveBinding, binding: DirectiveBinding) {
|
||||||
|
addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default waterMarker;
|
17
src/hooks/dicts/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { reactive, onMounted } from "vue";
|
||||||
|
import { listDataByType } from "@/api/system/dict/data/index.ts";
|
||||||
|
|
||||||
|
export function useKoiDict(dictType: Array<string>) {
|
||||||
|
let koiDicts: any = reactive({});
|
||||||
|
onMounted(async () => {
|
||||||
|
if (dictType.length > 0) {
|
||||||
|
for (const type of dictType) {
|
||||||
|
const res: any = await listDataByType(type);
|
||||||
|
if (res.data != null) {
|
||||||
|
koiDicts[type] = res.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { koiDicts };
|
||||||
|
}
|
36
src/hooks/screen/index.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { reactive } from "vue";
|
||||||
|
import { useBreakpoints } from "@vueuse/core";
|
||||||
|
|
||||||
|
export const breakpointsEnum = {
|
||||||
|
xl: 1600,
|
||||||
|
lg: 1199,
|
||||||
|
md: 991,
|
||||||
|
sm: 767,
|
||||||
|
xs: 575
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScreenStore = () => {
|
||||||
|
const breakpoints = reactive(useBreakpoints(breakpointsEnum));
|
||||||
|
// 手机端
|
||||||
|
const isMobile = breakpoints.smaller("sm");
|
||||||
|
// pad端
|
||||||
|
const isPad = breakpoints.between("sm", "md");
|
||||||
|
// pc端
|
||||||
|
const isDesktop = breakpoints.greater("md");
|
||||||
|
// 登录页面一
|
||||||
|
const isScreen = breakpoints.smaller("lg");
|
||||||
|
return {
|
||||||
|
breakpoints,
|
||||||
|
isMobile,
|
||||||
|
isPad,
|
||||||
|
isDesktop,
|
||||||
|
isScreen
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用步骤
|
||||||
|
/** 适配移动端开始 */
|
||||||
|
// import { useScreenStore } from "@/hooks/screen/index.ts";
|
||||||
|
// 获取当前为[移动端、IPad、PC端]仓库,阔以使用 watchEffect(() => {}) 进行监听。
|
||||||
|
// const { isMobile } = useScreenStore();
|
||||||
|
/** 适配移动端结束 */
|
18
src/languages/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { createI18n } from "vue-i18n";
|
||||||
|
import { getBrowserLang } from "@/utils";
|
||||||
|
|
||||||
|
import zh from "./modules/zh";
|
||||||
|
import en from "./modules/en";
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
// Use Composition API, Set to false
|
||||||
|
allowComposition: true,
|
||||||
|
legacy: false,
|
||||||
|
locale: getBrowserLang(),
|
||||||
|
messages: {
|
||||||
|
zh,
|
||||||
|
en
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
40
src/languages/modules/en.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
export default {
|
||||||
|
home: {
|
||||||
|
welcome: "Welcome"
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
refresh: "Refresh",
|
||||||
|
maximize: "Maximize",
|
||||||
|
closeCurrent: "Close Current",
|
||||||
|
closeLeft: "Close Left",
|
||||||
|
closeRight: "Close Right",
|
||||||
|
closeOther: "Close Other",
|
||||||
|
closeAll: "Close All"
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
searchMenu: "Search menu",
|
||||||
|
componentSize: "Component size",
|
||||||
|
refreshCache: "Refresh cache",
|
||||||
|
lightMode: "Light mode",
|
||||||
|
darkMode: "Dark mode",
|
||||||
|
language: "Language translation",
|
||||||
|
fullScreen: "Full Screen",
|
||||||
|
exitFullScreen: "Exit Full Screen",
|
||||||
|
personalCenter: "Personal Center",
|
||||||
|
settings: "Settings",
|
||||||
|
logout: "Log out"
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
welcome: "Welcome to login",
|
||||||
|
platform: "Management platform",
|
||||||
|
description: "Maybe we just got lucky",
|
||||||
|
account: "Account password login",
|
||||||
|
loginName: "Please enter your username",
|
||||||
|
password: "Please enter password",
|
||||||
|
security: "Please enter the verification code",
|
||||||
|
blur: "I can't see it. Change it",
|
||||||
|
in: "Log in",
|
||||||
|
center: "Be logging in",
|
||||||
|
beianhao: "Website record number"
|
||||||
|
}
|
||||||
|
};
|
41
src/languages/modules/zh.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
export default {
|
||||||
|
home: {
|
||||||
|
welcome: "欢迎使用"
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
refresh: "重新刷新",
|
||||||
|
maximize: "全屏切换",
|
||||||
|
closeCurrent: "关闭当前",
|
||||||
|
closeLeft: "关闭左侧",
|
||||||
|
closeRight: "关闭右侧",
|
||||||
|
closeOther: "关闭其它",
|
||||||
|
closeAll: "关闭所有"
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
searchMenu: "搜索菜单",
|
||||||
|
componentSize: "组件大小",
|
||||||
|
refreshCache: "刷新缓存",
|
||||||
|
lightMode: "明亮模式",
|
||||||
|
darkMode: "暗黑模式",
|
||||||
|
language: "语言翻译",
|
||||||
|
fullScreen: "全屏",
|
||||||
|
exitFullScreen: "退出全屏",
|
||||||
|
settings: "设置",
|
||||||
|
personalCenter: "个人中心",
|
||||||
|
changePassword: "修改密码",
|
||||||
|
logout: "退出登录"
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
welcome: "欢迎登录",
|
||||||
|
platform: "管理平台",
|
||||||
|
description: "或许我们只是差点运气",
|
||||||
|
account: "账号密码登录",
|
||||||
|
loginName: "请输入用户名",
|
||||||
|
password: "请输入密码",
|
||||||
|
security: "请输入验证码",
|
||||||
|
blur: "看不清,换一张",
|
||||||
|
in: "登录",
|
||||||
|
center: "登录中",
|
||||||
|
beianhao: "网站备案号"
|
||||||
|
}
|
||||||
|
};
|
98
src/layouts/LayoutClassic/index.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="layout-container">
|
||||||
|
<el-header class="layout-header">
|
||||||
|
<Logo :layout="globalStore.layout"></Logo>
|
||||||
|
<Header class="header"></Header>
|
||||||
|
</el-header>
|
||||||
|
<el-container class="layout-container-aside">
|
||||||
|
<el-aside
|
||||||
|
class="layout-aside transition-all"
|
||||||
|
:style="{ width: !globalStore.isCollapse ? globalStore.menuWidth + 'px' : settings.asideMenuCollapseWidth }"
|
||||||
|
>
|
||||||
|
<el-scrollbar class="layout-scrollbar">
|
||||||
|
<!-- :unique-opened="true" 子菜单不能同时展开 -->
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse="globalStore.isCollapse"
|
||||||
|
:collapse-transition="false"
|
||||||
|
:uniqueOpened="globalStore.uniqueOpened"
|
||||||
|
:router="false"
|
||||||
|
:class="menuAnimate"
|
||||||
|
>
|
||||||
|
<AsideSubMenu :menuList="menuList"></AsideSubMenu>
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-aside>
|
||||||
|
<el-container class="flex flex-col">
|
||||||
|
<!-- 路由页面 -->
|
||||||
|
<Main></Main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import settings from "@/settings.ts";
|
||||||
|
import Logo from "@/layouts/components/Logo/index.vue";
|
||||||
|
import Header from "@/layouts/components/Header/index.vue";
|
||||||
|
import AsideSubMenu from "@/layouts/components/Menu/AsideSubMenu.vue";
|
||||||
|
import Main from "@/layouts/components/Main/index.vue";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
|
||||||
|
console.log("左侧动态路由", authStore.showMenuList);
|
||||||
|
// 动态绑定左侧菜单animate动画
|
||||||
|
const menuAnimate = ref(settings.menuAnimate);
|
||||||
|
const menuList = computed(() => authStore.showMenuList);
|
||||||
|
const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
|
||||||
|
// const menuHoverCollapse = ref(settings.asideMenuHoverCollapse);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.layout-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
.layout-container-aside {
|
||||||
|
overflow: hidden;
|
||||||
|
.layout-aside {
|
||||||
|
z-index: $layout-aside-z-index; // 左侧菜单层级
|
||||||
|
padding-right: $aside-menu-padding-right; // 左侧布局右边距[用于悬浮和选中更明显]
|
||||||
|
padding-left: $aside-menu-padding-left; // 左侧布局左边距[用于悬浮和选中更明显]
|
||||||
|
border-right: none;
|
||||||
|
box-shadow: $aside-menu-box-shadow; // 左侧布局右边框阴影
|
||||||
|
background-color: var(--el-menu-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layout-header {
|
||||||
|
display: flex;
|
||||||
|
height: $aside-header-height;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--el-header-bg-color);
|
||||||
|
.header {
|
||||||
|
flex: 1; // 剩余空间全占满
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layout-scrollbar {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - $aside-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除菜单右侧边框
|
||||||
|
.el-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/** 菜单悬浮折叠宽度 */
|
||||||
|
.el-menu--collapse {
|
||||||
|
width: calc(var(--el-menu-icon-width) + var(--el-menu-base-level-padding)) !important;
|
||||||
|
}
|
||||||
|
</style>
|
214
src/layouts/LayoutColumns/index.vue
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 分栏布局 -->
|
||||||
|
<el-container class="layout-container">
|
||||||
|
<div class="layout-column">
|
||||||
|
<el-scrollbar>
|
||||||
|
<div
|
||||||
|
v-for="item in menuList"
|
||||||
|
:key="item.path"
|
||||||
|
class="left-column"
|
||||||
|
:class="{
|
||||||
|
'is-active': columnActive === item.path || `/${columnActive.split('/')[1]}` === item.path
|
||||||
|
}"
|
||||||
|
@click="handleSubMenu(item)"
|
||||||
|
>
|
||||||
|
<el-icon v-if="item.meta.icon && item.meta.icon.indexOf(SVG_PREFIX) == -1">
|
||||||
|
<component :is="item.meta.icon"></component>
|
||||||
|
</el-icon>
|
||||||
|
<!-- 本地SVG -->
|
||||||
|
<el-icon v-if="item.meta.icon && item.meta.icon.indexOf(SVG_PREFIX) == 0">
|
||||||
|
<component is="KoiSvgIcon" :name="item.meta.icon"></component>
|
||||||
|
</el-icon>
|
||||||
|
<span class="title">{{ getLanguage(globalStore.language, item.meta?.title, item.meta?.enName) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
<el-aside
|
||||||
|
class="layout-aside transition-all"
|
||||||
|
:style="{ width: !globalStore.isCollapse ? globalStore.menuWidth + 'px' : settings.columnMenuCollapseWidth }"
|
||||||
|
v-if="subMenuList.length != 0"
|
||||||
|
>
|
||||||
|
<Logo :isCollapse="globalStore.isCollapse" :layout="globalStore.layout"></Logo>
|
||||||
|
<el-scrollbar class="layout-scrollbar">
|
||||||
|
<!-- :unique-opened="true" 子菜单不能同时展开 -->
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse="globalStore.isCollapse"
|
||||||
|
:collapse-transition="false"
|
||||||
|
:uniqueOpened="globalStore.uniqueOpened"
|
||||||
|
:router="false"
|
||||||
|
:class="menuAnimate"
|
||||||
|
>
|
||||||
|
<ColumnSubMenu :menuList="subMenuList"></ColumnSubMenu>
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-aside>
|
||||||
|
<el-container>
|
||||||
|
<el-header class="layout-header">
|
||||||
|
<Header></Header>
|
||||||
|
</el-header>
|
||||||
|
<!-- 路由页面 -->
|
||||||
|
<Main></Main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import settings from "@/settings.ts";
|
||||||
|
import { SVG_PREFIX } from "@/config/index.ts";
|
||||||
|
import Logo from "@/layouts/components/Logo/index.vue";
|
||||||
|
import Header from "@/layouts/components/Header/index.vue";
|
||||||
|
import ColumnSubMenu from "@/layouts/components/Menu/ColumnSubMenu.vue";
|
||||||
|
import Main from "@/layouts/components/Main/index.vue";
|
||||||
|
import { ref, computed, watch } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
import { getLanguage } from "@/utils/index.ts";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
|
||||||
|
console.log("双栏布局左侧动态路由", authStore.showMenuList);
|
||||||
|
// 动态绑定左侧菜单animate动画
|
||||||
|
const menuAnimate = ref(settings.menuAnimate);
|
||||||
|
// 隐藏静态路由中isHide == '1'的数据
|
||||||
|
const menuList = computed(() => authStore.showMenuList.filter((item: any) => item.meta.isHide == "1"));
|
||||||
|
|
||||||
|
// const menuHoverCollapse = ref(settings.columnMenuHoverCollapse);
|
||||||
|
|
||||||
|
const columnActive = ref("");
|
||||||
|
const subMenuList = ref([]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [menuList, route],
|
||||||
|
() => {
|
||||||
|
// 当前菜单没有数据直接 return
|
||||||
|
if (!menuList.value.length) return;
|
||||||
|
columnActive.value = route.path;
|
||||||
|
const menuItem = menuList.value.filter((item: any) => {
|
||||||
|
// 刷新浏览器,一级路由就会变成点击的二级路由,所以需要加上`/${route.path.split("/")[1]}` 进行获取,否则就没有默认选中的颜色。
|
||||||
|
return route.path === item.path || `/${route.path.split("/")[1]}` === item.path;
|
||||||
|
});
|
||||||
|
// 若获取的路由没有子菜单,则赋值为空。
|
||||||
|
if (!menuItem[0].children?.length) return (subMenuList.value = []);
|
||||||
|
// 若有子菜单则赋值给子菜单变量。
|
||||||
|
subMenuList.value = menuItem[0].children;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 点击加载子菜单数据
|
||||||
|
const handleSubMenu = (item: any) => {
|
||||||
|
columnActive.value = item.path;
|
||||||
|
if (item.children?.length) {
|
||||||
|
// 该一级菜单,若是有子菜单,就会给第二个分栏菜单赋值。
|
||||||
|
// router.push(item.path); // 加这个,点击最左侧菜单会重定向第一个子菜单。
|
||||||
|
subMenuList.value = item.children;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 若是没有子菜单,则给子菜单变量赋值为空,并且跳转路由。例如:首页。
|
||||||
|
subMenuList.value = [];
|
||||||
|
router.push(item.path);
|
||||||
|
};
|
||||||
|
const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 第一列样式
|
||||||
|
.layout-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
background-color: var(--el-menu-bg-color); // 用来做色弱模式
|
||||||
|
box-shadow: $column-menu-box-shadow; // 双栏最左侧右边框阴影
|
||||||
|
.left-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 70px;
|
||||||
|
min-height: 60px;
|
||||||
|
margin: 3px 6px 3px 4px;
|
||||||
|
color: var(--el-menu-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
// border: 1px solid var(--el-border-color);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
&:hover {
|
||||||
|
// color: var(--el-color-primary);
|
||||||
|
color: var(--el-menu-hover-text-color);
|
||||||
|
// background: var(--el-color-primary-light-9);
|
||||||
|
background: var(--el-menu-hover-bg-color);
|
||||||
|
// border: 2px dashed var(--el-color-primary);
|
||||||
|
border: 1px solid var(--el-menu-border-left-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
&.is-active {
|
||||||
|
// color: var(--el-color-primary);
|
||||||
|
color: var(--el-menu-hover-text-color);
|
||||||
|
// background: var(--el-color-primary-light-8);
|
||||||
|
background: var(--el-menu-active-bg-color);
|
||||||
|
// border: 2px dashed var(--el-color-primary);
|
||||||
|
border: 1px solid var(--el-menu-border-left-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.el-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: $aside-menu-font-weight;
|
||||||
|
line-height: 14px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layout-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
.layout-aside {
|
||||||
|
z-index: $layout-aside-z-index; // 左侧菜单层级
|
||||||
|
padding-right: $column-menu-padding-right; // 左侧布局右边距[用于悬浮和选中更明显]
|
||||||
|
padding-left: $column-menu-padding-left; // 左侧布局左边距[用于悬浮和选中更明显]
|
||||||
|
background-color: var(--el-menu-bg-color);
|
||||||
|
border-right: none;
|
||||||
|
box-shadow: $aside-menu-box-shadow; // 双栏左侧布局菜单右边框阴影
|
||||||
|
}
|
||||||
|
.layout-header {
|
||||||
|
height: $aside-header-height;
|
||||||
|
background-color: var(--el-header-bg-color);
|
||||||
|
}
|
||||||
|
.layout-main {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layout-scrollbar {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - $aside-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除菜单右侧边框
|
||||||
|
.el-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/** 菜单悬浮折叠宽度 */
|
||||||
|
.el-menu--collapse {
|
||||||
|
width: calc(var(--el-menu-icon-width) + var(--el-menu-base-level-padding)) !important;
|
||||||
|
}
|
||||||
|
</style>
|
94
src/layouts/LayoutHorizontal/index.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="layout-container">
|
||||||
|
<el-header class="layout-header">
|
||||||
|
<div class="flex flex-items-center">
|
||||||
|
<Logo :layout="globalStore.layout"></Logo>
|
||||||
|
<el-scrollbar>
|
||||||
|
<el-menu mode="horizontal" :default-active="activeMenu" :router="false" :class="menuAnimate">
|
||||||
|
<HorizontalSubMenu :menuList="menuList" />
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toolbar></Toolbar>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<el-main class="layout-main">
|
||||||
|
<Main></Main>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import settings from "@/settings.ts";
|
||||||
|
import Logo from "@/layouts/components/Logo/index.vue";
|
||||||
|
import Toolbar from "@/layouts/components/Header/components/Toolbar.vue";
|
||||||
|
import HorizontalSubMenu from "@/layouts/components/Menu/HorizontalSubMenu.vue";
|
||||||
|
import Main from "@/layouts/components/Main/index.vue";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
|
||||||
|
console.log("横向布局左侧动态路由", authStore.showMenuList);
|
||||||
|
// 动态绑定左侧菜单animate动画
|
||||||
|
const menuAnimate = ref(settings.menuAnimate);
|
||||||
|
const menuList = computed(() => authStore.showMenuList);
|
||||||
|
|
||||||
|
const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.layout-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
:deep(.layout-header) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 20px 0 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: $aside-header-height;
|
||||||
|
background-color: var(--el-header-bg-color);
|
||||||
|
user-select: none;
|
||||||
|
.el-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border-bottom: none;
|
||||||
|
.el-menu-item.is-active {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
.el-sub-menu.is-active {
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.is-active {
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
border-bottom-color: var(--el-color-primary);
|
||||||
|
&::before {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.el-sub-menu__title {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
border-bottom-color: var(--el-color-primary);
|
||||||
|
i {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layout-main {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 15px 0 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
82
src/layouts/LayoutMobile/index.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="layout-container">
|
||||||
|
<el-header class="layout-header flex flex-items-center flex-justify-between">
|
||||||
|
<div class="w-30px flex flex-items-center">
|
||||||
|
<KoiSvgIcon name="koi-mobile-menu" width="30" height="30" @click="mobileDrawer = true"></KoiSvgIcon>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-items-center h-100%">
|
||||||
|
<!-- 明亮/暗黑模式图标 -->
|
||||||
|
<Dark></Dark>
|
||||||
|
<!-- 头像 AND 下拉折叠 -->
|
||||||
|
<User></User>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<!-- 路由页面 -->
|
||||||
|
<Main></Main>
|
||||||
|
</el-container>
|
||||||
|
|
||||||
|
<!-- 左侧抽屉菜单 -->
|
||||||
|
<KoiMobileDrawer style="width: 220px" v-model="mobileDrawer" placement="left">
|
||||||
|
<div class="transition-all mobile-drawer">
|
||||||
|
<Logo layout="mobile"></Logo>
|
||||||
|
<el-scrollbar class="layout-scrollbar">
|
||||||
|
<!-- :unique-opened="true" 子菜单不能同时展开 -->
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse-transition="false"
|
||||||
|
:uniqueOpened="globalStore.uniqueOpened"
|
||||||
|
:router="false"
|
||||||
|
:class="menuAnimate"
|
||||||
|
>
|
||||||
|
<ColumnSubMenu :menuList="menuList"></ColumnSubMenu>
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</KoiMobileDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import settings from "@/settings.ts";
|
||||||
|
import KoiMobileDrawer from "@/components/KoiMobileDrawer/Index.vue";
|
||||||
|
import User from "@/layouts/components/Header/components/User.vue";
|
||||||
|
import Dark from "@/layouts/components/Header/components/Dark.vue";
|
||||||
|
import Logo from "@/layouts/components/Logo/index.vue";
|
||||||
|
import ColumnSubMenu from "@/layouts/components/Menu/ColumnSubMenu.vue";
|
||||||
|
import Main from "@/layouts/components/Main/index.vue";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
// 动态绑定左侧菜单animate动画
|
||||||
|
const menuAnimate = ref(settings.menuAnimate);
|
||||||
|
const menuList = computed(() => authStore.showMenuList);
|
||||||
|
const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
|
||||||
|
const mobileDrawer = ref(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mobile-drawer {
|
||||||
|
background-color: var(--el-menu-bg-color);
|
||||||
|
}
|
||||||
|
// 去除菜单右侧边框
|
||||||
|
.el-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.layout-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
.layout-header {
|
||||||
|
height: $aside-header-height;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--el-header-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layout-scrollbar {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - $aside-header-height);
|
||||||
|
}
|
||||||
|
</style>
|
228
src/layouts/LayoutOptimum/index.vue
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 分栏布局 -->
|
||||||
|
<el-container class="layout-container">
|
||||||
|
<el-aside
|
||||||
|
class="layout-aside transition-all"
|
||||||
|
:style="{ width: !globalStore.isCollapse ? globalStore.menuWidth + 'px' : settings.columnMenuCollapseWidth }"
|
||||||
|
v-if="subMenuList.length != 0"
|
||||||
|
>
|
||||||
|
<Logo :isCollapse="globalStore.isCollapse" :layout="globalStore.layout"></Logo>
|
||||||
|
<el-scrollbar class="layout-scrollbar">
|
||||||
|
<!-- :unique-opened="true" 子菜单不能同时展开 -->
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse="globalStore.isCollapse"
|
||||||
|
:collapse-transition="false"
|
||||||
|
:uniqueOpened="globalStore.uniqueOpened"
|
||||||
|
:router="false"
|
||||||
|
:class="menuAnimate"
|
||||||
|
>
|
||||||
|
<ColumnSubMenu :menuList="subMenuList"></ColumnSubMenu>
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-aside>
|
||||||
|
<el-container>
|
||||||
|
<el-header class="layout-header">
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<!-- 左侧菜单展开和折叠图标 -->
|
||||||
|
<Collapse></Collapse>
|
||||||
|
<div class="layout-row m-l-12px">
|
||||||
|
<el-scrollbar>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
<div
|
||||||
|
v-for="item in menuList"
|
||||||
|
:key="item.path"
|
||||||
|
class="left-row line-clamp-1"
|
||||||
|
:class="{
|
||||||
|
'is-active': columnActive === item.path || `/${columnActive.split('/')[1]}` === item.path
|
||||||
|
}"
|
||||||
|
@click="handleSubMenu(item)"
|
||||||
|
>
|
||||||
|
<KoiGlobalIcon v-if="item.meta.icon" :name="item.meta.icon" size="18"></KoiGlobalIcon>
|
||||||
|
<span class="title">{{ getLanguage(globalStore.language, item.meta?.title, item.meta?.enName) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<Toolbar></Toolbar>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<!-- 路由页面 -->
|
||||||
|
<Main></Main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import settings from "@/settings.ts";
|
||||||
|
import Logo from "@/layouts/components/Logo/index.vue";
|
||||||
|
import Collapse from "@/layouts/components/Header/components/Collapse.vue";
|
||||||
|
import Toolbar from "@/layouts/components/Header/components/Toolbar.vue";
|
||||||
|
import ColumnSubMenu from "@/layouts/components/Menu/ColumnSubMenu.vue";
|
||||||
|
import Main from "@/layouts/components/Main/index.vue";
|
||||||
|
import { ref, computed, watch } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
import { getLanguage } from "@/utils/index.ts";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
|
||||||
|
console.log("上左布局左侧动态路由", authStore.showMenuList);
|
||||||
|
// 动态绑定左侧菜单animate动画
|
||||||
|
const menuAnimate = ref(settings.menuAnimate);
|
||||||
|
// 隐藏静态路由中isHide == '1'的数据
|
||||||
|
const menuList = computed(() => authStore.showMenuList.filter((item: any) => item.meta.isHide == "1"));
|
||||||
|
|
||||||
|
const columnActive = ref("");
|
||||||
|
const subMenuList = ref([]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [menuList, route],
|
||||||
|
() => {
|
||||||
|
// 当前菜单没有数据直接 return
|
||||||
|
if (!menuList.value.length) return;
|
||||||
|
columnActive.value = route.path;
|
||||||
|
const menuItem = menuList.value.filter((item: any) => {
|
||||||
|
// 刷新浏览器,一级路由就会变成点击的二级路由,所以需要加上`/${route.path.split("/")[1]}` 进行获取,否则就没有默认选中的颜色。
|
||||||
|
return route.path === item.path || `/${route.path.split("/")[1]}` === item.path;
|
||||||
|
});
|
||||||
|
// 若获取的路由没有子菜单,则赋值为空。
|
||||||
|
if (!menuItem[0].children?.length) return (subMenuList.value = []);
|
||||||
|
// 若有子菜单则赋值给子菜单变量。
|
||||||
|
subMenuList.value = menuItem[0].children;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 点击加载子菜单数据
|
||||||
|
const handleSubMenu = (item: any) => {
|
||||||
|
columnActive.value = item.path;
|
||||||
|
if (item.children?.length) {
|
||||||
|
// 该一级菜单,若是有子菜单,就会给第二个分栏菜单赋值。
|
||||||
|
// router.push(item.path); // 加这个,点击最左侧菜单会重定向第一个子菜单。
|
||||||
|
subMenuList.value = item.children;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 若是没有子菜单,则给子菜单变量赋值为空,并且跳转路由。例如:首页。
|
||||||
|
subMenuList.value = [];
|
||||||
|
router.push(item.path);
|
||||||
|
};
|
||||||
|
const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
|
||||||
|
const rowMenuColor = computed(() => {
|
||||||
|
if (globalStore.asideInverted && globalStore.headerInverted) return "white";
|
||||||
|
if (globalStore.asideInverted) return "black";
|
||||||
|
if (globalStore.headerInverted) return "white";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: $aside-header-height;
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-row {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
background-color: var(--el-header-bg-color);
|
||||||
|
|
||||||
|
.left-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 220px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 6px 4px 6px 4px;
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #000;
|
||||||
|
@apply dark:text-#E5E3EA;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: v-bind(rowMenuColor);
|
||||||
|
.el-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: $aside-menu-font-weight;
|
||||||
|
line-height: 14px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
// color: var(--el-color-primary);
|
||||||
|
color: var(--el-header-optimum-hover-color);
|
||||||
|
// background: var(--el-color-primary-light-9);
|
||||||
|
// background: var(--el-menu-hover-bg-color);
|
||||||
|
background: var(--el-header-optimum-hover-bg-color);
|
||||||
|
// border: 2px dashed var(--el-color-primary);
|
||||||
|
border: 1px solid var(--el-header-optimum-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
&.is-active {
|
||||||
|
// color: var(--el-color-primary);
|
||||||
|
color: var(--el-header-optimum-active-color);
|
||||||
|
// background: var(--el-color-primary-light-8);
|
||||||
|
// background: var(--el-menu-active-bg-color);
|
||||||
|
background: var(--el-header-optimum-active-bg-color);
|
||||||
|
// border: 2px dashed var(--el-color-primary);
|
||||||
|
border: 1px solid var(--el-header-optimum-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
.layout-aside {
|
||||||
|
z-index: $layout-aside-z-index; // 左侧菜单层级
|
||||||
|
padding-right: $column-menu-padding-right; // 左侧布局右边距[用于悬浮和选中更明显]
|
||||||
|
padding-left: $column-menu-padding-left; // 左侧布局左边距[用于悬浮和选中更明显]
|
||||||
|
background-color: var(--el-menu-bg-color);
|
||||||
|
border-right: none;
|
||||||
|
box-shadow: $aside-menu-box-shadow; // 双栏左侧布局菜单右边框阴影
|
||||||
|
}
|
||||||
|
.layout-header {
|
||||||
|
height: $aside-header-height;
|
||||||
|
background-color: var(--el-header-bg-color);
|
||||||
|
}
|
||||||
|
.layout-main {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layout-scrollbar {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - $aside-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除菜单右侧边框
|
||||||
|
.el-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
</style>
|
87
src/layouts/LayoutVertical/index.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="layout-container">
|
||||||
|
<el-aside
|
||||||
|
class="layout-aside transition-all"
|
||||||
|
:style="{ width: !globalStore.isCollapse ? globalStore.menuWidth + 'px' : settings.asideMenuCollapseWidth }"
|
||||||
|
>
|
||||||
|
<Logo :isCollapse="globalStore.isCollapse" :layout="globalStore.layout"></Logo>
|
||||||
|
<el-scrollbar class="layout-scrollbar">
|
||||||
|
<!-- :unique-opened="true" 子菜单不能同时展开 -->
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse="globalStore.isCollapse"
|
||||||
|
:collapse-transition="false"
|
||||||
|
:uniqueOpened="globalStore.uniqueOpened"
|
||||||
|
:router="false"
|
||||||
|
:class="menuAnimate"
|
||||||
|
>
|
||||||
|
<AsideSubMenu :menuList="menuList"></AsideSubMenu>
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-aside>
|
||||||
|
<el-container>
|
||||||
|
<el-header class="layout-header">
|
||||||
|
<Header></Header>
|
||||||
|
</el-header>
|
||||||
|
<!-- 路由页面 -->
|
||||||
|
<Main></Main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import settings from "@/settings.ts";
|
||||||
|
import Logo from "@/layouts/components/Logo/index.vue";
|
||||||
|
import Header from "@/layouts/components/Header/index.vue";
|
||||||
|
import AsideSubMenu from "@/layouts/components/Menu/AsideSubMenu.vue";
|
||||||
|
import Main from "@/layouts/components/Main/index.vue";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
console.log("纵向布局左侧动态路由", authStore.showMenuList);
|
||||||
|
// 动态绑定左侧菜单animate动画
|
||||||
|
const menuAnimate = ref(settings.menuAnimate);
|
||||||
|
const menuList = computed(() => authStore.showMenuList);
|
||||||
|
const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
|
||||||
|
// const menuHoverCollapse = ref(settings.asideMenuHoverCollapse);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.layout-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
.layout-aside {
|
||||||
|
z-index: $layout-aside-z-index; // 左侧菜单层级
|
||||||
|
padding-right: $aside-menu-padding-right; // 左侧布局右边距[用于悬浮和选中更明显]
|
||||||
|
padding-left: $aside-menu-padding-left; // 左侧布局左边距[用于悬浮和选中更明显]
|
||||||
|
background-color: var(--el-menu-bg-color);
|
||||||
|
border-right: none;
|
||||||
|
box-shadow: $aside-menu-box-shadow; // 左侧布局右边框阴影
|
||||||
|
}
|
||||||
|
.layout-header {
|
||||||
|
height: $aside-header-height;
|
||||||
|
background-color: var(--el-header-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除菜单右侧边框
|
||||||
|
.el-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.layout-scrollbar {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - $aside-header-height);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/** 菜单悬浮折叠宽度 */
|
||||||
|
.el-menu--collapse {
|
||||||
|
width: calc(var(--el-menu-icon-width) + var(--el-menu-base-level-padding)) !important;
|
||||||
|
}
|
||||||
|
</style>
|
139
src/layouts/components/Header/components/BreadCrumb.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['breadcrumb-box', 'mask-image']">
|
||||||
|
<el-breadcrumb :separator-icon="ArrowRight">
|
||||||
|
<transition-group name="breadcrumb">
|
||||||
|
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path">
|
||||||
|
<div
|
||||||
|
class="el-breadcrumb__inner is-link"
|
||||||
|
:class="{ 'item-no-icon': !item.meta.icon }"
|
||||||
|
@click="handleBreadcrumb(item, index)"
|
||||||
|
>
|
||||||
|
<KoiGlobalIcon class="breadcrumb-icon" v-if="item.meta.icon" :name="item.meta.icon" size="16"></KoiGlobalIcon>
|
||||||
|
<span class="breadcrumb-title">{{ getLanguage(globalStore.language, item.meta?.title, item.meta?.enName) }}</span>
|
||||||
|
</div>
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
</transition-group>
|
||||||
|
</el-breadcrumb>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { HOME_URL, STATIC_URL } from "@/config/index.ts";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { ArrowRight } from "@element-plus/icons-vue";
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
import { getLanguage } from "@/utils/index.ts";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const breadcrumbList = computed(() => {
|
||||||
|
let breadcrumbData = authStore.getBreadcrumbList[route.matched[route.matched.length - 1].path] ?? [];
|
||||||
|
// 子页面放置静态路由里面, activeMenu不存在值的时候,不符合子路由数据,则返回首页
|
||||||
|
if (breadcrumbData[0].path === STATIC_URL && !breadcrumbData[1].meta?.activeMenu) {
|
||||||
|
if (globalStore.language === "en") {
|
||||||
|
// 英文
|
||||||
|
return [{ path: HOME_URL, meta: { icon: "HomeFilled", title: "Master Station" } }];
|
||||||
|
} else {
|
||||||
|
return [{ path: HOME_URL, meta: { icon: "HomeFilled", title: "首页" } }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 子页面放置静态路由里面, activeMenu存在值的时候
|
||||||
|
if (breadcrumbData[0].path === STATIC_URL && breadcrumbData.length > 1 && breadcrumbData[1].meta?.activeMenu) {
|
||||||
|
const parentMenu = authStore.getMenuList.find((item: any) => item?.path === breadcrumbData[1].meta?.activeMenu);
|
||||||
|
if (parentMenu) {
|
||||||
|
if (globalStore.language === "en") {
|
||||||
|
// 英文
|
||||||
|
breadcrumbData[0].meta.enName = parentMenu.meta?.enName || "Children Page";
|
||||||
|
breadcrumbData[0].meta.icon = parentMenu.meta?.icon || "house";
|
||||||
|
} else {
|
||||||
|
breadcrumbData[0].meta.title = parentMenu.meta?.title || "子路由页面";
|
||||||
|
breadcrumbData[0].meta.icon = parentMenu.meta?.icon || "house";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 不需要首页面包屑可注释以下判断
|
||||||
|
// if (breadcrumbData[0].path !== HOME_URL) {
|
||||||
|
// breadcrumbData = [{ path: HOME_URL, meta: { icon: "HomeFilled", title: "首页" } }, ...breadcrumbData];
|
||||||
|
// }
|
||||||
|
return breadcrumbData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击面包屑
|
||||||
|
const handleBreadcrumb = (item: any, index: number) => {
|
||||||
|
if (breadcrumbList.value[0]?.path === STATIC_URL || breadcrumbList.value[1]?.path === STATIC_URL) {
|
||||||
|
router.push(HOME_URL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index !== breadcrumbList.value.length - 1) router.push(item.path);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
/* breadcrumb-transform 面包屑动画 */
|
||||||
|
.breadcrumb-enter-active {
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.breadcrumb-enter-from,
|
||||||
|
.breadcrumb-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 2px;
|
||||||
|
margin-left: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
.el-breadcrumb {
|
||||||
|
line-height: 15px;
|
||||||
|
white-space: nowrap;
|
||||||
|
.el-breadcrumb__item {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
float: none;
|
||||||
|
.breadcrumb-title {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.item-no-icon {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
.el-breadcrumb__inner {
|
||||||
|
display: inline-flex;
|
||||||
|
line-height: 16px;
|
||||||
|
&.is-link {
|
||||||
|
color: var(--el-header-text-color);
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.breadcrumb-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
// .breadcrumb-title {
|
||||||
|
// margin-top: 2px;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
&:last-child .el-breadcrumb__inner,
|
||||||
|
&:last-child .el-breadcrumb__inner:hover {
|
||||||
|
color: var(--el-header-text-color-regular);
|
||||||
|
}
|
||||||
|
:deep(.el-breadcrumb__separator) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 右侧向左侧移动,面包屑模糊 */
|
||||||
|
.mask-image {
|
||||||
|
padding-right: 50px;
|
||||||
|
mask-image: linear-gradient(90deg, #000000 0%, #000000 calc(100% - 50px), transparent);
|
||||||
|
}
|
||||||
|
</style>
|
20
src/layouts/components/Header/components/Collapse.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hover:bg-[rgba(0,0,0,0.06)] koi-icon w-36px h-36px rounded-md flex flex-justify-center flex-items-center" @click="changeCollapseIcon">
|
||||||
|
<el-icon :size="20">
|
||||||
|
<KoiSvgIcon name="koi-menu-left" width="20px" height="20px" v-if="!globalStore.isCollapse"></KoiSvgIcon>
|
||||||
|
<KoiSvgIcon name="koi-menu-right" width="20px" height="20px" v-if="globalStore.isCollapse"></KoiSvgIcon>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
// 切换图标
|
||||||
|
const changeCollapseIcon = () => {
|
||||||
|
// 定义图标切换变量(true-折叠,false-展开)
|
||||||
|
globalStore.isCollapse = globalStore.setCollapse(globalStore.isCollapse);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
104
src/layouts/components/Header/components/Dark.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hover:bg-[rgba(0,0,0,0.06)] w-32px h-100% flex flex-justify-center flex-items-center" @click="handleSwitchDark">
|
||||||
|
<!-- 明亮模式 -->
|
||||||
|
<el-tooltip :content="$t('header.lightMode')" v-if="!globalStore.isDark">
|
||||||
|
<el-icon class="koi-icon" :size="size">
|
||||||
|
<Sunny />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
<!-- 暗黑模式 -->
|
||||||
|
<el-tooltip :content="$t('header.darkMode')" v-if="globalStore.isDark">
|
||||||
|
<el-icon class="koi-icon" :size="size">
|
||||||
|
<Moon />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useTheme } from "@/utils/theme.ts";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const { switchDark } = useTheme();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 21
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暗黑主题和明亮主题切换
|
||||||
|
const handleSwitchDark = async (event: MouseEvent) => {
|
||||||
|
const x = event.clientX;
|
||||||
|
const y = event.clientY;
|
||||||
|
// 画圆
|
||||||
|
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
|
||||||
|
// @ts-ignore
|
||||||
|
if (document.startViewTransition == undefined) {
|
||||||
|
/** 明亮和暗黑模式核心逻辑 */
|
||||||
|
// 定义图标切换变量(true-月亮,false-太阳)
|
||||||
|
globalStore.setGlobalState("isDark", !globalStore.isDark);
|
||||||
|
switchDark();
|
||||||
|
/** 明亮和暗黑模式核心逻辑 */
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
const transition = document.startViewTransition(() => {
|
||||||
|
/** 明亮和暗黑模式核心逻辑 */
|
||||||
|
// 定义图标切换变量(true-月亮,false-太阳)
|
||||||
|
globalStore.setGlobalState("isDark", !globalStore.isDark);
|
||||||
|
switchDark();
|
||||||
|
/** 明亮和暗黑模式核心逻辑 */
|
||||||
|
});
|
||||||
|
await transition.ready;
|
||||||
|
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
|
||||||
|
document.documentElement.animate(
|
||||||
|
{
|
||||||
|
clipPath: globalStore.isDark ? clipPath : [...clipPath].reverse()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: 300,
|
||||||
|
easing: "ease-in",
|
||||||
|
pseudoElement: globalStore.isDark ? "::view-transition-new(root)" : "::view-transition-old(root)"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 暗黑主题和明亮主题切换
|
||||||
|
// const handleDark = () => {
|
||||||
|
// // 定义图标切换变量(true-月亮,false-太阳)
|
||||||
|
// globalStore.setGlobalState("isDark", !globalStore.isDark);
|
||||||
|
// // 获取HTML根节点
|
||||||
|
// let html = document.documentElement;
|
||||||
|
// // 判断HTML标签是否有类名dark
|
||||||
|
// if (!globalStore.isDark) {
|
||||||
|
// // 太阳
|
||||||
|
// html.className = "";
|
||||||
|
// } else {
|
||||||
|
// // 月亮
|
||||||
|
// html.className = "dark";
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
<style lang="scss">
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
::view-transition-old(root) {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
::view-transition-new(root) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.dark::view-transition-old(root) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.dark::view-transition-new(root) {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
</style>
|
69
src/layouts/components/Header/components/Dimension.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<el-tooltip placement="left" :content="$t('header.componentSize')">
|
||||||
|
<div class="hover:bg-[rgba(0,0,0,0.06)] koi-icon w-32px h-100% flex flex-justify-center flex-items-center">
|
||||||
|
<el-dropdown @command="handleDimension">
|
||||||
|
<el-icon class="koi-icon p-b-2px" :size="22"><ElementPlus /></el-icon>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="item in dimensionList"
|
||||||
|
:key="item.value"
|
||||||
|
:command="item.value"
|
||||||
|
:disabled="dimension === item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, watch, computed } from "vue";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
import { koiMsgSuccess } from "@/utils/koi.ts";
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const dimension = computed(() => globalStore.dimension);
|
||||||
|
|
||||||
|
const dimensionList = ref<any>([]);
|
||||||
|
onMounted(() => {
|
||||||
|
handleSwitchLanguage();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSwitchLanguage = () => {
|
||||||
|
// 当 language 变化时,手动触发 dimensionList 的更新
|
||||||
|
if (globalStore.language === "en") {
|
||||||
|
dimensionList.value = [
|
||||||
|
{ label: "default", value: "default" },
|
||||||
|
{ label: "large", value: "large" },
|
||||||
|
{ label: "small", value: "small" }
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
dimensionList.value = [
|
||||||
|
{ label: "默认", value: "default" },
|
||||||
|
{ label: "大型", value: "large" },
|
||||||
|
{ label: "小型", value: "small" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 globalStore.language 的变化
|
||||||
|
watch(
|
||||||
|
() => globalStore.language,
|
||||||
|
() => {
|
||||||
|
// 当 language 变化时,手动触发 dimensionList 的更新
|
||||||
|
handleSwitchLanguage();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDimension = (item: string) => {
|
||||||
|
if (dimension.value === item) return;
|
||||||
|
globalStore.setDimension(item);
|
||||||
|
koiMsgSuccess("配置成功🌻");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
30
src/layouts/components/Header/components/FullScreen.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 全屏 -->
|
||||||
|
<div class="hover:bg-[rgba(0,0,0,0.06)] koi-icon w-32px h-100% flex flex-justify-center flex-items-center" @click="toggle">
|
||||||
|
<el-tooltip :content="globalStore.isFullScreen === false ? $t('header.fullScreen') : $t('header.exitFullScreen')">
|
||||||
|
<el-icon class="koi-icon" :size="18">
|
||||||
|
<FullScreen v-if="!globalStore.isFullScreen" />
|
||||||
|
<CloseBold v-else />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useFullscreen } from "@vueuse/core";
|
||||||
|
import { watch } from "vue";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
// @vueuse/core 处理是否全屏
|
||||||
|
const { isFullscreen, toggle } = useFullscreen();
|
||||||
|
watch(isFullscreen, () => {
|
||||||
|
if (isFullscreen.value) {
|
||||||
|
globalStore.setGlobalState("isFullScreen", true);
|
||||||
|
} else {
|
||||||
|
globalStore.setGlobalState("isFullScreen", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
69
src/layouts/components/Header/components/Language.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<el-tooltip placement="left" :content="$t('header.language')">
|
||||||
|
<div class="hover:bg-[rgba(0,0,0,0.06)] koi-icon w-32px h-100% flex flex-justify-center flex-items-center">
|
||||||
|
<el-dropdown @command="handleChangeLanguage">
|
||||||
|
<el-icon class="koi-icon" :size="20"><SwitchFilled /></el-icon>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="item in languageList"
|
||||||
|
:key="item.value"
|
||||||
|
:command="item.value"
|
||||||
|
:disabled="language === item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { ref, onMounted, watch, computed } from "vue";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
import { LanguageType } from "@/stores/interface/index.ts";
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const language = computed(() => globalStore.language);
|
||||||
|
|
||||||
|
const languageList = ref<any>([]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleSwitchLanguage();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSwitchLanguage = () => {
|
||||||
|
// 当 language 变化时,手动触发 dimensionList 的更新
|
||||||
|
if (globalStore.language === "en") {
|
||||||
|
languageList.value = [
|
||||||
|
{ label: "Chinese", value: "zh" },
|
||||||
|
{ label: "English", value: "en" }
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
languageList.value = [
|
||||||
|
{ label: "简体中文", value: "zh" },
|
||||||
|
{ label: "英文", value: "en" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 globalStore.language 的变化
|
||||||
|
watch(
|
||||||
|
() => globalStore.language,
|
||||||
|
() => {
|
||||||
|
// 当 language 变化时,手动触发 dimensionList 的更新
|
||||||
|
handleSwitchLanguage();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeLanguage = (lang: string) => {
|
||||||
|
i18n.locale.value = lang;
|
||||||
|
globalStore.setGlobalState("language", lang as LanguageType);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
30
src/layouts/components/Header/components/Refresh.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 刷新 -->
|
||||||
|
<div
|
||||||
|
class="hover:bg-[rgba(0,0,0,0.06)] w-32px h-100% flex flex-justify-center flex-items-center"
|
||||||
|
@click="handleRefresh"
|
||||||
|
>
|
||||||
|
<el-tooltip :content="$t('header.refreshCache')">
|
||||||
|
<el-icon class="koi-icon" :size="20">
|
||||||
|
<RefreshRight />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { koiMsgSuccess } from "@/utils/koi.ts";
|
||||||
|
import { LOGIN_URL } from "@/config";
|
||||||
|
import { koiSessionStorage, koiLocalStorage } from "@/utils/storage.ts";
|
||||||
|
|
||||||
|
// 刷新路由
|
||||||
|
const handleRefresh = () => {
|
||||||
|
koiSessionStorage.clear();
|
||||||
|
koiLocalStorage.clear();
|
||||||
|
koiMsgSuccess("刷新本地缓存成功🌻");
|
||||||
|
// 必须使用这个把页面缓存刷掉
|
||||||
|
window.location.replace(LOGIN_URL);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
182
src/layouts/components/Header/components/SearchMenu.vue
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 搜索菜单 -->
|
||||||
|
<div class="search-menu hover:bg-[rgba(0,0,0,0.06)] w-32px h-100% flex flex-justify-center" @click="handleMenuOpen">
|
||||||
|
<el-tooltip :content="$t('header.searchMenu')">
|
||||||
|
<el-icon :size="20" class="koi-icon"><Search /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-dialog class="search-dialog" v-model="isShowSearch" :width="600" :show-close="false" top="10vh">
|
||||||
|
<el-input
|
||||||
|
v-model="searchMenu"
|
||||||
|
ref="menuInputRef"
|
||||||
|
placeholder="菜单搜索:支持菜单名称、路径🌻"
|
||||||
|
size="large"
|
||||||
|
clearable
|
||||||
|
:prefix-icon="Search"
|
||||||
|
></el-input>
|
||||||
|
<div v-if="searchList.length" class="menu-list" ref="menuListRef">
|
||||||
|
<div
|
||||||
|
v-for="item in searchList"
|
||||||
|
:key="item.path"
|
||||||
|
:class="['menu-item', { 'menu-active': item.path === activePath }]"
|
||||||
|
@mouseenter="mouseoverMenuItem(item)"
|
||||||
|
@click="handleClickMenu()"
|
||||||
|
>
|
||||||
|
<div class="menu-lf">
|
||||||
|
<el-icon class="menu-icon">
|
||||||
|
<component :is="item.meta.icon"></component>
|
||||||
|
</el-icon>
|
||||||
|
<span class="menu-title">{{ item.meta.title }}</span>
|
||||||
|
</div>
|
||||||
|
<el-icon :size="20" @click="handleMenuOpen"><Search /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else class="mt20 mb20" :image-size="100" description="暂无菜单" />
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, nextTick, watch } from "vue";
|
||||||
|
import { InputInstance } from "element-plus";
|
||||||
|
import { Search } from "@element-plus/icons-vue";
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const menuList = computed(() => authStore.menuList.filter((item: any) => item.meta.isHide == "1" && item.meta.parentId != "0"));
|
||||||
|
|
||||||
|
const activePath = ref("");
|
||||||
|
const mouseoverMenuItem = (menu: any) => {
|
||||||
|
activePath.value = menu.path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuInputRef = ref<InputInstance | null>(null);
|
||||||
|
const isShowSearch = ref<boolean>(false);
|
||||||
|
const searchMenu = ref<string>("");
|
||||||
|
|
||||||
|
watch(isShowSearch, val => {
|
||||||
|
if (val) {
|
||||||
|
document.addEventListener("keydown", keyboardOperation);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener("keydown", keyboardOperation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMenuOpen = () => {
|
||||||
|
isShowSearch.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
menuInputRef.value?.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchList = ref<any>([]);
|
||||||
|
const updateSearchList = () => {
|
||||||
|
searchList.value = searchMenu.value
|
||||||
|
? menuList.value.filter(
|
||||||
|
(item: any) =>
|
||||||
|
(item.path.toLowerCase().includes(searchMenu.value.toLowerCase()) ||
|
||||||
|
item.meta.title.toLowerCase().includes(searchMenu.value.toLowerCase())) &&
|
||||||
|
item.meta?.isHide === "1"
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
activePath.value = searchList.value.length ? searchList.value[0].path : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedUpdateSearchList = useDebounceFn(updateSearchList, 300);
|
||||||
|
|
||||||
|
watch(searchMenu, debouncedUpdateSearchList);
|
||||||
|
|
||||||
|
const menuListRef = ref<Element | null>(null);
|
||||||
|
const keyPressUpOrDown = (direction: number) => {
|
||||||
|
const length = searchList.value.length;
|
||||||
|
if (length === 0) return;
|
||||||
|
const index = searchList.value.findIndex((item: any) => item.path === activePath.value);
|
||||||
|
const newIndex = (index + direction + length) % length;
|
||||||
|
activePath.value = searchList.value[newIndex].path;
|
||||||
|
nextTick(() => {
|
||||||
|
if (!menuListRef.value?.firstElementChild) return;
|
||||||
|
const menuItemHeight = menuListRef.value.firstElementChild.clientHeight + 12 || 0;
|
||||||
|
menuListRef.value.scrollTop = newIndex * menuItemHeight;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyboardOperation = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
keyPressUpOrDown(-1);
|
||||||
|
} else if (event.key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
keyPressUpOrDown(1);
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
handleClickMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickMenu = () => {
|
||||||
|
const menu = searchList.value.find((item: any) => item.path === activePath.value);
|
||||||
|
if (!menu) return;
|
||||||
|
if (menu.meta?.isLink) window.open(menu.meta.isLink, "_blank");
|
||||||
|
else router.push(menu.path);
|
||||||
|
searchMenu.value = "";
|
||||||
|
isShowSearch.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.search-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
border-radius: 4px;
|
||||||
|
.el-dialog__header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.menu-list {
|
||||||
|
max-height: 515px;
|
||||||
|
margin-top: 15px;
|
||||||
|
overflow: auto;
|
||||||
|
.menu-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 45px;
|
||||||
|
padding: 0 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
.menu-lf {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.menu-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.menu-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.menu-active {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.menu-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
124
src/layouts/components/Header/components/SearchMenu2.vue
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 搜索菜单 -->
|
||||||
|
<div class="menu-search-dialog hover:bg-[rgba(0,0,0,0.06)] w-32px h-100% flex flex-justify-center">
|
||||||
|
<el-tooltip content="搜索菜单">
|
||||||
|
<el-icon class="koi-icon" :size="20" @click="handleMenuOpen"><Search /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-dialog v-model="isShowSearch" destroy-on-close :modal="false" :show-close="false" fullscreen @click="handleCloseSearch">
|
||||||
|
<el-autocomplete
|
||||||
|
ref="koiMenuRef"
|
||||||
|
v-model="searchMenu"
|
||||||
|
value-key="path"
|
||||||
|
placeholder="菜单搜索:支持菜单名称、路径🌻"
|
||||||
|
:fetch-suggestions="searchMenuList"
|
||||||
|
@select="handleClickMenu"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Search />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<el-icon>
|
||||||
|
<component :is="item.meta.icon"></component>
|
||||||
|
</el-icon>
|
||||||
|
<span> {{ item.meta.title }} </span>
|
||||||
|
</template>
|
||||||
|
</el-autocomplete>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, nextTick } from "vue";
|
||||||
|
import { Search } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import useAuthStore from "@/stores/modules/auth.ts";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const menuList = computed(() => authStore.menuList.filter((item: any) => item.meta.isHide == "1" && item.meta.parentId != "0"));
|
||||||
|
|
||||||
|
// 过滤数据
|
||||||
|
const searchMenuList = (queryString: string, callBack: Function) => {
|
||||||
|
const results = queryString ? menuList.value.filter(filterNodeMethod(queryString)) : menuList.value;
|
||||||
|
callBack(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开搜索框
|
||||||
|
const isShowSearch = ref(false);
|
||||||
|
const koiMenuRef = ref();
|
||||||
|
const searchMenu = ref("");
|
||||||
|
const handleMenuOpen = () => {
|
||||||
|
isShowSearch.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
koiMenuRef.value.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索窗关闭
|
||||||
|
const handleCloseSearch = () => {
|
||||||
|
isShowSearch.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 筛选菜单
|
||||||
|
const filterNodeMethod = (queryString: string) => {
|
||||||
|
return (restaurant: any) => {
|
||||||
|
return (
|
||||||
|
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
|
||||||
|
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击菜单跳转
|
||||||
|
const handleClickMenu = (menuItem: any | Record<string, any>) => {
|
||||||
|
searchMenu.value = "";
|
||||||
|
handleCloseSearch();
|
||||||
|
if (menuItem.meta.isLink) {
|
||||||
|
window.open(menuItem.meta.isLink, "_blank");
|
||||||
|
} else {
|
||||||
|
router.push(menuItem.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.menu-search-dialog {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
background-color: rgb(0 0 0 / 50%);
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: unset !important;
|
||||||
|
.el-dialog__header {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.el-autocomplete) {
|
||||||
|
position: absolute;
|
||||||
|
top: 100px;
|
||||||
|
left: 50%;
|
||||||
|
width: 550px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
.el-input__wrapper {
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-autocomplete__popper {
|
||||||
|
.el-icon {
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin: 0 0 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
23
src/layouts/components/Header/components/ThemeSetting.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 设置 -->
|
||||||
|
<div
|
||||||
|
class="hover:bg-[rgba(0,0,0,0.06)] koi-icon w-32px h-100% flex flex-justify-center flex-items-center m-r-12px"
|
||||||
|
@click="handleThemeDialog"
|
||||||
|
>
|
||||||
|
<el-tooltip :content="$t('header.settings')">
|
||||||
|
<el-icon class="koi-icon" :size="20">
|
||||||
|
<Setting />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import mittBus from "@/utils/mittBus.ts";
|
||||||
|
// 打开主题配置
|
||||||
|
const handleThemeDialog = () => {
|
||||||
|
mittBus.emit("handleThemeConfig");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
39
src/layouts/components/Header/components/Toolbar.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div class="header-right">
|
||||||
|
<!-- 搜索菜单 -->
|
||||||
|
<SearchMenu class="<md:visible"></SearchMenu>
|
||||||
|
<!-- ElementPlus 尺寸配置 -->
|
||||||
|
<Dimension class="<md:visible"></Dimension>
|
||||||
|
<!-- 路由缓存刷新 -->
|
||||||
|
<Refresh class="<md:visible"></Refresh>
|
||||||
|
<!-- 明亮/暗黑模式图标 -->
|
||||||
|
<Dark></Dark>
|
||||||
|
<!-- 中英文翻译 -->
|
||||||
|
<!-- <Language class="<md:visible"></Language>-->
|
||||||
|
<!-- 全屏图标 -->
|
||||||
|
<FullScreen></FullScreen>
|
||||||
|
<!-- 主题配置 -->
|
||||||
|
<!-- <ThemeSetting class="<md:visible"></ThemeSetting>-->
|
||||||
|
<!-- 头像 AND 下拉折叠 -->
|
||||||
|
<User></User>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import User from "@/layouts/components/Header/components/User.vue";
|
||||||
|
import FullScreen from "@/layouts/components/Header/components/FullScreen.vue";
|
||||||
|
import Dark from "@/layouts/components/Header/components/Dark.vue";
|
||||||
|
import ThemeSetting from "@/layouts/components/Header/components/ThemeSetting.vue";
|
||||||
|
import Refresh from "@/layouts/components/Header/components/Refresh.vue";
|
||||||
|
import Dimension from "@/layouts/components/Header/components/Dimension.vue";
|
||||||
|
import Language from "@/layouts/components/Header/components/Language.vue";
|
||||||
|
import SearchMenu from "@/layouts/components/Header/components/SearchMenu.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
69
src/layouts/components/Header/components/User.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 头像 -->
|
||||||
|
<el-image class="w-34px h-34px rounded-full select-none user-avatar" :src="avatar">
|
||||||
|
<template #error>
|
||||||
|
<el-image class="w-34px h-34px rounded-full select-none user-avatar" :src="errorAvatar"></el-image>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
<el-dropdown class="m-l-10px" :hide-on-click="false" @command="handleCommand">
|
||||||
|
<div class="koi-dropdown">
|
||||||
|
<div class="max-w-113px text-14px m-r-6px line-clamp-1 select-none">王将(管理员)</div>
|
||||||
|
<el-icon><arrow-down /></el-icon>
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="koiMine">{{ $t("header.personalCenter") }}</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="logout">{{ $t("header.logout") }}</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { koiSessionStorage, koiLocalStorage } from "@/utils/storage.ts";
|
||||||
|
import { LOGIN_URL } from "@/config";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLayout = () => {
|
||||||
|
koiSessionStorage.clear();
|
||||||
|
// 如果不想要保存上次登录设置的全局颜色、布局等,则将下方第一行清空全部代码打开。
|
||||||
|
// koiLocalStorage.clear();
|
||||||
|
koiLocalStorage.remove("user");
|
||||||
|
koiLocalStorage.remove("keepAlive");
|
||||||
|
koiLocalStorage.remove("tabs");
|
||||||
|
// 退出登录。必须使用replace把页面缓存刷掉。
|
||||||
|
window.location.replace(LOGIN_URL);
|
||||||
|
};
|
||||||
|
// 用户头像
|
||||||
|
const avatar = ref(
|
||||||
|
"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2Fae90b4c7-98b6-4a47-b1b3-9ee8bc71acf6%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1692146441&t=6fca60f3a0d323869b81d8fb53b5dd1b"
|
||||||
|
);
|
||||||
|
const errorAvatar = "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png";
|
||||||
|
// 下拉折叠
|
||||||
|
const handleCommand = (command: string | number) => {
|
||||||
|
switch (command) {
|
||||||
|
case "koiMine":
|
||||||
|
router.push("/system/personage");
|
||||||
|
break;
|
||||||
|
case "logout":
|
||||||
|
handleLayout();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// dropdown字体颜色
|
||||||
|
.koi-dropdown {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
white-space: nowrap; /* 不换行 */
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none; // 去除伪元素
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
32
src/layouts/components/Header/index.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="header-box">
|
||||||
|
<div class="header-left">
|
||||||
|
<!-- 左侧菜单展开和折叠图标 -->
|
||||||
|
<Collapse></Collapse>
|
||||||
|
<!-- 面包屑 -->
|
||||||
|
<BreadCrumb class="<md:hidden"></BreadCrumb>
|
||||||
|
</div>
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<Toolbar></Toolbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Collapse from "@/layouts/components/Header/components/Collapse.vue";
|
||||||
|
import BreadCrumb from "@/layouts/components/Header/components/BreadCrumb.vue";
|
||||||
|
import Toolbar from "@/layouts/components/Header/components/Toolbar.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: $aside-header-height;
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
85
src/layouts/components/Logo/index.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="koi-logo flex flex-items-center p-l-5px" v-show="isShow">
|
||||||
|
<div class="rounded-full" :style="{ width: logoSize, height: logoSize }" v-if="props.layout !== 'classic' && props.layout !== 'horizontal'">
|
||||||
|
<el-image
|
||||||
|
:src="logoUrl"
|
||||||
|
fit="cover"
|
||||||
|
class="w-100% h-100% rounded-full"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<el-icon class="w-100% h-100% rounded-full c-[--el-color-primary]" :size="34">
|
||||||
|
<CircleCloseFilled />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</div>
|
||||||
|
<div class="m-l--14px rounded-full" :style="{ width: logoSize, height: logoSize }" v-if="props.layout === 'classic'">
|
||||||
|
<el-image
|
||||||
|
:src="logoUrl"
|
||||||
|
fit="cover"
|
||||||
|
class="w-100% h-100% rounded-full"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<el-icon class="w-100% h-100% rounded-full c-[--el-color-primary]" :size="34">
|
||||||
|
<CircleCloseFilled />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</div>
|
||||||
|
<div class="m-l-6px rounded-full" :style="{ width: logoSize, height: logoSize }" v-if="props.layout === 'horizontal'">
|
||||||
|
<el-image
|
||||||
|
:src="logoUrl"
|
||||||
|
fit="cover"
|
||||||
|
class="w-100% h-100% rounded-full"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<el-icon class="w-100% h-100% rounded-full c-[--el-color-primary]" :size="34">
|
||||||
|
<CircleCloseFilled />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</div>
|
||||||
|
<div class="chroma-text w-160px m-l-10px font-bold truncate tracking-1px" :style="{ 'font-size': titleSize }" :class="titleAnimate" v-text="title" v-show="!props.isCollapse"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import settings from "@/settings";
|
||||||
|
import { getAssets } from "@/utils/index.ts";
|
||||||
|
import { getLanguage } from "@/utils/index.ts";
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
|
||||||
|
// 接收父组件传递的参数
|
||||||
|
const props = defineProps({
|
||||||
|
isCollapse: {
|
||||||
|
require: false, // true显示,false隐藏
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
require: "vertical", // 布局模式 (纵向:vertical | 分栏:columns | 经典:classic | 上左:optimum | 横向:horizontal )
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = ref(settings.logoTitle);
|
||||||
|
const titleSize = ref(`${settings.loginTitleSize}px`);
|
||||||
|
const logoUrl = ref(`${getAssets(settings.logoUrl)}`);
|
||||||
|
const isShow = ref(settings.logoShow);
|
||||||
|
const logoSize = ref(settings.logoSize);
|
||||||
|
const titleAnimate = ref(settings.logoTitleAnimate);
|
||||||
|
|
||||||
|
// 标题语言切换
|
||||||
|
title.value = computed(() => {
|
||||||
|
return getLanguage(globalStore.language, settings.logoTitle, settings.logoEnTitle);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.koi-logo {
|
||||||
|
height: $aside-header-height;
|
||||||
|
line-height: $aside-header-height;
|
||||||
|
}
|
||||||
|
</style>
|
40
src/layouts/components/Main/components/Maximize.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="maximize" @click="handleExitMaximize">
|
||||||
|
<el-icon :size="22" class="exitIcon"><CloseBold /></el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import useGlobalStore from "@/stores/modules/global.ts";
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
|
||||||
|
const handleExitMaximize = () => {
|
||||||
|
globalStore.setGlobalState("maximize", false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.maximize {
|
||||||
|
position: fixed;
|
||||||
|
top: -25px;
|
||||||
|
right: -25px;
|
||||||
|
z-index: 999;
|
||||||
|
width: 66px;
|
||||||
|
height: 66px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--el-color-primary-light-8);
|
||||||
|
border: 2px dashed var(--el-color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.9;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--el-color-primary-light-7);
|
||||||
|
}
|
||||||
|
.exitIcon {
|
||||||
|
position: relative;
|
||||||
|
top: 46%;
|
||||||
|
left: 19%;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|