瀏覽代碼

feat: 调整翻译页面

Jack Zhou 2 周之前
父節點
當前提交
7e5b939f99

+ 3 - 1
src/api/index.ts

@@ -8,6 +8,7 @@ import * as system from './module/system'
 import * as AIRobot from './module/AIRobot'
 import * as Delivery from './module/Delivery'
 import * as report from './module/report'
+import * as multilingual from './module/multilingual'
 /**
  * api 对象接口定义
  */
@@ -29,7 +30,8 @@ const apis = generateApiMap({
   ...system,
   ...AIRobot,
   ...Delivery,
-  ...report
+  ...report,
+  ...multilingual
 })
 export default {
   ...apis // 取出所有可遍历属性赋值在新的对象上

+ 17 - 2
src/api/module/multilingual.ts

@@ -19,9 +19,9 @@ export const saveMultilingualConfig = (params: any, config: any) => {
 }
 
 /**
- * search Multilingual Config
+ * get Multilingual Config
  */
-export const searchMultilingualConfig = (params: any, config: any) => {
+export const getMultilingualConfig = (params: any, config: any) => {
   return HttpAxios.post(
     `${baseUrl}`,
     {
@@ -31,4 +31,19 @@ export const searchMultilingualConfig = (params: any, config: any) => {
     },
     config
   )
+}
+
+/**
+ * get Multilingual pageInfo
+ */
+export const getMultilingualPageInfo = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'multilingual',
+      operate: 'multilingual_page',
+      ...params
+    },
+    config
+  )
 }

+ 11 - 66
src/locales/en.json

