add the power operation confirm selection in WEB UI

This commit is contained in:
LingkongSky
2025-04-08 22:16:42 +08:00
parent 7163854185
commit f71c2d3f8d
6 changed files with 135 additions and 11 deletions

View File

@@ -173,7 +173,8 @@ const en = {
reset: 'Reset',
power: 'Power',
powerShort: 'Power (short click)',
powerLong: 'Power (long click)'
powerLong: 'Power (long click)',
powerConfirm: 'Are you sure you want to perform this operation?'
},
settings: {
title: 'Settings',
@@ -225,9 +226,14 @@ const en = {
tip: 'Enable the default swap partition(128M)'
},
mouseJiggler: {
title: "Mouse Jiggler",
title: 'Mouse Jiggler',
description: 'Enable the mouse jiggler',
tip: "Automatically jiggles the mouse when inactive for 60 seconds to prevent system sleep"
tip: 'Automatically jiggles the mouse when inactive for 60 seconds to prevent system sleep'
},
powerConfirm: {
title: 'Power Operation Reconfirmation',
description: 'Enable power operation reconfirmation',
tip: 'When operating the power, a double confirmation dialog will pop up to prevent accidental touches'
},
mdns: {
description: 'Enable mDNS discovery service',

View File

@@ -23,6 +23,8 @@ const zh = {
illegalUsername: '用户名中包含非法字符',
illegalPassword: '密码中包含非法字符',
forgetPassword: '忘记密码',
reboot: '重启',
rebootDesc: '你确定要重启NanoKVM吗',
ok: '确定',
cancel: '取消',
loginButtonText: '登录',
@@ -157,7 +159,8 @@ const zh = {
reset: '重启',
power: '电源',
powerShort: '电源(短按)',
powerLong: '电源(长按)'
powerLong: '电源(长按)',
powerConfirm: '你确定要执行此操作吗?'
},
settings: {
title: '设置',
@@ -213,6 +216,11 @@ const zh = {
description: '启用鼠标抖动',
tip: '在鼠标离开60s未操作时自动进行抖动以防止系统睡眠'
},
powerConfirm:{
title: '电源操作确认',
description: '启用电源操作二次确认',
tip: '在操作电源时弹出二次确认框以防误触'
},
mdns: {
description: '启用 mDNS 发现服务',
tip: '如果您未使用此功能,建议将其关闭'
@@ -221,7 +229,9 @@ const zh = {
disk: '虚拟U盘',
diskDesc: '在远程主机中挂载虚拟U盘',
network: '虚拟网卡',
networkDesc: '在远程主机中挂载虚拟网卡'
networkDesc: '在远程主机中挂载虚拟网卡',
okBtn: '是',
cancelBtn: '否'
},
tailscale: {
title: 'Tailscale',

View File

@@ -13,6 +13,7 @@ const SKIP_UPDATE_KEY = 'nano-kvm-check-update';
const KEYBOARD_LAYOUT_KEY = 'nano-kvm-keyboard-layout';
const SKIP_MODIFY_PASSWORD_KEY = 'nano-kvm-skip-modify-password';
const MENU_DISABLED_ITEMS_KEY = 'nano-kvm-menu-disabled-items';
const POWER_CONFIRM_KEY = 'nano-kvm-power-confirm';
type ItemWithExpiry = {
value: string;
@@ -165,3 +166,12 @@ export function getMenuDisabledItems(): string[] {
const value = localStorage.getItem(MENU_DISABLED_ITEMS_KEY);
return value ? JSON.parse(value) : [];
}
export function getPowerConfirm(): string | null {
return getWithExpiry(POWER_CONFIRM_KEY);
}
export function setPowerConfirm(value: boolean) {
const expiry = 365 * 24 * 60 * 60 * 1000;
setWithExpiry(POWER_CONFIRM_KEY, String(value), expiry);
}

View File

@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
import { Slider } from 'antd';
import { Popconfirm, Slider } from 'antd';
import clsx from 'clsx';
import { CirclePowerIcon, LoaderCircleIcon, PowerIcon, RotateCcwIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { getPowerConfirm } from '../../../../lib/localstorage'
import * as api from '@/api/vm';
import { MenuItem } from '@/components/menu-item.tsx';
@@ -13,10 +14,10 @@ export const Power = () => {
const [isLoading, setIsLoading] = useState(false);
const [powerDuration, setPowerDuration] = useState(8);
useEffect(() => {
getLed();
const interval = setInterval(getLed, 5000);
return () => {
clearInterval(interval);
};
@@ -33,7 +34,10 @@ export const Power = () => {
});
}
function click(button: 'reset' | 'power', duration?: number) {
function click(button: 'reset' | 'power',type: string, duration?: number) {
if(getPowerConfirm() == 'true' && type == 'first') return;
if (isLoading) return;
setIsLoading(true);
@@ -48,6 +52,8 @@ export const Power = () => {
setPowerDuration(value);
}
const icon = (
<div className={clsx('h-[18px] w-[18px]', isPowerOn ? 'text-green-600' : 'text-neutral-500')}>
{isLoading ? (
@@ -60,30 +66,63 @@ export const Power = () => {
const content = (
<div className="flex flex-col space-y-1">
<Popconfirm
placement="bottom"
title={t('power.reset')}
description={t('power.powerConfirm')}
okText={t('settings.device.okBtn')}
cancelText={t('settings.device.cancelBtn')}
onConfirm={() => click('reset', 'twice', 0.8)}
disabled={!(getPowerConfirm() == 'true')}
>
<div
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
onClick={() => click('reset', 0.8)}
onClick={() => click('reset', 'first', 0.8)}
>
<RotateCcwIcon size={16} />
<span>{t('power.reset')}</span>
</div>
</Popconfirm>
<Popconfirm
placement="bottom"
title={t('power.powerShort')}
description={t('power.powerConfirm')}
okText={t('settings.device.okBtn')}
cancelText={t('settings.device.cancelBtn')}
onConfirm={() => click('power', 'twice', 0.8)}
disabled={!(getPowerConfirm() == 'true')}
>
<div
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
onClick={() => click('power', 0.8)}
onClick={() => click('power', 'first', 0.8)}
>
<PowerIcon size={16} />
<span>{t('power.powerShort')}</span>
</div>
</Popconfirm>
<Popconfirm
placement="bottom"
title={t('power.powerLong')}
description={t('power.powerConfirm')}
okText={t('settings.device.okBtn')}
cancelText={t('settings.device.cancelBtn')}
onConfirm={() => click('power', 'twice')}
disabled={!(getPowerConfirm() == 'true')}
>
<div
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
onClick={() => click('power')}
onClick={() => click('power', 'first')}
>
<CirclePowerIcon size={16} />
<span>{t('power.powerLong')}</span>
<div className="flex h-full items-start text-xs text-neutral-500">{`${powerDuration}s`}</div>
</div>
</Popconfirm>
<div className="px-3">
<Slider
defaultValue={8}

View File

@@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';
import { Switch, Tooltip } from 'antd';
import { CircleAlertIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { setPowerConfirm, getPowerConfirm } from '../../../../../lib/localstorage';
export const PowerConfirm = () => {
const { t } = useTranslation();
const [isEnabled, setIsEnabled] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
const cookieValue = getPowerConfirm();
if (cookieValue === 'true') {
setIsEnabled(true);
}
setIsLoading(false);
}, []);
function update() {
if (isLoading) return;
setIsLoading(true);
const newValue = !isEnabled;
setPowerConfirm(newValue);
setIsLoading(false);
setIsEnabled(!isEnabled);
}
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex items-center space-x-2">
<span>{t('settings.device.powerConfirm.title')}</span>
<Tooltip
title={t('settings.device.powerConfirm.tip')}
className="cursor-pointer"
placement="bottom"
overlayStyle={{ maxWidth: '300px' }}
>
<CircleAlertIcon size={15} />
</Tooltip>
</div>
<span className="text-xs text-neutral-500">{t('settings.device.powerConfirm.description')}</span>
</div>
<Switch checked={isEnabled} loading={isLoading} onChange={update} />
</div>
);
};

View File

@@ -15,6 +15,7 @@ import { VirtualDevices } from './virtual-devices.tsx';
import { Wifi } from './wifi.tsx';
import { Swap } from './swap.tsx';
import { MouseJiggler } from './mouse-jiggler.tsx';
import { PowerConfirm } from './PowerConfirm.tsx';
export const Device = () => {
const { t } = useTranslation();
@@ -42,6 +43,7 @@ export const Device = () => {
<Swap />
<MouseJiggler />
{hidMode === 'normal' ? <VirtualDevices /> : <HidMode />}
<PowerConfirm />
</div>
<Divider />