wip: layout / theme / settings / components / i18n / config
parent
c117a90750
commit
e16214e2c6
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
const loadingClasses = [
|
||||
'left-0 top-0',
|
||||
'left-0 bottom-0 animate-delay-500',
|
||||
'right-0 top-0 animate-delay-1000',
|
||||
'right-0 bottom-0 animate-delay-1500',
|
||||
]
|
||||
|
||||
const title = computed(() => import.meta.env.VITE_APP_TITLE)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed-center flex-col">
|
||||
<LIcon icon="i-logo" size="240" />
|
||||
<div class="my-36px h-25px w-56px">
|
||||
<div class="relative h-full animate-spin">
|
||||
<div
|
||||
v-for="(item, index) in loadingClasses"
|
||||
:key="index"
|
||||
class="absolute h-16px w-16px animate-pulse rounded-8px bg-primary"
|
||||
:class="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-28px font-500 text-#646464">
|
||||
{{ title }}
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { localeList } from '~/modules/i18n/config'
|
||||
|
||||
defineOptions({
|
||||
name: 'LocalPicker',
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
// 是否显示语言文字
|
||||
showText: { type: Boolean, default: true },
|
||||
// 语言变更时是否刷新页面
|
||||
reload: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
let selectedKeys = $ref<string[]>([])
|
||||
const { getLocale, changeLocale } = useLocale()
|
||||
|
||||
const getLocaleText = computed(() => {
|
||||
const key = selectedKeys[0]
|
||||
if (!key) {
|
||||
return ''
|
||||
}
|
||||
return localeList.find(item => item.event === key)?.text
|
||||
})
|
||||
|
||||
const { renderIcon } = useIcon()
|
||||
const getLocaleList = computed(() => localeList.map(item => ({
|
||||
label: item.text,
|
||||
key: item.event,
|
||||
icon: () => renderIcon({ icon: item.icon }),
|
||||
})))
|
||||
|
||||
watchEffect(() => {
|
||||
selectedKeys = [unref(getLocale)]
|
||||
})
|
||||
|
||||
async function toggleLocale(lang: string) {
|
||||
await changeLocale(lang)
|
||||
selectedKeys = [lang]
|
||||
props.reload && location.reload()
|
||||
}
|
||||
|
||||
function handleMenuEvent(menuLocale: string) {
|
||||
if (unref(getLocale) === menuLocale) {
|
||||
return
|
||||
}
|
||||
toggleLocale(menuLocale)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-3!">
|
||||
<NDropdown
|
||||
trigger="click"
|
||||
:options="getLocaleList"
|
||||
@select="handleMenuEvent"
|
||||
>
|
||||
<div class="flex cursor-pointer items-center">
|
||||
<LIcon icon="carbon:translate" class="hover:cursor-pointer" />
|
||||
<span v-if="showText" class="ml-1!">{{ getLocaleText }}</span>
|
||||
</div>
|
||||
</NDropdown>
|
||||
</div>
|
||||
</template>
|
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div>222</div>
|
||||
</template>
|
@ -1,9 +0,0 @@
|
||||
// these APIs are auto-imported from @vueuse/core
|
||||
export const isDark = useDark({
|
||||
selector: 'html',
|
||||
attribute: 'theme-mode',
|
||||
valueDark: 'dark',
|
||||
valueLight: '',
|
||||
})
|
||||
export const toggleDark = useToggle(isDark)
|
||||
export const preferredDark = usePreferredDark()
|
@ -0,0 +1,26 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { i18n, loadLanguageAsync } from '~/modules/i18n'
|
||||
|
||||
export function useLocale() {
|
||||
const localeStore = useLocaleStore()
|
||||
const { setLocale } = localeStore
|
||||
const { getLocale } = storeToRefs(localeStore)
|
||||
|
||||
const changeLocale = async (locale: string) => {
|
||||
const currentLocale = unref(i18n.global.locale) as string
|
||||
if (locale === currentLocale) {
|
||||
return locale
|
||||
}
|
||||
|
||||
await loadLanguageAsync(locale)
|
||||
|
||||
setLocale(locale)
|
||||
|
||||
return locale
|
||||
}
|
||||
|
||||
return {
|
||||
getLocale,
|
||||
changeLocale,
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
import type { ShallowRef, UnwrapRef } from 'vue'
|
||||
import { containsProp, isEqual } from '~/utils'
|
||||
|
||||
export interface UsePromiseOption {
|
||||
/**
|
||||
* 是否立即执行
|
||||
* default: true
|
||||
*/
|
||||
immediate?: boolean
|
||||
/**
|
||||
* 参数变化时是否自动执行函数
|
||||
* default: false
|
||||
*/
|
||||
redo?: boolean
|
||||
/**
|
||||
* 仅当redo = true时生效, 防抖时间. 单位 ms
|
||||
* default: 0(ms)
|
||||
*/
|
||||
debounce?: number
|
||||
/**
|
||||
* 再次请求时是否忽略当前的loading状态(能否重复执行)
|
||||
* default: false
|
||||
*/
|
||||
ignoreLoading?: boolean
|
||||
/**
|
||||
* 函数执行失败时是否对外抛出异常
|
||||
* default: false
|
||||
*/
|
||||
throwOnFailed?: boolean
|
||||
}
|
||||
|
||||
export interface UsePromiseReturnType<T> {
|
||||
data: ShallowRef<T | null>
|
||||
loading: Ref<UnwrapRef<boolean>>
|
||||
handleFn: () => Promise<T | null>
|
||||
error: ShallowRef<unknown>
|
||||
finished: Ref<UnwrapRef<boolean>>
|
||||
}
|
||||
|
||||
function isUsePromiseOption(obj: object): obj is UsePromiseOption {
|
||||
return containsProp(
|
||||
obj,
|
||||
'immediate',
|
||||
'redo',
|
||||
'debounce',
|
||||
'ignoreLoading',
|
||||
'throwOnFailed',
|
||||
)
|
||||
}
|
||||
|
||||
export function usePromise<T = any>(
|
||||
fn: (...args: any[]) => Promise<T>,
|
||||
): UsePromiseReturnType<T>
|
||||
|
||||
export function usePromise<T = any>(
|
||||
fn: (...args: any[]) => Promise<T>,
|
||||
opt: UsePromiseOption,
|
||||
): UsePromiseReturnType<T>
|
||||
|
||||
export function usePromise<T = any>(
|
||||
fn: (...args: any[]) => Promise<T>,
|
||||
fnArgs: unknown,
|
||||
opt?: UsePromiseOption,
|
||||
): UsePromiseReturnType<T>
|
||||
|
||||
export function usePromise<T = any>(
|
||||
fn: (...args: any[]) => Promise<T>,
|
||||
...args: any[]
|
||||
): UsePromiseReturnType<T> {
|
||||
const data = shallowRef<T | null>(null)
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = shallowRef<unknown>(null)
|
||||
|
||||
// fn params
|
||||
const fnArgs = ref<unknown>()
|
||||
|
||||
// config
|
||||
let config: UsePromiseOption = {
|
||||
immediate: true,
|
||||
redo: false,
|
||||
debounce: 0,
|
||||
ignoreLoading: false,
|
||||
throwOnFailed: false,
|
||||
}
|
||||
|
||||
function handleFn(): Promise<T | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ignoreLoading, throwOnFailed } = config
|
||||
if (!ignoreLoading && loading.value) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
finished.value = false
|
||||
fn(undefined, unref(fnArgs))
|
||||
.then((res) => {
|
||||
data.value = res
|
||||
error.value = null
|
||||
return resolve(res)
|
||||
})
|
||||
.catch((e) => {
|
||||
data.value = null
|
||||
error.value = e
|
||||
if (throwOnFailed) {
|
||||
return reject(e)
|
||||
}
|
||||
return resolve(null)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 存在不在vue3 setup中执行.
|
||||
const scoped = effectScope()
|
||||
|
||||
scoped.run(() => {
|
||||
if (args.length > 0) {
|
||||
if (isUsePromiseOption(args[0])) {
|
||||
config = { ...config, ...args[0] }
|
||||
}
|
||||
else { fnArgs.value = args[0] }
|
||||
}
|
||||
|
||||
if (args.length > 1) {
|
||||
if (isUsePromiseOption(args[1])) {
|
||||
config = { ...config, ...args[1] }
|
||||
}
|
||||
}
|
||||
|
||||
const { debounce, immediate, redo } = config
|
||||
const debounceFn = useDebounceFn(() => {
|
||||
return handleFn()
|
||||
}, debounce)
|
||||
|
||||
if (immediate) {
|
||||
debounceFn().then()
|
||||
}
|
||||
|
||||
if (redo) {
|
||||
watch(
|
||||
fnArgs,
|
||||
(newArgs, oldArgs) => {
|
||||
if (!isEqual(newArgs, oldArgs)) {
|
||||
debounceFn().then()
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
tryOnBeforeUnmount(() => {
|
||||
scoped.stop()
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
finished,
|
||||
error,
|
||||
handleFn,
|
||||
}
|
||||
}
|
@ -1,66 +1,54 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { filterTree, getAllParentPath, hideFilter } from '~/utils'
|
||||
|
||||
export function useMenu() {
|
||||
const menuStore = useMenuStore()
|
||||
const userStore = useUserStore()
|
||||
const { resolve } = useRouter()
|
||||
const routeStore = useRouteStore()
|
||||
const { initMenu, menuListRef } = storeToRefs(routeStore)
|
||||
|
||||
/** 判断给定的route中是否具有给定perms列表的权限 */
|
||||
function hasPermission(route: RouteRecordRaw, perms: any[] = []) {
|
||||
if (!route.meta?.perm) {
|
||||
return true
|
||||
const getMenuList = async () => {
|
||||
if (!unref(initMenu)) {
|
||||
await routeStore.buildMenu()
|
||||
}
|
||||
|
||||
// 递归寻找子节点perm
|
||||
if (route.meta?.perm === true && route.children?.length) {
|
||||
return filterAsyncRoutes(route.children, perms).length
|
||||
}
|
||||
|
||||
// 否则直接通过perm进行判断
|
||||
return perms.includes(
|
||||
Array.isArray(route.meta?.perm)
|
||||
? route.meta.perm[0]?.perm
|
||||
: route.meta?.perm,
|
||||
)
|
||||
const menuList = unref(menuListRef)
|
||||
return filterTree(menuList, hideFilter)
|
||||
}
|
||||
|
||||
// 过滤掉所有perm不匹配的路由
|
||||
function filterAsyncRoutes(routes: RouteRecordRaw[], perms: any[]) {
|
||||
return routes.reduce((rs: RouteRecordRaw[], route) => {
|
||||
if (hasPermission(route, perms)) {
|
||||
rs.push({
|
||||
...route,
|
||||
children: route.children ? filterAsyncRoutes(route.children, perms) : [],
|
||||
})
|
||||
}
|
||||
return rs
|
||||
}, [])
|
||||
const getCurrentParentPath = async (currentPath: string) => {
|
||||
const menus = await getMenuList()
|
||||
const allParentPath = await getAllParentPath(menus, currentPath)
|
||||
return allParentPath?.[0]
|
||||
}
|
||||
|
||||
async function generateRoutes() {
|
||||
const perms = userStore.userInfo?.perms ?? ['role', 'role/post']
|
||||
menuStore.menuList = filterAsyncRoutes(getRoutes(), perms)
|
||||
const getShallowMenus = async () => {
|
||||
const menus = await getMenuList()
|
||||
const shallowMenuList = menus.map(item => ({
|
||||
...item,
|
||||
children: undefined,
|
||||
}))
|
||||
// if (isRoleMode()) {
|
||||
// const routes = router.getRoutes()
|
||||
// return shallowMenuList.filter(basicFilter(routes))
|
||||
// }
|
||||
return shallowMenuList
|
||||
}
|
||||
|
||||
function getMenuList(routes: RouteRecordRaw[]) {
|
||||
return routes.reduce((rs: RouteRecordRaw[], route) => {
|
||||
if (!route.meta?.hidden && !route.meta?.hideInMenu) {
|
||||
rs.push({
|
||||
...route,
|
||||
path: resolve(<string>route.redirect || route).path,
|
||||
children: route.children ? getMenuList(route.children) : [],
|
||||
})
|
||||
}
|
||||
return rs
|
||||
}, []).sort((a, b) => {
|
||||
return (a.meta?.order || 999) - (b.meta?.order || 999)
|
||||
})
|
||||
const getChildrenMenus = async (parentPath: string) => {
|
||||
const menus = await getMenuList()
|
||||
const parent = menus.find(item => item.path === parentPath)
|
||||
if (!parent || !parent.children || !!parent?.meta?.hideChildrenInMenu) {
|
||||
return [] as Menu[]
|
||||
}
|
||||
// if (isRoleMode()) {
|
||||
// const routes = router.getRoutes()
|
||||
// return filterTree(parent.children, basicFilter(routes))
|
||||
// }
|
||||
return parent.children
|
||||
}
|
||||
|
||||
const menuList = computed(() => getMenuList(menuStore.menuList))
|
||||
|
||||
return {
|
||||
menuList,
|
||||
generateRoutes,
|
||||
getMenuList,
|
||||
getShallowMenus,
|
||||
getChildrenMenus,
|
||||
getCurrentParentPath,
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import routes from '~pages'
|
||||
|
||||
/**
|
||||
* 过滤所有hidden的路由
|
||||
*/
|
||||
function filterHiddenRoutes(routes: RouteRecordRaw[]) {
|
||||
return routes.filter((i) => {
|
||||
if (i.children) {
|
||||
i.children = filterHiddenRoutes(i.children)
|
||||
}
|
||||
return !i.meta?.hidden
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取真实存在的路由
|
||||
*/
|
||||
export function getRoutes() {
|
||||
return filterHiddenRoutes(routes)
|
||||
}
|
@ -0,0 +1,343 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ThemeEnum } from '~/constants'
|
||||
import type { MaybeElementRef } from '~/utils'
|
||||
import { darken, generateColors, lighten, pickTextColorBasedOnBgColor, setCssVar, toggleClass } from '~/utils'
|
||||
|
||||
const HEADER_HEIGHT = '--header-height'
|
||||
const HEADER_BG_COLOR_VAR = '--header-background-color'
|
||||
const HEADER_TEXT_COLOR_VAR = '--header-text-color'
|
||||
const HEADER_ACTION_HOVER_BG_COLOR_VAR = '--header-action-hover-bg-color'
|
||||
|
||||
const ASIDE_WIDTH = '--aside-width'
|
||||
const ASIDE_DARK_BG_COLOR = '--aside-background-color'
|
||||
const ASIDE_TEXT_COLOR_VAR = '--aside-text-color'
|
||||
|
||||
const TRIGGER_BG_COLOR_VAR = '--trigger-background-color'
|
||||
|
||||
const TAB_BAR_HEIGHT = '--tab-bar-height'
|
||||
|
||||
const FOOTER_HEIGHT = '--footer-height'
|
||||
|
||||
const LIGHT_TEXT_COLOR = 'rgba(0,0,0,.85)'
|
||||
const DARK_TEXT_COLOR = '#fff'
|
||||
|
||||
export function createMediaPrefersColorSchemeListen() {
|
||||
const { setAppConfig } = useAppConfig()
|
||||
|
||||
// 监听系统主题变更
|
||||
useEventListener(
|
||||
window.matchMedia('(prefers-color-scheme: dark)'),
|
||||
'change',
|
||||
(e: MediaQueryListEvent) => setAppConfig({ theme: e.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT }),
|
||||
)
|
||||
}
|
||||
|
||||
function toggleGrayMode(val: boolean) {
|
||||
toggleClass(val, 'gray-mode', document.documentElement)
|
||||
}
|
||||
|
||||
function toggleColorWeak(val: boolean) {
|
||||
toggleClass(val, 'color-weak', document.documentElement)
|
||||
}
|
||||
|
||||
export function createGridLayoutListen(el: MaybeElementRef | null) {
|
||||
const {
|
||||
isTopMenu,
|
||||
sidebar,
|
||||
header,
|
||||
footer,
|
||||
tabTar,
|
||||
getCollapsedShowTitle,
|
||||
menu,
|
||||
isMixSidebar,
|
||||
} = useAppConfig()
|
||||
const asideWidth = useCssVar(ASIDE_WIDTH, el, {
|
||||
initialValue: `${unref(sidebar).width}px`,
|
||||
})
|
||||
const headerHeight = useCssVar(HEADER_HEIGHT, el, {
|
||||
initialValue: `${unref(header).height}px`,
|
||||
})
|
||||
const tabBarHeight = useCssVar(TAB_BAR_HEIGHT, el, {
|
||||
initialValue: `${unref(tabTar).height}px`,
|
||||
})
|
||||
const footerHeight = useCssVar(FOOTER_HEIGHT, el, {
|
||||
initialValue: `${unref(footer).height}px`,
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const getAsideWidth = () => {
|
||||
if (unref(isTopMenu) || !unref(sidebar).visible)
|
||||
return 0
|
||||
if (unref(getCollapsedShowTitle)) {
|
||||
return unref(menu).mixSideFixed && unref(isMixSidebar)
|
||||
? unref(sidebar).mixSidebarWidth + unref(menu).subMenuWidth
|
||||
: unref(sidebar).mixSidebarWidth
|
||||
}
|
||||
if (unref(sidebar).collapsed) {
|
||||
return unref(menu).mixSideFixed && unref(isMixSidebar)
|
||||
? unref(sidebar).collapsedWidth + unref(menu).subMenuWidth
|
||||
: unref(sidebar).collapsedWidth
|
||||
}
|
||||
return unref(sidebar).width
|
||||
}
|
||||
|
||||
const getHeaderHeight = () => {
|
||||
if (!unref(header).visible)
|
||||
return 0
|
||||
return unref(header).height
|
||||
}
|
||||
|
||||
const getTabBarHeight = () => {
|
||||
if (!unref(tabTar).visible)
|
||||
return 0
|
||||
return unref(tabTar).height
|
||||
}
|
||||
|
||||
const getFooterHeight = () => {
|
||||
if (!unref(footer).visible)
|
||||
return 0
|
||||
return unref(footer).height
|
||||
}
|
||||
asideWidth.value = `${getAsideWidth()}px`
|
||||
headerHeight.value = `${getHeaderHeight()}px`
|
||||
tabBarHeight.value = `${getTabBarHeight()}px`
|
||||
footerHeight.value = `${getFooterHeight()}px`
|
||||
})
|
||||
}
|
||||
|
||||
export function createThemeColorListen() {
|
||||
const {
|
||||
sidebar,
|
||||
header,
|
||||
grayMode,
|
||||
colorWeak,
|
||||
setAppConfig,
|
||||
} = useAppConfig()
|
||||
|
||||
const headerBgColor = useCssVar(HEADER_BG_COLOR_VAR, null, {
|
||||
initialValue: `${unref(header).bgColor}px`,
|
||||
})
|
||||
|
||||
const headerTextColor = useCssVar(
|
||||
HEADER_TEXT_COLOR_VAR,
|
||||
null,
|
||||
{
|
||||
initialValue: LIGHT_TEXT_COLOR,
|
||||
},
|
||||
)
|
||||
const headerActionHoverBgColor = useCssVar(
|
||||
HEADER_ACTION_HOVER_BG_COLOR_VAR,
|
||||
null,
|
||||
)
|
||||
|
||||
const sidebarBgColor = useCssVar(
|
||||
ASIDE_DARK_BG_COLOR,
|
||||
null,
|
||||
{
|
||||
initialValue: `${unref(sidebar).bgColor}px`,
|
||||
},
|
||||
)
|
||||
|
||||
const asideTextColor = useCssVar(
|
||||
ASIDE_TEXT_COLOR_VAR,
|
||||
null,
|
||||
{
|
||||
initialValue: LIGHT_TEXT_COLOR,
|
||||
},
|
||||
)
|
||||
const triggerBackgroundColor = useCssVar(
|
||||
TRIGGER_BG_COLOR_VAR,
|
||||
null,
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
headerBgColor.value = unref(header).bgColor
|
||||
headerTextColor.value = pickTextColorBasedOnBgColor(
|
||||
unref(header).bgColor,
|
||||
LIGHT_TEXT_COLOR,
|
||||
DARK_TEXT_COLOR,
|
||||
)
|
||||
|
||||
if (['#fff', '#ffffff'].includes(unref(header).bgColor.toLowerCase())) {
|
||||
headerActionHoverBgColor.value = darken(unref(header).bgColor, 6)
|
||||
setAppConfig({ header: { theme: ThemeEnum.LIGHT } })
|
||||
}
|
||||
else {
|
||||
headerActionHoverBgColor.value = lighten(unref(header).bgColor, 6)
|
||||
setAppConfig({ header: { theme: ThemeEnum.DARK } })
|
||||
}
|
||||
|
||||
sidebarBgColor.value = unref(sidebar).bgColor
|
||||
asideTextColor.value = pickTextColorBasedOnBgColor(
|
||||
unref(sidebar).bgColor,
|
||||
LIGHT_TEXT_COLOR,
|
||||
DARK_TEXT_COLOR,
|
||||
)
|
||||
|
||||
if (['#fff', '#ffffff'].includes(unref(sidebar).bgColor.toLowerCase())) {
|
||||
triggerBackgroundColor.value = darken(unref(sidebar).bgColor, 6)
|
||||
setAppConfig({ sidebar: { theme: ThemeEnum.LIGHT } })
|
||||
}
|
||||
else {
|
||||
triggerBackgroundColor.value = lighten(unref(sidebar).bgColor, 6)
|
||||
setAppConfig({ sidebar: { theme: ThemeEnum.DARK } })
|
||||
}
|
||||
|
||||
toggleGrayMode(unref(grayMode))
|
||||
toggleColorWeak(unref(colorWeak))
|
||||
// toggleClass(
|
||||
// ThemeEnum.DARK === unref(theme),
|
||||
// ThemeEnum.DARK,
|
||||
// document.documentElement,
|
||||
// )
|
||||
})
|
||||
}
|
||||
|
||||
export function useAppTheme() {
|
||||
const themeStore = useThemeStore()
|
||||
const { setThemeConfig, setSidebarTheme, setHeaderTheme } = themeStore
|
||||
const { themeConfig: getThemeConfig, theme, sidebar, header } = storeToRefs(themeStore)
|
||||
|
||||
const darkRef = useDark({
|
||||
selector: 'html',
|
||||
attribute: 'theme-mode',
|
||||
valueDark: 'dark',
|
||||
valueLight: '',
|
||||
})
|
||||
const toggleDark = useToggle(darkRef)
|
||||
|
||||
const isDark = computed(() => unref(darkRef) && unref(theme) === ThemeEnum.DARK)
|
||||
const toggleTheme = (dark: boolean) => {
|
||||
theme.value = dark ? ThemeEnum.DARK : ThemeEnum.LIGHT
|
||||
toggleDark(dark)
|
||||
}
|
||||
|
||||
// sidebar
|
||||
const isSidebarDark = computed(() => (unref(theme) === ThemeEnum.DARK || unref(sidebar) === ThemeEnum.DARK))
|
||||
const toggleSidebarTheme = (dark: boolean) => {
|
||||
setSidebarTheme(dark ? ThemeEnum.DARK : ThemeEnum.LIGHT)
|
||||
}
|
||||
|
||||
const isHeaderDark = computed(() => (unref(theme) === ThemeEnum.DARK || unref(header) === ThemeEnum.DARK))
|
||||
const toggleHeaderTheme = (dark: boolean) => {
|
||||
setHeaderTheme(dark ? ThemeEnum.DARK : ThemeEnum.LIGHT)
|
||||
}
|
||||
|
||||
const primaryColor = computed(() => {
|
||||
return getThemeConfig.value.primaryColor
|
||||
})
|
||||
|
||||
const infoColor = computed(() => {
|
||||
return getThemeConfig.value.infoColor
|
||||
})
|
||||
|
||||
const successColor = computed(() => {
|
||||
return getThemeConfig.value.successColor
|
||||
})
|
||||
|
||||
const warningColor = computed(() => {
|
||||
return getThemeConfig.value.warningColor
|
||||
})
|
||||
|
||||
const errorColor = computed(() => {
|
||||
return getThemeConfig.value.errorColor
|
||||
})
|
||||
|
||||
const themeColors = computed(() => {
|
||||
let colors: ThemeColors = {}
|
||||
const themeConfig = getThemeConfig.value
|
||||
|
||||
if (themeConfig.primaryColor) {
|
||||
const primaryColorList = generateColors(themeConfig.primaryColor)
|
||||
colors = {
|
||||
...colors,
|
||||
...{
|
||||
primaryColor: primaryColorList[5],
|
||||
primaryColorHover: primaryColorList[4],
|
||||
primaryColorPressed: primaryColorList[4],
|
||||
primaryColorSuppl: primaryColorList[6],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (themeConfig.infoColor) {
|
||||
const infoColorList = generateColors(themeConfig.infoColor)
|
||||
colors = {
|
||||
...colors,
|
||||
...{
|
||||
infoColor: infoColorList[5],
|
||||
infoColorHover: infoColorList[4],
|
||||
infoColorPressed: infoColorList[4],
|
||||
infoColorSuppl: infoColorList[6],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (themeConfig.successColor) {
|
||||
const successColorList = generateColors(themeConfig.successColor)
|
||||
colors = {
|
||||
...colors,
|
||||
...{
|
||||
successColor: successColorList[5],
|
||||
successColorHover: successColorList[4],
|
||||
successColorPressed: successColorList[4],
|
||||
successColorSuppl: successColorList[6],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (themeConfig.warningColor) {
|
||||
const warningColorList = generateColors(themeConfig.warningColor)
|
||||
colors = {
|
||||
...colors,
|
||||
...{
|
||||
warningColor: warningColorList[5],
|
||||
warningColorHover: warningColorList[4],
|
||||
warningColorPressed: warningColorList[4],
|
||||
warningColorSuppl: warningColorList[6],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (themeConfig.errorColor) {
|
||||
const errorColorList = generateColors(themeConfig.errorColor)
|
||||
colors = {
|
||||
...colors,
|
||||
...{
|
||||
errorColor: errorColorList[5],
|
||||
errorColorHover: errorColorList[4],
|
||||
errorColorPressed: errorColorList[4],
|
||||
errorColorSuppl: errorColorList[6],
|
||||
},
|
||||
}
|
||||
}
|
||||
return colors
|
||||
})
|
||||
|
||||
watch(
|
||||
themeColors,
|
||||
(val) => {
|
||||
val.primaryColor && setCssVar('--primary-color', val.primaryColor)
|
||||
val.successColor && setCssVar('--success-color', val.successColor)
|
||||
val.errorColor && setCssVar('--error-color', val.errorColor)
|
||||
val.warningColor && setCssVar('--warning-color', val.warningColor)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return {
|
||||
isDark,
|
||||
isSidebarDark,
|
||||
isHeaderDark,
|
||||
toggleTheme,
|
||||
primaryColor,
|
||||
infoColor,
|
||||
successColor,
|
||||
warningColor,
|
||||
errorColor,
|
||||
themeColors,
|
||||
setThemeConfig,
|
||||
toggleSidebarTheme,
|
||||
toggleHeaderTheme,
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main p="x4 y10" text="center teal-700 dark:gray-200">
|
||||
<div text-4xl>
|
||||
<div i-carbon-warning inline-block />
|
||||
</div>
|
||||
<RouterView />
|
||||
<div>
|
||||
<button btn text-sm m="3 t8" @click="router.back()">
|
||||
{{ t('button.back') }}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
const { toggle, isFullscreen } = useFullscreen()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="px-3!"
|
||||
@click="toggle"
|
||||
>
|
||||
<LIcon
|
||||
class="hover:cursor-pointer"
|
||||
:icon="isFullscreen ? 'ant-design:fullscreen-exit-outlined' : 'ant-design:fullscreen-outlined'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@ -1,42 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { BASIC_HOME_PATH } from '~/constants'
|
||||
import { createNamespace } from '~/utils'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showTitle: { type: Boolean, default: true },
|
||||
homePath: { type: String, default: BASIC_HOME_PATH },
|
||||
})
|
||||
|
||||
const { getCollapsed } = useMenuSetting()
|
||||
const { push } = useRouter()
|
||||
const { logo } = useSiteSetting()
|
||||
const { getShowLogo } = useRootSetting()
|
||||
|
||||
const getIsShowLogo = computed(() => unref(getShowLogo))
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
const { bem } = createNamespace('app-logo')
|
||||
|
||||
function goHome() {
|
||||
push(props.homePath)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="logo">
|
||||
<img :src="logo" alt="" :class="{ 'mr-2': !getCollapsed }">
|
||||
<h2 v-show="!getCollapsed && props.showTitle" class="title">
|
||||
<div v-if="getIsShowLogo" :class="bem()" @click="goHome">
|
||||
<img :src="`/${logo}`" alt="logo">
|
||||
<div v-show="showTitle" class="ml-2 truncate md:opacity-100" :class="bem('title')">
|
||||
{{ title }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
.app-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 7px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
height: 48px;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
transition: all 0.5s;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,374 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Logo from '../logo/index.vue'
|
||||
import SiderTrigger from '../trigger/sider-trigger.vue'
|
||||
import MixSubMenu from './mix-sub-menu.vue'
|
||||
import { createNamespace, listenerRouteChange } from '~/utils'
|
||||
import { MixSidebarTriggerEnum, NavBarModeEnum } from '~/constants'
|
||||
|
||||
const props = defineProps({
|
||||
mixSidebarWidth: { type: Number, default: 40 },
|
||||
})
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
|
||||
const { t } = useI18n()
|
||||
const { bem } = createNamespace('layout-mix-menu')
|
||||
const { currentRoute } = useRouter()
|
||||
const go = useGo()
|
||||
let currentRouteRef = $ref<Nullable<RouteLocationNormalized>>(null)
|
||||
let menuModulesRef = $ref<Menu[]>([])
|
||||
let activePathRef = $ref<string>('')
|
||||
let childrenMenus = $ref<Menu[]>([])
|
||||
let openMenu = $ref(false)
|
||||
let childrenTitle = $ref('')
|
||||
const sideRef = ref<ElRef>(null)
|
||||
|
||||
const {
|
||||
getIsFixed,
|
||||
getIsMixSidebar,
|
||||
getMixSideFixed,
|
||||
getMixSideTrigger,
|
||||
getCloseMixSidebarOnChange,
|
||||
getMenuWidth,
|
||||
getCollapsed,
|
||||
getMenuType,
|
||||
mixSideHasChildren,
|
||||
setMenuSetting,
|
||||
} = useMenuSetting()
|
||||
|
||||
const {
|
||||
getChildrenMenus,
|
||||
getCurrentParentPath,
|
||||
getShallowMenus,
|
||||
} = useMenu()
|
||||
|
||||
let oldIsFixed = unref(getIsFixed)
|
||||
let pushpin = unref(getIsFixed)
|
||||
|
||||
onMounted(async () => {
|
||||
menuModulesRef = await getShallowMenus()
|
||||
activePathRef = await getCurrentParentPath(currentRoute.value.path)
|
||||
const currentItem = menuModulesRef.find(item => item.path === activePathRef)
|
||||
if (currentItem) {
|
||||
handleModuleClick(currentItem.path, false, currentItem.meta?.title || '')
|
||||
}
|
||||
})
|
||||
|
||||
listenerRouteChange(async (route) => {
|
||||
currentRouteRef = route
|
||||
activePathRef = await getCurrentParentPath(route.path)
|
||||
setActive(true)
|
||||
if (unref(getCloseMixSidebarOnChange)) {
|
||||
closeMenu()
|
||||
}
|
||||
})
|
||||
|
||||
// Set the currently active menu and submenu
|
||||
async function setActive(setChildren = false) {
|
||||
const path = currentRouteRef?.path
|
||||
if (!path) {
|
||||
return
|
||||
}
|
||||
// activePathRef = await getCurrentParentPath(path)
|
||||
if (unref(getIsMixSidebar)) {
|
||||
const activeMenu = unref(menuModulesRef).find(item => item.path === unref(activePathRef))
|
||||
const p = activeMenu?.path
|
||||
if (p) {
|
||||
const children = await getChildrenMenus(p)
|
||||
if (setChildren) {
|
||||
childrenMenus = children
|
||||
|
||||
if (unref(getMixSideFixed)) {
|
||||
openMenu = children.length > 0
|
||||
}
|
||||
}
|
||||
if (children.length === 0) {
|
||||
childrenMenus = []
|
||||
}
|
||||
}
|
||||
}
|
||||
mixSideHasChildren.value = childrenMenus.length > 0
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
if (!unref(getIsFixed)) {
|
||||
openMenu = false
|
||||
setActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Process module menu click
|
||||
async function handleModuleClick(path: string, hover = false, title = '') {
|
||||
const children = await getChildrenMenus(path)
|
||||
childrenTitle = t(title)
|
||||
if (unref(activePathRef) === path) {
|
||||
if (!hover) {
|
||||
if (!unref(openMenu)) {
|
||||
openMenu = true
|
||||
}
|
||||
else {
|
||||
closeMenu()
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!unref(openMenu)) {
|
||||
openMenu = true
|
||||
}
|
||||
}
|
||||
if (!unref(openMenu)) {
|
||||
await setActive()
|
||||
}
|
||||
}
|
||||
else {
|
||||
openMenu = true
|
||||
activePathRef = path
|
||||
}
|
||||
|
||||
if (!children || children.length === 0) {
|
||||
if (!hover) {
|
||||
go(path)
|
||||
}
|
||||
childrenMenus = []
|
||||
closeMenu()
|
||||
return
|
||||
}
|
||||
childrenMenus = children
|
||||
mixSideHasChildren.value = childrenMenus.length > 0
|
||||
}
|
||||
|
||||
function getItemEvents(item: Menu) {
|
||||
if (unref(getMixSideTrigger) === MixSidebarTriggerEnum.HOVER) {
|
||||
return {
|
||||
onMouseenter: () => {
|
||||
return handleModuleClick(item.path, true, item.meta?.title || '')
|
||||
},
|
||||
onClick: async () => {
|
||||
const children = await getChildrenMenus(item.path)
|
||||
if (item.path && (!children || children.length === 0)) {
|
||||
go(item.path)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
onClick: () => {
|
||||
handleModuleClick(item.path, false, item.meta?.title || '')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const getMenuEventsRef = computed(() => {
|
||||
return !unref(getMixSideFixed)
|
||||
? {
|
||||
onMouseleave: () => {
|
||||
// 鼠标移出菜单不做操作
|
||||
// if (!openMenu.value) {
|
||||
// setActive(true)
|
||||
// }
|
||||
// 鼠标离开Menu 不触发关闭菜单面板
|
||||
// closeMenu()
|
||||
},
|
||||
onMouseenter: () => {
|
||||
},
|
||||
}
|
||||
: {}
|
||||
})
|
||||
|
||||
function handleFixedMenu() {
|
||||
setMenuSetting({
|
||||
mixSideFixed: !unref(getMixSideFixed),
|
||||
})
|
||||
pushpin = !unref(getMixSideFixed)
|
||||
}
|
||||
|
||||
const getMenuStyle = computed((): CSSProperties => {
|
||||
if (unref(getIsFixed)) {
|
||||
setActive(true)
|
||||
}
|
||||
else {
|
||||
if (oldIsFixed !== unref(getIsFixed) && !pushpin) {
|
||||
closeMenu()
|
||||
}
|
||||
else {
|
||||
pushpin = false
|
||||
}
|
||||
}
|
||||
oldIsFixed = unref(getIsFixed)
|
||||
return {
|
||||
width: unref(openMenu) ? `${unref(getMenuWidth)}px` : 0,
|
||||
left: `${props.mixSidebarWidth}px`,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="bem()">
|
||||
<Logo
|
||||
v-if="getMenuType === NavBarModeEnum.MIX_SIDEBAR"
|
||||
class="shadow"
|
||||
:class="[bem('logo')]"
|
||||
:style="{ '--un-shadow-color': 'var(--n-border-color)' }"
|
||||
:show-title="false"
|
||||
/>
|
||||
<NScrollbar
|
||||
:class="bem('scrollbar')"
|
||||
v-bind="getMenuEventsRef"
|
||||
>
|
||||
<ul :class="bem('module')">
|
||||
<li
|
||||
v-for="item in menuModulesRef"
|
||||
:key="item.path"
|
||||
:class="[
|
||||
bem('module__item'),
|
||||
{
|
||||
[bem('module__item--active') as string]: item.path === activePathRef,
|
||||
},
|
||||
]"
|
||||
v-bind="getItemEvents(item)"
|
||||
>
|
||||
<LIcon
|
||||
:class="bem('module__icon')"
|
||||
:size="getCollapsed ? 16 : 20"
|
||||
:icon="item.icon || (item.meta && item.meta.icon)"
|
||||
/>
|
||||
<p v-show="!getCollapsed" :class="bem('module__name')">
|
||||
{{ $t(item.meta?.title || '') }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</NScrollbar>
|
||||
<SiderTrigger />
|
||||
<div
|
||||
ref="sideRef"
|
||||
class="shadow"
|
||||
:class="[bem('menu-list')]"
|
||||
:style="getMenuStyle"
|
||||
@mouseleave="closeMenu"
|
||||
>
|
||||
<div
|
||||
v-show="openMenu && childrenMenus.length > 0"
|
||||
class="shadow"
|
||||
:class="[
|
||||
bem('menu-list__title'),
|
||||
{
|
||||
show: openMenu,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<span class="text">{{ title }}</span>
|
||||
<LIcon
|
||||
:size="16"
|
||||
:icon="getMixSideFixed ? 'ant-design:pushpin-filled' : 'ant-design:pushpin-outlined'"
|
||||
class="pushpin hover:cursor-pointer"
|
||||
@click="handleFixedMenu"
|
||||
/>
|
||||
</div>
|
||||
<NH5
|
||||
v-if="openMenu"
|
||||
class="mb-2!"
|
||||
:class="bem('menu-list__children-title')"
|
||||
:style="{ '--n-text-color': 'var(--n-text-color)' }"
|
||||
prefix="bar"
|
||||
strong
|
||||
>
|
||||
{{ childrenTitle }}
|
||||
</NH5>
|
||||
<MixSubMenu :list="childrenMenus" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-mix-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
&__logo {
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__scrollbar {
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
}
|
||||
&__module {
|
||||
position: relative;
|
||||
padding: 1px 0 40px 0;
|
||||
margin: 0;
|
||||
&__item {
|
||||
position: relative;
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
// font-weight: 700;
|
||||
|
||||
color: #18a058;
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background-color: #18a058;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
&__icon {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-bottom: 0;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
&__menu-list {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
width: 0px;
|
||||
height: calc(100%);
|
||||
background-color: var(--n-color);
|
||||
transition: all 0.3s;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
font-size: 18px;
|
||||
opacity: 0%;
|
||||
transition: unset;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding-left: 20px;
|
||||
|
||||
&.show {
|
||||
min-width: 130px;
|
||||
opacity: 100%;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.pushpin {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
&__children-title {
|
||||
padding: 6px 20px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuInst } from 'naive-ui'
|
||||
import type { MenuMixedOption } from 'naive-ui/es/menu/src/interface'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { REDIRECT_NAME } from '~/constants'
|
||||
import { createNamespace, listenerRouteChange, mapTree } from '~/utils'
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array as PropType<Menu[]>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const { getAccordion } = useMenuSetting()
|
||||
const { renderIcon } = useIcon()
|
||||
const { t } = useI18n()
|
||||
const { currentRoute } = useRouter()
|
||||
const menuRef = $ref<Nullable<MenuInst>>(null)
|
||||
let activeKey = $ref<any>()
|
||||
|
||||
// 定位菜单选择 与 当前路由匹配
|
||||
function showOption() {
|
||||
nextTick(() => {
|
||||
if (!menuRef) {
|
||||
return
|
||||
}
|
||||
menuRef.showOption()
|
||||
})
|
||||
}
|
||||
|
||||
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
|
||||
const menu = route || unref(currentRoute)
|
||||
activeKey = menu.name
|
||||
}
|
||||
|
||||
listenerRouteChange((route) => {
|
||||
if (route.name === REDIRECT_NAME) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentActiveMenu = route.meta?.currentActiveMenu as string
|
||||
handleMenuChange(route)
|
||||
|
||||
if (currentActiveMenu) {
|
||||
activeKey.value = currentActiveMenu
|
||||
}
|
||||
showOption()
|
||||
})
|
||||
|
||||
// 路由格式化
|
||||
function routerToMenu(item: Menu): MenuMixedOption {
|
||||
const { name, children, meta, icon } = item
|
||||
const title = t(meta?.title as string)
|
||||
return {
|
||||
label: () => {
|
||||
if (children && children.length > 0) {
|
||||
return title
|
||||
}
|
||||
return h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
{ default: () => title },
|
||||
)
|
||||
},
|
||||
key: name,
|
||||
icon: () => renderIcon({ icon }),
|
||||
}
|
||||
}
|
||||
|
||||
const menuListRef = computed(() => {
|
||||
return mapTree(props.list, { conversion: menu => routerToMenu(menu) })
|
||||
})
|
||||
|
||||
const { bem } = createNamespace('layout-menu')
|
||||
|
||||
const { isSidebarDark } = useAppTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="bem()">
|
||||
<NScrollbar :class="bem('scrollbar')">
|
||||
<NMenu
|
||||
ref="menuRef"
|
||||
v-model:value="activeKey"
|
||||
:options="menuListRef"
|
||||
:collapsed-width="48"
|
||||
:collapsed="false"
|
||||
:collapsed-icon-size="22"
|
||||
:indent="18"
|
||||
:root-indent="18"
|
||||
:accordion="getAccordion"
|
||||
:inverted="isSidebarDark"
|
||||
/>
|
||||
</NScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
&__scrollbar {
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,52 +1,63 @@
|
||||
<script setup lang="ts" name="ThemeColorPicker">
|
||||
import type { HandlerSettingEnum } from '~/constants'
|
||||
import { ThemeChangeEnum } from '~/constants'
|
||||
|
||||
const props = defineProps({
|
||||
colorList: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
title: { type: String, default: '' },
|
||||
event: {
|
||||
type: Number as PropType<HandlerSettingEnum>,
|
||||
type: Number as PropType<ThemeChangeEnum>,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
def: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const { baseHandler } = useAppConfig()
|
||||
const { setThemeConfig } = useAppTheme()
|
||||
const color = $ref(props.def)
|
||||
|
||||
function handlerClick(color: any) {
|
||||
baseHandler(props.event, color)
|
||||
function onChange(color: string) {
|
||||
switch (props.event) {
|
||||
case ThemeChangeEnum.THEME_PRIMARY_COLOR_CHANGE:
|
||||
setThemeConfig({ primaryColor: color })
|
||||
break
|
||||
case ThemeChangeEnum.THEME_INFO_COLOR_CHANGE:
|
||||
setThemeConfig({ infoColor: color })
|
||||
break
|
||||
case ThemeChangeEnum.THEME_SUCCESS_COLOR_CHANGE:
|
||||
setThemeConfig({ successColor: color })
|
||||
break
|
||||
case ThemeChangeEnum.THEME_WARNING_COLOR_CHANGE:
|
||||
setThemeConfig({ warningColor: color })
|
||||
break
|
||||
case ThemeChangeEnum.THEME_ERROR_COLOR_CHANGE:
|
||||
setThemeConfig({ errorColor: color })
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-color-picker">
|
||||
<NSpace justify="space-between" :size="0" :wrap="false">
|
||||
<template v-for="color in colorList" :key="color">
|
||||
<span
|
||||
class="color-item box-border inline-block h-20px w-20px cursor-pointer border border-gray-300 rounded-sm"
|
||||
:class="{ active: def === color }"
|
||||
:style="{ background: color }" @click="handlerClick(color)"
|
||||
>
|
||||
<NSpace v-if="def === color" justify="center">
|
||||
<div class="i-ant-design:check-outlined text-white hover:text-#d1d5db" />
|
||||
</NSpace>
|
||||
</span>
|
||||
</template>
|
||||
</NSpace>
|
||||
<div class="min-w-83px">
|
||||
<NColorPicker
|
||||
v-model:value="color"
|
||||
:show-alpha="false"
|
||||
:modes="['hex', 'rgb']"
|
||||
:render-label="() => title"
|
||||
:swatches="colorList"
|
||||
@complete="onChange"
|
||||
@update:value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.color-item {
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border-color: rgba(6, 96, 189, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import ThemeColorPicker from './theme-color-picker.vue'
|
||||
import SwitchItem from './switch-item.vue'
|
||||
import { APP_PRESET_COLOR_LIST, HEADER_PRESET_BG_COLOR_LIST, ThemeChangeEnum } from '~/constants'
|
||||
|
||||
const { getHeaderBgColor } = useHeaderSetting()
|
||||
const {
|
||||
primaryColor,
|
||||
infoColor,
|
||||
successColor,
|
||||
warningColor,
|
||||
errorColor,
|
||||
isSidebarDark,
|
||||
toggleSidebarTheme,
|
||||
} = useAppTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace justify="space-between" align="center">
|
||||
<ThemeColorPicker
|
||||
:title="$t('layout.setting.primaryColor')"
|
||||
:def="primaryColor"
|
||||
:event="ThemeChangeEnum.THEME_PRIMARY_COLOR_CHANGE"
|
||||
:color-list="APP_PRESET_COLOR_LIST"
|
||||
/>
|
||||
<ThemeColorPicker
|
||||
:title="$t('layout.setting.infoColor')"
|
||||
:def="infoColor"
|
||||
:event="ThemeChangeEnum.THEME_INFO_COLOR_CHANGE"
|
||||
/>
|
||||
<ThemeColorPicker
|
||||
:title="$t('layout.setting.successColor')"
|
||||
:def="successColor"
|
||||
:event="ThemeChangeEnum.THEME_SUCCESS_COLOR_CHANGE"
|
||||
/>
|
||||
<ThemeColorPicker
|
||||
:title="$t('layout.setting.warningColor')"
|
||||
:def="warningColor"
|
||||
:event="ThemeChangeEnum.THEME_WARNING_COLOR_CHANGE"
|
||||
/>
|
||||
<ThemeColorPicker
|
||||
:title="$t('layout.setting.errorColor')"
|
||||
:def="errorColor"
|
||||
:event="ThemeChangeEnum.THEME_ERROR_COLOR_CHANGE"
|
||||
/>
|
||||
<ThemeColorPicker
|
||||
:title="$t('layout.setting.headerTheme')"
|
||||
:def="getHeaderBgColor"
|
||||
:event="ThemeChangeEnum.THEME_HEADER_BG_COLOR_CHANGE"
|
||||
:color-list="HEADER_PRESET_BG_COLOR_LIST"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.sidebarDark')"
|
||||
:value="isSidebarDark"
|
||||
:callback="toggleSidebarTheme"
|
||||
/>
|
||||
</NSpace>
|
||||
</template>
|
@ -0,0 +1,86 @@
|
||||
export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>, mix = false) {
|
||||
const { getMiniWidthNumber, getCollapsed, setMenuSetting } = useMenuSetting()
|
||||
|
||||
function getEl(elRef: Ref<ElRef | ComponentRef>): any {
|
||||
const el = unref(elRef)
|
||||
if (!el) {
|
||||
return null
|
||||
}
|
||||
if (Reflect.has(el, '$el')) {
|
||||
return (unref(elRef) as ComponentRef)?.$el
|
||||
}
|
||||
return unref(elRef)
|
||||
}
|
||||
|
||||
function handleMouseMove(ele: HTMLElement, wrap: HTMLElement, clientX: number) {
|
||||
document.onmousemove = function (innerE) {
|
||||
let iT = (ele as any).left + (innerE.clientX - clientX)
|
||||
innerE = innerE || window.event
|
||||
const maxT = 800
|
||||
const minT = unref(getMiniWidthNumber)
|
||||
iT < 0 && (iT = 0)
|
||||
iT > maxT && (iT = maxT)
|
||||
iT < minT && (iT = minT)
|
||||
ele.style.left = wrap.style.width = `${iT}px`
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and drop in the menu area-release the mouse
|
||||
function removeMouseup(ele: any) {
|
||||
const wrap = getEl(siderRef)
|
||||
document.onmouseup = function () {
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
wrap.style.transition = 'width 0.2s'
|
||||
const width = Number.parseInt(wrap.style.width)
|
||||
|
||||
if (!mix) {
|
||||
const miniWidth = unref(getMiniWidthNumber)
|
||||
if (!unref(getCollapsed)) {
|
||||
width > miniWidth + 20
|
||||
? setMenuSetting({ menuWidth: width })
|
||||
: setMenuSetting({ collapsed: true })
|
||||
}
|
||||
else {
|
||||
width > miniWidth && setMenuSetting({ collapsed: false, menuWidth: width })
|
||||
}
|
||||
}
|
||||
else {
|
||||
setMenuSetting({ menuWidth: width })
|
||||
}
|
||||
|
||||
ele.releaseCapture?.()
|
||||
}
|
||||
}
|
||||
|
||||
function changeWrapWidth() {
|
||||
const ele = getEl(dragBarRef)
|
||||
if (!ele) {
|
||||
return
|
||||
}
|
||||
const wrap = getEl(siderRef)
|
||||
if (!wrap) {
|
||||
return
|
||||
}
|
||||
|
||||
ele.onmousedown = (e: any) => {
|
||||
wrap.style.transition = 'unset'
|
||||
const clientX = e?.clientX
|
||||
ele.left = ele.offsetLeft
|
||||
handleMouseMove(ele, wrap, clientX)
|
||||
removeMouseup(ele)
|
||||
ele.setCapture?.()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const exec = useDebounceFn(changeWrapWidth, 80)
|
||||
exec()
|
||||
})
|
||||
})
|
||||
|
||||
return {}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { TabActionEnum } from '~/constants'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const x = ref(0)
|
||||
const y = ref(0)
|
||||
let targetTabRef = $ref<RouteLocationNormalized>()
|
||||
let showDropdownRef = $ref(false)
|
||||
|
||||
const tabStore = useMultipleTabStore()
|
||||
const { renderIcon } = useIcon()
|
||||
|
||||
const optionsRef = computed(() => {
|
||||
const tab = targetTabRef
|
||||
if (!tab)
|
||||
return []
|
||||
return (
|
||||
tabStore
|
||||
.getTabActions(tab)
|
||||
// 筛选非当前路由tab的重新加载按钮
|
||||
?.filter(v => !(v.key === 0 && v.disabled))
|
||||
// 渲染多语言和图标
|
||||
.map((v) => {
|
||||
const label = v.label && t(v.label)
|
||||
return { ...v, label, icon: () => renderIcon({ icon: v.icon }) }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
function openDropdown(e: MouseEvent, tabItem: RouteLocationNormalized) {
|
||||
targetTabRef = tabItem
|
||||
showDropdownRef = false
|
||||
nextTick().then(() => {
|
||||
showDropdownRef = true
|
||||
x.value = e.clientX
|
||||
y.value = e.clientY
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ openDropdown })
|
||||
|
||||
const {
|
||||
refreshPage,
|
||||
close,
|
||||
closeAll,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeOther,
|
||||
} = useTabs()
|
||||
|
||||
async function handleSelect(key: TabActionEnum) {
|
||||
const tab = unref(targetTabRef)
|
||||
switch (key) {
|
||||
case TabActionEnum.REFRESH_PAGE:
|
||||
await refreshPage()
|
||||
break
|
||||
case TabActionEnum.CLOSE_CURRENT:
|
||||
await close(tab)
|
||||
break
|
||||
case TabActionEnum.CLOSE_ALL:
|
||||
await closeAll()
|
||||
break
|
||||
case TabActionEnum.CLOSE_LEFT:
|
||||
await closeLeft(tab)
|
||||
break
|
||||
case TabActionEnum.CLOSE_RIGHT:
|
||||
await closeRight(tab)
|
||||
break
|
||||
case TabActionEnum.CLOSE_OTHER:
|
||||
await closeOther(tab)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDropdown
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
:show-arrow="true"
|
||||
:x="x"
|
||||
:y="y"
|
||||
:options="optionsRef"
|
||||
$show="showDropdownRef"
|
||||
@clickoutside="showDropdownRef = false"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { triggerWindowResize } from '~/utils'
|
||||
|
||||
const { getShowMenu, setMenuSetting } = useMenuSetting()
|
||||
const { getShowHeader, setHeaderSetting } = useHeaderSetting()
|
||||
|
||||
const getIsUnFoldRef = computed(() => !unref(getShowMenu) && !unref(getShowHeader))
|
||||
|
||||
const getIconRef = computed(() => unref(getIsUnFoldRef) ? 'codicon:screen-normal' : 'codicon:screen-full')
|
||||
|
||||
function handleFold() {
|
||||
// 设置菜单和头部是否显示
|
||||
const show = unref(getIsUnFoldRef)
|
||||
setMenuSetting({ show })
|
||||
setHeaderSetting({ show })
|
||||
|
||||
triggerWindowResize()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full w-32px flex-center cursor-pointer border-l border-[var(--n-border-color)]"
|
||||
@click="handleFold"
|
||||
>
|
||||
<LIcon :icon="getIconRef" />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { TabActionEnum } from '~/constants'
|
||||
|
||||
const props = defineProps({
|
||||
tabItem: {
|
||||
type: Object as PropType<RouteLocationNormalized>,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
const { t } = useI18n()
|
||||
const tabStore = useMultipleTabStore()
|
||||
const { renderIcon } = useIcon()
|
||||
const optionsRef = computed(() => {
|
||||
return tabStore.getTabActions(props.tabItem)
|
||||
?.map((v) => {
|
||||
const label = v.label && t(v.label)
|
||||
return { ...v, label, icon: () => renderIcon({ icon: v.icon }) }
|
||||
})
|
||||
})
|
||||
|
||||
const {
|
||||
refreshPage,
|
||||
close,
|
||||
closeAll,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeOther,
|
||||
} = useTabs()
|
||||
|
||||
async function handleSelect(key: TabActionEnum) {
|
||||
switch (key) {
|
||||
case TabActionEnum.REFRESH_PAGE:
|
||||
await refreshPage()
|
||||
break
|
||||
case TabActionEnum.CLOSE_CURRENT:
|
||||
await close(props.tabItem)
|
||||
break
|
||||
case TabActionEnum.CLOSE_ALL:
|
||||
await closeAll()
|
||||
break
|
||||
case TabActionEnum.CLOSE_LEFT:
|
||||
await closeLeft()
|
||||
break
|
||||
case TabActionEnum.CLOSE_RIGHT:
|
||||
await closeRight()
|
||||
break
|
||||
case TabActionEnum.CLOSE_OTHER:
|
||||
await closeOther()
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDropdown
|
||||
placement="bottom-start"
|
||||
trigger="click"
|
||||
:show-arrow="true"
|
||||
:options="optionsRef"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<div
|
||||
class="h-full w-32px flex-center cursor-pointer border-l border-[var(--n-border-color)]"
|
||||
>
|
||||
<LIcon icon="ant-design:down-outlined" />
|
||||
</div>
|
||||
</NDropdown>
|
||||
</template>
|
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
const { refreshPage } = useTabs()
|
||||
|
||||
function reload() {
|
||||
return new Promise((resolve) => {
|
||||
refreshPage().then(() => {
|
||||
setTimeout(() => {
|
||||
resolve({})
|
||||
}, 300)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const { loading, handleFn: handleRedo } = usePromise(reload, { immediate: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full w-32px flex-center cursor-pointer border-l border-[var(--n-border-color)]"
|
||||
@click="handleRedo"
|
||||
>
|
||||
<LIcon :spin="loading" icon="ant-design:reload-outlined" />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
|
||||
export interface TabDropDownInst {
|
||||
openDropdown: (e: MouseEvent, tabItem: RouteLocationNormalized) => void
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import LayoutMixMenu from './components/menu/mix-menu.vue'
|
||||
import LayoutHeader from './components/header.vue'
|
||||
import LayoutFooter from './components/footer.vue'
|
||||
import LayoutMain from './components/main.vue'
|
||||
import { createNamespace } from '~/utils'
|
||||
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '~/constants'
|
||||
|
||||
defineOptions({
|
||||
name: 'MixMenuLayout',
|
||||
})
|
||||
|
||||
const {
|
||||
headerRef,
|
||||
contentStyle,
|
||||
mainStyle,
|
||||
footerRef,
|
||||
contentRef,
|
||||
} = useLayout()
|
||||
const {
|
||||
getCollapsed,
|
||||
getMenuWidth,
|
||||
getIsFixed,
|
||||
getShowSidebar,
|
||||
} = useMenuSetting()
|
||||
const { getShowFooter } = useRootSetting()
|
||||
|
||||
const getMixSidebarWidth = computed(() => {
|
||||
return unref(getCollapsed)
|
||||
? SIDE_BAR_MINI_WIDTH
|
||||
: SIDE_BAR_SHOW_TIT_MINI_WIDTH
|
||||
})
|
||||
|
||||
const getContainerStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
paddingLeft: `${unref(getIsFixed) ? unref(getMenuWidth) : 0}px`,
|
||||
}
|
||||
})
|
||||
|
||||
const { bem } = createNamespace('layout-mix-sidebar')
|
||||
|
||||
const { isSidebarDark } = useAppTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLayout has-sider class="h-full">
|
||||
<NLayoutSider
|
||||
v-if="getShowSidebar"
|
||||
bordered
|
||||
:collapsed-width="getMixSidebarWidth"
|
||||
collapse-mode="width"
|
||||
:collapsed="true"
|
||||
:class="bem()"
|
||||
:inverted="isSidebarDark"
|
||||
>
|
||||
<slot name="sider">
|
||||
<LayoutMixMenu :mix-sidebar-width="getMixSidebarWidth" />
|
||||
</slot>
|
||||
</NLayoutSider>
|
||||
<NLayout :style="getContainerStyle" class="transition-all">
|
||||
<NLayoutHeader ref="headerRef">
|
||||
<slot name="header">
|
||||
<LayoutHeader />
|
||||
</slot>
|
||||
</NLayoutHeader>
|
||||
<NLayout :content-style="contentStyle">
|
||||
<NLayoutContent ref="contentRef" :content-style="mainStyle">
|
||||
<LayoutMain>
|
||||
<slot name="main" />
|
||||
</LayoutMain>
|
||||
</NLayoutContent>
|
||||
<NLayoutFooter v-if="getShowFooter" ref="footerRef">
|
||||
<slot name="footer">
|
||||
<LayoutFooter />
|
||||
</slot>
|
||||
</NLayoutFooter>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-mix-sidebar {
|
||||
z-index: var(--mix-sider-z-index);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,83 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, unref } from 'vue'
|
||||
import LayoutMenu from './components/menu/index.vue'
|
||||
import LayoutHeader from './components/header.vue'
|
||||
import LayoutTabs from './components/tabs/index.vue'
|
||||
import LayoutMain from './components/main.vue'
|
||||
import LayoutFooter from './components/footer.vue'
|
||||
|
||||
const {
|
||||
toggleCollapsed,
|
||||
getCollapsed,
|
||||
getMenuWidth,
|
||||
getShowSidebar,
|
||||
getShowCenterTrigger,
|
||||
} = useMenuSetting()
|
||||
const { getShowFooter } = useRootSetting()
|
||||
const { getShowMultipleTab } = useMultipleTabSetting()
|
||||
|
||||
const {
|
||||
headerRef,
|
||||
tabRef,
|
||||
footerRef,
|
||||
headerHeight,
|
||||
contentStyle,
|
||||
mainStyle,
|
||||
contentRef,
|
||||
} = useLayout()
|
||||
|
||||
const menuHeight = computed(() => `calc(100vh - ${unref(headerHeight)}px)`)
|
||||
|
||||
const { isSidebarDark } = useAppTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLayout class="h-full">
|
||||
<NLayoutHeader ref="headerRef">
|
||||
<slot name="header">
|
||||
<LayoutHeader>
|
||||
<template #menu>
|
||||
<LayoutMenu mode="horizontal" />
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
</slot>
|
||||
</NLayoutHeader>
|
||||
<NLayout has-sider :style="{ height: menuHeight }">
|
||||
<NLayoutSider
|
||||
v-if="getShowSidebar"
|
||||
:show-trigger="getShowCenterTrigger"
|
||||
bordered
|
||||
:collapsed-width="48"
|
||||
:width="getMenuWidth"
|
||||
collapse-mode="width"
|
||||
:collapsed="getCollapsed"
|
||||
:inverted="isSidebarDark"
|
||||
@update:collapsed="toggleCollapsed"
|
||||
>
|
||||
<slot name="sider">
|
||||
<LayoutMenu split />
|
||||
</slot>
|
||||
</NLayoutSider>
|
||||
|
||||
<NLayout>
|
||||
<NLayoutHeader v-if="getShowMultipleTab">
|
||||
<slot name="tabs">
|
||||
<LayoutTabs ref="tabRef" />
|
||||
</slot>
|
||||
</NLayoutHeader>
|
||||
<NLayout :content-style="contentStyle">
|
||||
<NLayoutContent ref="contentRef" :content-style="mainStyle">
|
||||
<LayoutMain>
|
||||
<slot name="main" />
|
||||
</LayoutMain>
|
||||
</NLayoutContent>
|
||||
<NLayoutFooter v-if="getShowFooter" ref="footerRef">
|
||||
<slot name="footer">
|
||||
<LayoutFooter />
|
||||
</slot>
|
||||
</NLayoutFooter>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</template>
|
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import LayoutMenu from './components/menu/index.vue'
|
||||
import LayoutHeader from './components/header.vue'
|
||||
import LayoutFooter from './components/footer.vue'
|
||||
import LayoutMain from './components/main.vue'
|
||||
|
||||
// import { useDragLine } from './components/sider/drag'
|
||||
|
||||
defineOptions({
|
||||
name: 'TopMenuLayout',
|
||||
})
|
||||
|
||||
const {
|
||||
headerRef,
|
||||
footerRef,
|
||||
contentRef,
|
||||
contentStyle,
|
||||
mainStyle,
|
||||
} = useLayout()
|
||||
|
||||
const { getShowFooter } = useRootSetting()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLayout class="h-full">
|
||||
<NLayoutHeader ref="headerRef">
|
||||
<slot name="header">
|
||||
<LayoutHeader>
|
||||
<template #menu>
|
||||
<LayoutMenu mode="horizontal" />
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
</slot>
|
||||
</NLayoutHeader>
|
||||
<NLayout :content-style="contentStyle">
|
||||
<NLayoutContent ref="contentRef" :content-style="mainStyle">
|
||||
<LayoutMain>
|
||||
<slot name="main" />
|
||||
</LayoutMain>
|
||||
</NLayoutContent>
|
||||
<NLayoutFooter v-if="getShowFooter" ref="footerRef">
|
||||
<slot name="footer">
|
||||
<LayoutFooter />
|
||||
</slot>
|
||||
</NLayoutFooter>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</template>
|
@ -0,0 +1,117 @@
|
||||
<script setup lang="ts" name="ExceptionPage">
|
||||
import { BASIC_LOGIN_PATH, ExceptionEnum } from '~/constants'
|
||||
import { createNamespace } from '~/utils'
|
||||
|
||||
interface MapValue {
|
||||
title: string
|
||||
subTitle: string
|
||||
btnText?: string
|
||||
icon?: string
|
||||
handler?: AnyFunction<any>
|
||||
status?: string
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
// 状态码
|
||||
status: {
|
||||
type: Number as PropType<number>,
|
||||
default: ExceptionEnum.PAGE_NOT_FOUND,
|
||||
},
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
|
||||
subTitle: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
|
||||
full: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const statusMapRef = $ref<Map<string | number, MapValue>>(new Map<string | number, MapValue>())
|
||||
|
||||
const { t } = useI18n()
|
||||
const { query } = useRoute()
|
||||
const go = useGo()
|
||||
const redo = useRedo()
|
||||
const { bem } = createNamespace('exception-page')
|
||||
const getStatus = computed(() => {
|
||||
const { status: routeStatus } = query
|
||||
const { status } = props
|
||||
return Number(routeStatus) || status
|
||||
})
|
||||
|
||||
const getMapValue = computed((): MapValue => statusMapRef.get(unref(getStatus)) as MapValue)
|
||||
|
||||
const backLoginI18n = t('sys.exception.backLogin')
|
||||
const backHomeI18n = t('sys.exception.backHome')
|
||||
|
||||
statusMapRef.set(ExceptionEnum.PAGE_NOT_ACCESS, {
|
||||
title: '403',
|
||||
status: `${ExceptionEnum.PAGE_NOT_ACCESS}`,
|
||||
subTitle: t('sys.exception.subTitle403'),
|
||||
btnText: props.full ? backLoginI18n : backHomeI18n,
|
||||
handler: () => (props.full ? go(BASIC_LOGIN_PATH) : go()),
|
||||
})
|
||||
|
||||
statusMapRef.set(ExceptionEnum.PAGE_NOT_FOUND, {
|
||||
title: '404',
|
||||
status: `${ExceptionEnum.PAGE_NOT_FOUND}`,
|
||||
subTitle: t('sys.exception.subTitle404'),
|
||||
btnText: props.full ? backLoginI18n : backHomeI18n,
|
||||
handler: () => (props.full ? go(BASIC_LOGIN_PATH) : go()),
|
||||
})
|
||||
|
||||
statusMapRef.set(ExceptionEnum.ERROR, {
|
||||
title: '500',
|
||||
status: `${ExceptionEnum.ERROR}`,
|
||||
subTitle: t('sys.exception.subTitle500'),
|
||||
btnText: backHomeI18n,
|
||||
handler: () => go(),
|
||||
})
|
||||
|
||||
statusMapRef.set(ExceptionEnum.PAGE_NOT_DATA, {
|
||||
title: t('sys.exception.noDataTitle'),
|
||||
subTitle: '',
|
||||
btnText: t('common.redo'),
|
||||
handler: () => redo(),
|
||||
icon: 'nl-no-data',
|
||||
})
|
||||
|
||||
statusMapRef.set(ExceptionEnum.NET_WORK_ERROR, {
|
||||
title: t('sys.exception.networkErrorTitle'),
|
||||
subTitle: t('sys.exception.networkErrorSubTitle'),
|
||||
btnText: t('common.redo'),
|
||||
handler: () => redo(),
|
||||
icon: 'nl-net-error',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NResult
|
||||
:class="bem()" class="m-4" :status="`${getStatus}` as any" :title="props.title || getMapValue.title"
|
||||
:description="props.subTitle || getMapValue.subTitle"
|
||||
>
|
||||
<template v-if="getMapValue.btnText" #footer>
|
||||
<NButton type="primary" @click="getMapValue.handler">
|
||||
{{ getMapValue.btnText }}
|
||||
</NButton>
|
||||
</template>
|
||||
<template v-if="getMapValue.icon" #icon>
|
||||
<LIcon size="400" :icon="getMapValue.icon" />
|
||||
</template>
|
||||
</NResult>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.exception-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,6 @@
|
||||
<script setup lang="ts" name="IFrame">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
@ -0,0 +1,28 @@
|
||||
<script setup lang="ts" name="Redirect">
|
||||
const { currentRoute, replace } = useRouter()
|
||||
const { params, query } = unref(currentRoute)
|
||||
const { path, _redirect_type = 'path' } = params
|
||||
|
||||
Reflect.deleteProperty(params, '_redirect_type')
|
||||
Reflect.deleteProperty(params, 'path')
|
||||
|
||||
const _path = Array.isArray(path) ? path.join('/') : path
|
||||
if (_redirect_type === 'name') {
|
||||
replace({
|
||||
name: _path,
|
||||
query,
|
||||
params,
|
||||
})
|
||||
}
|
||||
else {
|
||||
replace({
|
||||
path: _path.startsWith('/') ? _path : `/${_path}`,
|
||||
query,
|
||||
params,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
@ -1,50 +0,0 @@
|
||||
import type { App } from 'vue'
|
||||
import type { Locale } from 'vue-i18n'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
// Import i18n resources
|
||||
// https://vitejs.dev/guide/features.html#glob-import
|
||||
//
|
||||
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: '',
|
||||
messages: {},
|
||||
})
|
||||
|
||||
const localesMap = Object.fromEntries(
|
||||
Object.entries(import.meta.glob('../../locales/*.yml'))
|
||||
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
|
||||
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
|
||||
|
||||
export const availableLocales = Object.keys(localesMap)
|
||||
|
||||
const loadedLanguages: string[] = []
|
||||
|
||||
function setI18nLanguage(lang: Locale) {
|
||||
i18n.global.locale.value = lang as any
|
||||
if (typeof document !== 'undefined')
|
||||
document.querySelector('html')?.setAttribute('lang', lang)
|
||||
return lang
|
||||
}
|
||||
|
||||
export async function loadLanguageAsync(lang: string): Promise<Locale> {
|
||||
// If the same language
|
||||
if (i18n.global.locale.value === lang)
|
||||
return setI18nLanguage(lang)
|
||||
|
||||
// If the language was already loaded
|
||||
if (loadedLanguages.includes(lang))
|
||||
return setI18nLanguage(lang)
|
||||
|
||||
// If the language hasn't been loaded yet
|
||||
const messages = await localesMap[lang]()
|
||||
i18n.global.setLocaleMessage(lang, messages.default)
|
||||
loadedLanguages.push(lang)
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
export async function setupI18n(app: App) {
|
||||
app.use(i18n)
|
||||
await loadLanguageAsync('zh-CN')
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import type { Locale } from 'vue-i18n'
|
||||
|
||||
export const LOCALE: { [key: string]: Locale } = {
|
||||
ZH_CHS: 'zh-chs', // 简体中文
|
||||
ZH_CHT: 'zh-cht', // 繁体中文
|
||||
AR: 'ar', // 阿拉伯语
|
||||
BG: 'bg', // 保加利亚语
|
||||
HR: 'hr', // 克罗地亚语
|
||||
CS: 'cs', // 捷克语
|
||||
DA: 'da', // 丹麦语言
|
||||
DE: 'de', // 德语
|
||||
EL: 'el', // 希腊语
|
||||
EN: 'en', // 英语
|
||||
ET: 'et', // 爱沙尼亚语
|
||||
ES: 'es', // 西班牙语
|
||||
FI: 'fi', // 芬兰语
|
||||
FR: 'fr', // 法语
|
||||
GA: 'ga', // 爱尔兰语
|
||||
HI: 'hi', // 印地语
|
||||
HU: 'hu', // 匈牙利语
|
||||
HE: 'he', // 希伯来语
|
||||
IT: 'it', // 意大利语
|
||||
JA: 'ja', // 日语
|
||||
KO: 'ko', // 朝鲜语
|
||||
LV: 'lv', // 拉脱维亚语
|
||||
LT: 'lt', // 立陶宛语
|
||||
NL: 'nl', // 荷兰语
|
||||
NO: 'no', // 挪威语
|
||||
PL: 'pl', // 波兰语
|
||||
PT: 'pt', // 葡萄牙语
|
||||
SV: 'sv', // 瑞典语
|
||||
RO: 'ro', // 罗马尼亚语
|
||||
RU: 'ru', // 俄语
|
||||
SR_CS: 'sr-cs', // 塞尔维亚语
|
||||
SK: 'sk', // 斯洛伐克语
|
||||
SL: 'sl', // 斯洛文尼亚语
|
||||
TH: 'th', // 泰语
|
||||
TR: 'tr', // 土耳其语
|
||||
UK_UA: 'uk-ua', // 乌克兰语
|
||||
}
|
||||
|
||||
export const localesMap = Object.fromEntries(
|
||||
Object.entries(import.meta.glob('../../../locales/*.yml'))
|
||||
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
|
||||
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
|
||||
|
||||
export const localeSetting: LocaleConfig = {
|
||||
locale: LOCALE.ZH_CHS,
|
||||
fallback: LOCALE.ZH_CHS,
|
||||
availableLocales: Object.keys(localesMap),
|
||||
}
|
||||
|
||||
export const localeList: any[] = [
|
||||
{
|
||||
text: '简体中文',
|
||||
event: LOCALE.ZH_CHS,
|
||||
icon: 'emojione:flag-for-china',
|
||||
},
|
||||
// {
|
||||
// text: '繁体中文',
|
||||
// event: LOCALE.ZH_CHT,
|
||||
// icon: 'emojione:flag-for-hong-kong-sar-china',
|
||||
// },
|
||||
{
|
||||
text: 'English',
|
||||
event: LOCALE.EN,
|
||||
icon: 'emojione:flag-for-united-states',
|
||||
},
|
||||
{
|
||||
text: '日本語',
|
||||
event: LOCALE.JA,
|
||||
icon: 'emojione:flag-for-japan',
|
||||
},
|
||||
]
|
@ -0,0 +1,65 @@
|
||||
import type { App } from 'vue'
|
||||
import type { I18nOptions, Locale } from 'vue-i18n'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { localesMap } from './config'
|
||||
|
||||
const loadedLocalePool: string[] = []
|
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let i18n: ReturnType<typeof createI18n>
|
||||
|
||||
async function createI18nOptions(): Promise<I18nOptions> {
|
||||
// saved locale
|
||||
const localeStore = useLocaleStore()
|
||||
const { fallback, availableLocales, getLocale } = storeToRefs(localeStore)
|
||||
const locale = getLocale.value
|
||||
|
||||
return {
|
||||
legacy: false,
|
||||
locale,
|
||||
fallbackLocale: unref(fallback),
|
||||
messages: {},
|
||||
availableLocales: unref(availableLocales),
|
||||
sync: true,
|
||||
silentTranslationWarn: false,
|
||||
missingWarn: false,
|
||||
silentFallbackWarn: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function setI18nLanguage(lang: Locale) {
|
||||
i18n.global.locale = lang
|
||||
if (typeof document !== 'undefined') {
|
||||
document.querySelector('html')?.setAttribute('lang', lang)
|
||||
}
|
||||
return lang
|
||||
}
|
||||
|
||||
export async function loadLanguageAsync(lang: string): Promise<Locale> {
|
||||
// If the same language
|
||||
if (i18n.global.locale === lang) {
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
// If the language was already loaded
|
||||
if (loadedLocalePool.includes(lang)) {
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
// If the language hasn't been loaded yet
|
||||
const messages = await localesMap[lang]()
|
||||
i18n.global.setLocaleMessage(lang, messages.default)
|
||||
|
||||
loadedLocalePool.push(lang)
|
||||
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
export async function setupI18n(app: App) {
|
||||
const options = await createI18nOptions()
|
||||
i18n = createI18n(options)
|
||||
await loadLanguageAsync(options.locale!)
|
||||
|
||||
app.use(i18n)
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate'
|
||||
import type { App } from 'vue'
|
||||
import { resetSetupStorePlugin } from './reset'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
// setup-syntax $reset plugin
|
||||
pinia.use(resetSetupStorePlugin)
|
||||
|
||||
// 持久化插件(localStorage)
|
||||
pinia.use(createPersistedState({
|
||||
storage: localStorage,
|
@ -0,0 +1,15 @@
|
||||
import type { PiniaPluginContext } from 'pinia'
|
||||
import { cloneDeep } from '~/utils'
|
||||
|
||||
/**
|
||||
* setup语法的重置状态插件
|
||||
*/
|
||||
export function resetSetupStorePlugin(context: PiniaPluginContext) {
|
||||
const initialState = cloneDeep(context.store.$state)
|
||||
|
||||
context.store.$reset = () => {
|
||||
context.store.$patch(($state) => {
|
||||
Object.assign($state, initialState)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { PAGE_NOT_FOUND_NAME, REDIRECT_NAME } from '~/constants'
|
||||
|
||||
const PAGE_NOT_FOUND_ROUTE: RouteRecordRaw = {
|
||||
path: '/:all(.*)*',
|
||||
name: PAGE_NOT_FOUND_NAME,
|
||||
component: () => import('~/layouts/page/exception.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
title: 'ErrorPage',
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
},
|
||||
}
|
||||
|
||||
const REDIRECT_ROUTE: RouteRecordRaw = {
|
||||
path: '/redirect/:path(.*):_redirect_type(.*)',
|
||||
name: REDIRECT_NAME,
|
||||
component: () => import('~/layouts/page/redirect.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
title: REDIRECT_NAME,
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
},
|
||||
}
|
||||
|
||||
export {
|
||||
PAGE_NOT_FOUND_ROUTE,
|
||||
REDIRECT_ROUTE,
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: page-layout
|
||||
title: menu.login
|
||||
hideInMenu: true
|
||||
</route>
|
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: page-layout
|
||||
hideInMenu: true
|
||||
</route>
|
@ -0,0 +1,21 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
import { localeSetting } from '~/modules/i18n/config'
|
||||
|
||||
export const useLocaleStore = defineStore('LOCALE', () => {
|
||||
const state = $ref<LocaleConfig>(localeSetting)
|
||||
|
||||
const getLocale = computed(() => unref(state).locale)
|
||||
const setLocale = (locale: string) => {
|
||||
state.locale = locale
|
||||
}
|
||||
return {
|
||||
...toRefs(state),
|
||||
getLocale,
|
||||
setLocale,
|
||||
}
|
||||
}, { persist: {
|
||||
paths: Object.keys(localeSetting),
|
||||
} })
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useLocaleStore as any, import.meta.hot))
|
@ -1,12 +0,0 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const useMenuStore = defineStore('MENU', () => {
|
||||
const menuList = $ref<RouteRecordRaw[]>([])
|
||||
return {
|
||||
menuList,
|
||||
}
|
||||
}, { persist: true })
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useMenuStore as any, import.meta.hot))
|
@ -0,0 +1,118 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import pageRoutes from '~pages'
|
||||
import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '~/modules/router/routes/basic'
|
||||
import { cloneDeep, filterTree, transformRouteToMenu } from '~/utils'
|
||||
import { PermissionModeEnum } from '~/constants'
|
||||
|
||||
export const useRouteStore = defineStore('ROUTES', () => {
|
||||
const routesRef = shallowRef<RouteRecordRaw[]>([])
|
||||
const menuListRef = ref<Menu[]>([])
|
||||
const initMenu = ref(false)
|
||||
|
||||
/**
|
||||
* 初始化路由
|
||||
*/
|
||||
function initRoutes() {
|
||||
/**
|
||||
* 过滤忽略的路由
|
||||
*/
|
||||
const routeIgnoreFilter = (route: RouteRecordRaw) => {
|
||||
const { meta } = route
|
||||
const { ignore } = meta || {}
|
||||
return !ignore
|
||||
}
|
||||
|
||||
const routes: RouteRecordRaw[] = []
|
||||
routes.push(PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE)
|
||||
routes.push(...pageRoutes)
|
||||
|
||||
routesRef.value = filterTree(routes, routeIgnoreFilter)
|
||||
return routesRef.value
|
||||
}
|
||||
|
||||
/** 判断给定的route中是否具有给定perms列表的权限 */
|
||||
function hasPermission(route: RouteRecordRaw, perms: any[] = []) {
|
||||
if (!route.meta?.perm) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 递归寻找子节点perm
|
||||
if (route.meta?.perm === true && route.children?.length) {
|
||||
return filterAsyncRoutes(route.children, perms).length
|
||||
}
|
||||
|
||||
// 否则直接通过perm进行判断
|
||||
return perms.includes(
|
||||
Array.isArray(route.meta?.perm)
|
||||
? route.meta.perm[0]?.perm
|
||||
: route.meta?.perm,
|
||||
)
|
||||
}
|
||||
|
||||
// 过滤掉所有perm不匹配的路由
|
||||
function filterAsyncRoutes(routes: RouteRecordRaw[], perms: any[]) {
|
||||
return routes.reduce((rs: RouteRecordRaw[], route) => {
|
||||
if (hasPermission(route, perms)) {
|
||||
rs.push({
|
||||
...route,
|
||||
children: route.children ? filterAsyncRoutes(route.children, perms) : [],
|
||||
})
|
||||
}
|
||||
return rs
|
||||
}, [])
|
||||
}
|
||||
|
||||
async function buildMenu() {
|
||||
initMenu.value = false
|
||||
const appStore = useAppConfigStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const roleList = userStore.userInfo?.roles || []
|
||||
const permList = userStore.userInfo?.perms || []
|
||||
const permissionMode = unref(appStore.permissionMode)
|
||||
|
||||
const routeRoleFilter = (route: RouteRecordRaw) => {
|
||||
const { meta } = route
|
||||
const { roles } = meta || {}
|
||||
if (!roles) {
|
||||
return true
|
||||
}
|
||||
return roleList.some(role => roles.includes(role))
|
||||
}
|
||||
|
||||
const r = cloneDeep(unref(routesRef))
|
||||
let menuList: Menu[] = []
|
||||
switch (permissionMode) {
|
||||
case PermissionModeEnum.ROLE:
|
||||
{
|
||||
const routes = filterTree(r, routeRoleFilter)
|
||||
menuList = transformRouteToMenu(routes, true)
|
||||
}
|
||||
break
|
||||
case PermissionModeEnum.PERM:
|
||||
menuList = transformRouteToMenu(filterAsyncRoutes(r, permList), true)
|
||||
break
|
||||
}
|
||||
menuList.sort((a, b) => {
|
||||
return (a.meta?.order || 0) - (b.meta?.order || 0)
|
||||
})
|
||||
menuListRef.value = menuList
|
||||
initMenu.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
routesRef,
|
||||
menuListRef,
|
||||
initMenu,
|
||||
initRoutes,
|
||||
buildMenu,
|
||||
}
|
||||
}, {
|
||||
persist: {
|
||||
paths: ['menuListRef'],
|
||||
},
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useRouteStore as any, import.meta.hot))
|
@ -0,0 +1,55 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
|
||||
import { ThemeEnum } from '~/constants'
|
||||
|
||||
interface ThemeStoreState {
|
||||
themeConfig: ThemeColorConfig
|
||||
theme: ThemeEnum
|
||||
sidebar: ThemeEnum
|
||||
header: ThemeEnum
|
||||
}
|
||||
|
||||
const initState: ThemeStoreState = {
|
||||
themeConfig: {
|
||||
primaryColor: '#2a64d4',
|
||||
infoColor: '#2080F0',
|
||||
successColor: '#52c41a',
|
||||
warningColor: '#faad14',
|
||||
errorColor: '#D03050',
|
||||
textBaseColor: '#000000',
|
||||
bgBaseColor: '#ffffff',
|
||||
},
|
||||
theme: ThemeEnum.LIGHT,
|
||||
sidebar: ThemeEnum.DARK,
|
||||
header: ThemeEnum.DARK,
|
||||
}
|
||||
|
||||
export const useThemeStore = defineStore('APP_THEME', () => {
|
||||
const state = $ref<ThemeStoreState>(Object.assign({}, initState))
|
||||
|
||||
function setThemeConfig(config: Partial<ThemeColorConfig>) {
|
||||
state.themeConfig = { ...state.themeConfig, ...config }
|
||||
}
|
||||
|
||||
function setSidebarTheme(value: ThemeEnum) {
|
||||
state.sidebar = value
|
||||
}
|
||||
|
||||
function setHeaderTheme(value: ThemeEnum) {
|
||||
state.header = value
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
setThemeConfig,
|
||||
setSidebarTheme,
|
||||
setHeaderTheme,
|
||||
}
|
||||
}, {
|
||||
persist: {
|
||||
paths: Object.keys(initState),
|
||||
},
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useThemeStore as any, import.meta.hot))
|
@ -0,0 +1,172 @@
|
||||
* > .enter-x:nth-child(1) {
|
||||
transform: translateX(50px);
|
||||
}
|
||||
|
||||
* > .-enter-x:nth-child(1) {
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
* > .enter-x:nth-child(1),
|
||||
* > .-enter-x:nth-child(1) {
|
||||
z-index: 9;
|
||||
opacity: 0;
|
||||
animation: enter-x-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(1) {
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(1),
|
||||
* > .-enter-y:nth-child(1) {
|
||||
z-index: 9;
|
||||
opacity: 0;
|
||||
animation: enter-y-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
* > .enter-x:nth-child(2) {
|
||||
transform: translateX(50px);
|
||||
}
|
||||
|
||||
* > .-enter-x:nth-child(2) {
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
* > .enter-x:nth-child(2),
|
||||
* > .-enter-x:nth-child(2) {
|
||||
z-index: 8;
|
||||
opacity: 0;
|
||||
animation: enter-x-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(2) {
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(2),
|
||||
* > .-enter-y:nth-child(2) {
|
||||
z-index: 8;
|
||||
opacity: 0;
|
||||
animation: enter-y-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
* > .enter-x:nth-child(3) {
|
||||
transform: translateX(50px);
|
||||
}
|
||||
|
||||
* > .-enter-x:nth-child(3) {
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
* > .enter-x:nth-child(3),
|
||||
* > .-enter-x:nth-child(3) {
|
||||
z-index: 7;
|
||||
opacity: 0;
|
||||
animation: enter-x-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(3) {
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(3),
|
||||
* > .-enter-y:nth-child(3) {
|
||||
z-index: 7;
|
||||
opacity: 0;
|
||||
animation: enter-y-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
* > .enter-x:nth-child(4) {
|
||||
transform: translateX(50px);
|
||||
}
|
||||
|
||||
* > .-enter-x:nth-child(4) {
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
* > .enter-x:nth-child(4),
|
||||
* > .-enter-x:nth-child(4) {
|
||||
z-index: 6;
|
||||
opacity: 0;
|
||||
animation: enter-x-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(4) {
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(4),
|
||||
* > .-enter-y:nth-child(4) {
|
||||
z-index: 6;
|
||||
opacity: 0;
|
||||
animation: enter-y-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
* > .enter-x:nth-child(5) {
|
||||
transform: translateX(50px);
|
||||
}
|
||||
|
||||
* > .-enter-x:nth-child(5) {
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
* > .enter-x:nth-child(5),
|
||||
* > .-enter-x:nth-child(5) {
|
||||
z-index: 5;
|
||||
opacity: 0;
|
||||
animation: enter-x-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(5) {
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
* > .enter-y:nth-child(5),
|
||||
* > .-enter-y:nth-child(5) {
|
||||
z-index: 5;
|
||||
opacity: 0;
|
||||
animation: enter-y-animation 0.4s ease-in-out 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
@keyframes enter-x-animation {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes enter-y-animation {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-circle {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import './main.css'
|
||||
import './entry.css'
|
||||
import './variables.css'
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import 'uno.css'
|
@ -1,29 +1,90 @@
|
||||
@import './markdown.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
html {
|
||||
overflow: hidden;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
background: #121212;
|
||||
/* background: #121212; */
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:active,
|
||||
button,
|
||||
div,
|
||||
svg,
|
||||
span {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
input:-webkit-autofill {
|
||||
box-shadow: 0 0 0 1000px white inset !important;
|
||||
}
|
||||
|
||||
:-webkit-autofill {
|
||||
transition: background-color 5000s ease-in-out 0s !important;
|
||||
}
|
||||
|
||||
/* scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(144 147 153 / 30%);
|
||||
border-radius: 2px;
|
||||
box-shadow: inset 0 0 6px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #b6b7b9;
|
||||
}
|
||||
|
||||
/* nprogress */
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: rgb(13,148,136);
|
||||
opacity: 0.75;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99999;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
opacity: 0.75;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* app */
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html.color-weak {
|
||||
filter: invert(80%);
|
||||
}
|
||||
|
||||
html.gray-mode {
|
||||
filter: grayscale(100%);
|
||||
filter: progid:dximagetransform.microsoft.basicimage(grayscale=1);
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
:root {
|
||||
/* --primary-color: #0960bd;
|
||||
--success-color: #55d187;
|
||||
--error-color: #ed6f6f;
|
||||
--warning-color: #efbd47; */
|
||||
|
||||
--vxe-primary-color: var(--primary-color);
|
||||
--vxe-success-color: var(--success-color);
|
||||
--vxe-error-color: var(--error-color);
|
||||
--vxe-warning-color: var(--warning-color);
|
||||
|
||||
/* component */
|
||||
--component-background-color: #fff;
|
||||
|
||||
/* transition */
|
||||
--transition-bezier: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-all: all 0.3s var(--transition-bezier);
|
||||
|
||||
/* layout start */
|
||||
|
||||
--layout-color: #fff;
|
||||
--layout-container-background-color: #fff;
|
||||
--layout-border-color: rgb(239, 239, 245);
|
||||
|
||||
/* header */
|
||||
--header-height: 48px;
|
||||
--header-width: calc(100% - var(--aside-width));
|
||||
--header-background-color: #fff;
|
||||
--header-text-color: rgba(0, 0, 0, 0.85);
|
||||
--header-icon-color: var(--header-text-color);
|
||||
--header-action-hover-bg-color: #f6f6f6;
|
||||
|
||||
/* tab */
|
||||
--tab-bar-height: 36px;
|
||||
--tab-bar-width: calc(100% - var(--aside-width));
|
||||
|
||||
/* aside */
|
||||
--aside-height: calc(100% - var(--header-height));
|
||||
--aside-width: 210px;
|
||||
--aside-background-color: #001529;
|
||||
--aside-submenu-background-color: #0c2135;
|
||||
--aside-text-color: #fff;
|
||||
|
||||
--trigger-background-color: rgb(72, 72, 78);
|
||||
/*--trigger-border: 1px solid rgb(239, 239, 245);*/
|
||||
--trigger-border-color: rgb(239, 239, 245);
|
||||
/*--trigger-border-color: rgb(239, 239, 245);*/
|
||||
/*--trigger-icon-color: #f3f1f1;*/
|
||||
/*--trigger-icon-color: rgb(51, 54, 57);*/
|
||||
/*--trigger-hover-icon-color: #fff;*/
|
||||
|
||||
/* main */
|
||||
--main-height: calc(100% - var(--footer-height));
|
||||
--main-width: 100%;
|
||||
|
||||
/* footer */
|
||||
--footer-height: 60px;
|
||||
--footer-width: 100%;
|
||||
|
||||
/* layout end */
|
||||
--vxe-modal-header-background-color: #8eabf8;
|
||||
--vxe-modal-header-color: #fff;
|
||||
--vxe-table-header-font-color: #fff;
|
||||
--vxe-table-header-background-color: #8eabf8;
|
||||
--vxe-font-size: 14px;
|
||||
--vxe-font-color: #666;
|
||||
|
||||
/* mix sidebar */
|
||||
--mix-sider-z-index: 500;
|
||||
}
|
||||
|
||||
:root[class=dark] {
|
||||
--layout-border-color: rgba(255, 255, 255, 0.09);
|
||||
--layout-container-background-color: rgb(16, 16, 20);
|
||||
|
||||
--component-background-color: rgb(24, 24, 28);
|
||||
--vxe-modal-header-background-color: #27355d;
|
||||
--vxe-form-background-color: #212b4b;
|
||||
--vxe-table-body-background-color: #212b4b;
|
||||
--vxe-grid-maximize-background-color: #212b4b;
|
||||
--vxe-textarea-background-color: #212b4b;
|
||||
--vxe-table-row-current-background-color: #334579;
|
||||
--vxe-table-column-current-background-color: #334579;
|
||||
--vxe-table-column-hover-background-color: #334579;
|
||||
--vxe-table-row-hover-background-color: #405492;
|
||||
--vxe-table-row-hover-current-background-color: #4d63ad;
|
||||
--vxe-input-date-picker-hover-background-color: #4d63ad;
|
||||
--vxe-loading-background-color: #677dc780;
|
||||
--vxe-loading-color: #75bcea;
|
||||
--vxe-table-row-checkbox-checked-background-color: #405492;
|
||||
--vxe-table-row-radio-checked-background-color: #405492;
|
||||
--vxe-modal-body-background-color: #212b4b;
|
||||
--vxe-button-default-background-color: #212b4b;
|
||||
--vxe-pulldown-panel-background-color: #212b4b;
|
||||
--vxe-input-background-color: #212b4b;
|
||||
--vxe-select-panel-background-color: #212b4b;
|
||||
--vxe-radio-button-default-background-color: #212b4b;
|
||||
--vxe-toolbar-background-color: #212b4b;
|
||||
--vxe-pager-background-color: #212b4b;
|
||||
--vxe-pager-perfect-background-color: #212b4b;
|
||||
--vxe-table-tree-node-line-color: #bec1c5;
|
||||
--vxe-table-border-color: #e2ebf6;
|
||||
--vxe-textarea-count-color: #e2ebf6;
|
||||
--vxe-table-header-font-color: #fff;
|
||||
--vxe-table-header-background-color: #27355d;
|
||||
--vxe-select-option-hover-background-color: #27355d;
|
||||
--vxe-font-size: 14px;
|
||||
--vxe-font-color: #e2ebf6;
|
||||
--vxe-table-font-color: #e2ebf6;
|
||||
--vxe-font-l10-color: #f5f8fc;
|
||||
--vxe-font-l20-color: #fff;
|
||||
|
||||
/*
|
||||
--vxe-font-l10-color:#{darken(#e2ebf6,10%)};
|
||||
--vxe-font-l20-color:#{darken(#e2ebf6,20%)};
|
||||
*/
|
||||
--vxe-pager-font-color: #ffffffe6;
|
||||
--vxe-row-selected-bg-color: #d39a26;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue