mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-09-13 14:57:14 +08:00
refactor: dark toggle and usedict with fix dict can't persist (#47)
This commit is contained in:
65
src/components/ToggleDark/index.vue
Normal file
65
src/components/ToggleDark/index.vue
Normal 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>
|
@@ -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)
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@
|
||||
|
||||
<!-- 暗黑模式切换 -->
|
||||
<a-tooltip content="主题切换" position="bottom">
|
||||
<GiThemeBtn></GiThemeBtn>
|
||||
<ToggleDark />
|
||||
</a-tooltip>
|
||||
|
||||
<!-- 管理员账户 -->
|
||||
|
@@ -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,
|
||||
|
@@ -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
8
src/types/app.d.ts
vendored
@@ -29,4 +29,12 @@ declare namespace App {
|
||||
label: string
|
||||
value: AnimateType
|
||||
}
|
||||
|
||||
/** 字典项 */
|
||||
interface DictItem {
|
||||
disabled?: boolean
|
||||
extra?: string
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
1
src/types/components.d.ts
vendored
1
src/types/components.d.ts
vendored
@@ -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']
|
||||
|
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GiThemeBtn class="theme-btn" />
|
||||
<ToggleDark class="theme-btn" />
|
||||
<Background />
|
||||
</div>
|
||||
|
||||
|
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GiThemeBtn class="theme-btn" />
|
||||
<ToggleDark class="theme-btn" />
|
||||
<Background />
|
||||
</div>
|
||||
<div class="login h5">
|
||||
|
Reference in New Issue
Block a user