Ver código fonte

feat: 实现多语言功能

Jack Zhou 1 dia atrás
pai
commit
6a1511ed06

+ 8 - 2
src/components/VBreadcrumb/src/VBreadcrumb.vue

@@ -10,6 +10,12 @@ const themeStore = useThemeStore()
 const { t } = useI18n()
 const CancelRulesVisible = ref(false)
 
+const resolveBreadName = (routeItem: any) => {
+  if (routeItem?.breadKey) return t(routeItem.breadKey)
+  if (routeItem?.breadName) return routeItem.breadName
+  return routeItem?.label || ''
+}
+
 const handleGoBack = () => {
   const routeData = breadCrumb.getUpperRoute()
   router.push({
@@ -59,11 +65,11 @@ const jumpLinkMonitoring = () => {
     <template v-for="(routeItem, index) in breadCrumb.routeList" :key="routeItem.label">
       <template v-if="index + 1 !== breadCrumb.routeList.length">
         <span @click="jumpLink(routeItem.label, routeItem.query)" class="previous-route">{{
-          routeItem.breadName
+          resolveBreadName(routeItem)
         }}</span>
         <span class="interval">|</span>
       </template>
-      <span v-else>{{ routeItem.breadName }}</span>
+      <span v-else>{{ resolveBreadName(routeItem) }}</span>
     </template>
   </div>
   <div v-else></div>

+ 5 - 2
src/locales/en.json

@@ -1046,7 +1046,7 @@
     "transportMode": "Transport Mode",
     "shipmentStatus": "Shipment Status",
     "bookingListEmptyTipLine1": "We support the following references number to find bookings: Booking No./HAWB No./MAWB No./PO No./Carrier Booking No./Contract No./File No./Quote No.",
-    "bookingListEmptyTipLine2": "",
+    
     "etd": "ETD",
     "textSearch": "text search",
     "bookingNo": "Booking No.",
@@ -1148,7 +1148,10 @@
     "time": "Time",
     "places": "Places",
     "transportation": "Transportation",
-    "others": "Others"
+    "others": "Others",
+
+
+    "bookingListEmptyTipLine2": ""
   },
   "destinationDelivery": {
     "title": "Destination Delivery",

+ 57 - 2
src/locales/index.ts

@@ -14,6 +14,48 @@ const messages: any = {
 }
 messages.en_US.vxe.table.seqTitle = 'seq'
 
+type CachedLocalePayload = {
+  fetchedAt: number
+  messages: Record<string, any>
+}
+
+const I18N_CACHE_PREFIX = 'i18n_cache_v1'
+// 默认 6 小时内刷新页面不重复拉取“语言文件”
+const DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1000
+
+const getCacheKey = (locale: string) => `${I18N_CACHE_PREFIX}:${locale}`
+
+const safeJsonParse = <T>(raw: string | null): T | null => {
+  if (!raw) return null
+  try {
+    return JSON.parse(raw) as T
+  } catch {
+    return null
+  }
+}
+
+const readCachedLocale = (locale: string): CachedLocalePayload | null => {
+  return safeJsonParse<CachedLocalePayload>(localStorage.getItem(getCacheKey(locale)))
+}
+
+const writeCachedLocale = (locale: string, payload: CachedLocalePayload) => {
+  try {
+    localStorage.setItem(getCacheKey(locale), JSON.stringify(payload))
+  } catch {
+    // ignore quota / private mode
+  }
+}
+
+export const clearLocaleCache = (locale?: string) => {
+  if (locale) {
+    localStorage.removeItem(getCacheKey(locale))
+    return
+  }
+  Object.keys(localStorage).forEach((k) => {
+    if (k.startsWith(`${I18N_CACHE_PREFIX}:`)) localStorage.removeItem(k)
+  })
+}
+
 const i18n = createI18n({
   legacy: false, // 使用 Composition API 模式,则需要将其设置为false
   globalInjection: true, //全局生效$t
@@ -83,16 +125,29 @@ export const loadLocaleMessages = async (locale: string) => {
   if (!langkey) return
 
   try {
+    // 1) 优先命中本地缓存(避免刷新就重复请求语言文件)
+    const cached = readCachedLocale(locale)
+    const now = Date.now()
+    const isCacheFresh = !!cached && now - cached.fetchedAt < DEFAULT_CACHE_TTL_MS
+    if (cached?.messages) {
+      i18n.global.setLocaleMessage(locale, cached.messages)
+      loadedLocaleSet.add(locale)
+      if (isCacheFresh) return
+      // cache 过期:继续走下面的远端拉取刷新缓存(同一次启动只会拉一次)
+    }
+
     // Lazy import avoids circular-init issues while still using API module method.
     const { getMultilingualJsonFile } = await import('@/api/module/multilingual')
     const res = await getMultilingualJsonFile({ langkey }, {})
     const normalized = normalizeApiMessages(res?.data || res)
-    i18n.global.setLocaleMessage(locale, {
+    const mergedMessages = {
       ...en,
       ...normalized,
       ...enUS
-    })
+    }
+    i18n.global.setLocaleMessage(locale, mergedMessages)
     loadedLocaleSet.add(locale)
+    writeCachedLocale(locale, { fetchedAt: now, messages: mergedMessages })
   } catch (error) {
     // keep fallback English messages when remote file fails
     console.error('loadLocaleMessages failed:', error)

+ 0 - 3
src/main.ts

@@ -24,7 +24,6 @@ import { createPinia } from 'pinia'
 
 import App from './App.vue'
 import router from './router'
-import i18n from './locales'
 import { useThemeStore } from '@/stores/modules/theme'
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
 
@@ -45,7 +44,6 @@ VxeUIAll.VxeUI.setConfig({
   i18n: (key, args) => i18n.global.t(key, args)
 })
 
-app.use(createPinia())
 const pinia = createPinia()
 pinia.use(piniaPluginPersistedstate)
 app.use(pinia)
@@ -54,7 +52,6 @@ app.use(VXETable)
 app.use(VxeUI)
 app.use(router)
 app.use(VxeUIAll)
-app.use(i18n)
 app.use(Antd)
 
 // 注册全局指令

+ 23 - 26
src/router/index.ts

@@ -3,9 +3,6 @@ import { useUserStore } from '@/stores/modules/user'
 import { useBreadCrumb } from '@/stores/modules/breadCrumb'
 import { useHeaderSearch } from '@/stores/modules/headerSearch'
 import { useFiltersStore } from '@/stores/modules/filtersList'
-import i18n from '@/locales/index'
-
-const t = i18n.global.t
 const router = createRouter({
   history: createWebHistory(`${import.meta.env.VITE_BASE_URL}`),
   routes: [
@@ -20,7 +17,7 @@ const router = createRouter({
           name: 'Dashboard',
           component: () => import('../views/Dashboard'),
           meta: {
-            breadName: t('menu.dashboard')
+            breadKey: 'menu.dashboard'
           }
         },
         {
@@ -28,7 +25,7 @@ const router = createRouter({
           name: 'Booking',
           component: () => import('../views/Booking'),
           meta: {
-            breadName: t('menu.booking')
+            breadKey: 'menu.booking'
           }
         },
         {
@@ -36,7 +33,7 @@ const router = createRouter({
           name: 'Report Management',
           component: () => import('../views/Report'),
           meta: {
-            breadName: t('menu.reportManagement')
+            breadKey: 'menu.reportManagement'
           }
         },
         {
@@ -45,7 +42,7 @@ const router = createRouter({
           component: () => import('../views/Report/src/components/ReportDetail'),
           meta: {
             activeMenu: '/report',
-            breadName: t('menu.reportDetail')
+            breadKey: 'menu.reportDetail'
           }
         },
         {
@@ -54,7 +51,7 @@ const router = createRouter({
           component: () => import('../views/Report/src/components/ReportSchedule'),
           meta: {
             activeMenu: '/report',
-            breadName: t('menu.reportSchedule')
+            breadKey: 'menu.reportSchedule'
           }
         },
         {
@@ -63,7 +60,7 @@ const router = createRouter({
           component: () => import('../views/Booking/src/components/BookingDetail'),
           meta: {
             activeMenu: '/booking',
-            breadName: t('menu.bookingDetail')
+            breadKey: 'menu.bookingDetail'
           }
         },
         {
@@ -71,7 +68,7 @@ const router = createRouter({
           name: 'Tracking',
           component: () => import('../views/Tracking'),
           meta: {
-            breadName: t('menu.tracking')
+            breadKey: 'menu.tracking'
           }
         },
         {
@@ -80,7 +77,7 @@ const router = createRouter({
           component: () => import('../views/Tracking/src/components/TrackingDetail'),
           meta: {
             activeMenu: '/tracking',
-            breadName: t('menu.trackingDetail')
+            breadKey: 'menu.trackingDetail'
           }
         },
         {
@@ -89,7 +86,7 @@ const router = createRouter({
           component: () => import('../views/Tracking/src/components/DownloadAttachment'),
           meta: {
             activeMenu: '/tracking',
-            breadName: t('menu.trackingDownloadAttachment')
+            breadKey: 'menu.trackingDownloadAttachment'
 
           }
         },
@@ -99,7 +96,7 @@ const router = createRouter({
           component: () => import('../views/Tracking/src/components/TrackingDetail'),
           meta: {
             activeMenu: '/tracking',
-            breadName: t('menu.shipmentDetail')
+            breadKey: 'menu.shipmentDetail'
           }
         },
         {
@@ -109,7 +106,7 @@ const router = createRouter({
             import('../views/Tracking/src/components/TrackingTable/src/components/VGMView.vue'),
           meta: {
             activeMenu: '/tracking',
-            breadName: t('menu.addVGM')
+            breadKey: 'menu.addVGM'
           }
         },
         {
@@ -118,7 +115,7 @@ const router = createRouter({
           component: () => import('../views/Tracking/src/components/PublicTracking'),
           meta: {
             activeMenu: '/tracking',
-            breadName: t('menu.publicTracking')
+            breadKey: 'menu.publicTracking'
           }
         },
         {
@@ -130,7 +127,7 @@ const router = createRouter({
             ),
           meta: {
             activeMenu: '/tracking',
-            breadName: t('menu.publicTrackingDetail')
+            breadKey: 'menu.publicTrackingDetail'
 
           }
         },
@@ -140,7 +137,7 @@ const router = createRouter({
           component: () => import('../views/Login'),
           meta: {
             activeMenu: '/tracking',
-            breadName: t('menu.login')
+            breadKey: 'menu.login'
           }
         },
         {
@@ -148,7 +145,7 @@ const router = createRouter({
           name: 'Reset Password',
           component: () => import('../views/Login/src/components/ChangePasswordCard.vue'),
           meta: {
-            breadName: t('menu.resetPassword')
+            breadKey: 'menu.resetPassword'
           }
         },
         {
@@ -189,7 +186,7 @@ const router = createRouter({
           name: 'System Message',
           component: () => import('../views/SystemMessage'),
           meta: {
-            breadName: t('menu.systemMessage')
+            breadKey: 'menu.systemMessage'
           }
         },
         {
@@ -197,7 +194,7 @@ const router = createRouter({
           name: 'System Message Detail',
           component: () => import('../views/SystemMessage/src/components/SystemMessageDetail.vue'),
           meta: {
-            breadName: t('menu.systemMessageDetail'),
+            breadKey: 'menu.systemMessageDetail',
             activeMenu: '/system-message'
           },
         },
@@ -206,7 +203,7 @@ const router = createRouter({
           name: 'System Settings',
           component: () => import('../views/SystemSettings'),
           meta: {
-            breadName: t('menu.systemSettings')
+            breadKey: 'menu.systemSettings'
           }
         },
         {
@@ -215,7 +212,7 @@ const router = createRouter({
           component: () => import('../views/SystemSettings/src/components/CreateNewrule'),
           meta: {
             activeMenu: '/system-settings',
-            breadName: t('menu.createNewRule')
+            breadKey: 'menu.createNewRule'
           }
         },
         {
@@ -228,7 +225,7 @@ const router = createRouter({
           name: 'Destination Delivery',
           component: () => import('../views/DestinationDelivery'),
           meta: {
-            breadName: t('menu.destinationDelivery')
+            breadKey: 'menu.destinationDelivery'
           }
 
         },
@@ -237,7 +234,7 @@ const router = createRouter({
           name: 'Create New Booking',
           component: () => import('../views/DestinationDelivery/src/components/CreateNewBooking'),
           meta: {
-            breadName: t('menu.createNewBooking'),
+            breadKey: 'menu.createNewBooking',
             activeMenu: '/destination-delivery'
           }
 
@@ -248,7 +245,7 @@ const router = createRouter({
           component: () => import('../views/DestinationDelivery/src/components/ConfiguRations'),
           meta: {
             activeMenu: '/destination-delivery',
-            breadName: t('menu.configurations')
+            breadKey: 'menu.configurations'
           }
         },
         {
@@ -260,7 +257,7 @@ const router = createRouter({
             ),
           meta: {
             activeMenu: '/destination-delivery',
-            breadName: t('menu.createNewRule')
+            breadKey: 'menu.createNewRule'
 
           }
         },

+ 17 - 14
src/stores/modules/breadCrumb.ts

@@ -1,7 +1,8 @@
 import { defineStore } from 'pinia'
 
 interface Route {
-  breadName: string
+  breadName?: string
+  breadKey?: string
   label: string
   path: string
   query?: string
@@ -44,48 +45,48 @@ export const useBreadCrumb = defineStore('breadCrumb', {
         return
       }
       if (toRoute.name === 'Destination Create New Rule') {
-        let breadName = ''
+        let breadKey = ''
         if (toRoute.query.a != undefined) {
-          breadName = 'Modify Rule'
+          breadKey = 'destinationDelivery.modifyRule'
         } else {
-          breadName = 'Create New Rule'
+          breadKey = 'destinationDelivery.createNewRule'
         }
         this.routeList = [
           {
-            breadName: 'Destination Delivery',
+            breadKey: 'menu.destinationDelivery',
             label: 'Destination Delivery',
             path: '/destination-delivery',
             query: ''
           },
           {
-            breadName: 'Configurations',
+            breadKey: 'menu.configurations',
             label: 'Configurations',
             path: '/destination-delivery/configurations',
             query: ''
           },
           {
-            breadName: breadName,
+            breadKey,
             label: toRoute.name,
             path: '/destination-delivery/create-new-rule',
             query: toRoute.query
           }
         ]
       } else if (toRoute.name === 'Create New Booking') {
-        let breadName = ''
+        let breadKey = ''
         if (toRoute.query.a != undefined) {
-          breadName = 'Modify Booking'
+          breadKey = 'destinationDelivery.modifyBooking'
         } else {
-          breadName = 'Create New Booking'
+          breadKey = 'destinationDelivery.createNewBooking'
         }
         this.routeList = [
           {
-            breadName: 'Destination Delivery',
+            breadKey: 'menu.destinationDelivery',
             label: 'Destination Delivery',
             path: '/destination-delivery',
             query: ''
           },
           {
-            breadName: breadName,
+            breadKey,
             label: toRoute.name,
             path: '/destination-delivery/create-new-booking',
             query: toRoute.query
@@ -94,7 +95,8 @@ export const useBreadCrumb = defineStore('breadCrumb', {
 
       } else if (toRoute.name && whiteList.includes(toRoute.name)) {
         this.routeList.push({
-          breadName: toRoute?.meta?.breadName || toRoute.name,
+          breadName: toRoute?.meta?.breadName,
+          breadKey: toRoute?.meta?.breadKey,
           label: toRoute.name,
           path: toRoute.path,
           query: toRoute.query
@@ -103,7 +105,8 @@ export const useBreadCrumb = defineStore('breadCrumb', {
         this.routeList = [
           {
             label: toRoute.name,
-            breadName: toRoute?.meta?.breadName || toRoute.name,
+            breadName: toRoute?.meta?.breadName,
+            breadKey: toRoute?.meta?.breadKey,
             path: toRoute.path,
             query: toRoute.query
           }

+ 1 - 1
src/views/Booking/src/components/BookingTable/src/BookingTable.vue

@@ -547,7 +547,7 @@ defineExpose({
       </template>
       <!-- Status字段的插槽 -->
       <template #status="{ row, column }">
-        <VTag :type="row[column.field]">{{ t(`booking.${row[column.field]}`) }}</VTag>
+        <VTag :type="row[column.field]">{{ row[column.field] }}</VTag>
       </template>
       <!-- Booking No字段的插槽 -->
       <template #link="{ row, column }">

+ 0 - 1
src/views/Layout/src/components/Header/HeaderView.vue

@@ -250,7 +250,6 @@ const languageChange = async (item) => {
     await switchAppLocale(targetLocale)
     locale.value = targetLocale
   } finally {
-    window.location.reload()
     userManualLoading.value = false
     languagePopoverVisible.value = false
   }

+ 3 - 2
src/views/Layout/src/components/Header/components/KAMMapping.vue

@@ -197,7 +197,7 @@ watch(
 .kam-mapping-container {
   display: flex;
   align-items: center;
-  width: 280px;
+  // width: 280px;
   height: 32px;
   margin-left: 16px;
   margin-right: 4px;
@@ -211,7 +211,7 @@ watch(
     border-color: var(--color-theme) !important;
   }
   &.hide-mapping {
-    width: 175px;
+    // width: 175px;
     margin-left: 8px;
     margin-right: 0px;
     border: none;
@@ -219,6 +219,7 @@ watch(
   }
 }
 .select-customer {
+  min-width: 223px;
   cursor: pointer;
 }
 .customer-input {

+ 3 - 4
src/views/MultilingualConfig/src/MultilingualConfig.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import { type VxeGridProps } from 'vxe-table'
 import { useCalculatingHeight } from '@/hooks/calculatingHeight'
-import FilterTabs from './components/FilterTabs.vue'
 import { cloneDeep } from 'lodash'
 import { onBeforeRouteLeave } from 'vue-router'
 
@@ -511,7 +510,7 @@ const handleTagToggle = (index: any, checked: any) => {
           v-model="selectedPage"
           placeholder="Select Page"
           @change="getMultilingualConfig()"
-          style="width: 240px; height: 32px"
+          style="width: 260px; height: 32px"
         >
           <template #label="{ label }">
             <div style="display: flex; align-items: center">
@@ -555,7 +554,7 @@ const handleTagToggle = (index: any, checked: any) => {
             <span class="font_family icon-icon_search_b"></span>
           </template>
         </el-input>
-        <el-button type="primary" @click="saveMultilingualConfig">Save</el-button>
+        <!-- <el-button type="primary" @click="saveMultilingualConfig">Save</el-button> -->
       </div>
     </div>
 
@@ -579,7 +578,7 @@ const handleTagToggle = (index: any, checked: any) => {
         </template>
 
         <template #key="{ row }">
-          <div>{{ row.originEnglish }}</div>
+          <div style="margin-bottom: 4px">{{ row.originEnglish }}</div>
           <span>{{ row.key }}</span>
         </template>
         <template #cell="{ row, column, rowIndex }">