6 Commits

18 changed files with 1760 additions and 1256 deletions

View File

@@ -2,7 +2,15 @@
## 简介 ## 简介
一个多功能的网导航,绿色无广告。 一个使用 Vue 3 开发的网导航和工具箱,绿色无广告。
![](./screenshots/image_01.png)
![](./screenshots/image_02.png)
![](./screenshots/image_03.png)
![](./screenshots/image_04.png)
## 使用方法 ## 使用方法

View File

@@ -9,33 +9,34 @@
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
}, },
"packageManager": "pnpm@10.11.0",
"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": "^12.5.0", "@vueuse/core": "^13.3.0",
"axios": "^1.7.9", "axios": "^1.9.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"lunisolar": "^2.5.1", "lunisolar": "^2.5.2",
"mathjs": "^14.2.0", "mathjs": "^14.5.2",
"naive-ui": "^2.41.0", "naive-ui": "^2.41.0",
"radash": "^12.1.0", "radash": "^12.1.0",
"uuid": "^11.0.5", "uuid": "^11.1.0",
"vue": "^3.5.13", "vue": "^3.5.16",
"vue-router": "^4.5.0", "vue-router": "^4.5.1",
"zxing-wasm": "^2.0.1" "zxing-wasm": "^2.1.2"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node20": "^20.1.4", "@tsconfig/node20": "^20.1.5",
"@types/node": "^20.17.16", "@types/node": "^20.17.57",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vitejs/plugin-legacy": "^6.0.0", "@vitejs/plugin-legacy": "^6.1.1",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.4",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "~4.1.2",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"eslint": "^9.19.0", "eslint": "^9.28.0",
"eslint-plugin-vue": "^9.32.0", "eslint-plugin-vue": "^9.33.0",
"less": "^4.2.2", "less": "^4.3.0",
"vite": "^6.0.11" "vite": "^6.3.5"
} }
} }

2514
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
onlyBuiltDependencies:
- core-js
- esbuild

View File

@@ -1,2 +1,2 @@
zxing_full.wasm zxing_full.wasm
zxing-wasm v2.0.1 zxing-wasm v2.1.2

Binary file not shown.

