feat: 新增「通用设置 > 外观设置 > 主题模式」配置项 (#583)

This commit is contained in:
ayangweb
2025-07-23 21:16:07 +08:00
committed by GitHub
parent db542c3fe1
commit 55aea542df
9 changed files with 108 additions and 36 deletions

View File

@@ -14,6 +14,7 @@
"core:window:allow-set-ignore-cursor-events",
"core:window:allow-set-decorations",
"core:window:allow-set-position",
"core:window:allow-set-theme",
"custom-window:default",
"os:default",
"process:default",

View File

@@ -3,7 +3,7 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { error } from '@tauri-apps/plugin-log'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useEventListener } from '@vueuse/core'
import { ConfigProvider } from 'ant-design-vue'
import { ConfigProvider, theme } from 'ant-design-vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { isString } from 'es-toolkit'
import isURL from 'is-url'
@@ -29,6 +29,7 @@ const generalStore = useGeneralStore()
const shortcutStore = useShortcutStore()
const appWindow = getCurrentWebviewWindow()
const { isRestored, restoreState } = useWindowState()
const { darkAlgorithm, defaultAlgorithm } = theme
onMounted(async () => {
generateColorVars()
@@ -79,7 +80,12 @@ useEventListener('click', (event) => {
</script>
<template>
<ConfigProvider :locale="zhCN">
<ConfigProvider
:locale="zhCN"
:theme="{
algorithm: generalStore.appearance.isDark ? darkAlgorithm : defaultAlgorithm,
}"
>
<RouterView v-if="isRestored" />
</ConfigProvider>
</template>

View File

@@ -18,7 +18,7 @@ const hasDescription = computed(() => {
<template>
<Flex
:align="vertical ? void 0 : 'center'"
class="b b-color-2 rounded-lg b-solid bg-white p-4"
class="b b-color-2 rounded-lg b-solid bg-color-3 p-4"
gap="middle"
justify="space-between"
:vertical="vertical"

View File

@@ -4,7 +4,7 @@ import { PhysicalSize } from '@tauri-apps/api/dpi'
import { Menu } from '@tauri-apps/api/menu'
import { sep } from '@tauri-apps/api/path'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { exists } from '@tauri-apps/plugin-fs'
import { exists, readDir } from '@tauri-apps/plugin-fs'
import { useDebounceFn, useEventListener } from '@vueuse/core'
import { nth } from 'es-toolkit/compat'
import { onMounted, onUnmounted, ref, watch } from 'vue'
@@ -17,7 +17,9 @@ import { hideWindow, setAlwaysOnTop, setTaskbarVisibility, showWindow } from '@/
import { useCatStore } from '@/stores/cat'
import { useGeneralStore } from '@/stores/general.ts'
import { useModelStore } from '@/stores/model'
import { isImage } from '@/utils/is'
import { join } from '@/utils/path'
import { clearObject } from '@/utils/shared'
const { startListening } = useDevice()
const appWindow = getCurrentWebviewWindow()
@@ -47,15 +49,32 @@ useEventListener('resize', () => {
})
watch(() => modelStore.currentModel, async (model) => {
handleLoad()
if (!model) return
handleLoad()
const path = join(model.path, 'resources', 'background.png')
const existed = await exists(path)
backgroundImagePath.value = existed ? convertFileSrc(path) : void 0
clearObject([modelStore.supportKeys, modelStore.pressedKeys])
const resourcePath = join(model.path, 'resources')
const groups = ['left-keys', 'right-keys']
for await (const groupName of groups) {
const groupDir = join(resourcePath, groupName)
const files = await readDir(groupDir).catch(() => [])
const imageFiles = files.filter(file => isImage(file.name))
for (const file of imageFiles) {
const fileName = file.name.split('.')[0]
modelStore.supportKeys[fileName] = join(groupDir, file.name)
}
}
}, { deep: true, immediate: true })
watch([() => catStore.scale, modelSize], async () => {

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { Select, SelectOption } from 'ant-design-vue'
import { onMounted, watch } from 'vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { useGeneralStore } from '@/stores/general'
const generalStore = useGeneralStore()
const appWindow = getCurrentWebviewWindow()
onMounted(() => {
appWindow.onThemeChanged(async ({ payload }) => {
if (generalStore.appearance.theme !== 'auto') return
generalStore.appearance.isDark = payload === 'dark'
})
})
watch(() => generalStore.appearance.theme, async (value) => {
let nextTheme = value === 'auto' ? null : value
await appWindow.setTheme(nextTheme)
nextTheme = nextTheme ?? (await appWindow.theme())
generalStore.appearance.isDark = nextTheme === 'dark'
}, { immediate: true })
watch(() => generalStore.appearance.isDark, (value) => {
if (value) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}, { immediate: true })
</script>
<template>
<ProListItem title="主题模式">
<Select v-model:value="generalStore.appearance.theme">
<SelectOption value="auto">
跟随系统
</SelectOption>
<SelectOption value="light">
亮色模式
</SelectOption>
<SelectOption value="dark">
暗色模式
</SelectOption>
</Select>
</ProListItem>
</template>

View File

@@ -4,6 +4,7 @@ import { Switch } from 'ant-design-vue'
import { watch } from 'vue'
import MacosPermissions from './components/macos-permissions/index.vue'
import ThemeMode from './components/theme-mode/index.vue'
import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
@@ -40,6 +41,10 @@ watch(() => generalStore.autostart, async (value) => {
</ProListItem>
</ProList>
<ProList title="外观设置">
<ThemeMode />
</ProList>
<ProList title="更新设置">
<ProListItem title="自动检查更新">
<Switch v-model:checked="generalStore.autoCheckUpdate" />

View File

@@ -53,7 +53,7 @@ const menus = [
<template>
<Flex class="h-screen">
<div
class="h-full w-30 flex flex-col items-center gap-4 overflow-auto bg-gradient-from-primary-1 bg-gradient-to-black/1 bg-gradient-linear"
class="h-full w-30 flex flex-col items-center gap-4 overflow-auto dark:(bg-color-3 bg-none) bg-gradient-from-primary-1 bg-gradient-to-black/1 bg-gradient-linear"
:class="[isMac ? 'pt-8' : 'pt-4']"
data-tauri-drag-region
>
@@ -73,8 +73,8 @@ const menus = [
<div
v-for="(item, index) in menus"
:key="item.label"
class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 text-color-3 transition"
:class="{ 'bg-white! text-primary-5 font-bold': current === index }"
class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 dark:text-color-2 text-color-3 transition"
:class="{ 'bg-color-2! text-primary-5 dark:text-primary-7 font-bold dark:bg-color-8!': current === index }"
@click="current = index"
>
<div
@@ -91,7 +91,7 @@ const menus = [
v-for="(item, index) in menus"
v-show="current === index"
:key="item.label"
class="flex-1 overflow-auto bg-color-8 p-4"
class="flex-1 overflow-auto bg-color-8 dark:bg-color-2 p-4"
data-tauri-drag-region
>
<component :is="item.component" />

View File

@@ -1,14 +1,26 @@
import type { Theme } from '@tauri-apps/api/window'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { reactive, ref } from 'vue'
interface Appearance {
theme: 'auto' | Theme
isDark: boolean
}
export const useGeneralStore = defineStore('general', () => {
const autoCheckUpdate = ref(false)
const autostart = ref(false)
const taskbarVisibility = ref(false)
const appearance = reactive<Appearance>({
theme: 'auto',
isDark: false,
})
return {
autoCheckUpdate,
autostart,
taskbarVisibility,
appearance,
}
})

View File

@@ -1,13 +1,10 @@
import { resolveResource } from '@tauri-apps/api/path'
import { readDir } from '@tauri-apps/plugin-fs'
import { filter, find } from 'es-toolkit/compat'
import { nanoid } from 'nanoid'
import { defineStore } from 'pinia'
import { reactive, ref, watch } from 'vue'
import { reactive, ref } from 'vue'
import { isImage } from '@/utils/is'
import { join } from '@/utils/path'
import { clearObject } from '@/utils/shared'
export type ModelMode = 'standard' | 'keyboard' | 'gamepad'
@@ -69,27 +66,6 @@ export const useModelStore = defineStore('model', () => {
models.value = nextModels
}
watch(currentModel, async (model) => {
if (!model) return
clearObject([supportKeys, pressedKeys])
const resourcePath = join(model.path, 'resources')
const groups = ['left-keys', 'right-keys']
for await (const groupName of groups) {
const groupDir = join(resourcePath, groupName)
const files = await readDir(groupDir).catch(() => [])
const imageFiles = files.filter(file => isImage(file.name))
for (const file of imageFiles) {
const fileName = file.name.split('.')[0]
supportKeys[fileName] = join(groupDir, file.name)
}
}
}, { deep: true, immediate: true })
return {
models,
currentModel,