mirror of
https://github.com/XiaoDaiGua-Ray/ray-template.git
synced 2025-04-05 19:42:07 +08:00
修复一堆小问题
This commit is contained in:
parent
daf46ebe83
commit
bded833814
@ -4,10 +4,6 @@
|
||||
|
||||
> 模板按照个人习惯进行搭建, 可以根据个人喜好进行更改. 预设了一些组件库、国际化库的东西. 建议使用 `naive-ui` 作为组件库.
|
||||
|
||||
## 预览地址
|
||||
|
||||
[**`Ray Template`**](https://xiaodaigua-ray.github.io/#/)
|
||||
|
||||
## 项目说明
|
||||
|
||||
> 项目采用 `Vue 3` `TypeScript` `TSX` `Vite` 进行开发, 已经集成了一些常用的开发库, 进行了一些 `Vite` 相关配置, 例如全局自动引入、`GZ` 打包、按需引入打包、[reactivityTransform](https://vuejs.org/guide/extras/reactivity-transform.html)等, 解放你的双手. 国际化插件, 按照项目需求自己取舍. 引入了比较火的 `hook` 库 [@vueuse](https://vueuse.org/), 极大提高你的搬砖效率. `小提醒: 为了避免使用 @vueuse 时出现奇奇怪怪的错误(例如: useDraggable 在使用的时候, TSX 形式开发会失效), 建议采用 <script setup /> 形式进行开发`. 可以根据自己项目实际需求进行配置 `px` 与 'rem' 转换比例(使用 `postcss-pxtorem` 与 `autoprefixer` 实现).
|
||||
|
4
dist/production-dist/index.html
vendored
4
dist/production-dist/index.html
vendored
@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/ray.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ray template</title>
|
||||
<script type="module" crossorigin src="/assets/index.f568b427.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.4a9527dd.css">
|
||||
<script type="module" crossorigin src="/assets/index.c3f05d90.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.61e7b6d0.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
4
src/icons/error.svg
Normal file
4
src/icons/error.svg
Normal 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 |
50
src/layout/components/Menu/index.tsx
Normal file
50
src/layout/components/Menu/index.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { NMenu, NLayoutSider } from 'naive-ui'
|
||||
import { useMenu } from '@/store'
|
||||
|
||||
const LayoutMenu = defineComponent({
|
||||
name: 'LayoutMenu',
|
||||
setup() {
|
||||
const menuStore = useMenu()
|
||||
const { menuModelValueChange, setupAppRoutes, collapsedMenu } = menuStore
|
||||
const modelMenuKey = computed({
|
||||
get: () => menuStore.menuKey,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
set: () => {},
|
||||
})
|
||||
const modelMenuOptions = computed(() => menuStore.options)
|
||||
const modelCollapsed = computed(() => menuStore.collapsed)
|
||||
|
||||
setupAppRoutes()
|
||||
|
||||
return {
|
||||
modelMenuKey,
|
||||
menuModelValueChange,
|
||||
modelMenuOptions,
|
||||
modelCollapsed,
|
||||
collapsedMenu,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NLayoutSider
|
||||
bordered
|
||||
showTrigger
|
||||
collapseMode="width"
|
||||
collapsedWidth={64}
|
||||
onUpdateCollapsed={this.collapsedMenu.bind(this)}
|
||||
>
|
||||
<NMenu
|
||||
v-model:value={this.modelMenuKey}
|
||||
options={this.modelMenuOptions as NaiveMenuOptions[]}
|
||||
indent={24}
|
||||
collapsed={this.modelCollapsed}
|
||||
collapsedIconSize={22}
|
||||
collapsedWidth={64}
|
||||
onUpdateValue={this.menuModelValueChange.bind(this)}
|
||||
/>
|
||||
</NLayoutSider>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default LayoutMenu
|
14
src/layout/components/MenuTag/index.scss
Normal file
14
src/layout/components/MenuTag/index.scss
Normal file
@ -0,0 +1,14 @@
|
||||
$space: calc($layoutRouterViewContainer / 2);
|
||||
|
||||
.menu-tag {
|
||||
height: $layoutMenuHeight;
|
||||
|
||||
& .menu-tag-sapce {
|
||||
width: calc(100% - $space * 2);
|
||||
padding: $space;
|
||||
}
|
||||
|
||||
& .n-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
61
src/layout/components/MenuTag/index.tsx
Normal file
61
src/layout/components/MenuTag/index.tsx
Normal 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
|
@ -0,0 +1,7 @@
|
||||
export const useSwatchesColorOptions = () => [
|
||||
'#FFFFFF',
|
||||
'#18A058',
|
||||
'#2080F0',
|
||||
'#F0A020',
|
||||
'rgba(208, 48, 80, 1)',
|
||||
]
|
@ -0,0 +1,8 @@
|
||||
.setting-drawer__space {
|
||||
width: 100%;
|
||||
|
||||
& .n-descriptions-table-content {
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
import './index.scss'
|
||||
import {
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
NDivider,
|
||||
NSpace,
|
||||
NSwitch,
|
||||
NColorPicker,
|
||||
NTooltip,
|
||||
NDescriptions,
|
||||
NDescriptionsItem,
|
||||
} from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
import { useSwatchesColorOptions } from './hook'
|
||||
import { useSetting } from '@/store'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
const SettingDrawer = defineComponent({
|
||||
name: 'SettingDrawer',
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placement: {
|
||||
type: String as PropType<NaiveDrawerPlacement>,
|
||||
default: 'right',
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 280,
|
||||
},
|
||||
},
|
||||
emits: ['update:show'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSetting()
|
||||
|
||||
const { changeTheme, changePrimaryColor, changeMenuTagLog } = settingStore
|
||||
const { themeValue, primaryColorOverride, menuTagLog } =
|
||||
storeToRefs(settingStore)
|
||||
|
||||
const modelShow = computed({
|
||||
get: () => props.show,
|
||||
set: (bool) => {
|
||||
emit('update:show', bool)
|
||||
},
|
||||
})
|
||||
|
||||
const handleRailStyle = () => ({
|
||||
backgroundColor: '#000000',
|
||||
})
|
||||
|
||||
return {
|
||||
modelShow,
|
||||
ray: t,
|
||||
handleRailStyle,
|
||||
changePrimaryColor,
|
||||
changeTheme,
|
||||
themeValue,
|
||||
primaryColorOverride,
|
||||
changeMenuTagLog,
|
||||
menuTagLog,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NDrawer
|
||||
v-model:show={this.modelShow}
|
||||
placement={this.placement}
|
||||
width={this.width}
|
||||
>
|
||||
<NDrawerContent title={this.ray('LayoutHeaderSettingOptions.Title')}>
|
||||
<NSpace class="setting-drawer__space" vertical>
|
||||
<NDivider titlePlacement="center">
|
||||
{this.ray('LayoutHeaderSettingOptions.ThemeOptions.Title')}
|
||||
</NDivider>
|
||||
<NSpace justify="center">
|
||||
<NTooltip>
|
||||
{{
|
||||
trigger: () => (
|
||||
<NSwitch
|
||||
v-model:value={this.themeValue}
|
||||
railStyle={this.handleRailStyle.bind(this)}
|
||||
onUpdateValue={this.changeTheme.bind(this)}
|
||||
>
|
||||
{{
|
||||
'checked-icon': () =>
|
||||
h(
|
||||
RayIcon,
|
||||
{
|
||||
name: 'dark',
|
||||
},
|
||||
{},
|
||||
),
|
||||
'unchecked-icon': () =>
|
||||
h(
|
||||
RayIcon,
|
||||
{
|
||||
name: 'light',
|
||||
},
|
||||
{},
|
||||
),
|
||||
}}
|
||||
</NSwitch>
|
||||
),
|
||||
default: () =>
|
||||
this.themeValue
|
||||
? this.ray('LayoutHeaderSettingOptions.ThemeOptions.Dark')
|
||||
: this.ray(
|
||||
'LayoutHeaderSettingOptions.ThemeOptions.Light',
|
||||
),
|
||||
}}
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
<NDivider titlePlacement="center">
|
||||
{this.ray(
|
||||
'LayoutHeaderSettingOptions.ThemeOptions.PrimaryColorConfig',
|
||||
)}
|
||||
</NDivider>
|
||||
<NColorPicker
|
||||
swatches={useSwatchesColorOptions()}
|
||||
v-model:value={this.primaryColorOverride.common.primaryColor}
|
||||
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>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default SettingDrawer
|
14
src/layout/components/SiderBar/hook.ts
Normal file
14
src/layout/components/SiderBar/hook.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const useAvatarOptions = () => [
|
||||
{
|
||||
key: 'person',
|
||||
label: '个人信息',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
key: 'd1',
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
label: '退出登陆',
|
||||
},
|
||||
]
|
16
src/layout/components/SiderBar/index.scss
Normal file
16
src/layout/components/SiderBar/index.scss
Normal file
@ -0,0 +1,16 @@
|
||||
.layout-header {
|
||||
height: $layoutHeaderHeight;
|
||||
padding: 0 $layoutRouterViewContainer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .layout-header__method {
|
||||
width: 100%;
|
||||
|
||||
& .layout-header__method--icon {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
175
src/layout/components/SiderBar/index.tsx
Normal file
175
src/layout/components/SiderBar/index.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import './index.scss'
|
||||
import { NLayoutHeader, NSpace, NTooltip, NDropdown } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
import { useSetting } from '@/store'
|
||||
import { useLanguageOptions } from '@/language/index'
|
||||
import SettingDrawer from './Components/SettingDrawer/index'
|
||||
import { useAvatarOptions } from './hook'
|
||||
import { removeCache } from '@/utils/cache'
|
||||
|
||||
import type { IconEventMapOptions, IconEventMap } from './type'
|
||||
|
||||
const SiderBar = defineComponent({
|
||||
name: 'SiderBar',
|
||||
setup() {
|
||||
const settingStore = useSetting()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { updateLocale, changeReloadLog } = settingStore
|
||||
const modelDrawerPlacement = ref(settingStore.drawerPlacement)
|
||||
const showSettings = ref(false)
|
||||
|
||||
const leftIconOptions = [
|
||||
{
|
||||
name: 'reload',
|
||||
size: 18,
|
||||
tooltip: 'LayoutHeaderTooltipOptions.Reload',
|
||||
},
|
||||
]
|
||||
const rightIconOptions = [
|
||||
{
|
||||
name: 'language',
|
||||
size: 18,
|
||||
tooltip: '',
|
||||
dropdown: {
|
||||
methodName: 'handleSelect', // 默认为 `handleSelect`
|
||||
switch: true,
|
||||
options: useLanguageOptions(),
|
||||
handleSelect: (key: string | number) => updateLocale(String(key)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'github',
|
||||
size: 18,
|
||||
tooltip: 'LayoutHeaderTooltipOptions.Github',
|
||||
},
|
||||
{
|
||||
name: 'setting',
|
||||
size: 18,
|
||||
tooltip: 'LayoutHeaderTooltipOptions.Setting',
|
||||
},
|
||||
{
|
||||
name: 'ray',
|
||||
size: 22,
|
||||
tooltip: '',
|
||||
dropdown: {
|
||||
methodName: 'handleSelect', // 默认为 `handleSelect`
|
||||
switch: true,
|
||||
options: useAvatarOptions(),
|
||||
handleSelect: (key: string | number) => {
|
||||
if (key === 'logout') {
|
||||
window.$dialog.warning({
|
||||
title: '提示',
|
||||
content: '您确定要退出登录吗',
|
||||
positiveText: '确定',
|
||||
negativeText: '不确定',
|
||||
onPositiveClick: () => {
|
||||
window.$message.info('账号退出中...')
|
||||
|
||||
removeCache('all-sessionStorage')
|
||||
|
||||
setTimeout(() => window.location.reload(), 2 * 1000)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
window.$message.info('这个人很懒, 没做这个功能~')
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
const iconEventMap: IconEventMapOptions = {
|
||||
reload: () => {
|
||||
changeReloadLog(false)
|
||||
|
||||
setTimeout(() => changeReloadLog(true))
|
||||
},
|
||||
setting: () => {
|
||||
showSettings.value = true
|
||||
},
|
||||
github: () => {
|
||||
window.open('https://github.com/XiaoDaiGua-Ray/ray-template')
|
||||
},
|
||||
}
|
||||
|
||||
const handleIconClick = (key: IconEventMap) => {
|
||||
iconEventMap[key]?.()
|
||||
}
|
||||
|
||||
return {
|
||||
leftIconOptions,
|
||||
rightIconOptions,
|
||||
t,
|
||||
handleIconClick,
|
||||
modelDrawerPlacement,
|
||||
showSettings,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NLayoutHeader class="layout-header" bordered>
|
||||
<NSpace
|
||||
class="layout-header__method"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
<NSpace align="center">
|
||||
{this.leftIconOptions.map((curr) => (
|
||||
<NTooltip>
|
||||
{{
|
||||
trigger: () => (
|
||||
<RayIcon
|
||||
customClassName="layout-header__method--icon"
|
||||
name={curr.name}
|
||||
size={curr.size}
|
||||
onClick={this.handleIconClick.bind(this, curr.name)}
|
||||
/>
|
||||
),
|
||||
default: () => this.t(curr.tooltip),
|
||||
}}
|
||||
</NTooltip>
|
||||
))}
|
||||
</NSpace>
|
||||
<NSpace align="center">
|
||||
{this.rightIconOptions.map((curr) =>
|
||||
curr.dropdown?.switch ? (
|
||||
<NDropdown
|
||||
options={curr.dropdown.options}
|
||||
onSelect={
|
||||
curr.dropdown[curr.dropdown.methodName ?? 'handleSelect']
|
||||
}
|
||||
>
|
||||
<RayIcon
|
||||
customClassName="layout-header__method--icon"
|
||||
name={curr.name}
|
||||
size={curr.size}
|
||||
/>
|
||||
</NDropdown>
|
||||
) : (
|
||||
<NTooltip>
|
||||
{{
|
||||
trigger: () => (
|
||||
<RayIcon
|
||||
customClassName="layout-header__method--icon"
|
||||
name={curr.name}
|
||||
size={curr.size}
|
||||
onClick={this.handleIconClick.bind(this, curr.name)}
|
||||
/>
|
||||
),
|
||||
default: () => this.t(curr.tooltip),
|
||||
}}
|
||||
</NTooltip>
|
||||
),
|
||||
)}
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
<SettingDrawer
|
||||
v-model:show={this.showSettings}
|
||||
placement={this.modelDrawerPlacement}
|
||||
/>
|
||||
</NLayoutHeader>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default SiderBar
|
5
src/layout/components/SiderBar/type.ts
Normal file
5
src/layout/components/SiderBar/type.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface IconEventMapOptions {
|
||||
[propName: string]: (...args: unknown[]) => unknown
|
||||
}
|
||||
|
||||
export type IconEventMap = keyof IconEventMapOptions
|
13
src/layout/index.scss
Normal file
13
src/layout/index.scss
Normal file
@ -0,0 +1,13 @@
|
||||
.layout {
|
||||
box-sizing: border-box;
|
||||
|
||||
> .layout-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& .layout-content__router-view {
|
||||
// height: calc(100% - $layoutHeaderHeight - $layoutMenuHeight);
|
||||
height: var(--layout-content-height);
|
||||
padding: calc($layoutRouterViewContainer / 2);
|
||||
}
|
||||
}
|
64
src/layout/index.tsx
Normal file
64
src/layout/index.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import './index.scss'
|
||||
import { NLayout, NLayoutContent } from 'naive-ui'
|
||||
import RayTransitionComponent from '@/components/RayTransitionComponent/index.vue'
|
||||
import LayoutMenu from './components/Menu/index'
|
||||
import SiderBar from './components/SiderBar/index'
|
||||
import MenuTag from './components/MenuTag/index'
|
||||
import { useSetting } from '@/store'
|
||||
|
||||
const Layout = defineComponent({
|
||||
name: 'Layout',
|
||||
props: {},
|
||||
setup() {
|
||||
const menuStore = useSetting()
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
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 {
|
||||
windowHeight,
|
||||
modelReloadRoute,
|
||||
modelMenuTagLog,
|
||||
cssVarsRef,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
class="layout"
|
||||
style={[`height: ${this.windowHeight}px`, this.cssVarsRef]}
|
||||
>
|
||||
<NLayout class="layout-full" hasSider>
|
||||
<LayoutMenu />
|
||||
<NLayout>
|
||||
<SiderBar />
|
||||
{this.modelMenuTagLog ? <MenuTag /> : ''}
|
||||
<NLayoutContent
|
||||
class="layout-content__router-view"
|
||||
nativeScrollbar={false}
|
||||
>
|
||||
{this.modelReloadRoute ? <RayTransitionComponent /> : ''}
|
||||
</NLayoutContent>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default Layout
|
9
src/router/modules/error.ts
Normal file
9
src/router/modules/error.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default {
|
||||
path: '/error',
|
||||
name: 'error',
|
||||
component: () => import('@/views/error/index'),
|
||||
meta: {
|
||||
i18nKey: 'Error',
|
||||
icon: 'error',
|
||||
},
|
||||
}
|
13
src/router/modules/index.ts
Normal file
13
src/router/modules/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import dashboard from './dashboard'
|
||||
import reyl from './rely'
|
||||
import error from './error'
|
||||
|
||||
const routes = [dashboard, error, reyl]
|
||||
|
||||
export default routes
|
||||
|
||||
/**
|
||||
*
|
||||
* 弃用自动导入路由模块方式
|
||||
* 采用手动引入子路由模块方式
|
||||
*/
|
14
src/store/index.ts
Normal file
14
src/store/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
import type { App } from 'vue'
|
||||
|
||||
export { useSetting } from './modules/setting' // import { useSetting } from '@/store' 即可使用
|
||||
export { useMenu } from './modules/menu'
|
||||
|
||||
const store = createPinia()
|
||||
|
||||
export const setupStore = (app: App<Element>) => {
|
||||
app.use(store)
|
||||
|
||||
store.use(piniaPluginPersistedstate)
|
||||
}
|
150
src/store/modules/menu.ts
Normal file
150
src/store/modules/menu.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { getCache, setCache } from '@/utils/cache'
|
||||
import { NEllipsis } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const useMenu = defineStore('menu', () => {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const cacheMenuKey =
|
||||
getCache('menuKey') === 'no' ? '/dashboard' : getCache('menuKey')
|
||||
|
||||
const menuState = reactive({
|
||||
menuKey: cacheMenuKey as string | null, // 当前菜单 `key`
|
||||
options: [] as RouteRecordRaw[], // 菜单列表
|
||||
collapsed: false, // 是否折叠菜单
|
||||
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 item 菜单当前 `item`
|
||||
*
|
||||
* 修改 `menu key` 后的回调函数
|
||||
*/
|
||||
const menuModelValueChange = (key: string, item: MenuOption) => {
|
||||
handleMenuTagOptions(item as unknown as RouteRecordRaw)
|
||||
|
||||
menuState.menuKey = key
|
||||
|
||||
router.push(`${item.path}`)
|
||||
setCache('menuKey', key)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param path 路由地址
|
||||
*
|
||||
* 监听路由地址变化更新菜单状态
|
||||
*/
|
||||
const updateMenuKeyWhenRouteUpdate = (path: string) => {
|
||||
const matchMenuItem = (options: MenuOption[]) => {
|
||||
for (const i of options) {
|
||||
if (i?.children?.length) {
|
||||
matchMenuItem(i.children)
|
||||
}
|
||||
|
||||
if (path === i.path) {
|
||||
menuModelValueChange(i.path, i)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matchMenuItem(menuState.options)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 获取菜单列表
|
||||
* 缓存菜单
|
||||
*/
|
||||
const setupAppRoutes = () => {
|
||||
const layout = router.getRoutes().find((route) => route.name === 'layout')
|
||||
|
||||
const resolveRoutes = (routes: RouteRecordRaw[], index: number) => {
|
||||
return routes.map((curr) => {
|
||||
if (curr.children?.length) {
|
||||
curr.children = resolveRoutes(
|
||||
curr.children as RouteRecordRaw[],
|
||||
index++,
|
||||
)
|
||||
}
|
||||
|
||||
const route = {
|
||||
...curr,
|
||||
key: curr.path,
|
||||
label: () =>
|
||||
h(NEllipsis, null, {
|
||||
default: () => t(`GlobalMenuOptions.${curr!.meta!.i18nKey}`),
|
||||
}),
|
||||
}
|
||||
const expandIcon = {
|
||||
icon: () =>
|
||||
h(
|
||||
RayIcon,
|
||||
{
|
||||
name: curr?.meta?.icon as string,
|
||||
size: 20,
|
||||
},
|
||||
{},
|
||||
),
|
||||
}
|
||||
|
||||
const attr = curr.meta?.icon ? Object.assign(route, expandIcon) : route
|
||||
|
||||
// 初始化 `menu tag`
|
||||
if (curr.path === cacheMenuKey) {
|
||||
menuState.menuTagOptions.push(attr)
|
||||
}
|
||||
|
||||
return attr
|
||||
})
|
||||
}
|
||||
|
||||
menuState.options = resolveRoutes(layout?.children as RouteRecordRaw[], 0)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param collapsed 折叠菜单开关
|
||||
*/
|
||||
const collapsedMenu = (collapsed: boolean) =>
|
||||
(menuState.collapsed = collapsed)
|
||||
|
||||
const spliceMenTagOptions = (idx: number) =>
|
||||
menuState.menuTagOptions.splice(idx, 1)
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(newData) => {
|
||||
updateMenuKeyWhenRouteUpdate(newData)
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
...toRefs(menuState),
|
||||
menuModelValueChange,
|
||||
setupAppRoutes,
|
||||
collapsedMenu,
|
||||
spliceMenTagOptions,
|
||||
}
|
||||
})
|
57
src/store/modules/setting.ts
Normal file
57
src/store/modules/setting.ts
Normal file
@ -0,0 +1,57 @@
|
||||
export const useSetting = defineStore(
|
||||
'setting',
|
||||
() => {
|
||||
const settingState = reactive({
|
||||
drawerPlacement: 'right' as NaiveDrawerPlacement,
|
||||
primaryColorOverride: {
|
||||
common: {
|
||||
primaryColor: '#18A058', // 主题色
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
)
|
3
src/styles/setting.scss
Normal file
3
src/styles/setting.scss
Normal file
@ -0,0 +1,3 @@
|
||||
$layoutRouterViewContainer: 18px;
|
||||
$layoutHeaderHeight: 64px;
|
||||
$layoutMenuHeight: 46px;
|
63
src/views/login/index.tsx
Normal file
63
src/views/login/index.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import './index.scss'
|
||||
import {
|
||||
NSpace,
|
||||
NCard,
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NGradientText,
|
||||
NDropdown,
|
||||
} from 'naive-ui'
|
||||
import Signin from './components/Signin/index'
|
||||
import Register from './components/Register/index'
|
||||
import { useSetting } from '@/store'
|
||||
import RayIcon from '@/components/RayIcon'
|
||||
import { useLanguageOptions } from '@/language/index'
|
||||
|
||||
const Login = defineComponent({
|
||||
name: 'Login',
|
||||
setup() {
|
||||
const state = reactive({
|
||||
tabsValue: 'signin',
|
||||
})
|
||||
const { t } = useI18n()
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
const settingStore = useSetting()
|
||||
const { updateLocale } = settingStore
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
windowHeight,
|
||||
updateLocale,
|
||||
ray: t,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div class={['login']} style={[`height: ${this.windowHeight}px`]}>
|
||||
<NSpace>
|
||||
<NGradientText class="login-title" type="info">
|
||||
Ray Template
|
||||
</NGradientText>
|
||||
<NDropdown
|
||||
options={useLanguageOptions()}
|
||||
onSelect={(key) => this.updateLocale(key)}
|
||||
>
|
||||
<RayIcon customClassName="login-icon" name="language" size="18" />
|
||||
</NDropdown>
|
||||
</NSpace>
|
||||
<NCard>
|
||||
<NTabs v-model:value={this.tabsValue}>
|
||||
<NTabPane tab={this.ray('LoginModule.Signin')} name="signin">
|
||||
<Signin />
|
||||
</NTabPane>
|
||||
<NTabPane tab={this.ray('LoginModule.Register')} name="register">
|
||||
<Register />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NCard>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default Login
|
Loading…
x
Reference in New Issue
Block a user