BIN
screenshots/image_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
screenshots/image_02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
screenshots/image_03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
screenshots/image_04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -122,7 +122,8 @@ function initCssVars() {
'--color-green': 'var(--color-success)', '--color-green': 'var(--color-success)',
'--color-blue': 'var(--color-info)', '--color-blue': 'var(--color-info)',
'--color-orange': 'var(--color-warning)', '--color-orange': 'var(--color-warning)',
// 滚动条大小 // 元素大小
'--dialog-content-max-height': 'calc(100vh - 160px)',
'--scrollbar-size': '8px', '--scrollbar-size': '8px',
}; };
@@ -265,6 +266,11 @@ html {
// -- Naive UI -- // -- Naive UI --
.n-dialog-content--with-max-height {
max-height: var(--dialog-content-max-height);
overflow-y: auto;
}
.n-drawer--right-placement { .n-drawer--right-placement {
.n-drawer-body { .n-drawer-body {
height: 0; height: 0;

View File

@@ -3,19 +3,19 @@
import { useLocalStorage } from '@vueuse/core'; import { useLocalStorage } from '@vueuse/core';
/** 本地储存 key 前缀 */ /** 本地储存 key 前缀 */
const PREFIX = 'frost-navigation/'; export const KEY_PREFIX = 'frost-navigation/';
/** NavView 模块 */ /** NavView 模块 */
export const storeNavView = { export const storeNavView = {
/** 导航链接侧边栏折叠状态 */ /** 导航链接侧边栏折叠状态 */
isAsideCollapsed: useLocalStorage(PREFIX + 'nav-view/is-aside-collapsed', false), isAsideCollapsed: useLocalStorage(KEY_PREFIX + 'nav-view/isAsideCollapsed', false),
/** 导航链接当前选中分类 */ /** 导航链接当前选中分类 */
currentCategory: useLocalStorage(PREFIX + 'nav-view/current-category', ''), currentCategory: useLocalStorage(KEY_PREFIX + 'nav-view/currentCategory', ''),
/** 导航链接搜索类型 */ /** 导航链接搜索类型 */
searchType: useLocalStorage(PREFIX + 'nav-view/search-type', 'all'), searchType: useLocalStorage(KEY_PREFIX + 'nav-view/searchType', 'all'),
}; };
@@ -23,6 +23,6 @@ export const storeNavView = {
export const storeSearchView = { export const storeSearchView = {
/** 当前使用的搜索引擎名称 */ /** 当前使用的搜索引擎名称 */
searchEngineName: useLocalStorage(PREFIX + 'search-view/search-engine-name', '必应'), searchEngineName: useLocalStorage(KEY_PREFIX + 'search-view/searchEngineName', '必应'),
}; };

View File

@@ -0,0 +1,15 @@
/** 工具更新日志 */
export const CHANGE_LOGS = {
'json-formatter': [
'[2] - 2025-02-07\n优化“输出内容”显示样式解决内容较多时行号显示不全的问题。',
'[1] - 2025-02-04\n初始版本。',
],
'keep-screen-on': [
'[2] - 2024-10-13\n优化界面样式背景添加圆角。',
'[1] - 2024-10-11\n初始版本。',
],
'qrcode-reader-and-generator': [
'[2] - 2025-02-23\n支持生成二维码。\n支持解析剪贴板中的二维码图片。\n优化解析功能在二维码所在位置添加矩形标记。',
'[1] - 2025-02-21\n初始版本。',
],
};

View File

@@ -1,5 +1,7 @@
// 工具箱 // 工具箱
import { CHANGE_LOGS } from './toolbox-changelogs';
const MODULES = import.meta.glob('../../views/ToolboxView/**/*.vue'); const MODULES = import.meta.glob('../../views/ToolboxView/**/*.vue');
/** /**
@@ -84,6 +86,7 @@ export const toolList = [
updatedAt: '2025-02-23', updatedAt: '2025-02-23',
version: '2', version: '2',
enabled: true, enabled: true,
changelogs: CHANGE_LOGS['qrcode-reader-and-generator'],
}, },
{ {
id: 'convert-text-structure', id: 'convert-text-structure',
@@ -135,6 +138,7 @@ export const toolList = [
updatedAt: '2025-02-07', updatedAt: '2025-02-07',
version: '2', version: '2',
enabled: true, enabled: true,
changelogs: CHANGE_LOGS['json-formatter'],
}, },
], ],
}, },
@@ -273,6 +277,7 @@ export const toolList = [
updatedAt: '2024-10-13', updatedAt: '2024-10-13',
version: '2', version: '2',
enabled: true, enabled: true,
changelogs: CHANGE_LOGS['keep-screen-on'],
}, },
{ {
id: 'open-new-window', id: 'open-new-window',
@@ -307,6 +312,17 @@ export const toolList = [
version: '1', version: '1',
enabled: true, enabled: true,
}, },
{
id: 'visualized-working-hours',
component: 'Other/VisualizedWorkingHours/VisualizedWorkingHours',
title: '工作时间可视化',
iconClass: 'mdi mdi-clock-digital',
desc: '用趣味化的方式呈现工作收益与时间进度,让薪资进度和下班期待看得见。',
createdAt: '',
updatedAt: '',
version: '0',
enabled: false,
},
], ],
}, },
]; ];

View File

@@ -0,0 +1,243 @@
<template>
<div class="tool-detail-page">
<!-- 配置选项 -->
<n-card size="small" style="--color: #2196F3;">
<template #header>
<div class="card-title">
<span class="card-title__icon mdi mdi-cog-outline"></span>
<span class="card-title__label">配置选项</span>
</div>
</template>
<n-form
class="form-no-feedback config-inputs"
label-align="right"
label-placement="left"
label-width="auto"
>
<n-form-item label="日薪">
<n-input-number
v-model:value="configData.dailyWage.value"
:min="0"
:max="99999999"
:precision="2"
:step="1"
></n-input-number>
</n-form-item>
<n-form-item label="收入币种">
<n-input
v-model:value="configData.currencyOfIncome.value"
placeholder="用于显示,例如:¥"
type="text"
:maxlength="8"
></n-input>
</n-form-item>
<n-form-item label="工作时间">
<n-input-group>
<n-time-picker
v-model:formatted-value="configData.workTimeStart.value"
format="HH:mm"
/>
<n-time-picker
v-model:formatted-value="configData.workTimeStop.value"
format="HH:mm"
/>
</n-input-group>
</n-form-item>
</n-form>
</n-card>
<!-- 实时进度 -->
<n-card size="small" style="--color: #F44336;">
<template #header>
<div class="card-title">
<span class="card-title__icon mdi mdi-chart-line"></span>
<span class="card-title__label">实时进度</span>
</div>
</template>
<div class="progress-item">
<div class="progress-item__row">
<span>本月已赚</span>
<span>1000 </span>
</div>
<div class="progress-item__row">
<n-progress
color="var(--color)"
type="line"
:height="16"
:percentage="35"
:processing="true"
:show-indicator="false"
/>
</div>
<div class="progress-item__row">
<span>35%</span>
<span>剩余 20.5 </span>
</div>
</div>
<div class="progress-item">
<div class="progress-item__row">
<span>今日已赚</span>
<span>100 </span>
</div>
<div class="progress-item__row">
<n-progress
color="var(--color)"
type="line"
:height="16"
:percentage="60"
:processing="true"
:show-indicator="false"
/>
</div>
<div class="progress-item__row">
<span>60%</span>
<span>剩余 5.6 小时</span>
</div>
</div>
</n-card>
<!-- 下班冲刺 -->
<n-card size="small" style="--color: #4CAF50;">
<template #header>
<div class="card-title">
<span class="card-title__icon mdi mdi-clock-outline"></span>
<span class="card-title__label">下班冲刺</span>
</div>
</template>
<div class="time-info">
<div class="time-info__row">
<span>距离下班还剩</span>
</div>
<div class="time-info__row">
<span>05:30:20</span>
</div>
<div class="time-info__row">
<span>漫长的一天才过一半加油吧</span>
</div>
</div>
</n-card>
<!-- 获得成就 -->
<!-- <n-card size="small" style="--color: #FF9800;">
<template #header>
<div class="card-title">
<span class="card-title__icon mdi mdi-trophy-outline"></span>
<span class="card-title__label">获得成就</span>
</div>
</template>
</n-card> -->
</div>
</template>
<script setup>
import {
NButton, NCard, NForm, NFormItem,
NInput, NInputGroup, NInputNumber,
NProgress, NTimePicker,
} from 'naive-ui';
import {
onBeforeMount,
} from 'vue';
import {
configData, initData,
} from './data';
onBeforeMount(() => {
initData();
});
</script>
<style lang="less" scoped>
.card-title > span {
vertical-align: middle;
}
.card-title__icon {
color: var(--color, #252525);
font-size: 1.2em;
}
.card-title__label {
margin-left: 0.5em;
}
.config-inputs {
.n-form-item {
max-width: 480px;
}
:deep(.n-form-item-blank) > * {
width: 100%;
}
:deep(.n-input-group) > * {
flex-grow: 1;
width: 0;
}
}
.progress-item :not(:first-child) {
margin-top: 12px;
}
.progress-item__row {
display: flex;
align-items: center;
justify-content: space-between;
&:not(:first-child) {
margin-top: 4px;
}
&:first-child, &:last-child {
color: #505050;
}
&:first-child span:last-child {
color: var(--color);
font-size: 1.2em;
font-weight: bold;
}
&:last-child {
font-size: 0.8em;
}
}
.time-info {
padding: 16px 0;
background-color: var(--color-action);
}
.time-info__row {
text-align: center;
&:first-child, &:last-child {
color: #505050;
}
&:nth-child(1) {
font-size: 1em;
}
&:nth-child(2) {
font-size: 2em;
}
&:nth-child(3) {
font-size: 0.8em;
}
}
</style>

View File

@@ -0,0 +1,50 @@
import { useLocalStorage } from '@vueuse/core';
import { KEY_PREFIX } from '@/assets/js/local-storage';
/** 模块名称 */
const STORAGE_PREFIX = KEY_PREFIX + 'visualized-working-hours/';
/** 配置选项 */
export const configData = {
/** 收入币种 */
currencyOfIncome: useLocalStorage(STORAGE_PREFIX + 'currencyOfIncome', ''),
/** 日薪 */
dailyWage: useLocalStorage(STORAGE_PREFIX + 'dailyWage', 100),
/** 午休时长 */
lunchBreakDuration: useLocalStorage(STORAGE_PREFIX + 'lunchBreakDuration', 1),
/** 工作开始时间 */
workTimeStart: useLocalStorage(STORAGE_PREFIX + 'workTimeStart', ''),
/** 工作结束时间 */
workTimeStop: useLocalStorage(STORAGE_PREFIX + 'workTimeStop', ''),
};
/** 初始化数据 */
export function initData() {
let {
currencyOfIncome,
workTimeStart,
workTimeStop,
} = configData;
let timeRegExp = new RegExp(/^\d{2}:\d{2}$/);
if (!currencyOfIncome.value) {
currencyOfIncome.value = '¥';
}
if (!workTimeStart.value.match(timeRegExp)) {
workTimeStart.value = '09:00';
}
if (!workTimeStop.value.match(timeRegExp)) {
workTimeStop.value = '18:00';
}
}

View File

@@ -5,7 +5,7 @@
<!-- 返回上一级 --> <!-- 返回上一级 -->
<n-button <n-button
v-show="isToolDetail" v-show="isToolDetail"
class="back-button" class="header-button"
:text="true" :text="true"
@click="handleCloseTool" @click="handleCloseTool"
> >
@@ -18,10 +18,20 @@
<!-- 占位 --> <!-- 占位 -->
<div class="placeholder"></div> <div class="placeholder"></div>
<!-- 查看信息 -->
<n-button
v-show="isToolDetail"
class="header-button"
:text="true"
@click="showToolItemInfo"
>
<span class="mdi mdi-information-slab-circle-outline"></span>
</n-button>
<!-- 新窗口打开 --> <!-- 新窗口打开 -->
<n-button <n-button
v-show="isToolDetail" v-show="isToolDetail"
class="back-button" class="header-button"
:text="true" :text="true"
@click="handleOpenNewWindow" @click="handleOpenNewWindow"
> >
@@ -89,17 +99,47 @@
<router-view></router-view> <router-view></router-view>
</div> </div>
<!-- 工具信息 -->
<n-modal
v-model:show="toolInfo.show"
content-class="n-dialog-content--with-max-height"
preset="dialog"
title="工具信息"
:show-icon="false"
>
<template v-if="true">
<n-h4>版本信息</n-h4>
<n-ul>
<n-li>创建日期:{{ toolInfo.dateCreated }}</n-li>
<n-li>更新日期:{{ toolInfo.dateUpdated }}</n-li>
<n-li>当前版本:{{ toolInfo.currVersion }}</n-li>
</n-ul>
</template>
<template v-if="toolInfo.changelogs.length > 0">
<n-h4>更新日志</n-h4>
<n-ul>
<n-li
v-for="(text, index) in toolInfo.changelogs"
:key="index"
class="changelogs-row"
>{{ text }}</n-li>
</n-ul>
</template>
</n-modal>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { import {
NButton, NCollapse, NCollapseItem, NEllipsis, NTooltip, NButton, NCollapse, NCollapseItem,
NEllipsis, NModal, NTooltip,
NH4, NUl, NLi,
} from 'naive-ui'; } from 'naive-ui';
import { import {
computed, computed, reactive,
} from 'vue'; } from 'vue';
import { import {
@@ -112,7 +152,7 @@ import {
/** 是否为工具页面 */ /** 是否为工具页面 */
const isToolDetail = computed(() => { const isToolDetail = computed(() => {
return route.meta.isToolDetail; return Boolean(route.meta.isToolDetail);
}); });
/** 路由 */ /** 路由 */
@@ -126,6 +166,15 @@ const routeTitle = computed(() => {
return route.meta.title; return route.meta.title;
}); });
/** 工具信息 */
const toolInfo = reactive({
changelogs: [],
currVersion: '',
dateCreated: '',
dateUpdated: '',
show: false,
});
/** 关闭工具 */ /** 关闭工具 */
function handleCloseTool() { function handleCloseTool() {
return router.push({ return router.push({
@@ -154,10 +203,53 @@ function handleOpenTool(data) {
name: `Toolbox/${data.component}`, name: `Toolbox/${data.component}`,
}); });
} }
/** 查看当前工具信息 */
function showToolItemInfo() {
let routePath = route.path;
let toolIdMatch = routePath.match(/\/([^/]*)$/);
let toolIdStr = toolIdMatch ? toolIdMatch[1] : null;
let toolItem = null;
if (toolIdStr) {
for (let i = 0; i < toolList.length; i++) {
let category = toolList[i];
let list = category.items;
if (!category.enabled || !list) {
continue;
}
for (let j = 0; j < list.length; j++) {
let item = list[j];
if (item.id === toolIdStr) {
toolItem = item;
break;
}
}
if (toolItem) {
break;
}
}
}
if (toolItem) {
toolInfo.changelogs = toolItem.changelogs || [];
toolInfo.currVersion = toolItem.version || '';
toolInfo.dateCreated = toolItem.createdAt || '';
toolInfo.dateUpdated = toolItem.updatedAt || '';
toolInfo.show = true;
}
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.back-button { .header-button {
margin-right: 0.5em; margin-right: 0.5em;
font-size: 24px; font-size: 24px;
} }
@@ -243,4 +335,8 @@ function handleOpenTool(data) {
} }
} }
} }
.changelogs-row {
white-space: pre-wrap;
}
</style> </style>

2
types/web.d.ts vendored
View File

@@ -65,6 +65,8 @@ declare global {
version: string; version: string;
/** 是否启用 */ /** 是否启用 */
enabled: boolean; enabled: boolean;
/** 更新日志 */
changelogs: string[];
} }
// window // window