refactor: dark toggle and usedict with fix dict can't persist (#47)

This commit is contained in:
ppxb
2025-01-16 20:41:30 +08:00
committed by GitHub
parent 33e0c61bb6
commit 1c743fb097
9 changed files with 143 additions and 42 deletions

View File

@@ -0,0 +1,65 @@
<template>
<a-button size="mini" class="gi_hover_btn" @click="handleToggleTheme">
<template #icon>
<icon-moon-fill v-if="appStore.theme === 'light'" :size="18" />
<icon-sun-fill v-else :size="18" />
</template>
</a-button>
</template>
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import { useAppStore } from '@/stores'
defineOptions({
name: 'ToggleDark',
})
const appStore = useAppStore()
const isDark = useDark({
onChanged(dark: boolean) {
document.documentElement.setAttribute('class', dark ? 'dark' : 'light')
appStore.toggleTheme(dark)
},
})
const toggleTheme = useToggle(isDark)
const isAppearanceTransition = typeof document !== 'undefined'
&& 'startViewTransition' in document
&& !window.matchMedia('(prefers-reduced-motion: reduce)').matches
const handleToggleTheme = (event?: MouseEvent) => {
if (!isAppearanceTransition || !event) {
return toggleTheme()
}
const { clientX: x, clientY: y } = event
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
const transition = (document as any).startViewTransition(toggleTheme)
transition.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
]
document.documentElement.animate(
{
clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
},
{
duration: 500,
easing: 'ease-in',
pseudoElement: isDark.value
? '::view-transition-old(root)'
: '::view-transition-new(root)',
},
)
})
}
</script>

View File

@@ -2,34 +2,42 @@ import { ref, toRefs } from 'vue'
import { listCommonDict } from '@/apis'
import { useDictStore } from '@/stores'
const dictStore = useDictStore()
const tmpCodeZone: string[] = []
export function useDict(...codes: Array<string>) {
const res = ref<any>({})
return (() => {
codes.forEach((code) => {
res.value[code] = []
const dict = dictStore.getDict(code)
if (dict) {
res.value[code] = dict
} else {
if (!tmpCodeZone.includes(code)) {
// 防止多次触发
tmpCodeZone.push(code)
listCommonDict(code).then((resp) => {
res.value[code] = resp.data
dictStore.setDict(code, res.value[code])
tmpCodeZone.splice(tmpCodeZone.indexOf(code), 1)
}).catch(() => {
tmpCodeZone.splice(tmpCodeZone.indexOf(code), 1)
})
} else {
res.value[code] = computed(() => {
return dictStore.getDict(code)
})
}
}
const pendingRequests = new Map<string, Promise<any>>()
export function useDict(...codes: string[]) {
const dictStore = useDictStore()
const dictData = ref<Record<string, App.DictItem[]>>({})
codes.forEach(async (code) => {
dictData.value[code] = []
const cached = dictStore.getDict(code)
if (cached) {
dictData.value[code] = cached
return
}
if (!pendingRequests.has(code)) {
const request = listCommonDict(code)
.then(({ data }) => {
dictStore.setDict(code, data)
return data
})
.catch((error) => {
console.error(`Failed to load dict: ${code}`, error)
return []
})
.finally(() => {
pendingRequests.delete(code)
})
pendingRequests.set(code, request)
}
pendingRequests.get(code)!.then((data) => {
dictData.value[code] = data
})
return toRefs(res.value)
})()
})
return toRefs(dictData.value)
}

View File

@@ -41,7 +41,7 @@
<!-- 暗黑模式切换 -->
<a-tooltip content="主题切换" position="bottom">
<GiThemeBtn></GiThemeBtn>
<ToggleDark />
</a-tooltip>
<!-- 管理员账户 -->

View File

@@ -1,37 +1,39 @@
import { defineStore } from 'pinia'
import type { LabelValueState } from '@/types/global'
const storeSetup = () => {
const dictData = ref(new Map<string, LabelValueState[]>())
const dictData = ref<Record<string, App.DictItem[]>>({})
// 设置字典
const setDict = (code: string, items: Array<LabelValueState>) => {
const setDict = (code: string, items: App.DictItem[]) => {
if (code) {
dictData.value.set(code, items)
dictData.value[code] = items
}
}
// 获取字典
const getDict = (code: string) => {
if (!code) return null
return dictData.value.get(code) || null
if (!code) {
return null
}
return dictData.value[code] || null
}
// 删除字典
const deleteDict = (code: string) => {
try {
return dictData.value.delete(code)
} catch (e) {
if (!code || !(code in dictData.value)) {
return false
}
delete dictData.value[code]
return true
}
// 清空字典
const cleanDict = () => {
dictData.value.clear()
dictData.value = {}
}
return {
dictData,
setDict,
getDict,
deleteDict,

View File

@@ -501,3 +501,20 @@
border-radius: 0 4px 4px 0;
}
}
// 禁用 view-transition 动画
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root),
.dark::view-transition-new(root) {
z-index: 1;
}
::view-transition-new(root),
.dark::view-transition-old(root) {
z-index: 9999;
}

8
src/types/app.d.ts vendored
View File

@@ -29,4 +29,12 @@ declare namespace App {
label: string
value: AnimateType
}
/** 字典项 */
interface DictItem {
disabled?: boolean
extra?: string
label: string
value: string
}
}

View File

@@ -58,6 +58,7 @@ declare module 'vue' {
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
ToggleDark: typeof import('./../components/ToggleDark/index.vue')['default']
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
Verify: typeof import('./../components/Verify/index.vue')['default']
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']

View File

@@ -47,7 +47,7 @@
</div>
</div>
<GiThemeBtn class="theme-btn" />
<ToggleDark class="theme-btn" />
<Background />
</div>

View File

@@ -36,7 +36,7 @@
</div>
</div>
<GiThemeBtn class="theme-btn" />
<ToggleDark class="theme-btn" />
<Background />
</div>
<div class="login h5">