Browse Source

feat: multi-language layout

vben 4 năm trước cách đây
mục cha
commit
e5f8ce3fd8
44 tập tin đã thay đổi với 504 bổ sung386 xóa
  1. 1 1
      mock/sys/user.ts
  2. 3 3
      src/components/Application/index.ts
  3. 14 2
      src/components/Application/src/AppLocalePicker.vue
  4. 6 4
      src/components/Dropdown/src/Dropdown.tsx
  5. 7 72
      src/design/ant/index.less
  6. 0 15
      src/design/ant/input.less
  7. 4 1
      src/design/color.less
  8. 1 16
      src/design/index.less
  9. 5 5
      src/design/mixins.less
  10. 8 0
      src/design/public.less
  11. 0 103
      src/design/reset.less
  12. 2 2
      src/design/transition/base.less
  13. 1 1
      src/design/transition/fade.less
  14. 1 0
      src/design/var/index.less
  15. 11 1
      src/hooks/setting/useLocaleSetting.ts
  16. 21 0
      src/hooks/web/useI18n.ts
  17. 0 7
      src/hooks/web/useLocale.ts
  18. 5 2
      src/layouts/default/footer/index.tsx
  19. 29 9
      src/layouts/default/header/LayoutHeader.tsx
  20. 12 5
      src/layouts/default/header/UserDropdown.tsx
  21. 11 0
      src/layouts/default/header/index.less
  22. 2 1
      src/layouts/default/index.less
  23. 12 4
      src/layouts/default/lock/LockAction.tsx
  24. 6 1
      src/layouts/default/multitabs/TabContent.tsx
  25. 10 6
      src/layouts/default/multitabs/data.ts
  26. 49 47
      src/layouts/default/setting/SettingDrawer.tsx
  27. 15 23
      src/layouts/default/setting/enum.ts
  28. 14 12
      src/layouts/default/sider/index.tsx
  29. 4 0
      src/locales/lang/en/layout/footer.ts
  30. 18 0
      src/locales/lang/en/layout/header.ts
  31. 10 0
      src/locales/lang/en/layout/multipleTab.ts
  32. 73 0
      src/locales/lang/en/layout/setting.ts
  33. 4 0
      src/locales/lang/zh_CN/layout/footer.ts
  34. 19 0
      src/locales/lang/zh_CN/layout/header.ts
  35. 10 0
      src/locales/lang/zh_CN/layout/multipleTab.ts
  36. 72 0
      src/locales/lang/zh_CN/layout/setting.ts
  37. 1 0
      src/settings/projectSetting.ts
  38. 1 0
      src/types/config.d.ts
  39. 3 3
      src/views/sys/error-log/DetailModal.vue
  40. 7 8
      src/views/sys/error-log/data.tsx
  41. 3 3
      src/views/sys/error-log/index.vue
  42. 13 12
      src/views/sys/exception/Exception.tsx
  43. 4 5
      src/views/sys/lock/index.vue
  44. 12 12
      src/views/sys/login/Login.vue

+ 1 - 1
mock/sys/user.ts

@@ -6,7 +6,7 @@ function createFakeUserList() {
     {
       userId: '1',
       username: 'vben',
-      realName: 'Vben',
+      realName: 'Vben Admin',
       desc: 'manager',
       password: '123456',
       token: 'fakeToken1',

+ 3 - 3
src/components/Application/index.ts

@@ -1,8 +1,8 @@
-import AppLocalPicker from './src/AppLocalPicker.vue';
+import AppLocalePicker from './src/AppLocalePicker.vue';
 import AppPageFooter from './src/AppPageFooter.vue';
 import AppLogo from './src/AppLogo.vue';
 import { withInstall } from '../util';
 
-export { AppLocalPicker, AppPageFooter, AppLogo };
+export { AppLocalePicker, AppPageFooter, AppLogo };
 
-export default withInstall(AppLocalPicker, AppPageFooter, AppLogo);
+export default withInstall(AppLocalePicker, AppPageFooter, AppLogo);

+ 14 - 2
src/components/Application/src/AppLocalPicker.vue → src/components/Application/src/AppLocalePicker.vue

@@ -4,6 +4,7 @@
     :dropMenuList="localeList"
     :selectedKeys="selectedKeys"
     @menuEvent="handleMenuEvent"
+    overlayClassName="app-locale-picker-overlay"
   >
     <span class="app-local-picker">
       <GlobalOutlined class="app-local-picker__icon" />
@@ -30,8 +31,12 @@
         type: Boolean,
         default: true,
       },
+      reload: {
+        type: Boolean,
+        default: false,
+      },
     },
