Compare commits
73 Commits
V3.0.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 |
58
CHANGELOG.md
58
CHANGELOG.md
@@ -1,5 +1,61 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
## V3.0.0
|
## [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
|
||||||
|
|
||||||
重构新版本,支持“搜索”功能和“网址导航”功能。
|
重构新版本,支持“搜索”功能和“网址导航”功能。
|
||||||
|
@@ -9,6 +9,9 @@
|
|||||||
### 在线
|
### 在线
|
||||||
|
|
||||||
- [GitHub Pages](https://frost-zx.github.io/frost-navigation/)
|
- [GitHub Pages](https://frost-zx.github.io/frost-navigation/)
|
||||||
|
- [V1 版本](https://frost-zx.github.io/frost-navigation/v1/)(2020-09-10)
|
||||||
|
- [V2 版本](https://frost-zx.github.io/frost-navigation/v2/)(2024-08-31)
|
||||||
|
- [V3 版本](https://frost-zx.github.io/frost-navigation/v3/)(开发中)
|
||||||
- [Vercel](https://frost-navigation.vercel.app/)
|
- [Vercel](https://frost-navigation.vercel.app/)
|
||||||
|
|
||||||
### 离线(需要手动更新)
|
### 离线(需要手动更新)
|
||||||
|
39
package.json
39
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frost-navigation",
|
"name": "frost-navigation",
|
||||||
"description": "Frost Navigation",
|
"description": "Frost Navigation",
|
||||||
"version": "3.0.0",
|
"version": "3.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -12,27 +12,30 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@frost-utils/javascript": "^2.1.3",
|
"@frost-utils/javascript": "^2.1.3",
|
||||||
"@mdi/font": "^7.4.47",
|
"@mdi/font": "^7.4.47",
|
||||||
"@vueuse/core": "^11.0.3",
|
"@vueuse/core": "^12.5.0",
|
||||||
"axios": "^1.7.5",
|
"axios": "^1.7.9",
|
||||||
"lunisolar": "^2.5.0",
|
"dayjs": "^1.11.13",
|
||||||
"naive-ui": "^2.39.0",
|
"highlight.js": "^11.11.1",
|
||||||
|
"lunisolar": "^2.5.1",
|
||||||
|
"mathjs": "^14.2.0",
|
||||||
|
"naive-ui": "^2.41.0",
|
||||||
"radash": "^12.1.0",
|
"radash": "^12.1.0",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^11.0.5",
|
||||||
"vue": "^3.4.29",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.3.3",
|
"vue-router": "^4.5.0",
|
||||||
"zxing-wasm": "^1.2.12"
|
"zxing-wasm": "^2.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/node": "^20.14.5",
|
"@types/node": "^20.17.16",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-legacy": "^5.4.2",
|
"@vitejs/plugin-legacy": "^6.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.19.0",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.2",
|
||||||
"vite": "^5.3.1"
|
"vite": "^6.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3476
pnpm-lock.yaml
generated
3476
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
window['NAV_LINK_DATE'] = '2024-09-01';
|
window['NAV_LINK_DATE'] = '2025-02-03';
|
||||||
|
|
||||||
window['NAV_LINK_LIST'] = [
|
window['NAV_LINK_LIST'] = [
|
||||||
{
|
{
|
||||||
@@ -2558,8 +2558,8 @@ window['NAV_LINK_LIST'] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Minecraft Wiki',
|
title: 'Minecraft Wiki',
|
||||||
date: '2021-05-09',
|
date: '2025-02-03',
|
||||||
url: 'https://minecraft.fandom.com/zh/',
|
url: 'https://zh.minecraft.wiki/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Minecraft Wiki(哔哩哔哩)',
|
title: 'Minecraft Wiki(哔哩哔哩)',
|
||||||
@@ -2568,8 +2568,8 @@ window['NAV_LINK_LIST'] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Minecraft 插件百科',
|
title: 'Minecraft 插件百科',
|
||||||
date: '2021-02-08',
|
date: '2025-02-03',
|
||||||
url: 'http://mineplugin.org/',
|
url: 'https://mineplugin.org/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Minecraft 光影百科',
|
title: 'Minecraft 光影百科',
|
||||||
@@ -2802,6 +2802,7 @@ window['NAV_LINK_LIST'] = [
|
|||||||
{
|
{
|
||||||
title: 'Minecraft-HK Community',
|
title: 'Minecraft-HK Community',
|
||||||
date: '2021-02-08',
|
date: '2021-02-08',
|
||||||
|
isInvalid: true,
|
||||||
url: 'http://forum.minecraft-hk.com/',
|
url: 'http://forum.minecraft-hk.com/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2885,10 +2886,15 @@ window['NAV_LINK_LIST'] = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'CSSBattle',
|
title: '其他',
|
||||||
date: '2022-03-20',
|
children: [
|
||||||
desc: 'CSS 代码高尔夫(code-golfing)游戏',
|
{
|
||||||
url: 'https://cssbattle.dev/',
|
title: 'CSSBattle',
|
||||||
|
date: '2022-03-20',
|
||||||
|
desc: 'CSS 代码高尔夫(code-golfing)游戏',
|
||||||
|
url: 'https://cssbattle.dev/',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
2
public/wasm/README.txt
Normal file
2
public/wasm/README.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
zxing_full.wasm
|
||||||
|
zxing-wasm v2.0.1
|
BIN
public/wasm/zxing_full.wasm
Normal file
BIN
public/wasm/zxing_full.wasm
Normal file
Binary file not shown.
121
src/App.vue
121
src/App.vue
@@ -1,25 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-config-provider
|
<n-config-provider
|
||||||
:date-locale="configProviderProps.dateLocale"
|
:date-locale="configProviderProps.dateLocale"
|
||||||
|
:hljs="hljs"
|
||||||
:inline-theme-disabled="configProviderProps.inlineThemeDisabled"
|
:inline-theme-disabled="configProviderProps.inlineThemeDisabled"
|
||||||
:locale="configProviderProps.locale"
|
:locale="configProviderProps.locale"
|
||||||
:style="{
|
|
||||||
'--border-radius': themeCommon.borderRadius,
|
|
||||||
'--border-radius-small': themeCommon.borderRadiusSmall,
|
|
||||||
'--box-shadow-1': themeVars.boxShadow1,
|
|
||||||
'--box-shadow-2': themeVars.boxShadow2,
|
|
||||||
'--box-shadow-3': themeVars.boxShadow3,
|
|
||||||
'--color-action': themeVars.actionColor,
|
|
||||||
'--color-border': themeVars.borderColor,
|
|
||||||
'--color-error': themeCommon.errorColor,
|
|
||||||
'--color-info': themeCommon.infoColor,
|
|
||||||
'--color-primary': themeCommon.primaryColor,
|
|
||||||
'--color-success': themeCommon.successColor,
|
|
||||||
'--color-text-1': themeVars.textColor1,
|
|
||||||
'--color-text-2': themeVars.textColor2,
|
|
||||||
'--color-text-3': themeVars.textColor3,
|
|
||||||
'--color-warning': themeCommon.warningColor,
|
|
||||||
}"
|
|
||||||
:theme-overrides="themeOverrides"
|
:theme-overrides="themeOverrides"
|
||||||
>
|
>
|
||||||
|
|
||||||
@@ -51,14 +35,15 @@ import {
|
|||||||
configProviderProps,
|
configProviderProps,
|
||||||
} from './assets/js/naive-ui';
|
} from './assets/js/naive-ui';
|
||||||
|
|
||||||
|
import hljs from 'highlight.js/lib/core';
|
||||||
|
import hljsJavascript from 'highlight.js/lib/languages/javascript';
|
||||||
|
import hljsJson from 'highlight.js/lib/languages/json';
|
||||||
|
|
||||||
import AppAside from './components/AppAside.vue';
|
import AppAside from './components/AppAside.vue';
|
||||||
|
|
||||||
/** 主题变量配置 */
|
/** 主题变量配置 */
|
||||||
const themeOverrides = configProviderProps.themeOverrides;
|
const themeOverrides = configProviderProps.themeOverrides;
|
||||||
|
|
||||||
/** 主题变量配置 - common */
|
|
||||||
const themeCommon = themeOverrides.common;
|
|
||||||
|
|
||||||
/** 默认主题变量 */
|
/** 默认主题变量 */
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
|
|
||||||
@@ -68,26 +53,94 @@ const themeVars = useThemeVars();
|
|||||||
*/
|
*/
|
||||||
function handleContextMenu(event) {
|
function handleContextMenu(event) {
|
||||||
|
|
||||||
let element = event.target;
|
let elements = event.composedPath();
|
||||||
|
let classValue = '';
|
||||||
|
let classRegExp = /(n-code|n-input|n-input-number|n-ol|n-select)/;
|
||||||
|
|
||||||
// 排除按住 Ctrl 键时
|
// 排除按住 Ctrl 键时
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 排除输入框元素
|
for (let i = 0; i < elements.length; i++) {
|
||||||
if (
|
|
||||||
element instanceof HTMLInputElement &&
|
let element = elements[i];
|
||||||
['password', 'text', 'textarea'].includes(element.type)
|
|
||||||
) {
|
// 获取元素 class 信息
|
||||||
return;
|
if (element instanceof HTMLElement) {
|
||||||
|
classValue = element.classList.value;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除输入框元素
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除指定元素
|
||||||
|
if (classValue && classRegExp.test(classValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 初始化 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(() => {
|
onMounted(() => {
|
||||||
|
initCssVars();
|
||||||
|
initHighlightJs();
|
||||||
window.addEventListener('contextmenu', handleContextMenu);
|
window.addEventListener('contextmenu', handleContextMenu);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,18 +150,6 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
// 全局 CSS 变量
|
|
||||||
:root {
|
|
||||||
// 基础颜色
|
|
||||||
--color-black: rgb(51, 54, 57);
|
|
||||||
--color-gray: #E0E0E0;
|
|
||||||
// 分类颜色
|
|
||||||
--color-bg-dark: rgb(51, 54, 57);
|
|
||||||
--color-bg-light: #F8F8F8;
|
|
||||||
// 滚动条大小
|
|
||||||
--scrollbar-size: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 滚动条
|
// 滚动条
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: var(--scrollbar-size);
|
width: var(--scrollbar-size);
|
||||||
@@ -188,6 +229,7 @@ html {
|
|||||||
.app-view-header {
|
.app-view-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
@@ -203,6 +245,7 @@ html {
|
|||||||
|
|
||||||
.app-view-content {
|
.app-view-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
28
src/assets/js/local-storage.js
Normal file
28
src/assets/js/local-storage.js
Normal file
@@ -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', '必应'),
|
||||||
|
|
||||||
|
};
|
252
src/assets/js/qr-code.js
Normal file
252
src/assets/js/qr-code.js
Normal file
@@ -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 '';
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
@@ -5,12 +5,8 @@ import {
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SKEY_SEARCH_ENGINE_NAME,
|
storeSearchView,
|
||||||
} from '@/config/storage';
|
} from './local-storage';
|
||||||
|
|
||||||
import {
|
|
||||||
useLocalStorage,
|
|
||||||
} from '@vueuse/core';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
$message,
|
$message,
|
||||||
@@ -37,7 +33,7 @@ import icon_zhihu from '@/assets/website-icon/zhihu.svg';
|
|||||||
/** 打开搜索结果页面 */
|
/** 打开搜索结果页面 */
|
||||||
export function openSearchResult() {
|
export function openSearchResult() {
|
||||||
|
|
||||||
let engine = searchEngineName.value;
|
let engine = storeSearchView.searchEngineName.value;
|
||||||
let keyword = searchKeyword.value;
|
let keyword = searchKeyword.value;
|
||||||
let baseURL = '';
|
let baseURL = '';
|
||||||
let useURL = '';
|
let useURL = '';
|
||||||
@@ -221,8 +217,5 @@ export const searchEngineList = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 搜索引擎名称 */
|
|
||||||
export const searchEngineName = useLocalStorage(SKEY_SEARCH_ENGINE_NAME, '必应');
|
|
||||||
|
|
||||||
/** 搜索关键词 */
|
/** 搜索关键词 */
|
||||||
export const searchKeyword = ref('');
|
export const searchKeyword = ref('');
|
||||||
|
112
src/assets/js/svg-arc.js
Normal file
112
src/assets/js/svg-arc.js
Normal file
@@ -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;
|
334
src/assets/js/toolbox-data.js
Normal file
334
src/assets/js/toolbox-data.js
Normal file
@@ -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;
|
||||||
|
|
||||||
|
}
|
@@ -4,6 +4,46 @@ import {
|
|||||||
description as appDesc,
|
description as appDesc,
|
||||||
} from '@package-json';
|
} from '@package-json';
|
||||||
|
|
||||||
|
import {
|
||||||
|
v4 as uuidV4,
|
||||||
|
} from 'uuid';
|
||||||
|
|
||||||
|
/** 将十六进制颜色值转为灰度值 */
|
||||||
|
export function colorHexToGrayLevel(hex = '') {
|
||||||
|
|
||||||
|
let rgb = colorHexToRgb(hex);
|
||||||
|
|
||||||
|
return Math.round(rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将十六进制颜色值转为 RGB */
|
||||||
|
export function colorHexToRgb(hex = '') {
|
||||||
|
|
||||||
|
// 去除可能存在的 '#' 字符
|
||||||
|
hex = hex.replace('#', '');
|
||||||
|
|
||||||
|
// 检查十六进制颜色值的长度,并根据长度决定如何处理
|
||||||
|
if (hex.length === 3) {
|
||||||
|
// 如果是简写形式,如 #FFF,需要将其转换为完整形式 #FFFFFF
|
||||||
|
hex = hex.split('').map(char => char + char).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分别解析出红色、绿色和蓝色的值
|
||||||
|
let r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
let g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
let b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
|
||||||
|
return { r, g, b };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取 V4 UUID */
|
||||||
|
export function getUuidV4(noSplit = false) {
|
||||||
|
let uuid = uuidV4();
|
||||||
|
return noSplit ? uuid.replace(/-/g, '') : uuid;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 更新页面标题
|
* @description 更新页面标题
|
||||||
* @param {string} title
|
* @param {string} title
|
||||||
|
@@ -52,7 +52,7 @@ const router = useRouter();
|
|||||||
|
|
||||||
/** 切换页面 */
|
/** 切换页面 */
|
||||||
function changePage(routeName = '') {
|
function changePage(routeName = '') {
|
||||||
router.replace({ name: routeName });
|
router.push({ name: routeName });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化数据 */
|
/** 初始化数据 */
|
||||||
|
@@ -27,7 +27,7 @@ export const SEARCH_MODULE_ENABLED = true;
|
|||||||
export const SEARCH_MODULE_TITLE = '搜索';
|
export const SEARCH_MODULE_TITLE = '搜索';
|
||||||
|
|
||||||
/** 启用模块 */
|
/** 启用模块 */
|
||||||
export const TOOLBOX_MODULE_ENABLED = IS_DEV;
|
export const TOOLBOX_MODULE_ENABLED = true;
|
||||||
|
|
||||||
/** 模块标题 */
|
/** 模块标题 */
|
||||||
export const TOOLBOX_MODULE_TITLE = '工具箱';
|
export const TOOLBOX_MODULE_TITLE = '工具箱';
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
// 本地储存 key 信息
|
|
||||||
|
|
||||||
/** 储存 key 前缀 */
|
|
||||||
const PREFIX = 'frost-navigation/';
|
|
||||||
|
|
||||||
/** 导航链接侧边栏折叠状态 */
|
|
||||||
export const SKEY_NAV_LINK_ASIDE_COLLAPSED = PREFIX + 'nav-link-aside-collapsed';
|
|
||||||
|
|
||||||
/** 导航链接搜索类型 */
|
|
||||||
export const SKEY_NAV_LINK_SEARCH_TYPE = PREFIX + 'nav-link-search-type';
|
|
||||||
|
|
||||||
/** 当前使用的搜索引擎名称 */
|
|
||||||
export const SKEY_SEARCH_ENGINE_NAME = PREFIX + 'search-engine-name';
|
|
12
src/main.js
12
src/main.js
@@ -4,6 +4,12 @@ import { router } from './router';
|
|||||||
import '@mdi/font/css/materialdesignicons.css';
|
import '@mdi/font/css/materialdesignicons.css';
|
||||||
import '@/assets/fonts/index.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 lunisolar from 'lunisolar';
|
||||||
import lunisolarLang1 from 'lunisolar/locale/en';
|
import lunisolarLang1 from 'lunisolar/locale/en';
|
||||||
import lunisolarLang2 from 'lunisolar/locale/zh-cn';
|
import lunisolarLang2 from 'lunisolar/locale/zh-cn';
|
||||||
@@ -11,6 +17,12 @@ import lunisolarFestivals from 'lunisolar/markers/festivals.zh-cn';
|
|||||||
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
|
||||||
|
// 加载 Day.js Duration 插件
|
||||||
|
dayjs.extend(dayjsDuration);
|
||||||
|
|
||||||
|
// 配置 Day.js 默认语言
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
// 加载 Lunisolar 语言包
|
// 加载 Lunisolar 语言包
|
||||||
lunisolar.locale([
|
lunisolar.locale([
|
||||||
lunisolarLang1,
|
lunisolarLang1,
|
||||||
|
@@ -3,6 +3,10 @@ import {
|
|||||||
createWebHashHistory,
|
createWebHashHistory,
|
||||||
} from 'vue-router';
|
} from 'vue-router';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getToolboxRoutes,
|
||||||
|
} from '@/assets/js/toolbox-data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
updateAppTitle,
|
updateAppTitle,
|
||||||
} from '@/assets/js/utils';
|
} from '@/assets/js/utils';
|
||||||
@@ -62,9 +66,11 @@ export const router = createRouter({
|
|||||||
component: () => import('@/views/ToolboxView/ToolboxView.vue'),
|
component: () => import('@/views/ToolboxView/ToolboxView.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
iconClass: 'mdi mdi-tools',
|
iconClass: 'mdi mdi-tools',
|
||||||
|
isToolDetail: false,
|
||||||
showInAside: TOOLBOX_MODULE_ENABLED,
|
showInAside: TOOLBOX_MODULE_ENABLED,
|
||||||
title: TOOLBOX_MODULE_TITLE,
|
title: TOOLBOX_MODULE_TITLE,
|
||||||
},
|
},
|
||||||
|
children: getToolboxRoutes(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/minecraft-ctrl-view',
|
path: '/minecraft-ctrl-view',
|
||||||
|
@@ -27,18 +27,18 @@
|
|||||||
collapse-mode="width"
|
collapse-mode="width"
|
||||||
show-trigger="arrow-circle"
|
show-trigger="arrow-circle"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
:collapsed="isCollapsed"
|
:collapsed="storeNavView.isAsideCollapsed.value"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
:native-scrollbar="false"
|
:native-scrollbar="false"
|
||||||
:scrollbar-props="{ trigger: 'hover' }"
|
:scrollbar-props="{ trigger: 'hover' }"
|
||||||
:width="240"
|
:width="240"
|
||||||
@collapse="isCollapsed = true"
|
@collapse="storeNavView.isAsideCollapsed.value = true"
|
||||||
@expand="isCollapsed = false"
|
@expand="storeNavView.isAsideCollapsed.value = false"
|
||||||
>
|
>
|
||||||
<n-menu
|
<n-menu
|
||||||
v-model:value="navLinksTitle"
|
v-model:value="navLinksTitle"
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
:collapsed="isCollapsed"
|
:collapsed="storeNavView.isAsideCollapsed.value"
|
||||||
:collapsed-icon-size="24"
|
:collapsed-icon-size="24"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
:icon-size="24"
|
:icon-size="24"
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<div class="right-content-header">
|
<div class="right-content-header">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="searchType"
|
v-model:value="storeNavView.searchType.value"
|
||||||
class="search-type"
|
class="search-type"
|
||||||
:options="searchTypes"
|
:options="searchTypes"
|
||||||
/>
|
/>
|
||||||
@@ -144,9 +144,8 @@ import {
|
|||||||
} from '@/config/modules';
|
} from '@/config/modules';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SKEY_NAV_LINK_ASIDE_COLLAPSED,
|
storeNavView,
|
||||||
SKEY_NAV_LINK_SEARCH_TYPE,
|
} from '@/assets/js/local-storage';
|
||||||
} from '@/config/storage';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
$dialog, $message,
|
$dialog, $message,
|
||||||
@@ -171,12 +170,6 @@ const detailDrawer = reactive({
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 分类列表是否折叠 */
|
|
||||||
const isCollapsed = useLocalStorage(
|
|
||||||
SKEY_NAV_LINK_ASIDE_COLLAPSED,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
/** 完整的链接列表 */
|
/** 完整的链接列表 */
|
||||||
const navLinksAll = formatNavLinks(true);
|
const navLinksAll = formatNavLinks(true);
|
||||||
|
|
||||||
@@ -205,12 +198,6 @@ const navLinksTitle = shallowRef('');
|
|||||||
/** 搜索关键词 */
|
/** 搜索关键词 */
|
||||||
const searchKeyword = shallowRef('');
|
const searchKeyword = shallowRef('');
|
||||||
|
|
||||||
/** 搜索类型 */
|
|
||||||
const searchType = useLocalStorage(
|
|
||||||
SKEY_NAV_LINK_SEARCH_TYPE,
|
|
||||||
'all'
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc 搜索类型列表
|
* @desc 搜索类型列表
|
||||||
* @type { import('naive-ui').SelectOption[] }
|
* @type { import('naive-ui').SelectOption[] }
|
||||||
@@ -228,10 +215,34 @@ const searchTypes = [
|
|||||||
*/
|
*/
|
||||||
function changeList(data = null) {
|
function changeList(data = null) {
|
||||||
|
|
||||||
let useData = data || navLinksAll[0] || null;
|
let useData = null;
|
||||||
|
let storedKey = '';
|
||||||
|
|
||||||
navLinksCurr.value = useData ? useData.children : [];
|
if (data) {
|
||||||
navLinksTitle.value = useData ? useData.title : '';
|
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 = '';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +328,7 @@ function treeDataFilter(pattern = '', node = null) {
|
|||||||
pattern = pattern.toLowerCase();
|
pattern = pattern.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
let type = searchType.value;
|
let type = storeNavView.searchType.value;
|
||||||
|
|
||||||
let desc = String(node.desc).toLowerCase();
|
let desc = String(node.desc).toLowerCase();
|
||||||
let title = String(node.title).toLowerCase();
|
let title = String(node.title).toLowerCase();
|
||||||
|
@@ -27,7 +27,7 @@
|
|||||||
<!-- 搜索引擎列表 -->
|
<!-- 搜索引擎列表 -->
|
||||||
<div class="search-engines-wrapper">
|
<div class="search-engines-wrapper">
|
||||||
<n-radio-group
|
<n-radio-group
|
||||||
v-model:value="searchEngineName"
|
v-model:value="storeSearchView.searchEngineName.value"
|
||||||
class="search-engines-list"
|
class="search-engines-list"
|
||||||
>
|
>
|
||||||
<!-- 搜索引擎分类 -->
|
<!-- 搜索引擎分类 -->
|
||||||
@@ -68,9 +68,14 @@ import {
|
|||||||
NInput, NRadio, NRadioGroup,
|
NInput, NRadio, NRadioGroup,
|
||||||
} from 'naive-ui';
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
storeSearchView,
|
||||||
|
} from '@/assets/js/local-storage';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
openSearchResult,
|
openSearchResult,
|
||||||
searchEngineList, searchEngineName, searchKeyword,
|
searchEngineList,
|
||||||
|
searchKeyword,
|
||||||
} from '@/assets/js/search-engine';
|
} from '@/assets/js/search-engine';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
209
src/views/ToolboxView/Calculation/CalcDownloadTime.vue
Normal file
209
src/views/ToolboxView/Calculation/CalcDownloadTime.vue
Normal file
@@ -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>
|
183
src/views/ToolboxView/Calculation/CalcRatio.vue
Normal file
183
src/views/ToolboxView/Calculation/CalcRatio.vue
Normal file
@@ -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>
|
9
src/views/ToolboxView/Conversion/ConvertHtmlEntities.vue
Normal file
9
src/views/ToolboxView/Conversion/ConvertHtmlEntities.vue
Normal file
@@ -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>
|
267
src/views/ToolboxView/Conversion/ConvertTimestamp.vue
Normal file
267
src/views/ToolboxView/Conversion/ConvertTimestamp.vue
Normal file
@@ -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>
|
165
src/views/ToolboxView/Conversion/QrcodeReaderAndGenerator.vue
Normal file
165
src/views/ToolboxView/Conversion/QrcodeReaderAndGenerator.vue
Normal file
@@ -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>
|
9
src/views/ToolboxView/Conversion/UrlEncodeDecode.vue
Normal file
9
src/views/ToolboxView/Conversion/UrlEncodeDecode.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
9
src/views/ToolboxView/Edit/CsvEditor.vue
Normal file
9
src/views/ToolboxView/Edit/CsvEditor.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
232
src/views/ToolboxView/Edit/JsonFormatter.vue
Normal file
232
src/views/ToolboxView/Edit/JsonFormatter.vue
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page">
|
||||||
|
|
||||||
|
<!-- 设置 -->
|
||||||
|
<n-card size="small" title="设置">
|
||||||
|
<n-flex>
|
||||||
|
<!-- 缩进空格 -->
|
||||||
|
<div class="config-item">
|
||||||
|
<div class="config-item__label">缩进空格:</div>
|
||||||
|
<div class="config-item__content">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="data.indentSize"
|
||||||
|
:min="0"
|
||||||
|
:max="8"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 排序属性 -->
|
||||||
|
<div class="config-item">
|
||||||
|
<div class="config-item__label">排序属性:</div>
|
||||||
|
<div class="config-item__content">
|
||||||
|
<n-switch v-model:value="data.enabledSort" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<n-card size="small" title="操作">
|
||||||
|
<n-flex>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!data.jsonInput"
|
||||||
|
@click="formatJson"
|
||||||
|
>格式化</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!data.jsonOutput"
|
||||||
|
@click="copyOutputs"
|
||||||
|
>复制结果</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
:disabled="!data.jsonInput"
|
||||||
|
@click="clearInputs"
|
||||||
|
>清空输入</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
:disabled="!data.jsonOutput"
|
||||||
|
@click="clearOutputs"
|
||||||
|
>清空输出</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 输入内容 -->
|
||||||
|
<n-card size="small" title="输入内容">
|
||||||
|
<n-input
|
||||||
|
v-model:value="data.jsonInput"
|
||||||
|
class="json-input"
|
||||||
|
placeholder="请输入 JSON 字符串"
|
||||||
|
type="textarea"
|
||||||
|
:rows="8"
|
||||||
|
></n-input>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 输出内容 -->
|
||||||
|
<n-card size="small" title="输出内容">
|
||||||
|
<n-code
|
||||||
|
class="json-output"
|
||||||
|
language="json"
|
||||||
|
:code="data.jsonOutput"
|
||||||
|
:show-line-numbers="true"
|
||||||
|
/>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NButton, NCard, NCode, NFlex,
|
||||||
|
NInput, NInputNumber, NSwitch,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reactive,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
compareString, isArray, isObject,
|
||||||
|
} from '@frost-utils/javascript/common/index';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useClipboard,
|
||||||
|
} from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$message, $notification,
|
||||||
|
} from '@/assets/js/naive-ui';
|
||||||
|
|
||||||
|
/** 剪贴板 */
|
||||||
|
const clipboard = useClipboard({
|
||||||
|
legacy: true,
|
||||||
|
read: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 数据 */
|
||||||
|
const data = reactive({
|
||||||
|
|
||||||
|
/** 启用排序 */
|
||||||
|
enabledSort: true,
|
||||||
|
|
||||||
|
/** 缩进空格 */
|
||||||
|
indentSize: 2,
|
||||||
|
|
||||||
|
/** 输入内容 */
|
||||||
|
jsonInput: '',
|
||||||
|
|
||||||
|
/** 输出内容 */
|
||||||
|
jsonOutput: '',
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 清空输入内容 */
|
||||||
|
function clearInputs() {
|
||||||
|
data.jsonInput = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空输出内容 */
|
||||||
|
function clearOutputs() {
|
||||||
|
data.jsonOutput = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 复制格式化结果 */
|
||||||
|
function copyOutputs() {
|
||||||
|
if (clipboard.isSupported) {
|
||||||
|
return clipboard.copy(data.jsonOutput).then(() => {
|
||||||
|
$message.success('复制成功');
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('复制失败:');
|
||||||
|
console.error(error);
|
||||||
|
$message.error('复制失败:异常');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$message.error('复制失败:当前浏览器不支持该操作');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化输入的 JSON */
|
||||||
|
function formatJson() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
let obj = JSON.parse(data.jsonInput);
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
if (data.enabledSort) {
|
||||||
|
obj = sortObjectKeys(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.jsonOutput = JSON.stringify(obj, null, data.indentSize);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('格式化 JSON 失败:');
|
||||||
|
console.warn(error);
|
||||||
|
$notification.create({
|
||||||
|
content: String(error),
|
||||||
|
duration: 0,
|
||||||
|
title: '格式化 JSON 失败',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
data.jsonOutput = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 排序对象的 key */
|
||||||
|
function sortObjectKeys(obj) {
|
||||||
|
|
||||||
|
// 非对象直接返回
|
||||||
|
if (!isArray(obj) && !isObject(obj)) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数组,递归处理每个元素
|
||||||
|
if (isArray(obj)) {
|
||||||
|
return obj.map((item) => sortObjectKeys(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取并排序 key
|
||||||
|
let sortedKeys = Object.keys(obj).sort((a, b) => {
|
||||||
|
return compareString(a, b);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 排序后的对象
|
||||||
|
let sortedObj = {};
|
||||||
|
|
||||||
|
// 按顺序获取值 & 递归处理
|
||||||
|
for (let key of sortedKeys) {
|
||||||
|
sortedObj[key] = sortObjectKeys(obj[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedObj;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.config-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.n-input-number {
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-input {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-output {
|
||||||
|
user-select: text;
|
||||||
|
|
||||||
|
:deep(.__code__) {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
246
src/views/ToolboxView/Generator/GenerateRandomString.vue
Normal file
246
src/views/ToolboxView/Generator/GenerateRandomString.vue
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<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-number
|
||||||
|
v-model:value="info.strLength"
|
||||||
|
:min="1"
|
||||||
|
:max="1024"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="字符串格式:">
|
||||||
|
<n-flex>
|
||||||
|
<n-checkbox
|
||||||
|
v-model:checked="info.option.hasNum"
|
||||||
|
>数字</n-checkbox>
|
||||||
|
<n-checkbox
|
||||||
|
v-model:checked="info.option.hasChar"
|
||||||
|
>字母</n-checkbox>
|
||||||
|
<n-checkbox
|
||||||
|
v-model:checked="info.option.hasSymbol"
|
||||||
|
>其他符号</n-checkbox>
|
||||||
|
<n-checkbox
|
||||||
|
v-model:checked="info.option.caseSensitive"
|
||||||
|
>大小写</n-checkbox>
|
||||||
|
<n-checkbox
|
||||||
|
v-model:checked="info.option.lowerCase"
|
||||||
|
>全小写(需关闭“大小写”)</n-checkbox>
|
||||||
|
</n-flex>
|
||||||
|
</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>
|
||||||
|
<n-input
|
||||||
|
v-model:value="info.result"
|
||||||
|
placeholder=""
|
||||||
|
type="textarea"
|
||||||
|
:readonly="true"
|
||||||
|
:rows="3"
|
||||||
|
></n-input>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<n-card size="small" title="操作">
|
||||||
|
<n-flex>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleGenerate"
|
||||||
|
>生成字符串</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!info.result"
|
||||||
|
@click="handleCopy"
|
||||||
|
>复制结果</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 算法参考 -->
|
||||||
|
<n-card size="small" title="算法参考">
|
||||||
|
<n-ul>
|
||||||
|
<n-li>
|
||||||
|
<n-a
|
||||||
|
href="https://www.cnblogs.com/hankuksui/p/9892729.html"
|
||||||
|
target="_blank"
|
||||||
|
>博客园 - hankuksui</n-a>
|
||||||
|
</n-li>
|
||||||
|
</n-ul>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NA, NButton, NCard, NCheckbox,
|
||||||
|
NFlex, NForm, NFormItem,
|
||||||
|
NInput, NInputNumber, NLi, NUl,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reactive,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$message,
|
||||||
|
} from '@/assets/js/naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useClipboard,
|
||||||
|
} from '@vueuse/core';
|
||||||
|
|
||||||
|
const clipboard = useClipboard({
|
||||||
|
legacy: true,
|
||||||
|
read: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const info = reactive({
|
||||||
|
strLength: 8,
|
||||||
|
option: {
|
||||||
|
caseSensitive: true,
|
||||||
|
hasNum: true,
|
||||||
|
hasChar: true,
|
||||||
|
hasSymbol: false,
|
||||||
|
lowerCase: false
|
||||||
|
},
|
||||||
|
result: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 生成随机字符串
|
||||||
|
* @param {number} strLength 长度
|
||||||
|
* @param {boolean} hasNum 是否包含数字
|
||||||
|
* @param {boolean} hasChar 是否包含字母
|
||||||
|
* @param {boolean} hasSymbol 是否包含其他符号
|
||||||
|
* @param {boolean} caseSensitive 是否包含大小写
|
||||||
|
* @param {boolean} lowerCase 是否全小写
|
||||||
|
* - 当 caseSensitive 为 false 时起作用
|
||||||
|
* @returns {string} 生成的字符串
|
||||||
|
*/
|
||||||
|
function genRandomStr(strLength, hasNum, hasChar, hasSymbol, caseSensitive, lowerCase) {
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasNum === false &&
|
||||||
|
hasChar === false &&
|
||||||
|
hasSymbol === false
|
||||||
|
) {
|
||||||
|
return '请选中数字、字母或其他符号的其中一项!';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = strLength; i > 0; i--) {
|
||||||
|
|
||||||
|
let num = Math.floor((Math.random() * 94) + 33);
|
||||||
|
let flag = ((
|
||||||
|
(hasNum === false) && ((num >= 48) && (num <= 57))
|
||||||
|
) || (
|
||||||
|
(hasChar === false) && ((
|
||||||
|
(num >= 65) && (num <= 90)
|
||||||
|
) || (
|
||||||
|
(num >= 97) && (num <= 122)
|
||||||
|
))
|
||||||
|
) || (
|
||||||
|
(hasSymbol === false) && ((
|
||||||
|
(num >= 33) && (num <= 47)
|
||||||
|
) || (
|
||||||
|
(num >= 58) && (num <= 64)
|
||||||
|
) || (
|
||||||
|
(num >= 91) && (num <= 96)
|
||||||
|
) || (
|
||||||
|
(num >= 123) && (num <= 127)
|
||||||
|
)
|
||||||
|
)));
|
||||||
|
|
||||||
|
if (flag) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* | CharCode | 符号 |
|
||||||
|
* | :--------- | :----- |
|
||||||
|
* | 033 -> 047 | ! -> / |
|
||||||
|
* | 048 -> 057 | 0 -> 9 |
|
||||||
|
* | 058 -> 064 | : -> @ |
|
||||||
|
* | 065 -> 090 | A -> Z |
|
||||||
|
* | 091 -> 096 | [ -> ` |
|
||||||
|
* | 097 -> 122 | a -> z |
|
||||||
|
* | 123 -> 127 | { -> |
|
||||||
|
*/
|
||||||
|
|
||||||
|
result += String.fromCharCode(num);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caseSensitive === false) {
|
||||||
|
result = (lowerCase ? result.toLocaleLowerCase() : result.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理复制操作 */
|
||||||
|
function handleCopy() {
|
||||||
|
if (clipboard.isSupported) {
|
||||||
|
return clipboard.copy(info.result).then(() => {
|
||||||
|
$message.success('复制成功');
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('复制失败:');
|
||||||
|
console.error(error);
|
||||||
|
$message.error('复制失败:异常');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$message.error('复制失败:当前浏览器不支持该操作');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理生成操作 */
|
||||||
|
function handleGenerate() {
|
||||||
|
|
||||||
|
let length = info.strLength;
|
||||||
|
let opt = info.option;
|
||||||
|
let result = genRandomStr(length, opt.hasNum, opt.hasChar, opt.hasSymbol, opt.caseSensitive, opt.lowerCase);
|
||||||
|
|
||||||
|
info.result = result;
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.tool-detail-page {
|
||||||
|
:deep(.n-input-number) {
|
||||||
|
width: 8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input--textarea) {
|
||||||
|
max-width: 48em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
9
src/views/ToolboxView/Generator/GenerateUrls.vue
Normal file
9
src/views/ToolboxView/Generator/GenerateUrls.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
9
src/views/ToolboxView/Minecraft/CalcChunkLocation.vue
Normal file
9
src/views/ToolboxView/Minecraft/CalcChunkLocation.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
@@ -0,0 +1,556 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page">
|
||||||
|
|
||||||
|
<!-- 说明 -->
|
||||||
|
<n-card size="small" title="说明">
|
||||||
|
<n-p>已测试游戏版本:1.12.2 ~ 1.21.4</n-p>
|
||||||
|
<n-p>若内容出现乱码,请尝试更改“文件编码”后重新打开文件。</n-p>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 设置 -->
|
||||||
|
<n-card size="small" title="设置">
|
||||||
|
<n-flex>
|
||||||
|
<!-- 文件编码 -->
|
||||||
|
<div class="config-item">
|
||||||
|
<div class="config-item__label">文件编码:</div>
|
||||||
|
<div class="config-item__content">
|
||||||
|
<n-select
|
||||||
|
v-model:value="currState.textEncoding"
|
||||||
|
:options="[
|
||||||
|
{ label: 'GBK', value: 'gbk' },
|
||||||
|
{ label: 'UTF-8', value: 'utf-8' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 读取间隔 -->
|
||||||
|
<div class="config-item">
|
||||||
|
<div class="config-item__label">读取间隔:</div>
|
||||||
|
<div class="config-item__content">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="currState.readInterval"
|
||||||
|
:disabled="currState.isReadingFile"
|
||||||
|
:min="1"
|
||||||
|
:max="60"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
<span
|
||||||
|
style="margin-left: 0.5em;"
|
||||||
|
>秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<n-card size="small" title="操作">
|
||||||
|
<n-flex class="action-row">
|
||||||
|
<n-button
|
||||||
|
type="success"
|
||||||
|
@click="selectLogFile"
|
||||||
|
>选择文件</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!currState.isOpenedFile"
|
||||||
|
@click="parseLogFileData(true)"
|
||||||
|
>刷新内容</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!currState.isOpenedFile || currState.isReadingFile"
|
||||||
|
@click="setAutoReading(true)"
|
||||||
|
>开始读取</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
:disabled="!currState.isOpenedFile || !currState.isReadingFile"
|
||||||
|
@click="setAutoReading(false)"
|
||||||
|
>停止读取</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
@click="clearHistory"
|
||||||
|
>清空内容</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 信息 -->
|
||||||
|
<n-card size="small" title="信息">
|
||||||
|
<n-flex>
|
||||||
|
<n-tag type="info">文件名称:{{ fsFileName || '-' }}</n-tag>
|
||||||
|
<n-tag type="info">文件大小:{{ fileSizeDisplay }}</n-tag>
|
||||||
|
<n-tag type="info">修改时间:{{ fileLastModifiedDisplay }}</n-tag>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<n-card
|
||||||
|
size="small"
|
||||||
|
title="内容"
|
||||||
|
:class="{
|
||||||
|
'chat-history': true,
|
||||||
|
'chat-history--is-full': currState.isFullView,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #header-extra>
|
||||||
|
<div
|
||||||
|
class="chat-history__toggle-full"
|
||||||
|
@click="currState.isFullView = !currState.isFullView"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="currState.isFullView"
|
||||||
|
class="mdi mdi-arrow-collapse"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="mdi mdi-arrow-expand"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="chat-history__wrapper" @contextmenu.stop>
|
||||||
|
<div ref="chatHistoryListRef" class="chat-history__list">
|
||||||
|
<div
|
||||||
|
v-for="item in currState.textRows"
|
||||||
|
:key="item.id"
|
||||||
|
class="chat-history__item"
|
||||||
|
>
|
||||||
|
<n-tag
|
||||||
|
class="chat-history__time"
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
>{{ item.time }}</n-tag>
|
||||||
|
<div
|
||||||
|
class="chat-history__text"
|
||||||
|
>{{ item.text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NButton, NCard, NFlex,
|
||||||
|
NInputNumber, NP, NSelect, NTag,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed, reactive, ref,
|
||||||
|
nextTick, onBeforeMount, onBeforeUnmount,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCommonDateTime,
|
||||||
|
} from '@frost-utils/javascript/common/index';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useFileSystemAccess,
|
||||||
|
} from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$message,
|
||||||
|
} from '@/assets/js/naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getUuidV4,
|
||||||
|
} from '@/assets/js/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef TextRowItem
|
||||||
|
* @property {string} id UUID
|
||||||
|
* @property {TextRowType} type 文本类型
|
||||||
|
* @property {string} time 时间信息
|
||||||
|
* @property {string} text 文本内容
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @typedef {''|'chat'} TextRowType */
|
||||||
|
|
||||||
|
/** 正则表达式列表 */
|
||||||
|
const REG_EXP = {
|
||||||
|
LOG_CHAT_MSG_1: /\[CHAT\]\s+(.*)$/,
|
||||||
|
LOG_TIME: /\[(\d{2}:\d{2}:\d{2})\]/,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: fsData,
|
||||||
|
fileLastModified: fsFileLastModified,
|
||||||
|
fileName: fsFileName,
|
||||||
|
fileSize: fsFileSize,
|
||||||
|
isSupported: fsIsSupported,
|
||||||
|
open: fsOpen,
|
||||||
|
updateData: fsUpdateData,
|
||||||
|
} = useFileSystemAccess({
|
||||||
|
dataType: 'ArrayBuffer',
|
||||||
|
excludeAcceptAllOption: true,
|
||||||
|
types: [{
|
||||||
|
accept: { 'text/plain': ['.log'] },
|
||||||
|
description: 'Minecraft 日志文件',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 聊天内容列表 ref
|
||||||
|
* @type {VueRef<HTMLElement>}
|
||||||
|
*/
|
||||||
|
const chatHistoryListRef = ref(null);
|
||||||
|
|
||||||
|
/** 状态信息 */
|
||||||
|
const currState = reactive({
|
||||||
|
|
||||||
|
/** 内容列表是否满屏显示 */
|
||||||
|
isFullView: false,
|
||||||
|
|
||||||
|
/** 是否正在持续读取文件 */
|
||||||
|
isReadingFile: false,
|
||||||
|
|
||||||
|
/** 是否已选择文件 */
|
||||||
|
isOpenedFile: false,
|
||||||
|
|
||||||
|
/** 最后一次读取时的文件大小 */
|
||||||
|
lastFileSize: 0,
|
||||||
|
|
||||||
|
/** 最后一次读取时的文件更新时间 */
|
||||||
|
LastModifiedTime: 0,
|
||||||
|
|
||||||
|
/** 最后一次读取时的行数 */
|
||||||
|
lastReadLineNumber: 0,
|
||||||
|
|
||||||
|
/** 读取间隔秒数 */
|
||||||
|
readInterval: 2,
|
||||||
|
|
||||||
|
/** 自动读取定时器 */
|
||||||
|
readTimer: null,
|
||||||
|
|
||||||
|
/** 文本内容编码类型 */
|
||||||
|
textEncoding: 'utf-8',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 显示的文本行列表
|
||||||
|
* @type {TextRowItem[]}
|
||||||
|
*/
|
||||||
|
textRows: [],
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 显示的文件大小 */
|
||||||
|
const fileSizeDisplay = computed(() => {
|
||||||
|
let value = fsFileSize.value;
|
||||||
|
return (value ? `${(value / 1024).toFixed(2)} KB` : '-');
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 显示的文件修改时间 */
|
||||||
|
const fileLastModifiedDisplay = computed(() => {
|
||||||
|
let value = fsFileLastModified.value;
|
||||||
|
return (value ? getCommonDateTime(value, 'all') : '-');
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 自动滚动聊天内容列表到底部 */
|
||||||
|
function autoScrollChatHistory() {
|
||||||
|
|
||||||
|
let element = chatHistoryListRef.value;
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
console.error('自动滚动失败:元素不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let elRect = element.getBoundingClientRect();
|
||||||
|
let elHeight = Math.round(elRect.height);
|
||||||
|
|
||||||
|
let scrollHeight0 = element.scrollHeight;
|
||||||
|
let scrollHeight1 = 0;
|
||||||
|
let scrollTop = element.scrollTop;
|
||||||
|
|
||||||
|
// 检测当前是否位于底部
|
||||||
|
if (scrollHeight0 - scrollTop === elHeight) {
|
||||||
|
// 渲染新的内容后滚动到底部
|
||||||
|
nextTick(() => {
|
||||||
|
|
||||||
|
scrollHeight1 = element.scrollHeight;
|
||||||
|
|
||||||
|
if (scrollHeight1 > elHeight) {
|
||||||
|
element.scrollTo({
|
||||||
|
behavior: 'instant',
|
||||||
|
top: scrollHeight1 - elHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空记录内容 */
|
||||||
|
function clearHistory() {
|
||||||
|
currState.textRows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 解码字符串
|
||||||
|
* @param {ArrayBuffer} data
|
||||||
|
*/
|
||||||
|
function decodeFileData(data = null) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
let decoder = new TextDecoder(currState.textEncoding);
|
||||||
|
let text = decoder.decode(data);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解码失败:');
|
||||||
|
console.error(error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化数据 */
|
||||||
|
function initData(isFirst = false) {
|
||||||
|
currState.lastFileSize = 0;
|
||||||
|
currState.LastModifiedTime = 0;
|
||||||
|
currState.lastReadLineNumber = 0;
|
||||||
|
currState.textRows = isFirst ? [{
|
||||||
|
id: getUuidV4(),
|
||||||
|
type: 'chat',
|
||||||
|
time: getCommonDateTime(null, 'time'),
|
||||||
|
text: '请点击按钮选择日志文件以开始。',
|
||||||
|
}] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析日志文件内容 */
|
||||||
|
async function parseLogFileData(manualUpdate = false) {
|
||||||
|
|
||||||
|
if (manualUpdate) {
|
||||||
|
await fsUpdateData();
|
||||||
|
}
|
||||||
|
|
||||||
|
let currFileSize = fsFileSize.value;
|
||||||
|
let currFileTime = fsFileLastModified.value;
|
||||||
|
let currFileData = fsData.value;
|
||||||
|
let currFileText = '';
|
||||||
|
let readLineNumber = currState.lastReadLineNumber;
|
||||||
|
let readLineStrs = [];
|
||||||
|
let parsedData = [];
|
||||||
|
|
||||||
|
// 若文件内容没有变化,不处理
|
||||||
|
if (currFileTime === currState.LastModifiedTime) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
currState.LastModifiedTime = currFileTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码字符串
|
||||||
|
if (currFileData instanceof ArrayBuffer) {
|
||||||
|
currFileText = decodeFileData(currFileData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currFileText) {
|
||||||
|
// 以 \n 拆分,移除末尾 \r
|
||||||
|
readLineStrs = currFileText.split('\n').map((text) => {
|
||||||
|
return text.replace(/\r$/, '');
|
||||||
|
}).filter((text) => {
|
||||||
|
return Boolean(text);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('文件内容为空或解码失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若已重新创建日志文件,则从头开始读取
|
||||||
|
if (currFileSize < currState.lastFileSize) {
|
||||||
|
readLineNumber = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过读取过的行
|
||||||
|
readLineStrs.splice(0, readLineNumber);
|
||||||
|
readLineNumber += readLineStrs.length;
|
||||||
|
|
||||||
|
readLineStrs.forEach((text) => {
|
||||||
|
|
||||||
|
let parsed = parseLogLine(text);
|
||||||
|
|
||||||
|
if (parsed.type === 'chat') {
|
||||||
|
parsedData.push(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
currState.lastFileSize = currFileSize;
|
||||||
|
currState.lastReadLineNumber = readLineNumber;
|
||||||
|
currState.textRows.push.apply(currState.textRows, parsedData);
|
||||||
|
|
||||||
|
autoScrollChatHistory();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 解析日志文件行内容
|
||||||
|
* @returns {TextRowItem}
|
||||||
|
*/
|
||||||
|
function parseLogLine(text = '') {
|
||||||
|
try {
|
||||||
|
|
||||||
|
let msgTextMatched = text.match(REG_EXP.LOG_CHAT_MSG_1);
|
||||||
|
let msgTextStr = msgTextMatched ? msgTextMatched[1] : '';
|
||||||
|
|
||||||
|
let timeTextMatched = text.match(REG_EXP.LOG_TIME);
|
||||||
|
let timeTextStr = timeTextMatched ? timeTextMatched[1] : '';
|
||||||
|
|
||||||
|
if (msgTextStr) {
|
||||||
|
// 处理换行和 §
|
||||||
|
msgTextStr = msgTextStr.replace(/\\n/g, '\n').replace(/§\w/g, '');
|
||||||
|
return {
|
||||||
|
id: getUuidV4(),
|
||||||
|
type: 'chat',
|
||||||
|
time: timeTextStr,
|
||||||
|
text: msgTextStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析内容失败:');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
type: '',
|
||||||
|
time: '',
|
||||||
|
text: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择日志文件 */
|
||||||
|
async function selectLogFile() {
|
||||||
|
|
||||||
|
if (!fsIsSupported.value) {
|
||||||
|
$message.error('当前浏览器不支持该功能');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fsOpen();
|
||||||
|
$message.success('打开文件成功');
|
||||||
|
currState.isOpenedFile = true;
|
||||||
|
initData(false);
|
||||||
|
parseLogFileData(false);
|
||||||
|
setAutoReading(true);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开文件失败:');
|
||||||
|
console.error(error);
|
||||||
|
$message.error('打开文件失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开始或停止自动读取 */
|
||||||
|
function setAutoReading(isStart = false) {
|
||||||
|
|
||||||
|
if (currState.readTimer) {
|
||||||
|
clearInterval(currState.readTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isStart) {
|
||||||
|
currState.isReadingFile = false;
|
||||||
|
currState.readTimer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currState.isReadingFile = true;
|
||||||
|
currState.readTimer = setInterval(() => {
|
||||||
|
parseLogFileData(true);
|
||||||
|
}, currState.readInterval * 1000);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
initData(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
setAutoReading(false);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.config-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.config-item__content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-input-number, .n-select {
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history {
|
||||||
|
position: relative;
|
||||||
|
background-color: #FFF;
|
||||||
|
|
||||||
|
&.chat-history--is-full {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.chat-history__wrapper,
|
||||||
|
.chat-history__list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history__toggle-full {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history__wrapper {
|
||||||
|
--line-margin: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #F0F0F0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: calc(1em + var(--line-margin));
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history__list {
|
||||||
|
height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history__item {
|
||||||
|
display: flex;
|
||||||
|
margin: var(--line-margin) 0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history__time {
|
||||||
|
margin: auto 0;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history__text {
|
||||||
|
margin: auto 0;
|
||||||
|
user-select: text;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
9
src/views/ToolboxView/Minecraft/UuidConverter.vue
Normal file
9
src/views/ToolboxView/Minecraft/UuidConverter.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
553
src/views/ToolboxView/Network/WebSocketTestTool.vue
Normal file
553
src/views/ToolboxView/Network/WebSocketTestTool.vue
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page">
|
||||||
|
|
||||||
|
<!-- 注意 -->
|
||||||
|
<n-card size="small" title="注意">
|
||||||
|
<n-p>由于浏览器限制,通过 HTTPS 访问网站时只能连接带 SSL 的 WebSocket(WSS)。</n-p>
|
||||||
|
<n-p>若需要连接不带 SSL 的 WebSocket(WS),建议下载到本地后使用。</n-p>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 输入 -->
|
||||||
|
<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-input-group class="address-input">
|
||||||
|
<n-select
|
||||||
|
v-model:value="data.address.prefix"
|
||||||
|
:options="[
|
||||||
|
{ label: 'ws://', value: 'ws://' },
|
||||||
|
{ label: 'wss://', value: 'wss://' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<n-input
|
||||||
|
v-model:value="data.address.suffix"
|
||||||
|
type="text"
|
||||||
|
></n-input>
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="发送内容(自动移除换行符)">
|
||||||
|
<n-input
|
||||||
|
v-model:value="data.inputs"
|
||||||
|
placeholder="在此处输入要发送的内容"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
></n-input>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<n-card size="small" title="操作">
|
||||||
|
<n-flex>
|
||||||
|
<n-button
|
||||||
|
type="success"
|
||||||
|
:disabled="data.ws !== null"
|
||||||
|
@click="wsConnect"
|
||||||
|
>连接</n-button>
|
||||||
|
<n-button
|
||||||
|
type="warning"
|
||||||
|
:disabled="data.ws === null"
|
||||||
|
@click="wsClose"
|
||||||
|
>断开</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="data.ws === null"
|
||||||
|
@click="wsSend"
|
||||||
|
>发送</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
@click="clearInputs"
|
||||||
|
>清空输入</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
@click="clearMessages"
|
||||||
|
>清空消息</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 日志 -->
|
||||||
|
<n-card size="small" title="日志">
|
||||||
|
<div
|
||||||
|
ref="logsContentRef"
|
||||||
|
class="logs-content"
|
||||||
|
:style="{ height: (data.logsHeight + 'px') }"
|
||||||
|
@contextmenu.stop
|
||||||
|
>
|
||||||
|
<div class="message-list">
|
||||||
|
<div
|
||||||
|
v-for="item in data.messages"
|
||||||
|
:key="item.id"
|
||||||
|
class="message-item"
|
||||||
|
>
|
||||||
|
<!-- 时间 -->
|
||||||
|
<n-tag
|
||||||
|
:type="item.type === 'send' ? 'primary' : 'success'"
|
||||||
|
size="small"
|
||||||
|
>{{ getCommonDateTime(item.time) }}</n-tag>
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<div
|
||||||
|
v-if="data.parseType === 'html'"
|
||||||
|
v-html="item.message"
|
||||||
|
class="message-content"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="message-content"
|
||||||
|
>{{ item.message }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 设置 -->
|
||||||
|
<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-input-number
|
||||||
|
v-model:value="data.logsHeight"
|
||||||
|
:min="80"
|
||||||
|
:max="800"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 日志最大行数 -->
|
||||||
|
<n-form-item label="日志最大行数">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="data.logsMax"
|
||||||
|
:min="1"
|
||||||
|
:max="8192"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 解析类型 -->
|
||||||
|
<n-form-item label="解析类型">
|
||||||
|
<n-radio-group v-model:value="data.parseType">
|
||||||
|
<n-radio-button
|
||||||
|
v-for="item in data.parseTypes"
|
||||||
|
:key="item.name"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.name"
|
||||||
|
/>
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 自动滚动 -->
|
||||||
|
<n-form-item label="自动滚动">
|
||||||
|
<n-radio-group v-model:value="data.autoScroll">
|
||||||
|
<n-radio-button label="开启" :value="true" />
|
||||||
|
<n-radio-button label="关闭" :value="false" />
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NButton, NCard,
|
||||||
|
NFlex, NForm, NFormItem,
|
||||||
|
NInput, NInputGroup, NInputNumber,
|
||||||
|
NP, NRadioButton, NRadioGroup, NSelect, NTag,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reactive, ref, nextTick, onMounted, onBeforeUnmount,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCommonDateTime,
|
||||||
|
} from '@frost-utils/javascript/common/index';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$dialog, $notification,
|
||||||
|
} from '@/assets/js/naive-ui';
|
||||||
|
|
||||||
|
/** 数据 */
|
||||||
|
const data = reactive({
|
||||||
|
|
||||||
|
/** 连接地址 */
|
||||||
|
address: {
|
||||||
|
prefix: 'ws://',
|
||||||
|
suffix: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 自动滚动结果 */
|
||||||
|
autoScroll: true,
|
||||||
|
|
||||||
|
/** 发送内容 */
|
||||||
|
inputs: '',
|
||||||
|
|
||||||
|
/** 日志高度 */
|
||||||
|
logsHeight: 320,
|
||||||
|
|
||||||
|
/** 日志最大行数 */
|
||||||
|
logsMax: 100,
|
||||||
|
|
||||||
|
/** 接收内容 */
|
||||||
|
messages: [],
|
||||||
|
|
||||||
|
/** 消息 ID */
|
||||||
|
messageID: 0,
|
||||||
|
|
||||||
|
/** 消息解析类型 */
|
||||||
|
parseType: 'string',
|
||||||
|
|
||||||
|
/** 消息解析类型列表 */
|
||||||
|
parseTypes: [
|
||||||
|
{ name: 'html', label: 'HTML' },
|
||||||
|
{ name: 'json', label: 'JSON' },
|
||||||
|
{ name: 'string', label: '字符串' },
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc WebSocket 对象
|
||||||
|
* @type {WebSocket}
|
||||||
|
*/
|
||||||
|
ws: null,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 日志内容 ref
|
||||||
|
* @type {VueRef<HTMLElement>}
|
||||||
|
*/
|
||||||
|
const logsContentRef = ref(null);
|
||||||
|
|
||||||
|
/** 清空输入 */
|
||||||
|
function clearInputs() {
|
||||||
|
$dialog.create({
|
||||||
|
content: '确定要清空输入的内容吗?',
|
||||||
|
negativeText: '取消',
|
||||||
|
positiveText: '确定',
|
||||||
|
title: '确认',
|
||||||
|
type: 'default',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
data.inputs = '';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空消息 */
|
||||||
|
function clearMessages() {
|
||||||
|
$dialog.create({
|
||||||
|
content: '确定要清空消息内容吗?',
|
||||||
|
negativeText: '取消',
|
||||||
|
positiveText: '确定',
|
||||||
|
title: '确认',
|
||||||
|
type: 'default',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
data.messages = [];
|
||||||
|
data.messageID = 0;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 处理 WebSocket 关闭
|
||||||
|
* @param {CloseEvent} event
|
||||||
|
*/
|
||||||
|
function handleClose(event) {
|
||||||
|
|
||||||
|
let ws = event.target;
|
||||||
|
|
||||||
|
notify({
|
||||||
|
message: 'WebSocket 已关闭',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ws) {
|
||||||
|
ws.removeEventListener('close', handleClose);
|
||||||
|
ws.removeEventListener('error', handleError);
|
||||||
|
ws.removeEventListener('message', handleMessage);
|
||||||
|
ws.removeEventListener('open', handleOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理 WebSocket 错误 */
|
||||||
|
function handleError() {
|
||||||
|
notify({
|
||||||
|
message: 'WebSocket 发生错误',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 处理 WebSocket 消息
|
||||||
|
* @param {MessageEvent} ev
|
||||||
|
*/
|
||||||
|
function handleMessage(ev) {
|
||||||
|
|
||||||
|
let msg = ev.data;
|
||||||
|
let el = logsContentRef.value;
|
||||||
|
|
||||||
|
if (!msg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = wsParse(false, msg);
|
||||||
|
let result = (parsed || msg)
|
||||||
|
|
||||||
|
console.log('%c%s', 'color: #2196F3;', '[接收]', (parsed || result));
|
||||||
|
|
||||||
|
// 记录消息
|
||||||
|
pushMessage('receive', msg);
|
||||||
|
|
||||||
|
// 自动滚动
|
||||||
|
nextTick(() => {
|
||||||
|
if (el && data.autoScroll) {
|
||||||
|
el.scrollTo(0, el.scrollHeight)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理 WebSocket 打开 */
|
||||||
|
function handleOpen() {
|
||||||
|
notify({
|
||||||
|
message: 'WebSocket 已连接',
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
function init() {
|
||||||
|
|
||||||
|
// 检测兼容性
|
||||||
|
if (typeof WebSocket === 'undefined') {
|
||||||
|
notify({
|
||||||
|
duration: 0,
|
||||||
|
message: '您的浏览器不支持 WebSocket。',
|
||||||
|
title: '错误',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 提示信息
|
||||||
|
* @param {object} options
|
||||||
|
* @param {number} options.duration
|
||||||
|
* @param {string} options.message
|
||||||
|
* @param {string} options.title
|
||||||
|
* @param {string} options.type
|
||||||
|
*/
|
||||||
|
function notify(options) {
|
||||||
|
|
||||||
|
let {
|
||||||
|
duration = 3000,
|
||||||
|
message = '',
|
||||||
|
title = '提示',
|
||||||
|
type = 'info',
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return $notification.create({
|
||||||
|
content: message,
|
||||||
|
duration: duration,
|
||||||
|
title: title,
|
||||||
|
type: type,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 添加消息
|
||||||
|
* @param {string} type 类型(receive、send)
|
||||||
|
* @param {string} msg 消息内容
|
||||||
|
*/
|
||||||
|
function pushMessage(type, msg = '') {
|
||||||
|
|
||||||
|
let types = ['receive', 'send'];
|
||||||
|
|
||||||
|
if (types.indexOf(type) === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = data.messages.length;
|
||||||
|
let max = data.logsMax;
|
||||||
|
|
||||||
|
// 最大行数
|
||||||
|
if (current >= max) {
|
||||||
|
data.messages.splice(0, (current - max + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
data.messageID += 1;
|
||||||
|
data.messages.push({
|
||||||
|
id: data.messageID,
|
||||||
|
message: msg,
|
||||||
|
time: Date.now(),
|
||||||
|
type: type,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭连接 */
|
||||||
|
function wsClose() {
|
||||||
|
if (data.ws) {
|
||||||
|
data.ws.close();
|
||||||
|
data.ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开连接 */
|
||||||
|
function wsConnect() {
|
||||||
|
|
||||||
|
let info = data.address;
|
||||||
|
let address = (info.prefix + info.suffix);
|
||||||
|
|
||||||
|
if (!info.suffix) {
|
||||||
|
notify({
|
||||||
|
message: '请填写连接地址',
|
||||||
|
title: '连接失败',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
let ws = new WebSocket(address);
|
||||||
|
|
||||||
|
// 监听事件
|
||||||
|
ws.addEventListener('close', handleClose);
|
||||||
|
ws.addEventListener('error', handleError);
|
||||||
|
ws.addEventListener('message', handleMessage);
|
||||||
|
ws.addEventListener('open', handleOpen);
|
||||||
|
|
||||||
|
// 保存对象
|
||||||
|
data.ws = ws;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
message: String(error),
|
||||||
|
title: '连接失败',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 解析消息
|
||||||
|
* @param {boolean} isSend 是否为发送,否则为接收
|
||||||
|
* @param {string} content 消息文本内容
|
||||||
|
* @returns {null|object|string} 成功则返回解析后的消息,否则返回 null
|
||||||
|
*/
|
||||||
|
function wsParse(isSend = false, content = '') {
|
||||||
|
|
||||||
|
let parseType = data.parseType;
|
||||||
|
|
||||||
|
if (parseType === 'json') {
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
try {
|
||||||
|
return JSON.parse(content);
|
||||||
|
} catch (error) {
|
||||||
|
let s1 = isSend ? '解析发送的 JSON 消息失败' : '解析接收的 JSON 消息失败';
|
||||||
|
let s2 = String(error);
|
||||||
|
console.warn(s1);
|
||||||
|
console.warn(s2);
|
||||||
|
notify({
|
||||||
|
message: s2,
|
||||||
|
title: s1,
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// 默认不处理
|
||||||
|
return content;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送消息 */
|
||||||
|
function wsSend() {
|
||||||
|
|
||||||
|
let instance = data.ws;
|
||||||
|
let message = data.inputs.replace(/(\n|\r)/g, '');
|
||||||
|
let parsed = wsParse(true, message);
|
||||||
|
|
||||||
|
if (instance && parsed) {
|
||||||
|
console.log('%c%s', 'color: #4CAF50;', '[发送]', parsed);
|
||||||
|
instance.send(message);
|
||||||
|
pushMessage('send', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
wsClose();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.address-input {
|
||||||
|
.n-select {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-content {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #F0F0F0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #FFF;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-y: auto;
|
||||||
|
user-select: text;
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
display: flex;
|
||||||
|
margin: 8px 0;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-tag {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
163
src/views/ToolboxView/Other/GenshinImpactClock/ClockColor.vue
Normal file
163
src/views/ToolboxView/Other/GenshinImpactClock/ClockColor.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
class="clock-color"
|
||||||
|
:viewBox="`0 0 ${elSize} ${elSize}`"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 定义 -->
|
||||||
|
<defs>
|
||||||
|
<!-- 背景图案 -->
|
||||||
|
<pattern
|
||||||
|
id="color-pattern"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
patternContentUnits="userSpaceOnUse"
|
||||||
|
:width="elSize"
|
||||||
|
:height="elSize"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
:width="elSize"
|
||||||
|
:height="elSize"
|
||||||
|
:href="IMAGE_TIME_ZONE_COLOR"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 外层圆环 -->
|
||||||
|
<path
|
||||||
|
class="color-circle"
|
||||||
|
:class="{ faded: currAngle > 360 }"
|
||||||
|
:d="state.dOuter"
|
||||||
|
fill="url(#color-pattern)"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
|
||||||
|
<!-- 内层圆环 -->
|
||||||
|
<path
|
||||||
|
ref="innerCircle"
|
||||||
|
class="color-circle"
|
||||||
|
:d="state.dInner"
|
||||||
|
fill="url(#color-pattern)"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
onMounted,
|
||||||
|
reactive, watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IMAGE_TIME_ZONE_COLOR,
|
||||||
|
} from './common-data';
|
||||||
|
|
||||||
|
import arc from '@/assets/js/svg-arc';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
|
||||||
|
elSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
|
||||||
|
currAngle: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
startAngle: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
radius: {
|
||||||
|
type: Number,
|
||||||
|
default: 150,
|
||||||
|
},
|
||||||
|
|
||||||
|
thickness: {
|
||||||
|
type: Number,
|
||||||
|
default: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dInner: '',
|
||||||
|
dOuter: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const halfSize = computed(() => {
|
||||||
|
return (props.elSize / 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 角度变化时更新状态
|
||||||
|
watch(() => (props.currAngle), () => {
|
||||||
|
updateCircle();
|
||||||
|
});
|
||||||
|
watch(() => (props.startAngle), () => {
|
||||||
|
updateCircle();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 更新圆环状态 */
|
||||||
|
function updateCircle() {
|
||||||
|
|
||||||
|
const { currAngle, startAngle, thickness } = props;
|
||||||
|
|
||||||
|
const radius = halfSize.value;
|
||||||
|
const size = thickness;
|
||||||
|
const offset = size / 4;
|
||||||
|
|
||||||
|
const endAngleInner = Math.max(0, currAngle - 360) + startAngle;
|
||||||
|
const endAngleOuter = Math.min(360, currAngle) + startAngle;
|
||||||
|
|
||||||
|
// 内层圆环
|
||||||
|
state.dInner = arc({
|
||||||
|
x: radius,
|
||||||
|
y: radius,
|
||||||
|
R: radius - size * 2 - offset,
|
||||||
|
r: radius - size - offset,
|
||||||
|
start: startAngle,
|
||||||
|
end: endAngleInner,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 外层圆环
|
||||||
|
state.dOuter = arc({
|
||||||
|
x: radius,
|
||||||
|
y: radius,
|
||||||
|
R: radius - size,
|
||||||
|
r: radius,
|
||||||
|
start: startAngle,
|
||||||
|
end: endAngleOuter,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateCircle();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.clock-color {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.color-circle {
|
||||||
|
filter: brightness(1);
|
||||||
|
transition: filter 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faded {
|
||||||
|
filter: brightness(0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
736
src/views/ToolboxView/Other/GenshinImpactClock/ClockElement.vue
Normal file
736
src/views/ToolboxView/Other/GenshinImpactClock/ClockElement.vue
Normal file
@@ -0,0 +1,736 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="clockElement"
|
||||||
|
:class="{
|
||||||
|
'clock-element': true,
|
||||||
|
'clock-rotation': clockState.isRotation,
|
||||||
|
}"
|
||||||
|
:style="elStyle"
|
||||||
|
@touchmove.prevent
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 外部 -->
|
||||||
|
<div class="clock-outer bg-contain"></div>
|
||||||
|
|
||||||
|
<!-- 内部 -->
|
||||||
|
<div class="clock-inner">
|
||||||
|
|
||||||
|
<!-- 背景 -->
|
||||||
|
<div class="inner-bg bg-cover"></div>
|
||||||
|
<div class="inner-star bg-cover"></div>
|
||||||
|
|
||||||
|
<!-- 齿轮 -->
|
||||||
|
<div class="clock-gear">
|
||||||
|
<div class="gear-6"></div>
|
||||||
|
<div class="gear-5"></div>
|
||||||
|
<div class="gear-4"></div>
|
||||||
|
<div class="gear-3"></div>
|
||||||
|
<div class="gear-2"></div>
|
||||||
|
<div class="gear-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 色环 -->
|
||||||
|
<clock-color
|
||||||
|
:curr-angle="upperRealAngle"
|
||||||
|
:start-angle="lowerPointer.viewAngle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 下层指针 -->
|
||||||
|
<div class="pointer-wrapper pointer-lower bg-contain">
|
||||||
|
<div class="pointer-content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上层指针 -->
|
||||||
|
<div class="pointer-wrapper pointer-upper bg-contain">
|
||||||
|
<div
|
||||||
|
class="pointer-content"
|
||||||
|
@pointerdown="handleDragPointer('upper')"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表盘 -->
|
||||||
|
<div class="clock-dial bg-contain">
|
||||||
|
<div class="time-icons">
|
||||||
|
<div
|
||||||
|
class="time-morning bg-contain"
|
||||||
|
style="--self-angle-1: 270; --self-angle-2: 270;"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="time-noon bg-contain"
|
||||||
|
style="--self-angle-1: 0; --self-angle-2: 360;"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="time-dusk bg-contain"
|
||||||
|
style="--self-angle-1: 90; --self-angle-2: 90;"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="time-night bg-contain"
|
||||||
|
style="--self-angle-1: 180; --self-angle-2: 180;"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 遮罩层,用于阻止操作 -->
|
||||||
|
<div v-show="isAutoRotating" class="clock-mask"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
onBeforeUnmount, onMounted,
|
||||||
|
reactive, ref,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IMAGE_CLOCK_BG_INNER, IMAGE_CLOCK_BG_OUTER, IMAGE_CLOCK_DIAL,
|
||||||
|
IMAGE_CLOCK_GEAR_1, IMAGE_CLOCK_GEAR_4,
|
||||||
|
IMAGE_CLOCK_GEAR_5, IMAGE_CLOCK_GEAR_6,
|
||||||
|
IMAGE_CLOCK_PARTICLES, IMAGE_POINTER_LOWER, IMAGE_POINTER_UPPER,
|
||||||
|
IMAGE_TIME_ICON_DUSK, IMAGE_TIME_ICON_MORNING,
|
||||||
|
IMAGE_TIME_ICON_NIGHT, IMAGE_TIME_ICON_NOON,
|
||||||
|
isAutoRotating, isTimeExceeded, isTimeTooEarly,
|
||||||
|
timeCurrHour, timeCurrMinute, timeCurrValue,
|
||||||
|
timeDiffLabel, timeDiffLabelStill,
|
||||||
|
timeNewHour, timeNewMinute, timeNewValue,
|
||||||
|
} from './common-data';
|
||||||
|
|
||||||
|
import ClockColor from './ClockColor.vue';
|
||||||
|
|
||||||
|
const clockElement = ref(null);
|
||||||
|
|
||||||
|
/** 时钟状态 */
|
||||||
|
const clockState = reactive({
|
||||||
|
|
||||||
|
/** 指针是否正在旋转 */
|
||||||
|
isRotation: false,
|
||||||
|
|
||||||
|
/** 最新一次获取到的指针角度 */
|
||||||
|
lastAngle: 0,
|
||||||
|
|
||||||
|
/** 定时器 ID */
|
||||||
|
rotationWatcher: null,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 下层指针状态 */
|
||||||
|
const lowerPointer = reactive({
|
||||||
|
|
||||||
|
/** 视图角度,用于显示 */
|
||||||
|
viewAngle: 0,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 上层指针状态 */
|
||||||
|
const upperPointer = reactive({
|
||||||
|
|
||||||
|
/** 是否为第二圈 */
|
||||||
|
isSecond: false,
|
||||||
|
|
||||||
|
/** 上层指针与下层指针相差的角度 */
|
||||||
|
dataAngle: 0,
|
||||||
|
|
||||||
|
/** 视图角度,用于显示和交互 */
|
||||||
|
viewAngle: 0,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
window.upperPointer = upperPointer;
|
||||||
|
|
||||||
|
/** 元素 CSS */
|
||||||
|
const elStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
'--image-clock-bg-inner': IMAGE_CLOCK_BG_INNER,
|
||||||
|
'--image-clock-bg-outer': IMAGE_CLOCK_BG_OUTER,
|
||||||
|
'--image-clock-dial': IMAGE_CLOCK_DIAL,
|
||||||
|
'--image-clock-gear-1': IMAGE_CLOCK_GEAR_1,
|
||||||
|
'--image-clock-gear-4': IMAGE_CLOCK_GEAR_4,
|
||||||
|
'--image-clock-gear-5': IMAGE_CLOCK_GEAR_5,
|
||||||
|
'--image-clock-gear-6': IMAGE_CLOCK_GEAR_6,
|
||||||
|
'--image-clock-particles': IMAGE_CLOCK_PARTICLES,
|
||||||
|
'--image-pointer-lower': IMAGE_POINTER_LOWER,
|
||||||
|
'--image-pointer-upper': IMAGE_POINTER_UPPER,
|
||||||
|
'--image-time-icon-dusk': IMAGE_TIME_ICON_DUSK,
|
||||||
|
'--image-time-icon-morning': IMAGE_TIME_ICON_MORNING,
|
||||||
|
'--image-time-icon-night': IMAGE_TIME_ICON_NIGHT,
|
||||||
|
'--image-time-icon-noon': IMAGE_TIME_ICON_NOON,
|
||||||
|
'--pointer-lower-angle': `${lowerPointer.viewAngle}deg`,
|
||||||
|
'--pointer-lower-angle-value': lowerPointer.viewAngle,
|
||||||
|
'--pointer-upper-angle': `${upperPointer.viewAngle}deg`,
|
||||||
|
'--pointer-upper-angle-value': upperPointer.viewAngle,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 上层指针实际已旋转角度 */
|
||||||
|
const upperRealAngle = computed(() => {
|
||||||
|
const { dataAngle, isSecond } = upperPointer;
|
||||||
|
return (isSecond ? dataAngle + 360 : dataAngle);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 初始化定时器 */
|
||||||
|
function timerInit() {
|
||||||
|
|
||||||
|
// 指针旋转检测
|
||||||
|
clockState.rotationWatcher = setInterval(function () {
|
||||||
|
const currAngle = upperRealAngle.value;
|
||||||
|
const lastAngle = clockState.lastAngle;
|
||||||
|
clockState.isRotation = (Math.abs(currAngle - lastAngle) >= 5);
|
||||||
|
clockState.lastAngle = currAngle;
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置定时器 */
|
||||||
|
function timerReset() {
|
||||||
|
clearInterval(clockState.rotationWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取元素中心坐标
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
*/
|
||||||
|
function getCenterPoint(el) {
|
||||||
|
if (el) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: rect.left + rect.width / 2,
|
||||||
|
y: rect.top + rect.height / 2,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理拖拽上层指针 */
|
||||||
|
function handleDragPointer() {
|
||||||
|
|
||||||
|
const center = getCenterPoint(clockElement.value);
|
||||||
|
|
||||||
|
if (!center) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerX = center.x;
|
||||||
|
const centerY = center.y;
|
||||||
|
|
||||||
|
// 节流
|
||||||
|
let last = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 处理光标移动
|
||||||
|
* @param {PointerEvent} ev
|
||||||
|
*/
|
||||||
|
let handleMove = function (ev) {
|
||||||
|
|
||||||
|
let curr = Date.now();
|
||||||
|
|
||||||
|
if (curr - last >= 20) {
|
||||||
|
last = curr;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { pageX, pageY } = ev;
|
||||||
|
|
||||||
|
let numX = pageX - centerX;
|
||||||
|
let numY = pageY - centerY;
|
||||||
|
|
||||||
|
// 计算两点弧度 & 转换为角度(-180 ~ 180)
|
||||||
|
let calcAngle = Math.round(Math.atan2(numY, numX) * (180 / Math.PI));
|
||||||
|
|
||||||
|
// 转换为视图角度(0 ~ 359)
|
||||||
|
let viewAngle = calcAngle + (calcAngle >= -90 ? 90 : 450);
|
||||||
|
|
||||||
|
// 用于数据处理的角度
|
||||||
|
let dataAngle = 0;
|
||||||
|
|
||||||
|
// 起始角度偏移值
|
||||||
|
let offsetAngle = lowerPointer.viewAngle;
|
||||||
|
|
||||||
|
// 处理偏移,获取数据角度
|
||||||
|
if (viewAngle >= offsetAngle) {
|
||||||
|
dataAngle = viewAngle - offsetAngle;
|
||||||
|
} else if (viewAngle < offsetAngle) {
|
||||||
|
dataAngle = viewAngle + (360 - offsetAngle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新角度与原角度的差值
|
||||||
|
let diff = dataAngle - upperPointer.dataAngle;
|
||||||
|
|
||||||
|
// 顺时针越过起始点
|
||||||
|
if (diff <= -180) {
|
||||||
|
if (upperPointer.isSecond) {
|
||||||
|
// 当前为第二圈,阻止移动
|
||||||
|
dataAngle = 360;
|
||||||
|
viewAngle = 360 + offsetAngle;
|
||||||
|
} else {
|
||||||
|
// 当前为第一圈,进入第二圈
|
||||||
|
upperPointer.isSecond = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逆时针越过起始点
|
||||||
|
if (diff >= 180) {
|
||||||
|
if (upperPointer.isSecond) {
|
||||||
|
// 当前为第二圈,返回第一圈
|
||||||
|
upperPointer.isSecond = false;
|
||||||
|
} else {
|
||||||
|
// 当前为第一圈,阻止移动
|
||||||
|
dataAngle = 0;
|
||||||
|
viewAngle = offsetAngle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upperPointer.dataAngle = dataAngle;
|
||||||
|
upperPointer.viewAngle = viewAngle;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', handleMove);
|
||||||
|
window.addEventListener('pointerup', function () {
|
||||||
|
window.removeEventListener('pointermove', handleMove);
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确定当前选择的时间,下层指针旋转至上层指针的位置 */
|
||||||
|
function handleSubmitTime() {
|
||||||
|
|
||||||
|
let lowerAngleStart = lowerPointer.viewAngle;
|
||||||
|
let upperAngleStart = upperRealAngle.value;
|
||||||
|
let upperAngleCurr = upperAngleStart;
|
||||||
|
let timer = setInterval(() => {
|
||||||
|
|
||||||
|
// 每次 -2,最小值为 0
|
||||||
|
upperAngleCurr = Math.max(0, upperAngleCurr - 2);
|
||||||
|
|
||||||
|
// 结束
|
||||||
|
if (upperAngleCurr === 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
isAutoRotating.value = false;
|
||||||
|
timeDiffLabelStill.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let upperAngleDiff = upperAngleStart - upperAngleCurr;
|
||||||
|
let lowerAngleCurr = lowerAngleStart + upperAngleDiff;
|
||||||
|
|
||||||
|
lowerPointer.viewAngle = lowerAngleCurr % 360;
|
||||||
|
upperPointer.dataAngle = upperAngleCurr % 360;
|
||||||
|
upperPointer.isSecond = (upperAngleCurr >= 360);
|
||||||
|
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
/** 更新状态 */
|
||||||
|
isAutoRotating.value = true;
|
||||||
|
|
||||||
|
// 固定时间差文本
|
||||||
|
timeDiffLabelStill.value = timeDiffLabel.value;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
handleSubmitTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检测角度变化,计算时间信息(自动旋转时)
|
||||||
|
watch(() => {
|
||||||
|
return lowerPointer.viewAngle;
|
||||||
|
}, (viewAngle) => {
|
||||||
|
|
||||||
|
// 转换为对应 24 小时的角度
|
||||||
|
let timeAngle = viewAngle + (viewAngle < 180 ? 180 : -180);
|
||||||
|
let timeValue = timeAngle / 15;
|
||||||
|
let currHour = Math.floor(timeValue);
|
||||||
|
let currMinute = Math.round((timeValue - currHour) * 60);
|
||||||
|
|
||||||
|
// 计算时间值
|
||||||
|
timeCurrHour.value = String(currHour).padStart(2, '0');
|
||||||
|
timeCurrMinute.value = String(currMinute).padStart(2, '0');
|
||||||
|
timeCurrValue.value = timeValue;
|
||||||
|
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// 检测角度变化,计算时间信息(用户操作时)
|
||||||
|
watch(() => {
|
||||||
|
return upperPointer.dataAngle;
|
||||||
|
}, (dataAngle) => {
|
||||||
|
|
||||||
|
// 注:15° / 小时
|
||||||
|
|
||||||
|
let isSecond = upperPointer.isSecond;
|
||||||
|
let currAngle = lowerPointer.viewAngle;
|
||||||
|
let viewAngle = upperPointer.viewAngle;
|
||||||
|
|
||||||
|
let diffAngle = dataAngle + (isSecond ? 360 : 0);
|
||||||
|
let diffAngle1 = 0; // +1 日角度差
|
||||||
|
let diffAngle2 = 0; // +2 日角度差
|
||||||
|
let diffLabel = '';
|
||||||
|
|
||||||
|
// 转换为对应 24 小时的角度
|
||||||
|
let timeAngle = viewAngle + (viewAngle < 180 ? 180 : -180);
|
||||||
|
let timeValue = timeAngle / 15;
|
||||||
|
let newHour = Math.floor(timeValue);
|
||||||
|
let newMinute = Math.round((timeValue - newHour) * 60);
|
||||||
|
|
||||||
|
if (currAngle < 180) {
|
||||||
|
diffAngle1 = 180 - currAngle;
|
||||||
|
diffAngle2 = diffAngle1 + 360;
|
||||||
|
} else {
|
||||||
|
diffAngle1 = 540 - currAngle; // 360 + 180
|
||||||
|
diffAngle2 = diffAngle1 + 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理时间差信息
|
||||||
|
if (diffAngle < diffAngle1) {
|
||||||
|
diffLabel = '今日';
|
||||||
|
} else if (diffAngle < diffAngle2) {
|
||||||
|
diffLabel = '次日';
|
||||||
|
} else {
|
||||||
|
diffLabel = '+2日';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注:
|
||||||
|
// 若指针起始位置位于表盘左半边,
|
||||||
|
// 且拖拽指针旋转满 2 圈,
|
||||||
|
// 此时计算出的小时值会大于或等于 24。
|
||||||
|
if (newHour >= 24) {
|
||||||
|
newHour -= 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理提示信息显示
|
||||||
|
isTimeTooEarly.value = diffAngle < 7.5;
|
||||||
|
isTimeExceeded.value = diffAngle === 720;
|
||||||
|
|
||||||
|
// 更新时间差信息
|
||||||
|
timeDiffLabel.value = diffLabel;
|
||||||
|
|
||||||
|
// 计算时间值
|
||||||
|
timeNewHour.value = String(newHour).padStart(2, '0');
|
||||||
|
timeNewMinute.value = String(newMinute).padStart(2, '0');
|
||||||
|
timeNewValue.value = timeValue;
|
||||||
|
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
timerInit();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
timerReset();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.bg-contain {
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-cover {
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-element {
|
||||||
|
--pointer-lower-angle: 0deg;
|
||||||
|
--pointer-upper-angle: 0deg;
|
||||||
|
position: relative;
|
||||||
|
width: 32em;
|
||||||
|
height: 32em;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
filter: brightness(1.1) saturate(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-outer {
|
||||||
|
position: absolute;
|
||||||
|
top: -1%;
|
||||||
|
left: -1%;
|
||||||
|
width: 102%;
|
||||||
|
height: 102%;
|
||||||
|
background-image: var(--image-clock-bg-outer);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-inner {
|
||||||
|
--size: 46%;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - var(--size) / 2);
|
||||||
|
left: calc(50% - var(--size) / 2);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.inner-bg, .inner-star {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-bg {
|
||||||
|
background-image: var(--image-clock-bg-inner);
|
||||||
|
animation: rotation-backward 60s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-star {
|
||||||
|
background-image: var(--image-clock-particles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-gear {
|
||||||
|
--size: 120%;
|
||||||
|
--ratio: 1.55;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
--ani-duration: 2s;
|
||||||
|
position: absolute;
|
||||||
|
animation-duration: var(--ani-duration);
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
transform: rotate(0);
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
animation-duration: calc(var(--ani-duration) / 12);
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-name: inherit;
|
||||||
|
animation-play-state: paused;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gear-1 {
|
||||||
|
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
|
||||||
|
--gear-teeth: 32;
|
||||||
|
top: calc(50% - var(--size) / 2);
|
||||||
|
left: calc(50% - var(--size) / 2);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
animation-name: rotation-backward;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-image: var(--image-clock-gear-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gear-2 {
|
||||||
|
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
|
||||||
|
--gear-teeth: 32;
|
||||||
|
top: calc(0% - var(--size) / 2);
|
||||||
|
left: calc(72% - var(--size) / 2);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
animation-name: rotation-backward;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-image: var(--image-clock-gear-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gear-3 {
|
||||||
|
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
|
||||||
|
--gear-teeth: 32;
|
||||||
|
top: calc(77% - var(--size) / 2);
|
||||||
|
left: calc(100% - var(--size) / 2);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
animation-name: rotation-backward;
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-image: var(--image-clock-gear-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gear-4 {
|
||||||
|
--angle-offset: 4deg;
|
||||||
|
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
|
||||||
|
--gear-teeth: 32;
|
||||||
|
top: calc(77% - var(--size) / 2);
|
||||||
|
left: calc(100% - var(--size) / 2);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
animation-name: rotation-forward;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-image: var(--image-clock-gear-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常:约 74s / 圈
|
||||||
|
// 加速:约 7s / 圈
|
||||||
|
.gear-5 {
|
||||||
|
--ani-duration: calc(1s * (var(--gear-teeth) - 1) * var(--ratio));
|
||||||
|
--gear-teeth: 49;
|
||||||
|
top: calc(60% - var(--size) / 2);
|
||||||
|
left: calc(33% - var(--size) / 2);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
animation-name: rotation-backward;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-image: var(--image-clock-gear-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gear-6 {
|
||||||
|
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
|
||||||
|
--gear-teeth: 49;
|
||||||
|
top: calc(42% - var(--size) / 2);
|
||||||
|
left: calc(62% - var(--size) / 2);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
animation-name: rotation-backward;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-image: var(--image-clock-gear-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 齿轮加速转动
|
||||||
|
.clock-rotation .clock-gear > div::after {
|
||||||
|
animation-play-state: running;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer-wrapper {
|
||||||
|
--size: 180%;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
|
||||||
|
&.pointer-lower {
|
||||||
|
background-image: var(--image-pointer-lower);
|
||||||
|
transform: translate(-50%, -50%) rotate(var(--pointer-lower-angle));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pointer-upper {
|
||||||
|
background-image: var(--image-pointer-upper);
|
||||||
|
transform: translate(-50%, -50%) rotate(var(--pointer-upper-angle));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer-content {
|
||||||
|
--width: 5%;
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - var(--width) / 2);
|
||||||
|
width: var(--width);
|
||||||
|
height: 50%;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-dial {
|
||||||
|
--size: 88%;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - var(--size) / 2);
|
||||||
|
left: calc(50% - var(--size) / 2);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
background-image: var(--image-clock-dial);
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.time-icons {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
// [角度 1 相关计算]
|
||||||
|
// 计算指针角度与图标对应角度的差值(可能为负数)
|
||||||
|
--angle-1-diff: calc(var(--self-angle-1) - var(--pointer-lower-angle-value));
|
||||||
|
// 计算角度差值的绝对值
|
||||||
|
--angle-1-abs: ~"max(var(--angle-1-diff), var(--angle-1-diff) * -1)";
|
||||||
|
// 限制角度最大差值为 90
|
||||||
|
--angle-1-use: ~"min(90, var(--angle-1-abs))";
|
||||||
|
// [角度 2 相关计算,主要用于 360°]
|
||||||
|
// 计算指针角度与图标所在角度的差值(可能为负数)
|
||||||
|
--angle-2-diff: calc(var(--self-angle-2) - var(--pointer-lower-angle-value));
|
||||||
|
// 计算角度差值的绝对值
|
||||||
|
--angle-2-abs: ~"max(var(--angle-2-diff), var(--angle-2-diff) * -1)";
|
||||||
|
// 限制角度最大差值为 90
|
||||||
|
--angle-2-use: ~"min(90, var(--angle-2-abs))";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
// 根据角度计算透明度
|
||||||
|
// 注:计算两个角度的透明度,取最大值
|
||||||
|
opacity: ~"calc((90 - min(var(--angle-1-use), var(--angle-2-use))) / 90)";
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-morning {
|
||||||
|
background-image: var(--image-time-icon-morning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-noon {
|
||||||
|
background-image: var(--image-time-icon-noon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-dusk {
|
||||||
|
background-image: var(--image-time-icon-dusk);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-night {
|
||||||
|
background-image: var(--image-time-icon-night);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-mask {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顺时针旋转
|
||||||
|
@keyframes rotation-forward {
|
||||||
|
0% {
|
||||||
|
transform: rotate(calc(0deg + var(--angle-offset, 0deg)));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(calc(360deg + var(--angle-offset, 0deg)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逆时针旋转
|
||||||
|
@keyframes rotation-backward {
|
||||||
|
0% {
|
||||||
|
transform: rotate(calc(360deg + var(--angle-offset, 0deg)));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(calc(0deg + var(--angle-offset, 0deg)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="genshin-button">
|
||||||
|
<span
|
||||||
|
v-if="hasIcon"
|
||||||
|
class="btn-icon mdi"
|
||||||
|
:class="iconName"
|
||||||
|
:style="{ color: iconColor }"
|
||||||
|
></span>
|
||||||
|
<span class="btn-label">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
|
||||||
|
hasIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
iconColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#FFFFFF',
|
||||||
|
},
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
type: String,
|
||||||
|
default: 'mdi-help',
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.genshin-button {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: #ECE3D6;
|
||||||
|
color: #494246;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #2D2D2D;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 56px;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,221 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page has-radius">
|
||||||
|
|
||||||
|
<!-- 背景图片 -->
|
||||||
|
<div
|
||||||
|
class="page-bg-wrapper"
|
||||||
|
:style="{ '--curr-time': Number(timeCurrValue) }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'--bg-url': IMAGE_BG_DUSK,
|
||||||
|
'--self-hour-1': 18,
|
||||||
|
'--self-hour-2': 18,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'--bg-url': IMAGE_BG_MORNING,
|
||||||
|
'--self-hour-1': 6,
|
||||||
|
'--self-hour-2': 6,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'--bg-url': IMAGE_BG_NIGHT,
|
||||||
|
'--self-hour-1': 0,
|
||||||
|
'--self-hour-2': 24,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'--bg-url': IMAGE_BG_NOON,
|
||||||
|
'--self-hour-1': 12,
|
||||||
|
'--self-hour-2': 12,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div class="page-bg-mask"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左 -->
|
||||||
|
<div class="page-column">
|
||||||
|
<time-info />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右 -->
|
||||||
|
<div class="page-column">
|
||||||
|
|
||||||
|
<!-- 时钟 -->
|
||||||
|
<clock-element ref="clockRef" />
|
||||||
|
|
||||||
|
<!-- 上限提示 -->
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
'is-hide': isAutoRotating,
|
||||||
|
'time-notice': true,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-show="isTimeExceeded"
|
||||||
|
class="time-notice-text"
|
||||||
|
>时间到达上限</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认时间 -->
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
'is-hide': isAutoRotating,
|
||||||
|
'time-submit': true,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="isTimeTooEarly"
|
||||||
|
class="time-notice-text"
|
||||||
|
>时间少于30分钟</span>
|
||||||
|
<genshin-button
|
||||||
|
v-else
|
||||||
|
icon-color="#FFC107"
|
||||||
|
icon-name="mdi-circle-outline"
|
||||||
|
@click="handleConfirm"
|
||||||
|
>确认</genshin-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IMAGE_BG_DUSK,
|
||||||
|
IMAGE_BG_MORNING,
|
||||||
|
IMAGE_BG_NIGHT,
|
||||||
|
IMAGE_BG_NOON,
|
||||||
|
isAutoRotating,
|
||||||
|
isTimeTooEarly,
|
||||||
|
isTimeExceeded,
|
||||||
|
timeCurrValue,
|
||||||
|
} from './common-data';
|
||||||
|
|
||||||
|
import ClockElement from './ClockElement.vue';
|
||||||
|
import GenshinButton from './GenshinButton.vue';
|
||||||
|
import TimeInfo from './TimeInfo.vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 时钟元素 ref
|
||||||
|
* @type {VueRef<InstanceType<ClockElement>>}
|
||||||
|
*/
|
||||||
|
const clockRef = ref(null);
|
||||||
|
|
||||||
|
/** 处理点击确认按钮 */
|
||||||
|
function handleConfirm() {
|
||||||
|
|
||||||
|
let el = clockRef.value;
|
||||||
|
|
||||||
|
if (el) {
|
||||||
|
el.handleSubmitTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.tool-detail-page {
|
||||||
|
position: relative;
|
||||||
|
background-color: #000;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-bg-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
// [小时 1 相关计算]
|
||||||
|
// 计算当前小时与背景对应小时的差值(可能为负数)
|
||||||
|
--hour-1-diff: calc(var(--self-hour-1) - var(--curr-time));
|
||||||
|
// 计算小时差值的绝对值
|
||||||
|
--hour-1-abs: ~"max(var(--hour-1-diff), var(--hour-1-diff) * -1)";
|
||||||
|
// 限制小时最大差值为 6
|
||||||
|
--hour-1-use: ~"min(6, var(--hour-1-abs))";
|
||||||
|
// [小时 2 相关计算,主要用于 0 点]
|
||||||
|
// 计算当前小时与背景所在小时的差值(可能为负数)
|
||||||
|
--hour-2-diff: calc(var(--self-hour-2) - var(--curr-time));
|
||||||
|
// 计算小时差值的绝对值
|
||||||
|
--hour-2-abs: ~"max(var(--hour-2-diff), var(--hour-2-diff) * -1)";
|
||||||
|
// 限制小时最大差值为 6
|
||||||
|
--hour-2-use: ~"min(6, var(--hour-2-abs))";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: var(--bg-url);
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
// 根据小时计算透明度
|
||||||
|
// 注:计算两个小时的透明度,取最大值
|
||||||
|
opacity: ~"calc((6 - min(var(--hour-1-use), var(--hour-2-use))) / 6)";
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-bg-mask {
|
||||||
|
background-color: #000;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-column {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 100%;
|
||||||
|
white-space: initial;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-notice, .time-submit {
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transition: opacity 0.25s, visibility 0s 0s;
|
||||||
|
|
||||||
|
// 注:
|
||||||
|
// visibility 动画时长用于等待 opacity 动画过渡完毕
|
||||||
|
&.is-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s, visibility 0.25s 0s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-notice {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-notice-text {
|
||||||
|
margin: auto 0;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFF;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
128
src/views/ToolboxView/Other/GenshinImpactClock/TimeInfo.vue
Normal file
128
src/views/ToolboxView/Other/GenshinImpactClock/TimeInfo.vue
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="time-info"
|
||||||
|
:style="{ '--image-time-info-arrow': IMAGE_TIME_INFO_ARROW }"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 装饰元素 -->
|
||||||
|
<div class="arrow-element"></div>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="time-title time-title-current">当前时间</div>
|
||||||
|
|
||||||
|
<!-- 当前时间值 -->
|
||||||
|
<div class="time-value time-value-current">
|
||||||
|
<span>{{ timeCurrHour }}</span>
|
||||||
|
<span>:</span>
|
||||||
|
<span>{{ timeCurrMinute }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 三角形图标 -->
|
||||||
|
<div class="triangle-icon">
|
||||||
|
<span class="mdi mdi-triangle-small-down"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="time-title time-title-target">调整到</div>
|
||||||
|
|
||||||
|
<!-- 三角形图标 -->
|
||||||
|
<div class="triangle-icon">
|
||||||
|
<span class="mdi mdi-triangle-small-down"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 目标时间值 -->
|
||||||
|
<div class="time-value time-value-target">
|
||||||
|
<span>{{ timeNewHour }}</span>
|
||||||
|
<span>:</span>
|
||||||
|
<span>{{ timeNewMinute }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间差 -->
|
||||||
|
<div class="time-diff">
|
||||||
|
<span>{{ timeDiffLabelStill || timeDiffLabel }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 装饰元素 -->
|
||||||
|
<div class="arrow-element"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
IMAGE_TIME_INFO_ARROW,
|
||||||
|
timeCurrHour, timeCurrMinute,
|
||||||
|
timeDiffLabel, timeDiffLabelStill,
|
||||||
|
timeNewHour, timeNewMinute,
|
||||||
|
} from './common-data';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.time-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 8em;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-element {
|
||||||
|
margin: 1.75em 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1.5em;
|
||||||
|
background-image: var(--image-time-info-arrow);
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-diff {
|
||||||
|
display: flex;
|
||||||
|
width: 3.5em;
|
||||||
|
height: 1.75em;
|
||||||
|
border-radius: 1.75em;
|
||||||
|
background-color: #282C33;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
font-size: 0.75em;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-title {
|
||||||
|
color: #AFA189;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-title-target {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value {
|
||||||
|
color: #ECE3D6;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value-current {
|
||||||
|
margin-top: 1.25em;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value-target {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triangle-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
color: #FFF;
|
||||||
|
font-size: 1.5em;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,56 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export const IMAGE_BASE = `https://c.frost-zx.top/data/static/image/genshin-impact-clock`;
|
||||||
|
export const IMAGE_BG_DUSK = `url("${IMAGE_BASE}/bg_dusk.png")`;
|
||||||
|
export const IMAGE_BG_MORNING = `url("${IMAGE_BASE}/bg_morning.png")`;
|
||||||
|
export const IMAGE_BG_NIGHT = `url("${IMAGE_BASE}/bg_night.png")`;
|
||||||
|
export const IMAGE_BG_NOON = `url("${IMAGE_BASE}/bg_noon.png")`;
|
||||||
|
export const IMAGE_CLOCK_BG_INNER = `url("${IMAGE_BASE}/clock_bg_inner.png")`;
|
||||||
|
export const IMAGE_CLOCK_BG_OUTER = `url("${IMAGE_BASE}/clock_bg_outer.png")`;
|
||||||
|
export const IMAGE_CLOCK_DIAL = `url("${IMAGE_BASE}/clock_dial.png")`;
|
||||||
|
export const IMAGE_CLOCK_GEAR_1 = `url("${IMAGE_BASE}/clock_gear_1.png")`;
|
||||||
|
export const IMAGE_CLOCK_GEAR_4 = `url("${IMAGE_BASE}/clock_gear_4.png")`;
|
||||||
|
export const IMAGE_CLOCK_GEAR_5 = `url("${IMAGE_BASE}/clock_gear_5.png")`;
|
||||||
|
export const IMAGE_CLOCK_GEAR_6 = `url("${IMAGE_BASE}/clock_gear_6.png")`;
|
||||||
|
export const IMAGE_CLOCK_PARTICLES = `url("${IMAGE_BASE}/clock_particles.gif")`;
|
||||||
|
export const IMAGE_POINTER_LOWER = `url("${IMAGE_BASE}/pointer_lower.png")`;
|
||||||
|
export const IMAGE_POINTER_UPPER = `url("${IMAGE_BASE}/pointer_upper.png")`;
|
||||||
|
export const IMAGE_TIME_ICON_DUSK = `url("${IMAGE_BASE}/time_icon_dusk.png")`;
|
||||||
|
export const IMAGE_TIME_ICON_MORNING = `url("${IMAGE_BASE}/time_icon_morning.png")`;
|
||||||
|
export const IMAGE_TIME_ICON_NIGHT = `url("${IMAGE_BASE}/time_icon_night.png")`;
|
||||||
|
export const IMAGE_TIME_ICON_NOON = `url("${IMAGE_BASE}/time_icon_noon.png")`;
|
||||||
|
export const IMAGE_TIME_INFO_ARROW = `url("${IMAGE_BASE}/time_info_arrow.png")`;
|
||||||
|
export const IMAGE_TIME_ZONE_COLOR = `${IMAGE_BASE}/time_zone_color.png`;
|
||||||
|
|
||||||
|
/** 是否正在自动旋转 */
|
||||||
|
export const isAutoRotating = ref(false);
|
||||||
|
|
||||||
|
/** 是否时间少于 30 分钟 */
|
||||||
|
export const isTimeTooEarly = ref(false);
|
||||||
|
|
||||||
|
/** 是否时间到达上限 */
|
||||||
|
export const isTimeExceeded = ref(false);
|
||||||
|
|
||||||
|
/** 当前时 */
|
||||||
|
export const timeCurrHour = ref('00');
|
||||||
|
|
||||||
|
/** 当前分 */
|
||||||
|
export const timeCurrMinute = ref('00');
|
||||||
|
|
||||||
|
/** 当前时分 */
|
||||||
|
export const timeCurrValue = ref(0);
|
||||||
|
|
||||||
|
/** 时间差(动态)*/
|
||||||
|
export const timeDiffLabel = ref('');
|
||||||
|
|
||||||
|
/** 时间差(固定)*/
|
||||||
|
export const timeDiffLabelStill = ref('');
|
||||||
|
|
||||||
|
/** 新的时 */
|
||||||
|
export const timeNewHour = ref('00');
|
||||||
|
|
||||||
|
/** 新的分 */
|
||||||
|
export const timeNewMinute = ref('00');
|
||||||
|
|
||||||
|
/** 新的时分 */
|
||||||
|
export const timeNewValue = ref(0);
|
222
src/views/ToolboxView/Other/KeepScreenOn.vue
Normal file
222
src/views/ToolboxView/Other/KeepScreenOn.vue
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="selfRef"
|
||||||
|
class="tool-detail-page"
|
||||||
|
:class="{
|
||||||
|
'has-radius': !fullscreen.isFullscreen.value,
|
||||||
|
'is-dark-color': isDarkColor,
|
||||||
|
'is-faded': isFaded,
|
||||||
|
'is-on': wakeLock.isActive.value,
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
'--bg-color': bgColor,
|
||||||
|
}"
|
||||||
|
@click="isFaded = false"
|
||||||
|
>
|
||||||
|
<div class="controls">
|
||||||
|
|
||||||
|
<div class="title">开关 / Switch</div>
|
||||||
|
|
||||||
|
<n-switch
|
||||||
|
:value="wakeLock.isActive.value"
|
||||||
|
size="medium"
|
||||||
|
:round="false"
|
||||||
|
@update:value="handleToggleWakeLock"
|
||||||
|
>
|
||||||
|
<template #checked>开启</template>
|
||||||
|
<template #unchecked>关闭</template>
|
||||||
|
</n-switch>
|
||||||
|
|
||||||
|
<div class="title">背景颜色 / Background Color</div>
|
||||||
|
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="bgColor"
|
||||||
|
size="medium"
|
||||||
|
:modes="['hex']"
|
||||||
|
:show-preview="true"
|
||||||
|
:swatches="[
|
||||||
|
'#000000',
|
||||||
|
'#252525',
|
||||||
|
'#505050',
|
||||||
|
'#808080',
|
||||||
|
'#FFFFFF',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="fullscreen.isSupported"
|
||||||
|
class="title"
|
||||||
|
>切换全屏 / Toggle Fullscreen</div>
|
||||||
|
|
||||||
|
<n-switch
|
||||||
|
v-show="fullscreen.isSupported"
|
||||||
|
size="medium"
|
||||||
|
:round="false"
|
||||||
|
:value="fullscreen.isFullscreen.value"
|
||||||
|
@update:value="handleToggleFullscreen"
|
||||||
|
>
|
||||||
|
<template #checked>开启</template>
|
||||||
|
<template #unchecked>关闭</template>
|
||||||
|
</n-switch>
|
||||||
|
|
||||||
|
<div class="title">隐藏界面 / Hide UI</div>
|
||||||
|
|
||||||
|
<n-switch
|
||||||
|
v-model:value="isFaded"
|
||||||
|
size="medium"
|
||||||
|
:round="false"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<template #checked>隐藏</template>
|
||||||
|
<template #unchecked>显示</template>
|
||||||
|
</n-switch>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NColorPicker, NSwitch,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed, ref, onBeforeUnmount,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useFullscreen, useWakeLock,
|
||||||
|
} from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$message,
|
||||||
|
} from '@/assets/js/naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
colorHexToGrayLevel,
|
||||||
|
} from '@/assets/js/utils';
|
||||||
|
|
||||||
|
/** 背景颜色 */
|
||||||
|
const bgColor = ref('#505050');
|
||||||
|
|
||||||
|
/** 是否为深色 */
|
||||||
|
const isDarkColor = computed(() => {
|
||||||
|
|
||||||
|
let color = bgColor.value;
|
||||||
|
let level = colorHexToGrayLevel(color);
|
||||||
|
|
||||||
|
return level < 192;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 是否隐藏内容 */
|
||||||
|
const isFaded = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 自身元素 ref
|
||||||
|
* @type {VueRef<HTMLElement>}
|
||||||
|
*/
|
||||||
|
const selfRef = ref(null);
|
||||||
|
|
||||||
|
/** 全屏控制 */
|
||||||
|
const fullscreen = useFullscreen(selfRef, {
|
||||||
|
autoExit: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 唤醒锁 */
|
||||||
|
const wakeLock = useWakeLock();
|
||||||
|
|
||||||
|
/** 处理切换全屏 */
|
||||||
|
function handleToggleFullscreen() {
|
||||||
|
fullscreen.toggle().then(() => {
|
||||||
|
if (fullscreen.isFullscreen.value) {
|
||||||
|
$message.success('进入全屏');
|
||||||
|
} else {
|
||||||
|
$message.success('退出全屏');
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('切换全屏失败:',);
|
||||||
|
console.error(error);
|
||||||
|
$message.error('切换全屏失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理切换唤醒锁 */
|
||||||
|
function handleToggleWakeLock(isActive = false) {
|
||||||
|
|
||||||
|
if (!wakeLock.isSupported) {
|
||||||
|
$message.warning('当前浏览器不支持该功能');
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
return wakeLock.request('screen').then(() => {
|
||||||
|
$message.success('开启');
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('请求唤醒锁失败:',);
|
||||||
|
console.error(error);
|
||||||
|
$message.error('请求唤醒锁失败');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return wakeLock.release().then(() => {
|
||||||
|
$message.success('关闭');
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('释放唤醒锁失败:',);
|
||||||
|
console.error(error);
|
||||||
|
$message.error('释放唤醒锁失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (wakeLock.isActive.value) {
|
||||||
|
handleToggleWakeLock(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.tool-detail-page {
|
||||||
|
display: flex;
|
||||||
|
background-color: inherit;
|
||||||
|
transition: all 0.25s;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.is-faded > * {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-on {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
|
||||||
|
&.is-dark-color {
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin: auto;
|
||||||
|
padding-bottom: 48px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-color-picker) {
|
||||||
|
width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-color-picker-trigger__value) {
|
||||||
|
color: transparent !important;
|
||||||
|
}
|
||||||
|
</style>
|
103
src/views/ToolboxView/Other/OpenNewWindow.vue
Normal file
103
src/views/ToolboxView/Other/OpenNewWindow.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page">
|
||||||
|
|
||||||
|
<!-- 设置 -->
|
||||||
|
<n-card size="small" title="设置">
|
||||||
|
<n-form
|
||||||
|
class="form-no-feedback config-inputs"
|
||||||
|
label-align="left"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="auto"
|
||||||
|
>
|
||||||
|
|
||||||
|
<n-form-item label="目标链接:">
|
||||||
|
<n-input
|
||||||
|
v-model:value="data.url"
|
||||||
|
placeholder="请输入需要打开的 URL,需要包含协议部分(https://)"
|
||||||
|
type="text"
|
||||||
|
></n-input>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="窗口大小:">
|
||||||
|
<n-flex align="center">
|
||||||
|
<span>宽度</span>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="data.width"
|
||||||
|
:min="0"
|
||||||
|
:max="9999999"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
<span>高度</span>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="data.height"
|
||||||
|
:min="0"
|
||||||
|
:max="9999999"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
</n-flex>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<n-card size="small" title="操作">
|
||||||
|
<n-flex>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="openWindow"
|
||||||
|
>打开窗口</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NButton, NCard, NFlex,
|
||||||
|
NForm, NFormItem, NInput, NInputNumber,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reactive,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
/** 数据 */
|
||||||
|
const data = reactive({
|
||||||
|
url: '',
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 打开窗口 */
|
||||||
|
function openWindow() {
|
||||||
|
|
||||||
|
let link = data.url || 'https://github.com/Frost-ZX';
|
||||||
|
let width = data.width ?? 400;
|
||||||
|
let height = data.height ?? 300;
|
||||||
|
let features = `height=${height}, width=${width}, toolbar=no, menubar=no, scrollbars=yes, resizable=yes, location=yes, status=yes`;
|
||||||
|
|
||||||
|
window.open(link, '_blank', features);
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.config-inputs {
|
||||||
|
.n-input-number {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-flex {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-form-item-blank) {
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
9
src/views/ToolboxView/Other/RunJavaScript.vue
Normal file
9
src/views/ToolboxView/Other/RunJavaScript.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
@@ -1,18 +1,246 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="toolbox-view flex-col">
|
<div class="toolbox-view flex-col">
|
||||||
<div class="app-view-header">
|
<div class="app-view-header">
|
||||||
<span>{{ TOOLBOX_MODULE_TITLE }}</span>
|
|
||||||
|
<!-- 返回上一级 -->
|
||||||
|
<n-button
|
||||||
|
v-show="isToolDetail"
|
||||||
|
class="back-button"
|
||||||
|
:text="true"
|
||||||
|
@click="handleCloseTool"
|
||||||
|
>
|
||||||
|
<span class="mdi mdi-arrow-left"></span>
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<span>{{ routeTitle }}</span>
|
||||||
|
|
||||||
|
<!-- 占位 -->
|
||||||
|
<div class="placeholder"></div>
|
||||||
|
|
||||||
|
<!-- 新窗口打开 -->
|
||||||
|
<n-button
|
||||||
|
v-show="isToolDetail"
|
||||||
|
class="back-button"
|
||||||
|
:text="true"
|
||||||
|
@click="handleOpenNewWindow"
|
||||||
|
>
|
||||||
|
<span class="mdi mdi-open-in-new"></span>
|
||||||
|
</n-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="app-view-content is-transparent">
|
<div class="app-view-content is-transparent">
|
||||||
|
|
||||||
|
<!-- 工具列表 -->
|
||||||
|
<div class="tool-list">
|
||||||
|
<n-collapse
|
||||||
|
:default-expanded-names="toolList.map(item => item.id)"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 分类项 -->
|
||||||
|
<n-collapse-item
|
||||||
|
v-for="categoryItem in toolList"
|
||||||
|
v-show="categoryItem.enabled"
|
||||||
|
:key="categoryItem.id"
|
||||||
|
:name="categoryItem.id"
|
||||||
|
:title="categoryItem.title"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 工具项 -->
|
||||||
|
<div
|
||||||
|
v-for="toolItem in categoryItem.items"
|
||||||
|
v-show="toolItem.enabled"
|
||||||
|
:key="toolItem.id"
|
||||||
|
class="tool-item shadow-1"
|
||||||
|
@click="handleOpenTool(toolItem)"
|
||||||
|
>
|
||||||
|
<div class="item-header">
|
||||||
|
<n-tooltip placement="top-start" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<span :class="['item-icon', toolItem.iconClass || 'mdi mdi-package-variant-closed']"></span>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<div>创建:{{ toolItem.createdAt }}</div>
|
||||||
|
<div>更新:{{ toolItem.updatedAt }}</div>
|
||||||
|
<div>版本:{{ toolItem.version }}</div>
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="item-body">
|
||||||
|
<div class="item-title">{{ toolItem.title }}</div>
|
||||||
|
<n-ellipsis
|
||||||
|
class="item-desc"
|
||||||
|
:line-clamp="2"
|
||||||
|
:tooltip="{ placement: 'bottom-start' }"
|
||||||
|
>{{ toolItem.desc }}</n-ellipsis>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
</n-collapse>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具页面 -->
|
||||||
|
<div
|
||||||
|
v-show="isToolDetail"
|
||||||
|
class="tool-detail-wrapper"
|
||||||
|
>
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
TOOLBOX_MODULE_TITLE,
|
NButton, NCollapse, NCollapseItem, NEllipsis, NTooltip,
|
||||||
} from '@/config/modules';
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useRoute, useRouter,
|
||||||
|
} from 'vue-router';
|
||||||
|
|
||||||
|
import {
|
||||||
|
toolList,
|
||||||
|
} from '@/assets/js/toolbox-data';
|
||||||
|
|
||||||
|
/** 是否为工具页面 */
|
||||||
|
const isToolDetail = computed(() => {
|
||||||
|
return route.meta.isToolDetail;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 路由 */
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
/** 路由 */
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** 页面标题 */
|
||||||
|
const routeTitle = computed(() => {
|
||||||
|
return route.meta.title;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 关闭工具 */
|
||||||
|
function handleCloseTool() {
|
||||||
|
return router.push({
|
||||||
|
name: 'ToolboxView',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在新窗口中打开当前工具 */
|
||||||
|
function handleOpenNewWindow() {
|
||||||
|
|
||||||
|
let width = window.innerWidth ?? 400;
|
||||||
|
let height = window.innerHeight ?? 300;
|
||||||
|
let url = location.href;
|
||||||
|
let features = `height=${height}, width=${width}, toolbar=no, menubar=no, scrollbars=yes, resizable=yes, location=yes, status=yes`;
|
||||||
|
|
||||||
|
window.open(url, '_blank', features);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 打开工具
|
||||||
|
* @param {ToolboxItem} data
|
||||||
|
*/
|
||||||
|
function handleOpenTool(data) {
|
||||||
|
return router.push({
|
||||||
|
name: `Toolbox/${data.component}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
.back-button {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-window-button {
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-list {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-item {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 256px;
|
||||||
|
height: 132px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
background-color: #FFF;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: outline 0.25s;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
outline-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.item-desc) {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-detail-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #FFF;
|
||||||
|
|
||||||
|
:deep(.tool-detail-page) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&.has-radius {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .n-card {
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .n-card__content {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-no-feedback .n-form-item-feedback-wrapper,
|
||||||
|
.form-item-no-feedback .n-form-item-feedback-wrapper {
|
||||||
|
min-height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
47
types/web.d.ts
vendored
47
types/web.d.ts
vendored
@@ -4,7 +4,7 @@ import type {
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
RouteLocationRaw,
|
RouteLocationRaw, RouteRecordRaw,
|
||||||
} from 'vue-router';
|
} from 'vue-router';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -33,6 +33,40 @@ declare global {
|
|||||||
_key?: string;
|
_key?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 工具箱分类 */
|
||||||
|
interface ToolboxCategory {
|
||||||
|
/** 分类唯一 ID */
|
||||||
|
id: string;
|
||||||
|
/** 分类名称 */
|
||||||
|
title: string;
|
||||||
|
/** 是否启用 */
|
||||||
|
enabled: boolean;
|
||||||
|
/** 工具列表 */
|
||||||
|
items: ToolboxItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工具箱工具信息 */
|
||||||
|
interface ToolboxItem {
|
||||||
|
/** 工具唯一 ID */
|
||||||
|
id: string;
|
||||||
|
/** 工具名称 */
|
||||||
|
title: string;
|
||||||
|
/** 工具简介 */
|
||||||
|
desc: string;
|
||||||
|
/** 图标 */
|
||||||
|
iconClass: string;
|
||||||
|
/** 组件路径 */
|
||||||
|
component: string;
|
||||||
|
/** 创建日期 */
|
||||||
|
createdAt: string;
|
||||||
|
/** 更新日期 */
|
||||||
|
updatedAt: string;
|
||||||
|
/** 版本号 */
|
||||||
|
version: string;
|
||||||
|
/** 是否启用 */
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// window
|
// window
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
||||||
@@ -54,14 +88,25 @@ declare global {
|
|||||||
|
|
||||||
// Vue Router
|
// Vue Router
|
||||||
type VueRouteLocationRaw = RouteLocationRaw;
|
type VueRouteLocationRaw = RouteLocationRaw;
|
||||||
|
type VueRouteRecordRaw = RouteRecordRaw;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
|
|
||||||
|
/** 主界面侧边栏图标 class */
|
||||||
iconClass?: string;
|
iconClass?: string;
|
||||||
|
|
||||||
|
/** 是否为工具页面 */
|
||||||
|
isToolDetail?: boolean;
|
||||||
|
|
||||||
|
/** 是否在主界面侧边栏显示 */
|
||||||
showInAside?: boolean;
|
showInAside?: boolean;
|
||||||
|
|
||||||
|
/** 页面标题 */
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -14,9 +14,15 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
assetsInlineLimit: 0,
|
assetsInlineLimit: 0,
|
||||||
},
|
},
|
||||||
|
esbuild: {
|
||||||
|
supported: {
|
||||||
|
bigint: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
envPrefix: 'V_ENV_',
|
envPrefix: 'V_ENV_',
|
||||||
plugins: [
|
plugins: [
|
||||||
legacy({
|
legacy({
|
||||||
|
polyfills: false,
|
||||||
renderLegacyChunks: true,
|
renderLegacyChunks: true,
|
||||||
renderModernChunks: true,
|
renderModernChunks: true,
|
||||||
targets: ['defaults', 'not IE 11'],
|
targets: ['defaults', 'not IE 11'],
|
||||||
|
Reference in New Issue
Block a user