Compare commits
127 Commits
V2.1.0
..
084afc0cef
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
.vscode
|
||||
|
||||
dist
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
@@ -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,61 @@
|
||||
# 更新日志
|
||||
|
||||
## [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
|
||||
|
||||
Copyright (c) 2019 Frost-ZX
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
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.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <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,3 +1,5 @@
|
||||
# frost-navigation
|
||||
|
||||
## 简介
|
||||
|
||||
一个多功能的网址导航,绿色无广告。
|
||||
@@ -7,39 +9,21 @@
|
||||
### 在线
|
||||
|
||||
- [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/)
|
||||
|
||||
### 离线(需要手动更新)
|
||||
|
||||
1. [点击此处下载](https://github.com/Frost-ZX/frost-navigation/archive/gh-pages.zip)
|
||||
2. 解压,放置于任意合适的喜欢的位置(可重命名)
|
||||
2. 在浏览器中打开 `index.html`(也可以将其加入浏览器的书签栏或在浏览器设置中将其设置为主页)
|
||||
|
||||
### 自定义
|
||||
|
||||
- 设置
|
||||
|
||||
- 在“设置”页面中,可根据需要对字体大小、页面结构等进行调整。
|
||||
|
||||
- 导航链接列表
|
||||
|
||||
- 修改 `static/js/frostNavLinks.js`。
|
||||
2. 在浏览器中打开 `index.html`(也可以将其加入浏览器的书签栏或在浏览器设置中将其设置为主页)
|
||||
|
||||
## 注意
|
||||
|
||||
- 部分链接由于不能及时更新,可能已过期(不存在、指向了错误的网站等),访问时请注意。若您在使用时发现相关情况,欢迎进行反馈。
|
||||
|
||||
## 项目中主要使用的组件库和插件
|
||||
|
||||
- [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/)
|
||||
@@ -57,26 +41,26 @@
|
||||
|
||||
## 开发
|
||||
|
||||
### Project setup
|
||||
### Project Setup
|
||||
|
||||
```
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```
|
||||
pnpm run serve
|
||||
```sh
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
### Compile and Minify for Production
|
||||
|
||||
```
|
||||
pnpm run build-modern
|
||||
```sh
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
### Lint with ESLint
|
||||
|
||||
```
|
||||
pnpm run lint-fix
|
||||
```sh
|
||||
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,21 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"baseUrl": ".",
|
||||
"composite": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@package-json": ["./package.json"],
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"baseUrl": "./",
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"sourceMap": false,
|
||||
"target": "es5"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.vue",
|
||||
"src/**/*.js"
|
||||
]
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./jsconfig.node.json" },
|
||||
{ "path": "./jsconfig.app.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [],
|
||||
"include": [
|
||||
"vite.config.js"
|
||||
]
|
||||
}
|
||||
@@ -1,57 +1,41 @@
|
||||
{
|
||||
"name": "frost-navigation",
|
||||
"version": "2.1.0",
|
||||
"author": {
|
||||
"name": "Frost-ZX"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --mode development",
|
||||
"serve-production": "vue-cli-service serve --mode production",
|
||||
"serve-all": "vue-cli-service serve --mode development --host 0.0.0.0",
|
||||
"serve-local": "vue-cli-service serve --mode development --host 127.0.0.1",
|
||||
"build": "vue-cli-service build --mode production",
|
||||
"build-modern": "vue-cli-service build --mode production --modern",
|
||||
"lint-check": "vue-cli-service lint --no-fix",
|
||||
"lint-fix": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"bookmark-file-parser": "^1.0.6",
|
||||
"codemirror": "^5.65.2",
|
||||
"core-js": "^3.8.3",
|
||||
"dayjs": "^1.11.0",
|
||||
"element-ui": "^2.15.6",
|
||||
"font-awesome": "^4.7.0",
|
||||
"html-entities": "^2.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mathjs": "^10.4.0",
|
||||
"papaparse": "^5.3.2",
|
||||
"ress": "^5.0.2",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@types/papaparse": "^5.3.2",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"@vue/eslint-config-standard": "^6.1.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"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"
|
||||
}
|
||||
"name": "frost-navigation",
|
||||
"description": "Frost Navigation",
|
||||
"version": "3.1.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@frost-utils/javascript": "^2.1.3",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"axios": "^1.7.9",
|
||||
"dayjs": "^1.11.13",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lunisolar": "^2.5.1",
|
||||
"mathjs": "^14.2.0",
|
||||
"naive-ui": "^2.41.0",
|
||||
"radash": "^12.1.0",
|
||||
"uuid": "^11.0.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"zxing-wasm": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.17.16",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"less": "^4.2.2",
|
||||
"vite": "^6.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.0.1
|
||||
@@ -1,320 +1,278 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<n-config-provider
|
||||
:date-locale="configProviderProps.dateLocale"
|
||||
:hljs="hljs"
|
||||
:inline-theme-disabled="configProviderProps.inlineThemeDisabled"
|
||||
:locale="configProviderProps.locale"
|
||||
:theme-overrides="themeOverrides"
|
||||
>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div v-show="showLoading" class="loading-bar">
|
||||
<div class="bar-content"></div>
|
||||
<!-- Naive UI 全局样式 -->
|
||||
<n-global-style></n-global-style>
|
||||
|
||||
<!-- 全局侧边栏 -->
|
||||
<div class="app-aside-wrapper">
|
||||
<app-aside />
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-container>
|
||||
<!-- 路由页面 -->
|
||||
<router-view class="app-view"></router-view>
|
||||
|
||||
<!-- 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
|
||||
>
|
||||
|
||||
<!-- 菜单项 -->
|
||||
<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>
|
||||
|
||||
<!-- Container -->
|
||||
<keep-alive>
|
||||
<router-view class="main-container" />
|
||||
</keep-alive>
|
||||
|
||||
</el-container>
|
||||
|
||||
<!-- 悬浮按钮 -->
|
||||
<FloatingBtn />
|
||||
|
||||
</div>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
<script setup>
|
||||
import {
|
||||
NConfigProvider, NGlobalStyle,
|
||||
useThemeVars,
|
||||
} from 'naive-ui';
|
||||
|
||||
import FloatingBtn from '@/components/FloatingBtn.vue';
|
||||
import {
|
||||
onMounted, onBeforeUnmount,
|
||||
} from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
FloatingBtn,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
import {
|
||||
configProviderProps,
|
||||
} from './assets/js/naive-ui';
|
||||
|
||||
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 菜单项 */
|
||||
headerMenuItems: [
|
||||
{
|
||||
id: 'home',
|
||||
label: '主页',
|
||||
routeName: 'Home'
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
label: '小工具',
|
||||
routeName: 'Tools'
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: '设置',
|
||||
routeName: 'Settings'
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
label: '关于',
|
||||
routeName: 'About'
|
||||
}
|
||||
],
|
||||
import AppAside from './components/AppAside.vue';
|
||||
|
||||
/** 显示下拉菜单 */
|
||||
showHeaderDropdown: false,
|
||||
/** 主题变量配置 */
|
||||
const themeOverrides = configProviderProps.themeOverrides;
|
||||
|
||||
/** 默认主题变量 */
|
||||
const themeVars = useThemeVars();
|
||||
|
||||
/**
|
||||
* @description 阻止默认右键菜单
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
function handleContextMenu(event) {
|
||||
|
||||
let elements = event.composedPath();
|
||||
let classValue = '';
|
||||
let classRegExp = /(n-code|n-input|n-input-number|n-ol|n-select)/;
|
||||
|
||||
// 排除按住 Ctrl 键时
|
||||
if (event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
|
||||
let element = elements[i];
|
||||
|
||||
// 获取元素 class 信息
|
||||
if (element instanceof HTMLElement) {
|
||||
classValue = element.classList.value;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
...mapState({
|
||||
appConfig: 'config',
|
||||
showLoading: 'showLoading',
|
||||
}),
|
||||
// 排除输入框元素
|
||||
if (element instanceof HTMLInputElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** Header 默认激活的菜单项 */
|
||||
headerDefaultActive() {
|
||||
var routeName = this.$route.name;
|
||||
var item = '';
|
||||
// 排除指定元素
|
||||
if (classValue && classRegExp.test(classValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (routeName) {
|
||||
item = routeName.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
},
|
||||
event.preventDefault();
|
||||
|
||||
},
|
||||
watch: {
|
||||
|
||||
// 更新字体大小
|
||||
'appConfig.fontSize': {
|
||||
handler(value) {
|
||||
|
||||
clearTimeout(this.debounceFontSize);
|
||||
this.debounceFontSize = setTimeout(() => {
|
||||
document.documentElement.style.fontSize = `${value}px`;
|
||||
}, 500);
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
// 路由名称
|
||||
'$route.name': {
|
||||
handler() {
|
||||
// 切换路由时隐藏下拉菜单
|
||||
this.showHeaderDropdown = false;
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
|
||||
/** 初始化 */
|
||||
init() {
|
||||
this.$store.commit('readConfig');
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
/** 初始化 CSS 变量列表 */
|
||||
function initCssVars() {
|
||||
|
||||
let rootStyle = document.documentElement.style;
|
||||
let overrides = themeOverrides.common;
|
||||
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)',
|
||||
// 滚动条大小
|
||||
'--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>
|
||||
|
||||
<style lang="less">
|
||||
.loading-bar {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0.2rem;
|
||||
|
||||
.bar-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background-color: @colorPrimary;
|
||||
animation: loadingBar 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes loadingBar {
|
||||
0% {
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
50% {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 200;
|
||||
height: @headerHeight !important;
|
||||
background-color: #FFF;
|
||||
|
||||
@media screen and (min-width: 30rem) {
|
||||
.menu .item-dropdown {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 30rem) {
|
||||
.logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu {
|
||||
.item-normal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item-dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
i {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
margin-right: 1rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-image: url("./assets/icon/favicon.svg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.menu {
|
||||
flex-grow: 1;
|
||||
height: 2.5rem;
|
||||
border: none !important;
|
||||
|
||||
> li {
|
||||
padding: 0 1rem;
|
||||
height: 100%;
|
||||
line-height: 2.5rem;
|
||||
|
||||
&.is-active {
|
||||
border-bottom-color: transparent;
|
||||
color: @colorPrimary !important;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-left: 0;
|
||||
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;
|
||||
// 滚动条
|
||||
::-webkit-scrollbar {
|
||||
width: var(--scrollbar-size);
|
||||
height: var(--scrollbar-size);
|
||||
}
|
||||
::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background-color: #CFCFCF;
|
||||
|
||||
&:hover {
|
||||
background-color: #C0C0C0;
|
||||
}
|
||||
}
|
||||
|
||||
// 预设样式
|
||||
.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%;
|
||||
overflow: hidden;
|
||||
transform: translateY(-100%);
|
||||
transition: transform @transitionTime;
|
||||
}
|
||||
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;
|
||||
|
||||
&.show {
|
||||
transform: translateY(0);
|
||||
* {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.menu .item-dropdown {
|
||||
i {
|
||||
transition: color @transitionTime;
|
||||
.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;
|
||||
}
|
||||
|
||||
&.active i {
|
||||
color: @colorPrimary;
|
||||
.app-view-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
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;
|
||||
|
||||
&.is-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.with-margin {
|
||||
margin: 20px;
|
||||
width: calc(100% - 40px);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Naive UI --
|
||||
|
||||
.n-drawer--right-placement {
|
||||
.n-drawer-body {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.n-drawer-body-content-wrapper {
|
||||
padding: 20px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</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 前缀 */
|
||||
const PREFIX = 'frost-navigation/';
|
||||
|
||||
/** NavView 模块 */
|
||||
export const storeNavView = {
|
||||
|
||||
/** 导航链接侧边栏折叠状态 */
|
||||
isAsideCollapsed: useLocalStorage(PREFIX + 'nav-view/is-aside-collapsed', false),
|
||||
|
||||
/** 导航链接当前选中分类 */
|
||||
currentCategory: useLocalStorage(PREFIX + 'nav-view/current-category', ''),
|
||||
|
||||
/** 导航链接搜索类型 */
|
||||
searchType: useLocalStorage(PREFIX + 'nav-view/search-type', 'all'),
|
||||
|
||||
};
|
||||
|
||||
/** SearchView 模块 */
|
||||
export const storeSearchView = {
|
||||
|
||||
/** 当前使用的搜索引擎名称 */
|
||||
searchEngineName: useLocalStorage(PREFIX + 'search-view/search-engine-name', '必应'),
|
||||
|
||||
};
|
||||
@@ -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,252 @@
|
||||
import {
|
||||
prepareZXingModule,
|
||||
readBarcodes,
|
||||
writeBarcode,
|
||||
} from 'zxing-wasm/full';
|
||||
|
||||
/**
|
||||
* @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: 1,
|
||||
};
|
||||
|
||||
// 配置 wasm 文件路径
|
||||
prepareZXingModule({
|
||||
overrides: {
|
||||
locateFile: (path, prefix) => {
|
||||
if (path.endsWith('.wasm')) {
|
||||
return `./wasm/${path}`;
|
||||
} else {
|
||||
return (prefix + path);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 转换 Blob 为 DataURL
|
||||
* @param {Blob} blob
|
||||
* @param {Callback} callback
|
||||
*/
|
||||
export function blobToDataURL(blob, callback) {
|
||||
|
||||
/** @typedef {(data: { error: boolean, result: string }) => void} Callback */
|
||||
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onerror = function () {
|
||||
callback({
|
||||
error: true,
|
||||
result: reader.result,
|
||||
});
|
||||
};
|
||||
|
||||
reader.onload = function () {
|
||||
callback({
|
||||
error: false,
|
||||
result: reader.result,
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 将 SVG 字符串渲染到 Canvas
|
||||
* @param {object} options
|
||||
* @param {HTMLCanvasElement} options.canvas
|
||||
* @param {string} options.svgString
|
||||
* @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,
|
||||
drawLeft = 0, drawTop = 0,
|
||||
drawWidth = 0, drawHeight = 0,
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
||||
let svgBlob = new Blob([svgString], {
|
||||
type: 'image/svg+xml;charset=utf-8',
|
||||
});
|
||||
let svgUrl = URL.createObjectURL(svgBlob);
|
||||
let image = new Image();
|
||||
|
||||
image.onerror = () => {
|
||||
console.error('加载 SVG 失败');
|
||||
URL.revokeObjectURL(svgUrl);
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
image.onload = () => {
|
||||
let ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(image, drawLeft, drawTop, drawWidth, drawHeight);
|
||||
URL.revokeObjectURL(svgUrl);
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
image.src = svgUrl;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 解析二维码图片
|
||||
* @param {Blob} image 图片二进制
|
||||
*/
|
||||
export function readQrCodeImage(image) {
|
||||
|
||||
/**
|
||||
* @desc 返回结果
|
||||
* @type {{ error: string; image: string; textList: string[]; }}
|
||||
*/
|
||||
let returns = {
|
||||
error: '',
|
||||
image: '',
|
||||
textList: [],
|
||||
};
|
||||
|
||||
/** 读取图片,转换为 DataURL */
|
||||
let fileReader = new FileReader();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
||||
// 处理读取异常
|
||||
fileReader.onerror = function () {
|
||||
console.error('解析二维码失败:读取图片失败');
|
||||
returns.error = '读取图片失败';
|
||||
resolve('');
|
||||
};
|
||||
|
||||
// 处理读取完成
|
||||
fileReader.onload = function () {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
|
||||
// 开始读取
|
||||
fileReader.readAsDataURL(image);
|
||||
|
||||
}).then((dataURL) => {
|
||||
if (dataURL) {
|
||||
returns.image = dataURL;
|
||||
return readBarcodes(image, readerOptions);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).then((resultList) => {
|
||||
|
||||
let textList = returns.textList;
|
||||
|
||||
if (resultList.length === 0) {
|
||||
console.warn('解析二维码失败:未识别到内容');
|
||||
return returns;
|
||||
} else {
|
||||
console.debug('解析二维码成功:', resultList);
|
||||
}
|
||||
|
||||
for (let i = 0; i < resultList.length; i++) {
|
||||
|
||||
let item = resultList[i];
|
||||
|
||||
textList.push(item.text);
|
||||
|
||||
}
|
||||
|
||||
return returns;
|
||||
|
||||
}).catch((error) => {
|
||||
console.error('解析二维码失败:');
|
||||
console.error(error);
|
||||
returns.error = String(error);
|
||||
return returns;
|
||||
});;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生成二维码图片
|
||||
* @param {object} options
|
||||
* @param {string} options.content
|
||||
* @param {number} options.width
|
||||
* @param {number} options.height
|
||||
* @returns 二维码图片 DataURL
|
||||
*/
|
||||
export function writeQrCodeImage(options = {}) {
|
||||
|
||||
let { content = '', width = 256, height = 256 } = options;
|
||||
|
||||
let canvas = document.createElement('canvas');
|
||||
let ctx = canvas.getContext('2d');
|
||||
|
||||
// 更新画布大小
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// 设置背景颜色
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
return writeBarcode(content, writerOptions).then((result) => {
|
||||
|
||||
console.debug('生成二维码', result);
|
||||
|
||||
if (result.error) {
|
||||
console.error(`生成二维码失败:${result.error}`);
|
||||
return '';
|
||||
} else {
|
||||
return result.svg;
|
||||
}
|
||||
|
||||
}).then((svgString) => {
|
||||
if (svgString) {
|
||||
return renderSvgToCanvas({
|
||||
canvas: canvas,
|
||||
svgString: svgString,
|
||||
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('生成二维码失败:');
|
||||
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,334 @@
|
||||
// 工具箱
|
||||
|
||||
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: '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: '',
|
||||
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-21',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
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-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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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: 'generate-urls',
|
||||
component: 'Generator/GenerateUrls',
|
||||
title: '生成批量下载链接',
|
||||
iconClass: 'mdi mdi-link-variant',
|
||||
desc: '根据设置,生成有一定规律的用于批量下载的链接。',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
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: '2024-10-13',
|
||||
version: '2',
|
||||
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: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
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';
|
||||
|
||||
/**
|
||||
* @description 改变网页标题
|
||||
* @param {string} [value] 新的标题
|
||||
*/
|
||||
changeTitle(value) {
|
||||
document.title = ((value ? `${value} - ` : '') + 'Frost 网址导航');
|
||||
}
|
||||
import {
|
||||
v4 as uuidV4,
|
||||
} from 'uuid';
|
||||
|
||||
/**
|
||||
* @description JSONP
|
||||
* @param {object} options 配置选项
|
||||
*/
|
||||
jsonp(options) {
|
||||
var config = {
|
||||
url: '',
|
||||
cbName: 'cb',
|
||||
cbFunc: function (data) {
|
||||
console.log('[JSONP]', data);
|
||||
},
|
||||
};
|
||||
var el = document.createElement('script');
|
||||
/** 将十六进制颜色值转为灰度值 */
|
||||
export function colorHexToGrayLevel(hex = '') {
|
||||
|
||||
Object.assign(config, options);
|
||||
window[config.cbName] = config.cbFunc;
|
||||
let rgb = colorHexToRgb(hex);
|
||||
|
||||
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);
|
||||
}
|
||||
return Math.round(rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114);
|
||||
|
||||
}
|
||||
|
||||
const utils = new Utils;
|
||||
/** 将十六进制颜色值转为 RGB */
|
||||
export function colorHexToRgb(hex = '') {
|
||||
|
||||
export default utils;
|
||||
// 去除可能存在的 '#' 字符
|
||||
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 更新页面标题
|
||||
* @param {string} title
|
||||
*/
|
||||
export function updateAppTitle(title = '') {
|
||||
document.title = (title ? `${title} - ${appDesc}` : appDesc);
|
||||
}
|
||||
|
||||
|
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 = IS_DEV;
|
||||
|
||||
/** 模块标题 */
|
||||
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 ElementUI from 'element-ui';
|
||||
import { createApp } from 'vue';
|
||||
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 router from './router';
|
||||
import store from './store';
|
||||
|
||||
import 'ress/ress.css';
|
||||
import 'font-awesome/css/font-awesome.min.css';
|
||||
import './assets/css/element.scss';
|
||||
import './assets/css/global.less';
|
||||
// 加载 Day.js Duration 插件
|
||||
dayjs.extend(dayjsDuration);
|
||||
|
||||
Vue.use(ElementUI);
|
||||
// 配置 Day.js 默认语言
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
// 加载 Lunisolar 语言包
|
||||
lunisolar.locale([
|
||||
lunisolarLang1,
|
||||
lunisolarLang2,
|
||||
], false);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app');
|
||||
// 加载 Lunisolar 节日列表
|
||||
lunisolar.Markers.add(lunisolarFestivals, 'festivals');
|
||||
|
||||
// Lunisolar 全局配置
|
||||
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 VueRouter from 'vue-router';
|
||||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
} from 'vue-router';
|
||||
|
||||
import routes from './routes';
|
||||
import utils from '@/assets/js/utils';
|
||||
import store from '@/store/index';
|
||||
import {
|
||||
getToolboxRoutes,
|
||||
} from '@/assets/js/toolbox-data';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
import {
|
||||
updateAppTitle,
|
||||
} from '@/assets/js/utils';
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
});
|
||||
import {
|
||||
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) => {
|
||||
if (to.meta.loadingBar) {
|
||||
store.commit('toggleLoading', true);
|
||||
}
|
||||
next();
|
||||
import AboutView from '@/views/AboutView/AboutView.vue';
|
||||
import IndexView from '@/views/IndexView/IndexView.vue';
|
||||
|
||||
export const router = createRouter({
|
||||
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) => {
|
||||
if (to.meta.loadingBar) {
|
||||
store.commit('toggleLoading', false);
|
||||
}
|
||||
utils.changeTitle(to.meta.title);
|
||||
updateAppTitle(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>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="search-view flex-col">
|
||||
<div class="app-view-header">
|
||||
<span>{{ SEARCH_MODULE_TITLE }}</span>
|
||||
</div>
|
||||
<div class="app-view-content is-transparent">
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar-wrapper">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
class="search-bar-element shadow-1"
|
||||
placeholder="搜索"
|
||||
size="large"
|
||||
:clearable="true"
|
||||
@keydown.enter="openSearchResult"
|
||||
>
|
||||
<template #suffix>
|
||||
<div
|
||||
class="search-btn mdi mdi-magnify"
|
||||
@click="openSearchResult"
|
||||
></div>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
|
||||
<!-- 搜索引擎列表 -->
|
||||
<div class="search-engines-wrapper">
|
||||
<n-radio-group
|
||||
v-model:value="storeSearchView.searchEngineName.value"
|
||||
class="search-engines-list"
|
||||
>
|
||||
<!-- 搜索引擎分类 -->
|
||||
<div
|
||||
v-for="categoryItem in searchEngineList"
|
||||
:key="categoryItem.title"
|
||||
class="search-engine-category"
|
||||
>
|
||||
<div class="category-title">{{ categoryItem.title }}</div>
|
||||
<div class="category-items">
|
||||
<!-- 搜索引擎项 -->
|
||||
<n-radio
|
||||
v-for="engineItem in categoryItem.list"
|
||||
:key="engineItem.name"
|
||||
:value="engineItem.name"
|
||||
class="search-engine-item shadow-1"
|
||||
>
|
||||
<img
|
||||
class="item-icon"
|
||||
draggable="false"
|
||||
:src="engineItem.icon"
|
||||
/>
|
||||
<span class="item-label">{{ engineItem.name }}</span>
|
||||
<span class="item-desc">{{ engineItem.desc }}</span>
|
||||
</n-radio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</n-radio-group>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NInput, NRadio, NRadioGroup,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
storeSearchView,
|
||||
} from '@/assets/js/local-storage';
|
||||
|
||||
import {
|
||||
openSearchResult,
|
||||
searchEngineList,
|
||||
searchKeyword,
|
||||
} from '@/assets/js/search-engine';
|
||||
|
||||
import {
|
||||
SEARCH_MODULE_TITLE,
|
||||
} from '@/config/modules';
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.app-view-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-bar-wrapper,
|
||||
.search-engines-wrapper {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-bar-wrapper {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
margin-top: 64px;
|
||||
max-width: 640px;
|
||||
|
||||
:deep(.search-bar-element) {
|
||||
.n-input-wrapper {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.n-input__border, .n-input__state-border {
|
||||
box-shadow: none;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
transition: color 0.25s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-engines-wrapper {
|
||||
--item-margin: 12px;
|
||||
margin-top: 32px;
|
||||
padding-bottom: 64px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.search-engine-category {
|
||||
&:not(:first-child) {
|
||||
margin-top: var(--item-margin);
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 16px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.category-items {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.search-engine-item {
|
||||
margin: var(--item-margin);
|
||||
width: 240px;
|
||||
height: 40px;
|
||||
border-radius: var(--border-radius);
|
||||
border-left: 4px solid transparent;
|
||||
background-color: #FFF;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
transition: border 0.25s;
|
||||
|
||||
&.n-radio--checked {
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
|
||||
:deep(.n-radio__dot-wrapper) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.n-radio__label) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,179 +0,0 @@
|
||||
<template>
|
||||
<el-container class="settings">
|
||||
<div class="wrapper shadow-2">
|
||||
|
||||
<el-form label-position="left" label-width="12rem">
|
||||
|
||||
<el-form-item label="字体大小" class="set-font">
|
||||
<el-input-number
|
||||
v-model="configFontSize"
|
||||
:min="12"
|
||||
:max="32"
|
||||
controls-position="right"
|
||||
label="字体大小"
|
||||
size="small"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="显示网站标题">
|
||||
<el-switch v-model="configShowSiteTitle"></el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="折叠主页侧边菜单">
|
||||
<el-switch v-model="configSideMenuCollapse"></el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="获取搜索引擎关键词建议">
|
||||
<el-switch v-model="configSearchSuggestion"></el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="清除数据">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="medium"
|
||||
@click="resetDatas()"
|
||||
>清除设置</el-button>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'SettingsView',
|
||||
data() {
|
||||
return { }
|
||||
},
|
||||
computed: {
|
||||
|
||||
...mapState({
|
||||
appConfig: 'config',
|
||||
}),
|
||||
|
||||
configFontSize: {
|
||||
/** @this */
|
||||
get() {
|
||||
return this.appConfig.fontSize;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setConfig', {
|
||||
key: 'fontSize',
|
||||
value,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
configShowSiteTitle: {
|
||||
/** @this */
|
||||
get() {
|
||||
return this.appConfig.showSiteTitle;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setConfig', {
|
||||
key: 'showSiteTitle',
|
||||
value,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
configSideMenuCollapse: {
|
||||
/** @this */
|
||||
get() {
|
||||
return this.appConfig.sideMenuCollapse;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setConfig', {
|
||||
key: 'sideMenuCollapse',
|
||||
value,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
configSearchSuggestion: {
|
||||
/** @this */
|
||||
get() {
|
||||
return this.appConfig.searchSuggestion;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setConfig', {
|
||||
key: 'searchSuggestion',
|
||||
value,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
/** 清除数据 */
|
||||
resetDatas() {
|
||||
this.$confirm('确定要清除吗?', '', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
|
||||
this.$store.commit('resetConfig');
|
||||
|
||||
this.$message({
|
||||
message: '已清除,2s 后自动刷新',
|
||||
type: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
|
||||
}).catch(() => {
|
||||
|
||||
this.$message({
|
||||
message: '取消清除',
|
||||
type: 'info'
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.settings {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.set-font {
|
||||
/deep/ .el-input-number {
|
||||
width: 7rem;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .el-form-item {
|
||||
@media screen and (max-width: 520px) {
|
||||
.el-form-item__label {
|
||||
float: unset !important;
|
||||
}
|
||||
.el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 参数 -->
|
||||
<n-card size="small" title="参数">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
|
||||
<n-form-item label="文件大小:">
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
v-model:value="inputs.sizeValue"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
:step="1"
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="inputs.sizeUnit"
|
||||
label-field="name"
|
||||
value-field="name"
|
||||
:options="units"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="已下载大小:">
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
v-model:value="inputs.downloadedValue"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
:step="1"
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="inputs.downloadedUnit"
|
||||
label-field="name"
|
||||
value-field="name"
|
||||
:options="units"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="下载速度:">
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
v-model:value="inputs.speedValue"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
:step="1"
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="inputs.speedUnit"
|
||||
label-field="speed"
|
||||
value-field="name"
|
||||
:options="units"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 计算结果 -->
|
||||
<n-card size="small" title="计算结果">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
|
||||
<n-form-item label="大约需要时长:">
|
||||
<span>{{ outputs.duration || '未计算' }}</span>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="大约结束时间:">
|
||||
<span>{{ outputs.time || '未计算' }}</span>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NCard, NForm, NFormItem,
|
||||
NInputGroup, NInputNumber, NSelect,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
reactive, ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
$message,
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/** 单位和比率(基于 KB) */
|
||||
const units = [
|
||||
{ name: 'KiB', speed: 'KiB/s', rate: 1 },
|
||||
{ name: 'MiB', speed: 'MiB/s', rate: 1024 },
|
||||
{ name: 'GiB', speed: 'GiB/s', rate: 1048576 },
|
||||
];
|
||||
|
||||
/** 参数 */
|
||||
const inputs = reactive({
|
||||
downloadedUnit: 'KiB',
|
||||
downloadedValue: 0,
|
||||
sizeUnit: 'KiB',
|
||||
sizeValue: 0,
|
||||
speedUnit: 'KiB',
|
||||
speedValue: 0,
|
||||
});
|
||||
|
||||
/** 计算结果 */
|
||||
const outputs = reactive({
|
||||
duration: '',
|
||||
time: '',
|
||||
});
|
||||
|
||||
/** 防抖定时器 */
|
||||
const timer = ref(null);
|
||||
|
||||
/** 计算结果 */
|
||||
function calc() {
|
||||
|
||||
let {
|
||||
downloadedUnit, downloadedValue,
|
||||
sizeUnit, sizeValue,
|
||||
speedUnit, speedValue,
|
||||
} = inputs;
|
||||
|
||||
if (sizeValue === 0 || speedValue === 0) {
|
||||
outputs.duration = '';
|
||||
outputs.time = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取转换比例
|
||||
let downloadedRate = units[units.findIndex((obj) => {
|
||||
return (obj.name === downloadedUnit);
|
||||
})].rate;
|
||||
let sizeRate = units[units.findIndex((obj) => {
|
||||
return (obj.name === sizeUnit);
|
||||
})].rate;
|
||||
let speedRate = units[units.findIndex((obj) => {
|
||||
return (obj.name === speedUnit);
|
||||
})].rate;
|
||||
|
||||
// 转为 KB 单位
|
||||
let realDownloaded = downloadedValue * downloadedRate;
|
||||
let realSize = sizeValue * sizeRate - realDownloaded;
|
||||
let realSpeed = speedValue * speedRate;
|
||||
|
||||
if (realSize < 0) {
|
||||
$message.warning('参数有误,请检查');
|
||||
return;
|
||||
}
|
||||
|
||||
// 时长(秒)
|
||||
let dSeconds = (realSize / realSpeed).toFixed(0);
|
||||
// 起始时间
|
||||
let timeStart = dayjs();
|
||||
// 结束时间
|
||||
let timeEnd = timeStart.add(dSeconds, 'second');
|
||||
// 时长(天,整数)
|
||||
let dDays = timeEnd.diff(timeStart, 'day');
|
||||
// 最后一天的起始时间
|
||||
let timeLastDay = timeStart.add(dDays, 'day');
|
||||
// 时长(格式化,最后一天剩余)
|
||||
let dLastDay = dayjs.duration(timeEnd.diff(timeLastDay)).format('HH 时 mm 分 ss 秒');
|
||||
|
||||
outputs.duration = `${dDays} 天 ${dLastDay}`;
|
||||
outputs.time = timeEnd.format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
}
|
||||
|
||||
// 自动计算
|
||||
watch(inputs, () => {
|
||||
clearTimeout(timer.value);
|
||||
timer.value = setTimeout(() => {
|
||||
calc();
|
||||
}, 1000);
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tool-detail-page {
|
||||
:deep(.n-input-number) {
|
||||
width: 12em;
|
||||
}
|
||||
|
||||
:deep(.n-select) {
|
||||
width: 8em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 计算模式 -->
|
||||
<n-card size="small" title="计算模式">
|
||||
<n-form-item
|
||||
class="form-item-no-feedback"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="mode"
|
||||
:options="[
|
||||
{ label: '1 -> 2', value: '1-to-2' },
|
||||
{ label: '2 -> 1', value: '2-to-1' },
|
||||
]"
|
||||
:on-blur="update"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
|
||||
<!-- 小数位数 -->
|
||||
<n-card size="small" title="小数位数">
|
||||
<n-form-item
|
||||
class="form-item-no-feedback"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-input-number
|
||||
v-model:value="decimals"
|
||||
:min="0"
|
||||
:max="10"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
:on-blur="update"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
|
||||
<!-- 基础比例 -->
|
||||
<n-card size="small" title="基础比例">
|
||||
<n-form-item
|
||||
class="form-item-no-feedback"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-input-number
|
||||
v-model:value="base.a"
|
||||
:min="limit.min"
|
||||
:max="limit.max"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
:on-blur="update"
|
||||
></n-input-number>
|
||||
<span class="split">:</span>
|
||||
<n-input-number
|
||||
v-model:value="base.b"
|
||||
:min="limit.min"
|
||||
:max="limit.max"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
:on-blur="update"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
|
||||
<!-- 计算比例 -->
|
||||
<n-card size="small" title="计算比例">
|
||||
<n-form-item
|
||||
class="form-item-no-feedback"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-input-number
|
||||
v-model:value="calc.a"
|
||||
:disabled="mode === '2-to-1'"
|
||||
:step="1"
|
||||
:on-blur="update"
|
||||
></n-input-number>
|
||||
<span class="split">:</span>
|
||||
<n-input-number
|
||||
v-model:value="calc.b"
|
||||
:disabled="mode === '1-to-2'"
|
||||
:step="1"
|
||||
:on-blur="update"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
bignumber, divide, floor, multiply, number, round,
|
||||
} from 'mathjs';
|
||||
|
||||
import {
|
||||
NCard, NFormItem, NInputNumber, NSelect,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
reactive, ref,
|
||||
} from 'vue';
|
||||
|
||||
/** 基础比例 */
|
||||
const base = reactive({
|
||||
a: 1,
|
||||
b: 1,
|
||||
});
|
||||
|
||||
/** 计算比例 */
|
||||
const calc = reactive({
|
||||
a: 1,
|
||||
b: 1,
|
||||
});
|
||||
|
||||
/** 小数位数 */
|
||||
const decimals = ref(5);
|
||||
|
||||
/** 数值范围限制 */
|
||||
const limit = {
|
||||
min: -99999999,
|
||||
max: 99999999,
|
||||
};
|
||||
|
||||
/** 模式 */
|
||||
const mode = ref('1-to-2');
|
||||
|
||||
/** 计算 */
|
||||
function calculate() {
|
||||
|
||||
let { min, max } = limit;
|
||||
|
||||
let useMode = mode.value;
|
||||
let ratio = divide(bignumber(base.a), bignumber(base.b));
|
||||
|
||||
if (useMode === '1-to-2') {
|
||||
|
||||
// 注:只允许整数
|
||||
let a = number(floor(calc.a));
|
||||
|
||||
(a < min) && (a = min);
|
||||
(a > max) && (a = max);
|
||||
|
||||
calc.a = a;
|
||||
calc.b = number(round(divide(bignumber(a), ratio), decimals.value));
|
||||
|
||||
} else if (useMode === '2-to-1') {
|
||||
|
||||
// 注:只允许整数
|
||||
let b = number(floor(calc.b));
|
||||
|
||||
(b < min) && (b = min);
|
||||
(b > max) && (b = max);
|
||||
|
||||
calc.a = number(round(multiply(bignumber(b), ratio), decimals.value));
|
||||
calc.b = b;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** 更新 */
|
||||
function update() {
|
||||
calculate();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tool-detail-page {
|
||||
:deep(.n-input-number) {
|
||||
width: 12em;
|
||||
}
|
||||
|
||||
:deep(.n-select) {
|
||||
width: 12em;
|
||||
}
|
||||
}
|
||||
|
||||
.split {
|
||||
display: inline-block;
|
||||
width: 2em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="tool-detail-page"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="tool-detail-page"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 控制 -->
|
||||
<n-card size="small" title="控制">
|
||||
<n-form
|
||||
class="form-no-feedback form-data"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="9em"
|
||||
>
|
||||
|
||||
<n-form-item label="转换模式:">
|
||||
<n-select
|
||||
v-model:value="data.convertMode"
|
||||
:options="[
|
||||
{ label: '本地时间 -> 时间戳', value: 'toTimestamp' },
|
||||
{ label: '时间戳 -> 本地时间', value: 'toLocalTime' },
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="时间戳单位:">
|
||||
<n-select
|
||||
v-model:value="data.timestampUnit"
|
||||
:options="[
|
||||
{ label: '毫秒(ms)', value: 'ms' },
|
||||
{ label: '秒(s)', value: 's' },
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="自动更新:">
|
||||
<n-switch v-model:value="data.autoUpdate" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="操作:">
|
||||
<n-flex>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="convertTime"
|
||||
>转换</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="clearInputs"
|
||||
>清空</n-button>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 当前 -->
|
||||
<n-card size="small" title="当前">
|
||||
<n-form
|
||||
class="form-no-feedback form-data"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="9em"
|
||||
>
|
||||
|
||||
<n-form-item label="本地时间:">
|
||||
<n-input
|
||||
v-model:value="data.currentLocalTime"
|
||||
placeholder=""
|
||||
type="text"
|
||||
:readonly="true"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="时间戳(ms):">
|
||||
<n-input
|
||||
v-model:value="data.currentTimestamp"
|
||||
placeholder=""
|
||||
type="text"
|
||||
:readonly="true"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 转换 -->
|
||||
<n-card size="small" title="转换">
|
||||
<n-form
|
||||
class="form-no-feedback form-data"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="9em"
|
||||
>
|
||||
|
||||
<n-form-item label="本地时间:">
|
||||
<n-input
|
||||
v-model:value="data.convertLocalTime"
|
||||
:placeholder="`参考格式:年-月-日 时:分:秒`"
|
||||
:readonly="data.convertMode === 'toLocalTime'"
|
||||
type="text"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="时间戳:">
|
||||
<n-input
|
||||
v-model:value="data.convertTimestamp"
|
||||
:placeholder="timestampPlaceholder"
|
||||
:readonly="data.convertMode === 'toTimestamp'"
|
||||
type="text"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
<n-p>注意:</n-p>
|
||||
<n-p>在“本地时间 -> 时间戳”的模式中,若输入的“本地时间”不包含“时间”部分(例如 2025-02-01),将会加上本地时区与零时区的时差后计算。</n-p>
|
||||
<n-p>即本地时区为北京时间(UTC+8)时按 08:00 计算,本地时区为东京时间(UTC+9)时按 09:00 计算。</n-p>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCard, NFlex,
|
||||
NForm, NFormItem,
|
||||
NInput, NP, NSelect, NSwitch,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
computed, reactive,
|
||||
onBeforeMount, onBeforeUnmount,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
$message,
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
import {
|
||||
getCommonDateTime,
|
||||
} from '@frost-utils/javascript/common/index';
|
||||
|
||||
/** 数据 */
|
||||
const data = reactive({
|
||||
autoUpdate: true,
|
||||
convertLocalTime: '',
|
||||
convertMode: 'toTimestamp',
|
||||
convertTimestamp: '',
|
||||
currentLocalTime: '',
|
||||
currentTimestamp: '',
|
||||
timestampUnit: 'ms',
|
||||
timer: null,
|
||||
});
|
||||
|
||||
/** 时间戳输入占位文本 */
|
||||
const timestampPlaceholder = computed(() => {
|
||||
let suffix = (data.timestampUnit === 'ms' ? '000' : '');
|
||||
return '示例:1577808000' + suffix;
|
||||
});
|
||||
|
||||
/** 清空输入内容 */
|
||||
function clearInputs() {
|
||||
data.convertLocalTime = '';
|
||||
data.convertTimestamp = '';
|
||||
}
|
||||
|
||||
/** 转换输入的时间 */
|
||||
function convertTime() {
|
||||
|
||||
let mode = data.convertMode;
|
||||
let unit = data.timestampUnit;
|
||||
|
||||
if (mode === 'toLocalTime') {
|
||||
|
||||
let ts = parseInt(data.convertTimestamp);
|
||||
|
||||
// 检测输入内容
|
||||
if (isNaN(ts)) {
|
||||
$message.warning('请输入时间戳');
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换时间戳为毫秒
|
||||
if (unit === 's') {
|
||||
ts *= 1000;
|
||||
}
|
||||
|
||||
// 更新结果
|
||||
data.convertLocalTime = getCommonDateTime(ts, 'all');
|
||||
|
||||
} else if (mode === 'toTimestamp') {
|
||||
|
||||
let localTime = data.convertLocalTime;
|
||||
let converted = 0;
|
||||
|
||||
if (localTime) {
|
||||
converted = new Date(localTime).getTime();
|
||||
} else {
|
||||
$message.warning('请输入本地时间');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测输入内容
|
||||
if (isNaN(converted)) {
|
||||
$message.warning('请输入有效的本地时间');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新结果
|
||||
data.convertLocalTime = getCommonDateTime(converted, 'all');
|
||||
|
||||
// 转换时间戳为秒
|
||||
if (unit === 's') {
|
||||
converted = Math.round(converted / 1000);
|
||||
}
|
||||
|
||||
// 更新输入内容
|
||||
data.convertTimestamp = String(converted);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** 处理定时器 */
|
||||
function setTimer(isStart = false) {
|
||||
|
||||
if (data.timer) {
|
||||
clearInterval(data.timer);
|
||||
}
|
||||
|
||||
if (!isStart) {
|
||||
data.timer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
data.timer = setInterval(() => {
|
||||
|
||||
if (!data.autoUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currTime = Math.round(Date.now() / 1000) * 1000;
|
||||
let timeText = getCommonDateTime(currTime, 'all');
|
||||
|
||||
data.currentLocalTime = timeText;
|
||||
data.currentTimestamp = String(currTime);
|
||||
|
||||
}, 1000);
|
||||
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
setTimer(true);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
setTimer(false);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form-data {
|
||||
.n-input {
|
||||
max-width: 256px;
|
||||
}
|
||||
|
||||
.n-select {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 解析二维码 -->
|
||||
<n-card size="small" title="解析二维码">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="left"
|
||||
label-placement="top"
|
||||
label-width="auto"
|
||||
>
|
||||
|
||||
<n-form-item label="选择图片">
|
||||
<n-upload
|
||||
accept="image/jpeg,image/png"
|
||||
:default-upload="false"
|
||||
:file-list="readerData.fileList"
|
||||
:show-file-list="false"
|
||||
@change="handleSelectQrImage"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<span>点击选择图片 / 拖拽图片到此区域</span>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="解析结果">
|
||||
<n-flex
|
||||
class="reader-result"
|
||||
:wrap="true"
|
||||
>
|
||||
<div class="reader-result__image-preview">
|
||||
<n-image
|
||||
v-show="readerData.dataURL"
|
||||
object-fit="contain"
|
||||
width="100%"
|
||||
height="100%"
|
||||
:preview-disabled="true"
|
||||
:src="readerData.dataURL"
|
||||
/>
|
||||
</div>
|
||||
<n-flex
|
||||
class="reader-result__text-list"
|
||||
:vertical="true"
|
||||
>
|
||||
<n-ol v-if="readerData.results.length > 0">
|
||||
<n-li
|
||||
v-for="(value, index) in readerData.results"
|
||||
:key="index"
|
||||
>{{ value }}</n-li>
|
||||
</n-ol>
|
||||
<span v-else>请选择二维码图片以进行解析</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 生成二维码 -->
|
||||
<n-card v-if="false" size="small" title="生成二维码">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="top"
|
||||
label-width="auto"
|
||||
></n-form>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NCard, NFlex, NForm, NFormItem,
|
||||
NImage, NLi, NOl,
|
||||
NUpload, NUploadDragger,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
reactive,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
$message,
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
import {
|
||||
readQrCodeImage,
|
||||
} from '@/assets/js/qr-code';
|
||||
|
||||
/** 二维码解析相关数据 */
|
||||
const readerData = reactive({
|
||||
|
||||
/** 图片 DataURL */
|
||||
dataURL: '',
|
||||
|
||||
/** 选择文件列表 */
|
||||
fileList: [],
|
||||
|
||||
/** 解析结果 */
|
||||
results: [],
|
||||
|
||||
});
|
||||
|
||||
// /** 二维码生成相关数据 */
|
||||
// const writerData = reactive({
|
||||
// });
|
||||
|
||||
/**
|
||||
* @description 处理选择图片
|
||||
* @type { import('naive-ui').UploadOnChange }
|
||||
*/
|
||||
function handleSelectQrImage(options) {
|
||||
|
||||
let file = options.file.file;
|
||||
|
||||
return readQrCodeImage(file).then((result) => {
|
||||
|
||||
let { error, image, textList } = result;
|
||||
|
||||
if (error) {
|
||||
$message.error(error);
|
||||
readerData.dataURL = '';
|
||||
readerData.results = [];
|
||||
} else {
|
||||
if (textList.length === 0) {
|
||||
$message.warning('未识别到有效的二维码');
|
||||
} else {
|
||||
$message.success('识别成功');
|
||||
}
|
||||
readerData.dataURL = image;
|
||||
readerData.results = textList;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.reader-result {
|
||||
width: 100%;
|
||||
|
||||
.reader-result__image-preview {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
.n-image {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.reader-result__text-list {
|
||||
flex-grow: 1;
|
||||
min-width: 256px;
|
||||
width: 0;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="tool-detail-page"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||