Browse Source

1、添加多语言支持

qmj 3 weeks ago
parent
commit
6c37472a7d

+ 2 - 1
.env.production

@@ -10,9 +10,10 @@ VITE_APP_BASE_API = '/prod-api'
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip
 
-//文件路径
+//测试信息
 //VITE_APP_FILE_PATH='https://loanapi.waimai-paotui.com'
 //VITE_APP_USER_APP_URL='https://loan.waimai-paotui.com'
+//正式信息
 VITE_APP_FILE_PATH='https://api.shoujida.com'
 //用户h5地址
 VITE_APP_USER_APP_URL='https://shoujida.com'

+ 10 - 10
src/api/menu.js

@@ -13,28 +13,28 @@ export const getRouters = () => {
                     icon:"peoples",
                     link: null,
                     noCache: false,
-                    title:"我的會員",
+                    title:"menu.myMember",
                 },
                 children: [
                     {
                         path: 'myuser',
                         component: 'agent/myuser',
                         name: 'myuser',
-                        meta: {title: '我的會員', icon: 'user', affix: true},
+                        meta: {title: 'menu.myMember', icon: 'user', affix: true},
                         children: []
                     },
                     {
                         path: 'agentLink',
                         component: 'agent/agentLink',
                         name: 'agentLink',
-                        meta: {title: '代理分享', icon: 'user', affix: true},
+                        meta: {title: 'menu.agentShare', icon: 'user', affix: true},
                         children: []
                     },
                     {
                         path: 'income',
                         component: 'agent/income',
                         name: 'income',
-                        meta: {title: '我的收益', icon: 'money', affix: false},
+                        meta: {title: 'menu.myIncome', icon: 'money', affix: false},
                         children: []
                     }
                 ]
@@ -48,7 +48,7 @@ export const getRouters = () => {
                     icon:"upload",
                     link: null,
                     noCache: false,
-                    title:"會員授權上傳",
+                    title:"menu.memberAuthUpload",
 
                 },
                 children: [
@@ -56,35 +56,35 @@ export const getRouters = () => {
                         path: 'authUp/userContact',
                         component: 'agent/userContact',
                         name: 'userContact',
-                        meta: {title: '會員聯絡人', icon: 'people', affix: false},
+                        meta: {title: 'menu.memberContact', icon: 'people', affix: false},
                         children: []
                     },
                     {
                         path: 'authUp/position',
                         component: 'agent/position',
                         name: 'position',
-                        meta: {title: '會員定位資訊', icon: 'monitor', affix: false},
+                        meta: {title: 'menu.memberPosition', icon: 'monitor', affix: false},
                         children: []
                     },
                     {
                         path: 'authUp/image',
                         component: 'agent/image',
                         name: 'image',
-                        meta: {title: '會員圖片', icon: 'eye', affix: false},
+                        meta: {title: 'menu.memberImage', icon: 'eye', affix: false},
                         children: []
                     },
                     {
                         path: 'authUp/call',
                         component: 'agent/call',
                         name: 'call',
-                        meta: {title: '通話記錄', icon: 'phone', affix: false},
+                        meta: {title: 'menu.callRecord', icon: 'phone', affix: false},
                         children: []
                     },
                     {
                         path: 'authUp/sms',
                         component: 'agent/sms',
                         name: 'sms',
-                        meta: {title: '簡訊記錄', icon: 'message', affix: false},
+                        meta: {title: 'menu.smsRecord', icon: 'message', affix: false},
                         children: []
                     },
                 ]

+ 23 - 3
src/components/Breadcrumb/index.vue

@@ -1,9 +1,9 @@
 <template>
   <el-breadcrumb class="app-breadcrumb" separator="/">
     <transition-group name="breadcrumb">
-      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
-        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span>
-        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      <el-breadcrumb-item v-for="(item, index) in levelList" :key="`${item.path}-${locale.value}`">
+        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ getMenuTitle(item.meta.title) }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ getMenuTitle(item.meta.title) }}</a>
       </el-breadcrumb-item>
     </transition-group>
   </el-breadcrumb>
@@ -11,10 +11,12 @@
 
 <script setup>
 import usePermissionStore from '@/store/modules/permission'
+import { useI18n } from '@/composables/useI18n'
 
 const route = useRoute()
 const router = useRouter()
 const permissionStore = usePermissionStore()
