diff --git a/.env b/.env index 77ad0d3..b48df03 100644 --- a/.env +++ b/.env @@ -5,7 +5,7 @@ VITE_APP_NAME=Nova - Admin # 路由模式 VITE_ROUTE_MODE = web # 权限路由模式: static | dynamic -VITE_AUTH_ROUTE_MODE=dynamic +VITE_AUTH_ROUTE_MODE=static # 设置登陆后跳转地址 VITE_HOME_PATH = /dashboard/workbench diff --git a/.vscode/settings.json b/.vscode/settings.json index abd048b..b20e46b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -65,5 +65,14 @@ "jsonc", "yaml", "toml" - ] + ], + "i18n-ally.displayLanguage": "zh", + // "i18n-ally.enabledParsers": ["ts"], + "i18n-ally.enabledFrameworks": ["vue"], + "i18n-ally.editor.preferEditor": true, + "i18n-ally.keystyle": "nested", + "i18n-ally.localesPaths": [ + "locales" + ], + "commentTranslate.source": "Google" } diff --git a/build/plugins.ts b/build/plugins.ts index 0e18d54..3ef7498 100644 --- a/build/plugins.ts +++ b/build/plugins.ts @@ -27,7 +27,7 @@ export function createVitePlugins(env: ImportMetaEnv) { // auto import api of lib AutoImport({ - imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'], + imports: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'], include: [ /\.[tj]sx?$/, /\.vue$/, diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..83b2af8 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,119 @@ +{ + "common": { + "cancel": "Cancel", + "confirm": "Confirm", + "close": "Closure", + "reload": "Refresh" + }, + "app": { + "loginOut": "Login out", + "loginOutContent": "Confirm to log out of current account?", + "loginOutTitle": "Sign out", + "userCenter": "Personal center", + "lignt": "Light", + "dark": "Dark", + "system": "System", + "backTop": "Back to top", + "toggleSider": "Toggle sidebar", + "BreadcrumbIcon": "Breadcrumbs icon", + "blackAndWhite": "Black and white mode", + "bottomCopyright": "Bottom copyright", + "breadcrumb": "Bread crumbs", + "colorWeak": "Color Weakness Mode", + "interfaceDisplay": "Interface display", + "logoDisplay": "LOGO display", + "messages": "Messages", + "multitab": "Display multiple tabs", + "notifications": "Notify", + "notificationsTips": "Notification", + "pageTransition": "Page transition", + "reset": "Reset", + "resetSettingContent": "Confirm to reset all settings?", + "resetSettingMeaasge": "Reset successful", + "resetSettingTitle": "Reset settings", + "searchPlaceholder": "Search page/path", + "setting": "Setting", + "systemSetting": "System settings", + "themeColor": "Theme color", + "themeSetting": "Theme settings", + "todos": "Todos", + "toggleFullScreen": "Toggle full screen", + "topProgress": "Top progress", + "transitionFadeBottom": "Bottom fade", + "transitionFadeScale": "Scale fade", + "transitionFadeSlide": "Side fade", + "transitionNull": "No transition", + "transitionSoft": "Soft", + "transitionZoomFade": "Expand fade out", + "transitionZoomOut": "Zoom out", + "watermake": "Watermark", + "closeOther": "Close other", + "closeAll": "Close all", + "closeLeft": "Close left", + "closeRight": "Close right" + }, + "login": { + "signInTitle": "Login", + "accountRuleTip": "Please enter account", + "passwordRuleTip": "Please enter password", + "or": "Or", + "rememberMe": "Remember me", + "forgotPassword": "Forget the password?", + "signIn": "Sign in", + "signUp": "Sign up", + "noAccountText": "Don't have an account?", + "accountPlaceholder": "Enter the account number", + "checkPasswordPlaceholder": "Please enter password again", + "checkPasswordRuleTip": "Please confirm password again", + "haveAccountText": "Do you have an account?", + "passwordPlaceholder": "Enter password", + "readAndAgree": "I have read and agree", + "registerTitle": "Register", + "userAgreement": "User Agreement", + "resetPassword": "Reset password", + "resetPasswordPlaceholder": "Enter account/mobile phone number", + "resetPasswordRuleTip": "Please enter your account/mobile phone number", + "resetPasswordTitle": "Reset" + }, + "route": { + "appRoot": "Home", + "cardList": "Card list", + "commonList": "Common list", + "dashboard": "Dashboard", + "demo": "Function example", + "fetch": "Request example", + "list": "List", + "monitor": "Monitoring", + "test": "Multi-level menu", + "test2": "Multi-level menu subpage", + "test2Detail": "Details page of multi-level menu", + "test3": "multi-level menu", + "test4": "Multi-level menu 3-1", + "workbench": "Workbench", + "QRCode": "QR code", + "about": "About", + "clipboard": "Clipboard", + "demo403": "403", + "demo404": "404", + "demo500": "500", + "dictionarySetting": "Dictionary settings", + "docments": "Document", + "docmentsVite": "Vite", + "docmentsVue": "Vue", + "docmentsVueuse": "VueUse (external link)", + "echarts": "Echarts", + "editor": "Editor", + "editorMd": "MarkDown editor", + "editorRich": "Rich text editor", + "error": "Exception page", + "icons": "Icon", + "justSuper": "Supervisible", + "map": "Map", + "menuSetting": "Menu Settings", + "permission": "Permissions", + "permissionDemo": "Permissions example", + "setting": "System settings", + "userCenter": "Personal Center", + "accountSetting": "User settings" + } +} diff --git a/locales/zh.json b/locales/zh.json new file mode 100644 index 0000000..b2ef18e --- /dev/null +++ b/locales/zh.json @@ -0,0 +1,119 @@ +{ + "common": { + "confirm": "确认", + "cancel": "取消", + "reload": "刷新", + "close": "关闭" + }, + "app": { + "loginOut": "退出登录", + "loginOutTitle": "退出登录", + "loginOutContent": "确认退出当前账号?", + "userCenter": "个人中心", + "lignt": "浅色", + "dark": "深色", + "system": "跟随系统", + "backTop": "返回顶部", + "toggleSider": "切换侧边栏", + "toggleFullScreen": "切换全屏", + "notificationsTips": "消息通知", + "notifications": "通知", + "messages": "消息", + "todos": "待办", + "searchPlaceholder": "搜索页面/路径", + "resetSettingTitle": "重置设置", + "resetSettingContent": "确认重置所有设置?", + "resetSettingMeaasge": "重置成功", + "reset": "重置", + "setting": "设置", + "themeSetting": "主题设置", + "colorWeak": "色弱模式", + "blackAndWhite": "黑白模式", + "themeColor": "主题色", + "pageTransition": "页面过渡", + "transitionNull": "无过渡", + "transitionFadeSlide": "侧边淡出", + "transitionFadeBottom": "底边淡出", + "transitionFadeScale": "收缩淡出", + "transitionZoomFade": "扩大淡出", + "transitionZoomOut": "收缩", + "transitionSoft": "柔和", + "systemSetting": "系统设置", + "interfaceDisplay": "界面显示", + "logoDisplay": "LOGO显示", + "topProgress": "顶部进度", + "multitab": "多页签显示", + "bottomCopyright": "底部版权", + "breadcrumb": "面包屑", + "BreadcrumbIcon": "面包屑图标", + "watermake": "水印", + "closeOther": "关闭其他", + "closeLeft": "关闭左侧", + "closeRight": "关闭右侧", + "closeAll": "全部关闭" + }, + "login": { + "signInTitle": "登录", + "accountPlaceholder": "输入账号", + "passwordPlaceholder": "输入密码", + "accountRuleTip": "请输入账户", + "passwordRuleTip": "请输入密码", + "or": "其他", + "signIn": "登录", + "rememberMe": "记住我", + "forgotPassword": "忘记密码?", + "signUp": "注册", + "noAccountText": "你没有账户?", + "haveAccountText": "已有账号?", + "checkPasswordRuleTip": "请再次确认密码", + "registerTitle": "注册", + "checkPasswordPlaceholder": "请再次输入密码", + "readAndAgree": "我已阅读并同意", + "userAgreement": "用户协议", + "resetPasswordTitle": "重置密码", + "resetPasswordPlaceholder": "输入账号/手机号码", + "resetPasswordRuleTip": "请输入账号/手机号码", + "resetPassword": "重置密码" + }, + "route": { + "appRoot": "首页", + "dashboard": "仪表盘", + "workbench": "工作台", + "monitor": "监控页", + "test": "多级菜单演示", + "test2": "多级菜单子页", + "test2Detail": "多级菜单的详情页", + "test3": "多级菜单", + "test4": "多级菜单3-1", + "list": "列表页", + "commonList": "常用列表", + "cardList": "卡片列表", + "demo": "功能示例", + "fetch": "请求示例", + "echarts": "Echarts示例", + "map": "地图", + "editor": "编辑器", + "editorMd": "MarkDown编辑器", + "editorRich": "富文本编辑器", + "clipboard": "剪贴板", + "icons": "图标", + "QRCode": "二维码", + "docments": "文档", + "docmentsVue": "Vue", + "docmentsVite": "Vite", + "docmentsVueuse": "VueUse(外链)", + "permission": "权限", + "permissionDemo": "权限示例", + "justSuper": "super可见", + "error": "异常页", + "demo403": "403", + "demo404": "404", + "demo500": "500", + "setting": "系统设置", + "accountSetting": "用户设置", + "dictionarySetting": "字典设置", + "menuSetting": "菜单设置", + "userCenter": "个人中心", + "about": "关于" + } +} diff --git a/package.json b/package.json index 42ed094..b1de248 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "qs": "^6.12.0", "radash": "^12.1.0", "vue": "^3.4.21", + "vue-i18n": "^9.11.0", "vue-router": "^4.3.0" }, "devDependencies": { diff --git a/src/components/common/CommonWrapper.vue b/src/components/common/CommonWrapper.vue index bb7d27b..f752769 100644 --- a/src/components/common/CommonWrapper.vue +++ b/src/components/common/CommonWrapper.vue @@ -18,7 +18,6 @@ <style scoped> .el { color: var(--n-text-color); - background-color: var(--card-color); transition: 0.3s var(--cubic-bezier-ease-in-out); } .el:hover { diff --git a/src/components/common/DarkModeSwitch.vue b/src/components/common/DarkModeSwitch.vue index 5208a0d..fb92ee1 100644 --- a/src/components/common/DarkModeSwitch.vue +++ b/src/components/common/DarkModeSwitch.vue @@ -3,30 +3,35 @@ import { NFlex, NText } from 'naive-ui' import { useAppStore } from '@/store' import { renderIcon } from '@/utils' +const { t } = useI18n() + const appStore = useAppStore() -const options = [ - { - label: 'Light', - value: 'light', - icon: 'icon-park-outline:sun-one', - }, - { - label: 'Dark', - value: 'dark', - icon: 'icon-park-outline:moon', - }, - { - label: 'System', - value: 'auto', - icon: 'icon-park-outline:laptop-computer', - }, -] + +const options = computed(() => { + return [ + { + label: t('app.lignt'), + value: 'light', + icon: 'icon-park-outline:sun-one', + }, + { + label: t('app.dark'), + value: 'dark', + icon: 'icon-park-outline:moon', + }, + { + label: t('app.system'), + value: 'auto', + icon: 'icon-park-outline:laptop-computer', + }, + ] +}) function renderLabel(option: any) { return h(NFlex, { align: 'center' }, { default: () => [ renderIcon(option.icon)(), - h(NText, { depth: 3 }, { default: () => option.value }), + option.label, ], }) } diff --git a/src/components/common/langsSwitch.vue b/src/components/common/langsSwitch.vue new file mode 100644 index 0000000..8cd1d33 --- /dev/null +++ b/src/components/common/langsSwitch.vue @@ -0,0 +1,25 @@ +<script setup lang="ts"> +import { useAppStore } from '@/store' + +const appStore = useAppStore() +const options = [ + { + label: 'English', + value: 'en', + }, + { + label: '中文', + value: 'zh', + }, +] +</script> + +<template> + <n-popselect :value="appStore.lang" :options="options" trigger="click" @update:value="appStore.setAppLang"> + <CommonWrapper> + <icon-park-outline-translate /> + </CommonWrapper> + </n-popselect> +</template> + +<style scoped></style> diff --git a/src/layouts/BasicLayout/index.vue b/src/layouts/BasicLayout/index.vue index d6b3d67..d729acc 100644 --- a/src/layouts/BasicLayout/index.vue +++ b/src/layouts/BasicLayout/index.vue @@ -54,6 +54,7 @@ const appStore = useAppStore() <Notices /> <FullScreen /> <DarkModeSwitch /> + <LangsSwitch /> <Setting /> <UserCenter /> </div> diff --git a/src/layouts/components/common/BackTop.vue b/src/layouts/components/common/BackTop.vue index 84eb8db..109a4e8 100644 --- a/src/layouts/components/common/BackTop.vue +++ b/src/layouts/components/common/BackTop.vue @@ -8,9 +8,7 @@ <icon-park-outline-to-top /> </div> </template> - <span>返回顶部</span> + <span>{{ $t('app.backTop') }}</span> </n-tooltip> </n-back-top> </template> - -<style scoped></style> diff --git a/src/layouts/components/header/Breadcrumb.vue b/src/layouts/components/header/Breadcrumb.vue index 7010357..ec9518f 100644 --- a/src/layouts/components/header/Breadcrumb.vue +++ b/src/layouts/components/header/Breadcrumb.vue @@ -22,7 +22,7 @@ const appStore = useAppStore() @click="router.push(item.path)" > <nova-icon v-if="appStore.showBreadcrumbIcon" :icon="item.meta.icon" /> - <span class="whitespace-nowrap">{{ item.meta.title }}</span> + <span class="whitespace-nowrap">{{ $t(`route.${String(item.name)}`, item.meta.title) }}</span> </n-el> </TransitionGroup> </template> @@ -34,7 +34,6 @@ const appStore = useAppStore() } .list-move, -/* 对移动中的元素应用的过渡 */ .list-enter-active, .list-leave-active { transition: all 0.3s ease; diff --git a/src/layouts/components/header/CollapaseButton.vue b/src/layouts/components/header/CollapaseButton.vue index e41d308..f39ef0b 100644 --- a/src/layouts/components/header/CollapaseButton.vue +++ b/src/layouts/components/header/CollapaseButton.vue @@ -12,7 +12,7 @@ const appStore = useAppStore() <icon-park-outline-menu-fold v-else /> </CommonWrapper> </template> - <span>切换侧边栏</span> + <span>{{ $t('app.toggleSider') }}</span> </n-tooltip> </template> diff --git a/src/layouts/components/header/FullScreen.vue b/src/layouts/components/header/FullScreen.vue index 3dd5f65..9409809 100644 --- a/src/layouts/components/header/FullScreen.vue +++ b/src/layouts/components/header/FullScreen.vue @@ -12,7 +12,7 @@ const appStore = useAppStore() <icon-park-outline-full-screen-two v-else /> </CommonWrapper> </template> - <span>全屏</span> + <span>{{ $t('app.toggleFullScreen') }}</span> </n-tooltip> </template> diff --git a/src/layouts/components/header/Notices.vue b/src/layouts/components/header/Notices.vue index 78101e0..eacfab4 100644 --- a/src/layouts/components/header/Notices.vue +++ b/src/layouts/components/header/Notices.vue @@ -81,11 +81,10 @@ const MassageData = ref<Message.List[]>([ ]) const currentTab = ref(0) function handleRead(id: number) { - // MassageData.value[currentTab.value].list[index].isRead = true const data = MassageData.value.find(i => i.id === id) if (data) data.isRead = true - window.$message.success(`已读id: ${id}`) + window.$message.success(`id: ${id}`) } const massageCount = computed(() => { return MassageData.value.filter(i => !i.isRead).length @@ -106,14 +105,14 @@ const groupMessage = computed(() => { </n-badge> </CommonWrapper> </template> - <span>消息通知</span> + <span>{{ $t('app.notificationsTips') }}</span> </n-tooltip> </template> <n-tabs v-model:value="currentTab" type="line" animated justify-content="space-evenly" class="w-390px"> <n-tab-pane :name="0"> <template #tab> <n-space class="w-130px" justify="center"> - 通知 + {{ $t('app.notifications') }} <n-badge type="info" :value="groupMessage[0]?.filter(i => !i.isRead).length" :max="99" /> </n-space> </template> @@ -122,7 +121,7 @@ const groupMessage = computed(() => { <n-tab-pane :name="1"> <template #tab> <n-space class="w-130px" justify="center"> - 消息 + {{ $t('app.messages') }} <n-badge type="warning" :value="groupMessage[1]?.filter(i => !i.isRead).length" :max="99" /> </n-space> </template> @@ -131,7 +130,7 @@ const groupMessage = computed(() => { <n-tab-pane :name="2"> <template #tab> <n-space class="w-130px" justify="center"> - 待办 + {{ $t('app.todos') }} <n-badge type="error" :value="groupMessage[2]?.filter(i => !i.isRead).length" :max="99" /> </n-space> </template> diff --git a/src/layouts/components/header/Search.vue b/src/layouts/components/header/Search.vue index c844cc7..e840ec2 100644 --- a/src/layouts/components/header/Search.vue +++ b/src/layouts/components/header/Search.vue @@ -6,16 +6,18 @@ import { renderIcon } from '@/utils' const routeStore = useRouteStore() const searchValue = ref('') +const { t } = useI18n() + const options = computed(() => { return routeStore.rowRoutes.filter((item) => { const conditions = [ - item['meta.title']?.includes(searchValue.value), + t(`route.${String(item.name)}`, item['meta.title'] || item.name)?.includes(searchValue.value), item.path?.includes(searchValue.value), ] return conditions.some(condition => condition) }).map((item) => { return { - label: item['meta.title'], + label: t(`route.${String(item.name)}`, item['meta.title'] || item.name), value: item.path, icon: item['meta.icon'], } @@ -44,7 +46,7 @@ function handleSelect(value: string) { <n-auto-complete v-model:value="searchValue" class="w-20em m-r-1em" :input-props="{ autocomplete: 'disabled', - }" :options="options" :render-label="renderLabel" placeholder="搜索页面/路径" clearable @select="handleSelect" + }" :options="options" :render-label="renderLabel" :placeholder="$t('app.searchPlaceholder')" clearable @select="handleSelect" > <template #prefix> <n-icon> diff --git a/src/layouts/components/header/Setting.vue b/src/layouts/components/header/Setting.vue index c284d50..8d5e6bc 100644 --- a/src/layouts/components/header/Setting.vue +++ b/src/layouts/components/header/Setting.vue @@ -3,40 +3,45 @@ import { useAppStore } from '@/store' const appStore = useAppStore() +const { t } = useI18n() + const drawerActive = ref(false) function openSetting() { drawerActive.value = !drawerActive.value } -const transitionSelectorOptions = [ - { - label: '无', - value: '', - }, - { - label: '侧滑', - value: 'fade-slide', - }, - { - label: '下滑', - value: 'fade-bottom', - }, - { - label: '收缩', - value: 'fade-scale', - }, - { - label: '扩张', - value: 'zoom-fade', - }, - { - label: '坍缩', - value: 'zoom-out', - }, - { - label: '柔和', - value: 'fade', - }, -] + +const transitionSelectorOptions = computed(() => { + return [ + { + label: t('app.transitionNull'), + value: '', + }, + { + label: t('app.transitionFadeSlide'), + value: 'fade-slide', + }, + { + label: t('app.transitionFadeBottom'), + value: 'fade-bottom', + }, + { + label: t('app.transitionFadeScale'), + value: 'fade-scale', + }, + { + label: t('app.transitionZoomFade'), + value: 'zoom-fade', + }, + { + label: t('app.transitionZoomOut'), + value: 'zoom-out', + }, + { + label: t('app.transitionSoft'), + value: 'fade', + }, + ] +}) const palette = [ '#ffb8b8', @@ -59,13 +64,13 @@ const palette = [ function resetSetting() { window.$dialog.warning({ - title: '重置所有设置', - content: '你确定重置所有设置?', - positiveText: '确定', - negativeText: '取消', + title: t('app.resetSettingTitle'), + content: t('app.resetSettingContent'), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), onPositiveClick: () => { appStore.resetAlltheme() - window.$message.success('重置成功') + window.$message.success(t('app.resetSettingMeaasge')) }, }) } @@ -77,65 +82,65 @@ function resetSetting() { <CommonWrapper @click="openSetting"> <div> <icon-park-outline-setting-two /> - <n-drawer v-model:show="drawerActive" :width="300"> - <n-drawer-content title="系统设置" closable> + <n-drawer v-model:show="drawerActive" :width="360"> + <n-drawer-content :title="t('app.systemSetting')" closable> <n-space vertical> - <n-divider>主题设置</n-divider> + <n-divider>{{ $t('app.themeSetting') }}</n-divider> <n-space justify="space-between"> - 色弱模式 + {{ $t('app.colorWeak') }} <n-switch :value="appStore.colorWeak" @update:value="appStore.toggleColorWeak" /> </n-space> <n-space justify="space-between"> - 灰色模式 + {{ $t('app.blackAndWhite') }} <n-switch :value="appStore.grayMode" @update:value="appStore.toggleGrayMode" /> </n-space> <n-space align="center" justify="space-between"> - 主题色 + {{ $t('app.themeColor') }} <n-color-picker v-model:value="appStore.primaryColor" - class="w-7em" :swatches="palette" :show-alpha="false" + class="w-10em" :swatches="palette" @update:value="appStore.setPrimaryColor" /> </n-space> <n-space align="center" justify="space-between"> - 切换动效 - <n-select v-model:value="appStore.transitionAnimation" class="w-7em" :options="transitionSelectorOptions" @update:value="appStore.reloadPage" /> + {{ $t('app.pageTransition') }} + <n-select v-model:value="appStore.transitionAnimation" class="w-10em" :options="transitionSelectorOptions" @update:value="appStore.reloadPage" /> </n-space> - <n-divider>界面显示</n-divider> + <n-divider>{{ $t('app.interfaceDisplay') }}</n-divider> <n-space justify="space-between"> - LOGO显示 + {{ $t('app.logoDisplay') }} <n-switch v-model:value="appStore.showLogo" /> </n-space> <n-space justify="space-between"> - 顶部进度 + {{ $t('app.topProgress') }} <n-switch v-model:value="appStore.showProgress" /> </n-space> <n-space justify="space-between"> - 多页签显示 + {{ $t('app.multitab') }} <n-switch v-model:value="appStore.showTabs" /> </n-space> <n-space justify="space-between"> - 底部标签显示 + {{ $t('app.bottomCopyright') }} <n-switch v-model:value="appStore.showFooter" /> </n-space> <n-space justify="space-between"> - 面包屑 + {{ $t('app.breadcrumb') }} <n-switch v-model:value="appStore.showBreadcrumb" /> </n-space> <n-space justify="space-between"> - 面包屑图标 + {{ $t('app.BreadcrumbIcon') }} <n-switch v-model:value="appStore.showBreadcrumbIcon" /> </n-space> <n-space justify="space-between"> - 水印 + {{ $t('app.watermake') }} <n-switch v-model:value="appStore.showWatermark" /> </n-space> </n-space> <template #footer> <n-button type="error" @click="resetSetting"> - 重置设置 + {{ $t('app.reset') }} </n-button> </template> </n-drawer-content> @@ -143,8 +148,6 @@ function resetSetting() { </div> </CommonWrapper> </template> - <span>设置</span> + <span>{{ $t('app.setting') }}</span> </n-tooltip> </template> - -<style scoped></style> diff --git a/src/layouts/components/header/UserCenter.vue b/src/layouts/components/header/UserCenter.vue index 71e02d1..9351fcb 100644 --- a/src/layouts/components/header/UserCenter.vue +++ b/src/layouts/components/header/UserCenter.vue @@ -2,46 +2,50 @@ import { renderIcon } from '@/utils/icon' import { useAuthStore } from '@/store' +const { t } = useI18n() + const { userInfo, resetAuthStore } = useAuthStore() const router = useRouter() -const options = [ - { - label: '个人中心', - key: 'userCenter', - icon: renderIcon('carbon:user-avatar-filled-alt'), - }, - { - type: 'divider', - key: 'd1', - }, - { - label: 'Github', - key: 'guthub', - icon: renderIcon('icon-park-outline:github'), - }, - { - label: 'gitee', - key: 'gitee', - icon: renderIcon('simple-icons:gitee'), - }, - { - type: 'divider', - key: 'd1', - }, - { - label: '退出登录', - key: 'loginOut', - icon: renderIcon('icon-park-outline:logout'), - }, -] +const options = computed(() => { + return [ + { + label: t('app.userCenter'), + key: 'userCenter', + icon: renderIcon('carbon:user-avatar-filled-alt'), + }, + { + type: 'divider', + key: 'd1', + }, + { + label: 'Github', + key: 'guthub', + icon: renderIcon('icon-park-outline:github'), + }, + { + label: 'Gitee', + key: 'gitee', + icon: renderIcon('simple-icons:gitee'), + }, + { + type: 'divider', + key: 'd1', + }, + { + label: t('app.loginOut'), + key: 'loginOut', + icon: renderIcon('icon-park-outline:logout'), + }, + ] +}) function handleSelect(key: string | number) { if (key === 'loginOut') { window.$dialog?.info({ - title: '退出登录', - content: '确认退出当前账号?', - positiveText: '确定', - negativeText: '取消', + title: t('app.loginOutTitle'), + content: t('app.loginOutContent'), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), onPositiveClick: () => { resetAuthStore() }, diff --git a/src/layouts/components/tab/DropTabs.vue b/src/layouts/components/tab/DropTabs.vue new file mode 100644 index 0000000..1335f02 --- /dev/null +++ b/src/layouts/components/tab/DropTabs.vue @@ -0,0 +1,39 @@ +<script setup lang="ts"> +import { useTabStore } from '@/store' +import { renderIcon } from '@/utils' + +const tabStore = useTabStore() + +const { t } = useI18n() + +function renderDropTabsLabel(option: any) { + return t(`route.${String(option.name)}`, option.meta.title) +} +function renderDropTabsIcon(option: any) { + return renderIcon(option.meta.icon)!() +} + +const router = useRouter() +function handleDropTabs(key: string, option: any) { + router.push(option.path) +} +</script> + +<template> + <n-dropdown + :options="tabStore.allTabs" + :render-label="renderDropTabsLabel" + :render-icon="renderDropTabsIcon" + trigger="click" + size="small" + @select="handleDropTabs" + > + <CommonWrapper> + <icon-park-outline-application-menu /> + </CommonWrapper> + </n-dropdown> +</template> + +<style scoped> + +</style> diff --git a/src/layouts/components/tab/Reload.vue b/src/layouts/components/tab/Reload.vue index 75631fe..aae2f83 100644 --- a/src/layouts/components/tab/Reload.vue +++ b/src/layouts/components/tab/Reload.vue @@ -21,7 +21,7 @@ function handleReload() { <icon-park-outline-refresh :class="{ 'animate-spin': loading }" /> </CommonWrapper> </template> - <span>刷新页面</span> + <span>{{ $t('common.reload') }}</span> </n-tooltip> </template> diff --git a/src/layouts/components/tab/TabBar.vue b/src/layouts/components/tab/TabBar.vue index 3855e4a..76e537f 100644 --- a/src/layouts/components/tab/TabBar.vue +++ b/src/layouts/components/tab/TabBar.vue @@ -1,6 +1,7 @@ <script setup lang="ts"> import type { RouteLocationNormalized } from 'vue-router' import Reload from './Reload.vue' +import DropTabs from './DropTabs.vue' import { renderIcon } from '@/utils' import { useAppStore, useTabStore } from '@/store' @@ -14,38 +15,41 @@ function handleTab(route: RouteLocationNormalized) { function handleClose(path: string) { tabStore.closeTab(path) } -const options = [ - { - label: '刷新', - key: 'reload', - icon: renderIcon('icon-park-outline:redo'), - }, - { - label: '关闭', - key: 'closeCurrent', - icon: renderIcon('icon-park-outline:close'), - }, - { - label: '关闭其他', - key: 'closeOther', - icon: renderIcon('icon-park-outline:delete-four'), - }, - { - label: '关闭左侧', - key: 'closeLeft', - icon: renderIcon('icon-park-outline:to-left'), - }, - { - label: '关闭右侧', - key: 'closeRight', - icon: renderIcon('icon-park-outline:to-right'), - }, - { - label: '全部关闭', - key: 'closeAll', - icon: renderIcon('icon-park-outline:fullwidth'), - }, -] +const { t } = useI18n() +const options = computed(() => { + return [ + { + label: t('common.reload'), + key: 'reload', + icon: renderIcon('icon-park-outline:redo'), + }, + { + label: t('common.close'), + key: 'closeCurrent', + icon: renderIcon('icon-park-outline:close'), + }, + { + label: t('app.closeOther'), + key: 'closeOther', + icon: renderIcon('icon-park-outline:delete-four'), + }, + { + label: t('app.closeLeft'), + key: 'closeLeft', + icon: renderIcon('icon-park-outline:to-left'), + }, + { + label: t('app.closeRight'), + key: 'closeRight', + icon: renderIcon('icon-park-outline:to-right'), + }, + { + label: t('app.closeAll'), + key: 'closeAll', + icon: renderIcon('icon-park-outline:fullwidth'), + }, + ] +}) const showDropdown = ref(false) const x = ref(0) const y = ref(0) @@ -91,17 +95,6 @@ function handleContextMenu(e: MouseEvent, route: RouteLocationNormalized) { function onClickoutside() { showDropdown.value = false } - -function renderDropTabsLabel(option: any) { - return option.meta.title -} -function renderDropTabsIcon(option: any) { - return renderIcon(option.meta.icon)!() -} - -function handleDropTabs(key: string, option: any) { - router.push(option.path) -} </script> <template> @@ -119,7 +112,9 @@ function handleDropTabs(key: string, option: any) { :name="item.path" @click="router.push(item.path)" > - {{ item.meta.title }} + <div class="flex-x-center gap-2"> + <nova-icon :icon="item.meta.icon" /> {{ $t(`route.${String(item.name)}`, item.meta.title) }} + </div> </n-tab> <n-tab v-for="item in tabStore.tabs" @@ -130,23 +125,12 @@ function handleDropTabs(key: string, option: any) { @contextmenu="handleContextMenu($event, item)" > <div class="flex-x-center gap-2"> - <nova-icon :icon="item.meta.icon" /> {{ item.meta.title }} + <nova-icon :icon="item.meta.icon" /> {{ $t(`route.${String(item.name)}`, item.meta.title) }} </div> </n-tab> <template #suffix> <Reload /> - <n-dropdown - :options="tabStore.allTabs" - :render-label="renderDropTabsLabel" - :render-icon="renderDropTabsIcon" - trigger="click" - size="small" - @select="handleDropTabs" - > - <CommonWrapper> - <icon-park-outline-application-menu /> - </CommonWrapper> - </n-dropdown> + <DropTabs /> </template> </n-tabs> <n-dropdown @@ -162,4 +146,4 @@ function handleDropTabs(key: string, option: any) { </div> </template> -<style scoped></style> +<style scoped></style>./DropTabs.vue diff --git a/src/modules/i18n.ts b/src/modules/i18n.ts new file mode 100644 index 0000000..ed3e0c7 --- /dev/null +++ b/src/modules/i18n.ts @@ -0,0 +1,19 @@ +import { createI18n } from 'vue-i18n' +import type { App } from 'vue' +import en from '../../locales/en.json' +import zh from '../../locales/zh.json' +import { local } from '@/utils' + +export const i18n = createI18n({ + legacy: false, + locale: local.get('lang') || 'zh', // 默认显示语言 + fallbackLocale: 'en', + messages: { + zh, + en, + }, +}) + +export function install(app: App) { + app.use(i18n) +} diff --git a/src/router/routes.static.ts b/src/router/routes.static.ts index 3854085..9971d0a 100644 --- a/src/router/routes.static.ts +++ b/src/router/routes.static.ts @@ -10,7 +10,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': null, }, { - 'name': 'dashboard_workbench', + 'name': 'workbench', 'path': '/dashboard/workbench', 'meta.title': '工作台', 'meta.requiresAuth': true, @@ -21,7 +21,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 1, }, { - 'name': 'dashboard_monitor', + 'name': 'monitor', 'path': '/dashboard/monitor', 'meta.title': '监控页', 'meta.requiresAuth': true, @@ -51,7 +51,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 4, }, { - 'name': 'test2_detail', + 'name': 'test2Detail', 'path': '/test/test2/detail', 'meta.title': '多级菜单的详情页', 'meta.requiresAuth': true, @@ -93,7 +93,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': null, }, { - 'name': 'list_commonList', + 'name': 'commonList', 'path': '/list/commonList', 'meta.title': '常用列表', 'meta.requiresAuth': true, @@ -103,7 +103,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 10, }, { - 'name': 'list_cardList', + 'name': 'cardList', 'path': '/list/cardList', 'meta.title': '卡片列表', 'meta.requiresAuth': true, @@ -113,8 +113,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 10, }, { - 'name': 'plugin', - 'path': '/plugin', + 'name': 'demo', + 'path': '/demo', 'meta.title': '功能示例', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:application-one', @@ -124,38 +124,38 @@ export const staticRoutes: AppRoute.RowRoute[] = [ }, { 'name': 'fetch', - 'path': '/plugin/fetch', - 'meta.title': '接口功能测试', + 'path': '/demo/fetch', + 'meta.title': '请求示例', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:international', - 'componentPath': '/plugin/fetch/index.vue', + 'componentPath': '/demo/fetch/index.vue', 'id': 5, 'pid': 13, }, { - 'name': 'plugin_echarts', - 'path': '/plugin/echarts', + 'name': 'echarts', + 'path': '/demo/echarts', 'meta.title': 'ECharts', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:chart-proportion', - 'componentPath': '/plugin/echarts/index.vue', + 'componentPath': '/demo/echarts/index.vue', 'id': 15, 'pid': 13, }, { - 'name': 'PluginMap', - 'path': '/plugin/map', + 'name': 'map', + 'path': '/demo/map', 'meta.title': '地图', 'meta.requiresAuth': true, 'meta.icon': 'carbon:map', 'meta.keepAlive': true, - 'componentPath': '/plugin/map/index.vue', + 'componentPath': '/demo/map/index.vue', 'id': 17, 'pid': 13, }, { - 'name': 'plugin_editor', - 'path': '/plugin/editor', + 'name': 'editor', + 'path': '/demo/editor', 'meta.title': '编辑器', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:editor', @@ -164,52 +164,52 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 13, }, { - 'name': 'plugin_md', - 'path': '/plugin/editor/md', + 'name': 'editorMd', + 'path': '/demo/editor/md', 'meta.title': 'MarkDown', 'meta.requiresAuth': true, 'meta.icon': 'ri:markdown-line', - 'componentPath': '/plugin/editor/md/index.vue', + 'componentPath': '/demo/editor/md/index.vue', 'id': 19, 'pid': 18, }, { - 'name': 'plugin_rich', - 'path': '/plugin/editor/rich', + 'name': 'editorRich', + 'path': '/demo/editor/rich', 'meta.title': '富文本', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:edit-one', - 'componentPath': '/plugin/editor/rich/index.vue', + 'componentPath': '/demo/editor/rich/index.vue', 'id': 20, 'pid': 18, }, { - 'name': 'plugin_clipboard', - 'path': '/plugin/clipboard', + 'name': 'clipboard', + 'path': '/demo/clipboard', 'meta.title': '剪贴板', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:clipboard', - 'componentPath': '/plugin/clipboard/index.vue', + 'componentPath': '/demo/clipboard/index.vue', 'id': 21, 'pid': 13, }, { - 'name': 'plugin_icons', - 'path': '/plugin/icons', + 'name': 'icons', + 'path': '/demo/icons', 'meta.title': '图标', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:winking-face-with-open-eyes', - 'componentPath': '/plugin/icons/index.vue', + 'componentPath': '/demo/icons/index.vue', 'id': 22, 'pid': 13, }, { - 'name': 'plugin_QRCode', - 'path': '/plugin/QRCode', + 'name': 'QRCode', + 'path': '/demo/QRCode', 'meta.title': '二维码', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:two-dimensional-code', - 'componentPath': '/plugin/QRCode/index.vue', + 'componentPath': '/demo/QRCode/index.vue', 'id': 23, 'pid': 13, }, @@ -224,9 +224,9 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': null, }, { - 'name': 'docments_vue', + 'name': 'docmentsVue', 'path': '/docments/vue', - 'meta.title': 'vue', + 'meta.title': 'Vue', 'meta.requiresAuth': true, 'meta.icon': 'logos:vue', 'componentPath': '/docments/vue/index.vue', @@ -234,9 +234,9 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 24, }, { - 'name': 'docments_vite', + 'name': 'docmentsVite', 'path': '/docments/vite', - 'meta.title': 'vite', + 'meta.title': 'Vite', 'meta.requiresAuth': true, 'meta.icon': 'logos:vitejs', 'componentPath': '/docments/vite/index.vue', @@ -244,7 +244,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 24, }, { - 'name': 'docments_vueuse', + 'name': 'docmentsVueuse', 'path': '/docments/vueuse', 'meta.title': 'VueUse(外链)', 'meta.requiresAuth': true, @@ -257,7 +257,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ { 'name': 'permission', 'path': '/permission', - 'meta.title': '权限示例', + 'meta.title': '权限', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:people-safe', 'componentPath': null, @@ -265,7 +265,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': null, }, { - 'name': 'permission_permission', + 'name': 'permissionDemo', 'path': '/permission/permission', 'meta.title': '权限示例', 'meta.requiresAuth': true, @@ -275,9 +275,9 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 28, }, { - 'name': 'permission_justSuper', + 'name': 'justSuper', 'path': '/permission/justSuper', - 'meta.title': '超管super可见', + 'meta.title': 'super可见', 'meta.requiresAuth': true, 'meta.roles': [ 'super', @@ -300,7 +300,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ { 'name': 'demo403', 'path': '/error/403', - 'meta.title': '403页', + 'meta.title': '403', 'meta.requiresAuth': true, 'meta.icon': 'carbon:error', 'meta.order': 3, @@ -311,7 +311,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ { 'name': 'demo404', 'path': '/error/404', - 'meta.title': '404页', + 'meta.title': '404', 'meta.requiresAuth': true, 'meta.icon': 'icon-park-outline:error', 'meta.order': 2, @@ -322,7 +322,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ { 'name': 'demo500', 'path': '/error/500', - 'meta.title': '500页', + 'meta.title': '500', 'meta.requiresAuth': true, 'meta.icon': 'carbon:data-error', 'meta.order': 1, @@ -341,7 +341,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': null, }, { - 'name': 'setting_account', + 'name': 'accountSetting', 'path': '/setting/account', 'meta.title': '用户设置', 'meta.requiresAuth': true, @@ -351,7 +351,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 35, }, { - 'name': 'setting_dictionary', + 'name': 'dictionarySetting', 'path': '/setting/dictionary', 'meta.title': '字典设置', 'meta.requiresAuth': true, @@ -361,7 +361,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [ 'pid': 35, }, { - 'name': 'setting_menu', + 'name': 'menuSetting', 'path': '/setting/menu', 'meta.title': '菜单设置', 'meta.requiresAuth': true, diff --git a/src/store/app/index.ts b/src/store/app/index.ts index d0cf436..03bd8b0 100644 --- a/src/store/app/index.ts +++ b/src/store/app/index.ts @@ -2,6 +2,7 @@ import type { GlobalThemeOverrides } from 'naive-ui' import chroma from 'chroma-js' import { set } from 'radash' import themeConfig from './theme.json' +import { local, setLocale } from '@/utils' type TransitionAnimation = '' | 'fade-slide' | 'fade-bottom' | 'fade-scale' | 'zoom-fade' | 'zoom-out' @@ -17,6 +18,7 @@ export const useAppStore = defineStore('app-store', { state: () => { return { footerText: 'Copyright © 2024 chansee97', + lang: 'zh', theme: themeConfig as GlobalThemeOverrides, primaryColor: themeConfig.common.primaryColor, collapsed: false, @@ -65,6 +67,11 @@ export const useAppStore = defineStore('app-store', { // 重置所有配色 this.setPrimaryColor(this.primaryColor) }, + setAppLang(lang: App.lang) { + setLocale(lang) + local.set('lang', lang) + this.lang = lang + }, /* 设置主题色 */ setPrimaryColor(color: string) { const brightenColor = chroma(color).brighten(1).hex() diff --git a/src/store/route.ts b/src/store/route.ts index 75ea41d..4274b88 100644 --- a/src/store/route.ts +++ b/src/store/route.ts @@ -3,7 +3,7 @@ import { RouterLink } from 'vue-router' import { h } from 'vue' import { clone, construct, min } from 'radash' import type { RouteRecordRaw } from 'vue-router' -import { arrayToTree, local, renderIcon } from '@/utils' +import { $t, arrayToTree, local, renderIcon } from '@/utils' import { router } from '@/router' import { fetchUserRoutes } from '@/service' import { staticRoutes } from '@/router/routes.static' @@ -81,9 +81,9 @@ export const useRouteStore = defineStore('route-store', { path: item.path, }, }, - { default: () => item.meta.title }, + { default: () => $t(`route.${String(item.name)}`, item.meta.title) }, ) - : item.meta.title, + : $t(`route.${String(item.name)}`, item.meta.title), key: item.path, icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined, } @@ -142,7 +142,7 @@ export const useRouteStore = defineStore('route-store', { redirect: import.meta.env.VITE_HOME_PATH, component: BasicLayout, meta: { - title: '首页', + title: '', icon: 'icon-park-outline:home', }, children: [], diff --git a/src/typings/global.d.ts b/src/typings/global.d.ts index af687ad..d2f4ed2 100644 --- a/src/typings/global.d.ts +++ b/src/typings/global.d.ts @@ -19,6 +19,13 @@ declare namespace NaiveUI { type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning' } +declare module '~icons/*' { + import type { FunctionalComponent, SVGAttributes } from 'vue' + + const component: FunctionalComponent<SVGAttributes> + export default component +} + declare namespace Storage { interface Session { demoKey: string @@ -33,5 +40,11 @@ declare namespace Storage { refreshToken: string /* 存储登录账号 */ loginAccount: any + /* 存储当前语言 */ + lang: App.lang } } + +declare namespace App { + type lang = 'zh' | 'en' +} diff --git a/src/typings/route.d.ts b/src/typings/route.d.ts index 7c2edf8..85c4a22 100644 --- a/src/typings/route.d.ts +++ b/src/typings/route.d.ts @@ -23,6 +23,8 @@ declare namespace AppRoute { withoutTab?: boolean /** 当前路由是否会被固定在Tab中,用于一些常驻页面 */ pinTab?: boolean + /** 当前路由i18n标识 */ + i18nKey?: string } /** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */ interface baseRoute { diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..7cc9411 --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,7 @@ +import { i18n } from '@/modules/i18n' + +export function setLocale(locale: App.lang) { + i18n.global.locale.value = locale +} + +export const $t = i18n.global.t diff --git a/src/utils/index.ts b/src/utils/index.ts index c8551d7..d9bedb9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './icon' export * from './storage' export * from './array' +export * from './i18n' diff --git a/src/views/plugin/QRCode/index.vue b/src/views/demo/QRCode/index.vue similarity index 100% rename from src/views/plugin/QRCode/index.vue rename to src/views/demo/QRCode/index.vue diff --git a/src/views/plugin/clipboard/index.vue b/src/views/demo/clipboard/index.vue similarity index 100% rename from src/views/plugin/clipboard/index.vue rename to src/views/demo/clipboard/index.vue diff --git a/src/views/plugin/echarts/index.vue b/src/views/demo/echarts/index.vue similarity index 100% rename from src/views/plugin/echarts/index.vue rename to src/views/demo/echarts/index.vue diff --git a/src/views/plugin/editor/md/index.vue b/src/views/demo/editor/md/index.vue similarity index 100% rename from src/views/plugin/editor/md/index.vue rename to src/views/demo/editor/md/index.vue diff --git a/src/views/plugin/editor/rich/index.vue b/src/views/demo/editor/rich/index.vue similarity index 100% rename from src/views/plugin/editor/rich/index.vue rename to src/views/demo/editor/rich/index.vue diff --git a/src/views/plugin/fetch/components/Delete.vue b/src/views/demo/fetch/components/Delete.vue similarity index 100% rename from src/views/plugin/fetch/components/Delete.vue rename to src/views/demo/fetch/components/Delete.vue diff --git a/src/views/plugin/fetch/components/DownLoad.vue b/src/views/demo/fetch/components/DownLoad.vue similarity index 100% rename from src/views/plugin/fetch/components/DownLoad.vue rename to src/views/demo/fetch/components/DownLoad.vue diff --git a/src/views/plugin/fetch/components/DownLoadWithProgress.vue b/src/views/demo/fetch/components/DownLoadWithProgress.vue similarity index 100% rename from src/views/plugin/fetch/components/DownLoadWithProgress.vue rename to src/views/demo/fetch/components/DownLoadWithProgress.vue diff --git a/src/views/plugin/fetch/components/Env.vue b/src/views/demo/fetch/components/Env.vue similarity index 100% rename from src/views/plugin/fetch/components/Env.vue rename to src/views/demo/fetch/components/Env.vue diff --git a/src/views/plugin/fetch/components/FailedRequest.vue b/src/views/demo/fetch/components/FailedRequest.vue similarity index 100% rename from src/views/plugin/fetch/components/FailedRequest.vue rename to src/views/demo/fetch/components/FailedRequest.vue diff --git a/src/views/plugin/fetch/components/FailedResponse.vue b/src/views/demo/fetch/components/FailedResponse.vue similarity index 100% rename from src/views/plugin/fetch/components/FailedResponse.vue rename to src/views/demo/fetch/components/FailedResponse.vue diff --git a/src/views/plugin/fetch/components/FailedResponseWithoutTip.vue b/src/views/demo/fetch/components/FailedResponseWithoutTip.vue similarity index 100% rename from src/views/plugin/fetch/components/FailedResponseWithoutTip.vue rename to src/views/demo/fetch/components/FailedResponseWithoutTip.vue diff --git a/src/views/plugin/fetch/components/FormPost.vue b/src/views/demo/fetch/components/FormPost.vue similarity index 100% rename from src/views/plugin/fetch/components/FormPost.vue rename to src/views/demo/fetch/components/FormPost.vue diff --git a/src/views/plugin/fetch/components/Get.vue b/src/views/demo/fetch/components/Get.vue similarity index 100% rename from src/views/plugin/fetch/components/Get.vue rename to src/views/demo/fetch/components/Get.vue diff --git a/src/views/plugin/fetch/components/NoToken.vue b/src/views/demo/fetch/components/NoToken.vue similarity index 100% rename from src/views/plugin/fetch/components/NoToken.vue rename to src/views/demo/fetch/components/NoToken.vue diff --git a/src/views/plugin/fetch/components/Post.vue b/src/views/demo/fetch/components/Post.vue similarity index 100% rename from src/views/plugin/fetch/components/Post.vue rename to src/views/demo/fetch/components/Post.vue diff --git a/src/views/plugin/fetch/components/Put.vue b/src/views/demo/fetch/components/Put.vue similarity index 100% rename from src/views/plugin/fetch/components/Put.vue rename to src/views/demo/fetch/components/Put.vue diff --git a/src/views/plugin/fetch/components/RefreshToken.vue b/src/views/demo/fetch/components/RefreshToken.vue similarity index 100% rename from src/views/plugin/fetch/components/RefreshToken.vue rename to src/views/demo/fetch/components/RefreshToken.vue diff --git a/src/views/plugin/fetch/components/TokenExpiration.vue b/src/views/demo/fetch/components/TokenExpiration.vue similarity index 100% rename from src/views/plugin/fetch/components/TokenExpiration.vue rename to src/views/demo/fetch/components/TokenExpiration.vue diff --git a/src/views/plugin/fetch/components/Transform.vue b/src/views/demo/fetch/components/Transform.vue similarity index 100% rename from src/views/plugin/fetch/components/Transform.vue rename to src/views/demo/fetch/components/Transform.vue diff --git a/src/views/plugin/fetch/components/UseRequest.vue b/src/views/demo/fetch/components/UseRequest.vue similarity index 100% rename from src/views/plugin/fetch/components/UseRequest.vue rename to src/views/demo/fetch/components/UseRequest.vue diff --git a/src/views/plugin/fetch/index.vue b/src/views/demo/fetch/index.vue similarity index 100% rename from src/views/plugin/fetch/index.vue rename to src/views/demo/fetch/index.vue diff --git a/src/views/plugin/icons/index.vue b/src/views/demo/icons/index.vue similarity index 100% rename from src/views/plugin/icons/index.vue rename to src/views/demo/icons/index.vue diff --git a/src/views/plugin/map/components/AMap.vue b/src/views/demo/map/components/AMap.vue similarity index 100% rename from src/views/plugin/map/components/AMap.vue rename to src/views/demo/map/components/AMap.vue diff --git a/src/views/plugin/map/components/BMap.vue b/src/views/demo/map/components/BMap.vue similarity index 100% rename from src/views/plugin/map/components/BMap.vue rename to src/views/demo/map/components/BMap.vue diff --git a/src/views/plugin/map/index.vue b/src/views/demo/map/index.vue similarity index 100% rename from src/views/plugin/map/index.vue rename to src/views/demo/map/index.vue diff --git a/src/views/login/components/Login/index.vue b/src/views/login/components/Login/index.vue index 53cc8a9..b9276e2 100644 --- a/src/views/login/components/Login/index.vue +++ b/src/views/login/components/Login/index.vue @@ -10,18 +10,22 @@ const authStore = useAuthStore() function toOtherForm(type: any) { emit('update:modelValue', type) } -const rules = { - account: { - required: true, - trigger: 'blur', - message: '请输入账户', - }, - pwd: { - required: true, - trigger: 'blur', - message: '请输入密码', - }, -} + +const { t } = useI18n() +const rules = computed(() => { + return { + account: { + required: true, + trigger: 'blur', + message: t('login.accountRuleTip'), + }, + pwd: { + required: true, + trigger: 'blur', + message: t('login.passwordRuleTip'), + }, + } +}) const formValue = ref({ account: 'super', pwd: '123456', @@ -46,6 +50,9 @@ function handleLogin() { isLoading.value = false }) } +onMounted(() => { + checkUserAccount() +}) function checkUserAccount() { const loginAccount = local.get('loginAccount') if (!loginAccount) @@ -54,20 +61,19 @@ function checkUserAccount() { formValue.value = loginAccount isRemember.value = true } -checkUserAccount() </script> <template> <div> <n-h2 depth="3" class="text-center"> - 登录 + {{ $t('login.signInTitle') }} </n-h2> <n-form ref="formRef" :rules="rules" :model="formValue" :show-label="false" size="large"> <n-form-item path="account"> - <n-input v-model:value="formValue.account" clearable placeholder="输入账号" /> + <n-input v-model:value="formValue.account" clearable :placeholder="$t('login.accountPlaceholder')" /> </n-form-item> <n-form-item path="pwd"> - <n-input v-model:value="formValue.pwd" type="password" placeholder="输入密码" clearable show-password-on="click"> + <n-input v-model:value="formValue.pwd" type="password" :placeholder="$t('login.passwordPlaceholder')" clearable show-password-on="click"> <template #password-invisible-icon> <icon-park-outline-preview-close-one /> </template> @@ -79,22 +85,25 @@ checkUserAccount() <n-space vertical :size="20"> <div class="flex-y-center justify-between"> <n-checkbox v-model:checked="isRemember"> - 记住我 + {{ $t('login.rememberMe') }} </n-checkbox> <n-button type="primary" text @click="toOtherForm('resetPwd')"> - 忘记密码? + {{ $t('login.forgotPassword') }} </n-button> </div> <n-button block type="primary" size="large" :loading="isLoading" :disabled="isLoading" @click="handleLogin"> - 登录 - </n-button> - <n-button type="primary" text @click="toOtherForm('register')"> - 立即注册 + {{ $t('login.signIn') }} </n-button> + <n-flex> + <n-text>{{ $t('login.noAccountText') }}</n-text> + <n-button type="primary" text @click="toOtherForm('register')"> + {{ $t('login.signUp') }} + </n-button> + </n-flex> </n-space> </n-form> <n-divider> - <span op-80>其他登录</span> + <span op-80>{{ $t('login.or') }}</span> </n-divider> <n-space justify="center"> <n-button circle> diff --git a/src/views/login/components/Register/index.vue b/src/views/login/components/Register/index.vue index e87aafd..69f6e30 100644 --- a/src/views/login/components/Register/index.vue +++ b/src/views/login/components/Register/index.vue @@ -3,21 +3,23 @@ const emit = defineEmits(['update:modelValue']) function toLogin() { emit('update:modelValue', 'login') } +const { t } = useI18n() + const rules = { account: { required: true, trigger: 'blur', - message: '请输入账户', + message: t('login.accountRuleTip'), }, pwd: { required: true, trigger: 'blur', - message: '请输入密码', + message: t('login.passwordRuleTip'), }, rePwd: { required: true, trigger: 'blur', - message: '请再次确认密码', + message: t('login.checkPasswordRuleTip'), }, } const formValue = ref({ @@ -34,7 +36,7 @@ function handleRegister() {} <template> <div> <n-h2 depth="3" class="text-center"> - 注册 + {{ $t('login.registerTitle') }} </n-h2> <n-form :rules="rules" @@ -46,14 +48,14 @@ function handleRegister() {} <n-input v-model:value="formValue.account" clearable - placeholder="输入账号" + :placeholder="$t('login.accountPlaceholder')" /> </n-form-item> <n-form-item path="pwd"> <n-input v-model:value="formValue.pwd" type="password" - placeholder="输入密码" + :placeholder="$t('login.passwordPlaceholder')" clearable show-password-on="click" > @@ -69,7 +71,7 @@ function handleRegister() {} <n-input v-model:value="formValue.rePwd" type="password" - placeholder="请再次输入密码" + :placeholder="$t('login.checkPasswordPlaceholder')" clearable show-password-on="click" > @@ -88,17 +90,11 @@ function handleRegister() {} class="w-full" > <n-checkbox v-model:checked="isRead"> - 我已阅读并同意 <n-button + {{ $t('login.readAndAgree') }} <n-button type="primary" text > - 用户协议 - </n-button> 及 - <n-button - type="primary" - text - > - xx社区规范 + {{ $t('login.userAgreement') }} </n-button> </n-checkbox> <n-button @@ -106,16 +102,18 @@ function handleRegister() {} type="primary" @click="handleRegister" > - 立即注册 - </n-button> - <n-button - tertiary - block - type="primary" - @click="toLogin" - > - 已有账号?去登录 + {{ $t('login.signUp') }} </n-button> + <n-flex justify="center"> + <n-text>{{ $t('login.haveAccountText') }}</n-text> + <n-button + text + type="primary" + @click="toLogin" + > + {{ $t('login.signIn') }} + </n-button> + </n-flex> </n-space> </n-form-item> </n-form> diff --git a/src/views/login/components/ResetPwd/index.vue b/src/views/login/components/ResetPwd/index.vue index a721da3..6eb49fe 100644 --- a/src/views/login/components/ResetPwd/index.vue +++ b/src/views/login/components/ResetPwd/index.vue @@ -3,26 +3,33 @@ const emit = defineEmits(['update:modelValue']) function toLogin() { emit('update:modelValue', 'login') } -const rules = { - account: { - required: true, - trigger: 'blur', - message: '请输入账号/手机号码', - }, -} +const { t } = useI18n() + +const rules = computed(() => { + return { + account: { + required: true, + trigger: 'blur', + message: t('login.resetPasswordRuleTip'), + }, + } +}) const formValue = ref({ account: '', }) - -function handleRegister() {} +const formRef = ref<FormInst | null>(null) +function handleRegister() { + formRef.value?.validate() +} </script> <template> <div> <n-h2 depth="3" class="text-center"> - 重置密码 + {{ $t('login.resetPasswordTitle') }} </n-h2> <n-form + ref="formRef" :rules="rules" :model="formValue" :show-label="false" @@ -32,7 +39,7 @@ function handleRegister() {} <n-input v-model:value="formValue.account" clearable - placeholder="账号/手机号码" + :placeholder="$t('login.resetPasswordPlaceholder')" /> </n-form-item> <n-form-item> @@ -46,16 +53,18 @@ function handleRegister() {} type="primary" @click="handleRegister" > - 重置密码 - </n-button> - <n-button - tertiary - block - type="primary" - @click="toLogin" - > - 已有账号?去登录 + {{ $t('login.resetPassword') }} </n-button> + <n-flex justify="center"> + <n-text>{{ $t('login.haveAccountText') }}</n-text> + <n-button + text + type="primary" + @click="toLogin" + > + {{ $t('login.signIn') }} + </n-button> + </n-flex> </n-space> </n-form-item> </n-form> diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 7312d98..f4b4163 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -16,6 +16,7 @@ const appName = import.meta.env.VITE_APP_NAME <n-el class="wh-full flex-center" style="background-color: var(--body-color);"> <div class="fixed top-40px right-40px text-lg"> <DarkModeSwitch /> + <LangsSwitch /> </div> <n-el class="p-4xl h-full w-full sm:w-450px sm:h-700px"