-    setup() {
+    setup(props) {
       const { localeList } = useLocaleSetting();
       const selectedKeys = ref<string[]>([]);
 
@@ -50,6 +55,7 @@
       function toggleLocale(lang: LocaleType | string) {
         changeLocale(lang as LocaleType);
         selectedKeys.value = [lang as string];
+        props.reload && location.reload();
       }
 
       function handleMenuEvent(menu: DropMenu) {
@@ -61,7 +67,13 @@
   });
 </script>
 
-<style lang="less" scoped>
+<style lang="less">
+  .app-locale-picker-overlay {
+    .ant-dropdown-menu-item {
+      min-width: 160px;
+    }
+  }
+
   .app-local-picker {
     display: flex;
     align-items: center;

+ 6 - 4
src/components/Dropdown/src/Dropdown.tsx

@@ -1,11 +1,12 @@
+import type { Trigger } from './types';
+
 import { defineComponent, computed, unref } from 'vue';
-import { Dropdown, Menu, Divider } from 'ant-design-vue';
+import { Dropdown, Menu } from 'ant-design-vue';
 
 import Icon from '/@/components/Icon/index';
 
 import { basicDropdownProps } from './props';
 import { getSlot } from '/@/utils/helper/tsxHelper';
-import { Trigger } from './types';
 
 export default defineComponent({
   name: 'Dropdown',
@@ -24,7 +25,7 @@ export default defineComponent({
         <Menu onClick={handleClickMenu} selectedKeys={props.selectedKeys}>
           {() => (
             <>
-              {unref(getMenuList).map((item, index) => {
+              {unref(getMenuList).map((item) => {
                 const { disabled, icon, text, divider, event } = item;
                 return [
                   <Menu.Item key={`${event}`} disabled={disabled}>
@@ -35,7 +36,8 @@ export default defineComponent({
                       </>
                     )}
                   </Menu.Item>,
-                  divider && <Divider key={`d-${index}`} />,
+                  // @ts-ignore
+                  divider && <Menu.Divider key={`d-${event}`} />,
                 ];
               })}
             </>

+ 7 - 72
src/design/ant/index.less

@@ -13,20 +13,17 @@
   }
 }
 
-// .ant-form-item-label {
-//   text-align: unset;
-// }
-
 // =================================
 // ==============descriptions=======
 // =================================
-.ant-descriptions-bordered .ant-descriptions-item-label {
-  background-color: @background-color-light;
-}
+// .ant-descriptions-bordered .ant-descriptions-item-label {
+//   background-color: @background-color-light;
+// }
+
+// .ant-descriptions .ant-descriptions-item-content {
+//   color: @text-color-call-out;
+// }
 
-.ant-descriptions .ant-descriptions-item-content {
-  color: @text-color-call-out;
-}
 // =================================
 // ==============modal message======
 // =================================
@@ -46,68 +43,6 @@
   color: @primary-color !important;
 }
 
-.ant-modal-mask {
-  background-color: rgba(0, 0, 0, 0.2);
-}
-// =================================
-// ==============menu===============
-// =================================
-.ant-menu-item {
-  &-selected {
-    a {
-      color: @primary-color;
-
-      &:hover {
-        color: @primary-color;
-      }
-    }
-  }
-}
-// =================================
-// ==============dropdown===========
-// =================================
-.ant-dropdown {
-  .ant-divider {
-    margin: 4px 0;
-  }
-
-  &-menu-item {
-    line-height: 30px;
-    color: @text-color-call-out;
-
-    &:hover {
-      color: inherit;
-      background-color: @border-color-shallow-light;
-    }
-  }
-}
-// =================================
-// ==============back-top===========
-// =================================
-.ant-back-top {
-  right: 50px;
-  bottom: 60px;
-}
-// =================================
-// ==============calendar===========
-// =================================
-.ant-calendar-picker {
-  width: 100%;
-}
-// =================================
-// ==============tooltip============
-// =================================
-
-.ant-tooltip {
-  &-inner {
-    padding: 6px 16px;
-    line-height: 20px;
-    color: @white;
-    background: @text-color-base;
-    border-radius: 4px;
-    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
-  }
-}
 // =================================
 // ==============form===============
 // =================================

+ 0 - 15
src/design/ant/input.less

@@ -5,24 +5,9 @@
 .ant-input {
   &-number {
     min-width: 110px;
-    border-color: @border-color-shallow-dark;
   }
 }
 
-.ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled) {
-  border-color: @info-color;
-}
-
-.ant-input-disabled,
-.ant-select-disabled .ant-select-selection,
-.ant-cascader-picker-label {
-  color: @text-color-base !important;
-}
-
-.ant-input-disabled {
-  background-color: @background-color-light;
-}
-
 .ant-input-affix-wrapper .ant-input-suffix {
   right: 9px;
 }

+ 4 - 1
src/design/color.less

@@ -13,6 +13,9 @@
 }
 
 @white: #fff;
+
+@content-bg: #f0f2f5;
+
 @info-color: @primary-color;
 
 @basic-mask-color: fade(@white, 30%);
@@ -24,7 +27,7 @@
 @iconify-bg-color: #5551;
 
 // =================================
-// ==============border-color============
+// ==============border-color=======
 // =================================
 
 // Dark-dark

+ 1 - 16
src/design/index.less

@@ -54,8 +54,7 @@ input::-ms-reveal {
 }
 
 body {
-  // font-family: 'Microsoft YaHei,微软雅黑,Arial,sans-serif,Helvetica Neue,Helvetica,Pingfang SC,Hiragino Sans GB';
-  font-family: '-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji';
+  font-family: 'BlinkMacSystemFont,segoe ui,Microsoft YaHei,Arial,sans-serif,Helvetica Neue,Helvetica,Pingfang SC,Hiragino Sans GB,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji';
   font-style: normal;
   font-weight: normal;
   line-height: 1.428571429; // 20/14
@@ -159,17 +158,3 @@ embed,
 object {
   vertical-align: baseline !important;
 }
-
-#app {
-  width: 100%;
-  height: 100%;
-}
-
-.ant-layout {
-  background: #f0f2f5;
-
-  // &-content {
-  //   position: relative;
-  //   overflow: hidden;
-  // }
-}

+ 5 - 5
src/design/mixins.less

@@ -16,33 +16,33 @@
     color: @color;
   }
 }
-// 文本截断
+// Text truncation
 .text-truncate() {
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
-/* 强制不换行 */
+/* Force no line break */
 .word-nowrap() {
   word-wrap: normal;
   white-space: nowrap;
 }
 
-/* 强制换行 */
+/* Force line break */
 .break-all() {
   word-break: break-all;
   word-wrap: break-word;
   white-space: normal;
 }
 
-// 禁止选中
+// Prohibit selection
 .unselect() {
   cursor: pointer;
   user-select: none;
 }
 
-/* 适用于webkit内核和移动端 */
+/* Suitable for webkit core and mobile */
 .ellipsis-multiple(@num: 1) {
   display: -webkit-box;
   overflow: hidden;

+ 8 - 0
src/design/public.less

@@ -1,8 +1,16 @@
+#app {
+  width: 100%;
+  height: 100%;
+}
+
 .app-svg-loading {
   position: relative;
   width: auto;
 }
 
+// =================================
+// ==============scrollbar==========
+// =================================
 ::-webkit-scrollbar {
   width: 6px;
   height: 6px;

+ 0 - 103
src/design/reset.less

@@ -1,103 +0,0 @@
-@import 'var/link';
-@import './mixins/reset-text.less';
-@import 'color/index';
-.reset() {
-  html,
-  body {
-    width: 100%;
-    height: 100%;
-
-    &.color-weak {
-      filter: invert(80%);
-    }
-
-    &.gray-mode {
-      filter: grayscale(100%);
-      filter: progid:dximagetransform.microsoft.basicimage(grayscale=1);
-    }
-  }
-
-  input::-ms-clear,
-  input::-ms-reveal {
-    display: none;
-  }
-
-  body {
-    .reset-text();
-  }
-
-  h1,
-  h2,
-  h3,
-  h4,
-  h5,
-  h6 {
-    margin-top: 0;
-    margin-bottom: 0.5em;
-    font-weight: 500;
-    color: @heading-color;
-  }
-
-  ul,
-  ol {
-    list-style: none;
-  }
-
-  li {
-    list-style-type: none;
-  }
-
-  img {
-    vertical-align: top;
-    border: 0;
-  }
-
-  table {
-    border-collapse: collapse;
-    border-spacing: 0;
-  }
-
-  a:focus,
-  a:active {
-    outline: none;
-  }
-
-  i,
-  em {
-    font-style: normal;
-  }
-
-  div:focus {
-    outline: none;
-  }
-
-  a {
-    color: @link-color;
-    text-decoration: @link-decoration;
-    cursor: pointer;
-    background-color: transparent; // remove the gray background on active links in IE 10.
-    outline: none;
-    transition: color 0.3s;
-    -webkit-text-decoration-skip: objects; // remove gaps in links underline in iOS 8+ and Safari 8+.
-
-    &:hover {
-      color: @link-hover-color;
-    }
-
-    &:active {
-      color: @link-active-color;
-    }
-
-    &:active,
-    &:hover {
-      text-decoration: @link-hover-decoration;
-      outline: 0;
-    }
-
-    &[disabled] {
-      color: @disabled-color;
-      pointer-events: none;
-      cursor: not-allowed;
-    }
-  }
-}

+ 2 - 2
src/design/transition/base.less

@@ -1,11 +1,11 @@
 .transition-default() {
   &-enter-active,
   &-leave-active {
-    transition: 0.1s cubic-bezier(0.25, 0.8, 0.5, 1) !important;
+    transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1) !important;
   }
 
   &-move {
-    transition: transform 0.2s;
+    transition: transform 0.4s;
   }
 }
 

+ 1 - 1
src/design/transition/fade.less

@@ -11,7 +11,7 @@
 /* fade-slide */
 .fade-slide-leave-active,
 .fade-slide-enter-active {
-  transition: all 0.2s;
+  transition: all 0.3s;
 }
 
 .fade-slide-enter-from {

+ 1 - 0
src/design/var/index.less

@@ -13,6 +13,7 @@
 
 //
 @side-drag-z-index: 200;
+
 @page-loading-z-index: 10000;
 
 // left-menu

+ 11 - 1
src/hooks/setting/useLocaleSetting.ts

@@ -19,10 +19,20 @@ export function useLocaleSetting() {
   // get Fallback Locales
   const getFallbackLocale = computed((): string => unref(getLocale).fallback);
 
+  const getShowLocale = computed(() => unref(getLocale).show);
+
   // Set locale configuration
   function setLocale(locale: Partial<LocaleSetting>): void {
     appStore.commitProjectConfigState({ locale });
   }
 
-  return { getLocale, getLang, localeList, setLocale, getAvailableLocales, getFallbackLocale };
+  return {
+    getLocale,
+    getLang,
+    localeList,
+    setLocale,
+    getShowLocale,
+    getAvailableLocales,
+    getFallbackLocale,
+  };
 }

+ 21 - 0
src/hooks/web/useI18n.ts

@@ -0,0 +1,21 @@
+import { getI18n } from '/@/setup/i18n';
+
+export function useI18n(namespace?: string) {
+  const { t, ...methods } = getI18n().global;
+
+  function getKey(key: string) {
+    if (!namespace) {
+      return key;
+    }
+    if (key.startsWith(namespace)) {
+      return key;
+    }
+    return `${namespace}.${key}`;
+  }
+  return {
+    ...methods,
+    t: (key: string, ...arg: Parameters<typeof t>) => {
+      return t(getKey(key), ...arg);
+    },
+  };
+}

+ 0 - 7
src/hooks/web/useLocale.ts

@@ -65,10 +65,3 @@ export function useLocale() {
     antConfigLocale: antConfigLocaleRef,
   };
 }
-
-/**
- * For non-setup setting
- */
-export function useExternalI18n() {
-  return getI18n().global;
-}

+ 5 - 2
src/layouts/default/footer/index.tsx

@@ -8,18 +8,21 @@ import { GithubFilled } from '@ant-design/icons-vue';
 import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
 import { openWindow } from '/@/utils';
 
+import { useI18n } from '/@/hooks/web/useI18n';
+
 export default defineComponent({
   name: 'LayoutContent',
   setup() {
+    const { t } = useI18n('layout.footer');
     return () => {
       return (
         <Layout.Footer class="layout-footer">
           {() => (
             <>
               <div class="layout-footer__links">
-                <a onClick={() => openWindow(SITE_URL)}>在线预览</a>
+                <a onClick={() => openWindow(SITE_URL)}>{t('onlinePreview')}</a>
                 <GithubFilled onClick={() => openWindow(GITHUB_URL)} class="github" />
-                <a onClick={() => openWindow(DOC_URL)}>在线文档</a>
+                <a onClick={() => openWindow(DOC_URL)}>{t('onlineDocument')}</a>
               </div>
               <div>Copyright &copy;2020 Vben Admin</div>
             </>

+ 29 - 9
src/layouts/default/header/LayoutHeader.tsx

@@ -1,6 +1,7 @@
 import './index.less';
 
 import type { FunctionalComponent } from 'vue';
+import type { Component } from '/@/components/types';
 
 import { defineComponent, unref, computed, ref, nextTick } from 'vue';
 
@@ -27,6 +28,7 @@ import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
 import { useRootSetting } from '/@/hooks/setting/useRootSetting';
+import { useLocaleSetting } from '/@/hooks/setting/useLocaleSetting';
 
 import { useRouter } from 'vue-router';
 
@@ -34,7 +36,8 @@ import { errorStore } from '/@/store/modules/error';
 
 import { PageEnum } from '/@/enums/pageEnum';
 import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
-import { Component } from '/@/components/types';
+import { AppLocalePicker } from '/@/components/Application';
+import { useI18n } from '/@/hooks/web/useI18n';
 
 interface TooltipItemProps {
   title: string;
@@ -65,9 +68,11 @@ export default defineComponent({
     const logoWidthRef = ref(200);
     const logoRef = ref<ComponentRef>(null);
     const { refreshPage } = useTabs();
+    const { t } = useI18n('layout.header');
 
     const { getShowTopMenu, getShowHeaderTrigger, getSplit, getTopMenuAlign } = useMenuSetting();
 
+    const { getShowLocale } = useLocaleSetting();
     const { getUseErrorHandle, getShowBreadCrumbIcon } = useRootSetting();
 
     const {
@@ -160,8 +165,8 @@ export default defineComponent({
 
     function renderActionDefault(Comp: Component | any, event: Fn) {
       return (
-        <div class={`layout-header__action-item`} onClick={event}>
-          <Comp class={`layout-header__action-icon`} />
+        <div class="layout-header__action-item" onClick={event}>
+          <Comp class="layout-header__action-icon" />
         </div>
       );
     }
@@ -170,7 +175,7 @@ export default defineComponent({
       return (
         <div class={`layout-header__action`}>
           {unref(getUseErrorHandle) && (
-            <TooltipItem title="错误日志">
+            <TooltipItem title={t('layout.header.tooltipErrorLog')}>
               {() => (
                 <Badge
                   count={errorStore.getErrorListCountState}
@@ -185,23 +190,31 @@ export default defineComponent({
           )}
 
           {unref(getUseLockPage) && (
-            <TooltipItem title="锁定屏幕">
+            <TooltipItem title={t('layout.header.tooltipLock')}>
               {() => renderActionDefault(LockOutlined, handleLockPage)}
             </TooltipItem>
           )}
 
           {unref(getShowNotice) && (
-            <TooltipItem title="消息通知">{() => <NoticeAction />}</TooltipItem>
+            <TooltipItem title={t('layout.header.tooltipNotify')}>
+              {() => <NoticeAction />}
+            </TooltipItem>
           )}
 
           {unref(getShowRedo) && (
-            <TooltipItem title="刷新">
+            <TooltipItem title={t('layout.header.tooltipRedo')}>
               {() => renderActionDefault(RedoOutlined, refreshPage)}
             </TooltipItem>
           )}
 
           {unref(getShowFullScreen) && (
-            <TooltipItem title={unref(isFullscreenRef) ? '退出全屏' : '全屏'}>
+            <TooltipItem
+              title={
+                unref(isFullscreenRef)
+                  ? t('layout.header.tooltipExitFull')
+                  : t('layout.header.tooltipEntryFull')
+              }
+            >
               {() => {
                 const Icon = !unref(isFullscreenRef) ? (
                   <FullscreenOutlined />
@@ -212,7 +225,14 @@ export default defineComponent({
               }}
             </TooltipItem>
           )}
-          <UserDropdown class={`layout-header__user-dropdown`} />
+          <UserDropdown class="layout-header__user-dropdown" />
+          {unref(getShowLocale) && (
+            <AppLocalePicker
+              reload={true}
+              showText={false}
+              class="layout-header__action-item locale"
+            />
+          )}
         </div>
       );
     }

+ 12 - 5
src/layouts/default/header/UserDropdown.tsx

@@ -1,5 +1,5 @@
 // components
-import { Dropdown, Menu, Divider } from 'ant-design-vue';
+import { Dropdown, Menu } from 'ant-design-vue';
 
 import { defineComponent, computed, unref } from 'vue';
 
@@ -16,6 +16,7 @@ import { openWindow } from '/@/utils';
 
 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
 import { FunctionalComponent } from 'vue';
+import { useI18n } from '/@/hooks/web/useI18n';
 
 type MenuEvent = 'loginOut' | 'doc';
 interface MenuItemProps {
@@ -43,6 +44,7 @@ const MenuItem: FunctionalComponent<MenuItemProps> = (props) => {
 export default defineComponent({
   name: 'UserDropdown',
   setup() {
+    const { t } = useI18n('layout.header');
     const { getShowDoc } = useHeaderSetting();
 
     const getUserInfo = computed(() => {
@@ -89,9 +91,14 @@ export default defineComponent({
         <Menu onClick={handleMenuClick}>
           {() => (
             <>
-              {showDoc && <MenuItem key="doc" text="文档" icon="gg:loadbar-doc" />}
-              {showDoc && <Divider />}
-              <MenuItem key="loginOut" text="退出系统" icon="ant-design:poweroff-outlined" />
+              {showDoc && <MenuItem key="doc" text={t('dropdownItemDoc')} icon="gg:loadbar-doc" />}
+              {/* @ts-ignore */}
+              {showDoc && <Menu.Divider />}
+              <MenuItem
+                key="loginOut"
+                text={t('dropdownItemLoginOut')}
+                icon="ant-design:poweroff-outlined"
+              />
             </>
           )}
         </Menu>
@@ -100,7 +107,7 @@ export default defineComponent({
 
     return () => {
       return (
-        <Dropdown placement="bottomLeft">
+        <Dropdown placement="bottomLeft" overlayClassName="app-layout-header-user-dropdown-overlay">
           {{
             default: () => renderSlotsDefault(),
             overlay: () => renderSlotOverlay(),

+ 11 - 0
src/layouts/default/header/index.less

@@ -92,6 +92,11 @@
         &:hover {
           background: @header-light-bg-hover-color;
         }
+
+        &.locale {
+          padding: 0 10px;
+          color: rgba(0, 0, 0, 0.65);
+        }
       }
 
       &-icon {
@@ -221,3 +226,9 @@
     }
   }
 }
+
+.app-layout-header-user-dropdown-overlay {
+  .ant-dropdown-menu-item {
+    min-width: 160px;
+  }
+}

+ 2 - 1
src/layouts/default/index.less

@@ -2,9 +2,10 @@
 
 .default-layout {
   display: flex;
-  flex-direction: column;
   width: 100%;
   min-height: 100%;
+  background: @content-bg;
+  flex-direction: column;
 
   > .ant-layout {
     min-height: 100%;

+ 12 - 4
src/layouts/default/lock/LockAction.tsx

@@ -9,11 +9,13 @@ import headerImg from '/@/assets/images/header.jpg';
 
 import { appStore } from '/@/store/modules/app';
 import { userStore } from '/@/store/modules/user';
+import { useI18n } from '/@/hooks/web/useI18n';
 
 const prefixCls = 'lock-modal';
 export default defineComponent({
   name: 'LockModal',
   setup(_, { attrs }) {
+    const { t } = useI18n('layout.header');
     const [register, { closeModal }] = useModalInner();
 
     const [registerForm, { validateFields, resetFields }] = useForm({
@@ -21,7 +23,7 @@ export default defineComponent({
       schemas: [
         {
           field: 'password',
-          label: '锁屏密码',
+          label: t('lockScreenPassword'),
           component: 'InputPassword',
           required: true,
         },
@@ -49,7 +51,13 @@ export default defineComponent({
     }
 
     return () => (
-      <BasicModal footer={null} title="锁定屏幕" {...attrs} class={prefixCls} onRegister={register}>
+      <BasicModal
+        footer={null}
+        title={t('lockScreen')}
+        {...attrs}
+        class={prefixCls}
+        onRegister={register}
+      >
         {() => (
           <div class={`${prefixCls}__entry`}>
             <div class={`${prefixCls}__header`}>
@@ -61,10 +69,10 @@ export default defineComponent({
 
             <div class={`${prefixCls}__footer`}>
               <Button type="primary" block class="mt-2" onClick={lock}>
-                {() => '锁屏'}
+                {() => t('lockScreenBtn')}
               </Button>
               <Button block class="mt-2" onClick={lock.bind(null, false)}>
-                {() => ' 不设置密码锁屏'}
+                {() => t('notLockScreenPassword')}
               </Button>
             </div>
           </div>

+ 6 - 1
src/layouts/default/multitabs/TabContent.tsx

@@ -13,6 +13,7 @@ import { useTabDropdown } from './useTabDropdown';
 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
 import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
+import { useI18n } from '/@/hooks/web/useI18n';
 
 const ExtraContent: FunctionalComponent = () => {
   return (
@@ -56,6 +57,7 @@ export default defineComponent({
     },
   },
   setup(props) {
+    const { t } = useI18n('layout.multipleTab');
     const { getShowMenu } = useMenuSetting();
     const { getShowHeader } = useHeaderSetting();
     const { getShowQuick } = useMultipleTabSetting();
@@ -71,7 +73,10 @@ export default defineComponent({
     const { getDropMenuList, handleMenuEvent } = useTabDropdown(props as TabContentProps);
 
     return () => {
-      const scaleAction = getScaleAction(unref(getIsScale) ? '收起' : '展开', unref(getIsScale));
+      const scaleAction = getScaleAction(
+        unref(getIsScale) ? t('putAway') : t('unfold'),
+        unref(getIsScale)
+      );
       const dropMenuList = unref(getDropMenuList) || [];
 
       const isTab = unref(getIsTab);

+ 10 - 6
src/layouts/default/multitabs/data.ts

@@ -2,6 +2,10 @@ import { DropMenu } from '/@/components/Dropdown/index';
 import { AppRouteRecordRaw } from '/@/router/types';
 import type { TabItem } from '/@/store/modules/tab';
 
+import { useI18n } from '/@/hooks/web/useI18n';
+
+const { t } = useI18n('layout.multipleTab');
+
 export enum TabContentEnum {
   TAB_TYPE,
   EXTRA_TYPE,
@@ -37,40 +41,40 @@ export function getActions() {
   const REFRESH_PAGE: DropMenu = {
     icon: 'ant-design:reload-outlined',
     event: MenuEventEnum.REFRESH_PAGE,
-    text: '刷新',
+    text: t('redo'),
     disabled: false,
   };
   const CLOSE_CURRENT: DropMenu = {
     icon: 'ant-design:close-outlined',
     event: MenuEventEnum.CLOSE_CURRENT,
-    text: '关闭',
+    text: t('close'),
     disabled: false,
     divider: true,
   };
   const CLOSE_LEFT: DropMenu = {
     icon: 'ant-design:pic-left-outlined',
     event: MenuEventEnum.CLOSE_LEFT,
-    text: '关闭左侧',
+    text: t('closeLeft'),
     disabled: false,
     divider: false,
   };
   const CLOSE_RIGHT: DropMenu = {
     icon: 'ant-design:pic-right-outlined',
     event: MenuEventEnum.CLOSE_RIGHT,
-    text: '关闭右侧',
+    text: t('closeRight'),
     disabled: false,
     divider: true,
   };
   const CLOSE_OTHER: DropMenu = {
     icon: 'ant-design:pic-center-outlined',
     event: MenuEventEnum.CLOSE_OTHER,
-    text: '关闭其他',
+    text: t('closeOther'),
     disabled: false,
   };
   const CLOSE_ALL: DropMenu = {
     icon: 'ant-design:line-outlined',
     event: MenuEventEnum.CLOSE_ALL,
-    text: '关闭全部',
+    text: t('closeAll'),
     disabled: false,
   };
   return [REFRESH_PAGE, CLOSE_CURRENT, CLOSE_LEFT, CLOSE_RIGHT, CLOSE_OTHER, CLOSE_ALL];

+ 49 - 47
src/layouts/default/setting/SettingDrawer.tsx

@@ -18,6 +18,7 @@ import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
 import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
 import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
+import { useI18n } from '/@/hooks/web/useI18n';
 
 import { updateColorWeak, updateGrayMode } from '/@/setup/theme';
 
@@ -55,6 +56,7 @@ interface ThemePickerProps {
 }
 
 const { createSuccessModal, createMessage } = useMessage();
+const { t } = useI18n('layout.setting');
 
 /**
  * Menu type Picker comp
@@ -120,8 +122,8 @@ const FooterButton: FunctionalComponent = () => {
     const { isSuccessRef } = useCopyToClipboard(JSON.stringify(unref(getRootSetting), null, 2));
     unref(isSuccessRef) &&
       createSuccessModal({
-        title: '操作成功',
-        content: '复制成功,请到 src/settings/projectSetting.ts 中修改配置!',
+        title: t('operatingTitle'),
+        content: t('operatingContent'),
       });
   }
   function handleResetSetting() {
@@ -131,7 +133,7 @@ const FooterButton: FunctionalComponent = () => {
       // updateTheme(themeColor);
       updateColorWeak(colorWeak);
       updateGrayMode(grayMode);
-      createMessage.success('重置成功!');
+      createMessage.success(t('resetSuccess'));
     } catch (error) {
       createMessage.error(error);
     }
@@ -149,7 +151,7 @@ const FooterButton: FunctionalComponent = () => {
         {() => (
           <>
             <CopyOutlined class="mr-2" />
-            拷贝
+            {t('copyBtn')}
           </>
         )}
       </Button>
@@ -157,7 +159,7 @@ const FooterButton: FunctionalComponent = () => {
         {() => (
           <>
             <RedoOutlined class="mr-2" />
-            重置
+            {t('resetBtn')}
           </>
         )}
       </Button>
@@ -165,7 +167,7 @@ const FooterButton: FunctionalComponent = () => {
         {() => (
           <>
             <RedoOutlined class="mr-2" />
-            清空缓存并返回登录页
+            {t('clearBtn')}
           </>
         )}
       </Button>
@@ -224,7 +226,7 @@ export default defineComponent({
       return (
         <>
           <MenuTypePicker />
-          {renderSwitchItem('分割菜单', {
+          {renderSwitchItem(t('splitMenu'), {
             handler: (e) => {
               baseHandler(HandlerEnum.MENU_SPLIT, e);
             },
@@ -238,7 +240,7 @@ export default defineComponent({
     function renderTheme() {
       return (
         <>
-          <Divider>{() => '顶栏主题'}</Divider>
+          <Divider>{() => t('headerTheme')}</Divider>
           <ThemePicker
             colorList={HEADER_PRESET_BG_COLOR_LIST}
             def={unref(getHeaderBgColor)}
@@ -246,7 +248,7 @@ export default defineComponent({
               baseHandler(HandlerEnum.HEADER_THEME, e);
             }}
           />
-          <Divider>{() => '菜单主题'}</Divider>
+          <Divider>{() => t('sidebarTheme')}</Divider>
           <ThemePicker
             colorList={SIDE_BAR_BG_COLOR_LIST}
             def={unref(getMenuBgColor)}
@@ -263,56 +265,56 @@ export default defineComponent({
      */
     function renderFeatures() {
       return [
-        renderSwitchItem('侧边菜单拖拽', {
+        renderSwitchItem(t('menuDrag'), {
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_HAS_DRAG, e);
           },
           def: unref(getCanDrag),
           disabled: !unref(getShowMenuRef),
         }),
-        renderSwitchItem('侧边菜单搜索', {
+        renderSwitchItem(t('menuSearch'), {
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_SHOW_SEARCH, e);
           },
           def: unref(getShowSearch),
           disabled: !unref(getShowMenuRef),
         }),
-        renderSwitchItem('侧边菜单手风琴模式', {
+        renderSwitchItem(t('menuAccordion'), {
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_ACCORDION, e);
           },
           def: unref(getAccordion),
           disabled: !unref(getShowMenuRef),
         }),
-        renderSwitchItem('折叠菜单', {
+        renderSwitchItem(t('menuCollapse'), {
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_COLLAPSED, e);
           },
           def: unref(getCollapsed),
           disabled: !unref(getShowMenuRef),
         }),
-        renderSwitchItem('折叠菜单显示名称', {
+        renderSwitchItem(t('collapseMenuDisplayName'), {
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_COLLAPSED_SHOW_TITLE, e);
           },
           def: unref(getCollapsedShowTitle),
           disabled: !unref(getShowMenuRef) || !unref(getCollapsed),
         }),
-        renderSwitchItem('固定header', {
+        renderSwitchItem(t('fixedHeader'), {
           handler: (e) => {
             baseHandler(HandlerEnum.HEADER_FIXED, e);
           },
           def: unref(getHeaderFixed),
           disabled: !unref(getShowHeader),
         }),
-        renderSwitchItem('固定Siderbar', {
+        renderSwitchItem(t('fixedSideBar'), {
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_FIXED, e);
           },
           def: unref(getMenuFixed),
           disabled: !unref(getShowMenuRef),
         }),
-        renderSelectItem('顶部菜单布局', {
+        renderSelectItem(t('topMenuLayout'), {
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_TOP_ALIGN, e);
           },
@@ -320,7 +322,7 @@ export default defineComponent({
           options: topMenuAlignOptions,
           disabled: !unref(getShowHeader) || (!unref(getIsTopMenu) && !unref(getSplit)),
         }),
-        renderSelectItem('菜单折叠按钮', {
+        renderSelectItem(t('menuCollapseButton'), {
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_TRIGGER, e);
           },
@@ -329,7 +331,7 @@ export default defineComponent({
           options: menuTriggerOptions,
         }),
 
-        renderSelectItem('内容区域宽度', {
+        renderSelectItem(t('contentMode'), {
           handler: (e) => {
             baseHandler(HandlerEnum.CONTENT_MODE, e);
           },
@@ -337,9 +339,9 @@ export default defineComponent({
           options: contentModeOptions,
         }),
         <div class={`setting-drawer__cell-item`}>
-          <span>自动锁屏</span>
+          <span>{t('autoScreenLock')}</span>
           <InputNumber
-            style="width:120px"
+            style="width:126px"
             size="small"
             min={0}
             onChange={(e: any) => {
@@ -348,16 +350,16 @@ export default defineComponent({
             defaultValue={appStore.getProjectConfig.lockTime}
             formatter={(value: string) => {
               if (parseInt(value) === 0) {
-                return '0(不自动锁屏)';
+                return `0(${t('notAutoScreenLock')})`;
               }
-              return `${value}分钟`;
+              return `${value}${t('minute')}`;
             }}
           />
         </div>,
         <div class={`setting-drawer__cell-item`}>
-          <span>菜单展开宽度</span>
+          <span>{t('expandedMenuWidth')}</span>
           <InputNumber
-            style="width:120px"
+            style="width:126px"
             size="small"
             max={600}
             min={100}
@@ -375,27 +377,27 @@ export default defineComponent({
 
     function renderContent() {
       return [
-        renderSwitchItem('面包屑', {
+        renderSwitchItem(t('breadcrumb'), {
           handler: (e) => {
             baseHandler(HandlerEnum.SHOW_BREADCRUMB, e);
           },
           def: unref(getShowBreadCrumb),
           disabled: !unref(getShowHeader),
         }),
-        renderSwitchItem('面包屑图标', {
+        renderSwitchItem(t('breadcrumbIcon'), {
           handler: (e) => {
             baseHandler(HandlerEnum.SHOW_BREADCRUMB_ICON, e);
           },
           def: unref(getShowBreadCrumbIcon),
           disabled: !unref(getShowHeader),
         }),
-        renderSwitchItem('标签页', {
+        renderSwitchItem(t('tabs'), {
           handler: (e) => {
             baseHandler(HandlerEnum.TABS_SHOW, e);
           },
           def: unref(getShowMultipleTab),
         }),
-        renderSwitchItem('标签页快捷按钮', {
+        renderSwitchItem(t('tabsQuickBtn'), {
           handler: (e) => {
             baseHandler(HandlerEnum.TABS_SHOW_QUICK, e);
           },
@@ -403,14 +405,14 @@ export default defineComponent({
           disabled: !unref(getShowMultipleTab),
         }),
 
-        renderSwitchItem('左侧菜单', {
+        renderSwitchItem(t('sidebar'), {
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_SHOW_SIDEBAR, e);
           },
           def: unref(getShowMenu),
           disabled: unref(getIsHorizontal),
         }),
-        renderSwitchItem('顶栏', {
+        renderSwitchItem(t('header'), {
           handler: (e) => {
             baseHandler(HandlerEnum.HEADER_SHOW, e);
           },
@@ -422,25 +424,25 @@ export default defineComponent({
           },
           def: unref(getShowLogo),
         }),
-        renderSwitchItem('页脚', {
+        renderSwitchItem(t('footer'), {
           handler: (e) => {
             baseHandler(HandlerEnum.SHOW_FOOTER, e);
           },
           def: unref(getShowFooter),
         }),
-        renderSwitchItem('全屏内容', {
+        renderSwitchItem(t('fullContent'), {
           handler: (e) => {
             baseHandler(HandlerEnum.FULL_CONTENT, e);
           },
           def: unref(getFullContent),
         }),
-        renderSwitchItem('灰色模式', {
+        renderSwitchItem(t('grayMode'), {
           handler: (e) => {
             baseHandler(HandlerEnum.GRAY_MODE, e);
           },
           def: unref(getGrayMode),
         }),
-        renderSwitchItem('色弱模式', {
+        renderSwitchItem(t('colorWeak'), {
           handler: (e) => {
             baseHandler(HandlerEnum.COLOR_WEAK, e);
           },
@@ -452,13 +454,13 @@ export default defineComponent({
     function renderTransition() {
       return (
         <>
-          {renderSwitchItem('顶部进度条', {
+          {renderSwitchItem(t('progress'), {
             handler: (e) => {
               baseHandler(HandlerEnum.OPEN_PROGRESS, e);
             },
             def: unref(getOpenNProgress),
           })}
-          {renderSwitchItem('切换loading', {
+          {renderSwitchItem(t('switchLoading'), {
             handler: (e) => {
               baseHandler(HandlerEnum.OPEN_PAGE_LOADING, e);
             },
@@ -466,14 +468,14 @@ export default defineComponent({
             disabled: !unref(getEnableTransition),
           })}
 
-          {renderSwitchItem('切换动画', {
+          {renderSwitchItem(t('switchAnimation'), {
             handler: (e) => {
               baseHandler(HandlerEnum.OPEN_ROUTE_TRANSITION, e);
             },
             def: unref(getEnableTransition),
           })}
 
-          {renderSelectItem('动画类型', {
+          {renderSelectItem(t('animationType'), {
             handler: (e) => {
               baseHandler(HandlerEnum.ROUTER_TRANSITION, e);
             },
@@ -495,7 +497,7 @@ export default defineComponent({
             {...opt}
             disabled={disabled}
             size="small"
-            style={{ width: '120px' }}
+            style={{ width: '126px' }}
             onChange={(e) => {
               handler && handler(e);
             }}
@@ -517,26 +519,26 @@ export default defineComponent({
             onChange={(e: any) => {
               handler && handler(e);
             }}
-            checkedChildren="开"
-            unCheckedChildren="关"
+            checkedChildren={t('on')}
+            unCheckedChildren={t('off')}
           />
         </div>
       );
     }
 
     return () => (
-      <BasicDrawer {...attrs} title="项目配置" width={300} wrapClassName="setting-drawer">
+      <BasicDrawer {...attrs} title={t('drawerTitle')} width={330} wrapClassName="setting-drawer">
         {{
           default: () => (
             <>
-              <Divider>{() => '导航栏模式'}</Divider>
+              <Divider>{() => t('navMode')}</Divider>
               {renderSidebar()}
               {renderTheme()}
-              <Divider>{() => '界面功能'}</Divider>
+              <Divider>{() => t('interfaceFunction')}</Divider>
               {renderFeatures()}
-              <Divider>{() => '界面显示'}</Divider>
+              <Divider>{() => t('interfaceDisplay')}</Divider>
               {renderContent()}
-              <Divider>{() => '切换动画'}</Divider>
+              <Divider>{() => t('animation')}</Divider>
               {renderTransition()}
               <Divider />
               <FooterButton />

+ 15 - 23
src/layouts/default/setting/enum.ts

@@ -1,9 +1,12 @@
-import { ContentEnum, RouterTransitionEnum, ThemeEnum } from '/@/enums/appEnum';
+import { ContentEnum, RouterTransitionEnum } from '/@/enums/appEnum';
 import { MenuModeEnum, MenuTypeEnum, TopMenuAlignEnum, TriggerEnum } from '/@/enums/menuEnum';
 
 import mixImg from '/@/assets/images/layout/menu-mix.svg';
 import sidebarImg from '/@/assets/images/layout/menu-sidebar.svg';
 import menuTopImg from '/@/assets/images/layout/menu-top.svg';
+import { useI18n } from '/@/hooks/web/useI18n';
+
+const { t } = useI18n('layout.setting');
 
 export enum HandlerEnum {
   CHANGE_LAYOUT,
@@ -45,55 +48,44 @@ export enum HandlerEnum {
   OPEN_ROUTE_TRANSITION,
 }
 
-export const themeOptions = [
-  {
-    value: ThemeEnum.LIGHT,
-    label: '亮色',
-  },
-  {
-    value: ThemeEnum.DARK,
-    label: '暗色',
-  },
-];
-
 export const contentModeOptions = [
   {
     value: ContentEnum.FULL,
-    label: '流式',
+    label: t('contentModeFull'),
   },
   {
     value: ContentEnum.FIXED,
-    label: '定宽',
+    label: t('contentModeFixed'),
   },
 ];
 
 export const topMenuAlignOptions = [
   {
     value: TopMenuAlignEnum.CENTER,
-    label: '居中',
+    label: t('topMenuAlignRight'),
   },
   {
     value: TopMenuAlignEnum.START,
-    label: '居左',
+    label: t('topMenuAlignLeft'),
   },
   {
     value: TopMenuAlignEnum.END,
-    label: '居右',
+    label: t('topMenuAlignCenter'),
   },
 ];
 
 export const menuTriggerOptions = [
   {
     value: TriggerEnum.NONE,
-    label: '不显示',
+    label: t('menuTriggerNone'),
   },
   {
     value: TriggerEnum.FOOTER,
-    label: '底部',
+    label: t('menuTriggerBottom'),
   },
   {
     value: TriggerEnum.HEADER,
-    label: '顶部',
+    label: t('menuTriggerTop'),
   },
 ];
 
@@ -112,20 +104,20 @@ export const routerTransitionOptions = [
 
 export const menuTypeList = [
   {
-    title: '左侧菜单模式',
+    title: t('menuTypeSidebar'),
     mode: MenuModeEnum.INLINE,
     type: MenuTypeEnum.SIDEBAR,
     src: sidebarImg,
   },
   {
-    title: '混合模式',
+    title: t('menuTypeMix'),
     mode: MenuModeEnum.INLINE,
     type: MenuTypeEnum.MIX,
     src: mixImg,
   },
 
   {
-    title: '顶部菜单模式',
+    title: t('menuTypeTopMenu'),
     mode: MenuModeEnum.HORIZONTAL,
     type: MenuTypeEnum.TOP_MENU,
     src: menuTopImg,

+ 14 - 12
src/layouts/default/sider/index.tsx

@@ -1,6 +1,6 @@
 import './index.less';
 
-import { computed, defineComponent, ref, unref, watch, nextTick } from 'vue';
+import { computed, defineComponent, ref, unref, watch, nextTick, CSSProperties } from 'vue';
 
 import { Layout } from 'ant-design-vue';
 import LayoutMenu from '../menu';
@@ -91,17 +91,19 @@ export default defineComponent({
       }
     );
 
-    const getHiddenDomStyle = computed(() => {
-      const width = `${unref(getRealWidth)}px`;
-      return {
-        width: width,
-        overflow: 'hidden',
-        flex: `0 0 ${width}`,
-        'max-width': width,
-        'min-width': width,
-        transition: 'all 0.2s',
-      };
-    });
+    const getHiddenDomStyle = computed(
+      (): CSSProperties => {
+        const width = `${unref(getRealWidth)}px`;
+        return {
+          width: width,
+          overflow: 'hidden',
+          flex: `0 0 ${width}`,
+          maxWidth: width,
+          minWidth: width,
+          transition: 'all 0.2s',
+        };
+      }
+    );
 
     function renderDefault() {
       return (

+ 4 - 0
src/locales/lang/en/layout/footer.ts

@@ -0,0 +1,4 @@
+export default {
+  onlinePreview: 'Preview',
+  onlineDocument: 'Document',
+};

+ 18 - 0
src/locales/lang/en/layout/header.ts

@@ -0,0 +1,18 @@
+export default {
+  // user dropdown
+  dropdownItemDoc: 'Document',
+  dropdownItemLoginOut: 'Login Out',
+
+  tooltipErrorLog: 'Error log',
+  tooltipLock: 'Lock screen',
+  tooltipNotify: 'Notification',
+  tooltipRedo: 'Refresh',
+  tooltipEntryFull: 'Full Screen',
+  tooltipExitFull: 'Exit Full Screen',
+
+  // lock
+  lockScreenPassword: 'Lock screen password',
+  lockScreen: 'Lock screen',
+  lockScreenBtn: 'Locking',
+  notLockScreenPassword: 'No password lock screen',
+};

+ 10 - 0
src/locales/lang/en/layout/multipleTab.ts

@@ -0,0 +1,10 @@
+export default {
+  redo: 'Refresh',
+  close: 'Close',
+  closeLeft: 'Close Left',
+  closeRight: 'Close Right',
+  closeOther: 'Close Other',
+  closeAll: 'Close All',
+  putAway: 'PutAway',
+  unfold: 'Unfold',
+};

+ 73 - 0
src/locales/lang/en/layout/setting.ts

@@ -0,0 +1,73 @@
+export default {
+  // content mode
+  contentModeFull: 'Full',
+  contentModeFixed: 'Fixed width',
+  // topMenu align
+  topMenuAlignLeft: 'Left',
+  topMenuAlignRight: 'Center',
+  topMenuAlignCenter: 'Right',
+  // menu trigger
+  menuTriggerNone: 'Not Show',
+  menuTriggerBottom: 'Bottom',
+  menuTriggerTop: 'Top',
+  // menu type
+  menuTypeSidebar: 'Left menu mode',
+  menuTypeMix: 'Mixed mode',
+  menuTypeTopMenu: 'Top menu mode',
+
+  on: 'On',
+  off: 'Off',
+  minute: 'Minute',
+
+  operatingTitle: 'Successful!',
+  operatingContent:
+    'The copy is successful, please go to src/settings/projectSetting.ts to modify the configuration!',
+  resetSuccess: 'Successfully reset!',
+
+  copyBtn: 'Copy',
+  resetBtn: 'Reset',
+  clearBtn: 'Clear cache and to the login page',
+
+  drawerTitle: 'Configuration',
+
+  navMode: 'Navigation mode',
+  interfaceFunction: 'Interface function',
+  interfaceDisplay: 'Interface display',
+  animation: 'Animation',
+  splitMenu: 'Split menu',
+
+  headerTheme: 'Header theme',
+  sidebarTheme: 'Menu theme',
+
+  menuDrag: 'Drag Sidebar',
+  menuSearch: 'Sidebar search',
+  menuAccordion: 'Sidebar accordion',
+  menuCollapse: 'Collapse menu',
+  collapseMenuDisplayName: 'Collapse menu display name',
+  topMenuLayout: 'Top menu layout',
+  menuCollapseButton: 'Menu collapse button',
+  contentMode: 'Content area width',
+  expandedMenuWidth: 'Expanded menu width',
+
+  breadcrumb: 'Breadcrumbs',
+  breadcrumbIcon: 'Breadcrumbs Icon',
+  tabs: 'Tabs',
+  tabsQuickBtn: 'Tabs quick button',
+  sidebar: 'Sidebar',
+  header: 'Header',
+  footer: 'Footer',
+  fullContent: 'Full content',
+  grayMode: 'Gray mode',
+  colorWeak: 'Color Weak Mode',
+
+  progress: 'Progress',
+  switchLoading: 'Switch Loading',
+  switchAnimation: 'Switch animation',
+  animationType: 'Animation type',
+
+  autoScreenLock: 'Auto screen lock',
+  notAutoScreenLock: 'Not auto lock',
+
+  fixedHeader: 'Fixed header',
+  fixedSideBar: 'Fixed Sidebar',
+};

+ 4 - 0
src/locales/lang/zh_CN/layout/footer.ts

@@ -0,0 +1,4 @@
+export default {
+  onlinePreview: '在线预览',
+  onlineDocument: '在线文档',
+};

+ 19 - 0
src/locales/lang/zh_CN/layout/header.ts

@@ -0,0 +1,19 @@
+export default {
+  // user dropdown
+  dropdownItemDoc: '文档',
+  dropdownItemLoginOut: '退出系统',
+
+  // tooltip
+  tooltipErrorLog: '错误日志',
+  tooltipLock: '锁定屏幕',
+  tooltipNotify: '消息通知',
+  tooltipRedo: '刷新',
+  tooltipEntryFull: '全屏',
+  tooltipExitFull: '退出全屏',
+
+  // lock
+  lockScreenPassword: '锁屏密码',
+  lockScreen: '锁定屏幕',
+  lockScreenBtn: '锁定',
+  notLockScreenPassword: '不设置密码锁屏',
+};

+ 10 - 0
src/locales/lang/zh_CN/layout/multipleTab.ts

@@ -0,0 +1,10 @@
+export default {
+  redo: '刷新',
+  close: '关闭',
+  closeLeft: '关闭左侧',
+  closeRight: '关闭右侧',
+  closeOther: '关闭其他',
+  closeAll: '关闭全部',
+  putAway: '收起',
+  unfold: '展开',
+};

+ 72 - 0
src/locales/lang/zh_CN/layout/setting.ts

@@ -0,0 +1,72 @@
+export default {
+  // content mode
+  contentModeFull: '流式',
+  contentModeFixed: '定宽',
+  // topMenu align
+  topMenuAlignLeft: '居左',
+  topMenuAlignRight: '居中',
+  topMenuAlignCenter: '居右',
+  // menu trigger
+  menuTriggerNone: '不显示',
+  menuTriggerBottom: '底部',
+  menuTriggerTop: '顶部',
+  // menu type
+  menuTypeSidebar: '左侧菜单模式',
+  menuTypeMix: '混合模式',
+  menuTypeTopMenu: '顶部菜单模式',
+
+  on: '开',
+  off: '关',
+  minute: '分钟',
+
+  operatingTitle: '操作成功',
+  operatingContent: '复制成功,请到 src/settings/projectSetting.ts 中修改配置!',
+  resetSuccess: '重置成功!',
+
+  copyBtn: '拷贝',
+  resetBtn: '重置',
+  clearBtn: '清空缓存并返回登录页',
+
+  drawerTitle: '项目配置',
+
+  navMode: '导航栏模式',
+  interfaceFunction: '界面功能',
+  interfaceDisplay: '界面显示',
+  animation: '动画',
+  splitMenu: '分割菜单',
+
+  headerTheme: '顶栏主题',
+  sidebarTheme: '菜单主题',
+
+  menuDrag: '侧边菜单拖拽',
+  menuSearch: '侧边菜单搜索',
+  menuAccordion: '侧边菜单手风琴模式',
+  menuCollapse: '折叠菜单',
+  collapseMenuDisplayName: '折叠菜单显示名称',
+  topMenuLayout: '顶部菜单布局',
+  menuCollapseButton: '菜单折叠按钮',
+  contentMode: '内容区域宽度',
+  expandedMenuWidth: '菜单展开宽度',
+
+  breadcrumb: '面包屑',
+  breadcrumbIcon: '面包屑图标',
+  tabs: '标签页',
+  tabsQuickBtn: '标签页快捷按钮',
+  sidebar: '左侧菜单',
+  header: '顶栏',
+  footer: '页脚',
+  fullContent: '全屏内容',
+  grayMode: '灰色模式',
+  colorWeak: '色弱模式',
+
+  progress: '顶部进度条',
+  switchLoading: '切换loading',
+  switchAnimation: '切换动画',
+  animationType: '动画类型',
+
+  autoScreenLock: '自动锁屏',
+  notAutoScreenLock: '不自动锁屏',
+
+  fixedHeader: '固定header',
+  fixedSideBar: '固定Sidebar',
+};

+ 1 - 0
src/settings/projectSetting.ts

@@ -41,6 +41,7 @@ const setting: ProjectConfig = {
 
   // locale setting
   locale: {
+    show: true,
     // Locale
     lang: 'zh_CN',
     // Default locale

+ 1 - 0
src/types/config.d.ts

@@ -51,6 +51,7 @@ export interface HeaderSetting {
 }
 
 export interface LocaleSetting {
+  show: boolean;
   // Current language
   lang: LocaleType;
   // default language

+ 3 - 3
src/views/sys/error-log/DetailModal.vue

@@ -1,15 +1,15 @@
 <template>
-  <BasicModal :width="800" :title="t('sys.errorLog.tableActionDesc')" v-bind="$attrs">
+  <BasicModal :width="800" :title="t('tableActionDesc')" v-bind="$attrs">
     <Description :data="info" @register="register" />
   </BasicModal>
 </template>
 <script lang="ts">
   import { defineComponent, PropType } from 'vue';
-  import { useI18n } from 'vue-i18n';
 
   import { BasicModal } from '/@/components/Modal/index';
   import { ErrorInfo } from '/@/store/modules/error';
   import { Description, useDescription } from '/@/components/Description/index';
+  import { useI18n } from '/@/hooks/web/useI18n';
 
   import { getDescSchema } from './data';
 
@@ -23,7 +23,7 @@
       },
     },
     setup() {
-      const { t } = useI18n();
+      const { t } = useI18n('sys.errorLog');
       const [register] = useDescription({
         column: 2,
         schema: getDescSchema(),

+ 7 - 8
src/views/sys/error-log/data.tsx

@@ -1,16 +1,15 @@
 import { Tag } from 'ant-design-vue';
 import { BasicColumn } from '/@/components/Table/index';
 import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
+import { useI18n } from '/@/hooks/web/useI18n';
 
-import { useExternalI18n } from '/@/hooks/web/useLocale';
-
-const { t } = useExternalI18n();
+const { t } = useI18n('sys.errorLog');
 
 export function getColumns(): BasicColumn[] {
   return [
     {
       dataIndex: 'type',
-      title: t('sys.errorLog.tableColumnType'),
+      title: t('tableColumnType'),
       width: 80,
       customRender: ({ text }) => {
         const color =
@@ -33,12 +32,12 @@ export function getColumns(): BasicColumn[] {
     },
     {
       dataIndex: 'time',
-      title: t('sys.errorLog.tableColumnDate'),
+      title: t('tableColumnDate'),
       width: 160,
     },
     {
       dataIndex: 'file',
-      title: t('sys.errorLog.tableColumnFile'),
+      title: t('tableColumnFile'),
       width: 200,
     },
     {
@@ -48,12 +47,12 @@ export function getColumns(): BasicColumn[] {
     },
     {
       dataIndex: 'message',
-      title: t('sys.errorLog.tableColumnMsg'),
+      title: t('tableColumnMsg'),
       width: 300,
     },
     {
       dataIndex: 'stack',
-      title: t('sys.errorLog.tableColumnStackMsg'),
+      title: t('tableColumnStackMsg'),
       width: 300,
     },
   ];

+ 3 - 3
src/views/sys/error-log/index.vue

@@ -35,7 +35,7 @@
 
   import { useModal } from '/@/components/Modal/index';
   import { useMessage } from '/@/hooks/web/useMessage';
-  import { useI18n } from 'vue-i18n';
+  import { useI18n } from '/@/hooks/web/useI18n';
 
   import { errorStore, ErrorInfo } from '/@/store/modules/error';
 
@@ -53,7 +53,7 @@
       const rowInfoRef = ref<ErrorInfo>();
       const imgListRef = ref<string[]>([]);
 
-      const { t } = useI18n();
+      const { t } = useI18n('sys.errorLog');
 
       const [register, { setTableData }] = useTable({
         title: t('sys.errorLog.tableTitle'),
@@ -80,7 +80,7 @@
       );
       const { createMessage } = useMessage();
       if (isDevMode()) {
-        createMessage.info(t('sys.errorLog.enableMessage'));
+        createMessage.info(t('enableMessage'));
       }
       // 查看详情
       function handleDetail(row: ErrorInfo) {

+ 13 - 12
src/views/sys/exception/Exception.tsx

@@ -1,3 +1,5 @@
+import './exception.less';
+
 import type { PropType } from 'vue';
 
 import { Result, Button } from 'ant-design-vue';
@@ -12,9 +14,8 @@ import { useRoute } from 'vue-router';
 
 import { useGo, useRedo } from '/@/hooks/web/usePage';
 import { PageEnum } from '/@/enums/pageEnum';
-import { useI18n } from 'vue-i18n';
+import { useI18n } from '/@/hooks/web/useI18n';
 
-import './exception.less';
 interface MapValue {
   title: string;
   subTitle: string;
@@ -52,7 +53,7 @@ export default defineComponent({
     const { query } = useRoute();
     const go = useGo();
     const redo = useRedo();
-    const { t } = useI18n();
+    const { t } = useI18n('sys.exception');
 
     const getStatus = computed(() => {
       const { status: routeStatus } = query;
@@ -66,13 +67,13 @@ export default defineComponent({
       }
     );
 
-    const backLoginI18n = t('sys.exception.backLogin');
-    const backHomeI18n = t('sys.exception.backHome');
+    const backLoginI18n = t('backLogin');
+    const backHomeI18n = t('backHome');
 
     unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_ACCESS, {
       title: '403',
       status: `${ExceptionEnum.PAGE_NOT_ACCESS}`,
-      subTitle: t('sys.exception.subTitle403'),
+      subTitle: t('subTitle403'),
       btnText: props.full ? backLoginI18n : backHomeI18n,
       handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()),
     });
@@ -80,7 +81,7 @@ export default defineComponent({
     unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_FOUND, {
       title: '404',
       status: `${ExceptionEnum.PAGE_NOT_FOUND}`,
-      subTitle: t('sys.exception.subTitle404'),
+      subTitle: t('subTitle404'),
       btnText: props.full ? backLoginI18n : backHomeI18n,
       handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()),
     });
@@ -88,22 +89,22 @@ export default defineComponent({
     unref(statusMapRef).set(ExceptionEnum.ERROR, {
       title: '500',
       status: `${ExceptionEnum.ERROR}`,
-      subTitle: t('sys.exception.subTitle500'),
+      subTitle: t('subTitle500'),
       btnText: backHomeI18n,
       handler: () => go(),
     });
 
     unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_DATA, {
-      title: t('sys.exception.noDataTitle'),
+      title: t('noDataTitle'),
       subTitle: '',
-      btnText: t('sys.exception.redo'),
+      btnText: t('redo'),
       handler: () => redo(),
       icon: notDataImg,
     });
 
     unref(statusMapRef).set(ExceptionEnum.NET_WORK_ERROR, {
-      title: t('sys.exception.networkErrorTitle'),
-      subTitle: t('sys.exception.networkErrorSubTitle'),
+      title: t('networkErrorTitle'),
+      subTitle: t('networkErrorSubTitle'),
       btnText: 'Refresh',
       handler: () => redo(),
       icon: netWorkImg,

+ 4 - 5
src/views/sys/lock/index.vue

@@ -6,7 +6,7 @@
         <p class="lock-page__header-name">{{ realName }}</p>
       </div>
       <BasicForm @register="register" v-if="!getIsNotPwd" />
-      <Alert v-if="errMsgRef" type="error" :message="t('sys.lock.alert')" banner />
+      <Alert v-if="errMsgRef" type="error" :message="t('alert')" banner />
       <div class="lock-page__footer">
         <a-button type="default" class="mt-2 mr-2" @click="goLogin" v-if="!getIsNotPwd">
           {{ t('sys.lock.backToLogin') }}
@@ -26,8 +26,7 @@
 
   import { userStore } from '/@/store/modules/user';
   import { appStore } from '/@/store/modules/app';
-
-  import { useI18n } from 'vue-i18n';
+  import { useI18n } from '/@/hooks/web/useI18n';
 
   export default defineComponent({
     name: 'LockPage',
@@ -37,7 +36,7 @@
       const loadingRef = ref(false);
       const errMsgRef = ref(false);
 
-      const { t } = useI18n();
+      const { t } = useI18n('sys.lock');
       const [register, { validateFields }] = useForm({
         showActionButtonGroup: false,
         schemas: [
@@ -47,7 +46,7 @@
             component: 'InputPassword',
             componentProps: {
               style: { width: '100%' },
-              placeholder: t('sys.lock.placeholder'),
+              placeholder: t('placeholder'),
             },
             rules: [{ required: true }],
           },

+ 12 - 12
src/views/sys/login/Login.vue

@@ -4,7 +4,7 @@
     <div class="login-form-wrap">
       <div class="login-form mx-6">
         <div class="login-form__content px-2 py-10">
-          <AppLocalPicker class="login-form__locale" />
+          <AppLocalePicker v-if="showLocale" class="login-form__locale" />
           <header>
             <img :src="logo" class="mr-4" />
             <h1>{{ title }}</h1>
@@ -64,23 +64,23 @@
   import { Checkbox } from 'ant-design-vue';
 
   import Button from '/@/components/Button/index.vue';
-  import { AppLocalPicker } from '/@/components/Application';
+  import { AppLocalePicker } from '/@/components/Application';
   // import { BasicDragVerify, DragVerifyActionType } from '/@/components/Verify/index';
 
   import { userStore } from '/@/store/modules/user';
-  import { useI18n } from 'vue-i18n';
 
   // import { appStore } from '/@/store/modules/app';
   import { useMessage } from '/@/hooks/web/useMessage';
-  import { useGlobSetting } from '/@/hooks/setting';
+  import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
   import logo from '/@/assets/images/logo.png';
+  import { useI18n } from '/@/hooks/web/useI18n';
 
   export default defineComponent({
     components: {
       //  BasicDragVerify,
       AButton: Button,
       ACheckbox: Checkbox,
-      AppLocalPicker,
+      AppLocalePicker,
     },
     setup() {
       const formRef = ref<any>(null);
@@ -88,8 +88,9 @@
       // const verifyRef = ref<RefInstanceType<DragVerifyActionType>>(null);
 
       const globSetting = useGlobSetting();
+      const { locale } = useProjectSetting();
       const { notification } = useMessage();
-      const { t } = useI18n();
+      const { t } = useI18n('sys.login');
 
       // const openLoginVerifyRef = computed(() => appStore.getProjectConfig.openLoginVerify);
 
@@ -103,10 +104,8 @@
       });
 
       const formRules = reactive({
-        account: [{ required: true, message: t('sys.login.accountPlaceholder'), trigger: 'blur' }],
-        password: [
-          { required: true, message: t('sys.login.passwordPlaceholder'), trigger: 'blur' },
-        ],
+        account: [{ required: true, message: t('accountPlaceholder'), trigger: 'blur' }],
+        password: [{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }],
         // verify: unref(openLoginVerifyRef) ? [{ required: true, message: '请通过验证码校验' }] : [],
       });
 
@@ -131,8 +130,8 @@
           );
           if (userInfo) {
             notification.success({
-              message: t('sys.login.loginSuccessTitle'),
-              description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
+              message: t('loginSuccessTitle'),
+              description: `${t('loginSuccessDesc')}: ${userInfo.realName}`,
               duration: 3,
             });
           }
@@ -154,6 +153,7 @@
         title: globSetting && globSetting.title,
         logo,
         t,
+        showLocale: locale.show,
       };
     },
   });