+const { t, locale } = useI18n()
 const levelList = ref([])
 
 function getBreadcrumb() {
@@ -73,11 +75,29 @@ function handleLink(item) {
   router.push(path)
 }
 
+// 获取菜单标题,响应语言变化(参考 ruoyi-ui 的 generateTitle 实现)
+function getMenuTitle(title) {
+  if (!title) return ''
+  
+  // 访问响应式的 locale.value 来建立依赖关系
+  // 当 locale 变化时,面包屑会重新渲染
+  const currentLocale = locale.value
+  
+  // 如果是 i18n key(包含点号),使用翻译
+  if (title.includes('.')) {
+    return t(title)
+  }
+  // 否则直接返回原文本
+  return title
+}
+
 watchEffect(() => {
   // if you go to the redirect page, do not update the breadcrumbs
   if (route.path.startsWith('/redirect/')) {
     return
   }
+  // 访问 locale.value 来建立响应式依赖,当语言变化时重新获取面包屑
+  const currentLocale = locale.value
   getBreadcrumb()
 })
 getBreadcrumb()

+ 92 - 0
src/components/LangSelect/index.vue

@@ -0,0 +1,92 @@
+<template>
+  <el-select 
+    v-model="currentLocale" 
+    @change="handleChange" 
+    :size="size"
+    :style="style"
+    :placeholder="t('common.language')"
+  >
+    <el-option label="繁體中文" value="zh-TW" />
+    <el-option label="Tiếng Việt" value="vi" />
+  </el-select>
+</template>
+
+<script setup>
+import { ref, onMounted, watch, nextTick } from 'vue'
+import { useI18n } from '@/composables/useI18n'
+import { useDynamicTitle } from '@/utils/dynamicTitle'
+
+const props = defineProps({
+  size: {
+    type: String,
+    default: 'default'
+  },
+  style: {
+    type: Object,
+    default: () => ({})
+  },
+  reload: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits(['language-changed'])
+
+const { locale, setLocale, t } = useI18n()
+const currentLocale = ref(locale.value)
+
+onMounted(() => {
+  currentLocale.value = locale.value
+})
+
+// 监听语言变化
+watch(locale, (newLocale) => {
+  currentLocale.value = newLocale
+})
+
+const handleChange = (value) => {
+  setLocale(value)
+  currentLocale.value = value
+  // 更新页面标题
+  useDynamicTitle()
+  // 触发语言切换事件(参考 ruoyi-ui 的实现)
+  nextTick(() => {
+    emit('language-changed', value)
+  })
+  // 如果需要刷新页面以应用语言更改(默认不刷新,使用响应式更新)
+  if (props.reload) {
+    window.location.reload()
+  }
+}
+</script>
+
+<style scoped>
+:deep(.el-select) {
+  width: 100%;
+  
+  .el-input__wrapper {
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    box-shadow: none;
+    transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+    
+    &:hover {
+      border-color: #c0c4cc;
+    }
+    
+    &.is-focus {
+      border-color: #409eff;
+    }
+  }
+  
+  &:hover .el-input__wrapper {
+    border-color: #c0c4cc;
+  }
+  
+  .el-input__inner {
+    color: #606266;
+  }
+}
+</style>
+

+ 17 - 7
src/components/TopNav/index.vue

@@ -10,13 +10,13 @@
         <svg-icon
         v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
         :icon-class="item.meta.icon"/>
-        {{ item.meta.title }}
+        {{ getMenuTitle(item.meta.title) }}
       </el-menu-item>
     </template>
 
     <!-- 顶部菜单超出数量折叠 -->
     <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
-      <template #title>更多菜单</template>
+      <template #title>{{ t('menu.moreMenu') }}</template>
       <template v-for="(item, index) in topMenus">
         <el-menu-item
           :index="item.path"
@@ -25,7 +25,7 @@
         <svg-icon
           v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
           :icon-class="item.meta.icon"/>
-        {{ item.meta.title }}
+        {{ getMenuTitle(item.meta.title) }}
         </el-menu-item>
       </template>
     </el-sub-menu>
@@ -38,6 +38,9 @@ import { isHttp } from '@/utils/validate'
 import useAppStore from '@/store/modules/app'
 import useSettingsStore from '@/store/modules/settings'
 import usePermissionStore from '@/store/modules/permission'
+import { useI18n } from '@/composables/useI18n'
+
+const { t } = useI18n()
 
 // 顶部栏初始数
 const visibleNumber = ref(null)
@@ -157,17 +160,24 @@ function activeRoutes(key) {
   return routes
 }
 
+function getMenuTitle(title) {
+  if (!title) return ''
+  // 如果是 i18n key(包含点号),使用翻译
+  if (title.includes('.')) {
+    return t(title)
+  }
+  // 否则直接返回原文本
+  return title
+}
+
 onMounted(() => {
   window.addEventListener('resize', setVisibleNumber)
+  setVisibleNumber()
 })
 
 onBeforeUnmount(() => {
   window.removeEventListener('resize', setVisibleNumber)
 })
-
-onMounted(() => {
-  setVisibleNumber()
-})
 </script>
 
 <style lang="scss">

+ 28 - 0
src/composables/useI18n.js

@@ -0,0 +1,28 @@
+import { ref, watch } from 'vue'
+import i18n from '@/i18n'
+
+// 创建全局共享的 locale ref(确保所有组件使用同一个实例)
+const globalLocale = ref(i18n.getLocale())
+
+// 监听 localStorage 变化(用于跨标签页同步)
+if (typeof window !== 'undefined') {
+  window.addEventListener('storage', (e) => {
+    if (e.key === 'locale') {
+      globalLocale.value = e.newValue || 'zh-TW'
+      i18n.setLocale(globalLocale.value)
+    }
+  })
+}
+
+// 使用 i18n 的 composable
+export function useI18n() {
+  return {
+    t: (key, params) => i18n.t(key, params),
+    locale: globalLocale, // 返回全局共享的 ref
+    setLocale: (newLocale) => {
+      i18n.setLocale(newLocale)
+      globalLocale.value = newLocale
+    }
+  }
+}
+

+ 70 - 0
src/i18n/index.js

@@ -0,0 +1,70 @@
+import zhTW from './locales/zh-TW.js'
+import vi from './locales/vi.js'
+
+// 语言配置
+const messages = {
+  'zh-TW': zhTW,
+  'vi': vi
+}
+
+// 默认语言
+const defaultLocale = 'zh-TW'
+
+// 获取当前语言
+const getLocale = () => {
+  return localStorage.getItem('locale') || defaultLocale
+}
+
+// 设置语言
+const setLocale = (locale) => {
+  localStorage.setItem('locale', locale)
+}
+
+// i18n 实例
+class I18n {
+  constructor() {
+    this.locale = getLocale()
+  }
+
+  // 获取翻译
+  t(key, params = {}) {
+    const keys = key.split('.')
+    let value = messages[this.locale]
+    
+    for (const k of keys) {
+      if (value && typeof value === 'object') {
+        value = value[k]
+      } else {
+        return key
+      }
+    }
+    
+    if (typeof value === 'string') {
+      // 简单的参数替换
+      return value.replace(/\{(\w+)\}/g, (match, key) => {
+        return params[key] !== undefined ? params[key] : match
+      })
+    }
+    
+    return value || key
+  }
+
+  // 设置语言
+  setLocale(locale) {
+    if (messages[locale]) {
+      this.locale = locale
+      setLocale(locale)
+    }
+  }
+
+  // 获取当前语言
+  getLocale() {
+    return this.locale
+  }
+}
+
+// 创建全局实例
+const i18n = new I18n()
+
+export default i18n
+

+ 190 - 0
src/i18n/locales/vi.js

@@ -0,0 +1,190 @@
+// 越南语语言包
+export default {
+  // 通用
+  common: {
+    search: 'Tìm kiếm',
+    reset: 'Đặt lại',
+    cancel: 'Hủy',
+    confirm: 'Xác nhận',
+    view: 'Xem',
+    operation: 'Thao tác',
+    expandCollapse: 'Mở rộng/Thu gọn',
+    userId: 'ID Người dùng',
+    userName: 'Người dùng thuộc',
+    userContact: 'Tên liên hệ',
+    userPhone: 'Số điện thoại liên hệ',
+    createTime: 'Thời gian tạo',
+    pleaseEnter: 'Vui lòng nhập',
+    unknown: 'Người dùng không xác định',
+    language: 'Ngôn ngữ'
+  },
+  // 登录页
+  login: {
+    title: 'Hệ thống quản lý đại lý',
+    username: 'Tài khoản',
+    password: 'Mật khẩu',
+    code: 'Mã xác minh',
+    rememberMe: 'Ghi nhớ mật khẩu',
+    login: 'Đăng nhập',
+    logging: 'Đang đăng nhập...',
+    pleaseEnterUsername: 'Vui lòng nhập tài khoản',
+    pleaseEnterPassword: 'Vui lòng nhập mật khẩu',
+    pleaseEnterCode: 'Vui lòng nhập mã xác minh',
+    logout: 'Đăng xuất',
+    logoutConfirm: 'Bạn có chắc chắn muốn đăng xuất không?',
+    tip: 'Thông báo'
+  },
+  // 通话记录
+  call: {
+    title: 'Lịch sử cuộc gọi',
+    phoneNumber: 'Số điện thoại',
+    callType: 'Loại cuộc gọi',
+    startTime: 'Thời gian bắt đầu',
+    duration: 'Thời lượng',
+    contactName: 'Tên danh bạ',
+    viewTitle: 'Xem thông tin cuộc gọi',
+    incoming: 'Cuộc gọi đến',
+    outgoing: 'Cuộc gọi đi',
+    missed: 'Cuộc gọi nhỡ',
+    hangup: 'Cúp máy',
+    pleaseEnterPhoneNumber: 'Vui lòng nhập số điện thoại',
+    pleaseEnterStartTime: 'Vui lòng nhập thời gian bắt đầu',
+    pleaseEnterDuration: 'Vui lòng nhập thời lượng',
+    pleaseEnterContactName: 'Vui lòng nhập tên danh bạ'
+  },
+  // 联系人
+  contact: {
+    title: 'Liên hệ thành viên',
+    viewTitle: 'Xem liên hệ',
+    pleaseEnterContactName: 'Vui lòng nhập tên liên hệ'
+  },
+  // 短信
+  sms: {
+    title: 'Lịch sử tin nhắn',
+    smsType: 'Loại tin nhắn',
+    phoneNumber: 'Số điện thoại đối phương',
+    content: 'Nội dung tin nhắn',
+    smsTime: 'Thời gian tin nhắn',
+    viewTitle: 'Xem thông tin tin nhắn',
+    receive: 'Nhận',
+    send: 'Gửi',
+    pleaseEnterPhoneNumber: 'Vui lòng nhập số điện thoại đối phương',
+    pleaseEnterSmsTime: 'Vui lòng nhập thời gian tin nhắn'
+  },
+  // 图片
+  image: {
+    title: 'Hình ảnh thành viên',
+    image: 'Hình ảnh',
+    imageTime: 'Thời gian hình ảnh',
+    viewTitle: 'Xem thông tin hình ảnh',
+    pleaseEnterImageTime: 'Vui lòng nhập thời gian hình ảnh'
+  },
+  // 定位
+  position: {
+    title: 'Thông tin vị trí thành viên',
+    longitude: 'Kinh độ',
+    latitude: 'Vĩ độ',
+    notes: 'Ghi chú',
+    viewTitle: 'Xem thông tin vị trí',
+    pleaseEnterLongitude: 'Vui lòng nhập kinh độ',
+    pleaseEnterLatitude: 'Vui lòng nhập vĩ độ',
+    pleaseEnterNotes: 'Vui lòng nhập ghi chú'
+  },
+  // 菜单
+  menu: {
+    myMember: 'Thành viên của tôi',
+    agentShare: 'Chia sẻ đại lý',
+    myIncome: 'Thu nhập của tôi',
+    memberAuthUpload: 'Tải lên ủy quyền thành viên',
+    memberContact: 'Liên hệ thành viên',
+    memberPosition: 'Thông tin vị trí thành viên',
+    memberImage: 'Hình ảnh thành viên',
+    callRecord: 'Lịch sử cuộc gọi',
+    smsRecord: 'Lịch sử tin nhắn',
+    moreMenu: 'Thêm menu'
+  },
+  // 代理分享链接
+  agentLink: {
+    title: 'Liên kết chia sẻ đại lý',
+    yourShareLink: 'Liên kết chia sẻ của bạn:',
+    copyLink: 'Sao chép liên kết',
+    tip: 'Thông báo',
+    tipContent: 'Sao chép liên kết này và gửi cho người khác, họ có thể đăng ký làm thành viên của bạn thông qua liên kết này.',
+    linkGenerateFailed: 'Tạo liên kết chia sẻ thất bại, vui lòng thử lại sau',
+    linkCopied: 'Liên kết đã được sao chép vào clipboard, bạn có thể dán và gửi cho người khác',
+    copyFailed: 'Sao chép thất bại, vui lòng sao chép liên kết thủ công',
+    getUserInfoFailed: 'Lấy thông tin người dùng thất bại'
+  },
+  // 我的会员
+  myuser: {
+    username: 'Tên người dùng',
+    pleaseEnterUsername: 'Vui lòng nhập tên người dùng',
+    phone: 'Số điện thoại',
+    pleaseEnterPhone: 'Vui lòng nhập số điện thoại',
+    statusNormal: 'Bình thường',
+    statusDisable: 'Vô hiệu hóa',
+    userId: 'ID Người dùng',
+    userName: 'Tên người dùng',
+    nickname: 'Biệt danh người dùng',
+    avatar: 'Ảnh đại diện',
+    contactUploadStatus: 'Trạng thái tải lên danh bạ',
+    smsUploadStatus: 'Trạng thái tải lên tin nhắn',
+    callUploadStatus: 'Trạng thái tải lên lịch sử cuộc gọi',
+    positionUploadStatus: 'Trạng thái tải lên vị trí người dùng',
+    imageUploadStatus: 'Trạng thái tải lên hình ảnh người dùng',
+    agent: 'Đại lý',
+    yes: 'Có',
+    no: 'Không',
+    agentId: 'ID Đại lý thuộc',
+    status: 'Trạng thái',
+    createTime: 'Thời gian tạo',
+    operation: 'Thao tác',
+    view: 'Xem',
+    userInfo: 'Tên người dùng',
+    pleaseEnterUserInfo: 'Vui lòng nhập tên người dùng',
+    userNickname: 'Biệt danh người dùng',
+    pleaseEnterUserNickname: 'Vui lòng nhập biệt danh người dùng',
+    phoneNumber: 'Số điện thoại',
+    pleaseEnterPhoneNumber: 'Vui lòng nhập số điện thoại',
+    uploaded: 'Đã tải lên',
+    rejected: 'Từ chối',
+    addMember: 'Thêm quản lý thành viên',
+    editMember: 'Chỉnh sửa quản lý thành viên',
+    viewMember: 'Xem quản lý thành viên',
+    usernameRequired: 'Tên người dùng không được để trống',
+    nicknameRequired: 'Biệt danh người dùng không được để trống',
+    passwordRequired: 'Mật khẩu không được để trống',
+    phoneRequired: 'Số điện thoại không được để trống',
+    statusRequired: 'Trạng thái không được để trống',
+    agentRequired: 'Đại lý không được để trống',
+    adminPasswordRequired: 'Mật khẩu quản trị viên không được để trống',
+    deleteConfirm: 'Bạn có chắc chắn muốn xóa dữ liệu quản lý thành viên có số {id} không?',
+    deleteSuccess: 'Xóa thành công',
+    resetPasswordPrompt: 'Vui lòng nhập mật khẩu mới cho {username}',
+    passwordLengthError: 'Độ dài mật khẩu người dùng phải từ 5 đến 20 ký tự',
+    invalidChars: 'Không được chứa ký tự không hợp lệ: < > " \' \\ |',
+    resetPasswordConfirm: 'Bạn có chắc chắn muốn đặt lại mật khẩu thành viên thành 123456 không?',
+    resetPasswordSuccess: 'Đặt lại thành công'
+  },
+  // 我的收益
+  income: {
+    id: 'ID',
+    loanId: 'ID khoản vay',
+    pleaseEnterLoanId: 'Vui lòng nhập ID khoản vay',
+    userId: 'ID người dùng',
+    pleaseEnterUserId: 'Vui lòng nhập ID người dùng',
+    username: 'Tên người dùng',
+    pleaseEnterUsername: 'Vui lòng nhập tên người dùng',
+    loanAmount: 'Số tiền vay',
+    pleaseEnterLoanAmount: 'Vui lòng nhập số tiền vay',
+    income: 'Thu nhập',
+    pleaseEnterIncome: 'Vui lòng nhập thu nhập',
+    createTime: 'Thời gian tạo',
+    operation: 'Thao tác',
+    view: 'Xem',
+    viewIncome: 'Xem thu nhập',
+    agentId: 'ID đại lý',
+    pleaseEnterAgentId: 'Vui lòng nhập ID đại lý'
+  }
+}
+

+ 190 - 0
src/i18n/locales/zh-TW.js

@@ -0,0 +1,190 @@
+// 繁体中文语言包
+export default {
+  // 通用
+  common: {
+    search: '搜尋',
+    reset: '重設',
+    cancel: '取 消',
+    confirm: '確 定',
+    view: '查看',
+    operation: '操作',
+    expandCollapse: '展開/折疊',
+    userId: '用戶ID',
+    userName: '所屬用戶',
+    userContact: '聯絡人名稱',
+    userPhone: '聯絡電話',
+    createTime: '創建時間',
+    pleaseEnter: '請輸入',
+    unknown: '未知用户',
+    language: '語言'
+  },
+  // 登录页
+  login: {
+    title: '代理管理系統',
+    username: '賬號',
+    password: '密碼',
+    code: '驗證碼',
+    rememberMe: '記住密碼',
+    login: '登 錄',
+    logging: '登 錄 中...',
+    pleaseEnterUsername: '請輸入您的賬號',
+    pleaseEnterPassword: '請輸入您的密碼',
+    pleaseEnterCode: '請輸入驗證碼',
+    logout: '退出登錄',
+    logoutConfirm: '確定註銷並退出系統嗎?',
+    tip: '提示'
+  },
+  // 通话记录
+  call: {
+    title: '通話記錄',
+    phoneNumber: '電話號碼',
+    callType: '通話類型',
+    startTime: '通話開始時間',
+    duration: '通話時長',
+    contactName: '通訊錄名稱',
+    viewTitle: '查看通話資訊',
+    incoming: '呼入',
+    outgoing: '呼出',
+    missed: '未接',
+    hangup: '挂断',
+    pleaseEnterPhoneNumber: '請輸入電話號碼',
+    pleaseEnterStartTime: '請輸入通話開始時間',
+    pleaseEnterDuration: '請輸入通話時長',
+    pleaseEnterContactName: '請輸入通訊錄名稱'
+  },
+  // 联系人
+  contact: {
+    title: '會員聯絡人',
+    viewTitle: '查看聯絡人',
+    pleaseEnterContactName: '請輸入聯絡人名稱'
+  },
+  // 短信
+  sms: {
+    title: '簡訊記錄',
+    smsType: '簡訊類型',
+    phoneNumber: '對方號碼',
+    content: '簡訊內容',
+    smsTime: '簡訊時間',
+    viewTitle: '查看簡訊資訊',
+    receive: '接收',
+    send: '發送',
+    pleaseEnterPhoneNumber: '請輸入對方號碼',
+    pleaseEnterSmsTime: '請輸入簡訊時間'
+  },
+  // 图片
+  image: {
+    title: '會員圖片',
+    image: '圖片',
+    imageTime: '圖片時間',
+    viewTitle: '查看圖片資訊',
+    pleaseEnterImageTime: '請輸入圖片時間'
+  },
+  // 定位
+  position: {
+    title: '會員定位資訊',
+    longitude: '經度',
+    latitude: '緯度',
+    notes: '備註',
+    viewTitle: '查看定位資訊',
+    pleaseEnterLongitude: '請輸入經度',
+    pleaseEnterLatitude: '請輸入緯度',
+    pleaseEnterNotes: '請輸入備註'
+  },
+  // 菜单
+  menu: {
+    myMember: '我的會員',
+    agentShare: '代理分享',
+    myIncome: '我的收益',
+    memberAuthUpload: '會員授權上傳',
+    memberContact: '會員聯絡人',
+    memberPosition: '會員定位資訊',
+    memberImage: '會員圖片',
+    callRecord: '通話記錄',
+    smsRecord: '簡訊記錄',
+    moreMenu: '更多菜單'
+  },
+  // 代理分享链接
+  agentLink: {
+    title: '代理分享連結',
+    yourShareLink: '您的分享連結:',
+    copyLink: '複製連結',
+    tip: '提示',
+    tipContent: '複製此連結發送給其他人,他們可以通過此連結註冊成為您的會員。',
+    linkGenerateFailed: '分享連結生成失敗,請稍後再試',
+    linkCopied: '連結已複製到剪貼板,可以貼上發送給其他人了',
+    copyFailed: '複製失敗,請手動複製連結',
+    getUserInfoFailed: '獲取用戶資訊失敗'
+  },
+  // 我的会员
+  myuser: {
+    username: '用戶名稱',
+    pleaseEnterUsername: '請輸入用戶名稱',
+    phone: '手機號碼',
+    pleaseEnterPhone: '請輸入手機號碼',
+    statusNormal: '正常',
+    statusDisable: '停用',
+    userId: '使用者ID',
+    userName: '使用者名稱',
+    nickname: '使用者暱稱',
+    avatar: '頭像',
+    contactUploadStatus: '通訊錄上傳狀態',
+    smsUploadStatus: '簡訊上傳狀態',
+    callUploadStatus: '通話記錄上傳狀態',
+    positionUploadStatus: '用户定位上傳狀態',
+    imageUploadStatus: '用户圖片上傳狀態',
+    agent: '代理',
+    yes: '是',
+    no: '否',
+    agentId: '所属代理ID',
+    status: '狀態',
+    createTime: '建立時間',
+    operation: '操作',
+    view: '查看',
+    userInfo: '用户名称',
+    pleaseEnterUserInfo: '请输入用户名称',
+    userNickname: '用户昵称',
+    pleaseEnterUserNickname: '请输入用户昵称',
+    phoneNumber: '手机号码',
+    pleaseEnterPhoneNumber: '请输入手机号码',
+    uploaded: '已上傳',
+    rejected: '拒絕',
+    addMember: '新增會員管理',
+    editMember: '修改會員管理',
+    viewMember: '查看會員管理',
+    usernameRequired: '使用者名稱不能為空',
+    nicknameRequired: '使用者暱稱不能為空',
+    passwordRequired: '密碼不能為空',
+    phoneRequired: '手機號碼不能為空',
+    statusRequired: '狀態不能為空',
+    agentRequired: '代理不能為空',
+    adminPasswordRequired: '管理員密碼不能為空',
+    deleteConfirm: '是否確認刪除會員管理編號為{id}的資料項?',
+    deleteSuccess: '刪除成功',
+    resetPasswordPrompt: '請輸入{username}的新密碼',
+    passwordLengthError: '使用者密碼長度必須介於 5 和 20 之間',
+    invalidChars: '不能包含非法字元:< > " \' \\ |',
+    resetPasswordConfirm: '是否確認重置會員密碼為123456?',
+    resetPasswordSuccess: '重置成功'
+  },
+  // 我的收益
+  income: {
+    id: 'ID',
+    loanId: '借款id',
+    pleaseEnterLoanId: '請輸入借款id',
+    userId: '用戶id',
+    pleaseEnterUserId: '請輸入用戶id',
+    username: '用戶名稱',
+    pleaseEnterUsername: '請輸入用戶名稱',
+    loanAmount: '借款金額',
+    pleaseEnterLoanAmount: '請輸入借款金額',
+    income: '收益',
+    pleaseEnterIncome: '請輸入收益',
+    createTime: '創建時間',
+    operation: '操作',
+    view: '查看',
+    viewIncome: '查看收益',
+    agentId: '代理id',
+    pleaseEnterAgentId: '請輸入代理id'
+  }
+}
+

+ 26 - 4
src/layout/components/Navbar.vue

@@ -18,6 +18,12 @@
 
         <screenfull id="screenfull" class="right-menu-item hover-effect" />
 
+        <el-tooltip :content="t('common.language')" effect="dark" placement="bottom">
+          <div class="right-menu-item hover-effect lang-select-wrapper">
+            <lang-select :reload="false" size="small" style="width: 120px;" @language-changed="handleLanguageChanged" />
+          </div>
+        </el-tooltip>
+
         <el-tooltip content="主题模式" effect="dark" placement="bottom">
           <div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme">
             <svg-icon v-if="settingsStore.isDark" icon-class="sunny" />
@@ -43,7 +49,7 @@
         <template #dropdown>
           <el-dropdown-menu>
             <el-dropdown-item divided command="logout" class="logout-item">
-              <span class="logout-text">退出登录</span>
+              <span class="logout-text">{{ t('login.logout') }}</span>
             </el-dropdown-item>
           </el-dropdown-menu>
         </template>
@@ -62,13 +68,17 @@ import SizeSelect from '@/components/SizeSelect'
 import HeaderSearch from '@/components/HeaderSearch'
 import RuoYiGit from '@/components/RuoYi/Git'
 import RuoYiDoc from '@/components/RuoYi/Doc'
+import LangSelect from '@/components/LangSelect'
 import useAppStore from '@/store/modules/app'
 import useUserStore from '@/store/modules/user'
 import useSettingsStore from '@/store/modules/settings'
 import usePermissionStore from '@/store/modules/permission'
 import useTagsViewStore from '@/store/modules/tagsView'
+import { useI18n } from '@/composables/useI18n'
 import router from '@/router'
 
+const { t } = useI18n()
+
 const appStore = useAppStore()
 const userStore = useUserStore()
 const settingsStore = useSettingsStore()
@@ -93,9 +103,9 @@ function handleCommand(command) {
 }
 
 function logout() {
-  ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
+  ElMessageBox.confirm(t('login.logoutConfirm'), t('login.tip'), {
+    confirmButtonText: t('common.confirm'),
+    cancelButtonText: t('common.cancel'),
     type: 'warning'
   }).then(() => {
     userStore.logOut().then(() => {
@@ -136,6 +146,12 @@ function setLayout() {
 function toggleTheme() {
   settingsStore.toggleTheme()
 }
+
+// 处理语言切换事件(参考 ruoyi-ui 的实现)
+function handleLanguageChanged(lang) {
+  // 语言切换时的处理,可以在这里添加额外的逻辑
+  // 例如刷新页面数据等
+}
 </script>
 
 <style lang='scss' scoped>
@@ -212,6 +228,12 @@ function toggleTheme() {
           }
         }
       }
+
+      &.lang-select-wrapper {
+        display: flex;
+        align-items: center;
+        padding: 0 12px;
+      }
     }
 
     .logout-wrapper {

+ 8 - 1
src/layout/components/Sidebar/Logo.vue

@@ -14,9 +14,11 @@
 </template>
 
 <script setup>
+import { computed } from 'vue'
 import logo from '@/assets/logo/logo.png'
 import useSettingsStore from '@/store/modules/settings'
 import variables from '@/assets/styles/variables.module.scss'
+import { useI18n } from '@/composables/useI18n'
 
 defineProps({
   collapse: {
@@ -25,7 +27,12 @@ defineProps({
   }
 })
 
-const title = import.meta.env.VITE_APP_TITLE
+const { t, locale } = useI18n()
+const title = computed(() => {
+  // 访问 locale.value 来建立响应式依赖,确保语言切换时重新计算
+  const currentLocale = locale.value
+  return t('login.title')
+})
 const settingsStore = useSettingsStore()
 const sideTheme = computed(() => settingsStore.sideTheme)
 

+ 31 - 11
src/layout/components/Sidebar/SidebarItem.vue

@@ -4,7 +4,7 @@
       <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
         <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
           <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
-          <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
+          <template #title><span class="menu-title" :title="hasTitle(getMenuTitleComputed(onlyOneChild.meta.title))">{{ getMenuTitleComputed(onlyOneChild.meta.title) }}</span></template>
         </el-menu-item>
       </app-link>
     </template>
@@ -12,25 +12,29 @@
     <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
       <template v-if="item.meta" #title>
         <svg-icon :icon-class="item.meta && item.meta.icon" />
-        <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
+        <span class="menu-title" :title="hasTitle(getMenuTitleComputed(item.meta.title))">{{ getMenuTitleComputed(item.meta.title) }}</span>
       </template>
 
-      <sidebar-item
-        v-for="(child, index) in item.children"
-        :key="child.path + index"
-        :is-nest="true"
-        :item="child"
-        :base-path="resolvePath(child.path)"
-        class="nest-menu"
-      />
+        <sidebar-item
+          v-for="(child, index) in item.children"
+          :key="`${child.path}-${index}-${locale.value}`"
+          :is-nest="true"
+          :item="child"
+          :base-path="resolvePath(child.path)"
+          class="nest-menu"
+        />
     </el-sub-menu>
   </div>
 </template>
 
 <script setup>
+import { computed, watchEffect } from 'vue'
 import { isExternal } from '@/utils/validate'
 import AppLink from './Link'
 import { getNormalPath } from '@/utils/ruoyi'
+import { useI18n } from '@/composables/useI18n'
+
+const { t, locale } = useI18n()
 
 const props = defineProps({
   // route object
@@ -91,10 +95,26 @@ function resolvePath(routePath, routeQuery) {
 }
 
 function hasTitle(title){
-  if (title.length > 5) {
+  if (title && title.length > 5) {
     return title
   } else {
     return ""
   }
 }
+
+// 获取菜单标题,响应语言变化(参考 ruoyi-ui 的 generateTitle 实现)
+function getMenuTitleComputed(title) {
+  if (!title) return ''
+  
+  // 访问响应式的 locale.value 来建立依赖关系
+  // 当 locale 变化时,由于 key 的变化会强制重新渲染,这个函数会被重新执行
+  const currentLocale = locale.value
+  
+  // 如果是 i18n key(包含点号),使用翻译
+  if (title.includes('.')) {
+    return t(title)
+  }
+  // 否则直接返回原文本
+  return title
+}
 </script>

+ 14 - 2
src/layout/components/Sidebar/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div :class="{ 'has-logo': showLogo }" class="sidebar-container">
-    <logo v-if="showLogo" :collapse="isCollapse" />
+      <logo v-if="showLogo" :collapse="isCollapse" :key="`logo-${localeKey}`" />
     <el-scrollbar wrap-class="scrollbar-wrapper">
       <el-menu
         :default-active="activeMenu"
@@ -15,7 +15,7 @@
       >
         <sidebar-item
           v-for="(route, index) in sidebarRouters"
-          :key="route.path + index"
+          :key="`${route.path}-${index}-${localeKey}`"
           :item="route"
           :base-path="route.path"
         />
@@ -25,17 +25,29 @@
 </template>
 
 <script setup>
+import { watch, ref, computed } from 'vue'
 import Logo from './Logo'
 import SidebarItem from './SidebarItem'
 import variables from '@/assets/styles/variables.module.scss'
 import useAppStore from '@/store/modules/app'
 import useSettingsStore from '@/store/modules/settings'
 import usePermissionStore from '@/store/modules/permission'
+import { useI18n } from '@/composables/useI18n'
 
 const route = useRoute()
 const appStore = useAppStore()
 const settingsStore = useSettingsStore()
 const permissionStore = usePermissionStore()
+const { locale } = useI18n()
+
+// 使用 locale 作为 key 的一部分,当语言变化时强制重新渲染菜单
+const localeKey = computed(() => locale.value)
+
+// 监听语言变化,强制更新菜单(参考 ruoyi-ui 的实现:watch '$i18n.locale' 并调用 $forceUpdate)
+watch(locale, () => {
+  // 语言切换时,通过改变 localeKey 来强制重新渲染菜单
+  // 由于 localeKey 是 computed,会自动更新
+}, { immediate: false })
 
 const sidebarRouters = computed(() => permissionStore.sidebarRouters)
 const showLogo = computed(() => settingsStore.sidebarLogo)

+ 20 - 2
src/layout/components/TagsView/index.vue

@@ -3,7 +3,7 @@
     <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
       <router-link
         v-for="tag in visitedViews"
-        :key="tag.path"
+        :key="`${tag.path}-${locale.value}`"
         :data-path="tag.path"
         :class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
         :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
@@ -13,7 +13,7 @@
         @contextmenu.prevent="openMenu(tag, $event)"
       >
         <svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" />
-        {{ tag.title }}
+        {{ getMenuTitle(tag.title || tag.meta.title) }}
         <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
           <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
         </span>
@@ -48,6 +48,9 @@ import { getNormalPath } from '@/utils/ruoyi'
 import useTagsViewStore from '@/store/modules/tagsView'
 import useSettingsStore from '@/store/modules/settings'
 import usePermissionStore from '@/store/modules/permission'
+import { useI18n } from '@/composables/useI18n'
+
+const { t, locale } = useI18n()
 
 const visible = ref(false)
 const top = ref(0)
@@ -257,6 +260,21 @@ function closeMenu() {
 function handleScroll() {
   closeMenu()
 }
+
+function getMenuTitle(title) {
+  if (!title) return ''
+  
+  // 访问响应式的 locale.value 来建立依赖关系
+  // 当 locale 变化时,由于 key 的变化会强制重新渲染,这个函数会被重新执行
+  const currentLocale = locale.value
+  
+  // 如果是 i18n key(包含点号),使用翻译
+  if (title.includes('.')) {
+    return t(title)
+  }
+  // 否则直接返回原文本
+  return title
+}
 </script>
 
 <style lang="scss" scoped>

+ 6 - 1
src/permission.js

@@ -6,6 +6,7 @@ import { isPathMatch } from '@/utils/validate'
 import useSettingsStore from '@/store/modules/settings'
 import usePermissionStore from '@/store/modules/permission'
 import useUserStore from '@/store/modules/user'
+import i18n from '@/i18n'
 
 NProgress.configure({ showSpinner: false })
 
@@ -18,7 +19,11 @@ const isWhiteList = (path) => {
 router.beforeEach(async (to, from, next) => {
   NProgress.start()
   if (getToken()) {
-    to.meta.title && useSettingsStore().setTitle(to.meta.title)
+    if (to.meta.title) {
+      // 如果是 i18n key(包含点号),使用翻译
+      const title = to.meta.title.includes('.') ? i18n.t(to.meta.title) : to.meta.title
+      useSettingsStore().setTitle(title)
+    }
     /* has token*/
     const userStore = useUserStore()
     // 如果还没有用户信息,先获取用户信息

+ 4 - 2
src/utils/dynamicTitle.js

@@ -1,14 +1,16 @@
 import defaultSettings from '@/settings'
 import useSettingsStore from '@/store/modules/settings'
+import i18n from '@/i18n'
 
 /**
  * 动态修改标题
  */
 export function useDynamicTitle() {
   const settingsStore = useSettingsStore()
+  const systemTitle = i18n.t('login.title')
   if (settingsStore.dynamicTitle) {
-    document.title = settingsStore.title + ' - ' + defaultSettings.title
+    document.title = settingsStore.title + ' - ' + systemTitle
   } else {
-    document.title = defaultSettings.title
+    document.title = systemTitle
   }
 }

+ 15 - 12
src/views/agent/agentLink.vue

@@ -3,11 +3,11 @@
     <el-card class="box-card">
       <template #header>
         <div class="card-header">
-          <span>代理分享連結</span>
+          <span>{{ t('agentLink.title') }}</span>
         </div>
       </template>
       <div class="share-link-container">
-        <div class="link-label">您的分享連結:</div>
+        <div class="link-label">{{ t('agentLink.yourShareLink') }}</div>
         <div class="link-content">
           <el-input
             v-model="shareLink"
@@ -20,20 +20,20 @@
                 :icon="DocumentCopy"
                 @click="handleCopy"
               >
-                複製連結
+                {{ t('agentLink.copyLink') }}
               </el-button>
             </template>
           </el-input>
         </div>
         <div class="link-tip">
           <el-alert
-            title="提示"
+            :title="t('agentLink.tip')"
             type="info"
             :closable="false"
             show-icon
           >
             <template #default>
-              <p>複製此連結發送給其他人,他們可以通過此連結註冊成為您的會員。</p>
+              <p>{{ t('agentLink.tipContent') }}</p>
             </template>
           </el-alert>
         </div>
@@ -47,6 +47,9 @@ import { ref, computed, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import { DocumentCopy } from '@element-plus/icons-vue'
 import useUserStore from '@/store/modules/user'
+import { useI18n } from '@/composables/useI18n'
+
+const { t } = useI18n()
 
 const userStore = useUserStore()
 
@@ -115,30 +118,30 @@ function copyTextToClipboard(text) {
 // 複製連結
 function handleCopy() {
   if (!shareLink.value) {
-    ElMessage.warning('分享連結生成失敗,請稍後再試')
+    ElMessage.warning(t('agentLink.linkGenerateFailed'))
     return
   }
   
   // 優先使用現代 Clipboard API
   if (navigator.clipboard && window.isSecureContext) {
     navigator.clipboard.writeText(shareLink.value).then(() => {
-      ElMessage.success('連結已複製到剪貼板,可以貼上發送給其他人了')
+      ElMessage.success(t('agentLink.linkCopied'))
     }).catch(() => {
       // 如果 Clipboard API 失敗,回退到傳統方法
       const success = copyTextToClipboard(shareLink.value)
       if (success) {
-        ElMessage.success('連結已複製到剪貼板,可以貼上發送給其他人了')
+        ElMessage.success(t('agentLink.linkCopied'))
       } else {
-        ElMessage.error('複製失敗,請手動複製連結')
+        ElMessage.error(t('agentLink.copyFailed'))
       }
     })
   } else {
     // 使用傳統方法
     const success = copyTextToClipboard(shareLink.value)
     if (success) {
-      ElMessage.success('連結已複製到剪貼板,可以貼上發送給其他人了')
+      ElMessage.success(t('agentLink.linkCopied'))
     } else {
-      ElMessage.error('複製失敗,請手動複製連結')
+      ElMessage.error(t('agentLink.copyFailed'))
     }
   }
 }
@@ -147,7 +150,7 @@ onMounted(() => {
   // 確保用戶資訊已載入
   if (!userStore.name && userStore.token) {
     userStore.getInfo().catch(() => {
-      ElMessage.error('獲取用戶資訊失敗')
+      ElMessage.error(t('agentLink.getUserInfoFailed'))
     })
   }
 })

+ 39 - 36
src/views/agent/call.vue

@@ -1,25 +1,25 @@
 <template>
   <div class="app-container">
     <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
-      <el-form-item label="用戶ID" prop="username">
+      <el-form-item :label="t('common.userId')" prop="username">
         <el-input
             v-model="queryParams.userId"
-            placeholder="請輸入用戶ID"
+            :placeholder="t('common.pleaseEnter') + t('common.userId')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="所屬用戶" prop="username">
+      <el-form-item :label="t('common.userName')" prop="username">
         <el-input
             v-model="queryParams.username"
-            placeholder="請輸入所屬用戶"
+            :placeholder="t('common.pleaseEnter') + t('common.userName')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜尋</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重設</el-button>
+        <el-button type="primary" icon="Search" @click="handleQuery">{{ t('common.search') }}</el-button>
+        <el-button icon="Refresh" @click="resetQuery">{{ t('common.reset') }}</el-button>
       </el-form-item>
     </el-form>
     <el-row :gutter="10" class="mb8">
@@ -29,7 +29,7 @@
           plain
           icon="Sort"
           @click="toggleExpandAll"
-        >展開/折疊</el-button>
+        >{{ t('common.expandCollapse') }}</el-button>
       </el-col>
     </el-row>
     <el-table 
@@ -41,25 +41,25 @@
       :default-expand-all="isExpandAll"
     >
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
+      <el-table-column :label="t('common.userName')" prop="userName" width="200" show-overflow-tooltip>
         <template #default="{ row }">
-          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || t('common.unknown') }}</span>
           <span v-else>{{ row.userName || '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="用戶ID" prop="userId" width="120" align="center"/>
-      <el-table-column label="電話號碼" prop="phoneNumber" align="center" />
-      <el-table-column label="通話類型" prop="callType" align="center">
+      <el-table-column :label="t('common.userId')" prop="userId" width="120" align="center"/>
+      <el-table-column :label="t('call.phoneNumber')" prop="phoneNumber" align="center" />
+      <el-table-column :label="t('call.callType')" prop="callType" align="center">
         <template #default="{ row }">
           <span v-if="!row.isParent">{{ callTypeText(row.callType) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="通話開始時間" prop="startTime" align="center" />
-      <el-table-column label="通話時長" prop="duration" align="center" />
-      <el-table-column label="通訊錄名稱" prop="contactName" align="center" />
-      <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
+      <el-table-column :label="t('call.startTime')" prop="startTime" align="center" />
+      <el-table-column :label="t('call.duration')" prop="duration" align="center" />
+      <el-table-column :label="t('call.contactName')" prop="contactName" align="center" />
+      <el-table-column :label="t('common.operation')" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
+          <el-tooltip :content="t('common.view')" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -68,28 +68,28 @@
     <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     <el-dialog :title="title" v-model="open" width="600px" append-to-body>
       <el-form ref="formRef" :model="form"  label-width="80px">
-        <el-form-item label="用戶id" prop="userId">
-          <el-input v-model="form.userId" placeholder="請輸入用戶id" />
+        <el-form-item :label="t('common.userId')" prop="userId">
+          <el-input v-model="form.userId" :placeholder="t('common.pleaseEnter') + t('common.userId')" />
         </el-form-item>
-        <el-form-item label="所屬用戶" prop="userName">
+        <el-form-item :label="t('common.userName')" prop="userName">
           <el-input v-model="form.userName" />
         </el-form-item>
-        <el-form-item label="電話號碼" prop="phoneNumber">
-          <el-input v-model="form.phoneNumber" placeholder="請輸入電話號碼" />
+        <el-form-item :label="t('call.phoneNumber')" prop="phoneNumber">
+          <el-input v-model="form.phoneNumber" :placeholder="t('call.pleaseEnterPhoneNumber')" />
         </el-form-item>
-        <el-form-item label="通話開始時間" prop="startTime">
-          <el-input v-model="form.startTime" placeholder="請輸入通話開始時間" />
+        <el-form-item :label="t('call.startTime')" prop="startTime">
+          <el-input v-model="form.startTime" :placeholder="t('call.pleaseEnterStartTime')" />
         </el-form-item>
-        <el-form-item label="通話時長" prop="duration">
-          <el-input v-model="form.duration" placeholder="請輸入通話時長" />
+        <el-form-item :label="t('call.duration')" prop="duration">
+          <el-input v-model="form.duration" :placeholder="t('call.pleaseEnterDuration')" />
         </el-form-item>
-        <el-form-item label="通訊錄名稱" prop="contactName">
-          <el-input v-model="form.contactName" placeholder="請輸入通訊錄名稱" />
+        <el-form-item :label="t('call.contactName')" prop="contactName">
+          <el-input v-model="form.contactName" :placeholder="t('call.pleaseEnterContactName')" />
         </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button @click="cancel">取 消</el-button>
+          <el-button @click="cancel">{{ t('common.cancel') }}</el-button>
         </div>
       </template>
     </el-dialog>
@@ -99,6 +99,9 @@
 <script setup>
 import {ref, reactive, getCurrentInstance, onMounted, nextTick} from 'vue'
 import {getCallPageList} from '@/api/system/call.js'
+import {useI18n} from '@/composables/useI18n'
+
+const { t } = useI18n()
 const { proxy } = getCurrentInstance()
 const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable")
 
@@ -134,7 +137,7 @@ const convertToTree = (data) => {
     if (!treeMap.has(key)) {
       treeMap.set(key, {
         id: `parent_${key}`,
-        userName: item.userName || item.userId || '未知用户',
+        userName: item.userName || item.userId || t('common.unknown'),
         userId: item.userId,
         isParent: true,
         children: []
@@ -159,20 +162,20 @@ onMounted(()=>{
   getList()
 })
 
-const  callTypeText=(type)=> {
+const callTypeText = (type) => {
   switch (type) {
     case 1:
     case '1':
-      return '呼入';
+      return t('call.incoming');
     case 2:
     case '2':
-      return '呼出';
+      return t('call.outgoing');
     case 3:
     case '3':
-      return '未接';
+      return t('call.missed');
     case 5:
     case '5':
-      return '挂断';
+      return t('call.hangup');
     default:
       return type;
   }
@@ -256,7 +259,7 @@ const handleView = (row) => {
   reset()
   form.value = {...row}
   open.value = true
-  title.value = "查看通話資訊"
+  title.value = t('call.viewTitle')
   nextTick(() => {
     if (formRef.value) formRef.value.clearValidate && formRef.value.clearValidate()
   })

+ 26 - 23
src/views/agent/image.vue

@@ -1,25 +1,25 @@
 <template>
   <div class="app-container">
     <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
-      <el-form-item label="用戶ID" prop="username">
+      <el-form-item :label="t('common.userId')" prop="username">
         <el-input
             v-model="queryParams.userId"
-            placeholder="請輸入用戶ID"
+            :placeholder="t('common.pleaseEnter') + t('common.userId')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="所屬用戶" prop="username">
+      <el-form-item :label="t('common.userName')" prop="username">
         <el-input
             v-model="queryParams.username"
-            placeholder="請輸入所屬用戶"
+            :placeholder="t('common.pleaseEnter') + t('common.userName')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜尋</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重設</el-button>
+        <el-button type="primary" icon="Search" @click="handleQuery">{{ t('common.search') }}</el-button>
+        <el-button icon="Refresh" @click="resetQuery">{{ t('common.reset') }}</el-button>
       </el-form-item>
     </el-form>
     <el-row :gutter="10" class="mb8">
@@ -29,7 +29,7 @@
           plain
           icon="Sort"
           @click="toggleExpandAll"
-        >展開/折疊</el-button>
+        >{{ t('common.expandCollapse') }}</el-button>
       </el-col>
     </el-row>
     <el-table 
@@ -41,23 +41,23 @@
       :default-expand-all="isExpandAll"
     >
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
+      <el-table-column :label="t('common.userName')" prop="userName" width="200" show-overflow-tooltip>
         <template #default="{ row }">
-          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || t('common.unknown') }}</span>
           <span v-else>{{ row.userName || '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="用戶id" prop="userId" width="120" align="center"/>
-      <el-table-column label="圖片" prop="url" width="120" align="center">
+      <el-table-column :label="t('common.userId')" prop="userId" width="120" align="center"/>
+      <el-table-column :label="t('image.image')" prop="url" width="120" align="center">
         <template #default="{ row }">
           <image-preview v-if="!row.isParent && row.url" :src="getImageUrl(row.url)" :width="80" :height="80"/>
           <span v-else-if="!row.isParent" style="color: #909399;">-</span>
         </template>
       </el-table-column>
-      <el-table-column label="圖片時間" prop="createDate" align="center" />
-      <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
+      <el-table-column :label="t('image.imageTime')" prop="createDate" align="center" />
+      <el-table-column :label="t('common.operation')" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
+          <el-tooltip :content="t('common.view')" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -66,23 +66,23 @@
     <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     <el-dialog :title="title" v-model="open" width="600px" append-to-body>
       <el-form ref="formRef" :model="form"  label-width="80px">
-        <el-form-item label="用戶id" prop="userId">
-          <el-input v-model="form.userId" placeholder="請輸入用戶id" />
+        <el-form-item :label="t('common.userId')" prop="userId">
+          <el-input v-model="form.userId" :placeholder="t('common.pleaseEnter') + t('common.userId')" />
         </el-form-item>
-        <el-form-item label="所屬用戶" prop="userName">
+        <el-form-item :label="t('common.userName')" prop="userName">
           <el-input v-model="form.userName" />
         </el-form-item>
-        <el-form-item label="圖片" prop="url">
+        <el-form-item :label="t('image.image')" prop="url">
           <image-preview v-if="form.url" :src="getImageUrl(form.url)" :width="100" :height="100"/>
 <!--          <image-upload :limit="1" v-model="form.url"/>-->
         </el-form-item>
-        <el-form-item label="圖片時間" prop="createDate">
-          <el-input v-model="form.createDate" placeholder="請輸入圖片時間" />
+        <el-form-item :label="t('image.imageTime')" prop="createDate">
+          <el-input v-model="form.createDate" :placeholder="t('image.pleaseEnterImageTime')" />
         </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button @click="cancel">取 消</el-button>
+          <el-button @click="cancel">{{ t('common.cancel') }}</el-button>
         </div>
       </template>
     </el-dialog>
@@ -93,6 +93,9 @@
 import {ref, reactive, getCurrentInstance, onMounted, nextTick} from 'vue'
 import {getImagePageList} from '@/api/system/image.js'
 import { isExternal } from '@/utils/validate'
+import {useI18n} from '@/composables/useI18n'
+
+const { t } = useI18n()
 
 // 获取文件路径前缀
 const filePath = import.meta.env.VITE_APP_FILE_PATH || ''
@@ -129,7 +132,7 @@ const convertToTree = (data) => {
     if (!treeMap.has(key)) {
       treeMap.set(key, {
         id: `parent_${key}`,
-        userName: item.userName || item.userId || '未知用户',
+        userName: item.userName || item.userId || t('common.unknown'),
         userId: item.userId,
         isParent: true,
         children: []
@@ -251,7 +254,7 @@ const handleView = (row) => {
   reset()
   form.value = {...row}
   open.value = true
-  title.value = "查看圖片資訊"
+  title.value = t('image.viewTitle')
   nextTick(() => {
     if (formRef.value) formRef.value.clearValidate && formRef.value.clearValidate()
   })

+ 25 - 22
src/views/agent/income.vue

@@ -1,17 +1,17 @@
 <template>
   <div class="app-container">
     <el-table v-loading="loading" :data="incomeList" @selection-change="handleSelectionChange">
-      <el-table-column label="id" align="center" prop="id" />
-<!--      <el-table-column label="代理id" align="center" prop="agentId" />-->
-      <el-table-column label="借款id" align="center" prop="orderId" />
-      <el-table-column label="用戶id" align="center" prop="loanUserId" />
-      <el-table-column label="用戶名稱" align="center" prop="username" />
-      <el-table-column label="借款金額" align="center" prop="loanAmount" />
-      <el-table-column label="收益" align="center" prop="income" />
-      <el-table-column label="創建時間" align="center" prop="createTime" />
-      <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
+      <el-table-column :label="t('income.id')" align="center" prop="id" />
+<!--      <el-table-column :label="t('income.agentId')" align="center" prop="agentId" />-->
+      <el-table-column :label="t('income.loanId')" align="center" prop="orderId" />
+      <el-table-column :label="t('income.userId')" align="center" prop="loanUserId" />
+      <el-table-column :label="t('income.username')" align="center" prop="username" />
+      <el-table-column :label="t('income.loanAmount')" align="center" prop="loanAmount" />
+      <el-table-column :label="t('income.income')" align="center" prop="income" />
+      <el-table-column :label="t('income.createTime')" align="center" prop="createTime" />
+      <el-table-column :label="t('income.operation')" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top">
+          <el-tooltip :content="t('income.view')" placement="top">
             <el-button link type="primary" icon="View" @click="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -24,26 +24,26 @@
 <!--        <el-form-item label="代理id" prop="agentId">-->
 <!--          <el-input v-model="form.agentId" placeholder="請輸入代理id" />-->
 <!--        </el-form-item>-->
-        <el-form-item label="借款id" prop="orderId">
-          <el-input v-model="form.orderId" placeholder="請輸入借款id" />
+        <el-form-item :label="t('income.loanId')" prop="orderId">
+          <el-input v-model="form.orderId" :placeholder="t('income.pleaseEnterLoanId')" />
         </el-form-item>
-        <el-form-item label="用戶id" prop="loanUserId">
-          <el-input v-model="form.loanUserId" placeholder="請輸入用戶id" />
+        <el-form-item :label="t('income.userId')" prop="loanUserId">
+          <el-input v-model="form.loanUserId" :placeholder="t('income.pleaseEnterUserId')" />
         </el-form-item>
-        <el-form-item label="用戶名稱" prop="username">
-          <el-input v-model="form.username" placeholder="請輸入用戶名稱" />
+        <el-form-item :label="t('income.username')" prop="username">
+          <el-input v-model="form.username" :placeholder="t('income.pleaseEnterUsername')" />
         </el-form-item>
-        <el-form-item label="借款金額" prop="loanAmount">
-          <el-input v-model="form.loanAmount" placeholder="請輸入借款金額" />
+        <el-form-item :label="t('income.loanAmount')" prop="loanAmount">
+          <el-input v-model="form.loanAmount" :placeholder="t('income.pleaseEnterLoanAmount')" />
         </el-form-item>
-        <el-form-item label="收益" prop="income">
-          <el-input v-model="form.income" placeholder="請輸入收益" />
+        <el-form-item :label="t('income.income')" prop="income">
+          <el-input v-model="form.income" :placeholder="t('income.pleaseEnterIncome')" />
         </el-form-item>
 
       </el-form>
       <div slot="footer" class="dialog-footer">
         <!--        <el-button type="primary" @click="submitForm">确 定</el-button>-->
-        <el-button @click="cancel">取 消</el-button>
+        <el-button @click="cancel">{{ t('common.cancel') }}</el-button>
       </div>
     </el-dialog>
   </div>
@@ -52,6 +52,9 @@
 <script setup="income">
 import {reactive, ref, onMounted} from "vue";
 import {getAgentIncome} from "@/api/system/income.js";
+import { useI18n } from '@/composables/useI18n'
+
+const { t } = useI18n()
 
 const loading = ref(true)
 const isView=ref(true)
@@ -100,7 +103,7 @@ const handleView = (row) => {
   form.value = { ...row }
   isView.value = true
   open.value = true
-  title.value = "查看收益"
+  title.value = t('income.viewIncome')
 }
 
 // 表單重設

+ 82 - 63
src/views/agent/myuser.vue

@@ -1,10 +1,10 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryFormRef" :inline="true" v-show="showSearch" label-width="80px">
-      <el-form-item label="用戶名稱" prop="username">
+    <el-form :model="queryParams" ref="queryFormRef" :inline="true" v-show="showSearch" label-width="120px">
+      <el-form-item :label="t('myuser.username')" prop="username">
         <el-input
             v-model="queryParams.username"
-            placeholder="請輸入用戶名稱"
+            :placeholder="t('myuser.pleaseEnterUsername')"
             clearable
             @keyup.enter="handleQuery"
         />
@@ -17,10 +17,10 @@
 <!--            @keyup.enter="handleQuery"-->
 <!--        />-->
 <!--      </el-form-item>-->
-      <el-form-item label="手機號碼" prop="phone">
+      <el-form-item :label="t('myuser.phone')" prop="phone">
         <el-input
             v-model="queryParams.phone"
-            placeholder="請輸入手機號碼"
+            :placeholder="t('myuser.pleaseEnterPhone')"
             clearable
             @keyup.enter="handleQuery"
         />
@@ -56,8 +56,8 @@
 <!--        />-->
 <!--      </el-form-item>-->
       <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜尋</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重設</el-button>
+        <el-button type="primary" icon="Search" @click="handleQuery">{{ t('common.search') }}</el-button>
+        <el-button icon="Refresh" @click="resetQuery">{{ t('common.reset') }}</el-button>
       </el-form-item>
     </el-form>
 
@@ -105,52 +105,52 @@
 
     <el-table v-loading="loading" :data="loanuserList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="50" align="center" />
-      <el-table-column label="使用者ID" align="center" prop="loanUserId" />
-      <el-table-column label="使用者名稱" align="center" prop="username" :show-overflow-tooltip="true" />
-      <el-table-column label="使用者暱稱" align="center" prop="nickname" :show-overflow-tooltip="true" />
-      <el-table-column label="頭像" align="center" prop="avatar" width="100">
+      <el-table-column :label="t('myuser.userId')" align="center" prop="loanUserId" />
+      <el-table-column :label="t('myuser.userName')" align="center" prop="username" :show-overflow-tooltip="true" />
+      <el-table-column :label="t('myuser.nickname')" align="center" prop="nickname" :show-overflow-tooltip="true" />
+      <el-table-column :label="t('myuser.avatar')" align="center" prop="avatar" width="100">
         <template #default="{ row }">
           <image-preview :src="getImageUrl(row.avatar)" :width="40" :height="40"/>
         </template>
       </el-table-column>
-      <el-table-column label="手機號碼" align="center" prop="phone" width="120" />
-      <el-table-column label="通訊錄上傳狀態" align="center" prop="upContact">
+      <el-table-column :label="t('myuser.phone')" align="center" prop="phone" width="120" />
+      <el-table-column :label="t('myuser.contactUploadStatus')" align="center" prop="upContact">
         <template #default="{ row }">
           <span>{{ upText(row.upContact) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="簡訊上傳狀態" align="center" prop="upSms">
+      <el-table-column :label="t('myuser.smsUploadStatus')" align="center" prop="upSms">
         <template #default="{ row }">
           <span>{{ upText(row.upSms) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="通話記錄上傳狀態" align="center" prop="upCall">
+      <el-table-column :label="t('myuser.callUploadStatus')" align="center" prop="upCall">
         <template #default="{ row }">
           <span>{{ upText(row.upCall) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="用户定位上傳狀態" align="center" prop="upPosition">
+      <el-table-column :label="t('myuser.positionUploadStatus')" align="center" prop="upPosition">
         <template #default="{ row }">
           <span>{{ upText(row.upPosition) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="用户圖片上傳狀態" align="center" prop="upImage">
+      <el-table-column :label="t('myuser.imageUploadStatus')" align="center" prop="upImage">
         <template #default="{ row }">
           <span>{{ upText(row.upImage) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="代理" align="center" prop="isAgent">
+      <el-table-column :label="t('myuser.agent')" align="center" prop="isAgent">
         <template #default="{ row }">
-          <span>{{ row.isAgent ? '是' : '否' }}</span>
+          <span>{{ row.isAgent ? t('myuser.yes') : t('myuser.no') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="所属代理ID" align="center" prop="agentId" />
-      <el-table-column label="狀態" align="center" prop="status">
+      <el-table-column :label="t('myuser.agentId')" align="center" prop="agentId" />
+      <el-table-column :label="t('myuser.status')" align="center" prop="status">
         <template #default="{ row }">
           <dict-tag :options="sys_normal_disable" :value="row.status"/>
         </template>
       </el-table-column>
-      <el-table-column label="建立時間" align="center" prop="createTime" width="160">
+      <el-table-column :label="t('myuser.createTime')" align="center" prop="createTime" width="160">
         <template #default="{ row }">
           <span>{{ proxy.parseTime(row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
         </template>
@@ -160,9 +160,9 @@
       <!--          <span>{{ parseTime(row.updateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>-->
       <!--        </template>-->
       <!--      </el-table-column>-->
-      <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
+      <el-table-column :label="t('myuser.operation')" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top">
+          <el-tooltip :content="t('myuser.view')" placement="top">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -179,45 +179,45 @@
 
     <!-- 新增或修改会员对话框 -->
     <el-dialog :title="title" v-model="open" width="600px" append-to-body>
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="用户名称" prop="username">
-          <el-input v-model="form.username" :disabled="isUpdate || isView" placeholder="请输入用户名称" />
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
+        <el-form-item :label="t('myuser.userInfo')" prop="username">
+          <el-input v-model="form.username" :disabled="isUpdate || isView" :placeholder="t('myuser.pleaseEnterUserInfo')" />
         </el-form-item>
-        <el-form-item label="用户昵称" prop="nickname">
-          <el-input v-model="form.nickname" :disabled="isView" placeholder="请输入用户昵称" />
+        <el-form-item :label="t('myuser.userNickname')" prop="nickname">
+          <el-input v-model="form.nickname" :disabled="isView" :placeholder="t('myuser.pleaseEnterUserNickname')" />
         </el-form-item>
-        <el-form-item label="頭像" prop="avatar">
+        <el-form-item :label="t('myuser.avatar')" prop="avatar">
           <image-preview :src="getImageUrl(form.avatar)"  :limit="1" :width="80" :height="80" />
         </el-form-item>
-        <el-form-item label="手机号码" prop="phone">
-          <el-input v-model="form.phone" :disabled="isView" placeholder="请输入手机号码" />
+        <el-form-item :label="t('myuser.phoneNumber')" prop="phone">
+          <el-input v-model="form.phone" :disabled="isView" :placeholder="t('myuser.pleaseEnterPhoneNumber')" />
         </el-form-item>
-        <el-form-item label="代理" prop="isAgent">
+        <el-form-item :label="t('myuser.agent')" prop="isAgent">
           <el-radio-group v-model="form.isAgent" :disabled="isView">
             <el-radio
                 key="true"
                 :label="true"
-            ></el-radio>
+            >{{ t('myuser.yes') }}</el-radio>
             <el-radio
                 key="false"
                 :label="false"
-            ></el-radio>
+            >{{ t('myuser.no') }}</el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label="状态" prop="status">
+        <el-form-item :label="t('myuser.status')" prop="status">
           <el-radio-group v-model="form.status" :disabled="isView">
             <el-radio
-                v-for="dict in sys_normal_disable"
+                v-for="dict in statusOptions"
                 :key="dict.value"
                 :label="parseInt(dict.value)"
-            >{{dict.label}}</el-radio>
+            >{{ dict.label }}</el-radio>
           </el-radio-group>
         </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
 <!--          <el-button type="primary" :disabled="isView" @click="submitForm">确 定</el-button>-->
-          <el-button @click="cancel">取 消</el-button>
+          <el-button @click="cancel">{{ t('common.cancel') }}</el-button>
         </div>
       </template>
     </el-dialog>
@@ -225,14 +225,33 @@
 </template>
 
 <script setup name="Loanuser">
-import { ref, reactive, getCurrentInstance, onMounted, nextTick } from 'vue'
+import { ref, reactive, computed, getCurrentInstance, onMounted, nextTick } from 'vue'
 import { getMyAgentMember } from "@/api/system/myuser.js"
 import { ElMessageBox, ElMessage } from 'element-plus'
 import { isExternal } from '@/utils/validate'
+import { useI18n } from '@/composables/useI18n'
+
+const { t } = useI18n()
 
 const { proxy } = getCurrentInstance()
 const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable")
 
+// 状态选项(国际化)
+const statusOptions = computed(() => {
+  if (!sys_normal_disable.value || !Array.isArray(sys_normal_disable.value)) {
+    return []
+  }
+  return sys_normal_disable.value.map(dict => {
+    // 根据字典值返回对应的翻译
+    if (dict.value === '0' || dict.value === 0) {
+      return { ...dict, label: t('myuser.statusNormal') }
+    } else if (dict.value === '1' || dict.value === 1) {
+      return { ...dict, label: t('myuser.statusDisable') }
+    }
+    return dict
+  })
+})
+
 // 獲取文件路徑前綴
 const filePath = import.meta.env.VITE_APP_FILE_PATH || ''
 
@@ -282,27 +301,27 @@ const queryParams = reactive({
 const form = ref({})
 
 // 表单校验
-const rules = reactive({
+const rules = computed(() => ({
   username: [
-    { required: true, message: "使用者名稱不能為空", trigger: "blur" }
+    { required: true, message: t('myuser.usernameRequired'), trigger: "blur" }
   ],
   nickname: [
-    { required: true, message: "使用者暱稱不能為空", trigger: "blur" }
+    { required: true, message: t('myuser.nicknameRequired'), trigger: "blur" }
   ],
   password: [
-    { required: true, message: "密碼不能為空", trigger: "blur" }
+    { required: true, message: t('myuser.passwordRequired'), trigger: "blur" }
   ],
   phone: [
-    { required: true, message: "手機號碼不能為空", trigger: "blur" }
+    { required: true, message: t('myuser.phoneRequired'), trigger: "blur" }
   ],
   status: [
-    { required: true, message: "狀態不能為空", trigger: "change" }
+    { required: true, message: t('myuser.statusRequired'), trigger: "change" }
   ],
   isAgent: [
-    { required: true, message: "代理不能為空", trigger: "change" }
+    { required: true, message: t('myuser.agentRequired'), trigger: "change" }
   ],
-  adminPassword: [{ required: true, message: "管理員密碼不能為空", trigger: "blur" }],
-})
+  adminPassword: [{ required: true, message: t('myuser.adminPasswordRequired'), trigger: "blur" }],
+}))
 
 // 获取ref引用
 const queryFormRef = ref()
@@ -339,9 +358,9 @@ const getList = () => {
 const upText = (type) => {
   switch (type) {
     case true:
-      return '已上傳'
+      return t('myuser.uploaded')
     case false:
-      return '拒絕'
+      return t('myuser.rejected')
     default:
       return type
   }
@@ -349,15 +368,15 @@ const upText = (type) => {
 
 /** 重设密码按钮操作 */
 const handleResetPwd = (row) => {
-  proxy.$prompt('請輸入"' + row.username + '"的新密碼', "提示", {
-    confirmButtonText: "確定",
-    cancelButtonText: "取消",
+  proxy.$prompt(t('myuser.resetPasswordPrompt', { username: row.username }), t('login.tip'), {
+    confirmButtonText: t('common.confirm'),
+    cancelButtonText: t('common.cancel'),
     closeOnClickModal: false,
     inputPattern: /^.{5,20}$/,
-    inputErrorMessage: "使用者密碼長度必須介於 5 和 20 之間",
+    inputErrorMessage: t('myuser.passwordLengthError'),
     inputValidator: (value) => {
       if (/<|>|"|'|\||\\/.test(value)) {
-        return "不能包含非法字元:< > \" ' \\\ |"
+        return t('myuser.invalidChars')
       }
     },
   }).then(({ value }) => {
@@ -420,7 +439,7 @@ const handleAdd = () => {
   open.value = true
   isUpdate.value = false
   isView.value = false
-  title.value = "新增會員管理"
+  title.value = t('myuser.addMember')
   nextTick(() => {
     if (formRef.value) formRef.value.clearValidate && formRef.value.clearValidate()
   })
@@ -435,7 +454,7 @@ const handleUpdate = (row) => {
   open.value = true
   isUpdate.value = true
   isView.value = false
-  title.value = "修改會員管理"
+  title.value = t('myuser.editMember')
   nextTick(() => {
     if (formRef.value) formRef.value.clearValidate && formRef.value.clearValidate()
   })
@@ -448,7 +467,7 @@ const handleView = (row) => {
   isUpdate.value = false
   isView.value = true
   open.value = true
-  title.value = "查看會員管理"
+  title.value = t('myuser.viewMember')
   nextTick(() => {
     if (formRef.value) formRef.value.clearValidate && formRef.value.clearValidate()
   })
@@ -464,11 +483,11 @@ const handleUpdatePasswrod = (row) => {
 
 const submitChangePassword = (row) => {
   const loanUserIds = row.loanUserId || ids.value
-  ElMessageBox.confirm('是否確認重置會員密碼為123456?').then(() => {
+  ElMessageBox.confirm(t('myuser.resetPasswordConfirm')).then(() => {
     // return delLoanuser(loanUserIds)
   }).then(() => {
     getList()
-    ElMessage.success("重置成功")
+    ElMessage.success(t('myuser.resetPasswordSuccess'))
   }).catch(() => {})
 }
 
@@ -496,11 +515,11 @@ const submitForm = () => {
 /** 删除按钮操作 */
 const handleDelete = (row) => {
   const loanUserIds = row.loanUserId || ids.value
-  ElMessageBox.confirm('是否確認刪除會員管理編號為"' + loanUserIds + '"的資料項?').then(() => {
+  ElMessageBox.confirm(t('myuser.deleteConfirm', { id: loanUserIds })).then(() => {
     // return delLoanuser(loanUserIds)
   }).then(() => {
     getList()
-    ElMessage.success("刪除成功")
+    ElMessage.success(t('myuser.deleteSuccess'))
   }).catch(() => {})
 }
 

+ 30 - 27
src/views/agent/position.vue

@@ -1,25 +1,25 @@
 <template>
   <div class="app-container">
     <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
-      <el-form-item label="用戶ID" prop="username">
+      <el-form-item :label="t('common.userId')" prop="username">
         <el-input
             v-model="queryParams.userId"
-            placeholder="請輸入用戶ID"
+            :placeholder="t('common.pleaseEnter') + t('common.userId')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="所屬用戶" prop="username">
+      <el-form-item :label="t('common.userName')" prop="username">
         <el-input
             v-model="queryParams.username"
-            placeholder="請輸入所屬用戶"
+            :placeholder="t('common.pleaseEnter') + t('common.userName')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜尋</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重設</el-button>
+        <el-button type="primary" icon="Search" @click="handleQuery">{{ t('common.search') }}</el-button>
+        <el-button icon="Refresh" @click="resetQuery">{{ t('common.reset') }}</el-button>
       </el-form-item>
     </el-form>
     <el-row :gutter="10" class="mb8">
@@ -29,7 +29,7 @@
           plain
           icon="Sort"
           @click="toggleExpandAll"
-        >展開/折疊</el-button>
+        >{{ t('common.expandCollapse') }}</el-button>
       </el-col>
     </el-row>
     <el-table 
@@ -40,19 +40,19 @@
       row-key="id"
       :default-expand-all="isExpandAll"
     >
-      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
+      <el-table-column :label="t('common.userName')" prop="userName" width="200" show-overflow-tooltip>
         <template #default="{ row }">
-          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || t('common.unknown') }}</span>
           <span v-else>{{ row.userName || '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="用戶id" prop="userId" width="120" align="center"/>
-      <el-table-column label="經度" prop="longitude" align="center"/>
-      <el-table-column label="緯度" prop="latitude" align="center"/>
-      <el-table-column label="備註" prop="notes" align="center"/>
-      <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
+      <el-table-column :label="t('common.userId')" prop="userId" width="120" align="center"/>
+      <el-table-column :label="t('position.longitude')" prop="longitude" align="center"/>
+      <el-table-column :label="t('position.latitude')" prop="latitude" align="center"/>
+      <el-table-column :label="t('position.notes')" prop="notes" align="center"/>
+      <el-table-column :label="t('common.operation')" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
+          <el-tooltip :content="t('common.view')" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -61,25 +61,25 @@
     <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     <el-dialog :title="title" v-model="open" width="600px" append-to-body>
       <el-form ref="formRef" :model="form"  label-width="80px">
-        <el-form-item label="用戶id" prop="userId">
-          <el-input v-model="form.userId" placeholder="請輸入用戶id" />
+        <el-form-item :label="t('common.userId')" prop="userId">
+          <el-input v-model="form.userId" :placeholder="t('common.pleaseEnter') + t('common.userId')" />
         </el-form-item>
-        <el-form-item label="用戶名稱" prop="userName">
+        <el-form-item :label="t('common.userName')" prop="userName">
           <el-input v-model="form.userName"  />
         </el-form-item>
-        <el-form-item label="經度" prop="longitude">
-          <el-input v-model="form.longitude" placeholder="請輸入經度" />
+        <el-form-item :label="t('position.longitude')" prop="longitude">
+          <el-input v-model="form.longitude" :placeholder="t('position.pleaseEnterLongitude')" />
         </el-form-item>
-        <el-form-item label="緯度" prop="latitude">
-          <el-input v-model="form.latitude" placeholder="請輸入緯度" />
+        <el-form-item :label="t('position.latitude')" prop="latitude">
+          <el-input v-model="form.latitude" :placeholder="t('position.pleaseEnterLatitude')" />
         </el-form-item>
-        <el-form-item label="備註" prop="notes">
-          <el-input v-model="form.notes" placeholder="請輸入備註" />
+        <el-form-item :label="t('position.notes')" prop="notes">
+          <el-input v-model="form.notes" :placeholder="t('position.pleaseEnterNotes')" />
         </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button @click="cancel">取 消</el-button>
+          <el-button @click="cancel">{{ t('common.cancel') }}</el-button>
         </div>
       </template>
     </el-dialog>
@@ -89,6 +89,9 @@
 <script setup>
 import {ref, reactive, getCurrentInstance, onMounted, nextTick} from 'vue'
 import {getPositionPageList} from '@/api/system/position.js'
+import {useI18n} from '@/composables/useI18n'
+
+const { t } = useI18n()
 
 const positionList = ref([])
 const treeData = ref([])
@@ -122,7 +125,7 @@ const convertToTree = (data) => {
     if (!treeMap.has(key)) {
       treeMap.set(key, {
         id: `parent_${key}`,
-        userName: item.userName || item.userId || '未知用户',
+        userName: item.userName || item.userId || t('common.unknown'),
         userId: item.userId,
         isParent: true,
         children: []
@@ -222,7 +225,7 @@ const handleView = (row) => {
   reset()
   form.value = {...row}
   open.value = true
-  title.value = "查看定位資訊"
+  title.value = t('position.viewTitle')
   nextTick(() => {
     if (formRef.value) formRef.value.clearValidate && formRef.value.clearValidate()
   })

+ 33 - 30
src/views/agent/sms.vue

@@ -1,25 +1,25 @@
 <template>
   <div class="app-container">
     <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
-      <el-form-item label="用戶ID" prop="username">
+      <el-form-item :label="t('common.userId')" prop="username">
         <el-input
             v-model="queryParams.userId"
-            placeholder="請輸入用戶ID"
+            :placeholder="t('common.pleaseEnter') + t('common.userId')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="所屬用戶" prop="username">
+      <el-form-item :label="t('common.userName')" prop="username">
         <el-input
             v-model="queryParams.username"
-            placeholder="請輸入所屬用戶"
+            :placeholder="t('common.pleaseEnter') + t('common.userName')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜尋</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重設</el-button>
+        <el-button type="primary" icon="Search" @click="handleQuery">{{ t('common.search') }}</el-button>
+        <el-button icon="Refresh" @click="resetQuery">{{ t('common.reset') }}</el-button>
       </el-form-item>
     </el-form>
     <el-row :gutter="10" class="mb8">
@@ -29,7 +29,7 @@
           plain
           icon="Sort"
           @click="toggleExpandAll"
-        >展開/折疊</el-button>
+        >{{ t('common.expandCollapse') }}</el-button>
       </el-col>
     </el-row>
     <el-table 
@@ -41,28 +41,28 @@
       :default-expand-all="isExpandAll"
     >
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
+      <el-table-column :label="t('common.userName')" prop="userName" width="200" show-overflow-tooltip>
         <template #default="{ row }">
-          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || t('common.unknown') }}</span>
           <span v-else>{{ row.userName || '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="用戶id" prop="userId" width="120" align="center"/>
-      <el-table-column label="簡訊類型" prop="smsType" align="center">
+      <el-table-column :label="t('common.userId')" prop="userId" width="120" align="center"/>
+      <el-table-column :label="t('sms.smsType')" prop="smsType" align="center">
         <template #default="{ row }">
           <span v-if="!row.isParent">{{ smsTypeText(row.smsType) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="對方號碼" prop="phoneNumber" align="center" />
-      <el-table-column label="簡訊內容" prop="content" width="200" align="center">
+      <el-table-column :label="t('sms.phoneNumber')" prop="phoneNumber" align="center" />
+      <el-table-column :label="t('sms.content')" prop="content" width="200" align="center">
         <template #default="{ row }">
           <div v-if="!row.isParent" class="content-cell-text">{{ row.content }}</div>
         </template>
       </el-table-column>
-      <el-table-column label="簡訊時間" prop="smsTime" align="center" />
-      <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
+      <el-table-column :label="t('sms.smsTime')" prop="smsTime" align="center" />
+      <el-table-column :label="t('common.operation')" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
+          <el-tooltip :content="t('common.view')" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -71,25 +71,25 @@
     <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     <el-dialog :title="title" v-model="open" width="600px" append-to-body>
       <el-form ref="formRef" :model="form"  label-width="80px">
-          <el-form-item label="用戶id" prop="userId">
-            <el-input v-model="form.userId" placeholder="請輸入用戶id" />
+          <el-form-item :label="t('common.userId')" prop="userId">
+            <el-input v-model="form.userId" :placeholder="t('common.pleaseEnter') + t('common.userId')" />
           </el-form-item>
-          <el-form-item label="所屬用戶" prop="userName">
+          <el-form-item :label="t('common.userName')" prop="userName">
             <el-input v-model="form.userName" />
           </el-form-item>
-          <el-form-item label="對方號碼" prop="phoneNumber">
-            <el-input v-model="form.phoneNumber" placeholder="請輸入對方號碼" />
+          <el-form-item :label="t('sms.phoneNumber')" prop="phoneNumber">
+            <el-input v-model="form.phoneNumber" :placeholder="t('sms.pleaseEnterPhoneNumber')" />
           </el-form-item>
-          <el-form-item label="簡訊內容">
+          <el-form-item :label="t('sms.content')">
             <el-input type="textarea" autosize="{minRows:4}" pla v-model="form.content"/>
           </el-form-item>
-          <el-form-item label="簡訊時間" prop="smsTime">
-            <el-input v-model="form.smsTime" placeholder="請輸入簡訊時間" />
+          <el-form-item :label="t('sms.smsTime')" prop="smsTime">
+            <el-input v-model="form.smsTime" :placeholder="t('sms.pleaseEnterSmsTime')" />
           </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button @click="cancel">取 消</el-button>
+          <el-button @click="cancel">{{ t('common.cancel') }}</el-button>
         </div>
       </template>
     </el-dialog>
@@ -99,6 +99,9 @@
 <script setup>
 import {ref, reactive, getCurrentInstance, onMounted, nextTick} from 'vue'
 import {getSmsPageList} from '@/api/system/sms.js'
+import {useI18n} from '@/composables/useI18n'
+
+const { t } = useI18n()
 const { proxy } = getCurrentInstance()
 const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable")
 
@@ -134,7 +137,7 @@ const convertToTree = (data) => {
     if (!treeMap.has(key)) {
       treeMap.set(key, {
         id: `parent_${key}`,
-        userName: item.userName || item.userId || '未知用户',
+        userName: item.userName || item.userId || t('common.unknown'),
         userId: item.userId,
         isParent: true,
         children: []
@@ -159,14 +162,14 @@ onMounted(()=>{
   getList()
 })
 
-const smsTypeText = (type)=> {
+const smsTypeText = (type) => {
   switch (type) {
     case 1:
     case '1':
-      return '接收';
+      return t('sms.receive');
     case 2:
     case '2':
-      return '發送';
+      return t('sms.send');
     default:
       return type;
   }
@@ -249,7 +252,7 @@ const handleView = (row) => {
   reset()
   form.value = {...row}
   open.value = true
-  title.value = "查看簡訊資訊"
+  title.value = t('sms.viewTitle')
   nextTick(() => {
     if (formRef.value) formRef.value.clearValidate && formRef.value.clearValidate()
   })

+ 32 - 29
src/views/agent/userContact.vue

@@ -1,33 +1,33 @@
 <template>
   <div class="app-container">
     <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
-      <el-form-item label="用戶ID" prop="username">
+      <el-form-item :label="t('common.userId')" prop="username">
         <el-input
             v-model="queryParams.userId"
-            placeholder="請輸入用戶ID"
+            :placeholder="t('common.pleaseEnter') + t('common.userId')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="所屬用戶" prop="username">
+      <el-form-item :label="t('common.userName')" prop="username">
         <el-input
             v-model="queryParams.username"
-            placeholder="請輸入所屬用戶"
+            :placeholder="t('common.pleaseEnter') + t('common.userName')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="聯絡人名稱" prop="name">
+      <el-form-item :label="t('common.userContact')" prop="name">
         <el-input
             v-model="queryParams.name"
-            placeholder="請輸入聯絡人名稱"
+            :placeholder="t('contact.pleaseEnterContactName')"
             clearable
             @keyup.enter="handleQuery"
         />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜尋</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重設</el-button>
+        <el-button type="primary" icon="Search" @click="handleQuery">{{ t('common.search') }}</el-button>
+        <el-button icon="Refresh" @click="resetQuery">{{ t('common.reset') }}</el-button>
       </el-form-item>
     </el-form>
     <el-row :gutter="10" class="mb8">
@@ -37,7 +37,7 @@
           plain
           icon="Sort"
           @click="toggleExpandAll"
-        >展開/折疊</el-button>
+        >{{ t('common.expandCollapse') }}</el-button>
       </el-col>
     </el-row>
     <el-table 
@@ -48,19 +48,19 @@
       row-key="id"
       :default-expand-all="isExpandAll"
     >
-      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
+      <el-table-column :label="t('common.userName')" prop="userName" width="200" show-overflow-tooltip>
         <template #default="{ row }">
-          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || t('common.unknown') }}</span>
           <span v-else>{{ row.userName || '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="用戶id" prop="userId" width="120" align="center"/>
-      <el-table-column label="聯絡人名稱" prop="name" align="center"/>
-      <el-table-column label="聯絡電話" prop="phones" align="center"/>
-      <el-table-column label="創建時間" prop="createTime" align="center"/>
-      <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
+      <el-table-column :label="t('common.userId')" prop="userId" width="120" align="center"/>
+      <el-table-column :label="t('common.userContact')" prop="name" align="center"/>
+      <el-table-column :label="t('common.userPhone')" prop="phones" align="center"/>
+      <el-table-column :label="t('common.createTime')" prop="createTime" align="center"/>
+      <el-table-column :label="t('common.operation')" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
+          <el-tooltip :content="t('common.view')" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -70,25 +70,25 @@
     <!-- 新增或修改会员对话框 -->
     <el-dialog :title="title" v-model="open" width="600px" append-to-body>
       <el-form ref="formRef" :model="form"  label-width="80px">
-        <el-form-item label="用戶id" prop="userId">
-          <el-input v-model="form.userId" placeholder="請輸入用戶id" />
+        <el-form-item :label="t('common.userId')" prop="userId">
+          <el-input v-model="form.userId" :placeholder="t('common.pleaseEnter') + t('common.userId')" />
         </el-form-item>
-        <el-form-item label="所屬用戶" prop="userName">
+        <el-form-item :label="t('common.userName')" prop="userName">
           <el-input v-model="form.userName" />
         </el-form-item>
-        <el-form-item label="聯絡人名稱" prop="name">
-          <el-input v-model="form.name" placeholder="請輸入聯絡人名稱" />
+        <el-form-item :label="t('common.userContact')" prop="name">
+          <el-input v-model="form.name" :placeholder="t('contact.pleaseEnterContactName')" />
         </el-form-item>
-        <el-form-item label="聯絡電話" prop="phones">
-          <el-input v-model="form.phones" placeholder="請輸入內容" />
+        <el-form-item :label="t('common.userPhone')" prop="phones">
+          <el-input v-model="form.phones" :placeholder="t('common.pleaseEnter') + t('common.userPhone')" />
         </el-form-item>
-        <el-form-item label="創建時間" prop="createTime">
-          <el-input v-model="form.createTime" placeholder="請輸入內容" />
+        <el-form-item :label="t('common.createTime')" prop="createTime">
+          <el-input v-model="form.createTime" :placeholder="t('common.pleaseEnter') + t('common.createTime')" />
         </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button @click="cancel">取 消</el-button>
+          <el-button @click="cancel">{{ t('common.cancel') }}</el-button>
         </div>
       </template>
     </el-dialog>
@@ -97,6 +97,9 @@
 <script setup>
 import {ref, reactive, getCurrentInstance, onMounted, nextTick} from 'vue'
 import {getContactPageList} from "@/api/system/userContact.js"
+import {useI18n} from '@/composables/useI18n'
+
+const { t } = useI18n()
 
 const title = ref("")
 const open = ref(false)
@@ -131,7 +134,7 @@ const convertToTree = (data) => {
     if (!treeMap.has(key)) {
       treeMap.set(key, {
         id: `parent_${key}`,
-        userName: item.userName || item.userId || '未知用户',
+        userName: item.userName || item.userId || t('common.unknown'),
         userId: item.userId,
         isParent: true,
         children: []
@@ -232,7 +235,7 @@ const handleView = (row) => {
   reset()
   form.value = {...row}
   open.value = true
-  title.value = "查看聯絡人"
+  title.value = t('contact.viewTitle')
   nextTick(() => {
     if (formRef.value) formRef.value.clearValidate && formRef.value.clearValidate()
   })

+ 41 - 12
src/views/login.vue

@@ -2,13 +2,17 @@
   <div class="login">
     <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
       <h3 class="title">{{ title }}</h3>
+      <div class="language-select-wrapper">
+        <label class="language-label">{{ t('common.language') }}</label>
+        <lang-select :reload="false" size="large" style="width: 100%;" />
+      </div>
       <el-form-item prop="username">
         <el-input
           v-model="loginForm.username"
           type="text"
           size="large"
           auto-complete="off"
-          placeholder="账号"
+          :placeholder="t('login.username')"
         >
           <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
         </el-input>
@@ -19,7 +23,7 @@
           type="password"
           size="large"
           auto-complete="off"
-          placeholder="密码"
+          :placeholder="t('login.password')"
           @keyup.enter="handleLogin"
         >
           <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
@@ -30,7 +34,7 @@
           v-model="loginForm.code"
           size="large"
           auto-complete="off"
-          placeholder="验证码"
+          :placeholder="t('login.code')"
           style="width: 63%"
           @keyup.enter="handleLogin"
         >
@@ -40,7 +44,7 @@
           <img :src="codeUrl" @click="getCode" class="login-code-img"/>
         </div>
       </el-form-item>
-      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
+      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">{{ t('login.rememberMe') }}</el-checkbox>
       <el-form-item style="width:100%;">
         <el-button
           :loading="loading"
@@ -49,8 +53,8 @@
           style="width:100%;"
           @click.prevent="handleLogin"
         >
-          <span v-if="!loading">登 录</span>
-          <span v-else>登 录 中...</span>
+          <span v-if="!loading">{{ t('login.login') }}</span>
+          <span v-else>{{ t('login.logging') }}</span>
         </el-button>
         <div style="float: right;" v-if="register">
           <router-link class="link-type" :to="'/register'">立即注册</router-link>
@@ -69,8 +73,12 @@ import { getCodeImg } from "@/api/login"
 import Cookies from "js-cookie"
 import { encrypt, decrypt } from "@/utils/jsencrypt"
 import useUserStore from '@/store/modules/user'
+import { useI18n } from '@/composables/useI18n'
+import LangSelect from '@/components/LangSelect'
+import { ref, watch, computed } from 'vue'
 
-const title = import.meta.env.VITE_APP_TITLE
+const { t, locale } = useI18n()
+const title = computed(() => t('login.title'))
 const userStore = useUserStore()
 const route = useRoute()
 const router = useRouter()
@@ -84,11 +92,20 @@ const loginForm = ref({
   uuid: ""
 })
 
-const loginRules = {
-  username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
-  password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
-  code: [{ required: true, trigger: "change", message: "请输入验证码" }]
-}
+const loginRules = ref({
+  username: [{ required: true, trigger: "blur", message: t('login.pleaseEnterUsername') }],
+  password: [{ required: true, trigger: "blur", message: t('login.pleaseEnterPassword') }],
+  code: [{ required: true, trigger: "change", message: t('login.pleaseEnterCode') }]
+})
+
+// 监听语言变化,更新验证规则
+watch(locale, () => {
+  loginRules.value = {
+    username: [{ required: true, trigger: "blur", message: t('login.pleaseEnterUsername') }],
+    password: [{ required: true, trigger: "blur", message: t('login.pleaseEnterPassword') }],
+    code: [{ required: true, trigger: "change", message: t('login.pleaseEnterCode') }]
+  }
+})
 
 const codeUrl = ref("")
 const loading = ref(false)
@@ -201,6 +218,18 @@ getCookie()
     margin-left: 0px;
   }
 }
+
+.language-select-wrapper {
+  margin-bottom: 20px;
+  
+  .language-label {
+    display: block;
+    margin-bottom: 8px;
+    font-size: 14px;
+    color: #606266;
+    font-weight: normal;
+  }
+}
 .login-tip {
   font-size: 13px;
   text-align: center;

+ 1 - 1
vite.config.js

@@ -2,7 +2,7 @@ import { defineConfig, loadEnv } from 'vite'
 import path from 'path'
 import createVitePlugins from './vite/plugins'
 
-const baseUrl = 'http://localhost:8091' // 后端接口
+const baseUrl = 'http://localhost:8080' // 后端接口
 // const baseUrl = 'https://loanapi.waimai-paotui.com' // 后端接口
 // const baseUrl="https://api.shoujida.com"