feat: 完善 ATX 功能逻辑与控件样式

This commit is contained in:
mofeng-git
2026-07-07 00:21:54 +08:00
parent f6e97a06f5
commit e60152d38b
21 changed files with 1058 additions and 836 deletions

View File

@@ -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>