@@ -61,6 +61,7 @@
   "common": {
     "search": "Search",
     "searchUserNamePlaceholder": "Search user name",
+    "searchQuestionPlaceholder": "Search Question ID、User",
     "searchChatPlaceholder": "Search Question ID、User",
     "customerSearchPlaceholder": "Search by Customer code, Customer name",
     "customerType": "Customer Type",
@@ -70,9 +71,6 @@
     "cancel": "Cancel",
     "save": "Save",
     "reset": "Reset",
-    "copy": "Copy",
-    "edit": "Edit",
-    "delete": "Delete",
     "ok": "OK",
     "change": "Change",
     "login": "Login",
@@ -384,22 +382,9 @@
     "isActivePlaceholder": "Is Active",
     "applicationScopePlaceholder": "Application Scope",
     "partyIdsPlaceholder": "Party IDs",
-    "partyId": "Party ID",
-    "groupName": "Group Name",
-    "systemAccount": "System Account",
-    "klnOnlineAccount": "KLN Online Account",
-    "specificRoles": "Specific Roles",
-    "allUsersDescription": " This report will be available to all users in the system ",
-    "specificRolesDescription": "Restrict access to specific user roles",
-    "action" :"Action",
     "active": "Active",
     "inactive": "Inactive",
     "allUsers": "All Users",
-    "createNewReportTemplate": "Create New Report Template",
-    "emptyCreateTip": "Click the \"Create New Report Template\" button to add a report template.",
-    "shipmentLevel": "Shipment Level",
-    "containerLevel": "Container Level",
-    "itemLevel": "Item Level",
     "specificUsers": "Specific Users",
     "selectPartyIdsMulti": "Select Party IDs (Multi-select allowed)",
     "selectGroupNameMulti": "Select Group Name (Multi-select allowed)",
@@ -410,10 +395,7 @@
     "reportFieldsConfigurationReportLevelRequired": "Please select the report level.",
     "reportNameRequired": "Please enter the Report Name.",
     "reportDescriptionRequired": "Please enter the Report Description.",
-    "specificRolesRequired": "Please select Party ID or Group Name or KLN ONLINE Account for Specific Roles access control.",
     "savedSuccessfully": "Report Template saved successfully!",
-    "saveFailed": "Failed to save Report Template.",
-    "basicReportInformation": "Basic Report Information",
     "newFieldNameRequired": "Please enter a field name.",
     "fixedValueRequired": "Please enter a fixed value.",
     "reportLevelRequired": "Please enter the Report Level",
@@ -438,23 +420,7 @@
     "outputValueLabel": "Output Value",
     "addMapping": "Add Mapping",
     "cancel": "Cancel",
-    "apply": "Apply",
-    "reportName": "Report Name",
-    "reportLevel": "Report Level",
-    "isActive": "Is Active",
-    "applicationScope": "Application Scope",
-    "creationDate": "Creation Date",
-    "createdBy": "Created By",
-    "modifyBy": "Modify By",
-    "inactivate": "Inactivate",
-    "activate": "Activate",
-    "delete": "Delete",
-    "reportDescription": "Report Description",
-    "reportAccessControl": "Report Access Control",
-    "reportAccessControlDescription": "Report Access Control Description",
-    "fieldsVisible": "Fields Visible",
-    "systemName": "System Name",
-    "displayNameInReport": "Display Name in Report"
+    "apply": "Apply"
   },
   "reportSchedule": {
     "scheduleConfiguration": "Schedule Configuration - {name}",
@@ -525,11 +491,18 @@
     "title": "AI API Log",
     "searchPlaceholder": "Search Request ID, Question ID",
     "aiModel": "AI Model",
+    "responseDuration": "Response Duration",
     "selected": "Selected",
     "requestContent": "Request Content",
     "responseContent": "Response Content",
     "selectDataOnLogList": "Select data on your Operation Log list:",
-    "downloadWithSelectedColumns": "Download with selected columns"
+    "downloadWithSelectedColumns": "Download with selected columns",
+    "deepseek": "Deepseek",
+    "claude": "Claude",
+    "greaterOrEqual": ">=",
+    "equal": "=",
+    "lessOrEqual": "<=",
+    "download": "Download"
   },
   "operationLog": {
     "title": "Operation Log",
@@ -775,39 +748,12 @@
     "profile": "Profile",
     "personalProfileTab": "Personal Profile",
     "subscribeNotificationsTab": "Subscribe Notifications",
-    "event": "Event",
-    "eventDetails": "Event Details",
-    "frequency": "Frequency",
-    "methods": "Methods",
-    "hbolHawb": "HBOL/HAWB",
-    "shipper": "Shipper",
-    "consignee": "Consignee",
-    "etd": "ETD",
-    "recentMilestone": "Recent Milestone",
-    "recentMilestoneTitle": "Recent Milestone",
-    "recentMilestoneDescription": "Recent Milestone",
-    "recentMilestoneButton": "Recent Milestone",
-    "recentMilestoneButtonText": "Recent Milestone",
-    "selectDataOnLogList": "Select data on your Operation Log list:",
-    "downloadWithSelectedColumns": "Download with selected columns",
-    "supportedReferenceTip1": "We support the following references number to find booking:",
-    "supportedReferenceTip2": "· Booking No./HAWB No./MAWB No./PO No./Carrier Booking No./Contract No./File No./Quote No.",
     "monitoringSettingsTab": "Monitoring Settings",
-    "shipmentRange":"Shipment Range",
     "notificationEventsForSubscribedShipments": "Notification Events for Subscribed Shipments",
     "edit": "Edit",
     "add": "Add",
     "subscribedShipments": "Subscribed Shipments",
-    "addRule": "Add Rule",
-    "deleteRule": "Delete Rule",
-    "confirmDeleteRule": "Are you sure to delete this notification event?",
-    "configuration": "Configuration",
-    "addedRules": "Added Rules",
-    "failedToLoadOptions": "Failed to load options",
-    "bookings": "Bookings",
-    "customizeYourShipmentTrackingPreferences": "Customize your shipment tracking preferences",
-    "accountPasswordExpiredUnavailable": "This account's password has expired and is currently unavailable. Please select a different customer account to continue.",
-    "accountPasswordExpiredUnavailableDescription": "This account's password has expired and is currently unavailable. Please select a different customer account to continue."
+    "addRule": "Add Rule"
   },
  
   "dashboard": {
@@ -1068,7 +1014,6 @@
     "freeStoragePeriodEnds": "Free Storage Period Ends",
     "total": "Total",
     "shipments": "shipments",
-    "ctns": "ctns",
     "totalCartons": "Total Cartons",
     "packingList": "Packing List",
     "bookingDetail": "Booking Detail",

+ 0 - 2
src/router/index.ts

@@ -306,8 +306,6 @@ router.beforeEach(async (to, from, next) => {
     // 如果满足上述任一“保留”场景,则 NOT 清除 (返回 false)
     // 否则,清除 (返回 true)
 
-    console.log('路由守卫判断:', from)
-    console.log('路由TO', to)
     const shouldKeepFilters = isReturningFromDetail || isReturningFromVgm || isEnteringWhitelist || isRefresh;
 
     return !shouldKeepFilters;

+ 24 - 0
src/styles/theme.scss

@@ -82,6 +82,18 @@
   --color-tag-rejected-bg: rgba(201, 53, 63,0.2);
   --color-tag-rejected: rgb(201, 53, 63);
 
+  --color-tag-checked-bg: rgb(255, 117, 0, 0.1);
+  --color-tag-chinese-simplified-bg: rgb(191, 102, 234,0.15);
+  --color-tag-chinese-simplified-text-color: #c9353f;
+  --color-tag-chinese-traditional-bg: #eaecff;
+  --color-tag-chinese-traditional-text-color: #bf66ea;
+  --color-tag-spanish-bg: rgb(158, 97, 19,0.15);
+  --color-tag-spanish-text-color: #9e6113;
+  --color-tag-portuguese-bg: rgb(74, 94, 40,0.15);
+  --color-tag-portuguese-text-color: #4a5e28;
+
+  --color-multilingual-page-select-item-tag-bg: #fff4d1;
+
   --color-border: #eaebed;
   --color-select-border: #eaebed;
   --border-color-2: #eaebed;
@@ -601,6 +613,18 @@
   --color-tag-approved-bg: rgba(91, 180, 98, 0.2);
   --color-tag-rejected-bg: rgb(255, 220, 222);
 
+  --color-tag-checked-bg: rgb(255, 117, 0, 0.2);
+  --color-tag-chinese-simplified-bg: rgb(201, 53, 63,0.2);
+  --color-tag-chinese-simplified-text-color: #c9353f;
+  --color-tag-chinese-traditional-bg: rgb(191, 102, 234,0.2);
+  --color-tag-chinese-traditional-text-color: #bf66ea;
+  --color-tag-spanish-bg: rgb(158, 97, 19,0.3);
+  --color-tag-spanish-text-color: #9e6113;
+  --color-tag-portuguese-bg: rgb(168, 201, 122,0.3);
+  --color-tag-portuguese-text-color: #a8c97a;
+
+  --color-multilingual-page-select-item-tag-bg: rgb(224, 161, 0, 0.2);
+
   --color-card-icon-box-bg: #4f535c;
   --color-card-number-cancelled: #babcc0;
   --color-steps-unfinished-line: #6a6d73;

+ 6 - 6
src/views/AIApiLog/src/AIApiLog.vue

@@ -19,26 +19,26 @@ const searchData = ref({
 
 const aiModelList = [
   {
-    label: 'Deepseek',
+    label: t('aiApiLog.deepseek'),
     value: 'Deepseek'
   },
   {
-    label: 'Claude',
+    label: t('aiApiLog.claude'),
     value: 'Claude'
   }
 ]
 
 const comparatorList = [
   {
-    label: '>=',
+    label: t('aiApiLog.greaterOrEqual'),
     value: 'thanOrEqual'
   },
   {
-    label: '=',
+    label: t('aiApiLog.equal'),
     value: 'equal'
   },
   {
-    label: '<=',
+    label: t('aiApiLog.lessOrEqual'),
     value: 'lessOrEqual'
   }
 ]
@@ -94,7 +94,7 @@ const DateChange = (date: any) => {
           </el-select>
         </div>
         <div class="comparator-tips_filter">
-          <span>{{ t('common.responseDuration') }}</span>
+          <span>{{ t('aiApiLog.responseDuration') }}</span>
           <el-select
             placeholder=""
             clearable

+ 3 - 3
src/views/AIApiLog/src/components/TableView/src/TableView.vue

@@ -307,7 +307,7 @@ const exportTable = (status: number) => {
   const exportConfig: any = {
     type: 'xlsx',
     message: false,
-    filename: `AI API Log_${dayjs().format('YYYYMMDDHH[h]mm[m]ss[s]')}`
+    filename: `${t('aiApiLog.title')}_${dayjs().format('YYYYMMDDHH[h]mm[m]ss[s]')}`
   }
   if (status === 1) {
     exportConfig.columnFilterMethod = ({ column }: any) => {
@@ -345,7 +345,7 @@ const handleCheckAllChange = ({ records }: any) => {
 
 const logDialogRef = ref()
 const logLoading = ref(false)
-const handleLinkClick = (row) => {
+const handleLinkClick = (row: any) => {
   logLoading.value = true
   $api
     .getAIApiLogDialog({
@@ -386,7 +386,7 @@ defineExpose({
       <div class="right-tools-btn">
         <el-button class="el-button--main el-button--pain-theme" @click="handleDownload">
           <span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
-          {{ t('common.download') }}
+          {{ t('tracking.download') }}
         </el-button>
       </div>
     </div>

+ 5 - 4
src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue

@@ -731,9 +731,10 @@ onMounted(() => {
             >{{ t('destinationDelivery.selected') }}:
           </span>
           <span
-            >{{ bookingTableRef?.getTableCheckedRows().length }}
-            {{ t('destinationDelivery.shipments') }}|{{ ctnsCount }}
-            {{ t('destinationDelivery.ctns') }}</span
+            >{{ bookingTableRef?.getTableCheckedRows().length }} Shipments|{{
+              ctnsCount
+            }}
+            ctns</span
           >
         </div>
         <el-button @click="CancelRulesVisible = true" class="el-button--default create-button">
@@ -751,7 +752,7 @@ onMounted(() => {
     </div>
     <div class="booking-info" v-if="a != undefined">
       <div class="booking-no">
-        <span class="no">{{ t('booking.bookingNo') }}{{ booking }}</span>
+        <span class="no">Booking No.{{ booking }}</span>
         <v-tag class="tag" type="Pending Approval">{{ status }}</v-tag>
       </div>
     </div>

+ 1 - 5
src/views/Layout/src/components/Menu/MenuView.vue

@@ -261,11 +261,7 @@ const jumpLink = (link: string) => {
     </el-menu>
     <div class="blogroll" :class="{ collapse: isCollapse }">
       <el-collapse v-model="activeName" accordion>
-        <el-collapse-item
-          :title="t('common.regionalSolutions')"
-          name="1"
-          :icon="CaretRight"
-        >
+        <el-collapse-item :title="t('common.regionalSolutions')" name="1" :icon="CaretRight">
           <div class="blogroll-content">
             <div class="blogroll-item" @click="jumpLink('https://www.ksp.kln.com/')">
               <img

+ 5 - 5
src/views/Login/src/components/ChangePasswordCard.vue

@@ -46,22 +46,22 @@ const handleChangePwd = () => {
   }
   if (loginForm.value.newPassword.length < 12 || loginForm.value.newPassword.length > 20) {
     loginError.value.newPassword = true
-    newPwdErrTips.value = t('login.passwordLength12to20')
+    newPwdErrTips.value = 'Password length between 12 - 20'
     return
   }
   if (!/[A-Z]/.test(loginForm.value.newPassword)) {
     loginError.value.newPassword = true
-    newPwdErrTips.value = t('login.passwordMustContainUppercase')
+    newPwdErrTips.value = 'Password must contain uppercase letters'
     return
   }
   if (!/[a-z]/.test(loginForm.value.newPassword)) {
     loginError.value.newPassword = true
-    newPwdErrTips.value = t('login.passwordMustContainLowercase')
+    newPwdErrTips.value = 'Password must contain lowercase letters'
     return
   }
   if (!/[0-9]/.test(loginForm.value.newPassword)) {
     loginError.value.newPassword = true
-    newPwdErrTips.value = t('login.passwordMustContainNumber')
+    newPwdErrTips.value = 'Password must contain numbers'
     return
   }
   $api
@@ -132,7 +132,7 @@ const checkPassword = () => {
   <div class="login" :class="{ 'dark-bg': themeStore.theme === 'dark' }">
     <el-card class="login-card">
       <div class="title" :class="{ 'is-dark': themeStore.theme === 'dark' }">
-        <span class="welcome">{{ t('login.changePassword') }}</span>
+        <span class="welcome">Change Password</span>
         <span class="tips">{{ tips }}</span>
       </div>
       <div class="login-form">

+ 382 - 146
src/views/MultilingualConfig/src/MultilingualConfig.vue

@@ -1,19 +1,158 @@
 <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'
 
-const selectedPage = ref('destination-delivery')
-const pageList = [
-  { label: 'Destination Delivery', value: 'destination-delivery', unverifiedNumber: 1 },
-  { label: 'Page 2', value: 'page-2', unverifiedNumber: 1 },
-  { label: 'Page 3', value: 'page-3', unverifiedNumber: 1 }
+const languageStatusList = [
+  'englishStatus',
+  'simplifiedChineseStatus',
+  'traditionalChineseStatus',
+  'frenchStatus',
+  'spanishStatus',
+  'portugueseStatus'
 ]
+
+const tabList = ref([
+  { name: 'All', checked: true, number: 0, type: 'all', statusKey: languageStatusList },
+  { name: 'English', checked: false, number: 0, type: 'english', statusKey: 'englishStatus' },
+  {
+    name: 'Chinese (Simplified)',
+    checked: false,
+    number: 0,
+    type: 'chinese-simplified',
+    statusKey: 'simplifiedChineseStatus'
+  },
+  {
+    name: 'Chinese (Traditional)',
+    checked: false,
+    number: 0,
+    type: 'chinese-traditional',
+    statusKey: 'traditionalChineseStatus'
+  },
+  { name: 'Français', checked: false, number: 0, type: 'french', statusKey: 'frenchStatus' },
+  { name: 'Español', checked: false, number: 0, type: 'spanish', statusKey: 'spanishStatus' },
+  {
+    name: 'Português',
+    checked: false,
+    number: 0,
+    type: 'portuguese',
+    statusKey: 'portugueseStatus'
+  }
+])
+const currnentTab = computed(() => {
+  const checkedTab = tabList.value.filter((item) => item.checked)
+  let tabs = []
+  checkedTab.forEach((item) => {
+    if (item.name === 'All') {
+      tabs = item.statusKey as string[]
+    } else {
+      tabs.push(item.statusKey as string)
+    }
+  })
+  return tabs
+})
+const columnList = computed(() => {
+  const columns: any = [
+    {
+      title: 'Key',
+      field: 'key',
+      width: 300,
+      slots: {
+        default: 'key'
+      }
+    }
+  ]
+  currnentTab.value.forEach((statusKey) => {
+    let column = {
+      title: '',
+      field: '',
+      minWidth: 300,
+      slots: {
+        header: 'table_header',
+        default: 'cell'
+      }
+    }
+    switch (statusKey) {
+      case 'englishStatus':
+        column.title = 'English'
+        column.field = 'english'
+        break
+      case 'simplifiedChineseStatus':
+        column.title = 'Chinese (Simplified)'
+        column.field = 'simplifiedChinese'
+        break
+      case 'traditionalChineseStatus':
+        column.title = 'Chinese (Traditional)'
+        column.field = 'traditionalChinese'
+        break
+      case 'frenchStatus':
+        column.title = 'Français'
+        column.field = 'french'
+        break
+      case 'spanishStatus':
+        column.title = 'Español'
+        column.field = 'spanish'
+        break
+      case 'portugueseStatus':
+        column.title = 'Português'
+        column.field = 'portuguese'
+        break
+    }
+    columns.push(column)
+  })
+  return columns
+})
+
+const tabChange = (newTabList) => {
+  tabList.value = cloneDeep(newTabList)
+  getMultilingualConfig()
+  nextTick(() => {
+    tableData.value.columns = columnList.value
+  })
+}
+const initColumns = () => {
+  tableData.value.columns = columnList.value
+}
+
+onMounted(() => {
+  initColumns()
+})
+
+const selectedPage = ref('all')
+const pageList = ref<{ label: string; value: string; unverifiedNumber?: number }[]>([])
+
+// 将首字母大写
+const capitalizeFirstLetter = (str: string) => {
+  if (!str) return str
+  return str.charAt(0).toUpperCase() + str.slice(1)
+}
+const getMultilingualPageInfo = async () => {
+  try {
+    const res = await $api.getMultilingualPageInfo()
+    if (res.code === 200) {
+      pageList.value = res.data.map((item: any) => {
+        return {
+          label: capitalizeFirstLetter(item.page_key),
+          value: item.page_key,
+          unverifiedNumber: item.unverified_count
+        }
+      })
+    }
+  } catch (error) {
+    console.error(error)
+  }
+}
+
+onMounted(() => {
+  getMultilingualPageInfo()
+})
 const getCurrentPageUnverifiedNumber = () => {
-  return pageList.find((item) => item.value === selectedPage.value)?.unverifiedNumber || 0
+  return pageList.value.find((item) => item.value === selectedPage.value)?.unverifiedNumber || 0
 }
 const searchKey = ref('')
 
-const containerHeight = useCalculatingHeight(document.documentElement, 456, [])
+const containerHeight = useCalculatingHeight(document.documentElement, 276, [])
 
 type RowItem = {
   key: string
@@ -43,43 +182,39 @@ const tableData = ref<VxeGridProps<RowItem>>({
   border: true,
   round: true,
   columns: [
-    {
-      title: 'Key',
-      field: 'key',
-      width: 300,
-      slots: {
-        default: 'key'
-      }
-    },
-    {
-      title: 'English',
-      field: 'english',
-      minWidth: 300,
-      slots: {
-        header: 'table_header',
-        default: 'languageVerification'
-      }
-    },
-    {
-      title: 'Chinese',
-      field: 'simplifiedChinese',
-      minWidth: 300,
-      slots: {
-        header: 'table_header',
-        default: 'languageVerification'
-      }
-    }
+    // {
+    //   title: 'Key',
+    //   field: 'key',
+    //   width: 300,
+    //   slots: {
+    //     default: 'key'
+    //   }
+    // },
+    // {
+    //   title: 'English',
+    //   field: 'english',
+    //   minWidth: 300,
+    //   slots: {
+    //     header: 'table_header',
+    //     default: 'cell'
+    //   }
+    // },
+    // {
+    //   title: 'Chinese',
+    //   field: 'simplifiedChinese',
+    //   minWidth: 300,
+    //   slots: {
+    //     header: 'table_header',
+    //     default: 'cell'
+    //   }
+    // }
   ],
   cellConfig: {
-    height: 68
+    // minHeight: 68
   },
   scrollY: { enabled: true, oSize: 20, gt: 30 },
-  emptyText: ' ',
-  showHeaderOverflow: true,
-  showOverflow: true
+  emptyText: ' '
 })
-const tableLoadingTableData = ref(false)
-const tableLoadingColumn = ref(false)
 
 const originalTableData = ref([
   {
@@ -96,35 +231,20 @@ const originalTableData = ref([
     spanishStatus: 0, // 0: 未审核, 1: 已审核, 2: 已修改
     portuguese: 'Nome de usuário', // 葡萄牙语
     portugueseStatus: 0 // 0: 未审核, 1: 已审核, 2: 已修改
-  },
-  {
-    key: 'password',
-    traditionalChinese: '密碼', // 繁体中文
-    traditionalChineseStatus: 1, // 0: 未审核, 1: 已审核, 2: 已修改
-    simplifiedChinese: '密码', // 简体中文
-    simplifiedChineseStatus: 0, // 0: 未审核, 1: 已审核, 2: 已修改
-    english: 'Password', // 英文
-    englishStatus: 1, // 0: 未审核, 1: 已审核, 2: 已修改
-    french: 'Mot de passe', // 法语
-    frenchStatus: 0, // 0: 未审核, 1: 已审核, 2: 已修改
-    spanish: 'Contraseña', // 西班牙语
-    spanishStatus: 0, // 0: 未审核, 1: 已审核, 2: 已修改
-    portuguese: 'Senha', // 葡萄牙语
-    portugueseStatus: 0 // 0: 未审核, 1: 已审核, 2: 已修改
   }
 ])
+const variableTableData = ref([])
 
 const showTableData = computed(() => {
-  let arr = originalTableData.value.filter((item) => {
+  let arr = variableTableData.value.filter((item) => {
     return item.key.includes(searchKey.value)
   })
-  const unverifiedFilter = Object.entries(headerSwitchMap.value).find((item) => item[1] === true)
-  if (unverifiedFilter) {
+  // 筛选未审核的字段
+  if (unverifiedSwitch.value) {
     arr = arr.filter((item) => {
-      return item[unverifiedFilter[0]] === 0
+      return currnentTab.value.some((status) => item[status as keyof typeof item] === 0)
     })
   }
-  console.log(unverifiedFilter, 'unverifiedFilter')
   return arr
 })
 
@@ -158,22 +278,37 @@ const unverifiedNumberMap = computed(() => {
   }
 })
 
-const headerSwitchMap = ref<Record<string, boolean>>({
-  traditionalChineseStatus: false,
-  simplifiedChineseStatus: false,
-  englishStatus: false,
-  frenchStatus: false,
-  spanishStatus: false,
-  portugueseStatus: false
-})
+const tableLoading = ref(false)
 
-const handleSwitchChange = (field: EditableField) => {
-  Object.entries(headerSwitchMap.value).forEach((item) => {
-    if (item[0] !== field) {
-      headerSwitchMap.value[item[0]] = false
+const getMultilingualConfig = async () => {
+  tableLoading.value = true
+  try {
+    const { code, data: responseData } = await $api.getMultilingualConfig({
+      pagekey: selectedPage.value,
+      langKey: currnentTab.value.map((status) => status.replace('Status', '')).join(',')
+    })
+    if (code === 200) {
+      originalTableData.value = cloneDeep(responseData.data)
+      variableTableData.value = cloneDeep(responseData.data)
+      pageList.value = responseData.pageDate.map((item: any) => {
+        return {
+          label: capitalizeFirstLetter(item.page_key),
+          value: item.page_key,
+          unverifiedNumber: item.unverified_count
+        }
+      })
     }
-  })
+  } catch (error) {
+    console.log(error)
+  } finally {
+    tableLoading.value = false
+  }
 }
+onMounted(() => {
+  getMultilingualConfig()
+})
+
+const unverifiedSwitch = ref(false)
 
 const currentEditor = ref<{ rowIndex: number | null; field: EditableField | null }>({
   rowIndex: null,
@@ -186,6 +321,7 @@ const handleEdit = (rowIndex: number, field: EditableField, value: string) => {
 }
 const handleComfirmEdit = (rowIndex: number, field: EditableField) => {
   showTableData.value[rowIndex][field] = currentEditorValue.value
+  showTableData.value[rowIndex][field + 'Status'] = 1
   currentEditor.value = { rowIndex: null, field: null }
   currentEditorValue.value = ''
 }
@@ -193,6 +329,57 @@ const handleCancelEdit = () => {
   currentEditor.value = { rowIndex: null, field: null }
   currentEditorValue.value = ''
 }
+
+// 值对应语言映射
+const multilingualConfigMapping = {
+  englishStatus: 'english',
+  simplifiedChinese: 'simplifiedChinese',
+  simplifiedChineseStatus: 'simplifiedChinese',
+  traditionalChineseStatus: 'traditionalChinese',
+  frenchStatus: 'french',
+  spanishStatus: 'spanish',
+  portugueseStatus: 'portuguese'
+}
+
+const getChangedConfig = () => {
+  let changedConfigList: any = []
+
+  variableTableData.value.forEach((item, itemIndex) => {
+    currnentTab.value.forEach((statusKey) => {
+      if (item[statusKey] !== originalTableData.value[itemIndex][statusKey]) {
+        changedConfigList.push({
+          page: item.key_page,
+          lang: multilingualConfigMapping[statusKey],
+          trans_key: item.key,
+          status: 1, // 1代表已审核
+          trans_value: item[multilingualConfigMapping[statusKey]]
+        })
+      }
+    })
+  })
+  return {
+    multilingual_param: changedConfigList
+  }
+}
+
+const saveMultilingualConfig = async () => {
+  try {
+    const changedConfig = getChangedConfig()
+    console.log('changedConfig', changedConfig)
+    if (changedConfig.multilingual_param.length === 0) {
+      return
+    }
+    const res = await $api.saveMultilingualConfig(changedConfig)
+    if (res.code === 200) {
+      getMultilingualConfig()
+    } else {
+      ElMessage.error('Save failed')
+    }
+  } catch (error) {
+    console.log(error)
+    ElMessage.error('Save failed')
+  }
+}
 </script>
 
 <template>
@@ -201,17 +388,19 @@ const handleCancelEdit = () => {
       <span>Multilingual Config</span>
     </div>
     <div class="page-filter">
+      <div class="language-tab">
+        <FilterTabs :tabList="tabList" @tabChange="tabChange" />
+      </div>
       <div class="top-content">
-        <span class="font_family icon-icon_page_b" style="font-size: 24px"></span>
-        <span style="margin: 0 8px; font-size: 18px; font-weight: 700">Select Page</span>
         <el-select
           v-model="selectedPage"
           placeholder="Select Page"
-          style="width: 400px; height: 48px"
+          @change="getMultilingualConfig()"
+          style="width: 240px; height: 32px"
         >
           <template #label="{ label }">
             <div style="display: flex; align-items: center">
-              <span style="font-size: 18px">{{ label }}</span>
+              <span style="font-size: 14px">{{ label }}</span>
               <div class="page-select-unverified-tag">
                 <span> {{ getCurrentPageUnverifiedNumber() }} Unverified</span>
               </div>
@@ -226,30 +415,33 @@ const handleCancelEdit = () => {
             <div style="display: flex; align-items: center">
               <span>{{ item.label }}</span>
               <div class="page-item-unverified-tag" style="margin-left: 20px">
-                <span style="font-size: 10px">
-                  {{ getCurrentPageUnverifiedNumber() }} Unverified</span
-                >
+                <span style="font-size: 10px"> {{ item.unverifiedNumber || 0 }} Unverified</span>
               </div>
             </div>
           </el-option>
         </el-select>
+        <div class="unverified-switch">
+          <el-switch
+            v-model="unverifiedSwitch"
+            active-text="Unverified only"
+            inactive-text=""
+            size="small"
+          />
+        </div>
+        <el-input placeholder="Search key..." v-model="searchKey" style="width: 400px">
+          <template #prefix>
+            <span class="font_family icon-icon_search_b"></span>
+          </template>
+        </el-input>
+        <el-button type="primary" @click="saveMultilingualConfig">Save</el-button>
       </div>
     </div>
-    <div class="label">Filter</div>
-    <el-input
-      placeholder="Search key..."
-      v-model="searchKey"
-      style="margin-left: 24px; width: 400px"
-    >
-      <template #prefix>
-        <span class="font_family icon-icon_search_b"></span>
-      </template>
-    </el-input>
-    <el-divider style="width: calc(100% - 48px); margin: 8px auto" />
+
+    <el-divider style="margin: 8px auto" />
     <div class="config-table">
       <vxe-grid
         ref="tableRef"
-        v-vloading="tableLoadingTableData || tableLoadingColumn"
+        v-vloading="tableLoading"
         :height="containerHeight"
         :data="showTableData"
         :style="{ border: 'none' }"
@@ -261,27 +453,19 @@ const handleCancelEdit = () => {
             <span class="unverified-number">{{
               unverifiedNumberMap[column.field as keyof typeof unverifiedNumberMap]
             }}</span>
-            <el-switch
-              v-model="headerSwitchMap[(column.field + 'Status') as keyof typeof headerSwitchMap]"
-              active-text="Unverified only"
-              inactive-text=""
-              @change="handleSwitchChange((column.field + 'Status') as EditableField)"
-            />
           </div>
         </template>
 
         <template #key="{ row }">
+          <div>{{ row.orginEnglish }}</div>
           <span>{{ row.key }}</span>
         </template>
-        <template #languageVerification="{ row, column, rowIndex }">
-          <div
-            class="show-content"
+        <template #cell="{ row, column, rowIndex }">
+          <template
             v-if="currentEditor.rowIndex !== rowIndex || currentEditor.field !== column.field"
           >
-            <span>{{ row[column.field as EditableField] }}</span>
-            <span
-              class="font_family icon-icon_edit_b edit-icon"
-              style="display: none"
+            <div
+              class="show-content"
               @click="
                 handleEdit(
                   rowIndex,
@@ -289,31 +473,42 @@ const handleCancelEdit = () => {
                   row[column.field as EditableField]
                 )
               "
-            ></span>
-          </div>
+            >
+              <span>{{ row[column.field as EditableField] }}</span>
+            </div>
+            <div
+              class="unverified-tag"
+              v-if="row[(column.field as EditableField) + 'Status'] === 0"
+            >
+              <span class="font_family icon-icon_delay_b" style="font-size: 14px"></span>
+              <span style="display: inline-block; margin-top: 2px">Unverified</span>
+            </div>
+            <div class="verified-tag" v-if="row[(column.field as EditableField) + 'Status'] === 1">
+              <span class="font_family icon-icon_active" style="font-size: 9px"></span>
+              <span style="display: inline-block; margin-top: 1px">Verified</span>
+            </div>
+          </template>
+
           <div class="editor-input" v-else>
-            <el-input v-model="currentEditorValue" style="height: 28px; margin-bottom: 3px">
-              <template #suffix>
-                <span
-                  class="font_family icon-icon_reject_b"
-                  style="margin-top: 1px; margin-right: 10px; font-size: 16px"
-                  @click="handleCancelEdit"
-                ></span>
-                <span
-                  class="font_family icon-icon_confirm_b"
-                  style="margin-top: 1px; font-size: 18px; color: #ed6d00; cursor: pointer"
-                  @click="handleComfirmEdit(rowIndex, column.field as EditableField)"
-                ></span>
-              </template>
-            </el-input>
-          </div>
-          <div class="unverified-tag" v-if="row[(column.field as EditableField) + 'Status'] === 0">
-            <span class="font_family icon-icon_delay_b" style="font-size: 14px"></span>
-            <span style="display: inline-block; margin-top: 1px">Unverified</span>
-          </div>
-          <div class="verified-tag" v-if="row[(column.field as EditableField) + 'Status'] === 1">
-            <span class="font_family icon-icon_active" style="font-size: 9px"></span>
-            <span style="display: inline-block; margin-top: 1px">Verified</span>
+            <el-input
+              v-model="currentEditorValue"
+              type="textarea"
+              :autosize="{ minRows: 1, maxRows: 8 }"
+              style="margin-bottom: 3px"
+            />
+            <div class="operate-btn">
+              <el-button @click="handleCancelEdit">
+                <span class="font_family icon-icon_reject_b"></span>
+                <span>Cancel</span>
+              </el-button>
+              <el-button
+                class="el-button--main"
+                @click="handleComfirmEdit(rowIndex, column.field as EditableField)"
+              >
+                <span class="font_family icon-icon_confirm_b"></span>
+                <span>Verify</span>
+              </el-button>
+            </div>
           </div>
         </template>
       </vxe-grid>
@@ -327,25 +522,25 @@ const handleCancelEdit = () => {
   padding-bottom: 40px;
   background-color: var(--color-mode);
   .page-filter {
-    margin: 16px 24px 24px;
+    margin: 16px 24px 8px;
     border-radius: 12px;
     .top-content {
       display: flex;
       align-items: center;
-      height: 80px;
-      padding: 28px 16px;
-      background-color: var(--color-shipment-status-header-bg);
+      margin-top: 16px;
       :deep(.el-select__wrapper) {
-        height: 48px;
+        height: 32px;
+      }
+      .unverified-switch {
+        height: 34px;
+        margin: 0 8px;
+        padding: 4px 16px;
+        border-radius: 6px;
+        border: 1px solid var(--input-border);
       }
     }
   }
-  .label {
-    margin-left: 24px;
-    margin-bottom: 12px;
-    font-size: 18px;
-    font-weight: 700;
-  }
+
   .page-select-unverified-tag {
     display: inline-flex;
     height: 20px;
@@ -360,6 +555,19 @@ const handleCancelEdit = () => {
     }
   }
 }
+.page-item-unverified-tag {
+  display: inline-flex;
+  height: 20px;
+  margin-left: 8px;
+  padding: 5px 7px;
+  line-height: 11px;
+  border-radius: 3px;
+  background-color: var(--color-multilingual-page-select-item-tag-bg);
+  span {
+    color: var(--color-steps-current-icon-color);
+    font-size: 10px;
+  }
+}
 .header {
   position: sticky;
   top: 0;
@@ -394,6 +602,7 @@ const handleCancelEdit = () => {
   .unverified-tag,
   .verified-tag {
     display: inline-flex;
+    align-items: center;
     height: 16px;
     padding: 2px 4px;
     font-size: 10px;
@@ -423,17 +632,44 @@ const handleCancelEdit = () => {
   :deep(.el-switch__label--right) {
     margin-left: 4px;
   }
+
+  :deep(.vxe-cell) {
+    white-space: normal !important; /* 强制不换行改为自动换行 */
+    word-break: break-word; /* 长单词自动换行 */
+    line-height: normal; /* 行高恢复正常 */
+    overflow: visible; /* 允许溢出可见 */
+  }
+
+  /* 2. 让行高自动适应内容 */
+  :deep(.vxe-cell) {
+    height: auto !important; /* 关键:强制高度为 auto */
+  }
 }
 
 .show-content {
-  &:hover {
-    .edit-icon {
-      display: block !important;
-      float: right;
-      color: #ed6d00;
-      font-size: 18px;
-      cursor: pointer;
+  margin-bottom: 8px;
+  cursor: pointer;
+}
+.editor-input {
+  position: relative;
+  .operate-btn {
+    position: absolute;
+    bottom: 11px;
+    right: 10px;
+    .el-button {
+      height: 24px;
+      padding: 8px 4px;
+      .font_family {
+        margin-right: 1px;
+        font-size: 14px;
+      }
+      span {
+        font-size: 12px;
+      }
     }
   }
+  :deep(.el-textarea__inner) {
+    padding-bottom: 28px;
+  }
 }
 </style>

+ 165 - 0
src/views/MultilingualConfig/src/components/FilterTabs.vue

@@ -0,0 +1,165 @@
+<script setup lang="ts">
+import { cloneDeep } from 'lodash'
+import { useI18n } from 'vue-i18n'
+
+interface ListItem {
+  name: string
+  number: number
+  type: string
+  checked: boolean
+}
+interface Props {
+  tabList: ListItem[]
+}
+const props = withDefaults(defineProps<Props>(), {})
+const { t } = useI18n()
+
+const emits = defineEmits(['tabChange'])
+
+const getCheckedTabs = (tabList: ListItem[]) => {
+  const checkedList = tabList.filter((item) => item.checked)
+  return checkedList.map((item) => item.name)
+}
+// 判断是否除了all,其他的全选了
+const isAllExceptAllSelected = (tabList: ListItem[]) => {
+  const curCheckedTags = getCheckedTabs(tabList)
+  if (curCheckedTags.length === tabList.length - 1 && !curCheckedTags.includes('All')) {
+    return true
+  } else {
+    return false
+  }
+}
+//  点击标签 如果除了all
+const handleTagToggle = (index: any, checked: any) => {
+  // 如果点击的是all,并且当前选中也是all,那么直接返回
+  if (index === 0 && checked) return
+
+  const curTagList = cloneDeep(props.tabList)
+  if (index !== 0) {
+    const curTag = curTagList[index]
+    const curCheckedTags = getCheckedTabs(curTagList)
+    // 如果只选中了一个标签,并且这个标签不是all,那么改为选中all,取消选中当前选项
+    if (curCheckedTags.includes(curTag.name) && curCheckedTags.length === 1) {
+      curTagList[index].checked = false
+      curTagList[0].checked = true
+      emits('tabChange', curTagList)
+      return
+    }
+    curTagList[index].checked = !checked
+    const isCheckedAll = isAllExceptAllSelected(curTagList)
+    if (isCheckedAll) {
+      curTagList[0].checked = true
+      curTagList.forEach((item, index) => {
+        index !== 0 && (item.checked = false)
+      })
+    } else {
+      curTagList[0].checked = false
+    }
+  } else if (index === 0 && !checked) {
+    curTagList[0].checked = true
+    curTagList.forEach((item, index) => {
+      index !== 0 && (item.checked = false)
+    })
+  }
+  emits('tabChange', curTagList)
+}
+</script>
+
+<template>
+  <div class="body">
+    <div class="TagsBox">
+      <div
+        class="list"
+        v-for="(item, index) of props.tabList"
+        :key="item.name"
+        :class="[item.checked ? 'checked' : '']"
+        @click="handleTagToggle(index, item.checked)"
+      >
+        {{ item.name }}
+        <div
+          :class="[
+            'v-tag',
+            item.type && 'v-tag__' + item.type,
+            item.checked ? 'checked_color' : ''
+          ]"
+        >
+          {{ item.number }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.body {
+  margin-top: 8.73px;
+}
+.TagsBox {
+  display: flex;
+  flex-wrap: wrap;
+}
+.list {
+  // min-width: 93px;
+  padding: 5.42px 16px;
+  color: var(--color-neutral-2);
+  font-size: var(--font-size-3);
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: var(--border-radius-12);
+}
+.v-tag {
+  min-width: 18px;
+  font-size: var(--font-size-2);
+  margin-left: 8px;
+  border-radius: 6px;
+  padding: 3px 4px;
+  text-align: center;
+}
+.v-tag__all {
+  font-weight: 400;
+  background-color: var(--color-tag-all-bg-color);
+  color: #e26394;
+}
+.v-tag__english {
+  font-weight: 400;
+  background-color: var(--color-tag-created-bg);
+  color: var(--color-tag-created);
+}
+.v-tag__chinese-simplified {
+  font-weight: 400;
+  background-color: var(--color-tag-chinese-simplified-bg);
+  color: var(--color-tag-confirmed);
+}
+.v-tag__chinese-traditional {
+  font-weight: 400;
+  background-color: var(--color-tag-chinese-traditional-bg);
+  color: var(--color-tag-chinese-traditional);
+}
+
+.v-tag__french {
+  font-weight: 400;
+  background-color: var(--color-tag-cargo-received-bg);
+  color: var(--color-tag-cargo-received);
+}
+.v-tag__spanish {
+  font-weight: 400;
+  background-color: var(--color-tag-spanish-bg);
+  color: var(--color-tag-spanish);
+}
+.v-tag__portuguese {
+  font-weight: 400;
+  background-color: var(--color-tag-portuguese-bg);
+  color: var(--color-tag-portuguese-text-color);
+}
+.checked {
+  color: var(--color-theme);
+  font-weight: 700;
+  background: var(--color-tag-checked-bg);
+}
+.checked_color {
+  background-color: var(--color-tag-all-bg);
+  color: var(--color-tag-all);
+}
+</style>

+ 1 - 1
src/views/SystemSettings/src/components/MonitoringTable/src/MonitoringTable.vue

@@ -223,7 +223,7 @@ onMounted(() => {
               >{{ t('common.cancel') }}</el-button
             >
             <el-button style="width: 100px" type="warning" @click="deleteMoniTable(row)">
-              {{ t('common.ok') }}
+              OK
             </el-button>
           </div>
           <template #reference>

+ 9 - 9
src/views/TemplateManagement/src/components/CreateReportTemplate/src/CreateReportTemplate.vue

@@ -80,15 +80,15 @@ interface Field {
 const fieldsList = ref<Field[]>([])
 const levelOptions = [
   {
-    label: t('templateManagement.shipmentLevel'),
+    label: 'Shipment Level',
     value: 'Shipment Level'
   },
   {
-    label: t('templateManagement.containerLevel'),
+    label: 'Container Level',
     value: 'Container Level'
   },
   {
-    label: t('templateManagement.itemLevel'),
+    label: 'Item Level',
     value: 'Item Level'
   }
 ]
@@ -158,7 +158,9 @@ const handlePageSave = () => {
     !specificRoles.value.groupName?.length &&
     !specificRoles.value.systemAccount?.length
   ) {
-    ElMessage.warning(t('templateManagement.specificRolesRequired'))
+    ElMessage.warning(
+      t('templateManagement.specificRolesRequired')
+    )
     verified = false
   }
   if (!verified) {
@@ -183,10 +185,10 @@ const handlePageSave = () => {
     .saveNewReportTemplate({ ...data, serial_no })
     .then((res: any) => {
       if (res.code === 200) {
-        ElMessage.success(t('templateManagement.savedSuccessfully'))
+        ElMessage.success(t('templateManagement.createReportTemplate.savedSuccessfully'))
         router.push('/template-management')
       } else {
-        ElMessage.error(res.data.msg || t('templateManagement.saveFailed'))
+        ElMessage.error(res.data.msg || t('templateManagement.createReportTemplate.saveFailed'))
       }
     })
     .finally(() => {
@@ -286,9 +288,7 @@ const handlePageSave = () => {
                 <div class="radio-content">
                   <div class="top-options">
                     <p class="label">{{ t('templateManagement.specificRoles') }}</p>
-                    <p class="description">
-                      {{ t('templateManagement.specificRolesDescription') }}
-                    </p>
+                    <p class="description">{{ t('templateManagement.specificRolesDescription') }}</p>
                   </div>
                   <div
                     class="extended-filter"

+ 16 - 7
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/ReportFieldsConfiguration.vue

@@ -332,7 +332,8 @@ defineExpose({
     <div class="content-box">
       <div class="empty-box" v-if="fieldsList.length === 0">
         <el-button class="el-button--dark" @click="handleCustomizeColumns">
-          <span class="font_family icon-icon_add_b"></span>{{ t('templateManagement.addEditField') }}
+          <span class="font_family icon-icon_add_b"></span
+          >{{ t('templateManagement.addEditField') }}
         </el-button>
         <p>{{ t('templateManagement.noFieldSelectedTip') }}</p>
       </div>
@@ -455,9 +456,9 @@ defineExpose({
         @click="addNewFieldVisible = false"
         >{{ t('common.cancel') }}</el-button
       >
-      <el-button style="height: 40px; width: 120px" class="el-button--dark" @click="addNewField"
-        >{{ t('report.apply') }}</el-button
-      >
+      <el-button style="height: 40px; width: 120px" class="el-button--dark" @click="addNewField">{{
+        t('report.apply')
+      }}</el-button>
     </template>
   </el-dialog>
   <el-dialog
@@ -472,7 +473,7 @@ defineExpose({
         <div class="label">
           <div class="left-label">
             <span style="color: var(--color-danger)">*</span>
-              <span>{{ t('templateManagement.systemValueLabel') }}</span>
+            <span>{{ t('templateManagement.systemValueLabel') }}</span>
           </div>
           <div class="right-label">
             <span style="color: var(--color-danger)">*</span>
@@ -482,10 +483,18 @@ defineExpose({
         <div style="max-height: 380px; overflow-y: auto">
           <div class="list-item" v-for="(item, index) in mappingData" :key="index">
             <div class="left-system-value">
-              <el-input :placeholder="t('report.pleaseEnter')" clearable v-model="item.system"></el-input>
+              <el-input
+                :placeholder="t('report.pleaseEnter')"
+                clearable
+                v-model="item.system"
+              ></el-input>
             </div>
             <div class="right-converted-value">
-              <el-input :placeholder="t('report.pleaseEnter')" clearable v-model="item.converted"></el-input>
+              <el-input
+                :placeholder="t('report.pleaseEnter')"
+                clearable
+                v-model="item.converted"
+              ></el-input>
             </div>
             <div class="delete-icon">
               <span

+ 2 - 8
src/views/TemplateManagement/src/components/TableView/src/TableView.vue

@@ -111,12 +111,7 @@ const handleColumns = (columns: any) => {
 // 获取表格列
 const getTableColumns = async () => {
   tableData.value.columns = [
-    {
-      title: t('templateManagement.action'),
-      width: 120,
-      fixed: 'left',
-      slots: { default: 'action' }
-    },
+    { title: t('common.action'), width: 120, fixed: 'left', slots: { default: 'action' } },
     ...handleColumns(tableColumns)
   ]
   // tableRef.value && autoWidth(tableData.value, tableRef.value)
@@ -144,9 +139,8 @@ const assignTableData = (data: any) => {
   pageInfo.value.total = Number(data.rc) || 0
   isShowDeleteBtn.value = data?.isDelete || false
   const actionColumn = tableData.value.columns.find((item) => {
-    return item.title === t('templateManagement.action')
+    return item.title === 'Action'
   })
-  console.log('actionColumn', t('templateManagement.action'), isShowDeleteBtn.value)
   actionColumn.width = isShowDeleteBtn.value ? 150 : 120
   tableRef.value.loadColumn(tableData.value.columns || [])
 }