diff --git a/.DS_Store b/.DS_Store
index cd557aaf..6dc2ecce 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/README.md b/README.md
index a8e59546..22297bec 100644
--- a/README.md
+++ b/README.md
@@ -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 形式开发会失效), 建议采用 形式进行开发`. 可以根据自己项目实际需求进行配置 `px` 与 'rem' 转换比例(使用 `postcss-pxtorem` 与 `autoprefixer` 实现).
diff --git a/dist/production-dist/index.html b/dist/production-dist/index.html
index ebf26c1f..35c0989c 100644
--- a/dist/production-dist/index.html
+++ b/dist/production-dist/index.html
@@ -5,8 +5,8 @@
ray template
-
-
+
+
diff --git a/src/icons/error.svg b/src/icons/error.svg
new file mode 100644
index 00000000..ad9fb923
--- /dev/null
+++ b/src/icons/error.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src/layout/components/Menu/index.tsx b/src/layout/components/Menu/index.tsx
new file mode 100644
index 00000000..fba5acdc
--- /dev/null
+++ b/src/layout/components/Menu/index.tsx
@@ -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 (
+
+
+
+ )
+ },
+})
+
+export default LayoutMenu
diff --git a/src/layout/components/MenuTag/index.scss b/src/layout/components/MenuTag/index.scss
new file mode 100644
index 00000000..bc091e87
--- /dev/null
+++ b/src/layout/components/MenuTag/index.scss
@@ -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;
+ }
+}
diff --git a/src/layout/components/MenuTag/index.tsx b/src/layout/components/MenuTag/index.tsx
new file mode 100644
index 00000000..344a5cbc
--- /dev/null
+++ b/src/layout/components/MenuTag/index.tsx
@@ -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 (
+
+ )
+ },
+})
+
+export default MenuTag
diff --git a/src/layout/components/SiderBar/Components/SettingDrawer/hook.ts b/src/layout/components/SiderBar/Components/SettingDrawer/hook.ts
new file mode 100644
index 00000000..3d285190
--- /dev/null
+++ b/src/layout/components/SiderBar/Components/SettingDrawer/hook.ts
@@ -0,0 +1,7 @@
+export const useSwatchesColorOptions = () => [
+ '#FFFFFF',
+ '#18A058',
+ '#2080F0',
+ '#F0A020',
+ 'rgba(208, 48, 80, 1)',
+]
diff --git a/src/layout/components/SiderBar/Components/SettingDrawer/index.scss b/src/layout/components/SiderBar/Components/SettingDrawer/index.scss
new file mode 100644
index 00000000..760db0c1
--- /dev/null
+++ b/src/layout/components/SiderBar/Components/SettingDrawer/index.scss
@@ -0,0 +1,8 @@
+.setting-drawer__space {
+ width: 100%;
+
+ & .n-descriptions-table-content {
+ display: flex !important;
+ justify-content: space-between;
+ }
+}
diff --git a/src/layout/components/SiderBar/Components/SettingDrawer/index.tsx b/src/layout/components/SiderBar/Components/SettingDrawer/index.tsx
new file mode 100644
index 00000000..6ef7e098
--- /dev/null
+++ b/src/layout/components/SiderBar/Components/SettingDrawer/index.tsx
@@ -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,
+ 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 (
+
+
+
+
+ {this.ray('LayoutHeaderSettingOptions.ThemeOptions.Title')}
+
+
+
+ {{
+ trigger: () => (
+
+ {{
+ 'checked-icon': () =>
+ h(
+ RayIcon,
+ {
+ name: 'dark',
+ },
+ {},
+ ),
+ 'unchecked-icon': () =>
+ h(
+ RayIcon,
+ {
+ name: 'light',
+ },
+ {},
+ ),
+ }}
+
+ ),
+ default: () =>
+ this.themeValue
+ ? this.ray('LayoutHeaderSettingOptions.ThemeOptions.Dark')
+ : this.ray(
+ 'LayoutHeaderSettingOptions.ThemeOptions.Light',
+ ),
+ }}
+
+
+
+ {this.ray(
+ 'LayoutHeaderSettingOptions.ThemeOptions.PrimaryColorConfig',
+ )}
+
+
+ 界面显示
+
+
+
+
+
+
+
+
+ )
+ },
+})
+
+export default SettingDrawer
diff --git a/src/layout/components/SiderBar/hook.ts b/src/layout/components/SiderBar/hook.ts
new file mode 100644
index 00000000..013ff83f
--- /dev/null
+++ b/src/layout/components/SiderBar/hook.ts
@@ -0,0 +1,14 @@
+export const useAvatarOptions = () => [
+ {
+ key: 'person',
+ label: '个人信息',
+ },
+ {
+ type: 'divider',
+ key: 'd1',
+ },
+ {
+ key: 'logout',
+ label: '退出登陆',
+ },
+]
diff --git a/src/layout/components/SiderBar/index.scss b/src/layout/components/SiderBar/index.scss
new file mode 100644
index 00000000..83c39953
--- /dev/null
+++ b/src/layout/components/SiderBar/index.scss
@@ -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;
+ }
+ }
+}
diff --git a/src/layout/components/SiderBar/index.tsx b/src/layout/components/SiderBar/index.tsx
new file mode 100644
index 00000000..93e0a016
--- /dev/null
+++ b/src/layout/components/SiderBar/index.tsx
@@ -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 (
+
+ )
+ },
+})
+
+export default SiderBar
diff --git a/src/layout/components/SiderBar/type.ts b/src/layout/components/SiderBar/type.ts
new file mode 100644
index 00000000..daf5ba5b
--- /dev/null
+++ b/src/layout/components/SiderBar/type.ts
@@ -0,0 +1,5 @@
+export interface IconEventMapOptions {
+ [propName: string]: (...args: unknown[]) => unknown
+}
+
+export type IconEventMap = keyof IconEventMapOptions
diff --git a/src/layout/index.scss b/src/layout/index.scss
new file mode 100644
index 00000000..ab45f31c
--- /dev/null
+++ b/src/layout/index.scss
@@ -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);
+ }
+}
diff --git a/src/layout/index.tsx b/src/layout/index.tsx
new file mode 100644
index 00000000..8160bed6
--- /dev/null
+++ b/src/layout/index.tsx
@@ -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 (
+
+
+
+
+
+ {this.modelMenuTagLog ? : ''}
+
+ {this.modelReloadRoute ? : ''}
+
+
+
+
+ )
+ },
+})
+
+export default Layout
diff --git a/src/router/modules/error.ts b/src/router/modules/error.ts
new file mode 100644
index 00000000..6d683a67
--- /dev/null
+++ b/src/router/modules/error.ts
@@ -0,0 +1,9 @@
+export default {
+ path: '/error',
+ name: 'error',
+ component: () => import('@/views/error/index'),
+ meta: {
+ i18nKey: 'Error',
+ icon: 'error',
+ },
+}
diff --git a/src/router/modules/index.ts b/src/router/modules/index.ts
new file mode 100644
index 00000000..389eb8a9
--- /dev/null
+++ b/src/router/modules/index.ts
@@ -0,0 +1,13 @@
+import dashboard from './dashboard'
+import reyl from './rely'
+import error from './error'
+
+const routes = [dashboard, error, reyl]
+
+export default routes
+
+/**
+ *
+ * 弃用自动导入路由模块方式
+ * 采用手动引入子路由模块方式
+ */
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 00000000..64e51525
--- /dev/null
+++ b/src/store/index.ts
@@ -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) => {
+ app.use(store)
+
+ store.use(piniaPluginPersistedstate)
+}
diff --git a/src/store/modules/menu.ts b/src/store/modules/menu.ts
new file mode 100644
index 00000000..5560d93e
--- /dev/null
+++ b/src/store/modules/menu.ts
@@ -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,
+ }
+})
diff --git a/src/store/modules/setting.ts b/src/store/modules/setting.ts
new file mode 100644
index 00000000..1e9baac7
--- /dev/null
+++ b/src/store/modules/setting.ts
@@ -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',
+ },
+ },
+)
diff --git a/src/styles/setting.scss b/src/styles/setting.scss
new file mode 100644
index 00000000..e6146e3d
--- /dev/null
+++ b/src/styles/setting.scss
@@ -0,0 +1,3 @@
+$layoutRouterViewContainer: 18px;
+$layoutHeaderHeight: 64px;
+$layoutMenuHeight: 46px;
diff --git a/src/views/login/index.tsx b/src/views/login/index.tsx
new file mode 100644
index 00000000..9e112fb3
--- /dev/null
+++ b/src/views/login/index.tsx
@@ -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 (
+
+
+
+ Ray Template
+
+ this.updateLocale(key)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ },
+})
+
+export default Login