🎨 Updated
This commit is contained in:
@@ -415,9 +415,10 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
// 开机启动配置相关方法
|
// 开机启动配置相关方法
|
||||||
setStartAtLogin: async (value: boolean) => {
|
setStartAtLogin: async (value: boolean) => {
|
||||||
await safeCall(async () => {
|
await safeCall(async () => {
|
||||||
// 先调用系统设置
|
// 先更新配置文件
|
||||||
|
await updateGeneralConfig('startAtLogin', value);
|
||||||
|
// 再调用系统设置API
|
||||||
await StartupService.SetEnabled(value);
|
await StartupService.SetEnabled(value);
|
||||||
state.config.general.startAtLogin = value;
|
|
||||||
}, 'config.startupFailed', 'config.startupSuccess');
|
}, 'config.startupFailed', 'config.startupSuccess');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -73,7 +73,7 @@ const goBackToEditor = async () => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.settings-container {
|
.settings-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: var(--settings-bg);
|
background-color: var(--settings-bg);
|
||||||
@@ -193,7 +193,7 @@ const goBackToEditor = async () => {
|
|||||||
.settings-content {
|
.settings-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 20px 20px 40px 20px;
|
padding: 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: var(--settings-bg);
|
background-color: var(--settings-bg);
|
||||||
}
|
}
|
||||||
|
@@ -75,7 +75,6 @@ const updateSystemTheme = async (event: Event) => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.settings-page {
|
.settings-page {
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
padding-bottom: 48px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-input {
|
.select-input {
|
||||||
|
@@ -263,7 +263,6 @@ const handleAutoSaveDelayChange = async (event: Event) => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.settings-page {
|
.settings-page {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
padding-bottom: 48px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.number-control {
|
.number-control {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {computed, onUnmounted, ref, watch} from 'vue';
|
import {computed, onUnmounted, ref} from 'vue';
|
||||||
import SettingSection from '../components/SettingSection.vue';
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
import SettingItem from '../components/SettingItem.vue';
|
import SettingItem from '../components/SettingItem.vue';
|
||||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||||
@@ -403,7 +403,6 @@ onUnmounted(() => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.settings-page {
|
.settings-page {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
padding-bottom: 48px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotkey-selector {
|
.hotkey-selector {
|
||||||
|
@@ -228,7 +228,6 @@ onMounted(async () => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.settings-page {
|
.settings-page {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
padding-bottom: 48px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-bindings-container {
|
.key-bindings-container {
|
||||||
|
@@ -89,7 +89,6 @@ const downloadUpdate = () => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.settings-page {
|
.settings-page {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
padding-bottom: 48px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-info {
|
.update-info {
|
||||||
|
@@ -34,7 +34,7 @@ export default defineConfig(({mode}: { mode: string }): object => {
|
|||||||
write: true,
|
write: true,
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
brotliSize: true,
|
brotliSize: true,
|
||||||
chunkSizeWarningLimit: 1500,
|
chunkSizeWarningLimit: 2000,
|
||||||
watch: null,
|
watch: null,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
|
@@ -43,11 +43,5 @@ func (s *StartupService) SetEnabled(enabled bool) error {
|
|||||||
if err := s.impl.SetEnabled(enabled); err != nil {
|
if err := s.impl.SetEnabled(enabled); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新配置文件
|
|
||||||
if s.configService != nil {
|
|
||||||
s.configService.Set("general.startAtLogin", enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ type WindowsStartupImpl struct {
|
|||||||
registryKey string
|
registryKey string
|
||||||
execPath string
|
execPath string
|
||||||
workingDir string
|
workingDir string
|
||||||
batchFile string
|
taskName string // 任务计划程序任务名
|
||||||
}
|
}
|
||||||
|
|
||||||
// newStartupImplementation 创建平台特定的开机启动实现
|
// newStartupImplementation 创建平台特定的开机启动实现
|
||||||
@@ -48,12 +49,88 @@ func (w *WindowsStartupImpl) Initialize() error {
|
|||||||
// 获取工作目录(可执行文件所在目录)
|
// 获取工作目录(可执行文件所在目录)
|
||||||
w.workingDir = filepath.Dir(w.execPath)
|
w.workingDir = filepath.Dir(w.execPath)
|
||||||
|
|
||||||
// 使用文件名作为注册表键名
|
// 使用文件名作为注册表键名和任务名
|
||||||
w.registryKey = strings.TrimSuffix(filepath.Base(w.execPath), filepath.Ext(w.execPath))
|
baseName := strings.TrimSuffix(filepath.Base(w.execPath), filepath.Ext(w.execPath))
|
||||||
|
w.registryKey = baseName
|
||||||
|
w.taskName = baseName + "_Startup"
|
||||||
|
|
||||||
// 批处理文件路径(放在临时目录)
|
return nil
|
||||||
tempDir := os.TempDir()
|
}
|
||||||
w.batchFile = filepath.Join(tempDir, w.registryKey+"_startup.bat")
|
|
||||||
|
// createTaskSchedulerEntry 创建任务计划程序条目
|
||||||
|
func (w *WindowsStartupImpl) createTaskSchedulerEntry() error {
|
||||||
|
// 创建任务计划程序条目的XML内容
|
||||||
|
taskXML := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-16"?>
|
||||||
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||||
|
<RegistrationInfo>
|
||||||
|
<Description>%s startup task</Description>
|
||||||
|
</RegistrationInfo>
|
||||||
|
<Triggers>
|
||||||
|
<LogonTrigger>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
</LogonTrigger>
|
||||||
|
</Triggers>
|
||||||
|
<Principals>
|
||||||
|
<Principal id="Author">
|
||||||
|
<LogonType>InteractiveToken</LogonType>
|
||||||
|
<RunLevel>HighestAvailable</RunLevel>
|
||||||
|
</Principal>
|
||||||
|
</Principals>
|
||||||
|
<Settings>
|
||||||
|
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||||
|
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||||
|
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||||
|
<AllowHardTerminate>true</AllowHardTerminate>
|
||||||
|
<StartWhenAvailable>false</StartWhenAvailable>
|
||||||
|
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||||
|
<IdleSettings>
|
||||||
|
<StopOnIdleEnd>true</StopOnIdleEnd>
|
||||||
|
<RestartOnIdle>false</RestartOnIdle>
|
||||||
|
</IdleSettings>
|
||||||
|
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
<Hidden>false</Hidden>
|
||||||
|
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||||
|
<WakeToRun>false</WakeToRun>
|
||||||
|
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
||||||
|
<Priority>7</Priority>
|
||||||
|
</Settings>
|
||||||
|
<Actions Context="Author">
|
||||||
|
<Exec>
|
||||||
|
<Command>%s</Command>
|
||||||
|
<WorkingDirectory>%s</WorkingDirectory>
|
||||||
|
</Exec>
|
||||||
|
</Actions>
|
||||||
|
</Task>`, w.taskName, w.execPath, w.workingDir)
|
||||||
|
|
||||||
|
// 创建临时XML文件
|
||||||
|
tempFile := filepath.Join(os.TempDir(), w.taskName+".xml")
|
||||||
|
if err := os.WriteFile(tempFile, []byte(taskXML), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to create task XML file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFile) // 清理临时文件
|
||||||
|
|
||||||
|
// 使用schtasks命令创建任务
|
||||||
|
cmd := exec.Command("schtasks", "/create", "/tn", w.taskName, "/xml", tempFile, "/f")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create scheduled task: %w, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteTaskSchedulerEntry 删除任务计划程序条目
|
||||||
|
func (w *WindowsStartupImpl) deleteTaskSchedulerEntry() error {
|
||||||
|
cmd := exec.Command("schtasks", "/delete", "/tn", w.taskName, "/f")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
// 如果任务不存在
|
||||||
|
if strings.Contains(string(output), "cannot find") || strings.Contains(string(output), "does not exist") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to delete scheduled task: %w, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -71,43 +148,44 @@ func (w *WindowsStartupImpl) openRegistryKey() (registry.Key, error) {
|
|||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createBatchFile 创建批处理文件
|
|
||||||
func (w *WindowsStartupImpl) createBatchFile() error {
|
|
||||||
// 批处理文件内容
|
|
||||||
batchContent := fmt.Sprintf(`@echo off
|
|
||||||
cd /d "%s"
|
|
||||||
start "" "%s"
|
|
||||||
`, w.workingDir, w.execPath)
|
|
||||||
|
|
||||||
// 写入批处理文件
|
|
||||||
if err := os.WriteFile(w.batchFile, []byte(batchContent), 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to create batch file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteBatchFile 删除批处理文件
|
|
||||||
func (w *WindowsStartupImpl) deleteBatchFile() {
|
|
||||||
if _, err := os.Stat(w.batchFile); err == nil {
|
|
||||||
os.Remove(w.batchFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildStartupCommand 构建启动命令
|
// buildStartupCommand 构建启动命令
|
||||||
func (w *WindowsStartupImpl) buildStartupCommand() (string, error) {
|
func (w *WindowsStartupImpl) buildStartupCommand() (string, error) {
|
||||||
// 尝试直接使用可执行文件路径
|
|
||||||
execPath := w.execPath
|
execPath := w.execPath
|
||||||
|
|
||||||
if strings.Contains(execPath, " ") {
|
if strings.Contains(execPath, " ") {
|
||||||
execPath = `"` + execPath + `"`
|
execPath = `"` + execPath + `"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首先尝试直接路径,如果有问题再使用批处理文件
|
|
||||||
return execPath, nil
|
return execPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetEnabled 设置开机启动状态
|
// SetEnabled 设置开机启动状态
|
||||||
func (w *WindowsStartupImpl) SetEnabled(enabled bool) error {
|
func (w *WindowsStartupImpl) SetEnabled(enabled bool) error {
|
||||||
|
if enabled {
|
||||||
|
// 优先使用任务计划程序方式,可以绕过UAC限制
|
||||||
|
if err := w.createTaskSchedulerEntry(); err != nil {
|
||||||
|
// 如果任务计划程序失败,回退到注册表方式
|
||||||
|
return w.setRegistryStartup(true)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// 删除任务计划程序条目
|
||||||
|
if err := w.deleteTaskSchedulerEntry(); err != nil {
|
||||||
|
w.logger.Error("Failed to delete scheduled task", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除注册表条目
|
||||||
|
if err := w.setRegistryStartup(false); err != nil {
|
||||||
|
w.logger.Error("Failed to remove registry startup entry", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setRegistryStartup 设置注册表启动项(备用方法)
|
||||||
|
func (w *WindowsStartupImpl) setRegistryStartup(enabled bool) error {
|
||||||
key, err := w.openRegistryKey()
|
key, err := w.openRegistryKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to access registry: %w", err)
|
return fmt.Errorf("failed to access registry: %w", err)
|
||||||
@@ -120,8 +198,6 @@ func (w *WindowsStartupImpl) SetEnabled(enabled bool) error {
|
|||||||
return fmt.Errorf("failed to build startup command: %w", err)
|
return fmt.Errorf("failed to build startup command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.logger.Info("Setting Windows startup", "command", startupCmd)
|
|
||||||
|
|
||||||
if err := key.SetStringValue(w.registryKey, startupCmd); err != nil {
|
if err := key.SetStringValue(w.registryKey, startupCmd); err != nil {
|
||||||
return fmt.Errorf("failed to set startup entry: %w", err)
|
return fmt.Errorf("failed to set startup entry: %w", err)
|
||||||
}
|
}
|
||||||
@@ -130,21 +206,15 @@ func (w *WindowsStartupImpl) SetEnabled(enabled bool) error {
|
|||||||
if value, _, err := key.GetStringValue(w.registryKey); err != nil {
|
if value, _, err := key.GetStringValue(w.registryKey); err != nil {
|
||||||
return fmt.Errorf("startup entry verification failed: %w", err)
|
return fmt.Errorf("startup entry verification failed: %w", err)
|
||||||
} else if value != startupCmd {
|
} else if value != startupCmd {
|
||||||
w.logger.Error("Startup command verification mismatch", "expected", startupCmd, "actual", value)
|
return fmt.Errorf("startup command verification failed: expected %s, got %s", startupCmd, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.logger.Info("Windows startup enabled successfully")
|
|
||||||
} else {
|
} else {
|
||||||
// 删除批处理文件(如果存在)
|
|
||||||
w.deleteBatchFile()
|
|
||||||
|
|
||||||
if err := key.DeleteValue(w.registryKey); err != nil {
|
if err := key.DeleteValue(w.registryKey); err != nil {
|
||||||
// 如果键不存在,这不是错误
|
|
||||||
if err != registry.ErrNotExist {
|
if err != registry.ErrNotExist {
|
||||||
return fmt.Errorf("failed to remove startup entry: %w", err)
|
return fmt.Errorf("failed to remove startup entry: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.logger.Info("Windows startup disabled successfully")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
Reference in New Issue
Block a user