mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 08:31:45 +08:00
feat: 完善 ATX 功能逻辑与控件样式
This commit is contained in:
@@ -549,6 +549,8 @@ export const atxApi = {
|
||||
initialized: boolean
|
||||
power_status: 'on' | 'off' | 'unknown'
|
||||
led_supported: boolean
|
||||
hdd_status: 'active' | 'inactive' | 'unknown'
|
||||
hdd_supported: boolean
|
||||
}>('/atx/status'),
|
||||
|
||||
power: (action: 'short' | 'long' | 'reset') =>
|
||||
|
||||
@@ -2,25 +2,23 @@
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Power, RotateCcw, CircleDot, Wifi, Send } from 'lucide-vue-next'
|
||||
import { Power, RotateCcw, CircleDot, Wifi, Send, HardDrive } from 'lucide-vue-next'
|
||||
import { atxApi } from '@/api'
|
||||
import { atxConfigApi } from '@/api/config'
|
||||
|
||||
type AtxAction = 'short' | 'long' | 'reset'
|
||||
|
||||
const minActionFeedbackMs = 800
|
||||
const actionDurations: Record<AtxAction, number> = {
|
||||
short: 500,
|
||||
long: 5000,
|
||||
reset: 500,
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
(e: 'powerShort'): void
|
||||
@@ -34,21 +32,23 @@ const { t } = useI18n()
|
||||
const activeTab = ref('atx')
|
||||
|
||||
const powerState = ref<'on' | 'off' | 'unknown'>('unknown')
|
||||
const hddState = ref<'active' | 'inactive' | 'unknown'>('unknown')
|
||||
let powerStateTimer: number | null = null
|
||||
// Decouple action data from dialog visibility to prevent race conditions
|
||||
const pendingAction = ref<'short' | 'long' | 'reset' | null>(null)
|
||||
const confirmDialogOpen = ref(false)
|
||||
let actionTimer: number | null = null
|
||||
|
||||
const wolMacAddress = ref('')
|
||||
const wolHistory = ref<string[]>([])
|
||||
const wolSending = ref(false)
|
||||
const wolLoadingHistory = ref(false)
|
||||
const activeAction = ref<AtxAction | null>(null)
|
||||
|
||||
const powerStateColor = computed(() => {
|
||||
const actionBusy = computed(() => activeAction.value !== null)
|
||||
|
||||
const powerStateIconColor = computed(() => {
|
||||
switch (powerState.value) {
|
||||
case 'on': return 'bg-green-500'
|
||||
case 'off': return 'bg-slate-400'
|
||||
default: return 'bg-yellow-500'
|
||||
case 'on': return 'text-green-600'
|
||||
case 'off': return 'text-slate-500'
|
||||
default: return 'text-yellow-600'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -60,39 +60,42 @@ const powerStateText = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function requestAction(action: 'short' | 'long' | 'reset') {
|
||||
pendingAction.value = action
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
const hddStateIconColor = computed(() => {
|
||||
switch (hddState.value) {
|
||||
case 'active': return 'text-sky-600'
|
||||
case 'inactive': return 'text-slate-500'
|
||||
default: return 'text-yellow-600'
|
||||
}
|
||||
})
|
||||
|
||||
function handleAction() {
|
||||
console.log('[AtxPopover] Confirming action:', pendingAction.value)
|
||||
if (pendingAction.value === 'short') emit('powerShort')
|
||||
else if (pendingAction.value === 'long') emit('powerLong')
|
||||
else if (pendingAction.value === 'reset') emit('reset')
|
||||
confirmDialogOpen.value = false
|
||||
setTimeout(() => {
|
||||
const hddStateText = computed(() => {
|
||||
switch (hddState.value) {
|
||||
case 'active': return t('atx.hddActive')
|
||||
case 'inactive': return t('atx.hddInactive')
|
||||
default: return t('atx.stateUnknown')
|
||||
}
|
||||
})
|
||||
|
||||
function handleAction(action: AtxAction) {
|
||||
if (actionBusy.value) return
|
||||
|
||||
console.log('[AtxPopover] Running action:', action)
|
||||
activeAction.value = action
|
||||
|
||||
if (action === 'short') emit('powerShort')
|
||||
else if (action === 'long') emit('powerLong')
|
||||
else emit('reset')
|
||||
|
||||
if (actionTimer !== null) {
|
||||
window.clearTimeout(actionTimer)
|
||||
}
|
||||
actionTimer = window.setTimeout(() => {
|
||||
activeAction.value = null
|
||||
actionTimer = null
|
||||
refreshPowerState().catch(() => {})
|
||||
}, 1200)
|
||||
}, Math.max(actionDurations[action], minActionFeedbackMs))
|
||||
}
|
||||
|
||||
const confirmTitle = computed(() => {
|
||||
switch (pendingAction.value) {
|
||||
case 'short': return t('atx.confirmShortTitle')
|
||||
case 'long': return t('atx.confirmLongTitle')
|
||||
case 'reset': return t('atx.confirmResetTitle')
|
||||
default: return ''
|
||||
}
|
||||
})
|
||||
|
||||
const confirmDescription = computed(() => {
|
||||
switch (pendingAction.value) {
|
||||
case 'short': return t('atx.confirmShortDesc')
|
||||
case 'long': return t('atx.confirmLongDesc')
|
||||
case 'reset': return t('atx.confirmResetDesc')
|
||||
default: return ''
|
||||
}
|
||||
})
|
||||
const isValidMac = computed(() => {
|
||||
const mac = wolMacAddress.value.trim()
|
||||
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^([0-9A-Fa-f]{12})$/
|
||||
@@ -142,8 +145,10 @@ async function refreshPowerState() {
|
||||
try {
|
||||
const state = await atxApi.status()
|
||||
powerState.value = state.power_status
|
||||
hddState.value = state.hdd_status
|
||||
} catch {
|
||||
powerState.value = 'unknown'
|
||||
hddState.value = 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +164,10 @@ onUnmounted(() => {
|
||||
window.clearInterval(powerStateTimer)
|
||||
powerStateTimer = null
|
||||
}
|
||||
if (actionTimer !== null) {
|
||||
window.clearTimeout(actionTimer)
|
||||
actionTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -173,70 +182,91 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3 space-y-3">
|
||||
<div class="p-2.5 space-y-2.5">
|
||||
<Tabs v-model="activeTab">
|
||||
<TabsList class="w-full grid grid-cols-2">
|
||||
<TabsTrigger value="atx" class="text-xs">
|
||||
<Power class="h-3.5 w-3.5 mr-1" />
|
||||
<TabsList class="h-8 w-full grid grid-cols-2">
|
||||
<TabsTrigger value="atx" class="h-7 text-xs">
|
||||
<Power class="h-3 w-3 mr-1" />
|
||||
{{ t('atx.title') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="wol" class="text-xs">
|
||||
<Wifi class="h-3.5 w-3.5 mr-1" />
|
||||
<TabsTrigger value="wol" class="h-7 text-xs">
|
||||
<Wifi class="h-3 w-3 mr-1" />
|
||||
WOL
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- ATX Tab -->
|
||||
<TabsContent value="atx" class="mt-3 space-y-3">
|
||||
<TabsContent value="atx" class="mt-2.5 space-y-2.5">
|
||||
<p class="text-xs text-muted-foreground">{{ t('atx.description') }}</p>
|
||||
|
||||
<!-- Power State -->
|
||||
<div class="flex items-center justify-between p-2 rounded-md bg-muted/50">
|
||||
<span class="text-xs text-muted-foreground">{{ t('atx.powerState') }}</span>
|
||||
<Badge variant="outline" class="gap-1.5 text-xs">
|
||||
<span :class="['h-2 w-2 rounded-full', powerStateColor]" />
|
||||
{{ powerStateText }}
|
||||
</Badge>
|
||||
<!-- Status -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5">
|
||||
<Power :class="['h-4 w-4 shrink-0', powerStateIconColor]" />
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.powerState') }}</p>
|
||||
<p class="mt-1 truncate text-xs font-medium leading-none">{{ powerStateText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5">
|
||||
<HardDrive :class="['h-4 w-4 shrink-0', hddStateIconColor]" />
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.hddState') }}</p>
|
||||
<p class="mt-1 truncate text-xs font-medium leading-none">{{ hddStateText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Power Actions -->
|
||||
<div class="space-y-1.5">
|
||||
<div class="space-y-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full justify-start gap-2 h-8 text-xs"
|
||||
@click="requestAction('short')"
|
||||
:disabled="actionBusy"
|
||||
:class="[
|
||||
'w-full justify-start gap-2 h-7 text-xs',
|
||||
activeAction === 'short' ? 'bg-muted text-muted-foreground' : '',
|
||||
]"
|
||||
@click="handleAction('short')"
|
||||
>
|
||||
<Power class="h-3.5 w-3.5" />
|
||||
<Power class="h-3 w-3" />
|
||||
{{ t('atx.shortPress') }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full justify-start gap-2 h-8 text-xs text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:hover:bg-orange-950"
|
||||
@click="requestAction('long')"
|
||||
:disabled="actionBusy"
|
||||
:class="[
|
||||
'w-full justify-start gap-2 h-7 text-xs text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:hover:bg-orange-950',
|
||||
activeAction === 'long' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
|
||||
]"
|
||||
@click="handleAction('long')"
|
||||
>
|
||||
<CircleDot class="h-3.5 w-3.5" />
|
||||
<CircleDot class="h-3 w-3" />
|
||||
{{ t('atx.longPress') }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full justify-start gap-2 h-8 text-xs text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950"
|
||||
@click="requestAction('reset')"
|
||||
:disabled="actionBusy"
|
||||
:class="[
|
||||
'w-full justify-start gap-2 h-7 text-xs text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950',
|
||||
activeAction === 'reset' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
|
||||
]"
|
||||
@click="handleAction('reset')"
|
||||
>
|
||||
<RotateCcw class="h-3.5 w-3.5" />
|
||||
<RotateCcw class="h-3 w-3" />
|
||||
{{ t('atx.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- WOL Tab -->
|
||||
<TabsContent value="wol" class="mt-3 space-y-3">
|
||||
<TabsContent value="wol" class="mt-2.5 space-y-2.5">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('atx.wolDescription') }}
|
||||
</p>
|
||||
@@ -287,18 +317,4 @@ watch(
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Dialog -->
|
||||
<AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ confirmTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{{ confirmDescription }}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleAction">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</template>
|
||||
|
||||
@@ -193,18 +193,15 @@ export default {
|
||||
title: 'Power Control',
|
||||
description: 'Control remote host power state',
|
||||
powerState: 'Power State',
|
||||
hddState: 'HDD Activity',
|
||||
stateOn: 'On',
|
||||
stateOff: 'Off',
|
||||
stateUnknown: 'Unknown',
|
||||
hddActive: 'Active',
|
||||
hddInactive: 'Inactive',
|
||||
shortPress: 'Power (Short)',
|
||||
longPress: 'Power (Long/Force Off)',
|
||||
reset: 'Reset',
|
||||
confirmShortTitle: 'Confirm Power Press',
|
||||
confirmShortDesc: 'This will simulate pressing the power button, same as a physical short press.',
|
||||
confirmLongTitle: 'Confirm Force Power Off',
|
||||
confirmLongDesc: 'This will force power off the host, which may cause data loss. Are you sure?',
|
||||
confirmResetTitle: 'Confirm Reset',
|
||||
confirmResetDesc: 'This will reset the host, which may cause unsaved data loss. Are you sure?',
|
||||
wol: 'Wake-on-LAN',
|
||||
wolDescription: 'Send Wake-on-LAN magic packet to power on a remote machine.',
|
||||
macAddress: 'MAC Address',
|
||||
@@ -548,7 +545,6 @@ export default {
|
||||
detecting: 'Detecting...',
|
||||
networkSettings: 'Network Settings',
|
||||
msdSettings: 'MSD Settings',
|
||||
atxSettings: 'ATX Settings',
|
||||
httpSettings: 'HTTP Settings',
|
||||
httpPort: 'HTTP Port',
|
||||
configureHttpPort: 'Configure HTTP server port',
|
||||
@@ -676,33 +672,22 @@ export default {
|
||||
disabled: 'Disabled',
|
||||
msdDesc: 'Mass Storage Device allows you to mount ISO images and virtual drives to the target machine. Use the MSD panel on the main page to manage images.',
|
||||
atxDesc: 'ATX power control allows you to remotely power on/off and reset the target machine. Use the ATX panel on the main page to control power.',
|
||||
atxSettingsDesc: 'Configure ATX power control hardware bindings',
|
||||
atxEnable: 'Enable ATX Control',
|
||||
atxEnableDesc: 'Enable remote control of power and reset buttons',
|
||||
atxPowerButton: 'Power Button',
|
||||
atxPowerButtonDesc: 'For power on (short press) and force off (long press)',
|
||||
atxResetButton: 'Reset Button',
|
||||
atxResetButtonDesc: 'For resetting the target machine',
|
||||
atxDriver: 'Driver Type',
|
||||
atxDriverNone: 'Disabled',
|
||||
atxDriverGpio: 'GPIO',
|
||||
atxDriverUsbRelay: 'USB LCUS HID Relay',
|
||||
atxDriverSerial: 'USB LCUS Serial Relay',
|
||||
atxDevice: 'Device',
|
||||
atxGpioChip: 'GPIO Chip',
|
||||
atxPin: 'GPIO Pin',
|
||||
atxChannel: 'Relay Channel',
|
||||
atxSharedSerialBaudHint: 'When Power and Reset share one serial relay device, baud rate is controlled by the first config',
|
||||
atxActiveLevel: 'Active Level',
|
||||
atxLevelHigh: 'Active High',
|
||||
atxLevelLow: 'Active Low',
|
||||
atxActiveLevel: 'Trigger Level',
|
||||
atxLevelHigh: 'High-level trigger',
|
||||
atxLevelLow: 'Low-level trigger',
|
||||
atxLedSensing: 'LED Status Sensing',
|
||||
atxLedSensingDesc: 'Detect host power LED to determine power state (optional)',
|
||||
atxLedEnable: 'Enable LED Sensing',
|
||||
atxLedEnableDesc: 'Read power LED status via GPIO',
|
||||
atxLedChip: 'GPIO Chip',
|
||||
atxLedPin: 'GPIO Pin',
|
||||
atxLedInverted: 'Invert Logic',
|
||||
atxLedInvertedDesc: 'GPIO is low when LED is on',
|
||||
atxHddSensing: 'HDD Activity Sensing',
|
||||
atxWolSettings: 'Wake-on-LAN Settings',
|
||||
atxWolSettingsDesc: 'Configure WOL magic packet sending options',
|
||||
atxWolInterface: 'Network Interface',
|
||||
|
||||
@@ -193,18 +193,15 @@ export default {
|
||||
title: '电源控制',
|
||||
description: '控制远程主机的电源状态',
|
||||
powerState: '电源状态',
|
||||
hddState: '硬盘活动',
|
||||
stateOn: '已开机',
|
||||
stateOff: '已关机',
|
||||
stateUnknown: '未知',
|
||||
hddActive: '活动',
|
||||
hddInactive: '空闲',
|
||||
shortPress: '短按电源',
|
||||
longPress: '长按电源 (强制关机)',
|
||||
reset: '重启',
|
||||
confirmShortTitle: '确认短按电源',
|
||||
confirmShortDesc: '这将模拟按下电源按钮,与物理短按电源键效果相同。',
|
||||
confirmLongTitle: '确认强制关机',
|
||||
confirmLongDesc: '这将强制关闭主机,可能导致数据丢失。确定继续吗?',
|
||||
confirmResetTitle: '确认重启',
|
||||
confirmResetDesc: '这将重启主机,可能导致未保存的数据丢失。确定继续吗?',
|
||||
wol: '网络唤醒',
|
||||
wolDescription: '发送 Wake-on-LAN 魔术包以远程开机。',
|
||||
macAddress: 'MAC 地址',
|
||||
@@ -547,7 +544,6 @@ export default {
|
||||
detecting: '探测中...',
|
||||
networkSettings: '网络设置',
|
||||
msdSettings: 'MSD 设置',
|
||||
atxSettings: 'ATX 设置',
|
||||
httpSettings: 'HTTP 设置',
|
||||
httpPort: 'HTTP 端口',
|
||||
configureHttpPort: '配置 HTTP 服务器端口',
|
||||
@@ -675,33 +671,22 @@ export default {
|
||||
disabled: '已禁用',
|
||||
msdDesc: '虚拟存储设备允许您将 ISO 镜像和虚拟驱动器挂载到目标机器。请在主页面的 MSD 面板中管理镜像。',
|
||||
atxDesc: 'ATX 电源控制允许您远程开关机和重启目标机器。请在主页面的 ATX 面板中控制电源。',
|
||||
atxSettingsDesc: '配置 ATX 电源控制硬件绑定',
|
||||
atxEnable: '启用 ATX 控制',
|
||||
atxEnableDesc: '启用后可以远程控制电源和重启按钮',
|
||||
atxPowerButton: '电源按钮',
|
||||
atxPowerButtonDesc: '用于开机(短按)和强制关机(长按)',
|
||||
atxResetButton: '重启按钮',
|
||||
atxResetButtonDesc: '用于重启目标机器',
|
||||
atxDriver: '驱动类型',
|
||||
atxDriverNone: '禁用',
|
||||
atxDriverGpio: 'GPIO',
|
||||
atxDriverUsbRelay: 'USB LCUS HID继电器',
|
||||
atxDriverSerial: 'USB LCUS 串口继电器',
|
||||
atxDevice: '设备',
|
||||
atxGpioChip: 'GPIO 芯片',
|
||||
atxPin: 'GPIO 引脚',
|
||||
atxChannel: '继电器通道',
|
||||
atxSharedSerialBaudHint: 'Power 与 Reset 使用同一串口继电器时,波特率由第一个配置统一控制',
|
||||
atxActiveLevel: '有效电平',
|
||||
atxLevelHigh: '高电平有效',
|
||||
atxLevelLow: '低电平有效',
|
||||
atxActiveLevel: '触发电平',
|
||||
atxLevelHigh: '高电平触发',
|
||||
atxLevelLow: '低电平触发',
|
||||
atxLedSensing: 'LED 状态检测',
|
||||
atxLedSensingDesc: '检测主机电源 LED 以确定电源状态(可选)',
|
||||
atxLedEnable: '启用 LED 检测',
|
||||
atxLedEnableDesc: '通过 GPIO 读取电源 LED 状态',
|
||||
atxLedChip: 'GPIO 芯片',
|
||||
atxLedPin: 'GPIO 引脚',
|
||||
atxLedInverted: '反转逻辑',
|
||||
atxLedInvertedDesc: 'LED 亮起时 GPIO 为低电平',
|
||||
atxHddSensing: 'HDD 活动检测',
|
||||
atxWolSettings: '网络唤醒设置',
|
||||
atxWolSettingsDesc: '配置 Wake-on-LAN 魔术包发送选项',
|
||||
atxWolInterface: '网络接口',
|
||||
|
||||
@@ -50,6 +50,7 @@ interface AtxState {
|
||||
backend: string
|
||||
initialized: boolean
|
||||
powerOn: boolean
|
||||
hddStatus: 'active' | 'inactive' | 'unknown'
|
||||
error: string | null
|
||||
}
|
||||
|
||||
@@ -121,6 +122,7 @@ export interface AtxDeviceInfo {
|
||||
backend: string
|
||||
initialized: boolean
|
||||
power_on: boolean
|
||||
hdd_status: 'active' | 'inactive' | 'unknown'
|
||||
error: string | null
|
||||
}
|
||||
|
||||
@@ -235,6 +237,7 @@ export const useSystemStore = defineStore('system', () => {
|
||||
backend: state.backend,
|
||||
initialized: state.initialized,
|
||||
powerOn: state.power_status === 'on',
|
||||
hddStatus: state.hdd_status,
|
||||
error: null,
|
||||
}
|
||||
return state
|
||||
@@ -376,6 +379,7 @@ export const useSystemStore = defineStore('system', () => {
|
||||
backend: data.atx.backend,
|
||||
initialized: data.atx.initialized,
|
||||
powerOn: data.atx.power_on,
|
||||
hddStatus: data.atx.hdd_status,
|
||||
error: data.atx.error,
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -94,26 +94,29 @@ export enum ActiveLevel {
|
||||
Low = "low",
|
||||
}
|
||||
|
||||
export interface AtxKeyConfig {
|
||||
driver: AtxDriverType;
|
||||
export interface AtxOutputBinding {
|
||||
enabled: boolean;
|
||||
device: string;
|
||||
pin: number;
|
||||
active_level: ActiveLevel;
|
||||
baud_rate: number;
|
||||
}
|
||||
|
||||
export interface AtxLedConfig {
|
||||
export interface AtxInputBinding {
|
||||
enabled: boolean;
|
||||
gpio_chip: string;
|
||||
gpio_pin: number;
|
||||
inverted: boolean;
|
||||
device: string;
|
||||
pin: number;
|
||||
active_level: ActiveLevel;
|
||||
}
|
||||
|
||||
export interface AtxConfig {
|
||||
enabled: boolean;
|
||||
power: AtxKeyConfig;
|
||||
reset: AtxKeyConfig;
|
||||
led: AtxLedConfig;
|
||||
driver: AtxDriverType;
|
||||
device: string;
|
||||
baud_rate: number;
|
||||
power: AtxOutputBinding;
|
||||
reset: AtxOutputBinding;
|
||||
led: AtxInputBinding;
|
||||
hdd: AtxInputBinding;
|
||||
wol_interface: string;
|
||||
}
|
||||
|
||||
@@ -287,32 +290,36 @@ export interface AppConfig {
|
||||
redfish: RedfishConfig;
|
||||
}
|
||||
|
||||
/** Update for a single ATX key configuration */
|
||||
export interface AtxKeyConfigUpdate {
|
||||
driver?: AtxDriverType;
|
||||
/** Update for a single ATX output binding */
|
||||
export interface AtxOutputBindingUpdate {
|
||||
enabled?: boolean;
|
||||
device?: string;
|
||||
baud_rate?: number;
|
||||
pin?: number;
|
||||
active_level?: ActiveLevel;
|
||||
}
|
||||
|
||||
/** Update for LED sensing configuration */
|
||||
export interface AtxLedConfigUpdate {
|
||||
/** Update for ATX GPIO input sensing */
|
||||
export interface AtxInputBindingUpdate {
|
||||
enabled?: boolean;
|
||||
gpio_chip?: string;
|
||||
gpio_pin?: number;
|
||||
inverted?: boolean;
|
||||
device?: string;
|
||||
pin?: number;
|
||||
active_level?: ActiveLevel;
|
||||
}
|
||||
|
||||
/** ATX configuration update request */
|
||||
export interface AtxConfigUpdate {
|
||||
enabled?: boolean;
|
||||
driver?: AtxDriverType;
|
||||
device?: string;
|
||||
baud_rate?: number;
|
||||
/** Power button configuration */
|
||||
power?: AtxKeyConfigUpdate;
|
||||
power?: AtxOutputBindingUpdate;
|
||||
/** Reset button configuration */
|
||||
reset?: AtxKeyConfigUpdate;
|
||||
reset?: AtxOutputBindingUpdate;
|
||||
/** LED sensing configuration */
|
||||
led?: AtxLedConfigUpdate;
|
||||
led?: AtxInputBindingUpdate;
|
||||
/** HDD activity sensing configuration */
|
||||
hdd?: AtxInputBindingUpdate;
|
||||
/** Network interface for WOL packets (empty = auto) */
|
||||
wol_interface?: string;
|
||||
}
|
||||
|
||||
@@ -1137,29 +1137,41 @@ watch(bindMode, (mode) => {
|
||||
|
||||
const atxConfig = ref({
|
||||
enabled: false,
|
||||
driver: 'none' as AtxDriverType,
|
||||
device: '',
|
||||
baud_rate: 9600,
|
||||
power: {
|
||||
driver: 'none' as AtxDriverType,
|
||||
enabled: false,
|
||||
device: '',
|
||||
pin: 1,
|
||||
active_level: 'high' as ActiveLevel,
|
||||
baud_rate: 9600,
|
||||
},
|
||||
reset: {
|
||||
driver: 'none' as AtxDriverType,
|
||||
enabled: false,
|
||||
device: '',
|
||||
pin: 1,
|
||||
active_level: 'high' as ActiveLevel,
|
||||
baud_rate: 9600,
|
||||
},
|
||||
led: {
|
||||
enabled: false,
|
||||
gpio_chip: '',
|
||||
gpio_pin: 0,
|
||||
inverted: false,
|
||||
device: '',
|
||||
pin: 0,
|
||||
active_level: 'high' as ActiveLevel,
|
||||
},
|
||||
hdd: {
|
||||
enabled: false,
|
||||
device: '',
|
||||
pin: 0,
|
||||
active_level: 'high' as ActiveLevel,
|
||||
},
|
||||
wol_interface: '',
|
||||
})
|
||||
|
||||
const atxSaving = ref(false)
|
||||
const atxSaved = ref(false)
|
||||
const wolSaving = ref(false)
|
||||
const wolSaved = ref(false)
|
||||
|
||||
const atxDevices = ref<AtxDevices>({
|
||||
gpio_chips: [],
|
||||
usb_relays: [],
|
||||
@@ -1183,17 +1195,6 @@ const atxDriverOptions = computed(() => {
|
||||
: options
|
||||
})
|
||||
|
||||
const isSharedAtxSerialRelay = computed(() => {
|
||||
const power = atxConfig.value.power
|
||||
const reset = atxConfig.value.reset
|
||||
return (
|
||||
power.driver === 'serial'
|
||||
&& reset.driver === 'serial'
|
||||
&& !!power.device.trim()
|
||||
&& power.device === reset.device
|
||||
)
|
||||
})
|
||||
|
||||
const availableBackends = ref<EncoderBackendInfo[]>([])
|
||||
|
||||
const selectedBackendFormats = computed(() => {
|
||||
@@ -1737,14 +1738,29 @@ async function loadAtxConfig() {
|
||||
const config = await configStore.refreshAtx()
|
||||
atxConfig.value = {
|
||||
enabled: config.enabled,
|
||||
power: { ...config.power },
|
||||
reset: { ...config.reset },
|
||||
led: { ...config.led },
|
||||
driver: config.enabled ? config.driver : 'none' as AtxDriverType,
|
||||
device: config.device || '',
|
||||
baud_rate: config.baud_rate || 9600,
|
||||
power: {
|
||||
...config.power,
|
||||
active_level: config.power.active_level || 'high',
|
||||
},
|
||||
reset: {
|
||||
...config.reset,
|
||||
active_level: config.reset.active_level || 'high',
|
||||
},
|
||||
led: {
|
||||
...config.led,
|
||||
active_level: config.led.active_level || 'high',
|
||||
},
|
||||
hdd: {
|
||||
...config.hdd,
|
||||
active_level: config.hdd.active_level || 'high',
|
||||
},
|
||||
wol_interface: config.wol_interface || '',
|
||||
}
|
||||
clearAtxSerialDeviceConflicts()
|
||||
normalizeAtxRelayChannels()
|
||||
syncSharedAtxSerialBaudRate()
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -1756,43 +1772,64 @@ async function loadAtxDevices() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAtxConfig() {
|
||||
loading.value = true
|
||||
saved.value = false
|
||||
async function saveAtxSettings() {
|
||||
atxSaving.value = true
|
||||
atxSaved.value = false
|
||||
try {
|
||||
normalizeAtxRelayChannels()
|
||||
syncSharedAtxSerialBaudRate()
|
||||
const isGpio = atxConfig.value.driver === 'gpio'
|
||||
const isRelay = ['usbrelay', 'serial'].includes(atxConfig.value.driver)
|
||||
await configStore.updateAtx({
|
||||
enabled: atxConfig.value.enabled,
|
||||
enabled: atxConfig.value.driver !== 'none',
|
||||
driver: atxConfig.value.driver,
|
||||
device: atxConfig.value.device || undefined,
|
||||
baud_rate: atxConfig.value.baud_rate,
|
||||
power: {
|
||||
driver: atxConfig.value.power.driver,
|
||||
enabled: isGpio ? !!atxConfig.value.power.device : isRelay,
|
||||
device: atxConfig.value.power.device || undefined,
|
||||
pin: atxConfig.value.power.pin,
|
||||
active_level: atxConfig.value.power.active_level,
|
||||
baud_rate: atxConfig.value.power.baud_rate,
|
||||
},
|
||||
reset: {
|
||||
driver: atxConfig.value.reset.driver,
|
||||
enabled: isGpio ? !!atxConfig.value.reset.device : isRelay,
|
||||
device: atxConfig.value.reset.device || undefined,
|
||||
pin: atxConfig.value.reset.pin,
|
||||
active_level: atxConfig.value.reset.active_level,
|
||||
baud_rate: isSharedAtxSerialRelay.value
|
||||
? atxConfig.value.power.baud_rate
|
||||
: atxConfig.value.reset.baud_rate,
|
||||
},
|
||||
led: {
|
||||
enabled: atxConfig.value.led.enabled,
|
||||
gpio_chip: atxConfig.value.led.gpio_chip || undefined,
|
||||
gpio_pin: atxConfig.value.led.gpio_pin,
|
||||
inverted: atxConfig.value.led.inverted,
|
||||
enabled: isGpio && !!atxConfig.value.led.device,
|
||||
device: atxConfig.value.led.device || undefined,
|
||||
pin: atxConfig.value.led.pin,
|
||||
active_level: atxConfig.value.led.active_level,
|
||||
},
|
||||
hdd: {
|
||||
enabled: isGpio && !!atxConfig.value.hdd.device,
|
||||
device: atxConfig.value.hdd.device || undefined,
|
||||
pin: atxConfig.value.hdd.pin,
|
||||
active_level: atxConfig.value.hdd.active_level,
|
||||
},
|
||||
wol_interface: atxConfig.value.wol_interface || undefined,
|
||||
})
|
||||
saved.value = true
|
||||
setTimeout(() => (saved.value = false), 2000)
|
||||
atxConfig.value.enabled = atxConfig.value.driver !== 'none'
|
||||
atxSaved.value = true
|
||||
setTimeout(() => (atxSaved.value = false), 2000)
|
||||
} catch {
|
||||
} finally {
|
||||
loading.value = false
|
||||
atxSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWolSettings() {
|
||||
wolSaving.value = true
|
||||
wolSaved.value = false
|
||||
try {
|
||||
await configStore.updateAtx({
|
||||
wol_interface: atxConfig.value.wol_interface || undefined,
|
||||
})
|
||||
wolSaved.value = true
|
||||
setTimeout(() => (wolSaved.value = false), 2000)
|
||||
} catch {
|
||||
} finally {
|
||||
wolSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1823,25 +1860,21 @@ function clearAtxSerialDeviceConflicts() {
|
||||
const reserved = ch9329ReservedSerialDevice.value
|
||||
if (!reserved) return
|
||||
|
||||
if (atxConfig.value.power.driver === 'serial' && atxConfig.value.power.device === reserved) {
|
||||
atxConfig.value.power.device = ''
|
||||
if (atxConfig.value.driver === 'serial' && atxConfig.value.device === reserved) {
|
||||
atxConfig.value.device = ''
|
||||
}
|
||||
if (atxConfig.value.reset.driver === 'serial' && atxConfig.value.reset.device === reserved) {
|
||||
atxConfig.value.reset.device = ''
|
||||
}
|
||||
}
|
||||
|
||||
function syncSharedAtxSerialBaudRate() {
|
||||
if (!isSharedAtxSerialRelay.value) return
|
||||
atxConfig.value.reset.baud_rate = atxConfig.value.power.baud_rate
|
||||
}
|
||||
|
||||
function normalizeAtxRelayChannels() {
|
||||
for (const key of [atxConfig.value.power, atxConfig.value.reset]) {
|
||||
if (['usbrelay', 'serial'].includes(key.driver) && key.pin < 1) {
|
||||
if (['usbrelay', 'serial'].includes(atxConfig.value.driver) && key.pin < 1) {
|
||||
key.pin = 1
|
||||
}
|
||||
}
|
||||
if (atxConfig.value.driver !== 'gpio') {
|
||||
atxConfig.value.led.enabled = false
|
||||
atxConfig.value.hdd.enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -1852,22 +1885,10 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [atxConfig.value.power.driver, atxConfig.value.reset.driver],
|
||||
() => atxConfig.value.driver,
|
||||
() => {
|
||||
normalizeAtxRelayChannels()
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [
|
||||
atxConfig.value.power.driver,
|
||||
atxConfig.value.power.device,
|
||||
atxConfig.value.power.baud_rate,
|
||||
atxConfig.value.reset.driver,
|
||||
atxConfig.value.reset.device,
|
||||
],
|
||||
() => {
|
||||
syncSharedAtxSerialBaudRate()
|
||||
clearAtxSerialDeviceConflicts()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3947,83 +3968,40 @@ watch(isWindows, () => {
|
||||
|
||||
<!-- ATX Section -->
|
||||
<div v-show="activeSection === 'atx'" class="space-y-6">
|
||||
<!-- Enable ATX -->
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
||||
<div class="space-y-1.5">
|
||||
<CardTitle>{{ t('settings.atxSettings') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.atxSettingsDesc') }}</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadAtxDevices">
|
||||
<RefreshCw class="h-4 w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-0.5">
|
||||
<Label for="atx-enabled">{{ t('settings.atxEnable') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.atxEnableDesc') }}</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="atx-enabled"
|
||||
v-model="atxConfig.enabled"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Power Button Config -->
|
||||
<Card v-if="atxConfig.enabled">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.atxPowerButton') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.atxPowerButtonDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="power-driver">{{ t('settings.atxDriver') }}</Label>
|
||||
<select id="power-driver" v-model="atxConfig.power.driver" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<CardContent class="space-y-4 pt-6">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
||||
<Label for="atx-driver" class="shrink-0">{{ t('settings.atxDriver') }}</Label>
|
||||
<select id="atx-driver" v-model="atxConfig.driver" class="h-9 w-full max-w-xs px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option v-for="option in atxDriverOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="power-device">{{ t('settings.atxDevice') }}</Label>
|
||||
<select id="power-device" v-model="atxConfig.power.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="atxConfig.power.driver === 'none'">
|
||||
<Button variant="ghost" size="icon" class="h-9 w-9 shrink-0" :aria-label="t('common.refresh')" @click="loadAtxDevices">
|
||||
<RefreshCw class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="['usbrelay', 'serial'].includes(atxConfig.driver)" class="grid gap-4 sm:grid-cols-2">
|
||||
<div v-if="['usbrelay', 'serial'].includes(atxConfig.driver)" class="space-y-2">
|
||||
<Label for="atx-device">{{ t('settings.atxDevice') }}</Label>
|
||||
<select id="atx-device" v-model="atxConfig.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="">{{ t('settings.selectDevice') }}</option>
|
||||
<option
|
||||
v-for="dev in getAtxDevicesForDriver(atxConfig.power.driver)"
|
||||
v-for="dev in getAtxDevicesForDriver(atxConfig.driver)"
|
||||
:key="dev"
|
||||
:value="dev"
|
||||
:disabled="atxConfig.power.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
||||
:disabled="atxConfig.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
||||
>
|
||||
{{ formatAtxDeviceLabel(atxConfig.power.driver, dev) }}
|
||||
{{ formatAtxDeviceLabel(atxConfig.driver, dev) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="power-pin">{{ ['usbrelay', 'serial'].includes(atxConfig.power.driver) ? t('settings.atxChannel') : t('settings.atxPin') }}</Label>
|
||||
<Input
|
||||
id="power-pin"
|
||||
type="number"
|
||||
v-model.number="atxConfig.power.pin"
|
||||
:min="['usbrelay', 'serial'].includes(atxConfig.power.driver) ? 1 : 0"
|
||||
:disabled="atxConfig.power.driver === 'none'"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="atxConfig.power.driver === 'gpio'" class="space-y-2">
|
||||
<Label for="power-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||
<select id="power-level" v-model="atxConfig.power.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="atxConfig.power.driver === 'serial'" class="space-y-2">
|
||||
<Label for="power-baudrate">{{ t('settings.baudRate') }}</Label>
|
||||
<select id="power-baudrate" v-model.number="atxConfig.power.baud_rate" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<div v-if="atxConfig.driver === 'serial'" class="space-y-2">
|
||||
<Label for="atx-baudrate">{{ t('settings.baudRate') }}</Label>
|
||||
<select id="atx-baudrate" v-model.number="atxConfig.baud_rate" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option :value="9600">9600</option>
|
||||
<option :value="19200">19200</option>
|
||||
<option :value="38400">38400</option>
|
||||
@@ -4032,128 +4010,138 @@ watch(isWindows, () => {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Reset Button Config -->
|
||||
<Card v-if="atxConfig.enabled">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.atxResetButton') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.atxResetButtonDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-driver">{{ t('settings.atxDriver') }}</Label>
|
||||
<select id="reset-driver" v-model="atxConfig.reset.driver" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option v-for="option in atxDriverOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-device">{{ t('settings.atxDevice') }}</Label>
|
||||
<select id="reset-device" v-model="atxConfig.reset.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="atxConfig.reset.driver === 'none'">
|
||||
<option value="">{{ t('settings.selectDevice') }}</option>
|
||||
<option
|
||||
v-for="dev in getAtxDevicesForDriver(atxConfig.reset.driver)"
|
||||
:key="dev"
|
||||
:value="dev"
|
||||
:disabled="atxConfig.reset.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
||||
>
|
||||
{{ formatAtxDeviceLabel(atxConfig.reset.driver, dev) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-pin">{{ ['usbrelay', 'serial'].includes(atxConfig.reset.driver) ? t('settings.atxChannel') : t('settings.atxPin') }}</Label>
|
||||
<Input
|
||||
id="reset-pin"
|
||||
type="number"
|
||||
v-model.number="atxConfig.reset.pin"
|
||||
:min="['usbrelay', 'serial'].includes(atxConfig.reset.driver) ? 1 : 0"
|
||||
:disabled="atxConfig.reset.driver === 'none'"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="atxConfig.reset.driver === 'gpio'" class="space-y-2">
|
||||
<Label for="reset-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||
<select id="reset-level" v-model="atxConfig.reset.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="atxConfig.reset.driver === 'serial'" class="space-y-2">
|
||||
<Label for="reset-baudrate">{{ t('settings.baudRate') }}</Label>
|
||||
<select
|
||||
id="reset-baudrate"
|
||||
v-model.number="atxConfig.reset.baud_rate"
|
||||
class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
:disabled="isSharedAtxSerialRelay"
|
||||
>
|
||||
<option :value="9600">9600</option>
|
||||
<option :value="19200">19200</option>
|
||||
<option :value="38400">38400</option>
|
||||
<option :value="57600">57600</option>
|
||||
<option :value="115200">115200</option>
|
||||
</select>
|
||||
<p v-if="isSharedAtxSerialRelay" class="text-xs text-muted-foreground">
|
||||
{{ t('settings.atxSharedSerialBaudHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- LED Sensing Config -->
|
||||
<Card v-if="atxConfig.enabled && !isWindows">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.atxLedSensing') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.atxLedSensingDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-0.5">
|
||||
<Label for="led-enabled">{{ t('settings.atxLedEnable') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.atxLedEnableDesc') }}</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="led-enabled"
|
||||
v-model="atxConfig.led.enabled"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="atxConfig.led.enabled">
|
||||
<template v-if="atxConfig.driver === 'gpio'">
|
||||
<Separator />
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="led-chip">{{ t('settings.atxLedChip') }}</Label>
|
||||
<select id="led-chip" v-model="atxConfig.led.gpio_chip" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="">{{ t('settings.selectDevice') }}</option>
|
||||
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
||||
</select>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3 rounded-md border p-3">
|
||||
<Label>{{ t('settings.atxPowerButton') }}</Label>
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="power-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||
<select id="power-device" v-model="atxConfig.power.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="">{{ t('settings.atxDriverNone') }}</option>
|
||||
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="power-pin">{{ t('settings.atxPin') }}</Label>
|
||||
<Input id="power-pin" type="number" v-model.number="atxConfig.power.pin" min="0" :disabled="!atxConfig.power.device" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="power-active-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||
<select id="power-active-level" v-model="atxConfig.power.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!atxConfig.power.device">
|
||||
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="led-pin">{{ t('settings.atxLedPin') }}</Label>
|
||||
<Input id="led-pin" type="number" v-model.number="atxConfig.led.gpio_pin" min="0" />
|
||||
|
||||
<div class="space-y-3 rounded-md border p-3">
|
||||
<Label>{{ t('settings.atxResetButton') }}</Label>
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||
<select id="reset-device" v-model="atxConfig.reset.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="">{{ t('settings.atxDriverNone') }}</option>
|
||||
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-pin">{{ t('settings.atxPin') }}</Label>
|
||||
<Input id="reset-pin" type="number" v-model.number="atxConfig.reset.pin" min="0" :disabled="!atxConfig.reset.device" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-active-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||
<select id="reset-active-level" v-model="atxConfig.reset.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!atxConfig.reset.device">
|
||||
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 rounded-md border p-3">
|
||||
<Label>{{ t('settings.atxLedSensing') }}</Label>
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="led-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||
<select id="led-device" v-model="atxConfig.led.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="">{{ t('settings.atxDriverNone') }}</option>
|
||||
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="led-pin">{{ t('settings.atxPin') }}</Label>
|
||||
<Input id="led-pin" type="number" v-model.number="atxConfig.led.pin" min="0" :disabled="!atxConfig.led.device" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="led-active-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||
<select id="led-active-level" v-model="atxConfig.led.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!atxConfig.led.device">
|
||||
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 rounded-md border p-3">
|
||||
<Label>{{ t('settings.atxHddSensing') }}</Label>
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="hdd-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||
<select id="hdd-device" v-model="atxConfig.hdd.device" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||
<option value="">{{ t('settings.atxDriverNone') }}</option>
|
||||
<option v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="hdd-pin">{{ t('settings.atxPin') }}</Label>
|
||||
<Input id="hdd-pin" type="number" v-model.number="atxConfig.hdd.pin" min="0" :disabled="!atxConfig.hdd.device" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="hdd-active-level">{{ t('settings.atxActiveLevel') }}</Label>
|
||||
<select id="hdd-active-level" v-model="atxConfig.hdd.active_level" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!atxConfig.hdd.device">
|
||||
<option value="high">{{ t('settings.atxLevelHigh') }}</option>
|
||||
<option value="low">{{ t('settings.atxLevelLow') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-0.5">
|
||||
<Label for="led-inverted">{{ t('settings.atxLedInverted') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.atxLedInvertedDesc') }}</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="['usbrelay', 'serial'].includes(atxConfig.driver)">
|
||||
<Separator />
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3 rounded-md border p-3">
|
||||
<Label>{{ t('settings.atxPowerButton') }}</Label>
|
||||
<div class="space-y-2">
|
||||
<Label for="power-channel">{{ t('settings.atxChannel') }}</Label>
|
||||
<Input id="power-channel" type="number" v-model.number="atxConfig.power.pin" min="1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 rounded-md border p-3">
|
||||
<Label>{{ t('settings.atxResetButton') }}</Label>
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-channel">{{ t('settings.atxChannel') }}</Label>
|
||||
<Input id="reset-channel" type="number" v-model.number="atxConfig.reset.pin" min="1" />
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="led-inverted"
|
||||
v-model="atxConfig.led.inverted"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="flex justify-end">
|
||||
<Button :disabled="atxSaving" @click="saveAtxSettings">
|
||||
<Loader2 v-if="atxSaving" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="atxSaved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ atxSaving ? t('actionbar.applying') : atxSaved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- WOL Config -->
|
||||
<Card v-if="atxConfig.enabled">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.atxWolSettings') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.atxWolSettingsDesc') }}</CardDescription>
|
||||
@@ -4170,11 +4158,9 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="flex justify-end">
|
||||
<Button :disabled="loading" @click="saveAtxConfig">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
<Button :disabled="wolSaving" @click="saveWolSettings">
|
||||
<Loader2 v-if="wolSaving" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="wolSaved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ wolSaving ? t('actionbar.applying') : wolSaved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user