Compare commits
172 Commits
V2.1.0
..
760a457410
| Author | SHA1 | Date | |
|---|---|---|---|
| 760a457410 | |||
| 58116ff311 | |||
| b2d715a1b1 | |||
| f31020d31c | |||
| 7e5100fc5c | |||
| 9ca0619373 | |||
| 02237e3e30 | |||
| 2d805cf02d | |||
| 266a1764be | |||
| 52d154e1a3 | |||
| 7deefc8ea3 | |||
| 31b6a6ce2d | |||
| 7b76410df8 | |||
| c836302f99 | |||
| ecadb6ce2a | |||
| 1ff42aa911 | |||
| a666679ebd | |||
| 6cbbbe8541 | |||
| dcd46b9afe | |||
| 13ed3a8fe4 | |||
| 69e64fb08c | |||
| e6c35d25a5 | |||
| 26947ee678 | |||
| fe8f1606fa | |||
| b8113725bf | |||
| 66e1170c58 | |||
| 359acb41d1 | |||
| 6883e54a1b | |||
| 84273c3689 | |||
| 383280031c | |||
| bca8602a3a | |||
| 04c76dc347 | |||
| d75232e5e0 | |||
| ca27b2bbcc | |||
| 6486a90812 | |||
| 855695bc13 | |||
| d9869fcb87 | |||
| 2ecddb17aa | |||
| 73837d517d | |||
| 8e6a00f610 | |||
| ffbf926c9f | |||
| 93fea94c3a | |||
| eeb72097e5 | |||
| c2a496b228 | |||
| f27a869d6a | |||
| 084afc0cef | |||
| abb1fed1ef | |||
| 85a7f66af4 | |||
| d616564a55 | |||
| ba238a44d9 | |||
| 55f3e74cbf | |||
| 173cada6a4 | |||
| fb655552b3 | |||
| 9aa47a6b3b | |||
| b338b91e5a | |||
| 9034790421 | |||
| 204930e9e5 | |||
| b46b707329 | |||
| 3e526045bf | |||
| dab7d938f7 | |||
| 6e60f4a67c | |||
| 6fdc510f60 | |||
| 3b8a119ab0 | |||
| cd1c82082e | |||
| d836cecb91 | |||
| 5857af36dd | |||
| 9c8712ae26 | |||
| bc35da29af | |||
| 7291dedeaf | |||
| 0631f3ae24 | |||
| 3452dc748a | |||
| e1c02b68a3 | |||
| a789486823 | |||
| 0228151136 | |||
| 56ba451325 | |||
| c4537b1103 | |||
| 5ddb70e1ab | |||
| b90611e310 | |||
| 23d0b08242 | |||
| 744c054c1c | |||
| 2a76490656 | |||
| 53299b83b3 | |||
| 865ef1e383 | |||
| 0310eee39e | |||
| fcda974626 | |||
| 81b714333a | |||
| 6516fe4905 | |||
| e37d12a5f2 | |||
| 331e037714 | |||
| ef95e5ce73 | |||
| 75f9985265 | |||
| 95d1352a2b | |||
| 0c9091f5e6 | |||
| 0a86b75454 | |||
| b1c572903f | |||
| 521edd2a8f | |||
| 96425f7e70 | |||
| de3cd7bb34 | |||
| 1c87705166 | |||
| d2eb0fa284 | |||
| 4b1d0d2139 | |||
| bcd8fc3963 | |||
| b852159e39 | |||
| a31ad98a9e | |||
| 10ab2fb670 | |||
| 0d30d6cb5a | |||
| 05f3fbc454 | |||
| 2365231986 | |||
| 68aec74c80 | |||
| b2e4937c69 | |||
| 838856e53f | |||
| 9ad6d3bb9d | |||
| c7a10c7e98 | |||
| 9aed3ec064 | |||
| f1a7c74b93 | |||
| d3a29eed91 | |||
| ec35a111c7 | |||
| 171c8a404c | |||
| e56ff2ce07 | |||
| bc62099ddd | |||
| e968e5240a | |||
| 7c0fb75913 | |||
| c2d6fa1c86 | |||
| 494dd43bc9 | |||
| 7c84abdd64 | |||
| dbd6e4bd6b | |||
| ef724678cb | |||
| 1421865d5c | |||
| 530e5ca6a9 | |||
| c2561d5498 | |||
| b4ef6600c4 | |||
| 2cd8d41f5c | |||
| 0d9fdf2cd6 | |||
| 203c35b506 | |||
| a2432376c2 | |||
| b76dc08395 | |||
| 076b6e91dd | |||
| 75921e1b13 | |||
| 9ffd67758b | |||
| afcf5d56b4 | |||
| 18f3b97ab8 | |||
| e4673377fd | |||
| b63515128b | |||
| e4fab491db | |||
| b01ce6be88 | |||
| 6978b89ee5 | |||
| 2239c23a1e | |||
| fc2b56b9ad | |||
| 682c1eb6de | |||
| 97e7f6201a | |||
| 0518905615 | |||
| 2aefb3d75b | |||
| de004b4e0b | |||
| e85166d1f7 | |||
| 1cb7af4f66 | |||
| c85f6348bc | |||
| bf66ff6e1c | |||
| 702d65ef47 | |||
| 94e94956bb | |||
| 500509f837 | |||
| 925b9195a6 | |||
| f66d388ff7 | |||
| da9fc065c3 | |||
| 8b63bb71cc | |||
| d451b29b7f | |||
| 8db4370f17 | |||
| 2327d1c8bb | |||
| 8c91c76adc | |||
| 6f39070e9b | |||
| eb13a9efdb | |||
| d4f497f484 | |||
| f9d8b4670b |
@@ -1,3 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not dead
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
types/env.d.ts
|
||||||
|
types/web.d.ts
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
},
|
||||||
|
root: true,
|
||||||
|
rules: {
|
||||||
|
'vue/html-self-closing': 'off',
|
||||||
|
'vue/max-attributes-per-line': 'off',
|
||||||
|
'vue/multiline-html-element-content-newline': 'off',
|
||||||
|
'vue/require-default-prop': 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// const isProduction = (process.env.NODE_ENV === 'production');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'plugin:vue/recommended',
|
|
||||||
'eslint:recommended',
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
parser: '@babel/eslint-parser',
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
// 'no-console': (isProduction ? 'warn' : 'off'),
|
|
||||||
// 'no-debugger': (isProduction ? 'warn' : 'off'),
|
|
||||||
'vue/html-closing-bracket-newline': 'off',
|
|
||||||
'vue/html-indent': 'off',
|
|
||||||
'vue/html-self-closing': 'off',
|
|
||||||
'vue/max-attributes-per-line': 'off',
|
|
||||||
'vue/multiline-html-element-content-newline': 'off',
|
|
||||||
'vue/no-v-html': 'off',
|
|
||||||
'vue/singleline-html-element-content-newline': 'off',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
name: 添加链接
|
|
||||||
about: 提交新的链接(不接受违规网站)
|
|
||||||
title: 添加链接:链接名称
|
|
||||||
labels: add
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- 分类:
|
|
||||||
- 名称:
|
|
||||||
- 链接:
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
name: 发现问题
|
|
||||||
about: 提交一个发现的问题(BUG)
|
|
||||||
title: BUG:对发现的 BUG 进行简单描述
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 问题描述
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 解决方案(可选,若无请删除)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
name: 修正链接
|
|
||||||
about: 修正已有的链接
|
|
||||||
title: 修正链接:链接名称
|
|
||||||
labels: correction
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- 链接原分类:
|
|
||||||
- 链接原名称:
|
|
||||||
- 修正后的分类:
|
|
||||||
- 修正后的名称:
|
|
||||||
- 修正后的链接:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: 其他问题
|
|
||||||
about: 若无符合的项,请选择此项
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: 提交建议
|
|
||||||
about: 提交功能建议、修改建议等
|
|
||||||
title: 建议:建议的主要内容
|
|
||||||
labels: suggestion
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
请填写建议的具体内容
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
请填写描述,例如:修改的内容、作用。
|
|
||||||
@@ -1,5 +1,2 @@
|
|||||||
.DS_Store
|
dist
|
||||||
.vscode
|
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
1. 项目采用 Vite + Vue 3 + Naive UI 组件库开发,图标库使用 Material Design Icons(@mdi/font)。
|
||||||
|
2. 项目使用 pnpm 作为包管理器。
|
||||||
|
3. 项目开发服务已启动,首页访问地址是 http://localhost:9000/,不需要重复启动。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.wordWrap": "on",
|
||||||
|
"files.autoSave": "off",
|
||||||
|
"files.eol": "\n",
|
||||||
|
"typescript.suggest.autoImports": false,
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# 更新日志
|
||||||
|
|
||||||
|
## [3.1.6] - 2025-10-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `工具箱` 添加工具信息和工具更新日志显示。
|
||||||
|
- `工具箱` 添加“JSON 编辑器”工具。
|
||||||
|
|
||||||
|
## [3.1.5] - 2025-04-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `工具箱` 添加“二维码解析和生成”工具。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `工具箱` 调整工具列表项顺序。
|
||||||
|
|
||||||
|
## [3.1.4] - 2025-02-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `工具箱` 添加在新窗口中打开当前工具功能。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `工具箱/JSON 格式化` 优化“输出内容”显示样式,解决内容较多时行号显示不全的问题。
|
||||||
|
|
||||||
|
## [3.1.3] - 2025-02-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `工具箱` 添加“JSON 格式化”工具。
|
||||||
|
- `工具箱` 添加“Minecraft 聊天记录查看”工具。
|
||||||
|
- `工具箱` 添加“Unix 时间戳转换”工具。
|
||||||
|
- `工具箱` 添加“WebSocket 测试”工具。
|
||||||
|
- `工具箱` 添加“新窗口中打开”工具。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `网址导航` 更新导航链接列表(2025-02-03)。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `工具箱/原神时钟` 解决特定情况下显示的小时值大于或等于 24 的问题。
|
||||||
|
|
||||||
|
## [3.1.2] - 2024-10-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `工具箱` 添加“保持亮屏”。
|
||||||
|
- `工具箱` 添加“原神时钟”。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `工具箱` 调整页面路由生成逻辑,不跳过未启用的工具。
|
||||||
|
|
||||||
|
## [3.1.1] - 2024-09-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `配置文件` 解决打包异常。
|
||||||
|
|
||||||
|
## [3.1.0] - 2024-09-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `工具箱` 添加“工具箱”模块,包含“下载用时计算”“比例计算”“生成随机字符串”工具。
|
||||||
|
- `网址导航` 支持记录最后一次选中的链接分类。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `功能` 修改页面跳转方式,支持返回。
|
||||||
|
|
||||||
|
## [3.0.0] - 2024-09-01
|
||||||
|
|
||||||
|
重构新版本,支持“搜索”功能和“网址导航”功能。
|
||||||
@@ -1,21 +1,674 @@
|
|||||||
MIT License
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (c) 2019 Frost-ZX
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Preamble
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The GNU General Public License is a free, copyleft license for
|
||||||
copies or substantial portions of the Software.
|
software and other kinds of works.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
The licenses for most software and other practical works are designed
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
share and change all versions of a program--to make sure it remains free
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
GNU General Public License for most of our software; it applies also to
|
||||||
SOFTWARE.
|
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 <https://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
|
||||||
|
<https://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
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
|
# frost-navigation
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
一个多功能的网址导航,绿色无广告。
|
一个使用 Vue 3 开发的网站导航和工具箱,绿色无广告。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
### 在线
|
### 在线
|
||||||
|
|
||||||
- [GitHub Pages](https://frost-zx.github.io/frost-navigation/)
|
- [GitHub Pages](https://frost-zx.github.io/frost-navigation/)
|
||||||
|
- [V1 版本](https://frost-zx.github.io/frost-navigation/v1/)(2020-09-10)
|
||||||
|
- [V2 版本](https://frost-zx.github.io/frost-navigation/v2/)(2024-08-31)
|
||||||
|
- [V3 版本](https://frost-zx.github.io/frost-navigation/v3/)(开发中)
|
||||||
- [Vercel](https://frost-navigation.vercel.app/)
|
- [Vercel](https://frost-navigation.vercel.app/)
|
||||||
|
|
||||||
### 离线(需要手动更新)
|
### 离线(需要手动更新)
|
||||||
@@ -15,31 +28,10 @@
|
|||||||
2. 解压,放置于任意合适的喜欢的位置(可重命名)
|
2. 解压,放置于任意合适的喜欢的位置(可重命名)
|
||||||
2. 在浏览器中打开 `index.html`(也可以将其加入浏览器的书签栏或在浏览器设置中将其设置为主页)
|
2. 在浏览器中打开 `index.html`(也可以将其加入浏览器的书签栏或在浏览器设置中将其设置为主页)
|
||||||
|
|
||||||
### 自定义
|
|
||||||
|
|
||||||
- 设置
|
|
||||||
|
|
||||||
- 在“设置”页面中,可根据需要对字体大小、页面结构等进行调整。
|
|
||||||
|
|
||||||
- 导航链接列表
|
|
||||||
|
|
||||||
- 修改 `static/js/frostNavLinks.js`。
|
|
||||||
|
|
||||||
## 注意
|
## 注意
|
||||||
|
|
||||||
- 部分链接由于不能及时更新,可能已过期(不存在、指向了错误的网站等),访问时请注意。若您在使用时发现相关情况,欢迎进行反馈。
|
- 部分链接由于不能及时更新,可能已过期(不存在、指向了错误的网站等),访问时请注意。若您在使用时发现相关情况,欢迎进行反馈。
|
||||||
|
|
||||||
## 项目中主要使用的组件库和插件
|
|
||||||
|
|
||||||
- [Vue](https://vuejs.org/)
|
|
||||||
- [Vue CLI](https://cli.vuejs.org/)
|
|
||||||
- [Vue Router](https://router.vuejs.org/)
|
|
||||||
- [Vuex](https://vuex.vuejs.org/)
|
|
||||||
- [Element UI - 桌面端组件库](https://element.eleme.cn/)
|
|
||||||
- [Font Awesome](https://github.com/FortAwesome/Font-Awesome)
|
|
||||||
- [lodash](https://github.com/lodash/lodash)
|
|
||||||
- [ress - Modern CSS reset](https://github.com/filipelinhares/ress)
|
|
||||||
|
|
||||||
## 鸣谢
|
## 鸣谢
|
||||||
|
|
||||||
- [GitHub Pages - Websites for you and your projects](https://pages.github.com/)
|
- [GitHub Pages - Websites for you and your projects](https://pages.github.com/)
|
||||||
@@ -57,26 +49,26 @@
|
|||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
### Project setup
|
### Project Setup
|
||||||
|
|
||||||
```
|
```sh
|
||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
```
|
```sh
|
||||||
pnpm run serve
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and minifies for production
|
### Compile and Minify for Production
|
||||||
|
|
||||||
```
|
```sh
|
||||||
pnpm run build-modern
|
pnpm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lints and fixes files
|
### Lint with ESLint
|
||||||
|
|
||||||
```
|
```sh
|
||||||
pnpm run lint-fix
|
pnpm run lint
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Frost Navigation</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="/nav-links.js"></script>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"composite": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@package-json": ["./package.json"],
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"@types/w3c-web-serial"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [],
|
||||||
|
"include": [
|
||||||
|
"package.json",
|
||||||
|
"public/nav-links.js",
|
||||||
|
"src/**/*.js",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"types/env.d.ts",
|
||||||
|
"types/web.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,24 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"files": [],
|
||||||
"baseUrl": "./",
|
"references": [
|
||||||
"lib": [
|
{ "path": "./jsconfig.node.json" },
|
||||||
"esnext",
|
{ "path": "./jsconfig.app.json" }
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
],
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sourceMap": false,
|
|
||||||
"target": "es5"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.vue",
|
|
||||||
"src/**/*.js"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node20/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": [
|
||||||
|
"@types/node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [],
|
||||||
|
"include": [
|
||||||
|
"vite.config.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,57 +1,46 @@
|
|||||||
{
|
{
|
||||||
"name": "frost-navigation",
|
"name": "frost-navigation",
|
||||||
"version": "2.1.0",
|
"description": "Frost Navigation",
|
||||||
"author": {
|
"version": "3.1.6",
|
||||||
"name": "Frost-ZX"
|
"license": "GPL-3.0-only",
|
||||||
},
|
"type": "module",
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve --mode development",
|
"dev": "vite",
|
||||||
"serve-production": "vue-cli-service serve --mode production",
|
"dev-host": "vite dev --host 0.0.0.0",
|
||||||
"serve-all": "vue-cli-service serve --mode development --host 0.0.0.0",
|
"build": "vite build",
|
||||||
"serve-local": "vue-cli-service serve --mode development --host 127.0.0.1",
|
"preview": "vite preview",
|
||||||
"build": "vue-cli-service build --mode production",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||||
"build-modern": "vue-cli-service build --mode production --modern",
|
|
||||||
"lint-check": "vue-cli-service lint --no-fix",
|
|
||||||
"lint-fix": "vue-cli-service lint"
|
|
||||||
},
|
},
|
||||||
|
"packageManager": "pnpm@10.11.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bookmark-file-parser": "^1.0.6",
|
"@frost-utils/javascript": "^2.1.3",
|
||||||
"codemirror": "^5.65.2",
|
"@mdi/font": "^7.4.47",
|
||||||
"core-js": "^3.8.3",
|
"@vueuse/core": "^13.3.0",
|
||||||
"dayjs": "^1.11.0",
|
"axios": "^1.9.0",
|
||||||
"element-ui": "^2.15.6",
|
"dayjs": "^1.11.13",
|
||||||
"font-awesome": "^4.7.0",
|
"highlight.js": "^11.11.1",
|
||||||
"html-entities": "^2.3.2",
|
"lunisolar": "^2.5.2",
|
||||||
"lodash": "^4.17.21",
|
"mathjs": "^14.5.2",
|
||||||
"mathjs": "^10.4.0",
|
"monaco-editor": "^0.54.0",
|
||||||
"papaparse": "^5.3.2",
|
"naive-ui": "^2.41.0",
|
||||||
"ress": "^5.0.2",
|
"radash": "^12.1.0",
|
||||||
"vue": "^2.6.14",
|
"uuid": "^11.1.0",
|
||||||
"vue-router": "^3.5.1",
|
"vue": "^3.5.16",
|
||||||
"vuex": "^3.6.2"
|
"vue-router": "^4.5.1",
|
||||||
|
"zxing-wasm": "^2.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.16",
|
"@tsconfig/node20": "^20.1.5",
|
||||||
"@babel/eslint-parser": "^7.12.16",
|
"@types/node": "^20.17.57",
|
||||||
"@types/papaparse": "^5.3.2",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.0",
|
"@types/w3c-web-serial": "^1.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
"@vitejs/plugin-legacy": "^6.1.1",
|
||||||
"@vue/cli-plugin-router": "~5.0.0",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
"@vitejs/plugin-vue-jsx": "~4.1.2",
|
||||||
"@vue/cli-service": "~5.0.0",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
"@vue/eslint-config-standard": "^6.1.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"eslint-plugin-import": "^2.25.3",
|
"less": "^4.3.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"vite": "^6.3.5"
|
||||||
"eslint-plugin-promise": "^5.1.0",
|
|
||||||
"eslint-plugin-vue": "^8.0.3",
|
|
||||||
"less": "^4.0.0",
|
|
||||||
"less-loader": "^8.0.0",
|
|
||||||
"postcss-pxtorem": "^6.0.0",
|
|
||||||
"sass": "^1.32.7",
|
|
||||||
"sass-loader": "^12.0.0",
|
|
||||||
"style-resources-loader": "^1.5.0",
|
|
||||||
"vue-template-compiler": "^2.6.14"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- core-js
|
||||||
|
- esbuild
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
'autoprefixer': {
|
|
||||||
overrideBrowserslist: [
|
|
||||||
'Android >= 4.0',
|
|
||||||
'iOS >= 8'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'postcss-pxtorem': {
|
|
||||||
mediaQuery: false,
|
|
||||||
propList: ['*'],
|
|
||||||
rootValue: 16,
|
|
||||||
unitPrecision: 4,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html dir="ltr" lang="zh-Hans-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
<meta name="description" content="一个多功能的网址导航,绿色无广告。">
|
|
||||||
<meta name="keywords" content="collection,navigation,tool,合集,导航,网址导航,网址大全,网站导航,网站大全,工具">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript><strong>浏览器未启用或不支持 JavaScript,内容无法正常显示。</strong></noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
zxing_full.wasm
|
||||||
|
zxing-wasm v2.1.2
|
||||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 77 KiB |
@@ -1,320 +1,288 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<n-config-provider
|
||||||
|
:date-locale="configProviderProps.dateLocale"
|
||||||
<!-- 加载动画 -->
|
:hljs="hljs"
|
||||||
<div v-show="showLoading" class="loading-bar">
|
:inline-theme-disabled="configProviderProps.inlineThemeDisabled"
|
||||||
<div class="bar-content"></div>
|
:locale="configProviderProps.locale"
|
||||||
</div>
|
:theme-overrides="themeOverrides"
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
|
||||||
<el-container>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<el-header class="main-header shadow-1">
|
|
||||||
|
|
||||||
<!-- LOGO -->
|
|
||||||
<div class="logo"></div>
|
|
||||||
|
|
||||||
<!-- 菜单 -->
|
|
||||||
<el-menu class="menu" :default-active="headerDefaultActive" mode="horizontal" router>
|
|
||||||
|
|
||||||
<!-- 标题 -->
|
|
||||||
<el-menu-item
|
|
||||||
v-show="appConfig.showSiteTitle"
|
|
||||||
index="title"
|
|
||||||
class="title"
|
|
||||||
disabled
|
|
||||||
>Frost 网址导航</el-menu-item>
|
|
||||||
|
|
||||||
<!-- 菜单项 -->
|
|
||||||
<el-menu-item
|
|
||||||
v-for="item in headerMenuItems"
|
|
||||||
:key="item.id"
|
|
||||||
class="item-normal"
|
|
||||||
:index="item.id"
|
|
||||||
:route="{ name: item.routeName }"
|
|
||||||
>{{ item.label }}</el-menu-item>
|
|
||||||
|
|
||||||
<!-- 切换下拉菜单 -->
|
|
||||||
<el-menu-item
|
|
||||||
:class="['item-dropdown', { active: showHeaderDropdown }]"
|
|
||||||
@click="showHeaderDropdown = !showHeaderDropdown"
|
|
||||||
>
|
|
||||||
<i class="fa fa-bars"></i>
|
|
||||||
</el-menu-item>
|
|
||||||
|
|
||||||
</el-menu>
|
|
||||||
|
|
||||||
</el-header>
|
|
||||||
|
|
||||||
<!-- 下拉菜单 -->
|
|
||||||
<el-menu
|
|
||||||
:class="['header-dropdown', 'shadow-2', { show: showHeaderDropdown }]"
|
|
||||||
:default-active="headerDefaultActive"
|
|
||||||
router
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- 菜单项 -->
|
<!-- Naive UI 全局样式 -->
|
||||||
<el-menu-item
|
<n-global-style></n-global-style>
|
||||||
v-for="item in headerMenuItems"
|
|
||||||
:key="item.id"
|
|
||||||
class="item-normal"
|
|
||||||
:index="item.id"
|
|
||||||
:route="{ name: item.routeName }"
|
|
||||||
>{{ item.label }}</el-menu-item>
|
|
||||||
|
|
||||||
</el-menu>
|
|
||||||
|
|
||||||
<!-- Container -->
|
|
||||||
<keep-alive>
|
|
||||||
<router-view class="main-container" />
|
|
||||||
</keep-alive>
|
|
||||||
|
|
||||||
</el-container>
|
|
||||||
|
|
||||||
<!-- 悬浮按钮 -->
|
|
||||||
<FloatingBtn />
|
|
||||||
|
|
||||||
|
<!-- 全局侧边栏 -->
|
||||||
|
<div class="app-aside-wrapper">
|
||||||
|
<app-aside />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 路由页面 -->
|
||||||
|
<router-view class="app-view"></router-view>
|
||||||
|
|
||||||
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { mapState } from 'vuex';
|
import {
|
||||||
|
NConfigProvider, NGlobalStyle,
|
||||||
|
useThemeVars,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
import FloatingBtn from '@/components/FloatingBtn.vue';
|
import {
|
||||||
|
onMounted, onBeforeUnmount,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
export default {
|
import {
|
||||||
name: 'App',
|
configProviderProps,
|
||||||
components: {
|
} from './assets/js/naive-ui';
|
||||||
FloatingBtn,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
|
|
||||||
debounceFontSize: null,
|
import hljs from 'highlight.js/lib/core';
|
||||||
|
import hljsJavascript from 'highlight.js/lib/languages/javascript';
|
||||||
|
import hljsJson from 'highlight.js/lib/languages/json';
|
||||||
|
|
||||||
/** Header 菜单项 */
|
import AppAside from './components/AppAside.vue';
|
||||||
headerMenuItems: [
|
|
||||||
{
|
|
||||||
id: 'home',
|
|
||||||
label: '主页',
|
|
||||||
routeName: 'Home'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tools',
|
|
||||||
label: '小工具',
|
|
||||||
routeName: 'Tools'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'settings',
|
|
||||||
label: '设置',
|
|
||||||
routeName: 'Settings'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'about',
|
|
||||||
label: '关于',
|
|
||||||
routeName: 'About'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
/** 显示下拉菜单 */
|
/** 主题变量配置 */
|
||||||
showHeaderDropdown: false,
|
const themeOverrides = configProviderProps.themeOverrides;
|
||||||
|
|
||||||
}
|
/** 默认主题变量 */
|
||||||
},
|
const themeVars = useThemeVars();
|
||||||
computed: {
|
|
||||||
|
|
||||||
...mapState({
|
/**
|
||||||
appConfig: 'config',
|
* @description 阻止默认右键菜单
|
||||||
showLoading: 'showLoading',
|
* @param {PointerEvent} event
|
||||||
}),
|
*/
|
||||||
|
function handleContextMenu(event) {
|
||||||
|
|
||||||
/** Header 默认激活的菜单项 */
|
let elements = event.composedPath();
|
||||||
headerDefaultActive() {
|
let classValue = '';
|
||||||
var routeName = this.$route.name;
|
let classRegExp = /(n-code|n-input|n-input-number|n-ol|n-select)/;
|
||||||
var item = '';
|
|
||||||
|
|
||||||
if (routeName) {
|
// 排除按住 Ctrl 键时
|
||||||
item = routeName.toLowerCase();
|
if (event.ctrlKey) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
for (let i = 0; i < elements.length; i++) {
|
||||||
},
|
|
||||||
|
|
||||||
},
|
let element = elements[i];
|
||||||
watch: {
|
|
||||||
|
|
||||||
// 更新字体大小
|
// 获取元素 class 信息
|
||||||
'appConfig.fontSize': {
|
if (element instanceof HTMLElement) {
|
||||||
handler(value) {
|
classValue = element.classList.value;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
clearTimeout(this.debounceFontSize);
|
// 排除输入框元素
|
||||||
this.debounceFontSize = setTimeout(() => {
|
if (element instanceof HTMLInputElement) {
|
||||||
document.documentElement.style.fontSize = `${value}px`;
|
return;
|
||||||
}, 500);
|
}
|
||||||
|
|
||||||
|
// 排除指定元素
|
||||||
|
if (classValue && classRegExp.test(classValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
// 路由名称
|
event.preventDefault();
|
||||||
'$route.name': {
|
|
||||||
handler() {
|
|
||||||
// 切换路由时隐藏下拉菜单
|
|
||||||
this.showHeaderDropdown = false;
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
},
|
/** 初始化 CSS 变量列表 */
|
||||||
created() {
|
function initCssVars() {
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
/** 初始化 */
|
let rootStyle = document.documentElement.style;
|
||||||
init() {
|
let overrides = themeOverrides.common;
|
||||||
this.$store.commit('readConfig');
|
let variables = themeVars.value;
|
||||||
},
|
|
||||||
|
|
||||||
},
|
let cssVars = {
|
||||||
|
// 主题变量
|
||||||
|
'--border-radius': overrides.borderRadius,
|
||||||
|
'--border-radius-small': overrides.borderRadiusSmall,
|
||||||
|
'--box-shadow-1': variables.boxShadow1,
|
||||||
|
'--box-shadow-2': variables.boxShadow2,
|
||||||
|
'--box-shadow-3': variables.boxShadow3,
|
||||||
|
'--color-action': variables.actionColor,
|
||||||
|
'--color-border': variables.borderColor,
|
||||||
|
'--color-error': overrides.errorColor,
|
||||||
|
'--color-info': overrides.infoColor,
|
||||||
|
'--color-primary': overrides.primaryColor,
|
||||||
|
'--color-success': overrides.successColor,
|
||||||
|
'--color-text-1': variables.textColor1,
|
||||||
|
'--color-text-2': variables.textColor2,
|
||||||
|
'--color-text-3': variables.textColor3,
|
||||||
|
'--color-warning': overrides.warningColor,
|
||||||
|
// 其他颜色
|
||||||
|
'--color-bg-dark': 'var(--color-text-2)',
|
||||||
|
'--color-bg-light': '#F8F8F8',
|
||||||
|
'--color-black': 'var(--color-text-2)',
|
||||||
|
'--color-gray': '#E0E0E0',
|
||||||
|
'--color-red': 'var(--color-error)',
|
||||||
|
'--color-green': 'var(--color-success)',
|
||||||
|
'--color-blue': 'var(--color-info)',
|
||||||
|
'--color-orange': 'var(--color-warning)',
|
||||||
|
// 元素大小
|
||||||
|
'--dialog-content-max-height': 'calc(100vh - 160px)',
|
||||||
|
'--scrollbar-size': '8px',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let key in cssVars) {
|
||||||
|
rootStyle.setProperty(key, cssVars[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 highlight.js */
|
||||||
|
function initHighlightJs() {
|
||||||
|
hljs.registerLanguage('javascript', hljsJavascript);
|
||||||
|
hljs.registerLanguage('json', hljsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCssVars();
|
||||||
|
initHighlightJs();
|
||||||
|
window.addEventListener('contextmenu', handleContextMenu);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('contextmenu', handleContextMenu);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.loading-bar {
|
// 滚动条
|
||||||
position: absolute;
|
::-webkit-scrollbar {
|
||||||
z-index: 1000;
|
width: var(--scrollbar-size);
|
||||||
top: 0;
|
height: var(--scrollbar-size);
|
||||||
left: 0;
|
}
|
||||||
width: 100%;
|
::-webkit-scrollbar-corner {
|
||||||
height: 0.2rem;
|
display: none;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #CFCFCF;
|
||||||
|
|
||||||
.bar-content {
|
&:hover {
|
||||||
position: absolute;
|
background-color: #C0C0C0;
|
||||||
top: 0;
|
}
|
||||||
left: 0;
|
}
|
||||||
width: 0;
|
|
||||||
|
// 预设样式
|
||||||
|
.shadow-1 {
|
||||||
|
box-shadow: var(--box-shadow-1);
|
||||||
|
}
|
||||||
|
.shadow-2 {
|
||||||
|
box-shadow: var(--box-shadow-2);
|
||||||
|
}
|
||||||
|
.shadow-3 {
|
||||||
|
box-shadow: var(--box-shadow-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #app, .n-config-provider {
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: @colorPrimary;
|
overflow: hidden;
|
||||||
animation: loadingBar 4s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
|
html, body {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #FFF;
|
||||||
|
color: var(--color-black);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
@keyframes loadingBar {
|
* {
|
||||||
0% {
|
box-sizing: inherit;
|
||||||
left: 0;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 100%;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-header {
|
.n-config-provider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-aside, .app-view {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-aside-wrapper {
|
||||||
|
width: 64px;
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-view {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0;
|
||||||
|
background-color: var(--color-action);
|
||||||
|
|
||||||
|
&.flex-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-view-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 200;
|
position: relative;
|
||||||
height: @headerHeight !important;
|
padding: 0 16px;
|
||||||
|
height: 64px;
|
||||||
|
background-color: #FFF;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-view-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
|
|
||||||
@media screen and (min-width: 30rem) {
|
&.is-transparent {
|
||||||
.menu .item-dropdown {
|
background-color: transparent;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 30rem) {
|
&.with-margin {
|
||||||
.logo {
|
margin: 20px;
|
||||||
display: none;
|
width: calc(100% - 40px);
|
||||||
}
|
border: 1px solid var(--color-border);
|
||||||
|
|
||||||
.menu {
|
|
||||||
.item-normal {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 1.75em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
// -- Naive UI --
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 1rem;
|
.n-dialog-content--with-max-height {
|
||||||
width: 2rem;
|
max-height: var(--dialog-content-max-height);
|
||||||
height: 2rem;
|
overflow-y: auto;
|
||||||
background-image: url("./assets/icon/favicon.svg");
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
.n-drawer--right-placement {
|
||||||
flex-grow: 1;
|
.n-drawer-body {
|
||||||
height: 2.5rem;
|
height: 0;
|
||||||
border: none !important;
|
}
|
||||||
|
|
||||||
> li {
|
.n-drawer-body-content-wrapper {
|
||||||
padding: 0 1rem;
|
padding: 20px !important;
|
||||||
height: 100%;
|
line-height: 1;
|
||||||
line-height: 2.5rem;
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
color: @colorPrimary !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.n-flex {
|
||||||
padding-left: 0;
|
gap: 10px !important;
|
||||||
font-size: 1.2rem;
|
|
||||||
color: @colorPrimary;
|
|
||||||
opacity: 1;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-container {
|
|
||||||
height: calc(100vh - @headerHeight);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.header-dropdown {
|
|
||||||
position: absolute !important;
|
|
||||||
z-index: 150;
|
|
||||||
top: @headerHeight;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
transition: transform @transitionTime;
|
|
||||||
|
|
||||||
&.show {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu .item-dropdown {
|
|
||||||
i {
|
|
||||||
transition: color @transitionTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active i {
|
|
||||||
color: @colorPrimary;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
// icon 字体路径
|
|
||||||
$--font-path: '~element-ui/lib/theme-chalk/fonts';
|
|
||||||
|
|
||||||
// 颜色
|
|
||||||
$--color-primary: #2196F3;
|
|
||||||
$--color-success: #4CAF50;
|
|
||||||
$--color-text-primary: #555555;
|
|
||||||
$--color-text-secondary: #909090;
|
|
||||||
|
|
||||||
@import "~element-ui/packages/theme-chalk/src/index";
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
// 全局样式
|
|
||||||
|
|
||||||
// 滚动条
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-corner {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 1rem;
|
|
||||||
background-color: #DFDFDF;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #CCCCCC;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选中文本
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background-color: @colorPrimary;
|
|
||||||
color: #FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标签
|
|
||||||
|
|
||||||
html {
|
|
||||||
transition: font-size calc(@transitionTime * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: @textPrimary;
|
|
||||||
overflow: hidden;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 限制长度
|
|
||||||
|
|
||||||
.limitLine(@line) {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: @line;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.limit-line-1 {
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.limit-line-2 {
|
|
||||||
.limitLine(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.limit-line-3 {
|
|
||||||
.limitLine(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 背景居中
|
|
||||||
|
|
||||||
.bgCenter() {
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-center-contain {
|
|
||||||
.bgCenter();
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-center-cover {
|
|
||||||
.bgCenter();
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 阴影
|
|
||||||
|
|
||||||
.shadow-1 {
|
|
||||||
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-2 {
|
|
||||||
box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-3 {
|
|
||||||
box-shadow: 0 0.5rem 1rem -0.4rem rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 载入中(链接列表)
|
|
||||||
|
|
||||||
.loading-link {
|
|
||||||
background-color: rgba(255, 255, 255, 0.5) !important;
|
|
||||||
backdrop-filter: blur(0.2rem);
|
|
||||||
|
|
||||||
.el-loading-spinner {
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon-loading {
|
|
||||||
font-size: 2rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-loading-text {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.25rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Element UI
|
|
||||||
|
|
||||||
.el-checkbox {
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-drawer.drawer-full {
|
|
||||||
.el-drawer__header {
|
|
||||||
z-index: 500;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 1rem 1.5rem !important;
|
|
||||||
background-color: @colorPrimary;
|
|
||||||
color: #FFF;
|
|
||||||
.shadow-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-drawer__body {
|
|
||||||
padding: 2rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-form-item {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input {
|
|
||||||
&.is-disabled .el-input__inner {
|
|
||||||
background-color: #FFF;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
input::-webkit-inner-spin-button,
|
|
||||||
input::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="number"] {
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-link {
|
|
||||||
font-size: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu-item {
|
|
||||||
.fa {
|
|
||||||
margin-right: 0.3125rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message {
|
|
||||||
width: 80%;
|
|
||||||
min-width: 16rem;
|
|
||||||
max-width: 25rem;
|
|
||||||
.shadow-2;
|
|
||||||
|
|
||||||
&.el-message--info {
|
|
||||||
background-color: #FFF;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message-box {
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 90%;
|
|
||||||
min-width: 16rem;
|
|
||||||
max-width: 25rem;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select-dropdown {
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-switch {
|
|
||||||
.el-switch__core {
|
|
||||||
width: 2.5rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// 全局变量
|
|
||||||
|
|
||||||
@headerHeight: 4rem;
|
|
||||||
|
|
||||||
@colorPrimary: #2196F3;
|
|
||||||
@colorPrimaryDark: #1976D2;
|
|
||||||
@colorPrimaryLight: #BBDEFB;
|
|
||||||
@colorSecondary: #4CAF50;
|
|
||||||
@colorGrey: #BDBDBD;
|
|
||||||
@colorRed: #F44336;
|
|
||||||
@colorWhite: #F9F9F9;
|
|
||||||
|
|
||||||
@textPrimary: #555555;
|
|
||||||
@textSecondary: #909090;
|
|
||||||
|
|
||||||
@transitionTime: 0.25s;
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "HarmonyOS Sans SC";
|
||||||
|
font-weight: normal;
|
||||||
|
src: url("./HarmonyOS_Sans_SC_Regular.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "HarmonyOS Sans SC";
|
||||||
|
font-weight: bold;
|
||||||
|
src: url("./HarmonyOS_Sans_SC_Bold.ttf");
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<svg width="1024" height="1024" xmlns="http://www.w3.org/2000/svg" class="icon">
|
|
||||||
<path fill="#2196F3" d="m170.66667,554.66667m42.66666,0l213.33334,0q42.66666,0 42.66666,42.66666l0,213.33334q0,42.66666 -42.66666,42.66666l-213.33334,0q-42.66666,0 -42.66666,-42.66666l0,-213.33334q0,-42.66666 42.66666,-42.66666z"/>
|
|
||||||
<path fill="#2196F3" d="m554.66667,554.66667m42.66666,0l213.33334,0q42.66666,0 42.66666,42.66666l0,213.33334q0,42.66666 -42.66666,42.66666l-213.33334,0q-42.66666,0 -42.66666,-42.66666l0,-213.33334q0,-42.66666 42.66666,-42.66666z"/>
|
|
||||||
<path fill="#2196F3" d="m170.66667,170.66667m42.66666,0l213.33334,0q42.66666,0 42.66666,42.66666l0,213.33334q0,42.66666 -42.66666,42.66666l-213.33334,0q-42.66666,0 -42.66666,-42.66666l0,-213.33334q0,-42.66666 42.66666,-42.66666z"/>
|
|
||||||
<path transform="rotate(45 703.333251953125,320.3332824707031)" fill="#2196F3" d="m573,189.99999m37.23807,0l186.19041,0q37.23807,0 37.23807,37.23807l0,186.19041q0,37.23807 -37.23807,37.23807l-186.19041,0q-37.23807,0 -37.23807,-37.23807l0,-186.19041q0,-37.23807 37.23807,-37.23807z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,2 +0,0 @@
|
|||||||
<svg class="icon" viewBox="0 0 1056 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M516.384 96c229.664 0 415.872 186.208 415.872 415.872 0 229.696-186.208 415.904-415.872 415.904-229.696 0-415.904-186.208-415.904-415.904C100.48 282.24 286.72 96 516.384 96z m0 64C322.048 160 164.48 317.536 164.48 511.872s157.568 351.904 351.904 351.904c194.336 0 351.872-157.568 351.872-351.904C868.256 317.536 710.72 160 516.384 160z m8.64 515.392a32 32 0 1 1 0 64 32 32 0 0 1 0-64z m0-387.392c98.464 0 157.184 90.656 127.872 166.72-9.6 24.992-22.72 44.608-42.4 66.368l-11.84 12.576-17.024 17.6c-18.24 19.36-24.576 31.104-24.576 46.784a32 32 0 1 1-64 0c0-32.992 11.84-57.12 36.48-84.672l5.44-5.952c4.16-4.384 18.752-19.328 21.472-22.176 18.752-19.68 29.472-34.688 36.736-53.536 13.472-34.944-15.52-79.712-68.16-79.712-48.064 0-74.816 32.224-73.28 59.456l0.384 3.52a32 32 0 1 1-63.264 9.6C379.008 359.52 433.92 288 525.056 288z" fill="#555555"></path></svg>
|
|
||||||
<!-- IconFont UID: 399435 -->
|
|
||||||
|
Before Width: | Height: | Size: 989 B |
@@ -1,14 +0,0 @@
|
|||||||
let config = {
|
|
||||||
loading: {
|
|
||||||
subPage: false
|
|
||||||
},
|
|
||||||
storage: {
|
|
||||||
fontSize: 16,
|
|
||||||
showSiteTitle: true,
|
|
||||||
sideMenuCollapse: true,
|
|
||||||
searchEngine: '百度',
|
|
||||||
searchSuggestion: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// 本地储存
|
||||||
|
|
||||||
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
|
|
||||||
|
/** 本地储存 key 前缀 */
|
||||||
|
export const KEY_PREFIX = 'frost-navigation/';
|
||||||
|
|
||||||
|
/** NavView 模块 */
|
||||||
|
export const storeNavView = {
|
||||||
|
|
||||||
|
/** 导航链接侧边栏折叠状态 */
|
||||||
|
isAsideCollapsed: useLocalStorage(KEY_PREFIX + 'nav-view/isAsideCollapsed', false),
|
||||||
|
|
||||||
|
/** 导航链接当前选中分类 */
|
||||||
|
currentCategory: useLocalStorage(KEY_PREFIX + 'nav-view/currentCategory', ''),
|
||||||
|
|
||||||
|
/** 导航链接搜索类型 */
|
||||||
|
searchType: useLocalStorage(KEY_PREFIX + 'nav-view/searchType', 'all'),
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/** SearchView 模块 */
|
||||||
|
export const storeSearchView = {
|
||||||
|
|
||||||
|
/** 当前使用的搜索引擎名称 */
|
||||||
|
searchEngineName: useLocalStorage(KEY_PREFIX + 'search-view/searchEngineName', '必应'),
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||||
|
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||||
|
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
||||||
|
|
||||||
|
/** 初始化 MonacoEnvironment */
|
||||||
|
export function initMonacoEnvironment() {
|
||||||
|
if (!self.MonacoEnvironment) {
|
||||||
|
// 配置编辑器环境(Service Worker 等)
|
||||||
|
self.MonacoEnvironment = {
|
||||||
|
getWorker(workerId, label) {
|
||||||
|
if (label === 'javascript' || label === 'typescript') {
|
||||||
|
return new tsWorker();
|
||||||
|
} else if (label === 'json') {
|
||||||
|
return new jsonWorker();
|
||||||
|
} else {
|
||||||
|
return new editorWorker();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Naive UI 配置和功能
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDiscreteApi,
|
||||||
|
dateZhCN, zhCN,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
/** @type { import('naive-ui').ConfigProviderProps } */
|
||||||
|
export const configProviderProps = {
|
||||||
|
|
||||||
|
// 语言
|
||||||
|
dateLocale: dateZhCN,
|
||||||
|
locale: zhCN,
|
||||||
|
|
||||||
|
// 禁用 inline 主题
|
||||||
|
inlineThemeDisabled: true,
|
||||||
|
|
||||||
|
// 调整主题变量
|
||||||
|
themeOverrides: {
|
||||||
|
common: {
|
||||||
|
borderRadius: '4px',
|
||||||
|
borderRadiusSmall: '2px',
|
||||||
|
errorColor: '#E34D59',
|
||||||
|
errorColorHover: '#F36D78',
|
||||||
|
errorColorPressed: '#C9353F',
|
||||||
|
errorColorSuppl: '#F36D78',
|
||||||
|
fontFamily: '"HarmonyOS Sans SC", sans-serif',
|
||||||
|
fontFamilyMono: '"Sarasa Mono", "Sarasa Mono SC", monospace',
|
||||||
|
infoColor: '#0052D9',
|
||||||
|
infoColorHover: '#366EF4',
|
||||||
|
infoColorPressed: '#003CAB',
|
||||||
|
infoColorSuppl: '#366EF4',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
primaryColor: '#0894FA',
|
||||||
|
primaryColorHover: '#6BB2FF',
|
||||||
|
primaryColorPressed: '#007AD3',
|
||||||
|
primaryColorSuppl: '#6BB2FF',
|
||||||
|
successColor: '#00A870',
|
||||||
|
successColorHover: '#48C79C',
|
||||||
|
successColorPressed: '#078D5C',
|
||||||
|
successColorSuppl: '#48C79C',
|
||||||
|
warningColor: '#ED7B2F',
|
||||||
|
warningColorHover: '#F2995F',
|
||||||
|
warningColorPressed: '#D35A21',
|
||||||
|
warningColorSuppl: '#F2995F',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export const {
|
||||||
|
dialog: $dialog,
|
||||||
|
message: $message,
|
||||||
|
notification: $notification,
|
||||||
|
} = createDiscreteApi(
|
||||||
|
[
|
||||||
|
'dialog',
|
||||||
|
'message',
|
||||||
|
'notification',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
configProviderProps: configProviderProps,
|
||||||
|
messageProviderProps: {
|
||||||
|
closable: true,
|
||||||
|
duration: 2000,
|
||||||
|
keepAliveOnHover: true,
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
notificationProviderProps: {
|
||||||
|
keepAliveOnHover: true,
|
||||||
|
placement: 'top-right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* @description 处理导航链接列表
|
||||||
|
* @param {boolean} withAll 是否添加“全部”项
|
||||||
|
*/
|
||||||
|
export function formatNavLinks(withAll = false) {
|
||||||
|
|
||||||
|
/** @type {NavLinkItem[]} */
|
||||||
|
let list0 = JSON.parse(JSON.stringify(window['NAV_LINK_LIST']));
|
||||||
|
|
||||||
|
/** @type {NavLinkItem[]} */
|
||||||
|
let list1 = [];
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
(function fn(treeData, target) {
|
||||||
|
for (let i = 0; i < treeData.length; i++) {
|
||||||
|
|
||||||
|
let {
|
||||||
|
title, children, date, desc,
|
||||||
|
icon, isInvalid, showOnly, url,
|
||||||
|
} = treeData[i];
|
||||||
|
|
||||||
|
/** @type {NavLinkItem} */
|
||||||
|
let data = {
|
||||||
|
title: (typeof title === 'string' ? title : ''),
|
||||||
|
children: [],
|
||||||
|
date: (typeof date === 'string' ? date : ''),
|
||||||
|
desc: (typeof desc === 'string' ? desc : ''),
|
||||||
|
icon: (typeof icon === 'string' ? icon : ''),
|
||||||
|
isInvalid: (typeof isInvalid === 'boolean' ? isInvalid : false),
|
||||||
|
showOnly: (typeof showOnly === 'boolean' ? showOnly : false),
|
||||||
|
url: (typeof url === 'string' ? url : ''),
|
||||||
|
_key: `item_${++index}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加项
|
||||||
|
target && target.push(data);
|
||||||
|
|
||||||
|
// 递归处理
|
||||||
|
if (children) {
|
||||||
|
fn(children, data.children);
|
||||||
|
} else {
|
||||||
|
data.children = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})(list0, list1);
|
||||||
|
|
||||||
|
if (withAll) {
|
||||||
|
list1.unshift({
|
||||||
|
title: '全部',
|
||||||
|
icon: 'mdi mdi-view-grid',
|
||||||
|
children: JSON.parse(JSON.stringify(list1)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return list1;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { Notification } from 'element-ui';
|
|
||||||
|
|
||||||
/** 初始化导航链接数据,设置唯一 ID */
|
|
||||||
const fnInit = function () {
|
|
||||||
let index = 0;
|
|
||||||
function fn(obj) {
|
|
||||||
// 更新
|
|
||||||
index += 1;
|
|
||||||
// 设置 ID
|
|
||||||
obj.id = index;
|
|
||||||
// 递归
|
|
||||||
if (obj.items) {
|
|
||||||
obj.items.forEach((item) => {
|
|
||||||
fn(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fn;
|
|
||||||
};
|
|
||||||
|
|
||||||
const datas = {
|
|
||||||
list: [],
|
|
||||||
version: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
|
|
||||||
const el = document.createElement('script');
|
|
||||||
|
|
||||||
// 加载失败
|
|
||||||
el.onerror = function () {
|
|
||||||
Notification.error({
|
|
||||||
title: '错误',
|
|
||||||
message: '导航链接载入失败',
|
|
||||||
duration: 0
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 加载成功
|
|
||||||
el.onload = function () {
|
|
||||||
|
|
||||||
const { frostNavLinks } = window;
|
|
||||||
|
|
||||||
// 属性不存在
|
|
||||||
if (typeof frostNavLinks === 'undefined') {
|
|
||||||
Notification.error({
|
|
||||||
title: '错误',
|
|
||||||
message: '导航链接载入失败',
|
|
||||||
duration: 0
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: navList,
|
|
||||||
version: navVersion,
|
|
||||||
} = frostNavLinks;
|
|
||||||
|
|
||||||
const init = fnInit();
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
navList.forEach((item) => {
|
|
||||||
init(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
datas.list = navList;
|
|
||||||
datas.version = navVersion;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
el.setAttribute('type', 'text/javascript');
|
|
||||||
el.setAttribute('src', 'static/js/frostNavLinks.js');
|
|
||||||
|
|
||||||
document.body.appendChild(el);
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
export default datas;
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {object} ToolItem
|
|
||||||
* @property {string} name 工具名称
|
|
||||||
* @property {string} title 工具标题
|
|
||||||
* @property {string} [desc] 工具简介
|
|
||||||
* @property {string} component 组件名称
|
|
||||||
* @property {string} [update] 更新日期
|
|
||||||
* @property {string} [version] 版本号
|
|
||||||
* @property {boolean} enabled 启用状态
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} ToolCategory
|
|
||||||
* @property {string} title 分类标题
|
|
||||||
* @property {ToolItem[]} list 工具列表
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {Object.<string, ToolCategory>} */
|
|
||||||
const navTools = {
|
|
||||||
calculation: {
|
|
||||||
title: '计算',
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
name: 'calc-download-time',
|
|
||||||
component: 'CalcDownloadTime',
|
|
||||||
title: '下载用时计算',
|
|
||||||
desc: '根据设定的文件大小和下载速度简单计算大约下载完成所需的时间。',
|
|
||||||
update: '2021-12-06',
|
|
||||||
version: '1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'calc-ratio',
|
|
||||||
component: 'CalcRatio',
|
|
||||||
title: '比例计算',
|
|
||||||
desc: '按设定的比例计算给出的数值所对应的数值。',
|
|
||||||
update: '2021-11-16',
|
|
||||||
version: '2',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
generator: {
|
|
||||||
title: '生成',
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
name: 'gen-links',
|
|
||||||
component: 'GenLinks',
|
|
||||||
title: '生成批量下载链接',
|
|
||||||
desc: '根据设置,生成有一定规律的用于批量下载的链接。',
|
|
||||||
update: '2021-11-14',
|
|
||||||
version: '1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'gen-random-str',
|
|
||||||
component: 'GenRandomStr',
|
|
||||||
title: '生成随机字符串',
|
|
||||||
desc: '生成随机组合的字符串,可用于密码。',
|
|
||||||
update: '2021-05-04',
|
|
||||||
version: '1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
conversion: {
|
|
||||||
title: '转换',
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
name: 'convert-encode-decode',
|
|
||||||
component: 'ConvertEncodeDecode',
|
|
||||||
title: '编码转换',
|
|
||||||
desc: 'HTML / URI 编码、解码',
|
|
||||||
update: '2021-11-10',
|
|
||||||
version: '1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'convert-text-structure',
|
|
||||||
component: 'ConvertTextStructure',
|
|
||||||
title: '文本结构转换',
|
|
||||||
desc: '倒序、横竖互换等',
|
|
||||||
update: '2021-12-06',
|
|
||||||
version: '1',
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'convert-timestamp',
|
|
||||||
component: 'ConvertTimestamp',
|
|
||||||
title: 'Unix 时间戳转换',
|
|
||||||
desc: '时间戳转时间 / 时间转时间戳',
|
|
||||||
update: '2021-11-14',
|
|
||||||
version: '1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
minecraft: {
|
|
||||||
title: 'Minecraft',
|
|
||||||
list: [
|
|
||||||
// {
|
|
||||||
// name: 'minecraft-chunk-location-calc',
|
|
||||||
// component: 'MinecraftChunkLocationCalc',
|
|
||||||
// title: 'Minecraft 区块位置计算',
|
|
||||||
// enabled: false
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
name: 'minecraft-dynmap-renderdata-gen',
|
|
||||||
component: 'MinecraftDynmapRenderdataGen',
|
|
||||||
title: 'Dynmap renderdata 生成',
|
|
||||||
desc: '生成用于 Minecraft Dynmap 插件或模组的 renderdata 数据。',
|
|
||||||
update: '2021-12-06',
|
|
||||||
version: '1',
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'minecraft-uuid-converter',
|
|
||||||
component: 'MinecraftUUIDConverter',
|
|
||||||
title: 'Minecraft UUID 转换',
|
|
||||||
desc: '随机生成或转换 Minecraft 的 UUID。',
|
|
||||||
update: '2021-12-06',
|
|
||||||
version: '1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
other: {
|
|
||||||
title: '其他',
|
|
||||||
list: [
|
|
||||||
// {
|
|
||||||
// name: 'other-edit-csv',
|
|
||||||
// component: 'OtherEditCSV',
|
|
||||||
// title: 'CSV 编辑工具',
|
|
||||||
// desc: '查看或编辑 CSV 文件',
|
|
||||||
// update: '2022-03-17',
|
|
||||||
// version: '2',
|
|
||||||
// enabled: true,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
name: 'other-new-window',
|
|
||||||
component: 'OtherNewWindow',
|
|
||||||
title: '新窗口(小窗)中打开',
|
|
||||||
desc: '从新的小窗口中打开指定的链接(仅支持 PC 端浏览器)',
|
|
||||||
update: '2021-05-03',
|
|
||||||
version: '1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'other-run-js',
|
|
||||||
component: 'OtherRunJS',
|
|
||||||
title: '执行 JavaScript',
|
|
||||||
desc: '执行简单的 JavaScript 代码片段',
|
|
||||||
update: '2021-06-14',
|
|
||||||
version: '2',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'other-websocket',
|
|
||||||
component: 'OtherWebSocket',
|
|
||||||
title: 'WebSocket',
|
|
||||||
desc: 'WebSocket 测试工具',
|
|
||||||
update: '2021-11-19',
|
|
||||||
version: '4',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default navTools;
|
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
/** PNG 转 ICO 图标,支持生成多尺寸 ICO */
|
||||||
|
class PNGtoICOConverter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 将 PNG 图像转换为 ICO 格式
|
||||||
|
* @param {File|Blob|string} source PNG 文件、Blob 或 DataURL
|
||||||
|
* @param {number[]} sizes 需要生成的尺寸数组,默认 `[32, 128, 256]`
|
||||||
|
* @returns {Promise<Blob>} ICO 文件的 Blob 对象
|
||||||
|
*/
|
||||||
|
static async convert(source, sizes = [32, 128, 256]) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// 1. 加载 PNG 图像
|
||||||
|
let pngBlob = await this._preparePNG(source);
|
||||||
|
let imageBitmap = await createImageBitmap(pngBlob);
|
||||||
|
|
||||||
|
// 2. 创建各尺寸的图像
|
||||||
|
let imageDatas = await this._createMultiSizeImages(imageBitmap, sizes);
|
||||||
|
|
||||||
|
// 3. 生成 ICO 文件
|
||||||
|
let icoBlob = await this._generateICO(imageDatas);
|
||||||
|
|
||||||
|
return icoBlob;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`PNG 转 ICO 失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 准备 PNG 源数据
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static async _preparePNG(source) {
|
||||||
|
|
||||||
|
if (source instanceof File || source instanceof Blob) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof source === 'string') {
|
||||||
|
if (source.startsWith('data:image/png')) {
|
||||||
|
const response = await fetch(source);
|
||||||
|
return await response.blob();
|
||||||
|
} else {
|
||||||
|
throw new Error('不支持的数据格式,仅支持 PNG 文件、Blob 或 DataURL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('无效的输入类型');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 创建多尺寸图像数据
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static async _createMultiSizeImages(imageBitmap, sizes) {
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
for (let size of sizes) {
|
||||||
|
|
||||||
|
// 按尺寸创建 Canvas
|
||||||
|
let canvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// 设置高质量缩放
|
||||||
|
ctx.imageSmoothingEnabled = true;
|
||||||
|
ctx.imageSmoothingQuality = 'high';
|
||||||
|
|
||||||
|
// 绘制缩放后的图像
|
||||||
|
ctx.drawImage(imageBitmap, 0, 0, size, size);
|
||||||
|
|
||||||
|
// 获取 PNG 数据
|
||||||
|
let pngBlob = await new Promise(resolve => {
|
||||||
|
canvas.toBlob(resolve, 'image/png');
|
||||||
|
});
|
||||||
|
|
||||||
|
let arrayBuffer = await pngBlob.arrayBuffer();
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
size,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
data: new Uint8Array(arrayBuffer),
|
||||||
|
pngSize: arrayBuffer.byteLength
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 生成 ICO 文件
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static async _generateICO(imageDatas) {
|
||||||
|
|
||||||
|
// ICO 文件结构:
|
||||||
|
// 1. ICONDIR 头 (6 字节)
|
||||||
|
// 2. ICONDIRENTRY 数组 (每个 16 字节)
|
||||||
|
// 3. PNG 数据
|
||||||
|
|
||||||
|
let numImages = imageDatas.length;
|
||||||
|
|
||||||
|
// 计算总大小
|
||||||
|
let totalSize = 6; // ICONDIR 头
|
||||||
|
totalSize += numImages * 16; // ICONDIRENTRY 数组
|
||||||
|
totalSize += imageDatas.reduce((sum, img) => sum + img.pngSize, 0);
|
||||||
|
|
||||||
|
// 创建 ArrayBuffer
|
||||||
|
let buffer = new ArrayBuffer(totalSize);
|
||||||
|
let view = new DataView(buffer);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
// 1. 写入 ICONDIR 头
|
||||||
|
view.setUint16(offset, 0, true); // Reserved (0)
|
||||||
|
offset += 2;
|
||||||
|
view.setUint16(offset, 1, true); // Type (1 for ICO)
|
||||||
|
offset += 2;
|
||||||
|
view.setUint16(offset, numImages, true); // Number of images
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
// 2. 写入 ICONDIRENTRY 数组
|
||||||
|
let dataOffset = 6 + (numImages * 16);
|
||||||
|
|
||||||
|
for (let img of imageDatas) {
|
||||||
|
|
||||||
|
// 宽度和高度 (0 表示 256 像素)
|
||||||
|
view.setUint8(offset, img.width === 256 ? 0 : img.width);
|
||||||
|
offset += 1;
|
||||||
|
view.setUint8(offset, img.height === 256 ? 0 : img.height);
|
||||||
|
offset += 1;
|
||||||
|
|
||||||
|
view.setUint8(offset, 0); // Color palette (0 for no palette)
|
||||||
|
offset += 1;
|
||||||
|
view.setUint8(offset, 0); // Reserved
|
||||||
|
offset += 1;
|
||||||
|
|
||||||
|
view.setUint16(offset, 1, true); // Color planes
|
||||||
|
offset += 2;
|
||||||
|
view.setUint16(offset, 32, true); // Bits per pixel
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
view.setUint32(offset, img.pngSize, true); // PNG data size
|
||||||
|
offset += 4;
|
||||||
|
view.setUint32(offset, dataOffset, true); // PNG data offset
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
dataOffset += img.pngSize;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 写入 PNG 数据
|
||||||
|
for (let img of imageDatas) {
|
||||||
|
let uint8View = new Uint8Array(buffer, offset, img.pngSize);
|
||||||
|
uint8View.set(img.data);
|
||||||
|
offset += img.pngSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Blob([buffer], { type: 'image/x-icon' });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 下载 ICO 文件
|
||||||
|
* @param {Blob} icoBlob ICO 文件的 Blob 对象
|
||||||
|
* @param {string} filename 下载的文件名
|
||||||
|
*/
|
||||||
|
static download(icoBlob, filename = 'icon.ico') {
|
||||||
|
|
||||||
|
let url = URL.createObjectURL(icoBlob);
|
||||||
|
let a = document.createElement('a');
|
||||||
|
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 将 PNG 转换为 ICO 并下载
|
||||||
|
* @param {File|Blob|string} source PNG 文件、Blob 或 DataURL
|
||||||
|
* @param {number[]} sizes 需要生成的尺寸数组
|
||||||
|
* @param {string} filename 下载的文件名
|
||||||
|
*/
|
||||||
|
export async function convertPNGtoICO(source, sizes, filename) {
|
||||||
|
try {
|
||||||
|
let blob = await PNGtoICOConverter.convert(source, sizes);
|
||||||
|
PNGtoICOConverter.download(blob, filename);
|
||||||
|
return { blob: blob, error: '', success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('转换失败:', error);
|
||||||
|
return { blob: null, error: error.message, success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
import {
|
||||||
|
prepareZXingModule,
|
||||||
|
readBarcodes,
|
||||||
|
writeBarcode,
|
||||||
|
} from 'zxing-wasm/full';
|
||||||
|
|
||||||
|
/** 默认背景颜色 */
|
||||||
|
const DEFAULT_BGC = 'transparent';
|
||||||
|
|
||||||
|
/** 默认前景颜色 */
|
||||||
|
const DEFAULT_FGC = '#000000';
|
||||||
|
|
||||||
|
/** 模块名称 */
|
||||||
|
const PREFIX = '[qr-code]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 二维码读取配置选项
|
||||||
|
* @type { import('zxing-wasm').ReaderOptions }
|
||||||
|
*/
|
||||||
|
const readerOptions = {
|
||||||
|
formats: ['QRCode'],
|
||||||
|
maxNumberOfSymbols: 8,
|
||||||
|
textMode: 'Plain',
|
||||||
|
tryDownscale: true,
|
||||||
|
tryHarder: true,
|
||||||
|
tryInvert: true,
|
||||||
|
tryRotate: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 二维码生成配置选项
|
||||||
|
* @type { import('zxing-wasm').WriterOptions }
|
||||||
|
*/
|
||||||
|
const writerOptions = {
|
||||||
|
ecLevel: '',
|
||||||
|
format: 'QRCode',
|
||||||
|
scale: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 配置 wasm 文件路径
|
||||||
|
prepareZXingModule({
|
||||||
|
overrides: {
|
||||||
|
locateFile: (path, prefix) => {
|
||||||
|
if (path.endsWith('.wasm')) {
|
||||||
|
return `./wasm/${path}`;
|
||||||
|
} else {
|
||||||
|
return (prefix + path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 在图片上绘制矩形,返回 DataURL
|
||||||
|
* @param {Blob} blob 图片二进制
|
||||||
|
* @param {Rect[]} rects 矩形位置信息列表
|
||||||
|
*/
|
||||||
|
function drawRectsOnImage(blob, rects) {
|
||||||
|
|
||||||
|
/** @typedef {{ x: number; y: number; w: number; h: number }} Rect */
|
||||||
|
|
||||||
|
let canvas = document.createElement('canvas');
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
return renderImageToCanvas(blob, canvas).then((success) => {
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
rects.forEach((rect, index) => {
|
||||||
|
|
||||||
|
let { x, y, w, h } = rect;
|
||||||
|
|
||||||
|
let text = String(index + 1);
|
||||||
|
|
||||||
|
// 绘制矩形
|
||||||
|
ctx.fillStyle = 'rgba(0, 255, 0, 0.25)';
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.strokeStyle = 'rgba(0, 255, 0, 1)';
|
||||||
|
ctx.fillRect(x, y, w, h);
|
||||||
|
ctx.strokeRect(x, y, w, h);
|
||||||
|
|
||||||
|
// 绘制文本
|
||||||
|
ctx.font = `bold ${Math.round(w / 2)}px sans-serif`;
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
|
||||||
|
ctx.lineWidth = Number((w / 100).toFixed(4));
|
||||||
|
ctx.strokeStyle = 'rgba(0, 0, 0, 1)';
|
||||||
|
ctx.fillText(text, x, y + h);
|
||||||
|
ctx.strokeText(text, x, y + h);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 修改 SVG 内容,获取信息
|
||||||
|
* @param {object} options
|
||||||
|
* @param {string} options.background
|
||||||
|
* @param {string} options.foreground
|
||||||
|
* @param {string} options.svgString
|
||||||
|
*/
|
||||||
|
function modifySvgContent(options) {
|
||||||
|
|
||||||
|
let {
|
||||||
|
background = DEFAULT_BGC,
|
||||||
|
foreground = DEFAULT_FGC,
|
||||||
|
svgString,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let divElement = document.createElement('div');
|
||||||
|
let gElement = null;
|
||||||
|
let pathElement = null;
|
||||||
|
let rectElement = null;
|
||||||
|
let svgElement = null;
|
||||||
|
|
||||||
|
// 添加 DOM 元素,用于获取位置大小信息
|
||||||
|
document.body.appendChild(divElement);
|
||||||
|
|
||||||
|
divElement.innerHTML = svgString;
|
||||||
|
gElement = divElement.getElementsByTagName('g')[0] || null;
|
||||||
|
pathElement = divElement.getElementsByTagName('path')[0] || null;
|
||||||
|
rectElement = divElement.getElementsByTagName('rect')[0] || null;
|
||||||
|
svgElement = divElement.getElementsByTagName('svg')[0] || null;
|
||||||
|
|
||||||
|
if (!(gElement && pathElement && rectElement && svgElement)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改填充颜色
|
||||||
|
gElement.setAttribute('fill', foreground);
|
||||||
|
rectElement.setAttribute('fill', background);
|
||||||
|
|
||||||
|
// 获取位置大小信息
|
||||||
|
let rectOfG = gElement.getBoundingClientRect();
|
||||||
|
let rectOfPath = pathElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
let offsetX = Math.round(rectOfPath.left - rectOfG.left);
|
||||||
|
let offsetY = Math.round(rectOfPath.top - rectOfG.top);
|
||||||
|
let sizeW = Math.round(rectOfPath.width);
|
||||||
|
let sizeH = Math.round(rectOfPath.height);
|
||||||
|
let result = {
|
||||||
|
offsetX: offsetX,
|
||||||
|
offsetY: offsetY,
|
||||||
|
sizeW: sizeW,
|
||||||
|
sizeH: sizeH,
|
||||||
|
svgString: divElement.innerHTML,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 输出处理结果
|
||||||
|
console.debug(PREFIX, '处理 SVG', result);
|
||||||
|
|
||||||
|
// 处理完成,移除 DOM 元素
|
||||||
|
document.body.removeChild(divElement);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片 Blob 渲染到 Canvas
|
||||||
|
* @param {Blob} blob
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
function renderImageToCanvas(blob, canvas) {
|
||||||
|
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (window.createImageBitmap) {
|
||||||
|
return createImageBitmap(blob).then((bitmap) => {
|
||||||
|
canvas.width = bitmap.width;
|
||||||
|
canvas.height = bitmap.height;
|
||||||
|
ctx.drawImage(bitmap, 0, 0);
|
||||||
|
return true;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('渲染图片失败:');
|
||||||
|
console.error(error);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
|
||||||
|
let image = new Image();
|
||||||
|
let url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
image.onload = () => {
|
||||||
|
canvas.width = image.naturalWidth;
|
||||||
|
canvas.height = image.naturalHeight;
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
image.onerror = () => {
|
||||||
|
console.error(PREFIX, '渲染图片失败:加载图片失败');
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
image.src = url;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 将 SVG 字符串渲染到 Canvas
|
||||||
|
* @param {object} options
|
||||||
|
* @param {HTMLCanvasElement} options.canvas
|
||||||
|
* @param {string} options.svgString
|
||||||
|
* @param {string} options.background
|
||||||
|
* @param {string} options.foreground
|
||||||
|
* @param {number} options.drawLeft
|
||||||
|
* @param {number} options.drawTop
|
||||||
|
* @param {number} options.drawWidth
|
||||||
|
* @param {number} options.drawHeight
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
function renderSvgToCanvas(options) {
|
||||||
|
|
||||||
|
let {
|
||||||
|
canvas, svgString,
|
||||||
|
background = DEFAULT_BGC, foreground = DEFAULT_FGC,
|
||||||
|
drawLeft = 0, drawTop = 0,
|
||||||
|
drawWidth = 0, drawHeight = 0,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let svgInfo = modifySvgContent({
|
||||||
|
background: background,
|
||||||
|
foreground: foreground,
|
||||||
|
svgString: svgString,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!svgInfo) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
let image = new Image();
|
||||||
|
let svgBlob = new Blob([svgInfo.svgString], {
|
||||||
|
type: 'image/svg+xml;charset=utf-8',
|
||||||
|
});
|
||||||
|
let svgUrl = URL.createObjectURL(svgBlob);
|
||||||
|
|
||||||
|
image.onerror = () => {
|
||||||
|
console.error(PREFIX, '加载 SVG 失败');
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
image.onload = () => {
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
svgInfo.offsetX, svgInfo.offsetY, svgInfo.sizeW, svgInfo.sizeH,
|
||||||
|
drawLeft, drawTop, drawWidth, drawHeight,
|
||||||
|
);
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
image.src = svgUrl;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 解析二维码图片
|
||||||
|
* @param {Blob} blob 图片二进制
|
||||||
|
*/
|
||||||
|
export function readQrCodeImage(blob) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 返回结果
|
||||||
|
* @type {{ error: string; image: string; textList: string[]; }}
|
||||||
|
*/
|
||||||
|
let result = {
|
||||||
|
error: '',
|
||||||
|
image: '',
|
||||||
|
textList: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return readBarcodes(blob, readerOptions).then((codeList) => {
|
||||||
|
|
||||||
|
let rectList = [];
|
||||||
|
let textList = result.textList;
|
||||||
|
|
||||||
|
if (codeList.length === 0) {
|
||||||
|
console.warn(PREFIX, '解析二维码失败:未识别到内容');
|
||||||
|
} else {
|
||||||
|
console.debug(PREFIX, '解析二维码成功:', codeList);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < codeList.length; i++) {
|
||||||
|
|
||||||
|
let item = codeList[i];
|
||||||
|
let position = item.position;
|
||||||
|
let posX0 = position.topLeft.x;
|
||||||
|
let posX1 = position.bottomRight.x;
|
||||||
|
let posY0 = position.topLeft.y;
|
||||||
|
let posY1 = position.bottomRight.y;
|
||||||
|
|
||||||
|
// 记录二维码坐标
|
||||||
|
rectList.push({
|
||||||
|
x: posX0,
|
||||||
|
y: posY0,
|
||||||
|
w: posX1 - posX0,
|
||||||
|
h: posY1 - posY0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 记录二维码文本
|
||||||
|
textList.push(item.text);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 框选二维码区域
|
||||||
|
return drawRectsOnImage(blob, rectList);
|
||||||
|
|
||||||
|
}).then((dataURL) => {
|
||||||
|
|
||||||
|
if (dataURL) {
|
||||||
|
result.image = dataURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(PREFIX, '解析二维码失败:');
|
||||||
|
console.error(error);
|
||||||
|
result.error = String(error);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 生成二维码图片
|
||||||
|
* @param {object} options
|
||||||
|
* @param {string} options.content
|
||||||
|
* @param {string} options.background
|
||||||
|
* @param {string} options.foreground
|
||||||
|
* @param {number} options.width
|
||||||
|
* @param {number} options.height
|
||||||
|
* @returns 二维码图片 DataURL
|
||||||
|
*/
|
||||||
|
export function writeQrCodeImage(options = {}) {
|
||||||
|
|
||||||
|
let {
|
||||||
|
content = '',
|
||||||
|
background = DEFAULT_BGC, foreground = DEFAULT_FGC,
|
||||||
|
width = 256, height = 256,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let canvas = document.createElement('canvas');
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// 更新画布大小
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
// 设置背景颜色
|
||||||
|
ctx.fillStyle = background;
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
return writeBarcode(content, writerOptions).then((result) => {
|
||||||
|
|
||||||
|
console.debug(PREFIX, '生成二维码', result);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error(PREFIX, `生成二维码失败:${result.error}`);
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return result.svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
}).then((svgString) => {
|
||||||
|
if (svgString) {
|
||||||
|
return renderSvgToCanvas({
|
||||||
|
canvas: canvas,
|
||||||
|
svgString: svgString,
|
||||||
|
background: background,
|
||||||
|
foreground: foreground,
|
||||||
|
drawLeft: 0,
|
||||||
|
drawTop: 0,
|
||||||
|
drawWidth: width,
|
||||||
|
drawHeight: height,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).then((success) => {
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return canvas.toDataURL('image/png');
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(PREFIX, '生成二维码失败:');
|
||||||
|
console.error(error);
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
// 搜索引擎
|
||||||
|
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
storeSearchView,
|
||||||
|
} from './local-storage';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$message,
|
||||||
|
} from './naive-ui';
|
||||||
|
|
||||||
|
import icon_baidu from '@/assets/website-icon/baidu.svg';
|
||||||
|
import icon_bing from '@/assets/website-icon/bing.svg';
|
||||||
|
import icon_sogou from '@/assets/website-icon/sogou.svg';
|
||||||
|
import icon_360 from '@/assets/website-icon/360.svg';
|
||||||
|
import icon_google from '@/assets/website-icon/google.svg';
|
||||||
|
import icon_yandex from '@/assets/website-icon/yandex.svg';
|
||||||
|
import icon_netease_music from '@/assets/website-icon/netease_music.svg';
|
||||||
|
import icon_acfun from '@/assets/website-icon/acfun.svg';
|
||||||
|
import icon_bilibili from '@/assets/website-icon/bilibili.svg';
|
||||||
|
import icon_qq_music from '@/assets/website-icon/qq_music.svg';
|
||||||
|
import icon_cnblogs from '@/assets/website-icon/cnblogs.svg';
|
||||||
|
import icon_csdn from '@/assets/website-icon/csdn.svg';
|
||||||
|
import icon_github from '@/assets/website-icon/github.svg';
|
||||||
|
import icon_mdn from '@/assets/website-icon/mdn.svg';
|
||||||
|
import icon_zcool from '@/assets/website-icon/zcool.svg';
|
||||||
|
import icon_iconfont from '@/assets/website-icon/iconfont.svg';
|
||||||
|
import icon_zhihu from '@/assets/website-icon/zhihu.svg';
|
||||||
|
|
||||||
|
/** 打开搜索结果页面 */
|
||||||
|
export function openSearchResult() {
|
||||||
|
|
||||||
|
let engine = storeSearchView.searchEngineName.value;
|
||||||
|
let keyword = searchKeyword.value;
|
||||||
|
let baseURL = '';
|
||||||
|
let useURL = '';
|
||||||
|
|
||||||
|
// 获取搜索引擎 URL
|
||||||
|
for (let i = 0; i < searchEngineList.length; i++) {
|
||||||
|
|
||||||
|
let category = searchEngineList[i];
|
||||||
|
let engines = category.list;
|
||||||
|
|
||||||
|
for (let j = 0; j < engines.length; j++) {
|
||||||
|
let item = engines[j];
|
||||||
|
if (item.name === engine) {
|
||||||
|
baseURL = item.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseURL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseURL) {
|
||||||
|
useURL = baseURL.replace(/%keyword%/, keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useURL) {
|
||||||
|
window.open(useURL, '_blank');
|
||||||
|
} else {
|
||||||
|
$message.error('搜索失败:找不到对应的搜索引擎');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索引擎列表 */
|
||||||
|
export const searchEngineList = [
|
||||||
|
{
|
||||||
|
title: '搜索',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
name: '百度',
|
||||||
|
desc: 'www.baidu.com',
|
||||||
|
url: 'https://www.baidu.com/s?wd=%keyword%',
|
||||||
|
icon: icon_baidu,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '必应',
|
||||||
|
desc: 'cn.bing.com',
|
||||||
|
url: 'https://cn.bing.com/search?q=%keyword%',
|
||||||
|
icon: icon_bing,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '搜狗',
|
||||||
|
desc: 'www.sogou.com',
|
||||||
|
url: 'https://www.sogou.com/web?query=%keyword%',
|
||||||
|
icon: icon_sogou,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '360',
|
||||||
|
desc: 'www.so.com',
|
||||||
|
url: 'https://www.so.com/s?q=%keyword%',
|
||||||
|
icon: icon_360,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Google',
|
||||||
|
desc: 'www.google.com',
|
||||||
|
url: 'https://www.google.com/search?q=%keyword%',
|
||||||
|
icon: icon_google,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Yandex',
|
||||||
|
desc: 'yandex.com',
|
||||||
|
url: 'https://yandex.com/search/?text=%keyword%',
|
||||||
|
icon: icon_yandex,
|
||||||
|
show: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '多媒体',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
name: '网易云音乐',
|
||||||
|
desc: 'music.163.com',
|
||||||
|
url: 'https://music.163.com/#/search/m/?s=%keyword%',
|
||||||
|
icon: icon_netease_music,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AcFun',
|
||||||
|
desc: 'www.acfun.cn',
|
||||||
|
url: 'https://www.acfun.cn/search/?keyword=%keyword%',
|
||||||
|
icon: icon_acfun,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bilibili',
|
||||||
|
desc: 'search.bilibili.com',
|
||||||
|
url: 'https://search.bilibili.com/all?keyword=%keyword%',
|
||||||
|
icon: icon_bilibili,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QQ音乐',
|
||||||
|
desc: 'y.qq.com',
|
||||||
|
url: 'https://y.qq.com/n/ryqq/search?w=%keyword%',
|
||||||
|
icon: icon_qq_music,
|
||||||
|
show: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '技术',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
name: '博客园',
|
||||||
|
desc: 'zzk.cnblogs.co',
|
||||||
|
url: 'https://zzk.cnblogs.com/s?w=%keyword%',
|
||||||
|
icon: icon_cnblogs,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CSDN',
|
||||||
|
desc: 'so.csdn.net',
|
||||||
|
url: 'https://so.csdn.net/so/search/all?q=%keyword%',
|
||||||
|
icon: icon_csdn,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GitHub',
|
||||||
|
desc: 'github.com',
|
||||||
|
url: 'https://github.com/search?q=%keyword%',
|
||||||
|
icon: icon_github,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MDN',
|
||||||
|
desc: 'developer.mozilla.org',
|
||||||
|
url: 'https://developer.mozilla.org/zh-CN/search?q=%keyword%',
|
||||||
|
icon: icon_mdn,
|
||||||
|
show: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '设计',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
name: '站酷',
|
||||||
|
desc: 'www.zcool.com.cn',
|
||||||
|
url: 'https://www.zcool.com.cn/search/content?word=%keyword%',
|
||||||
|
icon: icon_zcool,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Iconfont',
|
||||||
|
desc: 'www.iconfont.cn',
|
||||||
|
url: 'https://www.iconfont.cn/search/index?searchType=icon&q=%keyword%',
|
||||||
|
icon: icon_iconfont,
|
||||||
|
show: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '社区',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
name: '知乎',
|
||||||
|
desc: 'www.zhihu.com',
|
||||||
|
url: 'https://www.zhihu.com/search?type=content&q=%keyword%',
|
||||||
|
icon: icon_zhihu,
|
||||||
|
show: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 搜索关键词 */
|
||||||
|
export const searchKeyword = ref('');
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
/**
|
|
||||||
* @desc 解析 360 推荐关键词(2021-12-29)
|
|
||||||
* @param {{ result: { word: string }[] }} datas
|
|
||||||
*/
|
|
||||||
function parseSearch360(datas) {
|
|
||||||
const list = datas.result;
|
|
||||||
const words = [];
|
|
||||||
|
|
||||||
list.forEach((item) => {
|
|
||||||
words.push(item.word);
|
|
||||||
});
|
|
||||||
|
|
||||||
return words;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @desc 解析百度推荐关键词(2021-12-29)
|
|
||||||
* @param {{ s: string[] }} datas
|
|
||||||
*/
|
|
||||||
function parseSearchBaidu(datas) {
|
|
||||||
const list = datas.s;
|
|
||||||
return (Array.isArray(list) ? list : []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef WordsBing
|
|
||||||
* @type {{ AS: { Results: { Suggests: { Txt: string }[] } } }}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @desc 解析 Bing 推荐关键词(2021-12-29)
|
|
||||||
* @param {WordsBing} datas
|
|
||||||
*/
|
|
||||||
function parseSearchBing(datas) {
|
|
||||||
const list = datas.AS.Results.Suggests;
|
|
||||||
const words = [];
|
|
||||||
|
|
||||||
list.forEach((item) => {
|
|
||||||
words.push(item.Txt);
|
|
||||||
});
|
|
||||||
|
|
||||||
return words;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef MDNSearchIndexDatas
|
|
||||||
* @type {{ title: string, url: string }[]}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 解析 MDN 搜索关键词
|
|
||||||
* @param {MDNSearchIndexDatas} datas 关键词数据
|
|
||||||
* @param {string} keyword 输入的关键词
|
|
||||||
* @param {number} [max] 最多返回的结果数量,默认为 10
|
|
||||||
* @returns {MDNSearchIndexDatas} 匹配到的关键词
|
|
||||||
*/
|
|
||||||
function parseSearchMDN(datas = [], keyword = '', max = 10) {
|
|
||||||
/** @type {MDNSearchIndexDatas} */
|
|
||||||
const result = [];
|
|
||||||
const word = String(keyword).toLowerCase();
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
for (const item of datas) {
|
|
||||||
if (item.title.toLowerCase().indexOf(word) > -1) {
|
|
||||||
result.push(item);
|
|
||||||
if ((count += 1) >= max) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 获取搜索 URL
|
|
||||||
* @param {string} type 搜索引擎类型
|
|
||||||
* @param {string} word 搜索关键词
|
|
||||||
* @param {string} [cbName] 回调函数名称,用于获取推荐关键词
|
|
||||||
* @returns {(string|null)} 根据参数返回不同的结果
|
|
||||||
* - 无 `cbName` 时返回搜索结果页面 URL
|
|
||||||
* - 有 `cbName` 时返回推荐关键词 URL
|
|
||||||
* - 查找失败时返回 `null`
|
|
||||||
*/
|
|
||||||
function getSearchURL(type, word, cbName) {
|
|
||||||
if (!(type && word)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = searchItems.find((item) => {
|
|
||||||
return (item.name === type);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!info) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
word = window.encodeURI(word);
|
|
||||||
|
|
||||||
const regCb = new RegExp('{cb}');
|
|
||||||
const regW = new RegExp('{w}');
|
|
||||||
|
|
||||||
if (cbName) {
|
|
||||||
const url = info.wordURL;
|
|
||||||
return url.replace(regCb, cbName).replace(regW, word);
|
|
||||||
} else {
|
|
||||||
const url = info.resultURL;
|
|
||||||
return url.replace(regW, word);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 解析推荐的关键词
|
|
||||||
* @param {string} type 来源类型
|
|
||||||
* @param {*} datas 关键词数据
|
|
||||||
* @param {number} [max] 最大返回数量
|
|
||||||
* @returns {string[]} 关键词列表
|
|
||||||
*/
|
|
||||||
function parseSearchWords(type = '', datas = null, max = 10) {
|
|
||||||
if (!datas) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {string[]} */
|
|
||||||
let words = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (type) {
|
|
||||||
case '360':
|
|
||||||
words = parseSearch360(datas);
|
|
||||||
break;
|
|
||||||
case 'baidu':
|
|
||||||
words = parseSearchBaidu(datas);
|
|
||||||
break;
|
|
||||||
case 'bing':
|
|
||||||
words = parseSearchBing(datas);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.error('来源类型错误');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解析失败');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const arrLength = words.length;
|
|
||||||
const cutLength = Math.max(arrLength - max, 0);
|
|
||||||
const cutStart = arrLength - cutLength;
|
|
||||||
|
|
||||||
words.splice(cutStart, cutLength);
|
|
||||||
|
|
||||||
let value = '';
|
|
||||||
|
|
||||||
for (value of words) {
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
console.error('关键词的数据类型错误');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return words;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 搜索引擎列表 */
|
|
||||||
const searchEngines = {
|
|
||||||
search: {
|
|
||||||
title: '搜索',
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
name: '百度',
|
|
||||||
desc: 'www.baidu.com',
|
|
||||||
url: 'https://www.baidu.com/s?wd=%keyword%',
|
|
||||||
icon: 'website/baidu.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '必应',
|
|
||||||
desc: 'cn.bing.com',
|
|
||||||
url: 'https://cn.bing.com/search?q=%keyword%',
|
|
||||||
icon: 'website/bing.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '搜狗',
|
|
||||||
desc: 'www.sogou.com',
|
|
||||||
url: 'https://www.sogou.com/web?query=%keyword%',
|
|
||||||
icon: 'website/sogou.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '360',
|
|
||||||
desc: 'www.so.com',
|
|
||||||
url: 'https://www.so.com/s?q=%keyword%',
|
|
||||||
icon: 'website/360.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Google',
|
|
||||||
desc: 'www.google.com',
|
|
||||||
url: 'https://www.google.com/search?q=%keyword%',
|
|
||||||
icon: 'website/google.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Yandex',
|
|
||||||
desc: 'yandex.com',
|
|
||||||
url: 'https://yandex.com/search/?text=%keyword%',
|
|
||||||
icon: 'website/yandex.svg',
|
|
||||||
show: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
media: {
|
|
||||||
title: '多媒体',
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
name: '网易云音乐',
|
|
||||||
desc: 'music.163.com',
|
|
||||||
url: 'https://music.163.com/#/search/m/?s=%keyword%',
|
|
||||||
icon: 'website/netease_music.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AcFun',
|
|
||||||
desc: 'www.acfun.cn',
|
|
||||||
url: 'https://www.acfun.cn/search/?keyword=%keyword%',
|
|
||||||
icon: 'website/acfun.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'bilibili',
|
|
||||||
desc: 'search.bilibili.com',
|
|
||||||
url: 'https://search.bilibili.com/all?keyword=%keyword%',
|
|
||||||
icon: 'website/bilibili.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'QQ音乐',
|
|
||||||
desc: 'y.qq.com',
|
|
||||||
url: 'https://y.qq.com/n/ryqq/search?w=%keyword%',
|
|
||||||
icon: 'website/qq_music.svg',
|
|
||||||
show: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
technology: {
|
|
||||||
title: '技术',
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
name: 'CSDN',
|
|
||||||
desc: 'so.csdn.net',
|
|
||||||
url: 'https://so.csdn.net/so/search/all?q=%keyword%',
|
|
||||||
icon: 'website/csdn.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GitHub',
|
|
||||||
desc: 'github.com',
|
|
||||||
url: 'https://github.com/search?q=%keyword%',
|
|
||||||
icon: 'website/github.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'MDN',
|
|
||||||
desc: 'developer.mozilla.org',
|
|
||||||
url: 'https://developer.mozilla.org/zh-CN/search?q=%keyword%',
|
|
||||||
icon: 'website/mdn.svg',
|
|
||||||
show: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
design: {
|
|
||||||
title: '设计',
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
name: '站酷',
|
|
||||||
desc: 'www.zcool.com.cn',
|
|
||||||
url: 'https://www.zcool.com.cn/search/content?word=%keyword%',
|
|
||||||
icon: 'website/zcool.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Iconfont',
|
|
||||||
desc: 'www.iconfont.cn',
|
|
||||||
url: 'https://www.iconfont.cn/search/index?searchType=icon&q=%keyword%',
|
|
||||||
icon: 'website/iconfont.svg',
|
|
||||||
show: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
community: {
|
|
||||||
title: '社区',
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
name: '天涯',
|
|
||||||
desc: 'search.tianya.cn',
|
|
||||||
url: 'https://search.tianya.cn/bbs?q=%keyword%',
|
|
||||||
icon: 'website/tianya.svg',
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '知乎',
|
|
||||||
desc: 'www.zhihu.com',
|
|
||||||
url: 'https://www.zhihu.com/search?type=content&q=%keyword%',
|
|
||||||
icon: 'website/zhihu.svg',
|
|
||||||
show: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} SearchItem
|
|
||||||
* @property {string} name 名称
|
|
||||||
* @property {string} resultURL 搜索结果页面 URL
|
|
||||||
* @property {string} wordURL 推荐关键词 URL
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {SearchItem[]} */
|
|
||||||
const searchItems = [
|
|
||||||
{
|
|
||||||
name: '360',
|
|
||||||
resultURL: 'https://www.so.com/s?q={w}',
|
|
||||||
wordURL: 'https://sug.so.360.cn/suggest?format=json&word={w}&callback={cb}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'baidu',
|
|
||||||
resultURL: 'https://www.baidu.com/s?wd={w}',
|
|
||||||
wordURL: 'https://suggestion.baidu.com/su?wd={w}&cb={cb}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'bing',
|
|
||||||
resultURL: 'https://cn.bing.com/search?q={w}',
|
|
||||||
wordURL: 'https://api.bing.com/qsonhs.aspx?type=cb&q={w}&cb={cb}',
|
|
||||||
// wordURL: 'https://sg1.api.bing.com/qsonhs.aspx?type=cb&cb={cb}&q={w}',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getSearchURL,
|
|
||||||
parseSearchMDN,
|
|
||||||
parseSearchWords,
|
|
||||||
searchEngines,
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// 生成扇形、环形、圆形,或弧形的 SVG 路径
|
||||||
|
//
|
||||||
|
// 修改自
|
||||||
|
// https://github.com/svgcamp/svg-arc
|
||||||
|
// License
|
||||||
|
// MIT
|
||||||
|
|
||||||
|
const PI = Math.PI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} x
|
||||||
|
* @param {number} y
|
||||||
|
* @param {number} r
|
||||||
|
* @param {number} angle
|
||||||
|
*/
|
||||||
|
function point(x, y, r, angle) {
|
||||||
|
return [
|
||||||
|
(x + Math.sin(angle) * r).toFixed(2),
|
||||||
|
(y - Math.cos(angle) * r).toFixed(2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} x
|
||||||
|
* @param {number} y
|
||||||
|
* @param {number} R
|
||||||
|
* @param {number} r
|
||||||
|
*/
|
||||||
|
function full(x, y, R, r) {
|
||||||
|
if (r <= 0) {
|
||||||
|
return `M ${x - R} ${y} A ${R} ${R} 0 1 1 ${x + R} ${y} A ${R} ${R} 1 1 1 ${x - R} ${y} Z`;
|
||||||
|
}
|
||||||
|
return `M ${x - R} ${y} A ${R} ${R} 0 1 1 ${x + R} ${y} A ${R} ${R} 1 1 1 ${x - R} ${y} M ${x - r} ${y} A ${r} ${r} 0 1 1 ${x + r} ${y} A ${r} ${r} 1 1 1 ${x - r} ${y} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} x
|
||||||
|
* @param {number} y
|
||||||
|
* @param {number} R
|
||||||
|
* @param {number} r
|
||||||
|
* @param {number} start
|
||||||
|
* @param {number} end
|
||||||
|
*/
|
||||||
|
function part(x, y, R, r, start, end) {
|
||||||
|
|
||||||
|
let s = (start / 360) * 2 * PI;
|
||||||
|
let e = (end / 360) * 2 * PI;
|
||||||
|
let P = [
|
||||||
|
point(x, y, r, s),
|
||||||
|
point(x, y, R, s),
|
||||||
|
point(x, y, R, e),
|
||||||
|
point(x, y, r, e),
|
||||||
|
];
|
||||||
|
let flag = (e - s > PI ? '1' : '0');
|
||||||
|
|
||||||
|
return `M ${P[0][0]} ${P[0][1]} L ${P[1][0]} ${P[1][1]} A ${R} ${R} 0 ${flag} 1 ${P[2][0]} ${P[2][1]} L ${P[3][0]} ${P[3][1]} A ${r} ${r} 0 ${flag} 0 ${P[0][0]} ${P[0][1]} Z`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关于角度:
|
||||||
|
// 12 点钟方向 - 0, 360, 720, ...
|
||||||
|
// 3 点钟方向 - 90, 450, ...
|
||||||
|
// 6 点钟方向 - 180, 540, ...
|
||||||
|
// 9 点钟方向 - 270, 630, ...
|
||||||
|
|
||||||
|
// 注意事项:
|
||||||
|
// 绘制环形时,需要将 `fill-rule` 属性的值设置为 `evenodd`,否则 `fill` 可能无法正确填充颜色。
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 生成 SVG `<path>` 元素的 d 属性值
|
||||||
|
* @param {object} opts 配置选项
|
||||||
|
* @param {number} opts.x 圆心水平坐标
|
||||||
|
* @param {number} opts.y 圆心垂直坐标
|
||||||
|
* @param {number} opts.R 内层半径(用于圆环)
|
||||||
|
* @param {number} opts.r 外层半径(圆形半径)
|
||||||
|
* @param {number} opts.start 起始角度(0 ~ 360)
|
||||||
|
* @param {number} opts.end 结束角度(0 ~ 360)
|
||||||
|
*/
|
||||||
|
function arc(opts = {}) {
|
||||||
|
|
||||||
|
let { x = 0, y = 0 } = opts;
|
||||||
|
let { R = 0, r = 0, start, end } = opts;
|
||||||
|
|
||||||
|
[R, r] = [Math.max(R, r), Math.min(R, r)];
|
||||||
|
|
||||||
|
if (R <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start !== +start || end !== +end) {
|
||||||
|
return full(x, y, R, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(start - end) < 0.000001) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(start - end) % 360 < 0.000001) {
|
||||||
|
return full(x, y, R, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
[start, end] = [start % 360, end % 360];
|
||||||
|
|
||||||
|
if (start > end) {
|
||||||
|
end += 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return part(x, y, R, r, start, end);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default arc;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/** 工具更新日志 */
|
||||||
|
export const CHANGE_LOGS = {
|
||||||
|
'json-formatter': [
|
||||||
|
'[2] - 2025-02-07\n优化“输出内容”显示样式,解决内容较多时行号显示不全的问题。',
|
||||||
|
'[1] - 2025-02-04\n初始版本。',
|
||||||
|
],
|
||||||
|
'keep-screen-on': [
|
||||||
|
'[3] - 2026-01-03\n调整界面样式,与其他模块统一。',
|
||||||
|
'[2] - 2024-10-13\n优化界面样式,背景添加圆角。',
|
||||||
|
'[1] - 2024-10-11\n初始版本。',
|
||||||
|
],
|
||||||
|
'msu2-usb-monitor-controller': [
|
||||||
|
'[2] - 2026-02-24\n完善功能:添加旧算法;支持设置显示方向;支持配置渲染间隔和发送间隔。',
|
||||||
|
'[1] - 2026-02-21\n初始版本。',
|
||||||
|
],
|
||||||
|
'qrcode-reader-and-generator': [
|
||||||
|
'[2] - 2025-02-23\n支持生成二维码。\n支持解析剪贴板中的二维码图片。\n优化解析功能,在二维码所在位置添加矩形标记。',
|
||||||
|
'[1] - 2025-02-21\n初始版本。',
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
// 工具箱
|
||||||
|
|
||||||
|
import { CHANGE_LOGS } from './toolbox-changelogs';
|
||||||
|
|
||||||
|
const MODULES = import.meta.glob('../../views/ToolboxView/**/*.vue');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 工具列表
|
||||||
|
* @type {ToolboxCategory[]}
|
||||||
|
*/
|
||||||
|
export const toolList = [
|
||||||
|
{
|
||||||
|
id: 'calculation-tools',
|
||||||
|
title: '计算',
|
||||||
|
enabled: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'calc-ratio',
|
||||||
|
component: 'Calculation/CalcRatio',
|
||||||
|
title: '比例计算',
|
||||||
|
iconClass: 'mdi mdi-calculator-variant-outline',
|
||||||
|
desc: '按设定的比例计算给出的数值所对应的数值。',
|
||||||
|
createdAt: '2024-09-08',
|
||||||
|
updatedAt: '2024-09-08',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'calc-download-time',
|
||||||
|
component: 'Calculation/CalcDownloadTime',
|
||||||
|
title: '下载用时计算',
|
||||||
|
iconClass: 'mdi mdi-calculator-variant-outline',
|
||||||
|
desc: '根据设定的文件大小和下载速度简单计算大约下载完成所需的时间。',
|
||||||
|
createdAt: '2024-09-08',
|
||||||
|
updatedAt: '2024-09-08',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'conversion-tools',
|
||||||
|
title: '转换',
|
||||||
|
enabled: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'base64-encode-decode',
|
||||||
|
component: 'Conversion/Base64StringEncodeDecode',
|
||||||
|
title: 'Base64 字符串编码 / 解码',
|
||||||
|
iconClass: 'mdi mdi-swap-horizontal',
|
||||||
|
desc: '处理 Base64 编码的字符串。',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'png-to-ico',
|
||||||
|
component: 'Conversion/PngToIco',
|
||||||
|
title: 'PNG 转 ICO 图标',
|
||||||
|
iconClass: 'mdi mdi-image-outline',
|
||||||
|
desc: '将 PNG 图片转换为 ICO 图标。',
|
||||||
|
createdAt: '2026-01-26',
|
||||||
|
updatedAt: '2026-01-26',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'convert-timestamp',
|
||||||
|
component: 'Conversion/ConvertTimestamp',
|
||||||
|
title: 'Unix 时间戳转换',
|
||||||
|
iconClass: 'mdi mdi-swap-horizontal',
|
||||||
|
desc: '时间戳转时间字符串 / 时间字符串转时间戳',
|
||||||
|
createdAt: '2025-02-05',
|
||||||
|
updatedAt: '2025-02-05',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'url-encode-decode',
|
||||||
|
component: 'Conversion/UrlEncodeDecode',
|
||||||
|
title: 'URL 编码 / 解码',
|
||||||
|
iconClass: 'mdi mdi-swap-horizontal',
|
||||||
|
desc: '处理 URL 编码的字符串。',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'qrcode-reader-and-generator',
|
||||||
|
component: 'Conversion/QrcodeReaderAndGenerator',
|
||||||
|
title: '二维码解析和生成',
|
||||||
|
iconClass: 'mdi mdi-qrcode',
|
||||||
|
desc: '解析二维码 / 生成二维码',
|
||||||
|
createdAt: '2025-02-21',
|
||||||
|
updatedAt: '2025-02-23',
|
||||||
|
version: '2',
|
||||||
|
enabled: true,
|
||||||
|
changelogs: CHANGE_LOGS['qrcode-reader-and-generator'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'convert-text-structure',
|
||||||
|
component: 'Conversion/ConvertTextStructure',
|
||||||
|
title: '文本结构转换',
|
||||||
|
iconClass: 'mdi mdi-swap-horizontal',
|
||||||
|
desc: '倒序、横竖互换等',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'convert-html-entities',
|
||||||
|
component: 'Conversion/ConvertHtmlEntities',
|
||||||
|
title: '转换 HTML 实体',
|
||||||
|
iconClass: 'mdi mdi-swap-horizontal',
|
||||||
|
desc: '',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit-tools',
|
||||||
|
title: '编辑',
|
||||||
|
enabled: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'csv-editor',
|
||||||
|
component: 'Edit/CsvEditor',
|
||||||
|
title: 'CSV 编辑',
|
||||||
|
iconClass: 'mdi mdi-table-edit',
|
||||||
|
desc: '查看或编辑 CSV 文件',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'json-editor',
|
||||||
|
component: 'Edit/JsonEditor',
|
||||||
|
title: 'JSON 编辑器',
|
||||||
|
iconClass: 'mdi mdi-code-json',
|
||||||
|
desc: '基于 Monaco Editor 实现的 JSON 编辑器。',
|
||||||
|
createdAt: '2025-10-26',
|
||||||
|
updatedAt: '2025-10-26',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'json-formatter',
|
||||||
|
component: 'Edit/JsonFormatter',
|
||||||
|
title: 'JSON 格式化',
|
||||||
|
iconClass: 'mdi mdi-code-json',
|
||||||
|
desc: '格式化 / 美化 JSON 字符串',
|
||||||
|
createdAt: '2025-02-04',
|
||||||
|
updatedAt: '2025-02-07',
|
||||||
|
version: '2',
|
||||||
|
enabled: true,
|
||||||
|
changelogs: CHANGE_LOGS['json-formatter'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'text-editor',
|
||||||
|
component: 'Edit/TextEditor',
|
||||||
|
title: '文本编辑器',
|
||||||
|
iconClass: 'mdi mdi-text-long',
|
||||||
|
desc: '基于 Monaco Editor 实现的文本编辑器。',
|
||||||
|
createdAt: '2025-12-21',
|
||||||
|
updatedAt: '2025-12-21',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'generator-tools',
|
||||||
|
title: '生成',
|
||||||
|
enabled: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'frp-config-generator',
|
||||||
|
component: 'Generator/FrpConfigGenerator/FrpConfigGenerator',
|
||||||
|
title: 'frp 配置文件生成',
|
||||||
|
iconClass: 'mdi mdi-file-cog-outline',
|
||||||
|
desc: '生成用于 frpc、frps 的配置文件。',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'uuid-generator',
|
||||||
|
component: 'Generator/UuidGenerator',
|
||||||
|
title: 'UUID 生成器',
|
||||||
|
iconClass: 'mdi mdi-identifier',
|
||||||
|
desc: '生成 UUID 列表。',
|
||||||
|
createdAt: '2026-01-03',
|
||||||
|
updatedAt: '2026-01-03',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'generate-urls',
|
||||||
|
component: 'Generator/GenerateUrls',
|
||||||
|
title: '生成批量下载链接',
|
||||||
|
iconClass: 'mdi mdi-link-variant',
|
||||||
|
desc: '根据设置,生成有一定规律的用于批量下载的链接。',
|
||||||
|
createdAt: '2026-01-03',
|
||||||
|
updatedAt: '2026-01-03',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'generate-random-string',
|
||||||
|
component: 'Generator/GenerateRandomString',
|
||||||
|
title: '生成随机字符串',
|
||||||
|
iconClass: 'mdi mdi-format-text',
|
||||||
|
desc: '生成随机组合的字符串,可用于密码。',
|
||||||
|
createdAt: '2024-09-08',
|
||||||
|
updatedAt: '2024-09-08',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'minecraft-tools',
|
||||||
|
title: 'Minecraft',
|
||||||
|
enabled: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'minecraft-uuid-converter',
|
||||||
|
component: 'Minecraft/UuidConverter',
|
||||||
|
title: 'Minecraft UUID 转换',
|
||||||
|
iconClass: 'mdi mdi-identifier',
|
||||||
|
desc: '随机生成或转换 Minecraft 的 UUID。',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'minecraft-chat-history-reader',
|
||||||
|
component: 'Minecraft/ChatHistoryReader/ChatHistoryReader',
|
||||||
|
title: 'Minecraft 聊天记录查看',
|
||||||
|
iconClass: 'mdi mdi-format-list-text',
|
||||||
|
desc: '读取并解析 latest.log 文件,显示聊天记录。',
|
||||||
|
createdAt: '2025-02-03',
|
||||||
|
updatedAt: '2025-02-03',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'calc-minecraft-chunk-location',
|
||||||
|
component: 'Minecraft/CalcChunkLocation',
|
||||||
|
title: 'Minecraft 区块位置计算',
|
||||||
|
iconClass: 'mdi mdi-calculator-variant-outline',
|
||||||
|
desc: '',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'generate-minecraft-dynmap-renderdata',
|
||||||
|
component: 'Minecraft/GenerateDynmapRenderdata',
|
||||||
|
title: '生成 Dynmap renderdata',
|
||||||
|
iconClass: 'mdi mdi-file-outline',
|
||||||
|
desc: '生成用于 Minecraft Dynmap 插件或模组的 renderdata 数据。',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'network-tools',
|
||||||
|
title: '网络',
|
||||||
|
enabled: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'websocket-test-tool',
|
||||||
|
component: 'Network/WebSocketTestTool',
|
||||||
|
title: 'WebSocket 测试',
|
||||||
|
iconClass: 'mdi mdi-connection',
|
||||||
|
desc: '连接 WebSocket 服务端,发送和接收消息。',
|
||||||
|
createdAt: '2024-12-01',
|
||||||
|
updatedAt: '2024-12-01',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'other-tools',
|
||||||
|
title: '其他',
|
||||||
|
enabled: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'keep-screen-on',
|
||||||
|
component: 'Other/KeepScreenOn',
|
||||||
|
title: '保持亮屏',
|
||||||
|
iconClass: 'mdi mdi-monitor',
|
||||||
|
desc: '保持屏幕开启,不息屏,不休眠。',
|
||||||
|
createdAt: '2024-10-11',
|
||||||
|
updatedAt: '2026-01-03',
|
||||||
|
version: '3',
|
||||||
|
enabled: true,
|
||||||
|
changelogs: CHANGE_LOGS['keep-screen-on'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'visualized-working-hours',
|
||||||
|
component: 'Other/VisualizedWorkingHours/VisualizedWorkingHours',
|
||||||
|
title: '工作时间可视化',
|
||||||
|
iconClass: 'mdi mdi-clock-digital',
|
||||||
|
desc: '用趣味化的方式呈现工作收益与时间进度,让薪资进度和下班期待看得见。',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'timer-tool',
|
||||||
|
component: 'Other/TimerTool',
|
||||||
|
title: '计时器',
|
||||||
|
iconClass: 'mdi mdi-timer-outline',
|
||||||
|
desc: '正计时、倒计时工具。',
|
||||||
|
createdAt: '2025-12-29',
|
||||||
|
updatedAt: '2025-12-29',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open-new-window',
|
||||||
|
component: 'Other/OpenNewWindow',
|
||||||
|
title: '新窗口中打开',
|
||||||
|
iconClass: 'mdi mdi-window-maximize',
|
||||||
|
desc: '从新的小窗口中打开指定的链接(仅支持 PC 端浏览器)。',
|
||||||
|
createdAt: '2025-02-04',
|
||||||
|
updatedAt: '2025-02-04',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'run-javascript',
|
||||||
|
component: 'Other/RunJavaScript',
|
||||||
|
title: '执行 JavaScript',
|
||||||
|
iconClass: 'mdi mdi-code-braces',
|
||||||
|
desc: '执行简单的 JavaScript 代码片段。',
|
||||||
|
createdAt: '2026-01-03',
|
||||||
|
updatedAt: '2026-01-03',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msu2-usb-monitor-controller',
|
||||||
|
component: 'Other/Msu2UsbMonitorController/Msu2UsbMonitorController',
|
||||||
|
title: 'MSU2 USB 小屏幕控制',
|
||||||
|
iconClass: 'mdi mdi-usb',
|
||||||
|
desc: 'MSU2 USB 小屏幕控制工具,参考了原 Python 程序的代码。',
|
||||||
|
createdAt: '2026-02-21',
|
||||||
|
updatedAt: '2026-02-24',
|
||||||
|
version: '2',
|
||||||
|
enabled: true,
|
||||||
|
changelogs: CHANGE_LOGS['msu2-usb-monitor-controller'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'genshin-impact-clock',
|
||||||
|
component: 'Other/GenshinImpactClock/GenshinImpactClock',
|
||||||
|
title: '《原神》时钟',
|
||||||
|
iconClass: 'mdi mdi-clock-outline',
|
||||||
|
desc: '在网页上实现的《原神》时钟效果。',
|
||||||
|
createdAt: '2024-10-13',
|
||||||
|
updatedAt: '2024-10-13',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取动态组件
|
||||||
|
* @param {string} path 工具页面相对路径
|
||||||
|
*/
|
||||||
|
function getDynamicComponent(path) {
|
||||||
|
|
||||||
|
let key = `../../views/ToolboxView/${path}.vue`;
|
||||||
|
let component = MODULES[key];
|
||||||
|
|
||||||
|
return component;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成工具箱页面路由 */
|
||||||
|
export function getToolboxRoutes() {
|
||||||
|
|
||||||
|
/** @type {VueRouteRecordRaw[]} */
|
||||||
|
let routes = [];
|
||||||
|
|
||||||
|
toolList.forEach((categoryItem) => {
|
||||||
|
categoryItem.items.forEach((toolItem) => {
|
||||||
|
|
||||||
|
// // 跳过未启用的工具
|
||||||
|
// if (!toolItem.enabled) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
routes.push({
|
||||||
|
path: `/toolbox-view/${toolItem.id}`,
|
||||||
|
name: `Toolbox/${toolItem.component}`,
|
||||||
|
component: getDynamicComponent(toolItem.component),
|
||||||
|
meta: {
|
||||||
|
isToolDetail: true,
|
||||||
|
title: toolItem.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,96 +1,53 @@
|
|||||||
class Utils {
|
// 工具函数
|
||||||
|
|
||||||
constructor() { }
|
import {
|
||||||
|
description as appDesc,
|
||||||
|
} from '@package-json';
|
||||||
|
|
||||||
/**
|
import {
|
||||||
* @description 改变网页标题
|
v4 as uuidV4,
|
||||||
* @param {string} [value] 新的标题
|
} from 'uuid';
|
||||||
*/
|
|
||||||
changeTitle(value) {
|
/** 将十六进制颜色值转为灰度值 */
|
||||||
document.title = ((value ? `${value} - ` : '') + 'Frost 网址导航');
|
export function colorHexToGrayLevel(hex = '') {
|
||||||
|
|
||||||
|
let rgb = colorHexToRgb(hex);
|
||||||
|
|
||||||
|
return Math.round(rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将十六进制颜色值转为 RGB */
|
||||||
|
export function colorHexToRgb(hex = '') {
|
||||||
|
|
||||||
|
// 去除可能存在的 '#' 字符
|
||||||
|
hex = hex.replace('#', '');
|
||||||
|
|
||||||
|
// 检查十六进制颜色值的长度,并根据长度决定如何处理
|
||||||
|
if (hex.length === 3) {
|
||||||
|
// 如果是简写形式,如 #FFF,需要将其转换为完整形式 #FFFFFF
|
||||||
|
hex = hex.split('').map(char => char + char).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分别解析出红色、绿色和蓝色的值
|
||||||
|
let r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
let g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
let b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
|
||||||
|
return { r, g, b };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取 V4 UUID */
|
||||||
|
export function getUuidV4(noSplit = false) {
|
||||||
|
let uuid = uuidV4();
|
||||||
|
return noSplit ? uuid.replace(/-/g, '') : uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description JSONP
|
* @description 更新页面标题
|
||||||
* @param {object} options 配置选项
|
* @param {string} title
|
||||||
*/
|
*/
|
||||||
jsonp(options) {
|
export function updateAppTitle(title = '') {
|
||||||
var config = {
|
document.title = (title ? `${title} - ${appDesc}` : appDesc);
|
||||||
url: '',
|
|
||||||
cbName: 'cb',
|
|
||||||
cbFunc: function (data) {
|
|
||||||
console.log('[JSONP]', data);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
var el = document.createElement('script');
|
|
||||||
|
|
||||||
Object.assign(config, options);
|
|
||||||
window[config.cbName] = config.cbFunc;
|
|
||||||
|
|
||||||
el.addEventListener('load', function () {
|
|
||||||
this.remove();
|
|
||||||
});
|
|
||||||
el.setAttribute('src', config.url);
|
|
||||||
|
|
||||||
document.body.appendChild(el);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 以文本方式读取文件(异步)
|
|
||||||
* @param {Event} ev 输入框 change 事件对象
|
|
||||||
* @param {boolean} resetValue 是否自动重置输入框 value 属性
|
|
||||||
* @returns {Promise<string[]>} `{ name: 文件名, content: 文件内容 }`
|
|
||||||
*/
|
|
||||||
readFileAsText(ev, resetValue) {
|
|
||||||
const blank = [];
|
|
||||||
|
|
||||||
/** @type {HTMLInputElement} */
|
|
||||||
const el = ev.currentTarget;
|
|
||||||
|
|
||||||
/** @type {FileList} */
|
|
||||||
const files = el.files;
|
|
||||||
|
|
||||||
if (!files || files.length === 0) {
|
|
||||||
return Promise.resolve(blank);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fnProc = (procResolve, procReject) => {
|
|
||||||
const reads = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const file = files[i];
|
|
||||||
const reader = new FileReader();
|
|
||||||
const fnRead = (readResolve, readReject) => {
|
|
||||||
// 成功
|
|
||||||
reader.onload = () => {
|
|
||||||
readResolve(reader.result);
|
|
||||||
};
|
|
||||||
// 失败
|
|
||||||
reader.onerror = () => {
|
|
||||||
readReject('');
|
|
||||||
};
|
|
||||||
// 读取
|
|
||||||
reader.readAsText(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
reads.push(new Promise(fnRead));
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(reads).then((res) => {
|
|
||||||
// [成功]
|
|
||||||
procResolve(res);
|
|
||||||
resetValue && el.setAttribute('value', '');
|
|
||||||
}).catch(() => {
|
|
||||||
// [失败]
|
|
||||||
procReject(blank);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise(fnProc);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const utils = new Utils;
|
|
||||||
|
|
||||||
export default utils;
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 317 B After Width: | Height: | Size: 317 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 466 B After Width: | Height: | Size: 466 B |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" with="64" height="64"><defs><clipPath id="prefix__clip-path"><path class="prefix__cls-1" d="M29.48 25.24c-.16-1.89-.08-1.5-.24-3.16a72.24 72.24 0 00-2-10.37L26 6.25l-2.6 5.41-.44.89q-.24.48-.54 1a20.43 20.43 0 01-1.24 1.84 20.08 20.08 0 01-3.08 3.24 19.72 19.72 0 01-3.87 2.53c-.72.33-1.45.71-2.23 1-.34.14-1.19.44-1.93.66a2.55 2.55 0 01-1.07-.3c-.34-1-.83-2.24-1.2-3.19-.5-1.25-1-2.5-1.51-3.73a59.11 59.11 0 00-3.62-7.25 59.82 59.82 0 001.51 7.94c.3 1.3.7 2.58 1.05 3.87S6 22.71 6.44 24a1.4 1.4 0 001.29.93c1.28 0 2.57.06 3.86.05s2.59 0 3.88-.11a56 56 0 007.79-.87c-1.92-.49-7.5-.81-10.79-1.06a43 43 0 006.53-1.7 24.06 24.06 0 005.38-3.45c.71-.62 1.21 1.88 2.39 5.27.62 1.69.34 1 1 2.66s1.37 4.38 2.17 6c-.07-1.82-.31-4.67-.46-6.48z"/></clipPath><clipPath id="prefix__clip-path-2"><path class="prefix__cls-1" d="M20.92 3.46a4.25 4.25 0 01.31 5.86 3.91 3.91 0 01-5.66.24 4.25 4.25 0 01-.31-5.86 3.91 3.91 0 015.66-.24m1.35-1.54a5.89 5.89 0 00-8.53.36 6.41 6.41 0 00.48 8.83 5.91 5.91 0 008.53-.36 6.41 6.41 0 00-.48-8.83z"/></clipPath><style>.prefix__cls-1{fill:#3e3a39}@media (prefers-color-scheme:dark){.prefix__cls-1{fill:#efefef}}</style></defs><g style="isolation:isolate"><g id="prefix__layer_1" data-name="layer 1"><path class="prefix__cls-1" d="M29.48 25.24c-.16-1.89-.08-1.5-.24-3.16a72.24 72.24 0 00-2-10.37L26 6.25l-2.6 5.41-.44.89q-.24.48-.54 1a20.43 20.43 0 01-1.24 1.84 20.08 20.08 0 01-3.08 3.24 19.72 19.72 0 01-3.87 2.53c-.72.33-1.45.71-2.23 1-.34.14-1.19.44-1.93.66a2.55 2.55 0 01-1.07-.3c-.34-1-.83-2.24-1.2-3.19-.5-1.25-1-2.5-1.51-3.73a59.11 59.11 0 00-3.62-7.25 59.82 59.82 0 001.51 7.94c.3 1.3.7 2.58 1.05 3.87S6 22.71 6.44 24a1.4 1.4 0 001.29.93c1.28 0 2.57.06 3.86.05s2.59 0 3.88-.11a56 56 0 007.79-.87c-1.92-.49-7.5-.81-10.79-1.06a43 43 0 006.53-1.7 24.06 24.06 0 005.38-3.45c.71-.62 1.21 1.88 2.39 5.27.62 1.69.34 1 1 2.66s1.37 4.38 2.17 6c-.07-1.82-.31-4.67-.46-6.48z"/><g clip-path="url(#prefix__clip-path)"><path class="prefix__cls-1" d="M-.87-.78h34.42v33.07H-.87z"/></g><path class="prefix__cls-1" d="M20.92 3.46a4.25 4.25 0 01.31 5.86 3.91 3.91 0 01-5.66.24 4.25 4.25 0 01-.31-5.86 3.91 3.91 0 015.66-.24m1.35-1.54a5.89 5.89 0 00-8.53.36 6.41 6.41 0 00.48 8.83 5.91 5.91 0 008.53-.36 6.41 6.41 0 00-.48-8.83z"/><g clip-path="url(#prefix__clip-path-2)"><path class="prefix__cls-1" d="M-.87-.78h34.42v33.07H-.87z"/></g></g></g></svg>
|
||||||
|
<!-- https://assets.cnblogs.com/favicon.svg -->
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 893 B After Width: | Height: | Size: 893 B |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 240 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<div class="app-aside">
|
||||||
|
|
||||||
|
<div class="app-icon-wrapper">
|
||||||
|
<div class="app-icon"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-modules">
|
||||||
|
<div
|
||||||
|
v-for="item in modules"
|
||||||
|
:key="item.name"
|
||||||
|
:title="item.title"
|
||||||
|
:class="{
|
||||||
|
'is-active': (currRouteName === item.name),
|
||||||
|
'module-item': true,
|
||||||
|
}"
|
||||||
|
@click="changePage(item.name)"
|
||||||
|
>
|
||||||
|
<span :class="item.className"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
computed, shallowRef, onBeforeMount,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useRoute, useRouter,
|
||||||
|
} from 'vue-router';
|
||||||
|
|
||||||
|
/** 当前路由名称 */
|
||||||
|
const currRouteName = computed(() => {
|
||||||
|
return route.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 模块列表
|
||||||
|
* @type {VueRef<{ className: string; name: string; title: string; }[]>}
|
||||||
|
*/
|
||||||
|
const modules = shallowRef([]);
|
||||||
|
|
||||||
|
/** 路由 */
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
/** 路由 */
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** 切换页面 */
|
||||||
|
function changePage(routeName = '') {
|
||||||
|
router.push({ name: routeName });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化数据 */
|
||||||
|
function initData() {
|
||||||
|
|
||||||
|
/** @type {typeof modules.value} */
|
||||||
|
let list = [];
|
||||||
|
|
||||||
|
// 提取路由信息
|
||||||
|
router.getRoutes().forEach((item) => {
|
||||||
|
|
||||||
|
let m = item.meta;
|
||||||
|
|
||||||
|
if (m.showInAside) {
|
||||||
|
list.push({
|
||||||
|
className: m.iconClass,
|
||||||
|
name: item.name,
|
||||||
|
title: m.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
modules.value = list;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
initData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.app-aside {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
margin: auto;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-image: url("/favicon.ico");
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-modules {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-item {
|
||||||
|
display: flex;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 1.8em;
|
||||||
|
height: 1.8em;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #808080;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.is-active {
|
||||||
|
background-color: #F2F2F2;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
<docs>
|
|
||||||
|
|
||||||
## $emit
|
|
||||||
|
|
||||||
### @changed
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
count: Number,
|
|
||||||
min: Number,
|
|
||||||
max: Number,
|
|
||||||
multiple: Boolean,
|
|
||||||
list: FileList,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### @exceeded
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
count: Number,
|
|
||||||
min: Number,
|
|
||||||
max: Number,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</docs>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="file-upload" @click="selectFile()">
|
|
||||||
<slot />
|
|
||||||
<input
|
|
||||||
ref="inputFile"
|
|
||||||
class="input-file"
|
|
||||||
type="file"
|
|
||||||
:accept="accept == '' ? false : accept"
|
|
||||||
:capture="capture == '' ? false : capture"
|
|
||||||
:multiple="multiple"
|
|
||||||
:name="name == '' ? false : name"
|
|
||||||
@change="fileChanged()"
|
|
||||||
@click.stop
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'FileUpload',
|
|
||||||
props: {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 接受的文件类型
|
|
||||||
* 格式与 input 标签的 accept 属性相同
|
|
||||||
*/
|
|
||||||
accept: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 格式与 input 标签的 capture 属性相同 */
|
|
||||||
capture: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 是否禁用 */
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 最大文件数量 */
|
|
||||||
filesMax: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 最小文件数量 */
|
|
||||||
filesMin: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 是否为多选 */
|
|
||||||
multiple: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 为 input 设置 name */
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
logPrefix: '[文件上传]',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
/** 选择了新的文件 */
|
|
||||||
fileChanged() {
|
|
||||||
|
|
||||||
/** @type {HTMLInputElement} */
|
|
||||||
const el = this.$refs['inputFile'];
|
|
||||||
|
|
||||||
/** @type {FileList} */
|
|
||||||
var files = null;
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
const max = this.filesMin;
|
|
||||||
const min = this.filesMax;
|
|
||||||
|
|
||||||
if (el) {
|
|
||||||
files = el.files;
|
|
||||||
count = files.length;
|
|
||||||
} else {
|
|
||||||
console.error(this.logPrefix, 'input 元素不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count == 0) {
|
|
||||||
console.warn(this.logPrefix, '未选择文件');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.multiple && min != 0 && max != 0) {
|
|
||||||
if (max < min) {
|
|
||||||
console.error(this.logPrefix, '文件数量限制设置错误');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (count < min || count > max) {
|
|
||||||
// 触发父组件 @exceeded
|
|
||||||
this.$emit('exceeded', { count, min, max });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发父组件 @changed
|
|
||||||
this.$emit('changed', {
|
|
||||||
count,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
multiple: this.multiple,
|
|
||||||
list: files,
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 选择文件 */
|
|
||||||
selectFile() {
|
|
||||||
|
|
||||||
if (this.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {HTMLInputElement} */
|
|
||||||
const el = this.$refs['inputFile'];
|
|
||||||
|
|
||||||
if (el) {
|
|
||||||
el.value = '';
|
|
||||||
el.click();
|
|
||||||
} else {
|
|
||||||
console.error(this.logPrefix, 'input 元素不存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.file-upload {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-file {
|
|
||||||
visibility: hidden;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="floatingBtn" class="floating-btn">
|
|
||||||
<div :class="['btns-inner', { show: showInner }]">
|
|
||||||
<button class="btn" type="button" title="折叠侧边菜单" @click="toggleSideCollapse()">
|
|
||||||
<i class="el-icon-menu" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn" type="button" title="刷新" @click="refreshPage()">
|
|
||||||
<i class="el-icon-refresh-right" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn" type="button" title="返回主页" @click="backToHome()">
|
|
||||||
<i class="el-icon-s-home" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="btns-outer">
|
|
||||||
<button class="btn" type="button" title="菜单">
|
|
||||||
<i class="fa fa-bars" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'FloatingBtn',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showInner: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.initAnimation();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
/** 设置动画 */
|
|
||||||
initAnimation() {
|
|
||||||
var vm = this;
|
|
||||||
var el = vm.$refs['floatingBtn'];
|
|
||||||
var btns = el.querySelectorAll('.btn');
|
|
||||||
var className = 'animate';
|
|
||||||
|
|
||||||
btns.forEach((elem) => {
|
|
||||||
elem.addEventListener('click', function () {
|
|
||||||
this.classList.remove(className);
|
|
||||||
setTimeout(() => {
|
|
||||||
this.classList.add(className);
|
|
||||||
vm.toggleInnerBtns();
|
|
||||||
}, 20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 切换按钮显示 */
|
|
||||||
toggleInnerBtns() {
|
|
||||||
this.showInner = !this.showInner;
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 返回主页 */
|
|
||||||
backToHome() {
|
|
||||||
const routeName = 'Home';
|
|
||||||
|
|
||||||
if (this.$route.name != routeName) {
|
|
||||||
this.$router.push({ name: routeName });
|
|
||||||
} else {
|
|
||||||
this.$message({
|
|
||||||
duration: 2000,
|
|
||||||
message: '已经在主页啦~',
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 刷新 */
|
|
||||||
refreshPage() {
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 切换侧边菜单折叠状态 */
|
|
||||||
toggleSideCollapse() {
|
|
||||||
this.$store.dispatch('toggleSideCollapse');
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.floating-btn {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 5000;
|
|
||||||
right: 2rem;
|
|
||||||
bottom: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.btns-inner.show .btn {
|
|
||||||
width: 2.6rem;
|
|
||||||
height: 2.6rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 400px) {
|
|
||||||
right: 1.5rem;
|
|
||||||
bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btns-inner {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
background-color: @colorSecondary;
|
|
||||||
font-size: 0;
|
|
||||||
color: transparent;
|
|
||||||
transition: all @transitionTime;
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btns-outer {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin-top: 1rem;
|
|
||||||
width: 3.2rem;
|
|
||||||
height: 3.2rem;
|
|
||||||
background-color: @colorPrimary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
box-shadow: 0 0.15rem 0.15rem 0 rgba(0, 0, 0, 0.14),
|
|
||||||
0 0.2rem 0.1rem -0.15rem rgba(0, 0, 0, 0.12),
|
|
||||||
0 0.1rem 0.3rem 0 rgba(0, 0, 0, 0.20);
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #FFF;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&::before, &::after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #FFF;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity @transitionTime;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover::before {
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.animate::after {
|
|
||||||
animation: floatingBtnClick calc(@transitionTime * 2) linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes floatingBtnClick {
|
|
||||||
0% {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<i class="icon-element bg-center-contain" :style="iconStyle"></i>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'IconElement',
|
|
||||||
props: {
|
|
||||||
// 来源(inner、outer)
|
|
||||||
from: {
|
|
||||||
type: String,
|
|
||||||
default: 'inner'
|
|
||||||
},
|
|
||||||
// 路径
|
|
||||||
// 若为 inner,自动添加 @/assets/icon/
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
default: 'unknown.svg'
|
|
||||||
},
|
|
||||||
// 尺寸
|
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
default: '1em'
|
|
||||||
},
|
|
||||||
// 显示图标
|
|
||||||
showIcon: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
// 图标样式
|
|
||||||
iconStyle() {
|
|
||||||
var style = {
|
|
||||||
width: this.size,
|
|
||||||
height: this.size
|
|
||||||
};
|
|
||||||
var iconPath = '';
|
|
||||||
|
|
||||||
// 不显示图标内容
|
|
||||||
if (!this.showIcon) {
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.from === 'inner') {
|
|
||||||
// 内部
|
|
||||||
iconPath = require(`@/assets/icon/${this.path}`);
|
|
||||||
} else if (this.from === 'outer') {
|
|
||||||
// 外部
|
|
||||||
iconPath = this.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
style.backgroundImage = `url('${iconPath}')`;
|
|
||||||
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.icon-element {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// 接口配置
|
||||||
|
|
||||||
|
/** HTTP 请求超时毫秒数 */
|
||||||
|
export const HTTP_TIMEOUT = 10000;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 环境变量
|
||||||
|
|
||||||
|
/** 环境变量信息 */
|
||||||
|
const ENV = import.meta.env;
|
||||||
|
|
||||||
|
/** 是否为开发环境 */
|
||||||
|
export const IS_DEV = ENV.DEV;
|
||||||
|
|
||||||
|
/** 是否为生产环境 */
|
||||||
|
export const IS_PROD = ENV.PROD;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// 模块显示配置
|
||||||
|
|
||||||
|
// import { IS_DEV } from './env';
|
||||||
|
|
||||||
|
/** 启用模块 */
|
||||||
|
export const ABOUT_MODULE_ENABLED = true;
|
||||||
|
|
||||||
|
/** 模块标题 */
|
||||||
|
export const ABOUT_MODULE_TITLE = '关于';
|
||||||
|
|
||||||
|
/** 启用模块 */
|
||||||
|
export const MC_CTRL_MODULE_ENABLED = false;
|
||||||
|
|
||||||
|
/** 模块标题 */
|
||||||
|
export const MC_CTRL_MODULE_TITLE = 'MINECRAFT 联动控制';
|
||||||
|
|
||||||
|
/** 启用模块 */
|
||||||
|
export const NAV_MODULE_ENABLED = true;
|
||||||
|
|
||||||
|
/** 模块标题 */
|
||||||
|
export const NAV_MODULE_TITLE = '网址导航';
|
||||||
|
|
||||||
|
/** 启用模块 */
|
||||||
|
export const SEARCH_MODULE_ENABLED = true;
|
||||||
|
|
||||||
|
/** 模块标题 */
|
||||||
|
export const SEARCH_MODULE_TITLE = '搜索';
|
||||||
|
|
||||||
|
/** 启用模块 */
|
||||||
|
export const TOOLBOX_MODULE_ENABLED = true;
|
||||||
|
|
||||||
|
/** 模块标题 */
|
||||||
|
export const TOOLBOX_MODULE_TITLE = '工具箱';
|
||||||
@@ -1,20 +1,44 @@
|
|||||||
import Vue from 'vue';
|
import { createApp } from 'vue';
|
||||||
import ElementUI from 'element-ui';
|
import { router } from './router';
|
||||||
|
|
||||||
|
import '@mdi/font/css/materialdesignicons.css';
|
||||||
|
import '@/assets/fonts/index.css';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import dayjsDuration from 'dayjs/plugin/duration';
|
||||||
|
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
import 'dayjs/locale/zh-hk';
|
||||||
|
|
||||||
|
import lunisolar from 'lunisolar';
|
||||||
|
import lunisolarLang1 from 'lunisolar/locale/en';
|
||||||
|
import lunisolarLang2 from 'lunisolar/locale/zh-cn';
|
||||||
|
import lunisolarFestivals from 'lunisolar/markers/festivals.zh-cn';
|
||||||
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import router from './router';
|
|
||||||
import store from './store';
|
|
||||||
|
|
||||||
import 'ress/ress.css';
|
// 加载 Day.js Duration 插件
|
||||||
import 'font-awesome/css/font-awesome.min.css';
|
dayjs.extend(dayjsDuration);
|
||||||
import './assets/css/element.scss';
|
|
||||||
import './assets/css/global.less';
|
|
||||||
|
|
||||||
Vue.use(ElementUI);
|
// 配置 Day.js 默认语言
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
// 加载 Lunisolar 语言包
|
||||||
|
lunisolar.locale([
|
||||||
|
lunisolarLang1,
|
||||||
|
lunisolarLang2,
|
||||||
|
], false);
|
||||||
|
|
||||||
new Vue({
|
// 加载 Lunisolar 节日列表
|
||||||
router,
|
lunisolar.Markers.add(lunisolarFestivals, 'festivals');
|
||||||
store,
|
|
||||||
render: h => h(App)
|
// Lunisolar 全局配置
|
||||||
}).$mount('#app');
|
lunisolar.config({
|
||||||
|
isUTC: false,
|
||||||
|
lang: 'zh-cn',
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.use(router);
|
||||||
|
app.mount('#app');
|
||||||
|
|||||||
@@ -1,28 +1,100 @@
|
|||||||
import Vue from 'vue';
|
import {
|
||||||
import VueRouter from 'vue-router';
|
createRouter,
|
||||||
|
createWebHashHistory,
|
||||||
|
} from 'vue-router';
|
||||||
|
|
||||||
import routes from './routes';
|
import {
|
||||||
import utils from '@/assets/js/utils';
|
getToolboxRoutes,
|
||||||
import store from '@/store/index';
|
} from '@/assets/js/toolbox-data';
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
import {
|
||||||
|
updateAppTitle,
|
||||||
|
} from '@/assets/js/utils';
|
||||||
|
|
||||||
const router = new VueRouter({
|
import {
|
||||||
routes
|
ABOUT_MODULE_ENABLED,
|
||||||
});
|
ABOUT_MODULE_TITLE,
|
||||||
|
MC_CTRL_MODULE_ENABLED,
|
||||||
|
MC_CTRL_MODULE_TITLE,
|
||||||
|
NAV_MODULE_ENABLED,
|
||||||
|
NAV_MODULE_TITLE,
|
||||||
|
SEARCH_MODULE_ENABLED,
|
||||||
|
SEARCH_MODULE_TITLE,
|
||||||
|
TOOLBOX_MODULE_ENABLED,
|
||||||
|
TOOLBOX_MODULE_TITLE,
|
||||||
|
} from '@/config/modules';
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
import AboutView from '@/views/AboutView/AboutView.vue';
|
||||||
if (to.meta.loadingBar) {
|
import IndexView from '@/views/IndexView/IndexView.vue';
|
||||||
store.commit('toggleLoading', true);
|
|
||||||
}
|
export const router = createRouter({
|
||||||
next();
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'IndexView',
|
||||||
|
component: IndexView,
|
||||||
|
meta: {
|
||||||
|
iconClass: 'mdi mdi-home',
|
||||||
|
showInAside: true,
|
||||||
|
title: '主页',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/search-view',
|
||||||
|
name: 'SearchView',
|
||||||
|
component: () => import('@/views/SearchView/SearchView.vue'),
|
||||||
|
meta: {
|
||||||
|
iconClass: 'mdi mdi-magnify',
|
||||||
|
showInAside: SEARCH_MODULE_ENABLED,
|
||||||
|
title: SEARCH_MODULE_TITLE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/nav-view',
|
||||||
|
name: 'NavView',
|
||||||
|
component: () => import('@/views/NavView/NavView.vue'),
|
||||||
|
meta: {
|
||||||
|
iconClass: 'mdi mdi-compass',
|
||||||
|
showInAside: NAV_MODULE_ENABLED,
|
||||||
|
title: NAV_MODULE_TITLE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/toolbox-view',
|
||||||
|
name: 'ToolboxView',
|
||||||
|
component: () => import('@/views/ToolboxView/ToolboxView.vue'),
|
||||||
|
meta: {
|
||||||
|
iconClass: 'mdi mdi-tools',
|
||||||
|
isToolDetail: false,
|
||||||
|
showInAside: TOOLBOX_MODULE_ENABLED,
|
||||||
|
title: TOOLBOX_MODULE_TITLE,
|
||||||
|
},
|
||||||
|
children: getToolboxRoutes(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/minecraft-ctrl-view',
|
||||||
|
name: 'MinecraftCtrlView',
|
||||||
|
component: () => import('@/views/MinecraftCtrlView/MinecraftCtrlView.vue'),
|
||||||
|
meta: {
|
||||||
|
iconClass: 'mdi mdi-gamepad',
|
||||||
|
showInAside: MC_CTRL_MODULE_ENABLED,
|
||||||
|
title: MC_CTRL_MODULE_TITLE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/about-view',
|
||||||
|
name: 'AboutView',
|
||||||
|
component: AboutView,
|
||||||
|
meta: {
|
||||||
|
iconClass: 'mdi mdi-information',
|
||||||
|
showInAside: ABOUT_MODULE_ENABLED,
|
||||||
|
title: ABOUT_MODULE_TITLE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach((to) => {
|
router.afterEach((to) => {
|
||||||
if (to.meta.loadingBar) {
|
updateAppTitle(to.meta.title);
|
||||||
store.commit('toggleLoading', false);
|
|
||||||
}
|
|
||||||
utils.changeTitle(to.meta.title);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import tools from './tools';
|
|
||||||
|
|
||||||
/** @type { import('vue-router').RouteConfig[] } */
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
name: 'Home',
|
|
||||||
meta: {
|
|
||||||
loadingBar: true,
|
|
||||||
title: '主页',
|
|
||||||
},
|
|
||||||
component: () => import(
|
|
||||||
/* webpackChunkName: 'home-view' */
|
|
||||||
'@/views/HomeView.vue'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/tools',
|
|
||||||
name: 'Tools',
|
|
||||||
meta: {
|
|
||||||
loadingBar: true,
|
|
||||||
title: '小工具',
|
|
||||||
},
|
|
||||||
component: () => import(
|
|
||||||
/* webpackChunkName: 'tools-view' */
|
|
||||||
'@/views/ToolsView.vue'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/tool',
|
|
||||||
name: 'Tool',
|
|
||||||
meta: {
|
|
||||||
loadingBar: true,
|
|
||||||
title: '小工具',
|
|
||||||
},
|
|
||||||
component: () => import(
|
|
||||||
/* webpackChunkName: 'tools-detail' */
|
|
||||||
'@/views/ToolsDetail.vue'
|
|
||||||
),
|
|
||||||
children: tools
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/settings',
|
|
||||||
name: 'Settings',
|
|
||||||
meta: {
|
|
||||||
loadingBar: true,
|
|
||||||
title: '设置',
|
|
||||||
},
|
|
||||||
component: () => import(
|
|
||||||
/* webpackChunkName: 'settings-view' */
|
|
||||||
'@/views/SettingsView.vue'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/about',
|
|
||||||
name: 'About',
|
|
||||||
meta: {
|
|
||||||
loadingBar: true,
|
|
||||||
title: '关于',
|
|
||||||
},
|
|
||||||
component: () => import(
|
|
||||||
/* webpackChunkName: 'about-view' */
|
|
||||||
'@/views/AboutView.vue'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export default routes;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import navTools from '@/assets/js/navTools';
|
|
||||||
|
|
||||||
/** @type { import('vue-router').RouteConfig[] } */
|
|
||||||
const routes = [];
|
|
||||||
|
|
||||||
// 提取工具列表信息,添加到路由
|
|
||||||
for (let categoryName in navTools) {
|
|
||||||
|
|
||||||
const toolList = (navTools[categoryName].list || []);
|
|
||||||
|
|
||||||
toolList.forEach((toolItem) => {
|
|
||||||
const {
|
|
||||||
name: toolName = null,
|
|
||||||
component = null,
|
|
||||||
title = '未知',
|
|
||||||
update = '',
|
|
||||||
version = '',
|
|
||||||
} = toolItem;
|
|
||||||
|
|
||||||
if (toolName === null || component === null) {
|
|
||||||
console.warn('部分小工具未设置工具名称或组件名称。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
routes.push({
|
|
||||||
path: `/tool/${toolName}`,
|
|
||||||
name: `Tool${component}`,
|
|
||||||
meta: {
|
|
||||||
loadingBar: true,
|
|
||||||
title,
|
|
||||||
update,
|
|
||||||
version,
|
|
||||||
},
|
|
||||||
component: () => import(`@/views/Tools/${component}.vue`)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default routes;
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
import Vuex from 'vuex';
|
|
||||||
|
|
||||||
Vue.use(Vuex);
|
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
state: {
|
|
||||||
|
|
||||||
/** 设置 */
|
|
||||||
config: {
|
|
||||||
fontSize: 16,
|
|
||||||
showSiteTitle: true,
|
|
||||||
sideMenuCollapse: true,
|
|
||||||
searchEngine: '百度',
|
|
||||||
searchSuggestion: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 显示加载动画 */
|
|
||||||
showLoading: false,
|
|
||||||
|
|
||||||
},
|
|
||||||
getters: {},
|
|
||||||
mutations: {
|
|
||||||
|
|
||||||
/** 从本地储存中读取设置 */
|
|
||||||
readConfig(state) {
|
|
||||||
|
|
||||||
let keyName = 'fn_config';
|
|
||||||
let storage = localStorage.getItem(keyName);
|
|
||||||
let parsed = null;
|
|
||||||
let config = state.config;
|
|
||||||
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(storage);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解析保存的设置失败。');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsed && parsed.constructor === Object) {
|
|
||||||
// [本地储存中有数据,读取]
|
|
||||||
for (let key in config) {
|
|
||||||
let value = parsed[key];
|
|
||||||
|
|
||||||
if (typeof value !== 'undefined') {
|
|
||||||
config[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// [本地储存中无数据,写入]
|
|
||||||
localStorage.setItem(keyName, JSON.stringify(config));
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 重置保存的设置 */
|
|
||||||
resetConfig() {
|
|
||||||
localStorage.removeItem('fn_config');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 更新设置
|
|
||||||
* @param {object} payload
|
|
||||||
* @param {string} payload.key
|
|
||||||
* @param {any} payload.value
|
|
||||||
*/
|
|
||||||
setConfig(state, payload) {
|
|
||||||
|
|
||||||
const {
|
|
||||||
key: cKey = null,
|
|
||||||
value: cValue = null,
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
const { config } = state;
|
|
||||||
|
|
||||||
if (cKey === null) {
|
|
||||||
console.error('更新设置失败,key 为空。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof config[cKey] === 'undefined') {
|
|
||||||
console.error('更新设置失败,key 不存在。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新设置
|
|
||||||
config[cKey] = cValue;
|
|
||||||
// 保存设置
|
|
||||||
localStorage.setItem('fn_config', JSON.stringify(config));
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 切换加载动画显示隐藏 */
|
|
||||||
toggleLoading(state, payload = false) {
|
|
||||||
state.showLoading = payload;
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
/** 切换导航链接侧边菜单显示隐藏 */
|
|
||||||
toggleSideCollapse(store) {
|
|
||||||
|
|
||||||
const { sideMenuCollapse } = store.state.config;
|
|
||||||
|
|
||||||
store.commit('setConfig', {
|
|
||||||
key: 'sideMenuCollapse',
|
|
||||||
value: !sideMenuCollapse,
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
modules: {},
|
|
||||||
});
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-container class="about">
|
|
||||||
<div class="wrapper shadow-2">
|
|
||||||
|
|
||||||
<div class="text">
|
|
||||||
<div class="title">简介</div>
|
|
||||||
<div class="content">
|
|
||||||
<p>一个多功能的网址导航,绿色无广告。</p>
|
|
||||||
<p>右键点击(PC端)或长按(移动端)链接项可查看链接详情。</p>
|
|
||||||
<p>当前版本:2.1.0</p>
|
|
||||||
<p>链接版本:{{ navLinks.version }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text">
|
|
||||||
<div class="title">开发人员</div>
|
|
||||||
<div class="content">
|
|
||||||
<p>Frost-ZX</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text">
|
|
||||||
<div class="title">注意事项</div>
|
|
||||||
<div class="content">
|
|
||||||
<p>部分链接由于不能及时更新,可能已过期(不存在、指向了错误的网站等),访问时请注意。</p>
|
|
||||||
<p>若您在使用时发现相关情况,欢迎进行反馈。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text">
|
|
||||||
<div class="title">GitHub</div>
|
|
||||||
<div class="content">
|
|
||||||
<p>
|
|
||||||
<el-link :href="info.github" target="_blank" type="primary">{{ info.github }}</el-link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</el-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import navLinks from '@/assets/js/navLinks';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'AboutView',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
navLinks,
|
|
||||||
info: {
|
|
||||||
github: 'https://github.com/Frost-ZX/frost-navigation'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.about {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: @colorWhite;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 50rem;
|
|
||||||
background-color: #FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
line-height: 2.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: @textPrimary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
line-height: 1.8rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: @textSecondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="about-view flex-col">
|
||||||
|
<div class="app-view-header">
|
||||||
|
<span>{{ ABOUT_MODULE_TITLE }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-view-content with-margin">
|
||||||
|
|
||||||
|
<n-h4>简介</n-h4>
|
||||||
|
|
||||||
|
<n-p>一个多功能的网址导航,绿色无广告。</n-p>
|
||||||
|
<n-p>当前版本:V{{ version }}</n-p>
|
||||||
|
<n-p>导航链接列表更新日期:{{ NAV_LINK_DATE }}</n-p>
|
||||||
|
|
||||||
|
<n-h4>开发人员</n-h4>
|
||||||
|
|
||||||
|
<n-ul>
|
||||||
|
<n-li>Frost-ZX</n-li>
|
||||||
|
</n-ul>
|
||||||
|
|
||||||
|
<n-h4>项目地址</n-h4>
|
||||||
|
|
||||||
|
<n-ul>
|
||||||
|
<n-li>
|
||||||
|
<span>GitHub:</span>
|
||||||
|
<n-a :href="URL_GITHUB" target="_blank">{{ URL_GITHUB }}</n-a>
|
||||||
|
</n-li>
|
||||||
|
<n-li>
|
||||||
|
<span>Gitee:</span>
|
||||||
|
<n-a :href="URL_GITEE" target="_blank">{{ URL_GITEE }}</n-a>
|
||||||
|
</n-li>
|
||||||
|
</n-ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { NA, NH4, NLi, NP, NUl } from 'naive-ui';
|
||||||
|
import { version } from '@package-json';
|
||||||
|
import { ABOUT_MODULE_TITLE } from '@/config/modules';
|
||||||
|
|
||||||
|
/** 导航链接列表更新日期 */
|
||||||
|
const NAV_LINK_DATE = window['NAV_LINK_DATE'];
|
||||||
|
|
||||||
|
/** Gitee 地址 */
|
||||||
|
const URL_GITEE = 'https://gitee.com/frost-craft/frost-navigation';
|
||||||
|
|
||||||
|
/** GitHub 地址 */
|
||||||
|
const URL_GITHUB = 'https://github.com/Frost-ZX/frost-navigation';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
||||||
@@ -1,938 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-container class="home">
|
|
||||||
|
|
||||||
<!-- 侧边栏 -->
|
|
||||||
<el-aside class="home-aside shadow-1">
|
|
||||||
<el-menu
|
|
||||||
class="side-nav"
|
|
||||||
default-active="search"
|
|
||||||
:collapse="appConfig.sideMenuCollapse"
|
|
||||||
:collapse-transition="false"
|
|
||||||
@select="changeCategory"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- 搜索引擎 -->
|
|
||||||
<el-menu-item index="search">
|
|
||||||
<i class="el-icon-search"></i>
|
|
||||||
<span slot="title">搜索</span>
|
|
||||||
</el-menu-item>
|
|
||||||
|
|
||||||
<!-- 全部链接 -->
|
|
||||||
<el-menu-item index="all">
|
|
||||||
<i class="el-icon-menu"></i>
|
|
||||||
<span slot="title">全部链接</span>
|
|
||||||
</el-menu-item>
|
|
||||||
|
|
||||||
<!-- 分类 -->
|
|
||||||
<el-menu-item
|
|
||||||
v-for="(item, itemIndex) in navLinks.list"
|
|
||||||
:key="'list-' + itemIndex"
|
|
||||||
:index="itemIndex.toString()"
|
|
||||||
>
|
|
||||||
<i :class="item.icon || 'el-icon-link'"></i>
|
|
||||||
<span slot="title">{{ item.title }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
|
|
||||||
</el-menu>
|
|
||||||
</el-aside>
|
|
||||||
|
|
||||||
<!-- 内容 -->
|
|
||||||
<el-main class="home-content">
|
|
||||||
<div class="wrapper">
|
|
||||||
|
|
||||||
<!-- 搜索引擎 -->
|
|
||||||
<div v-show="show.searchNet" class="search-engine">
|
|
||||||
|
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<div :class="['search-bar', 'shadow-3', { suggest: showSES }]">
|
|
||||||
|
|
||||||
<!-- 输入 -->
|
|
||||||
<input
|
|
||||||
v-model="searchNet.keyword"
|
|
||||||
class="input"
|
|
||||||
type="text"
|
|
||||||
@blur="searchNet.isFocus = false"
|
|
||||||
@focus="searchNet.isFocus = true"
|
|
||||||
@keydown.enter.exact="searchNetSubmit()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 清除 -->
|
|
||||||
<div
|
|
||||||
v-show="searchNet.keyword.length > 0"
|
|
||||||
class="btn btn-clear"
|
|
||||||
@click="searchNet.keyword = ''"
|
|
||||||
>
|
|
||||||
<i class="el-icon-close"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索 -->
|
|
||||||
<div class="btn btn-search" @click="searchNetSubmit()">
|
|
||||||
<i class="el-icon-search"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 关键词建议 -->
|
|
||||||
<div class="suggestion shadow-3">
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-for="item in searchNet.suggest.list"
|
|
||||||
:key="item.id"
|
|
||||||
@click="searchNet.keyword = item.label"
|
|
||||||
>
|
|
||||||
<!-- 文本 -->
|
|
||||||
<span class="label">{{ item.label }}</span>
|
|
||||||
<!-- 来源 -->
|
|
||||||
<el-tag
|
|
||||||
class="tag"
|
|
||||||
effect="plain"
|
|
||||||
size="mini"
|
|
||||||
type="success"
|
|
||||||
>{{ searchNet.suggest.name }}</el-tag>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 选择搜索引擎 -->
|
|
||||||
<el-radio-group
|
|
||||||
v-model="configSearchEngine"
|
|
||||||
size="small"
|
|
||||||
:class="['search-type', { fade: searchNet.isFocus }]"
|
|
||||||
>
|
|
||||||
<!-- 分类 -->
|
|
||||||
<div
|
|
||||||
v-for="(category, cIndex) in searchNet.list"
|
|
||||||
:key="cIndex"
|
|
||||||
class="category"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- 标题 -->
|
|
||||||
<div class="title">{{ category.title }}</div>
|
|
||||||
|
|
||||||
<!-- 项 -->
|
|
||||||
<el-radio
|
|
||||||
v-for="item in category.list"
|
|
||||||
:key="item.name"
|
|
||||||
:label="item.name"
|
|
||||||
class="shadow-2"
|
|
||||||
>
|
|
||||||
<IconElement :path="item.icon || 'website/default.svg'" size="1.2em" />
|
|
||||||
<i class="name">{{ item.name }}</i>
|
|
||||||
<i class="desc limit-line-1">{{ item.desc }}</i>
|
|
||||||
</el-radio>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</el-radio-group>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 链接搜索框 -->
|
|
||||||
<el-input
|
|
||||||
v-show="show.searchLink"
|
|
||||||
v-model="searchLink.keyword"
|
|
||||||
class="link-search shadow-2"
|
|
||||||
placeholder="搜索链接"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<el-select slot="prepend" v-model="searchLink.type" placeholder="类型">
|
|
||||||
<el-option label="全部" value="all"></el-option>
|
|
||||||
<el-option label="标题" value="title"></el-option>
|
|
||||||
<el-option label="链接" value="link"></el-option>
|
|
||||||
<el-option label="简介" value="desc"></el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-input>
|
|
||||||
|
|
||||||
<!-- 链接列表树 -->
|
|
||||||
<el-tree
|
|
||||||
v-show="show.linkTree"
|
|
||||||
ref="linkTree"
|
|
||||||
class="link-tree shadow-2"
|
|
||||||
:data="currentLinks"
|
|
||||||
node-key="id"
|
|
||||||
empty-text="没有查找到内容"
|
|
||||||
:props="{ label: 'title', children: 'items' }"
|
|
||||||
:filter-node-method="searchLinkSubmit"
|
|
||||||
:default-expand-all="false"
|
|
||||||
:expand-on-click-node="true"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
slot-scope="{ node, data }"
|
|
||||||
class="link-item"
|
|
||||||
:title="data.update"
|
|
||||||
@click="openLink(data.link, data.showOnly)"
|
|
||||||
@contextmenu.prevent="openDetail(data)"
|
|
||||||
>
|
|
||||||
<div class="row title">
|
|
||||||
<span>{{ node.label }}</span>
|
|
||||||
<span v-if="data.desc"> - {{ data.desc }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="row link">
|
|
||||||
<span>{{ data.link }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-tree>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</el-main>
|
|
||||||
|
|
||||||
<!-- 链接详情 -->
|
|
||||||
<el-dialog
|
|
||||||
:append-to-body="true"
|
|
||||||
custom-class="link-detail"
|
|
||||||
title="详情"
|
|
||||||
:visible="linkDetail.show"
|
|
||||||
@close="linkDetail.show = false"
|
|
||||||
>
|
|
||||||
<div class="content">
|
|
||||||
<div class="row">
|
|
||||||
<div class="label">ID:</div>
|
|
||||||
<div class="text">{{ linkDetail.id }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="label">标题:</div>
|
|
||||||
<div class="text">{{ linkDetail.title }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="label">链接:</div>
|
|
||||||
<div class="text">
|
|
||||||
<el-link
|
|
||||||
v-show="!linkDetail.linkCopy"
|
|
||||||
type="primary"
|
|
||||||
:href="linkDetail.link"
|
|
||||||
target="_blank"
|
|
||||||
>{{ linkDetail.link }}</el-link>
|
|
||||||
<input
|
|
||||||
v-show="linkDetail.linkCopy"
|
|
||||||
type="text"
|
|
||||||
:value="linkDetail.link"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-show="linkDetail.desc != ''" class="row">
|
|
||||||
<div class="label">简介:</div>
|
|
||||||
<div class="text">{{ linkDetail.desc }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-show="linkDetail.update != ''" class="row">
|
|
||||||
<div class="label">更新:</div>
|
|
||||||
<div class="text">{{ linkDetail.update }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
</el-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* @typedef {object} NavLinkItem
|
|
||||||
* @property {string} title
|
|
||||||
* @property {string} desc
|
|
||||||
* @property {string} link
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { mapState } from 'vuex';
|
|
||||||
|
|
||||||
import navLinks from '@/assets/js/navLinks';
|
|
||||||
import searchUtils from '@/assets/js/search-utils';
|
|
||||||
import utils from '@/assets/js/utils';
|
|
||||||
import IconElement from '@/components/IconElement.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HomeView',
|
|
||||||
components: {
|
|
||||||
IconElement
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
|
|
||||||
/** 显示的内容 */
|
|
||||||
show: {
|
|
||||||
searchNet: true,
|
|
||||||
searchLink: false,
|
|
||||||
linkTree: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 网络搜索 */
|
|
||||||
searchNet: {
|
|
||||||
isFocus: false,
|
|
||||||
keyword: '',
|
|
||||||
list: searchUtils.searchEngines,
|
|
||||||
url: '',
|
|
||||||
debounce: null,
|
|
||||||
suggest: {
|
|
||||||
list: [],
|
|
||||||
name: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 导航链接数据 */
|
|
||||||
navLinks,
|
|
||||||
|
|
||||||
/** 当前显示的链接 */
|
|
||||||
currentLinks: [],
|
|
||||||
|
|
||||||
/** 链接搜索 */
|
|
||||||
searchLink: {
|
|
||||||
debounce: null,
|
|
||||||
keyword: '',
|
|
||||||
type: 'all'
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 链接详情 */
|
|
||||||
linkDetail: {
|
|
||||||
show: false,
|
|
||||||
id: '',
|
|
||||||
title: '',
|
|
||||||
link: '',
|
|
||||||
linkCopy: false,
|
|
||||||
desc: '',
|
|
||||||
update: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
|
|
||||||
...mapState({
|
|
||||||
appConfig: 'config',
|
|
||||||
}),
|
|
||||||
|
|
||||||
/** 设置 - 搜索引擎名称 */
|
|
||||||
configSearchEngine: {
|
|
||||||
/** @this */
|
|
||||||
get() {
|
|
||||||
return this.appConfig.searchEngine;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$store.commit('setConfig', {
|
|
||||||
key: 'searchEngine',
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 显示搜索引擎关键词建议
|
|
||||||
* @this
|
|
||||||
*/
|
|
||||||
showSES() {
|
|
||||||
var sn = this.searchNet;
|
|
||||||
var isShow = (
|
|
||||||
(sn.isFocus) &&
|
|
||||||
(sn.keyword !== '') &&
|
|
||||||
(sn.suggest.list.length > 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
return isShow;
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
|
|
||||||
'searchNet.keyword': {
|
|
||||||
handler(value) {
|
|
||||||
if (!this.appConfig.searchSuggestion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearInterval(this.searchNet.debounce);
|
|
||||||
this.searchNet.debounce = setTimeout(() => {
|
|
||||||
value = window.encodeURIComponent(value);
|
|
||||||
this.searchNetGS(value);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'searchLink.keyword': {
|
|
||||||
handler(value) {
|
|
||||||
clearTimeout(this.searchLink.debounce);
|
|
||||||
this.searchLink.debounce = setTimeout(() => {
|
|
||||||
this.$refs.linkTree.filter(value);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'searchLink.type': {
|
|
||||||
handler() {
|
|
||||||
// 更改搜索类型时自动重新搜索
|
|
||||||
this.$refs.linkTree.filter(this.searchLink.keyword);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
/** 更改当前显示的分类 */
|
|
||||||
changeCategory(index) {
|
|
||||||
const { navLinks, searchLink, show } = this;
|
|
||||||
|
|
||||||
if (index === 'search') {
|
|
||||||
this.currentLinks = [];
|
|
||||||
show.searchNet = true;
|
|
||||||
show.searchLink = false;
|
|
||||||
show.linkTree = false;
|
|
||||||
} else if (index === 'all') {
|
|
||||||
this.currentLinks = navLinks.list;
|
|
||||||
show.searchNet = false;
|
|
||||||
show.searchLink = true;
|
|
||||||
show.linkTree = true;
|
|
||||||
} else {
|
|
||||||
this.currentLinks = navLinks.list[Number(index)].items;
|
|
||||||
show.searchNet = false;
|
|
||||||
show.searchLink = true;
|
|
||||||
show.linkTree = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchLink.keyword = '';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 查看详情
|
|
||||||
* @param {object} datas 当前链接的数据
|
|
||||||
*/
|
|
||||||
openDetail(datas) {
|
|
||||||
// 非链接 / 仅显示
|
|
||||||
if (datas.link === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var detail = this.linkDetail;
|
|
||||||
|
|
||||||
detail.id = datas.id;
|
|
||||||
detail.title = datas.title;
|
|
||||||
detail.link = datas.link;
|
|
||||||
detail.linkCopy = datas.showOnly || false;
|
|
||||||
detail.desc = datas.desc || '';
|
|
||||||
detail.update = datas.update || '';
|
|
||||||
detail.show = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 打开链接
|
|
||||||
* @param {string} link 需要打开的链接
|
|
||||||
* @param {boolean} showOnly 是否仅显示链接
|
|
||||||
*/
|
|
||||||
openLink(link, showOnly) {
|
|
||||||
if (link === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showOnly) {
|
|
||||||
this.$message({
|
|
||||||
duration: 5000,
|
|
||||||
message: '请在链接详情中复制后手动打开',
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.open(link, '_blank');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 搜索引擎(获取关键词建议)
|
|
||||||
* @param {string} keyword 当前输入的关键词
|
|
||||||
*/
|
|
||||||
searchNetGS(keyword) {
|
|
||||||
var suggest = this.searchNet.suggest;
|
|
||||||
// 设置的搜索引擎名称
|
|
||||||
var configSE = this.configSearchEngine;
|
|
||||||
// 关键词建议来源信息(默认使用百度)
|
|
||||||
var reqURLs = [
|
|
||||||
{
|
|
||||||
name: '百度',
|
|
||||||
url: `https://www.baidu.com/sugrec?json=1&prod=pc&wd=${keyword}&cb=cbSES`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'bilibili',
|
|
||||||
url: `https://s.search.bilibili.com/main/suggest?func=suggest&main_ver=v1&term=${keyword}&jsonp=jsonp&callback=cbSES`
|
|
||||||
},
|
|
||||||
];
|
|
||||||
var reqURL = reqURLs[0].url;
|
|
||||||
|
|
||||||
suggest.list = [];
|
|
||||||
suggest.name = reqURLs[0].name;
|
|
||||||
|
|
||||||
if (keyword === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据当前搜索引擎选择请求地址
|
|
||||||
for (let i = 0; i < reqURLs.length; i++) {
|
|
||||||
let item = reqURLs[i];
|
|
||||||
if (item.name === configSE) {
|
|
||||||
reqURL = item.url;
|
|
||||||
suggest.name = item.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理数据
|
|
||||||
var cbFunc = (data) => {
|
|
||||||
|
|
||||||
var id = 0; // 结果 ID
|
|
||||||
var reqName = suggest.name; // 来源名称
|
|
||||||
|
|
||||||
if (reqName === '百度') {
|
|
||||||
// [百度]
|
|
||||||
let result = (data.g || []);
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.forEach((item) => {
|
|
||||||
id += 1;
|
|
||||||
suggest.list.push({
|
|
||||||
id,
|
|
||||||
label: item.q,
|
|
||||||
highlight: keyword
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (reqName === 'bilibili') {
|
|
||||||
// [bilibili]
|
|
||||||
let result = (data.result ? (data.result.tag || []) : []);
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.forEach((item) => {
|
|
||||||
id += 1;
|
|
||||||
suggest.list.push({
|
|
||||||
id,
|
|
||||||
label: item['value'],
|
|
||||||
highlight: keyword
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用 JSONP 获取
|
|
||||||
utils.jsonp({
|
|
||||||
url: reqURL,
|
|
||||||
cbName: 'cbSES',
|
|
||||||
cbFunc
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 搜索引擎(搜索) */
|
|
||||||
searchNetSubmit() {
|
|
||||||
var vm = this;
|
|
||||||
var sn = this.searchNet;
|
|
||||||
var selectedSE = this.configSearchEngine;
|
|
||||||
var keyword = sn.keyword;
|
|
||||||
var url = '';
|
|
||||||
|
|
||||||
if (keyword) {
|
|
||||||
keyword = window.encodeURIComponent(keyword);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let category in sn.list) {
|
|
||||||
let list = sn.list[category].list;
|
|
||||||
|
|
||||||
for (let index in list) {
|
|
||||||
if (list[index].name === selectedSE) {
|
|
||||||
url = list[index].url.replace(/%keyword%/, keyword);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vm.openLink(url);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 搜索链接(搜索)
|
|
||||||
* @param {string} keyword 关键词
|
|
||||||
* @param {NavLinkItem} data 每一项链接的信息
|
|
||||||
*/
|
|
||||||
searchLinkSubmit(keyword, data) {
|
|
||||||
|
|
||||||
// 关键词为空,显示全部
|
|
||||||
if (keyword === '') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转为小写
|
|
||||||
keyword = keyword.toLowerCase();
|
|
||||||
|
|
||||||
const type = this.searchLink.type;
|
|
||||||
const title = (data.title || '').toLowerCase();
|
|
||||||
const link = (data.link || '').toLowerCase();
|
|
||||||
const desc = (data.desc || '').toLowerCase();
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
// 全部
|
|
||||||
case 'all':
|
|
||||||
return (
|
|
||||||
title.includes(keyword) ||
|
|
||||||
link.includes(keyword) ||
|
|
||||||
desc.includes(keyword)
|
|
||||||
);
|
|
||||||
// 标题
|
|
||||||
case 'title':
|
|
||||||
return title.includes(keyword);
|
|
||||||
// 链接
|
|
||||||
case 'link':
|
|
||||||
return link.includes(keyword);
|
|
||||||
// 简介
|
|
||||||
case 'desc':
|
|
||||||
return desc.includes(keyword);
|
|
||||||
// 未知
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.home-aside {
|
|
||||||
position: relative;
|
|
||||||
z-index: 110;
|
|
||||||
width: auto !important;
|
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
.side-nav {
|
|
||||||
min-height: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: @colorWhite;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 16rem;
|
|
||||||
max-width: 64rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-engine {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar {
|
|
||||||
@barHeight: 2.8rem;
|
|
||||||
@barRadius: 0.3rem;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: sticky;
|
|
||||||
top: 2.5rem;
|
|
||||||
z-index: 100;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 40rem;
|
|
||||||
height: @barHeight;
|
|
||||||
border-radius: @barRadius;
|
|
||||||
background-color: #FFF;
|
|
||||||
|
|
||||||
&.suggest {
|
|
||||||
border-radius: @barRadius @barRadius 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
flex-grow: 1;
|
|
||||||
padding-left: 1rem;
|
|
||||||
width: 0;
|
|
||||||
height: 100%;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 2.8rem;
|
|
||||||
height: 2.8rem;
|
|
||||||
background-color: transparent;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-clear {
|
|
||||||
width: 2rem;
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity @transitionTime;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-search {
|
|
||||||
border-radius: 0 @barRadius @barRadius 0;
|
|
||||||
color: @colorPrimary;
|
|
||||||
transition: background @transitionTime, color @transitionTime;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: @colorPrimary;
|
|
||||||
color: #FFF;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.suggest .btn-search {
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion {
|
|
||||||
display: block;
|
|
||||||
visibility: hidden;
|
|
||||||
position: absolute;
|
|
||||||
top: @barHeight;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 0.1rem solid #EEE;
|
|
||||||
border-radius: 0 0 @barRadius @barRadius;
|
|
||||||
background-color: #FFF;
|
|
||||||
overflow: hidden;
|
|
||||||
// 延迟隐藏
|
|
||||||
transition: visibility 0.2s;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
list-style: none;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: @colorWhite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
flex-grow: 1;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.suggest .suggestion {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-type {
|
|
||||||
display: block;
|
|
||||||
margin: 4.5rem 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: opacity calc(@transitionTime * 4);
|
|
||||||
|
|
||||||
&.fade {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .category {
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-radio {
|
|
||||||
margin: 0.5rem;
|
|
||||||
padding: 0.8rem 1rem;
|
|
||||||
width: 15rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
border-left: solid 0.2rem transparent;
|
|
||||||
background-color: #FFF;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: normal;
|
|
||||||
transition: border @transitionTime;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-left-color: @colorSecondary;
|
|
||||||
color: @colorSecondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-checked {
|
|
||||||
border-left-color: @colorPrimary;
|
|
||||||
color: @colorPrimary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-radio__input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-radio__label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
transition: color @transitionTime;
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.125rem 0;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fn-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #CCC;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .link-search {
|
|
||||||
@height: 2.8rem;
|
|
||||||
|
|
||||||
position: sticky;
|
|
||||||
z-index: 100;
|
|
||||||
top: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
line-height: @height;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> div,
|
|
||||||
> input {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input-group__prepend {
|
|
||||||
background-color: #FFF;
|
|
||||||
|
|
||||||
.el-select .el-input {
|
|
||||||
width: 4.5rem;
|
|
||||||
|
|
||||||
input {
|
|
||||||
padding: 0 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__inner {
|
|
||||||
height: @height;
|
|
||||||
line-height: @height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-tree {
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
/deep/ .el-tree-node__content {
|
|
||||||
height: 3.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-item {
|
|
||||||
flex-grow: 1;
|
|
||||||
width: 0;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .link-detail {
|
|
||||||
width: 80%;
|
|
||||||
max-width: 32rem;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 3.5em;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"] {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
line-height: 1em;
|
|
||||||
font-size: inherit;
|
|
||||||
color: @colorSecondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
@media screen and (max-width: 400px) {
|
|
||||||
.home-content {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-engine {
|
|
||||||
.search-bar {
|
|
||||||
top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-type {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.el-radio {
|
|
||||||
width: 90% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="index-view flex-col">
|
||||||
|
<div class="app-view-header">
|
||||||
|
<span>{{ appDesc }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-view-content">
|
||||||
|
|
||||||
|
<div class="date-time">
|
||||||
|
<div class="time-info">{{ timeInfo.time }}</div>
|
||||||
|
<div class="date-info">
|
||||||
|
<div class="date-info-row">
|
||||||
|
<span>{{ timeInfo.date1 }}</span>
|
||||||
|
<span>{{ timeInfo.week }}</span>
|
||||||
|
<span v-show="timeInfo.festival">{{ timeInfo.festival }}</span>
|
||||||
|
<span v-show="timeInfo.solarTerm">{{ timeInfo.solarTerm }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="date-info-row">
|
||||||
|
<span>{{ timeInfo.date2 }}</span>
|
||||||
|
<span>{{ timeInfo.date3 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
description as appDesc,
|
||||||
|
} from '@package-json';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reactive, ref,
|
||||||
|
onBeforeMount, onBeforeUnmount,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import lunisolar from 'lunisolar';
|
||||||
|
|
||||||
|
/** 当前时间信息 */
|
||||||
|
const timeInfo = reactive({
|
||||||
|
|
||||||
|
/** 公历 */
|
||||||
|
date1: '',
|
||||||
|
|
||||||
|
/** 农历 */
|
||||||
|
date2: '',
|
||||||
|
|
||||||
|
/** 八字 */
|
||||||
|
date3: '',
|
||||||
|
|
||||||
|
/** 节日 */
|
||||||
|
festival: '',
|
||||||
|
|
||||||
|
/** 节气 */
|
||||||
|
solarTerm: '',
|
||||||
|
|
||||||
|
/** 时间 */
|
||||||
|
time: '',
|
||||||
|
|
||||||
|
/** 星期 */
|
||||||
|
week: '',
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 时间更新定时器 */
|
||||||
|
const timeUpdater = ref(null);
|
||||||
|
|
||||||
|
/** 更新时间信息 */
|
||||||
|
function updateTime() {
|
||||||
|
|
||||||
|
let date = lunisolar();
|
||||||
|
|
||||||
|
let { markers, solarTerm } = date;
|
||||||
|
|
||||||
|
// 公历
|
||||||
|
timeInfo.date1 = date.format('YYYY 年 M 月 D 日');
|
||||||
|
// 农历
|
||||||
|
timeInfo.date2 = date.format('lMlD');
|
||||||
|
// 八字
|
||||||
|
timeInfo.date3 = date.format('cY年 cM月 cD日');
|
||||||
|
// 节日
|
||||||
|
timeInfo.festival = markers.list.map(v => v.name).join(' & ');
|
||||||
|
// 节气
|
||||||
|
timeInfo.solarTerm = solarTerm ? solarTerm.name : '';
|
||||||
|
// 时间
|
||||||
|
timeInfo.time = date.format('HH:mm:ss');
|
||||||
|
// 星期
|
||||||
|
timeInfo.week = date.format('dddd');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
updateTime();
|
||||||
|
timeUpdater.value = setInterval(updateTime, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timeUpdater.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.app-view-content {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-time {
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.time-info {
|
||||||
|
font-size: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-info {
|
||||||
|
margin-bottom: 160px;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
span:not(:first-child)::before {
|
||||||
|
content: " / ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div class="minecraft-ctrl-view flex-col">
|
||||||
|
<div class="app-view-header">
|
||||||
|
<span>{{ MC_CTRL_MODULE_TITLE }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-view-content is-transparent">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
MC_CTRL_MODULE_TITLE,
|
||||||
|
} from '@/config/modules';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
<template>
|
||||||
|
<div class="nav-view flex-col">
|
||||||
|
|
||||||
|
<div class="app-view-header">
|
||||||
|
<div>{{ NAV_MODULE_TITLE }}</div>
|
||||||
|
<div class="placeholder"></div>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:circle="true"
|
||||||
|
:secondary="true"
|
||||||
|
@click="showHelp"
|
||||||
|
>
|
||||||
|
<span class="mdi mdi-help"></span>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-view-content">
|
||||||
|
<n-layout
|
||||||
|
class="page-layout"
|
||||||
|
:has-sider="true"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 左 -->
|
||||||
|
<n-layout-sider
|
||||||
|
class="left-aside"
|
||||||
|
collapse-mode="width"
|
||||||
|
show-trigger="arrow-circle"
|
||||||
|
:bordered="true"
|
||||||
|
:collapsed="storeNavView.isAsideCollapsed.value"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:native-scrollbar="false"
|
||||||
|
:scrollbar-props="{ trigger: 'hover' }"
|
||||||
|
:width="240"
|
||||||
|
@collapse="storeNavView.isAsideCollapsed.value = true"
|
||||||
|
@expand="storeNavView.isAsideCollapsed.value = false"
|
||||||
|
>
|
||||||
|
<n-menu
|
||||||
|
v-model:value="navLinksTitle"
|
||||||
|
mode="vertical"
|
||||||
|
:collapsed="storeNavView.isAsideCollapsed.value"
|
||||||
|
:collapsed-icon-size="24"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:icon-size="24"
|
||||||
|
:indent="24"
|
||||||
|
:options="navLinksCategory"
|
||||||
|
:on-update:value="handleSelectCategory"
|
||||||
|
/>
|
||||||
|
</n-layout-sider>
|
||||||
|
|
||||||
|
<!-- 右 -->
|
||||||
|
<n-layout class="right-content">
|
||||||
|
<div class="right-content-header">
|
||||||
|
<n-input-group>
|
||||||
|
<n-select
|
||||||
|
v-model:value="storeNavView.searchType.value"
|
||||||
|
class="search-type"
|
||||||
|
:options="searchTypes"
|
||||||
|
/>
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchKeyword"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="搜索链接项"
|
||||||
|
:clearable="true"
|
||||||
|
/>
|
||||||
|
</n-input-group>
|
||||||
|
</div>
|
||||||
|
<div class="right-content-body">
|
||||||
|
<n-scrollbar trigger="none">
|
||||||
|
<n-tree
|
||||||
|
children-field="children"
|
||||||
|
key-field="_key"
|
||||||
|
label-field="title"
|
||||||
|
:cancelable="false"
|
||||||
|
:block-line="true"
|
||||||
|
:block-node="true"
|
||||||
|
:data="navLinksCurr"
|
||||||
|
:filter="treeDataFilter"
|
||||||
|
:pattern="searchKeyword"
|
||||||
|
:render-label="renderTreeLabel"
|
||||||
|
:selectable="true"
|
||||||
|
:show-irrelevant-nodes="false"
|
||||||
|
/>
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
</n-layout>
|
||||||
|
|
||||||
|
</n-layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 链接详情 -->
|
||||||
|
<n-drawer
|
||||||
|
v-model:show="detailDrawer.show"
|
||||||
|
class="nav-url-detail-drawer"
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<n-drawer-content
|
||||||
|
title="链接详情"
|
||||||
|
:closable="true"
|
||||||
|
>
|
||||||
|
<n-descriptions
|
||||||
|
label-placement="top"
|
||||||
|
:column="1"
|
||||||
|
>
|
||||||
|
<n-descriptions-item label="ID">
|
||||||
|
{{ detailDrawer.data._key || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="链接标题">
|
||||||
|
{{ detailDrawer.data.title || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="链接简介">
|
||||||
|
{{ detailDrawer.data.desc || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="链接地址">
|
||||||
|
{{ detailDrawer.data.url || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="更新日期">
|
||||||
|
{{ detailDrawer.data.date || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="是否有效">
|
||||||
|
{{ detailDrawer.data.isInvalid ? '否' : '是' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="jsx" setup>
|
||||||
|
import {
|
||||||
|
NButton, NDescriptions, NDescriptionsItem,
|
||||||
|
NDrawer, NDrawerContent, NInput, NInputGroup,
|
||||||
|
NLayout, NLayoutSider, NLi, NMenu,
|
||||||
|
NScrollbar, NSelect, NTree, NUl,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reactive, shallowRef, onBeforeMount,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
NAV_MODULE_TITLE,
|
||||||
|
} from '@/config/modules';
|
||||||
|
|
||||||
|
import {
|
||||||
|
storeNavView,
|
||||||
|
} from '@/assets/js/local-storage';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$dialog, $message,
|
||||||
|
} from '@/assets/js/naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatNavLinks,
|
||||||
|
} from '@/assets/js/nav-links';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useLocalStorage,
|
||||||
|
} from '@vueuse/core';
|
||||||
|
|
||||||
|
/** 链接详情 */
|
||||||
|
const detailDrawer = reactive({
|
||||||
|
|
||||||
|
/** @type {NavLinkItem | null} */
|
||||||
|
data: null,
|
||||||
|
|
||||||
|
/** @type {boolean} */
|
||||||
|
show: false,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 完整的链接列表 */
|
||||||
|
const navLinksAll = formatNavLinks(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 链接分类列表
|
||||||
|
* @type { import('naive-ui').MenuOption[] }
|
||||||
|
*/
|
||||||
|
const navLinksCategory = navLinksAll.map((item) => {
|
||||||
|
return {
|
||||||
|
icon: () => <span class={item.icon}></span>,
|
||||||
|
key: item.title,
|
||||||
|
label: item.title,
|
||||||
|
_data: item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 当前显示的链接列表
|
||||||
|
* @type {VueRef<NavLinkItem[]>}
|
||||||
|
*/
|
||||||
|
const navLinksCurr = shallowRef([]);
|
||||||
|
|
||||||
|
/** 当前显示的链接分类标题 */
|
||||||
|
const navLinksTitle = shallowRef('');
|
||||||
|
|
||||||
|
/** 搜索关键词 */
|
||||||
|
const searchKeyword = shallowRef('');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 搜索类型列表
|
||||||
|
* @type { import('naive-ui').SelectOption[] }
|
||||||
|
*/
|
||||||
|
const searchTypes = [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
{ label: '标题', value: 'title' },
|
||||||
|
{ label: '链接', value: 'url' },
|
||||||
|
{ label: '简介', value: 'desc' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 切换链接列表
|
||||||
|
* @param {NavLinkItem} [data] 链接分类信息
|
||||||
|
*/
|
||||||
|
function changeList(data = null) {
|
||||||
|
|
||||||
|
let useData = null;
|
||||||
|
let storedKey = '';
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
useData = data;
|
||||||
|
} else {
|
||||||
|
storedKey = storeNavView.currentCategory.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storedKey) {
|
||||||
|
useData = navLinksAll.find((item) => {
|
||||||
|
return item.title === storedKey;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useData) {
|
||||||
|
useData = navLinksAll[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useData) {
|
||||||
|
storeNavView.currentCategory.value = useData.title;
|
||||||
|
navLinksCurr.value = useData.children;
|
||||||
|
navLinksTitle.value = useData.title;
|
||||||
|
} else {
|
||||||
|
storeNavView.currentCategory.value = '';
|
||||||
|
navLinksCurr.value = [];
|
||||||
|
navLinksTitle.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 处理选择链接分类
|
||||||
|
* @param {string} key
|
||||||
|
* @param {NavLinkItem} item
|
||||||
|
*/
|
||||||
|
function handleSelectCategory(key, item) {
|
||||||
|
changeList(item._data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 打开链接
|
||||||
|
* @param {NavLinkItem} data
|
||||||
|
*/
|
||||||
|
function openURL(data) {
|
||||||
|
if (data.isInvalid) {
|
||||||
|
$message.warning('链接已失效,仅支持查看详情');
|
||||||
|
} else if (data.showOnly) {
|
||||||
|
$message.warning('该链接不支持直接打开,请在链接详情中复制后手动打开');
|
||||||
|
} else if (data.url) {
|
||||||
|
window.open(data.url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 树形数据节点内容渲染函数
|
||||||
|
* @param {object} info
|
||||||
|
* @param {NavLinkItem} info.option
|
||||||
|
*/
|
||||||
|
function renderTreeLabel(info) {
|
||||||
|
|
||||||
|
let data = info.option;
|
||||||
|
let isURL = !data.children;
|
||||||
|
|
||||||
|
let open = () => {
|
||||||
|
isURL && openURL(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
let show = () => {
|
||||||
|
isURL && toggleDetailDrawer(true, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div
|
||||||
|
class="item-wrapper"
|
||||||
|
onContextmenu={show}
|
||||||
|
onClick={open}
|
||||||
|
>
|
||||||
|
<div class="item-title">
|
||||||
|
<span>{ data.title }</span>
|
||||||
|
{ data.desc ? <span> - { data.desc }</span> : ''}
|
||||||
|
</div>
|
||||||
|
<div class="item-url">{ data.url }</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示说明 */
|
||||||
|
function showHelp() {
|
||||||
|
$dialog.create({
|
||||||
|
content: () => {
|
||||||
|
return <NUl>
|
||||||
|
<NLi>点击目录项左侧按钮以展开目录。</NLi>
|
||||||
|
<NLi>PC 端右键或移动端长按链接项以显示详情。</NLi>
|
||||||
|
</NUl>
|
||||||
|
},
|
||||||
|
title: '操作说明',
|
||||||
|
type: 'default',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 处理搜索
|
||||||
|
* @param {string} pattern
|
||||||
|
* @param {NavLinkItem} node
|
||||||
|
*/
|
||||||
|
function treeDataFilter(pattern = '', node = null) {
|
||||||
|
|
||||||
|
if (pattern === '') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
pattern = pattern.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = storeNavView.searchType.value;
|
||||||
|
|
||||||
|
let desc = String(node.desc).toLowerCase();
|
||||||
|
let title = String(node.title).toLowerCase();
|
||||||
|
let url = String(node.url).toLowerCase();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'all':
|
||||||
|
return (
|
||||||
|
title.includes(pattern) ||
|
||||||
|
url.includes(pattern) ||
|
||||||
|
desc.includes(pattern)
|
||||||
|
);
|
||||||
|
case 'desc':
|
||||||
|
return desc.includes(pattern);
|
||||||
|
case 'title':
|
||||||
|
return title.includes(pattern);
|
||||||
|
case 'url':
|
||||||
|
return url.includes(pattern);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 切换链接详情显示隐藏
|
||||||
|
* @param {boolean} show
|
||||||
|
* @param {NavLinkItem} data
|
||||||
|
*/
|
||||||
|
function toggleDetailDrawer(show = false, data = null) {
|
||||||
|
if (show) {
|
||||||
|
detailDrawer.data = data;
|
||||||
|
}
|
||||||
|
detailDrawer.show = show;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
changeList(null);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.page-layout {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-aside {
|
||||||
|
:deep(.n-menu-item-content__icon) {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-menu-item-content-header) {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-content {
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
:deep(> div) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-content-header,
|
||||||
|
.right-content-body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-content-header {
|
||||||
|
// 注:右侧 padding 2px 防止输入框聚焦样式显示不全
|
||||||
|
padding: 10px 2px 0 16px;
|
||||||
|
|
||||||
|
.search-type {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-content-body {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 16px 2px 0 16px;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
:deep(.n-tree-node) {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tree-node-content) {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tree-node-content__text) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tree-node-indent > div) {
|
||||||
|
width: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tree-node--selected) {
|
||||||
|
.n-tree-node-content,
|
||||||
|
.n-tree-node-switcher__icon {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tree-node-switcher__icon) {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.item-wrapper) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.item-url) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.nav-url-detail-drawer {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 320px;
|
||||||
|
|
||||||
|
.n-descriptions-table-content {
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||