feat: add i18n

This commit is contained in:
chansee97 2024-04-05 14:03:48 +08:00
parent a53c9e57f5
commit ec28d4e53d
59 changed files with 678 additions and 306 deletions

2
.env
View File

@ -5,7 +5,7 @@ VITE_APP_NAME=Nova - Admin
# 路由模式 # 路由模式
VITE_ROUTE_MODE = web VITE_ROUTE_MODE = web
# 权限路由模式: static dynamic # 权限路由模式: static dynamic
VITE_AUTH_ROUTE_MODE=dynamic VITE_AUTH_ROUTE_MODE=static
# 设置登陆后跳转地址 # 设置登陆后跳转地址
VITE_HOME_PATH = /dashboard/workbench VITE_HOME_PATH = /dashboard/workbench

11
.vscode/settings.json vendored
View File

@ -65,5 +65,14 @@
"jsonc", "jsonc",
"yaml", "yaml",
"toml" "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"
} }

View File

@ -27,7 +27,7 @@ export function createVitePlugins(env: ImportMetaEnv) {
// auto import api of lib // auto import api of lib
AutoImport({ AutoImport({
imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'], imports: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
include: [ include: [
/\.[tj]sx?$/, /\.[tj]sx?$/,
/\.vue$/, /\.vue$/,

119
locales/en.json Normal file
View File

@ -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"
}
}

119
locales/zh.json Normal file
View File

@ -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": "关于"
}
}

View File

@ -62,6 +62,7 @@
"qs": "^6.12.0", "qs": "^6.12.0",
"radash": "^12.1.0", "radash": "^12.1.0",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-i18n": "^9.11.0",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -18,7 +18,6 @@
<style scoped> <style scoped>
.el { .el {
color: var(--n-text-color); color: var(--n-text-color);
background-color: var(--card-color);
transition: 0.3s var(--cubic-bezier-ease-in-out); transition: 0.3s var(--cubic-bezier-ease-in-out);
} }
.el:hover { .el:hover {

View File

@ -3,30 +3,35 @@ import { NFlex, NText } from 'naive-ui'
import { useAppStore } from '@/store' import { useAppStore } from '@/store'
import { renderIcon } from '@/utils' import { renderIcon } from '@/utils'
const { t } = useI18n()
const appStore = useAppStore() const appStore = useAppStore()
const options = [
const options = computed(() => {
return [
{ {
label: 'Light', label: t('app.lignt'),
value: 'light', value: 'light',
icon: 'icon-park-outline:sun-one', icon: 'icon-park-outline:sun-one',
}, },
{ {
label: 'Dark', label: t('app.dark'),
value: 'dark', value: 'dark',
icon: 'icon-park-outline:moon', icon: 'icon-park-outline:moon',
}, },
{ {
label: 'System', label: t('app.system'),
value: 'auto', value: 'auto',
icon: 'icon-park-outline:laptop-computer', icon: 'icon-park-outline:laptop-computer',
}, },
] ]
})
function renderLabel(option: any) { function renderLabel(option: any) {
return h(NFlex, { align: 'center' }, { return h(NFlex, { align: 'center' }, {
default: () => [ default: () => [
renderIcon(option.icon)(), renderIcon(option.icon)(),
h(NText, { depth: 3 }, { default: () => option.value }), option.label,
], ],
}) })
} }

View File

@ -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>

View File

@ -54,6 +54,7 @@ const appStore = useAppStore()
<Notices /> <Notices />
<FullScreen /> <FullScreen />
<DarkModeSwitch /> <DarkModeSwitch />
<LangsSwitch />
<Setting /> <Setting />
<UserCenter /> <UserCenter />
</div> </div>

View File

@ -8,9 +8,7 @@
<icon-park-outline-to-top /> <icon-park-outline-to-top />
</div> </div>
</template> </template>
<span>返回顶部</span> <span>{{ $t('app.backTop') }}</span>
</n-tooltip> </n-tooltip>
</n-back-top> </n-back-top>
</template> </template>
<style scoped></style>

View File

@ -22,7 +22,7 @@ const appStore = useAppStore()
@click="router.push(item.path)" @click="router.push(item.path)"
> >
<nova-icon v-if="appStore.showBreadcrumbIcon" :icon="item.meta.icon" /> <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> </n-el>
</TransitionGroup> </TransitionGroup>
</template> </template>
@ -34,7 +34,6 @@ const appStore = useAppStore()
} }
.list-move, .list-move,
/* 对移动中的元素应用的过渡 */
.list-enter-active, .list-enter-active,
.list-leave-active { .list-leave-active {
transition: all 0.3s ease; transition: all 0.3s ease;

View File

@ -12,7 +12,7 @@ const appStore = useAppStore()
<icon-park-outline-menu-fold v-else /> <icon-park-outline-menu-fold v-else />
</CommonWrapper> </CommonWrapper>
</template> </template>
<span>切换侧边栏</span> <span>{{ $t('app.toggleSider') }}</span>
</n-tooltip> </n-tooltip>
</template> </template>

View File

@ -12,7 +12,7 @@ const appStore = useAppStore()
<icon-park-outline-full-screen-two v-else /> <icon-park-outline-full-screen-two v-else />
</CommonWrapper> </CommonWrapper>
</template> </template>
<span>全屏</span> <span>{{ $t('app.toggleFullScreen') }}</span>
</n-tooltip> </n-tooltip>
</template> </template>

View File

@ -81,11 +81,10 @@ const MassageData = ref<Message.List[]>([
]) ])
const currentTab = ref(0) const currentTab = ref(0)
function handleRead(id: number) { function handleRead(id: number) {
// MassageData.value[currentTab.value].list[index].isRead = true
const data = MassageData.value.find(i => i.id === id) const data = MassageData.value.find(i => i.id === id)
if (data) if (data)
data.isRead = true data.isRead = true
window.$message.success(`已读id: ${id}`) window.$message.success(`id: ${id}`)
} }
const massageCount = computed(() => { const massageCount = computed(() => {
return MassageData.value.filter(i => !i.isRead).length return MassageData.value.filter(i => !i.isRead).length
@ -106,14 +105,14 @@ const groupMessage = computed(() => {
</n-badge> </n-badge>
</CommonWrapper> </CommonWrapper>
</template> </template>
<span>消息通知</span> <span>{{ $t('app.notificationsTips') }}</span>
</n-tooltip> </n-tooltip>
</template> </template>
<n-tabs v-model:value="currentTab" type="line" animated justify-content="space-evenly" class="w-390px"> <n-tabs v-model:value="currentTab" type="line" animated justify-content="space-evenly" class="w-390px">
<n-tab-pane :name="0"> <n-tab-pane :name="0">
<template #tab> <template #tab>
<n-space class="w-130px" justify="center"> <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-badge type="info" :value="groupMessage[0]?.filter(i => !i.isRead).length" :max="99" />
</n-space> </n-space>
</template> </template>
@ -122,7 +121,7 @@ const groupMessage = computed(() => {
<n-tab-pane :name="1"> <n-tab-pane :name="1">
<template #tab> <template #tab>
<n-space class="w-130px" justify="center"> <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-badge type="warning" :value="groupMessage[1]?.filter(i => !i.isRead).length" :max="99" />
</n-space> </n-space>
</template> </template>
@ -131,7 +130,7 @@ const groupMessage = computed(() => {
<n-tab-pane :name="2"> <n-tab-pane :name="2">
<template #tab> <template #tab>
<n-space class="w-130px" justify="center"> <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-badge type="error" :value="groupMessage[2]?.filter(i => !i.isRead).length" :max="99" />
</n-space> </n-space>
</template> </template>

View File

@ -6,16 +6,18 @@ import { renderIcon } from '@/utils'
const routeStore = useRouteStore() const routeStore = useRouteStore()
const searchValue = ref('') const searchValue = ref('')
const { t } = useI18n()
const options = computed(() => { const options = computed(() => {
return routeStore.rowRoutes.filter((item) => { return routeStore.rowRoutes.filter((item) => {
const conditions = [ 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), item.path?.includes(searchValue.value),
] ]
return conditions.some(condition => condition) return conditions.some(condition => condition)
}).map((item) => { }).map((item) => {
return { return {
label: item['meta.title'], label: t(`route.${String(item.name)}`, item['meta.title'] || item.name),
value: item.path, value: item.path,
icon: item['meta.icon'], icon: item['meta.icon'],
} }
@ -44,7 +46,7 @@ function handleSelect(value: string) {
<n-auto-complete <n-auto-complete
v-model:value="searchValue" class="w-20em m-r-1em" :input-props="{ v-model:value="searchValue" class="w-20em m-r-1em" :input-props="{
autocomplete: 'disabled', 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> <template #prefix>
<n-icon> <n-icon>

View File

@ -3,40 +3,45 @@ import { useAppStore } from '@/store'
const appStore = useAppStore() const appStore = useAppStore()
const { t } = useI18n()
const drawerActive = ref(false) const drawerActive = ref(false)
function openSetting() { function openSetting() {
drawerActive.value = !drawerActive.value drawerActive.value = !drawerActive.value
} }
const transitionSelectorOptions = [
const transitionSelectorOptions = computed(() => {
return [
{ {
label: '无', label: t('app.transitionNull'),
value: '', value: '',
}, },
{ {
label: '侧滑', label: t('app.transitionFadeSlide'),
value: 'fade-slide', value: 'fade-slide',
}, },
{ {
label: '下滑', label: t('app.transitionFadeBottom'),
value: 'fade-bottom', value: 'fade-bottom',
}, },
{ {
label: '收缩', label: t('app.transitionFadeScale'),
value: 'fade-scale', value: 'fade-scale',
}, },
{ {
label: '扩张', label: t('app.transitionZoomFade'),
value: 'zoom-fade', value: 'zoom-fade',
}, },
{ {
label: '坍缩', label: t('app.transitionZoomOut'),
value: 'zoom-out', value: 'zoom-out',
}, },
{ {
label: '柔和', label: t('app.transitionSoft'),
value: 'fade', value: 'fade',
}, },
] ]
})
const palette = [ const palette = [
'#ffb8b8', '#ffb8b8',
@ -59,13 +64,13 @@ const palette = [
function resetSetting() { function resetSetting() {
window.$dialog.warning({ window.$dialog.warning({
title: '重置所有设置', title: t('app.resetSettingTitle'),
content: '你确定重置所有设置?', content: t('app.resetSettingContent'),
positiveText: '确定', positiveText: t('common.confirm'),
negativeText: '取消', negativeText: t('common.cancel'),
onPositiveClick: () => { onPositiveClick: () => {
appStore.resetAlltheme() appStore.resetAlltheme()
window.$message.success('重置成功') window.$message.success(t('app.resetSettingMeaasge'))
}, },
}) })
} }
@ -77,65 +82,65 @@ function resetSetting() {
<CommonWrapper @click="openSetting"> <CommonWrapper @click="openSetting">
<div> <div>
<icon-park-outline-setting-two /> <icon-park-outline-setting-two />
<n-drawer v-model:show="drawerActive" :width="300"> <n-drawer v-model:show="drawerActive" :width="360">
<n-drawer-content title="系统设置" closable> <n-drawer-content :title="t('app.systemSetting')" closable>
<n-space vertical> <n-space vertical>
<n-divider>主题设置</n-divider> <n-divider>{{ $t('app.themeSetting') }}</n-divider>
<n-space justify="space-between"> <n-space justify="space-between">
色弱模式 {{ $t('app.colorWeak') }}
<n-switch :value="appStore.colorWeak" @update:value="appStore.toggleColorWeak" /> <n-switch :value="appStore.colorWeak" @update:value="appStore.toggleColorWeak" />
</n-space> </n-space>
<n-space justify="space-between"> <n-space justify="space-between">
灰色模式 {{ $t('app.blackAndWhite') }}
<n-switch :value="appStore.grayMode" @update:value="appStore.toggleGrayMode" /> <n-switch :value="appStore.grayMode" @update:value="appStore.toggleGrayMode" />
</n-space> </n-space>
<n-space align="center" justify="space-between"> <n-space align="center" justify="space-between">
主题色 {{ $t('app.themeColor') }}
<n-color-picker <n-color-picker
v-model:value="appStore.primaryColor" v-model:value="appStore.primaryColor"
class="w-7em" :swatches="palette" :show-alpha="false" class="w-10em" :swatches="palette"
@update:value="appStore.setPrimaryColor" @update:value="appStore.setPrimaryColor"
/> />
</n-space> </n-space>
<n-space align="center" justify="space-between"> <n-space align="center" justify="space-between">
切换动效 {{ $t('app.pageTransition') }}
<n-select v-model:value="appStore.transitionAnimation" class="w-7em" :options="transitionSelectorOptions" @update:value="appStore.reloadPage" /> <n-select v-model:value="appStore.transitionAnimation" class="w-10em" :options="transitionSelectorOptions" @update:value="appStore.reloadPage" />
</n-space> </n-space>
<n-divider>界面显示</n-divider> <n-divider>{{ $t('app.interfaceDisplay') }}</n-divider>
<n-space justify="space-between"> <n-space justify="space-between">
LOGO显示 {{ $t('app.logoDisplay') }}
<n-switch v-model:value="appStore.showLogo" /> <n-switch v-model:value="appStore.showLogo" />
</n-space> </n-space>
<n-space justify="space-between"> <n-space justify="space-between">
顶部进度 {{ $t('app.topProgress') }}
<n-switch v-model:value="appStore.showProgress" /> <n-switch v-model:value="appStore.showProgress" />
</n-space> </n-space>
<n-space justify="space-between"> <n-space justify="space-between">
多页签显示 {{ $t('app.multitab') }}
<n-switch v-model:value="appStore.showTabs" /> <n-switch v-model:value="appStore.showTabs" />
</n-space> </n-space>
<n-space justify="space-between"> <n-space justify="space-between">
底部标签显示 {{ $t('app.bottomCopyright') }}
<n-switch v-model:value="appStore.showFooter" /> <n-switch v-model:value="appStore.showFooter" />
</n-space> </n-space>
<n-space justify="space-between"> <n-space justify="space-between">
面包屑 {{ $t('app.breadcrumb') }}
<n-switch v-model:value="appStore.showBreadcrumb" /> <n-switch v-model:value="appStore.showBreadcrumb" />
</n-space> </n-space>
<n-space justify="space-between"> <n-space justify="space-between">
面包屑图标 {{ $t('app.BreadcrumbIcon') }}
<n-switch v-model:value="appStore.showBreadcrumbIcon" /> <n-switch v-model:value="appStore.showBreadcrumbIcon" />
</n-space> </n-space>
<n-space justify="space-between"> <n-space justify="space-between">
水印 {{ $t('app.watermake') }}
<n-switch v-model:value="appStore.showWatermark" /> <n-switch v-model:value="appStore.showWatermark" />
</n-space> </n-space>
</n-space> </n-space>
<template #footer> <template #footer>
<n-button type="error" @click="resetSetting"> <n-button type="error" @click="resetSetting">
重置设置 {{ $t('app.reset') }}
</n-button> </n-button>
</template> </template>
</n-drawer-content> </n-drawer-content>
@ -143,8 +148,6 @@ function resetSetting() {
</div> </div>
</CommonWrapper> </CommonWrapper>
</template> </template>
<span>设置</span> <span>{{ $t('app.setting') }}</span>
</n-tooltip> </n-tooltip>
</template> </template>
<style scoped></style>

View File

@ -2,12 +2,15 @@
import { renderIcon } from '@/utils/icon' import { renderIcon } from '@/utils/icon'
import { useAuthStore } from '@/store' import { useAuthStore } from '@/store'
const { t } = useI18n()
const { userInfo, resetAuthStore } = useAuthStore() const { userInfo, resetAuthStore } = useAuthStore()
const router = useRouter() const router = useRouter()
const options = [ const options = computed(() => {
return [
{ {
label: '个人中心', label: t('app.userCenter'),
key: 'userCenter', key: 'userCenter',
icon: renderIcon('carbon:user-avatar-filled-alt'), icon: renderIcon('carbon:user-avatar-filled-alt'),
}, },
@ -21,7 +24,7 @@ const options = [
icon: renderIcon('icon-park-outline:github'), icon: renderIcon('icon-park-outline:github'),
}, },
{ {
label: 'gitee', label: 'Gitee',
key: 'gitee', key: 'gitee',
icon: renderIcon('simple-icons:gitee'), icon: renderIcon('simple-icons:gitee'),
}, },
@ -30,18 +33,19 @@ const options = [
key: 'd1', key: 'd1',
}, },
{ {
label: '退出登录', label: t('app.loginOut'),
key: 'loginOut', key: 'loginOut',
icon: renderIcon('icon-park-outline:logout'), icon: renderIcon('icon-park-outline:logout'),
}, },
] ]
})
function handleSelect(key: string | number) { function handleSelect(key: string | number) {
if (key === 'loginOut') { if (key === 'loginOut') {
window.$dialog?.info({ window.$dialog?.info({
title: '退出登录', title: t('app.loginOutTitle'),
content: '确认退出当前账号?', content: t('app.loginOutContent'),
positiveText: '确定', positiveText: t('common.confirm'),
negativeText: '取消', negativeText: t('common.cancel'),
onPositiveClick: () => { onPositiveClick: () => {
resetAuthStore() resetAuthStore()
}, },

View File

@ -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>

View File

@ -21,7 +21,7 @@ function handleReload() {
<icon-park-outline-refresh :class="{ 'animate-spin': loading }" /> <icon-park-outline-refresh :class="{ 'animate-spin': loading }" />
</CommonWrapper> </CommonWrapper>
</template> </template>
<span>刷新页面</span> <span>{{ $t('common.reload') }}</span>
</n-tooltip> </n-tooltip>
</template> </template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { RouteLocationNormalized } from 'vue-router' import type { RouteLocationNormalized } from 'vue-router'
import Reload from './Reload.vue' import Reload from './Reload.vue'
import DropTabs from './DropTabs.vue'
import { renderIcon } from '@/utils' import { renderIcon } from '@/utils'
import { useAppStore, useTabStore } from '@/store' import { useAppStore, useTabStore } from '@/store'
@ -14,38 +15,41 @@ function handleTab(route: RouteLocationNormalized) {
function handleClose(path: string) { function handleClose(path: string) {
tabStore.closeTab(path) tabStore.closeTab(path)
} }
const options = [ const { t } = useI18n()
const options = computed(() => {
return [
{ {
label: '刷新', label: t('common.reload'),
key: 'reload', key: 'reload',
icon: renderIcon('icon-park-outline:redo'), icon: renderIcon('icon-park-outline:redo'),
}, },
{ {
label: '关闭', label: t('common.close'),
key: 'closeCurrent', key: 'closeCurrent',
icon: renderIcon('icon-park-outline:close'), icon: renderIcon('icon-park-outline:close'),
}, },
{ {
label: '关闭其他', label: t('app.closeOther'),
key: 'closeOther', key: 'closeOther',
icon: renderIcon('icon-park-outline:delete-four'), icon: renderIcon('icon-park-outline:delete-four'),
}, },
{ {
label: '关闭左侧', label: t('app.closeLeft'),
key: 'closeLeft', key: 'closeLeft',
icon: renderIcon('icon-park-outline:to-left'), icon: renderIcon('icon-park-outline:to-left'),
}, },
{ {
label: '关闭右侧', label: t('app.closeRight'),
key: 'closeRight', key: 'closeRight',
icon: renderIcon('icon-park-outline:to-right'), icon: renderIcon('icon-park-outline:to-right'),
}, },
{ {
label: '全部关闭', label: t('app.closeAll'),
key: 'closeAll', key: 'closeAll',
icon: renderIcon('icon-park-outline:fullwidth'), icon: renderIcon('icon-park-outline:fullwidth'),
}, },
] ]
})
const showDropdown = ref(false) const showDropdown = ref(false)
const x = ref(0) const x = ref(0)
const y = ref(0) const y = ref(0)
@ -91,17 +95,6 @@ function handleContextMenu(e: MouseEvent, route: RouteLocationNormalized) {
function onClickoutside() { function onClickoutside() {
showDropdown.value = false 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> </script>
<template> <template>
@ -119,7 +112,9 @@ function handleDropTabs(key: string, option: any) {
:name="item.path" :name="item.path"
@click="router.push(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>
<n-tab <n-tab
v-for="item in tabStore.tabs" v-for="item in tabStore.tabs"
@ -130,23 +125,12 @@ function handleDropTabs(key: string, option: any) {
@contextmenu="handleContextMenu($event, item)" @contextmenu="handleContextMenu($event, item)"
> >
<div class="flex-x-center gap-2"> <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> </div>
</n-tab> </n-tab>
<template #suffix> <template #suffix>
<Reload /> <Reload />
<n-dropdown <DropTabs />
: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> </template>
</n-tabs> </n-tabs>
<n-dropdown <n-dropdown
@ -162,4 +146,4 @@ function handleDropTabs(key: string, option: any) {
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>./DropTabs.vue

19
src/modules/i18n.ts Normal file
View File

@ -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)
}

View File

@ -10,7 +10,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': null, 'pid': null,
}, },
{ {
'name': 'dashboard_workbench', 'name': 'workbench',
'path': '/dashboard/workbench', 'path': '/dashboard/workbench',
'meta.title': '工作台', 'meta.title': '工作台',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
@ -21,7 +21,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 1, 'pid': 1,
}, },
{ {
'name': 'dashboard_monitor', 'name': 'monitor',
'path': '/dashboard/monitor', 'path': '/dashboard/monitor',
'meta.title': '监控页', 'meta.title': '监控页',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
@ -51,7 +51,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 4, 'pid': 4,
}, },
{ {
'name': 'test2_detail', 'name': 'test2Detail',
'path': '/test/test2/detail', 'path': '/test/test2/detail',
'meta.title': '多级菜单的详情页', 'meta.title': '多级菜单的详情页',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
@ -93,7 +93,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': null, 'pid': null,
}, },
{ {
'name': 'list_commonList', 'name': 'commonList',
'path': '/list/commonList', 'path': '/list/commonList',
'meta.title': '常用列表', 'meta.title': '常用列表',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
@ -103,7 +103,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 10, 'pid': 10,
}, },
{ {
'name': 'list_cardList', 'name': 'cardList',
'path': '/list/cardList', 'path': '/list/cardList',
'meta.title': '卡片列表', 'meta.title': '卡片列表',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
@ -113,8 +113,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 10, 'pid': 10,
}, },
{ {
'name': 'plugin', 'name': 'demo',
'path': '/plugin', 'path': '/demo',
'meta.title': '功能示例', 'meta.title': '功能示例',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:application-one', 'meta.icon': 'icon-park-outline:application-one',
@ -124,38 +124,38 @@ export const staticRoutes: AppRoute.RowRoute[] = [
}, },
{ {
'name': 'fetch', 'name': 'fetch',
'path': '/plugin/fetch', 'path': '/demo/fetch',
'meta.title': '接口功能测试', 'meta.title': '请求示例',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:international', 'meta.icon': 'icon-park-outline:international',
'componentPath': '/plugin/fetch/index.vue', 'componentPath': '/demo/fetch/index.vue',
'id': 5, 'id': 5,
'pid': 13, 'pid': 13,
}, },
{ {
'name': 'plugin_echarts', 'name': 'echarts',
'path': '/plugin/echarts', 'path': '/demo/echarts',
'meta.title': 'ECharts', 'meta.title': 'ECharts',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:chart-proportion', 'meta.icon': 'icon-park-outline:chart-proportion',
'componentPath': '/plugin/echarts/index.vue', 'componentPath': '/demo/echarts/index.vue',
'id': 15, 'id': 15,
'pid': 13, 'pid': 13,
}, },
{ {
'name': 'PluginMap', 'name': 'map',
'path': '/plugin/map', 'path': '/demo/map',
'meta.title': '地图', 'meta.title': '地图',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'carbon:map', 'meta.icon': 'carbon:map',
'meta.keepAlive': true, 'meta.keepAlive': true,
'componentPath': '/plugin/map/index.vue', 'componentPath': '/demo/map/index.vue',
'id': 17, 'id': 17,
'pid': 13, 'pid': 13,
}, },
{ {
'name': 'plugin_editor', 'name': 'editor',
'path': '/plugin/editor', 'path': '/demo/editor',
'meta.title': '编辑器', 'meta.title': '编辑器',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:editor', 'meta.icon': 'icon-park-outline:editor',
@ -164,52 +164,52 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 13, 'pid': 13,
}, },
{ {
'name': 'plugin_md', 'name': 'editorMd',
'path': '/plugin/editor/md', 'path': '/demo/editor/md',
'meta.title': 'MarkDown', 'meta.title': 'MarkDown',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'ri:markdown-line', 'meta.icon': 'ri:markdown-line',
'componentPath': '/plugin/editor/md/index.vue', 'componentPath': '/demo/editor/md/index.vue',
'id': 19, 'id': 19,
'pid': 18, 'pid': 18,
}, },
{ {
'name': 'plugin_rich', 'name': 'editorRich',
'path': '/plugin/editor/rich', 'path': '/demo/editor/rich',
'meta.title': '富文本', 'meta.title': '富文本',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:edit-one', 'meta.icon': 'icon-park-outline:edit-one',
'componentPath': '/plugin/editor/rich/index.vue', 'componentPath': '/demo/editor/rich/index.vue',
'id': 20, 'id': 20,
'pid': 18, 'pid': 18,
}, },
{ {
'name': 'plugin_clipboard', 'name': 'clipboard',
'path': '/plugin/clipboard', 'path': '/demo/clipboard',
'meta.title': '剪贴板', 'meta.title': '剪贴板',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:clipboard', 'meta.icon': 'icon-park-outline:clipboard',
'componentPath': '/plugin/clipboard/index.vue', 'componentPath': '/demo/clipboard/index.vue',
'id': 21, 'id': 21,
'pid': 13, 'pid': 13,
}, },
{ {
'name': 'plugin_icons', 'name': 'icons',
'path': '/plugin/icons', 'path': '/demo/icons',
'meta.title': '图标', 'meta.title': '图标',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:winking-face-with-open-eyes', 'meta.icon': 'icon-park-outline:winking-face-with-open-eyes',
'componentPath': '/plugin/icons/index.vue', 'componentPath': '/demo/icons/index.vue',
'id': 22, 'id': 22,
'pid': 13, 'pid': 13,
}, },
{ {
'name': 'plugin_QRCode', 'name': 'QRCode',
'path': '/plugin/QRCode', 'path': '/demo/QRCode',
'meta.title': '二维码', 'meta.title': '二维码',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:two-dimensional-code', 'meta.icon': 'icon-park-outline:two-dimensional-code',
'componentPath': '/plugin/QRCode/index.vue', 'componentPath': '/demo/QRCode/index.vue',
'id': 23, 'id': 23,
'pid': 13, 'pid': 13,
}, },
@ -224,9 +224,9 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': null, 'pid': null,
}, },
{ {
'name': 'docments_vue', 'name': 'docmentsVue',
'path': '/docments/vue', 'path': '/docments/vue',
'meta.title': 'vue', 'meta.title': 'Vue',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'logos:vue', 'meta.icon': 'logos:vue',
'componentPath': '/docments/vue/index.vue', 'componentPath': '/docments/vue/index.vue',
@ -234,9 +234,9 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 24, 'pid': 24,
}, },
{ {
'name': 'docments_vite', 'name': 'docmentsVite',
'path': '/docments/vite', 'path': '/docments/vite',
'meta.title': 'vite', 'meta.title': 'Vite',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'logos:vitejs', 'meta.icon': 'logos:vitejs',
'componentPath': '/docments/vite/index.vue', 'componentPath': '/docments/vite/index.vue',
@ -244,7 +244,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 24, 'pid': 24,
}, },
{ {
'name': 'docments_vueuse', 'name': 'docmentsVueuse',
'path': '/docments/vueuse', 'path': '/docments/vueuse',
'meta.title': 'VueUse外链', 'meta.title': 'VueUse外链',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
@ -257,7 +257,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
{ {
'name': 'permission', 'name': 'permission',
'path': '/permission', 'path': '/permission',
'meta.title': '权限示例', 'meta.title': '权限',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:people-safe', 'meta.icon': 'icon-park-outline:people-safe',
'componentPath': null, 'componentPath': null,
@ -265,7 +265,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': null, 'pid': null,
}, },
{ {
'name': 'permission_permission', 'name': 'permissionDemo',
'path': '/permission/permission', 'path': '/permission/permission',
'meta.title': '权限示例', 'meta.title': '权限示例',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
@ -275,9 +275,9 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 28, 'pid': 28,
}, },
{ {
'name': 'permission_justSuper', 'name': 'justSuper',
'path': '/permission/justSuper', 'path': '/permission/justSuper',
'meta.title': '超管super可见', 'meta.title': 'super可见',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.roles': [ 'meta.roles': [
'super', 'super',
@ -300,7 +300,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
{ {
'name': 'demo403', 'name': 'demo403',
'path': '/error/403', 'path': '/error/403',
'meta.title': '403', 'meta.title': '403',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'carbon:error', 'meta.icon': 'carbon:error',
'meta.order': 3, 'meta.order': 3,
@ -311,7 +311,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
{ {
'name': 'demo404', 'name': 'demo404',
'path': '/error/404', 'path': '/error/404',
'meta.title': '404', 'meta.title': '404',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'icon-park-outline:error', 'meta.icon': 'icon-park-outline:error',
'meta.order': 2, 'meta.order': 2,
@ -322,7 +322,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
{ {
'name': 'demo500', 'name': 'demo500',
'path': '/error/500', 'path': '/error/500',
'meta.title': '500', 'meta.title': '500',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
'meta.icon': 'carbon:data-error', 'meta.icon': 'carbon:data-error',
'meta.order': 1, 'meta.order': 1,
@ -341,7 +341,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': null, 'pid': null,
}, },
{ {
'name': 'setting_account', 'name': 'accountSetting',
'path': '/setting/account', 'path': '/setting/account',
'meta.title': '用户设置', 'meta.title': '用户设置',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
@ -351,7 +351,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 35, 'pid': 35,
}, },
{ {
'name': 'setting_dictionary', 'name': 'dictionarySetting',
'path': '/setting/dictionary', 'path': '/setting/dictionary',
'meta.title': '字典设置', 'meta.title': '字典设置',
'meta.requiresAuth': true, 'meta.requiresAuth': true,
@ -361,7 +361,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'pid': 35, 'pid': 35,
}, },
{ {
'name': 'setting_menu', 'name': 'menuSetting',
'path': '/setting/menu', 'path': '/setting/menu',
'meta.title': '菜单设置', 'meta.title': '菜单设置',
'meta.requiresAuth': true, 'meta.requiresAuth': true,

View File

@ -2,6 +2,7 @@ import type { GlobalThemeOverrides } from 'naive-ui'
import chroma from 'chroma-js' import chroma from 'chroma-js'
import { set } from 'radash' import { set } from 'radash'
import themeConfig from './theme.json' import themeConfig from './theme.json'
import { local, setLocale } from '@/utils'
type TransitionAnimation = '' | 'fade-slide' | 'fade-bottom' | 'fade-scale' | 'zoom-fade' | 'zoom-out' type TransitionAnimation = '' | 'fade-slide' | 'fade-bottom' | 'fade-scale' | 'zoom-fade' | 'zoom-out'
@ -17,6 +18,7 @@ export const useAppStore = defineStore('app-store', {
state: () => { state: () => {
return { return {
footerText: 'Copyright © 2024 chansee97', footerText: 'Copyright © 2024 chansee97',
lang: 'zh',
theme: themeConfig as GlobalThemeOverrides, theme: themeConfig as GlobalThemeOverrides,
primaryColor: themeConfig.common.primaryColor, primaryColor: themeConfig.common.primaryColor,
collapsed: false, collapsed: false,
@ -65,6 +67,11 @@ export const useAppStore = defineStore('app-store', {
// 重置所有配色 // 重置所有配色
this.setPrimaryColor(this.primaryColor) this.setPrimaryColor(this.primaryColor)
}, },
setAppLang(lang: App.lang) {
setLocale(lang)
local.set('lang', lang)
this.lang = lang
},
/* 设置主题色 */ /* 设置主题色 */
setPrimaryColor(color: string) { setPrimaryColor(color: string) {
const brightenColor = chroma(color).brighten(1).hex() const brightenColor = chroma(color).brighten(1).hex()

View File

@ -3,7 +3,7 @@ import { RouterLink } from 'vue-router'
import { h } from 'vue' import { h } from 'vue'
import { clone, construct, min } from 'radash' import { clone, construct, min } from 'radash'
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import { arrayToTree, local, renderIcon } from '@/utils' import { $t, arrayToTree, local, renderIcon } from '@/utils'
import { router } from '@/router' import { router } from '@/router'
import { fetchUserRoutes } from '@/service' import { fetchUserRoutes } from '@/service'
import { staticRoutes } from '@/router/routes.static' import { staticRoutes } from '@/router/routes.static'
@ -81,9 +81,9 @@ export const useRouteStore = defineStore('route-store', {
path: item.path, 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, key: item.path,
icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined, 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, redirect: import.meta.env.VITE_HOME_PATH,
component: BasicLayout, component: BasicLayout,
meta: { meta: {
title: '首页', title: '',
icon: 'icon-park-outline:home', icon: 'icon-park-outline:home',
}, },
children: [], children: [],

View File

@ -19,6 +19,13 @@ declare namespace NaiveUI {
type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning' 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 { declare namespace Storage {
interface Session { interface Session {
demoKey: string demoKey: string
@ -33,5 +40,11 @@ declare namespace Storage {
refreshToken: string refreshToken: string
/* 存储登录账号 */ /* 存储登录账号 */
loginAccount: any loginAccount: any
/* 存储当前语言 */
lang: App.lang
} }
} }
declare namespace App {
type lang = 'zh' | 'en'
}

View File

@ -23,6 +23,8 @@ declare namespace AppRoute {
withoutTab?: boolean withoutTab?: boolean
/** 当前路由是否会被固定在Tab中,用于一些常驻页面 */ /** 当前路由是否会被固定在Tab中,用于一些常驻页面 */
pinTab?: boolean pinTab?: boolean
/** 当前路由i18n标识 */
i18nKey?: string
} }
/** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */ /** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */
interface baseRoute { interface baseRoute {

7
src/utils/i18n.ts Normal file
View File

@ -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

View File

@ -1,3 +1,4 @@
export * from './icon' export * from './icon'
export * from './storage' export * from './storage'
export * from './array' export * from './array'
export * from './i18n'

View File

@ -10,18 +10,22 @@ const authStore = useAuthStore()
function toOtherForm(type: any) { function toOtherForm(type: any) {
emit('update:modelValue', type) emit('update:modelValue', type)
} }
const rules = {
const { t } = useI18n()
const rules = computed(() => {
return {
account: { account: {
required: true, required: true,
trigger: 'blur', trigger: 'blur',
message: '请输入账户', message: t('login.accountRuleTip'),
}, },
pwd: { pwd: {
required: true, required: true,
trigger: 'blur', trigger: 'blur',
message: '请输入密码', message: t('login.passwordRuleTip'),
}, },
} }
})
const formValue = ref({ const formValue = ref({
account: 'super', account: 'super',
pwd: '123456', pwd: '123456',
@ -46,6 +50,9 @@ function handleLogin() {
isLoading.value = false isLoading.value = false
}) })
} }
onMounted(() => {
checkUserAccount()
})
function checkUserAccount() { function checkUserAccount() {
const loginAccount = local.get('loginAccount') const loginAccount = local.get('loginAccount')
if (!loginAccount) if (!loginAccount)
@ -54,20 +61,19 @@ function checkUserAccount() {
formValue.value = loginAccount formValue.value = loginAccount
isRemember.value = true isRemember.value = true
} }
checkUserAccount()
</script> </script>
<template> <template>
<div> <div>
<n-h2 depth="3" class="text-center"> <n-h2 depth="3" class="text-center">
登录 {{ $t('login.signInTitle') }}
</n-h2> </n-h2>
<n-form ref="formRef" :rules="rules" :model="formValue" :show-label="false" size="large"> <n-form ref="formRef" :rules="rules" :model="formValue" :show-label="false" size="large">
<n-form-item path="account"> <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>
<n-form-item path="pwd"> <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> <template #password-invisible-icon>
<icon-park-outline-preview-close-one /> <icon-park-outline-preview-close-one />
</template> </template>
@ -79,22 +85,25 @@ checkUserAccount()
<n-space vertical :size="20"> <n-space vertical :size="20">
<div class="flex-y-center justify-between"> <div class="flex-y-center justify-between">
<n-checkbox v-model:checked="isRemember"> <n-checkbox v-model:checked="isRemember">
记住我 {{ $t('login.rememberMe') }}
</n-checkbox> </n-checkbox>
<n-button type="primary" text @click="toOtherForm('resetPwd')"> <n-button type="primary" text @click="toOtherForm('resetPwd')">
忘记密码 {{ $t('login.forgotPassword') }}
</n-button> </n-button>
</div> </div>
<n-button block type="primary" size="large" :loading="isLoading" :disabled="isLoading" @click="handleLogin"> <n-button block type="primary" size="large" :loading="isLoading" :disabled="isLoading" @click="handleLogin">
登录 {{ $t('login.signIn') }}
</n-button> </n-button>
<n-flex>
<n-text>{{ $t('login.noAccountText') }}</n-text>
<n-button type="primary" text @click="toOtherForm('register')"> <n-button type="primary" text @click="toOtherForm('register')">
立即注册 {{ $t('login.signUp') }}
</n-button> </n-button>
</n-flex>
</n-space> </n-space>
</n-form> </n-form>
<n-divider> <n-divider>
<span op-80>其他登录</span> <span op-80>{{ $t('login.or') }}</span>
</n-divider> </n-divider>
<n-space justify="center"> <n-space justify="center">
<n-button circle> <n-button circle>

View File

@ -3,21 +3,23 @@ const emit = defineEmits(['update:modelValue'])
function toLogin() { function toLogin() {
emit('update:modelValue', 'login') emit('update:modelValue', 'login')
} }
const { t } = useI18n()
const rules = { const rules = {
account: { account: {
required: true, required: true,
trigger: 'blur', trigger: 'blur',
message: '请输入账户', message: t('login.accountRuleTip'),
}, },
pwd: { pwd: {
required: true, required: true,
trigger: 'blur', trigger: 'blur',
message: '请输入密码', message: t('login.passwordRuleTip'),
}, },
rePwd: { rePwd: {
required: true, required: true,
trigger: 'blur', trigger: 'blur',
message: '请再次确认密码', message: t('login.checkPasswordRuleTip'),
}, },
} }
const formValue = ref({ const formValue = ref({
@ -34,7 +36,7 @@ function handleRegister() {}
<template> <template>
<div> <div>
<n-h2 depth="3" class="text-center"> <n-h2 depth="3" class="text-center">
注册 {{ $t('login.registerTitle') }}
</n-h2> </n-h2>
<n-form <n-form
:rules="rules" :rules="rules"
@ -46,14 +48,14 @@ function handleRegister() {}
<n-input <n-input
v-model:value="formValue.account" v-model:value="formValue.account"
clearable clearable
placeholder="输入账号" :placeholder="$t('login.accountPlaceholder')"
/> />
</n-form-item> </n-form-item>
<n-form-item path="pwd"> <n-form-item path="pwd">
<n-input <n-input
v-model:value="formValue.pwd" v-model:value="formValue.pwd"
type="password" type="password"
placeholder="输入密码" :placeholder="$t('login.passwordPlaceholder')"
clearable clearable
show-password-on="click" show-password-on="click"
> >
@ -69,7 +71,7 @@ function handleRegister() {}
<n-input <n-input
v-model:value="formValue.rePwd" v-model:value="formValue.rePwd"
type="password" type="password"
placeholder="请再次输入密码" :placeholder="$t('login.checkPasswordPlaceholder')"
clearable clearable
show-password-on="click" show-password-on="click"
> >
@ -88,17 +90,11 @@ function handleRegister() {}
class="w-full" class="w-full"
> >
<n-checkbox v-model:checked="isRead"> <n-checkbox v-model:checked="isRead">
我已阅读并同意 <n-button {{ $t('login.readAndAgree') }} <n-button
type="primary" type="primary"
text text
> >
用户协议 {{ $t('login.userAgreement') }}
</n-button>
<n-button
type="primary"
text
>
xx社区规范
</n-button> </n-button>
</n-checkbox> </n-checkbox>
<n-button <n-button
@ -106,16 +102,18 @@ function handleRegister() {}
type="primary" type="primary"
@click="handleRegister" @click="handleRegister"
> >
立即注册 {{ $t('login.signUp') }}
</n-button> </n-button>
<n-flex justify="center">
<n-text>{{ $t('login.haveAccountText') }}</n-text>
<n-button <n-button
tertiary text
block
type="primary" type="primary"
@click="toLogin" @click="toLogin"
> >
已有账号去登录 {{ $t('login.signIn') }}
</n-button> </n-button>
</n-flex>
</n-space> </n-space>
</n-form-item> </n-form-item>
</n-form> </n-form>

View File

@ -3,26 +3,33 @@ const emit = defineEmits(['update:modelValue'])
function toLogin() { function toLogin() {
emit('update:modelValue', 'login') emit('update:modelValue', 'login')
} }
const rules = { const { t } = useI18n()
const rules = computed(() => {
return {
account: { account: {
required: true, required: true,
trigger: 'blur', trigger: 'blur',
message: '请输入账号/手机号码', message: t('login.resetPasswordRuleTip'),
}, },
} }
})
const formValue = ref({ const formValue = ref({
account: '', account: '',
}) })
const formRef = ref<FormInst | null>(null)
function handleRegister() {} function handleRegister() {
formRef.value?.validate()
}
</script> </script>
<template> <template>
<div> <div>
<n-h2 depth="3" class="text-center"> <n-h2 depth="3" class="text-center">
重置密码 {{ $t('login.resetPasswordTitle') }}
</n-h2> </n-h2>
<n-form <n-form
ref="formRef"
:rules="rules" :rules="rules"
:model="formValue" :model="formValue"
:show-label="false" :show-label="false"
@ -32,7 +39,7 @@ function handleRegister() {}
<n-input <n-input
v-model:value="formValue.account" v-model:value="formValue.account"
clearable clearable
placeholder="账号/手机号码" :placeholder="$t('login.resetPasswordPlaceholder')"
/> />
</n-form-item> </n-form-item>
<n-form-item> <n-form-item>
@ -46,16 +53,18 @@ function handleRegister() {}
type="primary" type="primary"
@click="handleRegister" @click="handleRegister"
> >
重置密码 {{ $t('login.resetPassword') }}
</n-button> </n-button>
<n-flex justify="center">
<n-text>{{ $t('login.haveAccountText') }}</n-text>
<n-button <n-button
tertiary text
block
type="primary" type="primary"
@click="toLogin" @click="toLogin"
> >
已有账号去登录 {{ $t('login.signIn') }}
</n-button> </n-button>
</n-flex>
</n-space> </n-space>
</n-form-item> </n-form-item>
</n-form> </n-form>

View File

@ -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);"> <n-el class="wh-full flex-center" style="background-color: var(--body-color);">
<div class="fixed top-40px right-40px text-lg"> <div class="fixed top-40px right-40px text-lg">
<DarkModeSwitch /> <DarkModeSwitch />
<LangsSwitch />
</div> </div>
<n-el <n-el
class="p-4xl h-full w-full sm:w-450px sm:h-700px" class="p-4xl h-full w-full sm:w-450px sm:h-700px"