Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
23d0b08242 | |||
744c054c1c | |||
2a76490656 | |||
53299b83b3 | |||
865ef1e383 | |||
0310eee39e | |||
fcda974626 | |||
81b714333a | |||
6516fe4905 | |||
e37d12a5f2 | |||
331e037714 | |||
ef95e5ce73 | |||
75f9985265 | |||
95d1352a2b | |||
0c9091f5e6 | |||
0a86b75454 |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,6 +1,23 @@
|
||||
# 更新日志
|
||||
|
||||
## V3.1.0
|
||||
## [3.1.2] - 2024-10-13
|
||||
|
||||
### Added
|
||||
|
||||
- `工具箱` 添加“保持亮屏”。
|
||||
- `工具箱` 添加“原神时钟”。
|
||||
|
||||
### Changed
|
||||
|
||||
- `工具箱` 调整页面路由生成逻辑,不跳过未启用的工具。
|
||||
|
||||
## [3.1.1] - 2024-09-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- `配置文件` 解决打包异常。
|
||||
|
||||
## [3.1.0] - 2024-09-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -11,6 +28,6 @@
|
||||
|
||||
- `功能` 修改页面跳转方式,支持返回。
|
||||
|
||||
## V3.0.0
|
||||
## [3.0.0] - 2024-09-01
|
||||
|
||||
重构新版本,支持“搜索”功能和“网址导航”功能。
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frost-navigation",
|
||||
"description": "Frost Navigation",
|
||||
"version": "3.1.0",
|
||||
"version": "3.1.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
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;
|
@@ -195,8 +195,30 @@ export const toolList = [
|
||||
{
|
||||
id: 'other-tools',
|
||||
title: '其他',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
items: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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',
|
||||
@@ -245,10 +267,10 @@ export function getToolboxRoutes() {
|
||||
toolList.forEach((categoryItem) => {
|
||||
categoryItem.items.forEach((toolItem) => {
|
||||
|
||||
// 跳过未启用的工具
|
||||
if (!toolItem.enabled) {
|
||||
return;
|
||||
}
|
||||
// // 跳过未启用的工具
|
||||
// if (!toolItem.enabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
routes.push({
|
||||
path: `/toolbox-view/${toolItem.id}`,
|
||||
|
@@ -4,6 +4,36 @@ import {
|
||||
description as appDesc,
|
||||
} from '@package-json';
|
||||
|
||||
/** 将十六进制颜色值转为灰度值 */
|
||||
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 };
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 更新页面标题
|
||||
* @param {string} title
|
||||
|
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>
|
728
src/views/ToolboxView/Other/GenshinImpactClock/ClockElement.vue
Normal file
728
src/views/ToolboxView/Other/GenshinImpactClock/ClockElement.vue
Normal file
@@ -0,0 +1,728 @@
|
||||
<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日';
|
||||
}
|
||||
|
||||
// 处理提示信息显示
|
||||
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>
|
@@ -192,6 +192,10 @@ function handleOpenTool(data) {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
&.has-radius {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
> .n-card:not(:first-child) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
@@ -14,9 +14,15 @@ export default defineConfig({
|
||||
build: {
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
esbuild: {
|
||||
supported: {
|
||||
bigint: true,
|
||||
},
|
||||
},
|
||||
envPrefix: 'V_ENV_',
|
||||
plugins: [
|
||||
legacy({
|
||||
polyfills: false,
|
||||
renderLegacyChunks: true,
|
||||
renderModernChunks: true,
|
||||
targets: ['defaults', 'not IE 11'],
|
||||
|
Reference in New Issue
Block a user