新增多标签页

This commit is contained in:
chuan_wuhao 2022-11-24 14:53:23 +08:00
parent 14ae981efb
commit 75b0f434cf
25 changed files with 250 additions and 79 deletions

View File

@ -2,7 +2,8 @@
"GlobalMenuOptions": { "GlobalMenuOptions": {
"Dashboard": "Home", "Dashboard": "Home",
"Rely": "Rely", "Rely": "Rely",
"RelyAbout": "Rely About" "RelyAbout": "Rely About",
"Error": "Error Page"
}, },
"LayoutHeaderTooltipOptions": { "LayoutHeaderTooltipOptions": {
"Reload": "Reload Current Page", "Reload": "Reload Current Page",

View File

@ -2,7 +2,8 @@
"GlobalMenuOptions": { "GlobalMenuOptions": {
"Dashboard": "首页", "Dashboard": "首页",
"Rely": "依赖项", "Rely": "依赖项",
"RelyAbout": "关于" "RelyAbout": "关于",
"Error": "错误页"
}, },
"LayoutHeaderTooltipOptions": { "LayoutHeaderTooltipOptions": {
"Reload": "刷新当前页面", "Reload": "刷新当前页面",

View File

@ -17,6 +17,7 @@
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"naive-ui": "^2.34.0", "naive-ui": "^2.34.0",
"pinia": "^2.0.17", "pinia": "^2.0.17",
"pinia-plugin-persistedstate": "^2.4.0",
"sass": "^1.54.3", "sass": "^1.54.3",
"scrollreveal": "^4.0.9", "scrollreveal": "^4.0.9",
"vue": "^3.2.37", "vue": "^3.2.37",

4
src/icons/error.svg Normal file
View File

@ -0,0 +1,4 @@
<svg t="1669270375884" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14105" width="200" height="200">
<path d="M832 160h-96v-64h-64v192h64v-64H832c17.92 0 32 14.08 32 32v96h-704V256c0-17.92 14.08-32 32-32h32v-64H192c-53.12 0-96 42.88-96 96v576c0 53.12 42.88 96 96 96h640c53.12 0 96-42.88 96-96V256c0-53.12-42.88-96-96-96z m0 704H192c-17.92 0-32-14.08-32-32V416h704V832c0 17.92-14.08 32-32 32z" fill="currentColor" p-id="14106"></path>
<path d="M352 224h256v-64h-256v-64h-64v192h64zM575.36 524.8L512 588.16l-63.36-63.36-45.44 45.44 63.36 63.36-63.36 63.36 45.44 45.44 63.36-63.36 63.36 63.36 45.44-45.44-63.36-63.36 63.36-63.36z" fill="currentColor" p-id="14107"></path>
</svg>

After

Width:  |  Height:  |  Size: 726 B

View File

@ -1,3 +0,0 @@
.setting-drawer__space {
width: 100%;
}

View File

@ -6,7 +6,11 @@ const LayoutMenu = defineComponent({
setup() { setup() {
const menuStore = useMenu() const menuStore = useMenu()
const { menuModelValueChange, setupAppRoutes, collapsedMenu } = menuStore const { menuModelValueChange, setupAppRoutes, collapsedMenu } = menuStore
const modelMenuKey = ref(menuStore.menuKey) const modelMenuKey = computed({
get: () => menuStore.menuKey,
// eslint-disable-next-line @typescript-eslint/no-empty-function
set: () => {},
})
const modelMenuOptions = computed(() => menuStore.options) const modelMenuOptions = computed(() => menuStore.options)
const modelCollapsed = computed(() => menuStore.collapsed) const modelCollapsed = computed(() => menuStore.collapsed)

View File

@ -0,0 +1,14 @@
$space: $layoutRouterViewContainer / 2;
.menu-tag {
height: $layoutMenuHeight;
& .menu-tag-sapce {
width: calc(100% - $space * 2);
padding: $layoutRouterViewContainer / 2;
}
& .n-tag {
cursor: pointer;
}
}

View File

@ -0,0 +1,61 @@
import './index.scss'
import { NScrollbar, NTag, NSpace } from 'naive-ui'
import { useMenu } from '@/store'
import type { MenuOption } from 'naive-ui'
const MenuTag = defineComponent({
name: 'MenuTag',
setup() {
const menuStore = useMenu()
const { menuTagOptions, menuKey } = storeToRefs(menuStore)
const { menuModelValueChange, spliceMenTagOptions } = menuStore
const handleCloseTag = (idx: number) => {
spliceMenTagOptions(idx)
if (menuKey.value !== '/dashboard') {
const options = menuTagOptions.value as MenuOption[]
const length = options.length
const tag = options[length - 1]
menuModelValueChange(tag.key as string, tag)
}
}
const handleTagClick = (item: MenuOption) => {
menuModelValueChange(item.key as string, item)
}
return {
menuTagOptions,
menuModelValueChange,
handleCloseTag,
menuKey,
handleTagClick,
}
},
render() {
return (
<NScrollbar class="menu-tag" xScrollable>
<NSpace class="menu-tag-sapce" wrap={false} align="center">
{this.menuTagOptions.map((curr: MenuOption, idx) => (
<NTag
closable={
curr.key !== '/dashboard' && this.menuTagOptions.length > 1
}
onClose={() => this.handleCloseTag(idx)}
type={curr.key === this.menuKey ? 'success' : 'default'}
onClick={this.handleTagClick.bind(this, curr)}
>
{typeof curr.label === 'function' ? curr.label() : curr.label}
</NTag>
))}
</NSpace>
</NScrollbar>
)
},
})
export default MenuTag

View File

@ -0,0 +1,8 @@
.setting-drawer__space {
width: 100%;
& .n-descriptions-table-content {
display: flex !important;
justify-content: space-between;
}
}

View File

@ -7,6 +7,8 @@ import {
NSwitch, NSwitch,
NColorPicker, NColorPicker,
NTooltip, NTooltip,
NDescriptions,
NDescriptionsItem,
} from 'naive-ui' } from 'naive-ui'
import RayIcon from '@/components/RayIcon/index' import RayIcon from '@/components/RayIcon/index'
import { useSwatchesColorOptions } from './hook' import { useSwatchesColorOptions } from './hook'
@ -35,8 +37,9 @@ const SettingDrawer = defineComponent({
const { t } = useI18n() const { t } = useI18n()
const settingStore = useSetting() const settingStore = useSetting()
const { changeTheme, changePrimaryColor } = settingStore const { changeTheme, changePrimaryColor, changeMenuTagLog } = settingStore
const { themeValue, primaryColorOverride } = storeToRefs(settingStore) const { themeValue, primaryColorOverride, menuTagLog } =
storeToRefs(settingStore)
const modelShow = computed({ const modelShow = computed({
get: () => props.show, get: () => props.show,
@ -57,6 +60,8 @@ const SettingDrawer = defineComponent({
changeTheme, changeTheme,
themeValue, themeValue,
primaryColorOverride, primaryColorOverride,
changeMenuTagLog,
menuTagLog,
} }
}, },
render() { render() {
@ -119,6 +124,15 @@ const SettingDrawer = defineComponent({
v-model:value={this.primaryColorOverride.common.primaryColor} v-model:value={this.primaryColorOverride.common.primaryColor}
onUpdateValue={this.changePrimaryColor.bind(this)} onUpdateValue={this.changePrimaryColor.bind(this)}
/> />
<NDivider titlePlacement="center"></NDivider>
<NDescriptions labelPlacement="left" column={1}>
<NDescriptionsItem label="显示多标签">
<NSwitch
v-model:value={this.menuTagLog}
onUpdateValue={this.changeMenuTagLog.bind(this)}
/>
</NDescriptionsItem>
</NDescriptions>
</NSpace> </NSpace>
</NDrawerContent> </NDrawerContent>
</NDrawer> </NDrawer>

View File

@ -1,7 +1,7 @@
import './index.scss' import './index.scss'
import { NLayoutHeader, NSpace, NTooltip, NDropdown } from 'naive-ui' import { NLayoutHeader, NSpace, NTooltip, NDropdown } from 'naive-ui'
import RayIcon from '@/components/RayIcon/index' import RayIcon from '@/components/RayIcon/index'
import { useMenu, useSetting } from '@/store' import { useSetting } from '@/store'
import { useLanguageOptions } from '@/language/index' import { useLanguageOptions } from '@/language/index'
import SettingDrawer from './Components/SettingDrawer/index' import SettingDrawer from './Components/SettingDrawer/index'
import { useAvatarOptions } from './hook' import { useAvatarOptions } from './hook'
@ -12,12 +12,10 @@ import type { IconEventMapOptions, IconEventMap } from './type'
const SiderBar = defineComponent({ const SiderBar = defineComponent({
name: 'SiderBar', name: 'SiderBar',
setup() { setup() {
const menuStore = useMenu()
const settingStore = useSetting() const settingStore = useSetting()
const { t } = useI18n() const { t } = useI18n()
const { changeReloadLog } = menuStore const { updateLocale, changeReloadLog } = settingStore
const { updateLocale } = settingStore
const modelDrawerPlacement = ref(settingStore.drawerPlacement) const modelDrawerPlacement = ref(settingStore.drawerPlacement)
const showSettings = ref(false) const showSettings = ref(false)

View File

@ -6,7 +6,8 @@
} }
& .layout-content__router-view { & .layout-content__router-view {
height: calc(100% - $layoutHeaderHeight); // height: calc(100% - $layoutHeaderHeight - $layoutMenuHeight);
padding: $layoutRouterViewContainer; height: var(--layout-content-height);
padding: $layoutRouterViewContainer / 2;
} }
} }

View File

@ -1,31 +1,53 @@
import './index.scss' import './index.scss'
import { Transition } from 'vue'
import { NLayout, NLayoutContent } from 'naive-ui' import { NLayout, NLayoutContent } from 'naive-ui'
import RayTransitionComponent from '@/components/RayTransitionComponent/index.vue' import RayTransitionComponent from '@/components/RayTransitionComponent/index.vue'
import LayoutMenu from './Menu/index' import LayoutMenu from './components/Menu/index'
import SiderBar from './SiderBar/index' import SiderBar from './components/SiderBar/index'
import { useMenu } from '@/store' import MenuTag from './components/MenuTag/index'
import { useSetting } from '@/store'
const Layout = defineComponent({ const Layout = defineComponent({
name: 'Layout', name: 'Layout',
props: {}, props: {},
setup() { setup() {
const menuStore = useMenu() const menuStore = useSetting()
const { height: windowHeight } = useWindowSize() const { height: windowHeight } = useWindowSize()
const modelReloadRoute = computed(() => menuStore.reloadRouteLog) const modelReloadRoute = computed(() => menuStore.reloadRouteLog)
const modelMenuTagLog = computed(() => menuStore.menuTagLog)
const cssVarsRef = computed(() => {
let cssVar = {}
if (menuStore.menuTagLog) {
cssVar = {
'--layout-content-height': 'calc(100% - 110px)',
}
} else {
cssVar = {
'--layout-content-height': 'calc(100% - 64px)',
}
}
return cssVar
})
return { return {
windowHeight, windowHeight,
modelReloadRoute, modelReloadRoute,
modelMenuTagLog,
cssVarsRef,
} }
}, },
render() { render() {
return ( return (
<div class="layout" style={[`height: ${this.windowHeight}px`]}> <div
class="layout"
style={[`height: ${this.windowHeight}px`, this.cssVarsRef]}
>
<NLayout class="layout-full" hasSider> <NLayout class="layout-full" hasSider>
<LayoutMenu /> <LayoutMenu />
<NLayout> <NLayout>
<SiderBar /> <SiderBar />
{this.modelMenuTagLog ? <MenuTag /> : ''}
<NLayoutContent <NLayoutContent
class="layout-content__router-view" class="layout-content__router-view"
nativeScrollbar={false} nativeScrollbar={false}

View File

@ -0,0 +1,9 @@
export default {
path: '/error',
name: 'error',
component: () => import('@/views/error/index'),
meta: {
i18nKey: 'Error',
icon: 'error',
},
}

View File

@ -1,7 +1,8 @@
import dashboard from './dashboard' import dashboard from './dashboard'
import reyl from './rely' import reyl from './rely'
import error from './error'
const routes = [dashboard, reyl] const routes = [dashboard, error, reyl]
export default routes export default routes

View File

@ -1,3 +1,5 @@
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import type { App } from 'vue' import type { App } from 'vue'
export { useSetting } from './modules/setting' // import { useSetting } from '@/store' 即可使用 export { useSetting } from './modules/setting' // import { useSetting } from '@/store' 即可使用
@ -7,4 +9,6 @@ const store = createPinia()
export const setupStore = (app: App<Element>) => { export const setupStore = (app: App<Element>) => {
app.use(store) app.use(store)
store.use(piniaPluginPersistedstate)
} }

View File

@ -9,15 +9,28 @@ export const useMenu = defineStore('menu', () => {
const router = useRouter() const router = useRouter()
const { t } = useI18n() const { t } = useI18n()
const cacheMenuKey = getCache('menuKey') === 'no' ? '' : getCache('menuKey') const cacheMenuKey =
getCache('menuKey') === 'no' ? '/dashboard' : getCache('menuKey')
const menuState = reactive({ const menuState = reactive({
menuKey: cacheMenuKey as string | null, // 当前菜单 `key` menuKey: cacheMenuKey as string | null, // 当前菜单 `key`
options: [] as RouteRecordRaw[], // 菜单列表 options: [] as RouteRecordRaw[], // 菜单列表
collapsed: false, // 是否折叠菜单 collapsed: false, // 是否折叠菜单
reloadRouteLog: true, // 刷新路由开关 menuTagOptions: [] as RouteRecordRaw[],
}) })
const handleMenuTagOptions = (item: RouteRecordRaw) => {
if (item.path !== menuState.menuKey) {
const tag = menuState.menuTagOptions.find(
(curr) => curr.path === item.path,
)
if (!tag) {
menuState.menuTagOptions.push(item)
}
}
}
/** /**
* *
* @param key `key` * @param key `key`
@ -26,6 +39,8 @@ export const useMenu = defineStore('menu', () => {
* `menu key` * `menu key`
*/ */
const menuModelValueChange = (key: string, item: MenuOption) => { const menuModelValueChange = (key: string, item: MenuOption) => {
handleMenuTagOptions(item as unknown as RouteRecordRaw)
menuState.menuKey = key menuState.menuKey = key
router.push(`${item.path}`) router.push(`${item.path}`)
@ -69,7 +84,14 @@ export const useMenu = defineStore('menu', () => {
), ),
} }
return curr.meta?.icon ? Object.assign(route, expandIcon) : route const attr = curr.meta?.icon ? Object.assign(route, expandIcon) : route
// 初始化 `menu tag`
if (curr.path === cacheMenuKey) {
menuState.menuTagOptions.push(attr)
}
return attr
}) })
} }
@ -83,17 +105,14 @@ export const useMenu = defineStore('menu', () => {
const collapsedMenu = (collapsed: boolean) => const collapsedMenu = (collapsed: boolean) =>
(menuState.collapsed = collapsed) (menuState.collapsed = collapsed)
/** const spliceMenTagOptions = (idx: number) =>
* menuState.menuTagOptions.splice(idx, 1)
* @param bool
*/
const changeReloadLog = (bool: boolean) => (menuState.reloadRouteLog = bool)
return { return {
...toRefs(menuState), ...toRefs(menuState),
menuModelValueChange, menuModelValueChange,
setupAppRoutes, setupAppRoutes,
collapsedMenu, collapsedMenu,
changeReloadLog, spliceMenTagOptions,
} }
}) })

View File

@ -1,49 +1,57 @@
import { setCache, getCache } from '@/utils/cache' export const useSetting = defineStore(
'setting',
export const useSetting = defineStore('setting', () => { () => {
const cachePrimaryColor: string = const settingState = reactive({
getCache('primaryColor', 'localStorage') === 'no' drawerPlacement: 'right' as NaiveDrawerPlacement,
? '#18A058' primaryColorOverride: {
: getCache('primaryColor', 'localStorage') common: {
const cacheTheme = primaryColor: '#18A058', // 主题色
getCache('theme', 'localStorage') === 'no' },
? false
: getCache('theme', 'localStorage')
const settingState = reactive({
drawerPlacement: 'right' as NaiveDrawerPlacement,
primaryColorOverride: {
common: {
primaryColor: cachePrimaryColor, // 主题色
}, },
themeValue: false, // `true` 为黑夜主题, `false` 为白色主题
reloadRouteLog: true, // 刷新路由开关
menuTagLog: true, // 多标签页开关
})
const { locale } = useI18n()
const updateLocale = (key: string) => {
// TODO: 修改语言
locale.value = key
}
const changeTheme = (bool: boolean) => {
settingState.themeValue = bool
}
const changePrimaryColor = (value: string) => {
settingState.primaryColorOverride.common.primaryColor = value
}
/**
*
* @param bool
*/
const changeReloadLog = (bool: boolean) =>
(settingState.reloadRouteLog = bool)
/**
*
* @param bool
*/
const changeMenuTagLog = (bool: boolean) => (settingState.menuTagLog = bool)
return {
...toRefs(settingState),
updateLocale,
changeTheme,
changePrimaryColor,
changeReloadLog,
changeMenuTagLog,
}
},
{
persist: {
key: 'piniaSettingStore',
}, },
themeValue: cacheTheme, // `true` 为黑夜主题, `false` 为白色主题 },
}) )
const { locale } = useI18n()
const updateLocale = (key: string) => {
// TODO: 修改语言
locale.value = key
setCache('localeLanguage', key, 'localStorage')
}
const changeTheme = (bool: boolean) => {
settingState.themeValue = bool
setCache('theme', bool, 'localStorage')
}
const changePrimaryColor = (value: string) => {
settingState.primaryColorOverride.common.primaryColor = value
setCache('primaryColor', value, 'localStorage')
}
return {
...toRefs(settingState),
updateLocale,
changeTheme,
changePrimaryColor,
}
})

View File

@ -1,2 +1,3 @@
$layoutRouterViewContainer: 18px; $layoutRouterViewContainer: 18px;
$layoutHeaderHeight: 64px; $layoutHeaderHeight: 64px;
$layoutMenuHeight: 46px;

View File

@ -22,13 +22,11 @@ const Login = defineComponent({
const { t } = useI18n() const { t } = useI18n()
const { height: windowHeight } = useWindowSize() const { height: windowHeight } = useWindowSize()
const settingStore = useSetting() const settingStore = useSetting()
const { themeValue } = storeToRefs(settingStore)
const { updateLocale } = settingStore const { updateLocale } = settingStore
return { return {
...toRefs(state), ...toRefs(state),
windowHeight, windowHeight,
themeValue,
updateLocale, updateLocale,
ray: t, ray: t,
} }

View File

@ -3489,6 +3489,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pinia-plugin-persistedstate@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-2.4.0.tgz#fda569b3c397517a0cf8aba83a628283767da620"
integrity sha512-bQcpv47jk3ISl+InuJWsFaS/K7pRZ97kfoD2WCf/suhnlLy48k3BnFM2tI6YZ1xMsDaPv4yOsaPuPAUuSmEO2Q==
pinia@^2.0.17: pinia@^2.0.17:
version "2.0.17" version "2.0.17"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.17.tgz#f925e5e4f73c15e16dfb4838176a9ca50752f26b" resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.17.tgz#f925e5e4f73c15e16dfb4838176a9ca50752f26b"