mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-09-25 06:57:12 +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 { listCommonDict } from '@/apis'
|
||||||
import { useDictStore } from '@/stores'
|
import { useDictStore } from '@/stores'
|
||||||
|
|
||||||
const dictStore = useDictStore()
|
const pendingRequests = new Map<string, Promise<any>>()
|
||||||
const tmpCodeZone: string[] = []
|
|
||||||
export function useDict(...codes: Array<string>) {
|
export function useDict(...codes: string[]) {
|
||||||
const res = ref<any>({})
|
const dictStore = useDictStore()
|
||||||
return (() => {
|
const dictData = ref<Record<string, App.DictItem[]>>({})
|
||||||
codes.forEach((code) => {
|
|
||||||
res.value[code] = []
|
codes.forEach(async (code) => {
|
||||||
const dict = dictStore.getDict(code)
|
dictData.value[code] = []
|
||||||
if (dict) {
|
|
||||||
res.value[code] = dict
|
const cached = dictStore.getDict(code)
|
||||||
} else {
|
if (cached) {
|
||||||
if (!tmpCodeZone.includes(code)) {
|
dictData.value[code] = cached
|
||||||
// 防止多次触发
|
return
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pendingRequests.has(code)) {
|
||||||
|
const request = listCommonDict(code)
|
||||||
|
.then(({ data }) => {
|
||||||
|
dictStore.setDict(code, data)
|
||||||
|
return data
|
||||||
})
|
})
|
||||||
return toRefs(res.value)
|
.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(dictData.value)
|
||||||
}
|
}
|
||||||
|
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
<!-- 暗黑模式切换 -->
|
<!-- 暗黑模式切换 -->
|
||||||
<a-tooltip content="主题切换" position="bottom">
|
<a-tooltip content="主题切换" position="bottom">
|
||||||
<GiThemeBtn></GiThemeBtn>
|
<ToggleDark />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
||||||
<!-- 管理员账户 -->
|
<!-- 管理员账户 -->
|
||||||
|
@@ -1,37 +1,39 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { LabelValueState } from '@/types/global'
|
|
||||||
|
|
||||||
const storeSetup = () => {
|
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) {
|
if (code) {
|
||||||
dictData.value.set(code, items)
|
dictData.value[code] = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取字典
|
// 获取字典
|
||||||
const getDict = (code: string) => {
|
const getDict = (code: string) => {
|
||||||
if (!code) return null
|
if (!code) {
|
||||||
return dictData.value.get(code) || null
|
return null
|
||||||
|
}
|
||||||
|
return dictData.value[code] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除字典
|
// 删除字典
|
||||||
const deleteDict = (code: string) => {
|
const deleteDict = (code: string) => {
|
||||||
try {
|
if (!code || !(code in dictData.value)) {
|
||||||
return dictData.value.delete(code)
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
delete dictData.value[code]
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空字典
|
// 清空字典
|
||||||
const cleanDict = () => {
|
const cleanDict = () => {
|
||||||
dictData.value.clear()
|
dictData.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
dictData,
|
||||||
setDict,
|
setDict,
|
||||||
getDict,
|
getDict,
|
||||||
deleteDict,
|
deleteDict,
|
||||||
|
@@ -501,3 +501,20 @@
|
|||||||
border-radius: 0 4px 4px 0;
|
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
|
label: string
|
||||||
value: AnimateType
|
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']
|
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
|
||||||
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
|
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
|
||||||
TextCopy: typeof import('./../components/TextCopy/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']
|
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
|
||||||
Verify: typeof import('./../components/Verify/index.vue')['default']
|
Verify: typeof import('./../components/Verify/index.vue')['default']
|
||||||
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']
|
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']
|
||||||
|
@@ -47,7 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiThemeBtn class="theme-btn" />
|
<ToggleDark class="theme-btn" />
|
||||||
<Background />
|
<Background />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiThemeBtn class="theme-btn" />
|
<ToggleDark class="theme-btn" />
|
||||||
<Background />
|
<Background />
|
||||||
</div>
|
</div>
|
||||||
<div class="login h5">
|
<div class="login h5">
|
||||||
|
Reference in New Issue
Block a user