Explorar o código

feature: 用户权限,用户列表,u卡页面

ljc hai 6 meses
pai
achega
8ef364005b

+ 2 - 2
src/components/NavMenu.vue

@@ -68,8 +68,8 @@
   // 计算属性 - 使用 mapState 的等效方式
   const menus = computed(() => {
     console.log(store.state.navMenu.menus, 'menus')
-    // 这是u卡只需要u卡路由
-    return store.state.navMenu.menus.filter((item) => item.name == 'R-Card')
+    // 这是u卡只需要u卡路由 .filter((item) => item.name == 'R-Card')
+    return store.state.navMenu.menus
   })
   const isCollapse = computed(() => store.state.navMenu.isCollapse)
 

+ 2 - 1
src/config/index.ts

@@ -38,7 +38,8 @@ switch (c) {
     // Host85="http://103.171.34.61:8500" // 测试
     Host05 = 'http://103.171.34.61:8705'
     Host80 = 'http://192.168.0.30:8000'
-    Host85 = 'http://192.168.0.18:8500' // 高超本地
+    // Host85 = 'http://192.168.0.18:8500' // 高超本地
+    Host85 = 'http://192.168.0.38:8501' // 孔向阳本地
     // Host85="http://192.168.0.30:8500"
     break
 }

+ 68 - 0
src/enum/card/globalOrder.js

@@ -0,0 +1,68 @@
+export const transferType = {
+  1: 'B2B',
+  2: 'B2C',
+  3: 'C2C',
+  4: 'C2B',
+}
+
+export const payoutMethod = {
+  1: 'global.p13',
+  7: 'global.p14',
+}
+/**
+ * 合规性检查状态。
+ * no_process: 无需合规性检查,
+ * pending_check: 待提交合规性检查资料,
+ * pending: 合规性检查中,
+ * approved: 合规性检查通过,
+ * rejected: 合规性检查拒绝
+ */
+export const complianceStatusEnum = {
+  NoProcess: 'no_process',
+  PendingCheck: 'pending_check',
+  Pending: 'pending',
+  Approved: 'approved',
+  Rejected: 'rejected',
+}
+
+export const deductionAccountText = {
+  1: 'Ucard.GlobalOrder.deduction1',
+  2: 'Ucard.GlobalOrder.deduction2',
+  3: 'Ucard.GlobalOrder.deduction3',
+  4: 'Ucard.GlobalOrder.deduction4',
+  5: 'Ucard.GlobalOrder.deduction5',
+  6: 'Ucard.GlobalOrder.deduction6',
+}
+
+export const approvalText = {
+  1: 'R-Review-Status-Pending',
+  2: 'R-Review-Status-Approved',
+  3: 'R-Review-Status-Rejected',
+}
+// 全球速汇订单状态。
+export const statusOpt = [
+  {
+    value: 'wait_process',
+    label: 'Ucard.GlobalOrder.wait',
+  },
+  {
+    value: 'success',
+    label: 'Ucard.GlobalOrder.success',
+  },
+  {
+    value: 'fail',
+    label: 'Ucard.GlobalOrder.fail',
+  },
+  {
+    value: 'processing',
+    label: 'Ucard.GlobalOrder.processing',
+  },
+  {
+    value: 'partner_processing',
+    label: 'Ucard.GlobalOrder.partner',
+  },
+  {
+    value: 'cancel',
+    label: 'Ucard.GlobalOrder.cancel',
+  },
+]

+ 3 - 1
src/routers/index.ts

@@ -13,6 +13,8 @@ interface extendRoute {
 
 //
 import uCardRoute from './modules/uCard'
+import userRoute from '@/routers/modules/user'
+import systemRouter from '@/routers/modules/system'
 
 /**
  * path ==> 路由路径
@@ -73,7 +75,7 @@ export const constantRoutes: Array<RouteRecordRaw & extendRoute> = [
       OnBreadCrumb: false,
       requiresAuth: true,
     },
-    children: [uCardRoute],
+    children: [uCardRoute, userRoute, systemRouter],
   },
 ]
 

+ 10 - 38
src/routers/modules/system.ts

@@ -1,47 +1,19 @@
-/** When your routing table is too long, you can split it into small modules**/
-
-import Layout from '@/layout/index.vue'
-
 const systemRouter = [
   {
-    path: '/system',
-    component: Layout,
-    redirect: '/system/user',
-    name: 'system',
     meta: {
-      title: '系统管理',
-      icon: 'Setting',
+      OnBreadCrumb: true,
     },
+    path: 'system',
+    name: 'R-System',
+    component: () => import(/* webpackChunkName: "page" */ '@/views/page/Page.vue'),
     children: [
       {
-        path: '/system/user',
-        component: () => import('@/views/system/user/index.vue'),
-        name: 'user',
-        meta: { title: '用户管理', icon: 'MenuIcon' },
-      },
-      {
-        path: '/system/dept',
-        component: () => import('@/views/system/dept/index.vue'),
-        name: 'dept',
-        meta: { title: '部门管理', icon: 'MenuIcon' },
-      },
-      {
-        path: '/system/role',
-        component: () => import('@/views/system/role/index.vue'),
-        name: 'role',
-        meta: { title: '角色管理', icon: 'MenuIcon' },
-      },
-      {
-        path: '/system/menu',
-        component: () => import('@/views/system/menu/index.vue'),
-        name: 'menu',
-        meta: { title: '菜单管理', icon: 'MenuIcon' },
-      },
-      {
-        path: '/system/dictionary',
-        component: () => import('@/views/system/dictionary/index.vue'),
-        name: 'dictionary',
-        meta: { title: '字典管理', icon: 'MenuIcon' },
+        meta: {
+          OnBreadCrumb: true,
+        },
+        path: 'google/email',
+        name: 'R-GoogleGroup',
+        component: () => import('@/views/system/GoogleEmail/index.vue'),
       },
     ],
   },

+ 36 - 25
src/routers/modules/uCard.ts

@@ -32,14 +32,15 @@ const uCardRoute = {
       name: 'R-CardType',
       component: () => import('@/views/card/CardType/index.vue'),
     },
-    //   {
-    //     meta: {
-    //       OnBreadCrumb: true,
-    //     },
-    //     path: 'operate',
-    //     name: 'R-CardOperate',
-    //     component: () => import(/* webpackChunkName: "CardOperate" */ '@/views/card/CardOperate.vue'),
-    //   },
+    {
+      meta: {
+        OnBreadCrumb: true,
+      },
+      path: 'operate',
+      name: 'R-CardOperate',
+      component: () =>
+        import(/* webpackChunkName: "CardOperate" */ '@/views/card/CardOperate/index.vue'),
+    },
     {
       meta: {
         OnBreadCrumb: true,
@@ -58,23 +59,24 @@ const uCardRoute = {
       component: () =>
         import(/* webpackChunkName: "UserRights" */ '@/views/card/CardUserRights/index.vue'),
     },
-    //   {
-    //     meta: {
-    //       OnBreadCrumb: true,
-    //     },
-    //     path: 'config',
-    //     name: 'R-CardConfig',
-    //     component: () => import(/* webpackChunkName: "CardConfig" */ '@/views/card/CardConfig.vue'),
-    //   },
-    //   {
-    //     meta: {
-    //       OnBreadCrumb: true,
-    //     },
-    //     path: 'idTypeConfig',
-    //     name: 'R-IdTypeConfig',
-    //     component: () =>
-    //       import(/* webpackChunkName: "IdTypeConfig" */ '@/views/card/IdTypeConfig.vue'),
-    //   },
+    {
+      meta: {
+        OnBreadCrumb: true,
+      },
+      path: 'config',
+      name: 'R-CardConfig',
+      component: () =>
+        import(/* webpackChunkName: "CardConfig" */ '@/views/card/CardConfig/index.vue'),
+    },
+    {
+      meta: {
+        OnBreadCrumb: true,
+      },
+      path: 'idTypeConfig',
+      name: 'R-IdTypeConfig',
+      component: () =>
+        import(/* webpackChunkName: "IdTypeConfig" */ '@/views/card/CardIdTypeConfig/index.vue'),
+    },
     {
       meta: {
         OnBreadCrumb: true,
@@ -100,6 +102,14 @@ const uCardRoute = {
       component: () =>
         import(/* webpackChunkName: "Transactions" */ '@/views/card/CardTransactions/index.vue'),
     },
+    {
+      meta: {
+        OnBreadCrumb: true,
+      },
+      path: 'file',
+      name: 'R-CardFile',
+      component: () => import('@/views/card/CardFile/index.vue'),
+    },
     {
       meta: {
         OnBreadCrumb: true,
@@ -109,6 +119,7 @@ const uCardRoute = {
       component: () =>
         import(/* webpackChunkName: "GlobalCurrency" */ '@/views/card/GlobalCurrency/index.vue'),
     },
+
     //   {
     //     meta: {
     //       OnBreadCrumb: true,

+ 29 - 0
src/routers/modules/user.ts

@@ -0,0 +1,29 @@
+// user路由
+const userRoute = {
+  meta: {
+    OnBreadCrumb: true,
+  },
+  path: 'user',
+  name: 'R-User',
+  component: () => import('@/views/page/Page.vue'),
+  children: [
+    {
+      meta: {
+        OnBreadCrumb: true,
+      },
+      path: 'list',
+      name: 'R-UserList',
+      component: () => import(/* webpackChunkName: "list" */ '@/views/user/userList/index.vue'),
+    },
+    {
+      meta: {
+        OnBreadCrumb: true,
+      },
+      path: 'role',
+      name: 'R-Role',
+      component: () =>
+        import(/* webpackChunkName: "CardOrder" */ '@/views/user/userRole/index.vue'),
+    },
+  ],
+}
+export default userRoute

+ 12 - 0
src/service/customer.ts

@@ -20,5 +20,17 @@ class SystemService extends Service {
   async pIbNoRealSingle(params = {}) {
     return await this.post('/ib/search/ib/single', params)
   }
+  //用户文件审核
+  async customFileApprove(params = {}) {
+    return await this.post('/custom/file/approve', params)
+  }
+  //反审核
+  async customFileApproveDe(params = {}) {
+    return await this.post('/custom/file/de/approve', params)
+  }
+  //根据条件查看拒绝列表-用于下拉和选择展示理由
+  async reasonsRefusalList(params = {}) {
+    return await this.post('/reasons/refusal/list', params)
+  }
 }
 export default new SystemService()

+ 20 - 0
src/service/financial.ts

@@ -0,0 +1,20 @@
+import Service from '../lib/service.js'
+import axios from 'axios'
+import Config from '../config'
+
+class FinancialService extends Service {
+  constructor() {
+    super()
+    axios.defaults.baseURL = Config.Host85
+  }
+  // 入金金额统计
+  async depositAmountStatistics(params = {}) {
+    return await this.post('/finance/deposit/amount/statictics', params)
+  }
+  // 出金金额统计
+  async withdrawAmountStatistics(params = {}) {
+    return await this.post('/finance/withdraw/amount/statictics', params)
+  }
+}
+
+export default new FinancialService()

+ 28 - 0
src/service/google.ts

@@ -0,0 +1,28 @@
+import Service from '../lib/service.js'
+import axios from 'axios'
+import Config from '../config'
+
+class GoogleService extends Service {
+  constructor() {
+    super()
+    axios.defaults.baseURL = Config.Host85
+  }
+  /* Google组别管理 */
+  // 查询组别列表
+  async googleGroupList(params = {}) {
+    return await this.post('/google/group/search/list', params)
+  }
+  // 删除组别
+  async googleGroupDelete(params = {}) {
+    return await this.post('/google/group/delete', params)
+  }
+  // 更新组别
+  async googleGroupUpdate(params = {}) {
+    return await this.post('/google/group/update', params)
+  }
+  // 新增组别
+  async googleGroupAdd(params = {}) {
+    return await this.post('/google/group/add', params)
+  }
+}
+export default new GoogleService()

+ 4 - 0
src/service/marketing.ts

@@ -16,5 +16,9 @@ class MarketingService extends Service {
   async emailUpdate(params = {}) {
     return await this.post('/approve/email/update', params)
   }
+  //获取国家列表
+  async Country(params = {}) {
+    return await this.post('/country/get', params)
+  }
 }
 export default new MarketingService()

+ 15 - 0
src/service/salaryPerformance.ts

@@ -0,0 +1,15 @@
+import Service from '../lib/service.js'
+import axios from 'axios'
+import Config from '../config'
+
+class salaryPerformanceService extends Service {
+  constructor() {
+    super()
+    axios.defaults.baseURL = Config.Host85
+  }
+  //下拉
+  async salesCommissionRuleDropdown(params = {}) {
+    return await this.post('/sales/commission/rule/search/dropdown', params)
+  }
+}
+export default new salaryPerformanceService()

+ 72 - 0
src/service/ucard.ts

@@ -227,6 +227,78 @@ class UCardService extends Service {
   async permissionPage(params = {}) {
     return await this.post('/wasabi/card/permission/page', params)
   }
+  // 证件类型配置删除
+  async idTypesConfigDelete(params = {}) {
+    return await this.post('/wasabi/card/id/type/config/delete', params)
+  }
+  // 证件类型配置新增
+  async idTypesConfigAdd(params = {}) {
+    return await this.post('/wasabi/card/id/type/config/add', params)
+  }
+  // 银行卡删除
+  async cardNumberDelete(params = {}) {
+    return await this.post('/wasabi/card/number/delete', params)
+  }
+  // 银行卡列表
+  async cardNumberList(params = {}) {
+    return await this.post('/wasabi/card/number/list', params)
+  }
+  // 银行卡修改
+  async cardNumberUpdate(params = {}) {
+    return await this.post('/wasabi/card/number/update/status', params)
+  }
+  // 操作记录
+  async operatePage(params = {}) {
+    return await this.post('/wasabi/card/operate/page', params)
+  }
+  //列表
+  async cardFileList(params = {}) {
+    return await this.post('/wasabi/card/file/search/list', params)
+  }
+  //激活操作指南
+  async cardFileAdd(params = {}) {
+    return await this.post('/wasabi/card/file/add', params)
+  }
+  //更新
+  async cardFileUpdate(params = {}) {
+    return await this.post('/wasabi/card/file/update', params)
+  }
+  //删除
+  async cardFileDelete(params = {}) {
+    return await this.post('/wasabi/card/file/delete', params)
+  }
+  //查询已认证用户列表
+  async cardUserList(params = {}) {
+    return await this.post('/wasabi/card/query/user/list', params)
+  }
+  //查询手续费率和汇率
+  async globalExchangeRate(params = {}) {
+    return await this.post('/wasabi/global/query/exchange/rate', params)
+  }
+  // 最近收款人列表
+  async globalReceiverList(params = {}) {
+    return await this.post('/wasabi/global/receiver/user/list', params)
+  }
+  //币种字段和可选值列表
+  async globalCurrenciesField(params = {}) {
+    return await this.post('/wasabi/global/currencies/field/list', params)
+  }
+  //查询支持的城市列表
+  async globalQueryBankCities(params = {}) {
+    return await this.post('/wasabi/global/query/bank/cities', params)
+  }
+  //查询ucard账户信息
+  async cardAccountDropdown(params = {}) {
+    return await this.post('/wasabi/card/account/dropdown', params)
+  }
+  // 订单详情
+  async globalOrdersDetail(params = {}) {
+    return await this.post('/wasabi/global/order/details', params)
+  }
+  //取消交易订单
+  async globalCancelOrder(params = {}) {
+    return await this.post('/wasabi/global/cancel/order', params)
+  }
 }
 
 export default new UCardService()

+ 136 - 0
src/service/user.ts

@@ -0,0 +1,136 @@
+import Service from '../lib/service.js'
+import axios from 'axios'
+import Config from '../config'
+
+class UserService extends Service {
+  constructor() {
+    super()
+    axios.defaults.baseURL = Config.Host85
+  }
+  //删除角色
+  async roleListDelete(params = {}) {
+    return await this.post('/user/role/delete', params)
+  }
+
+  //角色列表
+  async roleList(params = {}) {
+    return await this.post('/user/role/search/page', params)
+  }
+  //部门列表
+  async departmentList(params = {}) {
+    return await this.post('/department/search/list', params)
+  }
+
+  //权限查看add
+  async groupSingleAdd(params = {}) {
+    return await this.post('/user/role/add/detail/list', params)
+  }
+  //修改节点
+  async authorityNodeUpdate(params = {}) {
+    return await this.post('/authority/node/update', params)
+  }
+  //修改按钮
+  async authorityActionUpdate(params = {}) {
+    return await this.post('/authority/action/update', params)
+  }
+  //删除节点
+  async authorityNodeDelete(params = {}) {
+    return await this.post('/authority/node/delete', params)
+  }
+
+  //删除按钮
+  async authorityActionDelete(params = {}) {
+    return await this.post('/authority/action/delete', params)
+  }
+  //添加节点
+  async authorityNodeAdd(params = {}) {
+    return await this.post('/authority/node/add', params)
+  }
+
+  //权限查看update
+  async groupSingleUpdate(params = {}) {
+    return await this.post('/user/role/update/detail/list', params)
+  }
+
+  //修改角色
+  async roleListUpdate(params = {}) {
+    return await this.post('/user/role/update', params)
+  }
+
+  //添加角色
+  async roleListAdd(params = {}) {
+    return await this.post('/user/role/add', params)
+  }
+  //角色列表
+  async roleNameList(params = {}) {
+    return await this.post('/user/role/search/list', params)
+  }
+  //获取邮箱组别列表
+  async getGroupEmailList(params = {}) {
+    return await this.post('/google/group/user/list', params)
+  }
+  //角色列表
+  async userGroupGet(params = {}) {
+    return await this.post('/user/group/get', params)
+  }
+  //区域下拉
+  async arealListUP(params = {}) {
+    return await this.post('/sys/area/search/dd', params)
+  }
+  //用户列表
+  async userList(params = {}) {
+    return await this.post('/user/search/page', params)
+  }
+  //获取组别类型
+  async customGroupTypeList(params = {}) {
+    return await this.post('/custom/group/type/list', params)
+  }
+
+  //代理默认组别类型设置
+  async ibGroup(params = {}) {
+    return await this.post('/ib/update/login/group', params)
+  }
+
+  //用户开户设置
+  async userAccountSetup(params = {}) {
+    return await this.post('/user/update/show/login/type', params)
+  }
+  //解封
+  async userUnseal(params = {}) {
+    return await this.post('/user/unseal', params)
+  }
+  //重置密钥 后续绑定谷歌验证码
+  async secretKeyRe(params = {}) {
+    return await this.post('/user/re/secret/key', params)
+  }
+  //清理密钥 不接触绑定 无法通过谷歌验证码登录
+  async secretKeyClose(params = {}) {
+    return await this.post('/user/close/secret/key', params)
+  }
+  //重置密码
+  async rePassword(params = {}) {
+    return await this.post('/user/reset/password', params)
+  }
+  //获取谷歌密钥
+  async secretKeyGet(params = {}) {
+    return await this.post('/user/get/google/secretKey', params)
+  }
+  //删除用户
+  async userListDelete(params = {}) {
+    return await this.post('/user/delete', params)
+  }
+  //单个查询用户
+  async userSingle(params = {}) {
+    return await this.post('/user/search/single', params)
+  }
+  //添加用户
+  async userListAdd(params = {}) {
+    return await this.post('/user/add', params)
+  }
+  //修改用户
+  async userListUpdate(params = {}) {
+    return await this.post('/user/update', params)
+  }
+}
+
+export default new UserService()

+ 9 - 0
src/styles/cwg_common.scss

@@ -684,6 +684,15 @@ a {
 .crm_switch.el-switch.is-checked .el-switch__core::after {
   z-index: 2;
 }
+.el-table .state {
+  display: block;
+  min-width: 80px;
+  box-sizing: border-box;
+  line-height: 1.5;
+  border-radius: 2px;
+  padding: 2px 10px;
+  color: #ffffff;
+}
 
 //状态颜色
 .crm_state_blue {

+ 5 - 5
src/utils/export.js

@@ -5,12 +5,12 @@ const { t } = i18n.global
 
 /**
  * 导出表格
- * @param {Object} that - pigeon实例(弹窗实例)
+ * @param {Object} pigeon - pigeon实例(弹窗实例)
  * @param {String} url - 接口地址
  * @param {Object} params - 请求参数
  * @param {String} name - 文件名
  */
-export function exportExcel(that, url, params, name = 'Download') {
+export function exportExcel(pigeon, url, params, name = 'Download') {
   axios
     .post(url, { ...params }, { responseType: 'blob' })
     .then((res) => {
@@ -28,14 +28,14 @@ export function exportExcel(that, url, params, name = 'Download') {
             window.URL.revokeObjectURL(url)
           }
         } else {
-          that.MessageError(t('Msg.Download'))
+          pigeon.MessageError(t('Msg.Download'))
         }
       } catch (error) {
-        that.MessageError(t('Msg.Download'))
+        pigeon.MessageError(t('Msg.Download'))
       }
     })
     .catch(() => {
-      that.MessageError(t('Msg.SystemError'))
+      pigeon.MessageError(t('Msg.SystemError'))
     })
 }
 function formatDate(date = new Date(), format = 'yyyyMMddHHmmss') {

+ 154 - 0
src/utils/upload.js

@@ -0,0 +1,154 @@
+/**
+ * 统一文件上传工具
+ * 先调用原接口获取 uploadToken,再用新接口上传
+ */
+import axios from 'axios'
+import Config from '../config'
+
+/**
+ * 获取上传token(调用原上传接口)
+ * @param {File} file - 要上传的文件
+ * @param {String} originalUrl - 原上传接口地址(如 /common/upload)
+ * @returns {Promise<String>} uploadToken
+ */
+// async function getUploadToken(file, originalUrl = '/common/upload') {
+//   const formData = new FormData()
+//   formData.append('file', file)
+
+//   // 获取原接口的baseURL(通常是Host85)
+//   const baseURL = Config.Host85
+//   const headers = {}
+
+//   if (sessionStorage.getItem("access_token")) {
+//     headers['Access-Token'] = sessionStorage.getItem("access_token")
+//   }
+//   if (sessionStorage.getItem("lang")) {
+//     headers['Language'] = sessionStorage.getItem("lang")
+//   }
+//   if (sessionStorage.getItem("CLIENT")) {
+//     headers['CLIENT'] = sessionStorage.getItem("CLIENT")
+//   }
+
+//   try {
+//     const response = await axios.post(baseURL + originalUrl, formData, { headers })
+//     if (response.data && response.data.code === 200) {
+//       // 假设原接口返回的 data 就是 uploadToken,或者返回格式为 {uploadToken: "..."}
+//       // 如果返回的是字符串,直接使用;如果是对象,取 uploadToken 字段
+//       const token = typeof response.data.data === 'string'
+//         ? response.data.data
+//         : (response.data.data?.uploadToken || response.data.data)
+//       return token
+//     } else {
+//       throw new Error(response.data?.msg || '获取上传token失败')
+//     }
+//   } catch (error) {
+//     throw new Error(error.response?.data?.msg || error.message || '获取上传token失败')
+//   }
+// }
+async function upload(file, originalUrl = '/common/upload') {
+  const formData = new FormData()
+  formData.append('file', file)
+
+  // 获取原接口的baseURL(通常是Host85)
+  const baseURL = Config.Host85
+  const headers = {}
+
+  if (sessionStorage.getItem('access_token')) {
+    headers['Access-Token'] = sessionStorage.getItem('access_token')
+  }
+  if (sessionStorage.getItem('lang')) {
+    headers['Language'] = sessionStorage.getItem('lang')
+  }
+  if (sessionStorage.getItem('CLIENT')) {
+    headers['CLIENT'] = sessionStorage.getItem('CLIENT')
+  }
+
+  try {
+    const response = await axios.post(baseURL + originalUrl, formData, { headers })
+    if (response.data && response.data.code === 200) {
+      return response
+    } else {
+      throw new Error(response.data?.msg)
+    }
+  } catch (error) {
+    throw new Error(error.response?.data?.msg || error.message)
+  }
+}
+
+/**
+ * 统一文件上传方法
+ * @param {File} file - 要上传的文件
+ * @param {String} originalUrl - 原上传接口地址(可选,默认为 /common/upload)
+ * @returns {Promise<Object>} 上传结果 {code: 200, data: "文件路径", msg: "..."}
+ */
+export async function uploadFile(file, originalUrl = '/common/upload') {
+  try {
+    const response = await upload(file, originalUrl)
+    if (response.data && response.data.code === 200) {
+      return {
+        code: 200,
+        data: response.data.data,
+        msg: response.data.msg || '上传成功',
+      }
+    } else {
+      throw new Error(response.data?.msg || '上传失败')
+    }
+
+    // 1. 先调用原接口获取 uploadToken
+    // const uploadToken = await getUploadToken(file, originalUrl)
+    // // 2. 使用新接口上传,使用 Host05
+    // const formData = new FormData()
+    // formData.append('file', file)
+    // formData.append('uploadToken', uploadToken)
+
+    // const headers = {}
+    // if (sessionStorage.getItem("access_token")) {
+    //   headers['Access-Token'] = sessionStorage.getItem("access_token")
+    // }
+    // if (sessionStorage.getItem("lang")) {
+    //   headers['Language'] = sessionStorage.getItem("lang")
+    // }
+    // if (sessionStorage.getItem("CLIENT")) {
+    //   headers['CLIENT'] = sessionStorage.getItem("CLIENT")
+    // }
+
+    // const response = await axios.post(
+    //   Config.Host05 + '/common/base/upload',
+    //   formData,
+    //   { headers }
+    // )
+
+    // if (response.data && response.data.code === 200) {
+    //   return {
+    //     code: 200,
+    //     data: response.data.data,
+    //     msg: response.data.msg || '上传成功'
+    //   }
+    // } else {
+    //   throw new Error(response.data?.msg || '上传失败')
+    // }
+  } catch (error) {
+    return {
+      code: 400,
+      msg: error.response?.data?.msg || error.message || '上传失败',
+    }
+  }
+}
+
+/**
+ * 为 el-upload 组件提供的自定义上传方法
+ * @param {Object} option - el-upload 的 option 对象 {file, onSuccess, onError, onProgress}
+ * @param {String} originalUrl - 原上传接口地址(可选)
+ */
+export async function handleUpload(option, originalUrl = '/common/upload') {
+  try {
+    const result = await uploadFile(option.file, originalUrl)
+    if (result.code === 200) {
+      option.onSuccess({ ...option, ...result }, option.file)
+    } else {
+      option.onError(new Error(result.msg))
+    }
+  } catch (error) {
+    option.onError(error)
+  }
+}

+ 580 - 0
src/views/card/CardConfig/index.vue

@@ -0,0 +1,580 @@
+<template>
+  <div
+    id="review_Email"
+    v-loading="pictLoading"
+    class="view"
+    element-loading-background="rgba(43, 48, 67, 0.65)"
+    element-loading-spinner="el-icon-loading"
+  >
+    <div class="crm_search">
+      <el-form ref="formRef" :model="search" label-position="" label-width="">
+        <el-row>
+          <el-col :lg="24" :md="24" :span="24">
+            <el-form-item style="margin-right: 20px">
+              <el-input
+                v-model.trim="search.cardNumber"
+                :placeholder="t('card.Form.f24')"
+                class="crm-border-radius-no"
+                clearable
+                @keyup.enter="toSearch"
+              ></el-input>
+            </el-form-item>
+            <el-form-item>
+              <el-select
+                v-model="search.status"
+                :placeholder="t('card.Form.f45')"
+                class="crm-border-radius-no"
+                clearable
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="item in statusOptions"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button class="crm-border-radius-no crm-border-left-no" @click="toSearch">
+                <el-icon><Search /></el-icon>
+              </el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item v-if="display['R-CardConfig-Import'].show">
+          <div class="search_action_btn">
+            <span class="crm-cursor delete active" @click="Upload">
+              {{ t('R-CardConfig-Import') }}
+            </span>
+          </div>
+        </el-form-item>
+        <el-form-item v-if="display['R-CardConfig-Delete'].show">
+          <div class="search_action_btn">
+            <span class="crm-cursor delete active" @click="deleteAll">
+              {{ t('R-CardConfig-Delete') }}
+            </span>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <div class="card-mock-demo" style="margin: 30px 0">
+        <el-table
+          :data="mock_tableData"
+          stripe
+          style="margin-top: 20px; width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column align="left" type="selection" width="55"> </el-table-column>
+          <el-table-column :label="t('card.Form.f24')" align="left" prop="cardNumber" />
+          <el-table-column :label="t('card.Form.f45')" align="left" prop="status">
+            <template #default="scope">
+              <span v-if="scope.row.status == 3" class="state crm_state_gray">
+                {{ t('State.Used') }}
+              </span>
+              <span v-else-if="scope.row.status == 2" class="state crm_state_orange">
+                {{ t('State.InUse') }}
+              </span>
+              <span v-else class="state crm_state_yellow">
+                {{ t('State.NotUsed') }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="" align="center" :label="t('Label.Action')">
+            <template #default="scope">
+              <el-dropdown trigger="click" @command="handleCommand">
+                <span class="el-dropdown-link crm-cursor">
+                  <el-icon style="font-weight: bold; font-size: 20px">
+                    <MoreFilled />
+                  </el-icon>
+                </span>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item
+                      v-if="display['R-CardConfig-Update'].show"
+                      :command="{ type: 2, row: scope.row }"
+                    >
+                      <el-icon><Edit /></el-icon>
+                      {{ t('R-CardConfig-Update') }}
+                    </el-dropdown-item>
+                    <el-dropdown-item
+                      v-if="display['R-CardConfig-Delete'].show"
+                      :command="{ type: 1, row: scope.row }"
+                    >
+                      <el-icon><Delete /></el-icon>
+                      {{ t('R-CardConfig-Delete') }}
+                    </el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+    <PagePagination
+      :pager-info="pagerInfo"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+    <!-- 上传弹出 -->
+    <el-dialog
+      v-model="dialogCheckUpload"
+      :title="t('R-SynonymTransfer-Upload')"
+      center
+      custom-class="dialog_header_w"
+      width="600px"
+    >
+      <div
+        v-loading="fileLoading"
+        class="dia-content"
+        element-loading-background="rgba(43, 48, 67, 0.65)"
+        element-loading-spinner="el-icon-loading"
+      >
+        <div class="uploadBox">
+          <div class="title">
+            <div class="tit"></div>
+            <div>
+              {{ t('Marketing.SampleDownload') }}
+              <span class="demo crm-cursor" @click="exportAgents">Demo</span>
+            </div>
+          </div>
+          <div class="upload">
+            <el-upload
+              ref="uploadRef"
+              :action="action"
+              accept=".xlsx"
+              :data="{ name: name }"
+              :headers="AccessToken"
+              :show-file-list="false"
+              :auto-upload="false"
+              :limit="1"
+              :on-exceed="handleExceed"
+              :on-success="handleAvatarSuccess"
+              :on-change="handlePreview"
+            >
+              <template #trigger>
+                <el-button type="primary">
+                  {{ t('Marketing.SelectFile') }}
+                </el-button>
+              </template>
+            </el-upload>
+            <span v-if="file" style="margin-left: 10px">{{ file }}</span>
+            <span v-else style="margin-left: 10px">
+              {{ t('Marketing.NoFileSelected') }}
+            </span>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitUpload()">
+            {{ t('Btn.Confirm') }}
+          </el-button>
+          <el-button @click="Cancel">
+            {{ t('Btn.Cancel') }}
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <el-dialog
+      v-if="dialogCheck"
+      v-model="dialogCheck"
+      :title="t('R-CardConfig')"
+      :rules="rules"
+      width="900px"
+    >
+      <el-form
+        ref="dialogFormRef"
+        :rules="rules"
+        :model="form"
+        label-width="130px"
+        label-position="right"
+        class="business-edit-form"
+      >
+        <el-form-item prop="cardNumber" :label="t('card.Form.f24')">
+          <el-input v-model="form.cardNumber" disabled></el-input>
+        </el-form-item>
+
+        <el-form-item prop="status" :label="t('card.Form.f45')">
+          <el-select v-model="form.status" :placeholder="t('vaildate.select.empty')">
+            <el-option
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="dialogCheck = false">
+            {{ t('Ucard.Business.p32') }}
+          </el-button>
+          <el-button type="primary" @click="cardNumberUpdate">
+            {{ t('card.Btn.Confirm') }}
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, onMounted, watch, inject, nextTick } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { ElMessage, ElMessageBox } from 'element-plus'
+  import { Search, MoreFilled, Operation, Edit, Delete } from '@element-plus/icons-vue'
+  import PagePagination from '@/components/pagePagination'
+  import Service from '@/service/ucard'
+  import Config from '@/config/index'
+  import { exportExcel } from '@/utils/export'
+
+  const { t } = useI18n()
+  const { Code } = Config
+  const Session = inject('session')
+  const pigeon = inject('pigeon')
+
+  // Refs
+  const formRef = ref(null)
+  const dialogFormRef = ref(null)
+  const uploadRef = ref(null)
+  const pictLoading = ref(false)
+  const dialogCheckUpload = ref(false)
+  const dialogCheck = ref(false)
+  const file = ref('')
+  const name = ref('')
+  const fileLoading = ref(false)
+
+  // Constants
+  const action = Config.Host85 + '/wasabi/card/number/import'
+  const url = Config.Host85
+
+  // 定义选项数组
+  const statusOptions = [
+    { value: 1, label: t('State.NotUsed') },
+    { value: 2, label: t('State.InUse') },
+    { value: 3, label: t('State.Used') },
+  ]
+
+  // Reactive data
+  const search = reactive({
+    status: '',
+    cardNumber: '',
+  })
+
+  const form = reactive({
+    status: '',
+    cardNumber: '',
+  })
+
+  const mock_tableData = ref([])
+  const multipleSelection = ref([])
+
+  const pagerInfo = reactive({ row: 10, current: 1, pageTotal: 0, rowTotal: 0 })
+
+  const rules = reactive({
+    idType: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'blur',
+      },
+    ],
+    country: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'blur',
+      },
+    ],
+  })
+
+  // Computed properties
+  const display = computed(() => {
+    const displayStr = Session.Get('display', true)
+    return displayStr ? JSON.parse(displayStr) : {}
+  })
+
+  const user = computed(() => {
+    const userStr = Session.Get('user', true)
+    return userStr ? JSON.parse(userStr) : {}
+  })
+
+  const AccessToken = computed(() => {
+    return {
+      'Access-Token': Session.Get('access_token'),
+    }
+  })
+
+  // Methods
+  const deleteAll = async () => {
+    if (!multipleSelection.value.length) {
+      return
+    }
+
+    try {
+      await ElMessageBox.confirm(t('Msg.Delete'), t('Msg.SystemPrompt'), {
+        confirmButtonText: t('Btn.Confirm'),
+        cancelButtonText: t('Btn.Cancel'),
+        type: 'warning',
+      })
+
+      const res = await Service.cardNumberDelete({
+        ids: multipleSelection.value,
+      })
+      if (res.code == Code.StatusOK) {
+        ElMessage.success(t('Msg.DeleteSuccess'))
+        searchFunc()
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } catch (error) {
+      // 用户取消删除
+    }
+  }
+
+  const handleCommand = (command) => {
+    switch (command.type) {
+      case 1:
+        multipleSelection.value = [command.row.id]
+        deleteAll()
+        break
+      case 2: {
+        Object.assign(form, JSON.parse(JSON.stringify(command.row)))
+        dialogCheck.value = true
+        break
+      }
+    }
+  }
+
+  //选择多项
+  const handleSelectionChange = (val) => {
+    multipleSelection.value = []
+    val.forEach((item) => {
+      multipleSelection.value.push(item.id)
+    })
+  }
+
+  const Cancel = () => {
+    dialogCheckUpload.value = false
+  }
+
+  const submitUpload = () => {
+    if (!file.value) {
+      ElMessage.warning(t('Msg.upload'))
+      return
+    }
+    fileLoading.value = true
+    uploadRef?.value.submit()
+  }
+
+  const handleAvatarSuccess = (res) => {
+    if (res.code == 200) {
+      ElMessage.success(t('Msg.uploadSuccess'))
+      Cancel()
+      toSearch()
+    } else {
+      ElMessage.error(res.msg)
+    }
+    fileLoading.value = false
+  }
+
+  const handlePreview = (file) => {
+    if (!file.percentage) {
+      file.value = file.name
+    } else {
+      file.value = ''
+    }
+  }
+
+  const handleExceed = (files) => {
+    if (uploadRef.value) {
+      uploadRef.value.clearFiles()
+      const latest = files && files.length ? files[0] : null
+      if (latest) {
+        file.value = latest.name || ''
+        nextTick(() => {
+          if (uploadRef.value && uploadRef.value.handleStart) {
+            uploadRef.value.handleStart(latest)
+          }
+        })
+      }
+    }
+  }
+
+  const beforeAvatarUpload = (file) => {
+    const isLt2M = file.size / 1024 / 1024 < 3
+
+    if (!isLt2M) {
+      ElMessage.error(t('news_add_field.Des.item1'))
+    }
+    return isLt2M
+  }
+
+  //导出
+  const exportAgents = async () => {
+    exportExcel(
+      pigeon,
+      '/wasabi/card/number/template/export',
+      { ...search },
+      'Card_Number_Template'
+    )
+  }
+
+  const Upload = () => {
+    dialogCheckUpload.value = true
+  }
+
+  const toSearch = () => {
+    pagerInfo.current = 1
+    searchFunc()
+  }
+
+  // 列表
+  const searchFunc = async () => {
+    pictLoading.value = true
+
+    if (!display.value['R-CardConfig-List']?.show) {
+      ElMessage.warning(t('Msg.NotDisplay'))
+      pictLoading.value = false
+      return
+    }
+
+    const res = await Service.cardNumberList({
+      ...search,
+      page: {
+        current: pagerInfo.current,
+        row: pagerInfo.row,
+      },
+    })
+
+    if (res.code == Code.StatusOK) {
+      mock_tableData.value = res.data
+      if (res.page != null) {
+        pagerInfo.rowTotal = res.page.rowTotal
+        pagerInfo.pageTotal = res.page.pageTotal
+      } else {
+        pagerInfo.rowTotal = 0
+      }
+      ElMessage.success(t('Msg.SearchSuccess'))
+    } else {
+      ElMessage.error(res.msg)
+    }
+    pictLoading.value = false
+  }
+
+  const handleSizeChange = (val) => {
+    pagerInfo.row = val
+    searchFunc()
+  }
+
+  const handleCurrentChange = (val) => {
+    pagerInfo.current = val
+    searchFunc()
+  }
+
+  const cardNumberUpdate = async () => {
+    try {
+      const valid = await dialogFormRef.value.validate()
+      if (!valid) return
+
+      const res = await Service.cardNumberUpdate({ ...form })
+      if (res.code == 200) {
+        Object.assign(form, {
+          country: null,
+          idType: null,
+        })
+        dialogCheck.value = false
+        toSearch()
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } catch (error) {
+      console.log(error)
+      ElMessage.error(t('Msg.SystemError'))
+    }
+  }
+
+  // Lifecycle hooks
+  onMounted(() => {
+    searchFunc()
+  })
+</script>
+
+<style lang="scss" scoped>
+  #review_Email {
+    .crm_search {
+      .search_action_btn {
+        .delete {
+          background-color: #a1a1a1;
+        }
+
+        .delete.active {
+          background-color: #368fec;
+        }
+      }
+    }
+
+    .el-table .state {
+      display: inline-block;
+      min-width: 80px;
+      max-width: 150px;
+      box-sizing: border-box;
+      line-height: 1.5;
+      border-radius: 2px;
+      padding: 2px 10px;
+      color: #ffffff;
+    }
+
+    .crm_switch {
+      :deep(.el-form-item__content) {
+        text-align: left;
+      }
+    }
+  }
+</style>
+<style lang="scss">
+  #review_Email {
+    .dialog_header_w {
+      .crm_search_down,
+      .el-input__inner {
+        width: 400px;
+      }
+      .number-range-input {
+        .el-input__inner {
+          width: 190px !important;
+        }
+      }
+    }
+    .uploadBox {
+      padding: 25px;
+      @include bg_gray_7();
+      .title {
+        display: flex;
+        justify-content: space-between;
+        .tit {
+          font-weight: bold;
+        }
+        .demo {
+          @include font_blue_btn_1();
+        }
+      }
+      .input-all {
+        margin: 15px 0;
+      }
+      .upload {
+        display: flex;
+        align-items: center;
+      }
+      .btn-foot {
+        text-align: right;
+      }
+    }
+    .width-100 {
+      width: 100% !important;
+    }
+  }
+</style>

+ 61 - 0
src/views/card/CardFile/const.ts

@@ -0,0 +1,61 @@
+// 语言选项配置
+export const languageOptions = [
+  { value: 'cn', label: 'Web_info.cn' },
+  { value: 'zhHant', label: 'Web_info.zhHant' },
+  { value: 'en', label: 'Web_info.en' },
+  { value: 'vi', label: 'Web_info.vn' },
+  { value: 'th', label: 'Web_info.th' },
+  { value: 'ar', label: 'Web_info.ar' },
+  { value: 'de', label: 'Web_info.de' },
+  { value: 'es', label: 'Web_info.es' },
+  { value: 'id', label: 'Web_info.id' },
+  { value: 'ms', label: 'Web_info.ms' },
+  { value: 'ko', label: 'Web_info.ko' },
+  { value: 'pt', label: 'Web_info.pt' },
+  { value: 'fa', label: 'Web_info.fa' },
+  { value: 'tr', label: 'Web_info.tr' },
+]
+
+export const languageObj = {
+  cn: 'Web_info.cn',
+  zhHant: 'Web_info.zhHant',
+  en: 'Web_info.en',
+  vi: 'Web_info.vn',
+  th: 'Web_info.th',
+  ar: 'Web_info.ar',
+  de: 'Web_info.de',
+  es: 'Web_info.es',
+  id: 'Web_info.id',
+  ms: 'Web_info.ms',
+  ko: 'Web_info.ko',
+  pt: 'Web_info.pt',
+  fa: 'Web_info.fa',
+  tr: 'Web_info.tr',
+}
+
+export const JPGs = [
+  'mp4',
+  'avi',
+  '3gp',
+  'wmv',
+  'asf',
+  'navi',
+  'mov',
+  'mpeg',
+  'rm',
+  'rmvb',
+  'flv',
+  'f4v',
+  'MP4',
+  'AVI',
+  '3GP',
+  'WMV',
+  'ASF',
+  'NAVI',
+  'MOV',
+  'MPEG',
+  'RM',
+  'RMVB',
+  'FLV',
+  'F4V',
+]

+ 71 - 0
src/views/card/CardFile/index.scss

@@ -0,0 +1,71 @@
+#card_file {
+  .crm_search {
+    .search_action_btn {
+      .delete {
+        background-color: #a1a1a1;
+      }
+
+      .delete.active {
+        background-color: #368fec;
+      }
+    }
+  }
+
+  .avatar-uploader {
+    display: flex;
+    justify-content: start;
+  }
+
+  /* 弹窗中步骤/视频样式 */
+  .steps-wrap,
+  .videos-wrap {
+
+    .step-item,
+    .video-item {
+      border: 1px solid #eef1f6;
+      padding: 12px;
+      margin-bottom: 12px;
+      border-radius: 6px;
+      background: #fff;
+    }
+
+    .step-header,
+    .video-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 8px;
+    }
+
+    .step-body,
+    .video-body {
+      align-items: center;
+    }
+
+    .step-actions,
+    .video-actions {
+      > .el-button {
+        margin-left: 6px;
+      }
+    }
+
+  }
+
+  .mini-uploader {
+    border: 1px dashed #d9d9d9;
+    padding: 6px;
+    text-align: center;
+    background: #fafafa;
+  }
+
+  .upload-placeholder {
+    color: #999;
+    padding: 10px 0;
+  }
+
+  .upload-pdf {
+    //width: 200px;
+    height: 20px;
+    display: block;
+  }
+}

+ 727 - 0
src/views/card/CardFile/index.vue

@@ -0,0 +1,727 @@
+<template>
+  <div
+    id="card_file"
+    v-loading="pictLoading"
+    class="view"
+    element-loading-background="rgba(43, 48, 67, 0.65)"
+    element-loading-spinner="el-icon-loading"
+  >
+    <div class="crm_search">
+      <el-form ref="formRef" label-position="" :model="search" label-width="">
+        <el-row>
+          <el-col :span="24" :md="24" :lg="24">
+            <el-form-item>
+              <el-select
+                v-model="search.lang"
+                class="crm-border-radius-no"
+                clearable
+                :placeholder="$t('Label.Lang')"
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="lang in languageOptions"
+                  :key="lang.value"
+                  :label="$t(lang.label)"
+                  :value="lang.value"
+                >
+                </el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button
+                class="crm-border-radius-no crm-border-left-no"
+                :icon="Search"
+                @click="toSearch"
+              ></el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item>
+          <div class="search_action_btn">
+            <span class="crm-cursor" @click="toNew">
+              <el-icon><Plus /></el-icon>
+              <span>{{ $t('Btn.Add') }}</span>
+            </span>
+            <span
+              v-if="display['R-CardFile-Delete']?.show"
+              class="crm-cursor delete"
+              :class="{ active: multipleSelection.length }"
+              @click="toDeleteBatch"
+            >
+              <el-icon><Minus /></el-icon>
+              <span>{{ $t('Btn.Delete') }}</span>
+            </span>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+    <el-table
+      :data="mock_tableData"
+      stripe
+      style="width: 100%"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column align="left" type="selection" width="55"> </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('system_info.Label.Lang')">
+        <template #default="scope">
+          <span>
+            {{ $t(languageObj[scope.row.lang]) }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="addTime" align="left" :label="$t('Label.Date')"> </el-table-column>
+      <el-table-column prop="" align="center" :label="$t('Label.Action')">
+        <template #default="scope">
+          <el-dropdown trigger="click" @command="handleCommand">
+            <span class="el-dropdown-link crm-cursor">
+              <i style="font-weight: bold; font-size: 20px" class="iconfont iconcaidan"></i>
+            </span>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item
+                  v-if="display['R-CardFile-Update']?.show"
+                  :command="{ type: 'editor', row: scope.row }"
+                >
+                  <el-icon><Edit /></el-icon>
+                  <span>{{ $t('Btn.Editor') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-CardFile-Delete']?.show"
+                  :command="{ type: 'delete', id: scope.row.id }"
+                >
+                  <el-icon><Delete /></el-icon>
+                  <span>{{ $t('Btn.Delete') }}</span>
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+    <PagePagination
+      :pager-info="pagerInfo"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+    <el-dialog
+      v-if="dialogCheck"
+      v-model="dialogCheck"
+      :close-on-click-modal="false"
+      :rules="rules"
+      :title="isAdd ? $t('R-CardFile-Add') : $t('R-CardFile-Update')"
+      width="600px"
+    >
+      <el-form
+        ref="dialogFormRef"
+        :model="form"
+        :rules="dialogRules"
+        class="business-edit-form"
+        label-position="right"
+        label-width="120px"
+      >
+        <!-- 语言 -->
+        <el-form-item :label="$t('Label.Lang') + ':'" prop="lang">
+          <el-select
+            v-model="form.lang"
+            style="width: 100%"
+            :disabled="!isAdd"
+            :placeholder="$t('Placeholder.Choose')"
+          >
+            <el-option
+              v-for="lang in languageOptions"
+              :key="lang.value"
+              :label="$t(lang.label)"
+              :value="lang.value"
+            >
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <!-- ========== setpList 区块 ========== -->
+        <el-form-item :label="$t('card.CardFile.p1') + ':'" prop="setpList">
+          <div class="steps-wrap">
+            <div v-for="(step, idx) in form.setpList" :key="'step-' + idx" class="step-item">
+              <div class="step-header">
+                <span>{{ $t('card.CardFile.p2') }} {{ step.setp }}</span>
+                <div class="step-actions">
+                  <el-button
+                    type="primary"
+                    :icon="Plus"
+                    size="small"
+                    title="Add step after"
+                    @click="addStep(idx)"
+                  ></el-button>
+                  <el-button
+                    v-if="form.setpList.length > 1"
+                    type="danger"
+                    :icon="Minus"
+                    size="small"
+                    title="Remove"
+                    @click="removeStep(idx)"
+                  ></el-button>
+                </div>
+              </div>
+
+              <el-row :gutter="12" class="step-body">
+                <el-col :span="24" style="margin-bottom: 20px">
+                  <el-input
+                    v-model="step.d"
+                    type="textarea"
+                    :autosize="{ minRows: 2, maxRows: 10 }"
+                    :placeholder="$t('card.CardFile.p3')"
+                  ></el-input>
+                </el-col>
+
+                <el-col :span="24" style="margin-bottom: 20px">
+                  <el-select v-model="step.type" :placeholder="$t('Label.Type')">
+                    <el-option
+                      v-for="type in uploadTypes"
+                      :key="type.value"
+                      :label="$t(type.label)"
+                      :value="type.value"
+                    >
+                    </el-option>
+                  </el-select>
+                </el-col>
+
+                <el-col :span="24" style="margin-bottom: 20px">
+                  <div v-if="step.type == '1'">
+                    <el-upload
+                      :http-request="(opt) => handleFileUpload(opt, idx)"
+                      :on-success="handleSuccessStep"
+                      accept="image/png, image/jpeg, .jpg, .jpeg, .png"
+                      :action="''"
+                      :show-file-list="false"
+                      drag
+                      class="mini-uploader"
+                    >
+                      <img
+                        v-if="step.img"
+                        :src="imgUrl + step.img"
+                        style="width: 100%; max-height: 80px; object-fit: cover"
+                      />
+                      <div v-else class="upload-placeholder">{{ $t('card.CardFile.p5') }}</div>
+                    </el-upload>
+                  </div>
+                  <div v-else>
+                    <el-input v-model="step.img" :placeholder="$t('card.CardFile.p5')"></el-input>
+                  </div>
+                </el-col>
+              </el-row>
+            </div>
+          </div>
+        </el-form-item>
+
+        <!-- ========== videoList 区块 ========== -->
+        <el-form-item :label="$t('card.CardFile.p6') + ':'" prop="videoList">
+          <div class="videos-wrap">
+            <div v-for="(vitem, vidx) in form.videoList" :key="'video-' + vidx" class="video-item">
+              <div class="video-header">
+                <span>{{ $t('card.CardFile.p7') }} {{ vidx + 1 }}</span>
+                <div class="video-actions">
+                  <!-- <el-button type="primary" icon="el-icon-plus" size="mini" @click="addVideo(vidx)"
+                    title="Add video after" v-if="form.videoList.length != 2"></el-button>
+                  <el-button type="danger" icon="el-icon-minus" size="mini" @click="removeVideo(vidx)"
+                    v-if="form.videoList.length > 1" title="Remove"></el-button> -->
+                </div>
+              </div>
+
+              <el-row :gutter="12" class="video-body">
+                <el-col :span="24" style="margin-bottom: 20px">
+                  <el-select v-model="vitem.type" :placeholder="$t('Label.Type')">
+                    <el-option
+                      v-for="type in uploadTypes"
+                      :key="type.value"
+                      :label="$t(type.label)"
+                      :value="type.value"
+                    >
+                    </el-option>
+                  </el-select>
+                </el-col>
+
+                <el-col :span="24" style="margin-bottom: 20px">
+                  <div v-if="vitem.type == '1'">
+                    <el-upload
+                      :http-request="(opt) => handleVideoUpload(opt, vidx)"
+                      :action="''"
+                      :show-file-list="false"
+                      :before-upload="beforeAvatarUpload"
+                      :on-success="handleSuccessVideo"
+                      drag
+                      class="mini-uploader"
+                    >
+                      <video
+                        v-if="vitem.fileUrl"
+                        :src="imgUrl + vitem.fileUrl"
+                        style="width: 100%; max-height: 120px; object-fit: cover"
+                        controls
+                      ></video>
+                      <div v-else class="upload-placeholder">{{ $t('card.CardFile.p9') }}</div>
+                    </el-upload>
+                  </div>
+                  <div v-else>
+                    <el-input
+                      v-model="vitem.fileUrl"
+                      :placeholder="$t('card.CardFile.p8')"
+                    ></el-input>
+                  </div>
+                </el-col>
+              </el-row>
+            </div>
+          </div>
+        </el-form-item>
+        <el-form-item :label="$t('card.CardFile.p10') + ':'">
+          <div class="steps-wrap">
+            <div class="step-item">
+              <div class="step-header">
+                <span>{{ $t('card.CardFile.p10') }} </span>
+              </div>
+              <el-row :gutter="12" class="step-body">
+                <el-col :span="24" style="margin-bottom: 20px">
+                  <el-input
+                    v-model="form.pdfPassword"
+                    :placeholder="$t('card.CardFile.p11')"
+                  ></el-input>
+                </el-col>
+                <el-col :span="24" style="margin-bottom: 20px">
+                  <el-upload
+                    :http-request="(opt) => handleFileUpload(opt)"
+                    :action="''"
+                    :show-file-list="false"
+                    accept="application/pdf"
+                    :on-success="handleSuccessPdf"
+                    drag
+                    class="mini-uploader"
+                  >
+                    <div class="upload-placeholder">{{ $t('card.CardFile.p12') }}</div>
+                  </el-upload>
+                  <a
+                    v-if="form.pdfPath"
+                    class="upload-pdf"
+                    target="_blank"
+                    :href="imgUrl + form.pdfPath"
+                    controls
+                    >{{ form.pdfPath }}</a
+                  >
+                </el-col>
+              </el-row>
+            </div>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="onDialogCancel">{{ $t('Ucard.Business.p32') }}</el-button>
+          <el-button type="primary" @click="cardFileAdd">{{ $t('card.Btn.Confirm') }}</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, onMounted, watch, inject } from 'vue'
+  import Service from '@/service/ucard'
+  import Config from '@/config/index'
+  import { handleUpload } from '@/utils/upload'
+  import crypt from '@/lib/crypt'
+  import PagePagination from '@/components/pagePagination'
+  import { JPGs, languageObj, languageOptions } from '@/views/card/CardFile/const'
+  import { Delete, Edit, Minus, Plus, Search } from '@element-plus/icons-vue'
+  import { useI18n } from 'vue-i18n'
+
+  const { Code } = Config
+  const { t } = useI18n()
+  const Session = inject('session')
+  const pigeon = inject('pigeon')
+
+  // 上传类型选项
+  const uploadTypes = [
+    { value: '1', label: 'news_add_field.Label.Upload' },
+    { value: '2', label: 'news_add_field.Label.Url' },
+  ]
+
+  const pictLoading = ref(false)
+  const dialogCheck = ref(false)
+  const isAdd = ref(true)
+  const formRef = ref(null)
+  const dialogFormRef = ref(null)
+
+  const form = reactive({
+    lang: '',
+    tag: '',
+    setpList: [{ setp: 1, type: '2', d: '', img: '' }],
+    videoList: [
+      { type: '2', fileUrl: '', d: '' },
+      { type: '2', fileUrl: '', d: '' },
+    ],
+    pdfPassword: '',
+    pdfPath: '',
+  })
+
+  const mock_tableData = ref([])
+  const search = reactive({
+    valid: '',
+    tag: '',
+    lang: null,
+    date: [],
+    startDate: '',
+    endDate: '',
+  })
+
+  const url = Config.Host85
+  const imgUrl = Config.Host85
+  const imageUrl1 = ref('')
+  const uploadDes = ref('')
+  const multipleSelection = ref([])
+
+  const tags_options = [
+    {
+      tag: '1',
+      name: 'R-CardFil-tag',
+    },
+  ]
+
+  const tagMap = {
+    1: 'R-CardFil-tag',
+  }
+
+  const rules = {
+    tag: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'change',
+      },
+    ],
+    lang: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'change',
+      },
+    ],
+    type: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'change',
+      },
+    ],
+    fileUrl: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'change',
+      },
+    ],
+  }
+
+  const pagerInfo = reactive({ row: 10, current: 1, pageTotal: 0, rowTotal: 0 })
+
+  const display = computed(() => {
+    return JSON.parse(Session.Get('display', true))
+  })
+
+  const AccessToken = computed(() => {
+    return {
+      'Access-Token': Session.Get('access_token'),
+    }
+  })
+
+  const dialogRules = computed(() => {
+    return {
+      lang: [{ required: true, message: t('vaildate.select.empty'), trigger: 'change' }],
+      tag: [{ required: true, message: t('vaildate.select.empty'), trigger: 'change' }],
+    }
+  })
+
+  // 方法定义
+  const isPdf = (url) => {
+    return /\.pdf(\?.*)?$/i.test(url)
+  }
+
+  const handleCommand = (command) => {
+    if (command.type == 'delete') {
+      multipleSelection.value = [command.id]
+      toDeleteBatch(command.id)
+    } else if (command.type == 'editor') {
+      toUpdate(command.row)
+    }
+  }
+
+  const handleVideoUpload = (option, index) => {
+    handleUpload({ ...option, index }, '/wasabi/card/upload/video')
+  }
+
+  const handleFileUpload = (option, index) => {
+    handleUpload({ ...option, index }, '/common/upload')
+  }
+
+  const handleSuccessStep = (res) => {
+    const { index, data } = res
+    form.setpList[index].img = data
+  }
+
+  const handleSuccessVideo = (res) => {
+    const { index, data } = res
+    form.videoList[index].fileUrl = data
+  }
+
+  const handleSuccessPdf = (res) => {
+    const { data } = res
+    form.pdfPath = data
+  }
+
+  const handleAvatarSuccess1 = (res, file) => {
+    if (res.code == 200) {
+      if (file && file.raw) {
+        imageUrl1.value = URL.createObjectURL(file.raw)
+      }
+      form.fileUrl = res.data
+      uploadDes.value = t('Msg.Success')
+    } else {
+      // 使用全局message
+      console.error('Msg.Fail')
+    }
+  }
+
+  const beforeAvatarUpload = (file) => {
+    const isJPG = JPGs.includes(file.name.split('.').pop())
+
+    const isLt2M = file.size / 1024 / 1024 < 100
+
+    if (!isJPG) {
+      pigeon.MessageError(t('Msg.mp4'))
+    }
+    if (!isLt2M) {
+      pigeon.MessageError(t('Msg.10IMG'))
+    }
+    if (isJPG && isLt2M) {
+      uploadDes.value = t('news_add_field.Label.Uploading')
+    }
+    return isJPG && isLt2M
+  }
+
+  const searchFunc = async () => {
+    pictLoading.value = true
+    if (!display.value['R-CardFile-List']?.show) {
+      pigeon.MessageWarning(t('Msg.NotDisplay'))
+      pictLoading.value = false
+      return
+    }
+
+    if (search.date == null || search.date.length == 0) {
+      search.startDate = ''
+      search.endDate = ''
+    } else {
+      search.startDate = search.date[0]
+      search.endDate = search.date[1]
+    }
+
+    let res = await Service.cardFileList({
+      ...search,
+      page: {
+        current: pagerInfo.current,
+        row: pagerInfo.row,
+      },
+    })
+
+    if (res.code == Code.StatusOK) {
+      mock_tableData.value = res.data
+      if (res.page != null) {
+        pagerInfo.rowTotal = res.page.rowTotal
+        pagerInfo.pageTotal = res.page.pageTotal
+      } else {
+        pagerInfo.rowTotal = 0
+      }
+      pigeon.MessageOK(t('Msg.SearchSuccess'))
+      pictLoading.value = false
+    } else {
+      pigeon.MessageError(res.msg)
+      pictLoading.value = false
+    }
+  }
+
+  const getSearchDate = (e) => {
+    search.date = e
+  }
+
+  const toSearch = () => {
+    pagerInfo.current = 1
+    searchFunc()
+  }
+
+  const toNew = () => {
+    Object.assign(form, {
+      lang: '',
+      tag: '',
+      setpList: [{ setp: 1, type: '2', d: '', img: '' }],
+      videoList: [
+        { type: '2', fileUrl: '', d: '' },
+        { type: '2', fileUrl: '', d: '' },
+      ],
+      pdfPassword: '',
+      pdfPath: '',
+    })
+    imageUrl1.value = ''
+    isAdd.value = true
+    dialogCheck.value = true
+  }
+
+  const toUpdate = (row) => {
+    let parsed = {}
+    try {
+      parsed =
+        typeof row.fileInfo === 'string' && row.fileInfo.trim() ? JSON.parse(row.fileInfo) : {}
+    } catch {
+      parsed = {}
+    }
+
+    Object.assign(form, {
+      ...row,
+      lang: row.lang || '',
+      tag: row.tag || '',
+      ...parsed,
+      pdfPassword: crypt.Decrypt(row.pdfPassword),
+    })
+
+    dialogCheck.value = true
+    isAdd.value = false
+  }
+
+  const addStep = (afterIndex) => {
+    const insertAt = typeof afterIndex === 'number' ? afterIndex + 1 : form.setpList.length
+    form.setpList.splice(insertAt, 0, {
+      setp: insertAt + 1,
+      type: '2',
+      d: '',
+      img: '',
+    })
+    reindexSteps()
+  }
+
+  const removeStep = (index) => {
+    if (form.setpList.length <= 1) return
+    form.setpList.splice(index, 1)
+    reindexSteps()
+  }
+
+  const reindexSteps = () => {
+    form.setpList.forEach((s, i) => (s.setp = i + 1))
+  }
+
+  const addVideo = (afterIndex) => {
+    if (form.videoList.length >= 2) {
+      console.warn('最多只能添加两个视频')
+      return
+    }
+    const insertAt = typeof afterIndex === 'number' ? afterIndex + 1 : form.videoList.length
+    form.videoList.splice(insertAt, 0, { type: '2', fileUrl: '', d: '' })
+  }
+
+  const removeVideo = (index) => {
+    if (form.videoList.length <= 1) return
+    form.videoList.splice(index, 1)
+  }
+
+  const cardFileAdd = async () => {
+    if (!dialogFormRef.value) return
+    try {
+      const valid = await dialogFormRef.value.validate()
+      if (!valid) return
+    } catch (e) {
+      return
+    }
+
+    if (!Array.isArray(form.setpList) || form.setpList.length === 0) {
+      form.setpList = [{ setp: 1, type: '2', d: '', img: '' }]
+    }
+    if (!Array.isArray(form.videoList) || form.videoList.length === 0) {
+      form.videoList = [{ type: '2', fileUrl: '', d: '' }]
+    }
+
+    let res
+    const payload = {
+      ...form,
+      type: '1',
+      fileInfo: JSON.stringify({
+        setpList: form.setpList,
+        videoList: form.videoList,
+      }),
+      pdfPassword: crypt.Encrypt(String(form.pdfPassword)),
+    }
+
+    if (isAdd.value) {
+      res = await Service.cardFileAdd(payload)
+    } else {
+      res = await Service.cardFileUpdate(payload)
+    }
+
+    if (res && res.code == 200) {
+      toNew()
+      dialogCheck.value = false
+      pigeon.MessageOK(t('Msg.Success'))
+      searchFunc()
+    } else {
+      pigeon.MessageError(res.msg || t('Msg.Fail'))
+    }
+  }
+
+  const onDialogCancel = () => {
+    dialogCheck.value = false
+  }
+
+  const toDeleteBatch = async () => {
+    if (!multipleSelection.value.length) {
+      return
+    }
+    pigeon.MessageConfirm(
+      t('Msg.Delete'),
+      t('Msg.SystemPrompt'),
+      t('Btn.Confirm'),
+      t('Btn.Cancel'),
+      async () => {
+        let ids = []
+        ids.push(id)
+        let res = await Service.cardFileDelete({ ids: multipleSelection.value })
+        if (res.code == Code.StatusOK) {
+          pigeon.MessageOK(t('Msg.DeleteSuccess'))
+          searchFunc()
+        } else {
+          pigeon.MessageError(res.msg)
+        }
+      },
+      () => {}
+    )
+  }
+
+  const handleSelectionChange = (val) => {
+    multipleSelection.value = val.map((item) => item.id)
+  }
+
+  const handleSizeChange = (val) => {
+    pagerInfo.row = val
+    searchFunc()
+  }
+
+  const handleCurrentChange = (val) => {
+    pagerInfo.current = val
+    searchFunc()
+  }
+
+  onMounted(() => {
+    searchFunc()
+  })
+
+  watch(
+    () => form.type,
+    () => {
+      // form.fileUrl = ''
+    }
+  )
+</script>
+
+<style scoped lang="scss">
+  @import 'index.scss';
+</style>

+ 196 - 0
src/views/card/CardGlobalOrder/index.scss

@@ -0,0 +1,196 @@
+#review_Email {
+  .crm_search {
+    .search_action_btn {
+      .delete {
+        background-color: #a1a1a1;
+      }
+
+      .delete.active {
+        background-color: #368fec;
+      }
+    }
+
+    .chooseLang {
+      margin-top: 2px;
+      margin-right: 10px;
+
+      > span {
+        border: 1px solid #dcdfe6;
+        padding: 0 8px;
+        min-width: 100px;
+        display: inline-block;
+        height: 32px;
+        line-height: 32px;
+        box-sizing: border-box;
+        text-align: center;
+      }
+
+      span.active {
+        background-color: #368fec;
+        border-color: #368fec;
+        color: #ffffff;
+      }
+    }
+  }
+
+  .merchant-search-bar {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    width: 350px; // 或你喜欢的宽度
+    margin-bottom: 18px;
+    margin-left: 25%;
+  }
+
+  .el-table .state {
+    display: inline-block;
+    min-width: 80px;
+    max-width: 150px;
+    box-sizing: border-box;
+    line-height: 1.5;
+    border-radius: 2px;
+    padding: 2px 10px;
+    color: #ffffff;
+  }
+
+  .uploadBox {
+    padding: 25px;
+    @include bg_gray_7();
+
+    .title {
+      display: flex;
+      justify-content: space-between;
+
+      .tit {
+        font-weight: bold;
+      }
+
+      .demo {
+        @include font_blue_btn_1();
+      }
+    }
+
+    .input-all {
+      margin: 15px 0;
+    }
+
+    .upload {
+      display: flex;
+      align-items: center;
+    }
+
+    .btn-foot {
+      text-align: right;
+    }
+  }
+
+  .crm_verified_info_mask_trading {
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(43, 48, 67, 0.65);
+    z-index: 88;
+  }
+
+  :deep(.business-edit-form) {
+    .el-form-item__label {
+      width: 100%;
+      text-align: left;
+      padding: 0;
+    }
+
+    .el-form-item {
+      width: 100%;
+      text-align: left;
+      padding: 0;
+    }
+
+    .el-input,
+    .el-select,
+    .el-date-picker,
+    .el-date-editor {
+      width: 100%;
+    }
+
+    .el-form-item__label {
+      font-weight: 500;
+    }
+
+    .el-row {
+      margin-bottom: 0;
+    }
+
+    .el-col {
+      margin-bottom: 0;
+    }
+  }
+}
+
+.quota-tip {
+  color: #999;
+  font-size: 12px;
+  margin-left: 10px;
+}
+
+.fee-text {
+  font-size: 12px;
+  color: $theme_1_main_color1;
+}
+
+.avatar-uploader .el-upload {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+}
+
+.avatar-uploader .el-upload:hover {
+  border-color: #409eff;
+}
+
+.id-uploader {
+  width: 400px;
+  height: 200px;
+  border-radius: 16px;
+  overflow: hidden;
+  border: 1px #979797 dashed;
+
+  .avatar {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+}
+
+.id-uploader {
+  :deep(.el-upload) {
+    width: 400px;
+    height: 200px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+
+.id-uploader .el-upload-list__item {
+  width: 400px;
+  height: 200px;
+}
+
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 80px;
+  height: 80px;
+  line-height: 80px;
+  text-align: center;
+}
+
+.avatar {
+  width: 80px;
+  height: 80px;
+  display: block;
+}

+ 1279 - 0
src/views/card/CardGlobalOrder/index.vue

@@ -0,0 +1,1279 @@
+<template>
+  <div
+    id="review_Email"
+    v-loading="pictLoading"
+    class="view"
+    element-loading-background="rgba(43, 48, 67, 0.65)"
+    element-loading-spinner="el-icon-loading"
+  >
+    <div class="crm_search">
+      <el-form ref="formRef" label-position="" :model="search" label-width="">
+        <el-row>
+          <el-col :span="24" :md="24" :lg="24">
+            <el-form-item>
+              <el-select
+                v-model="search.tag"
+                class="crm_search_down crm-border-radius-no"
+                :placeholder="$t('Placeholder.Choose')"
+              >
+                <el-option :label="$t('Ucard.Business.text2')" :value="1"></el-option>
+                <el-option :label="$t('Ucard.Business.text4')" :value="3"></el-option>
+                <el-option :label="$t('Ucard.Business.text5')" :value="4"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item style="margin-right: 10px">
+              <el-input
+                v-if="search.tag == 1"
+                v-model.trim="search.cId"
+                class="crm-border-left-no crm-border-radius-no"
+                clearable
+                :placeholder="$t('Placeholder.Input')"
+                @keyup.enter="toSearch"
+              ></el-input>
+              <el-input
+                v-if="search.tag == 3"
+                v-model.trim="search.email"
+                class="crm-border-left-no crm-border-radius-no"
+                clearable
+                :placeholder="$t('Placeholder.Input')"
+                @keyup.enter="toSearch"
+              ></el-input>
+              <el-input
+                v-if="search.tag == 4"
+                v-model.trim="search.mobile"
+                class="crm-border-left-no crm-border-radius-no"
+                clearable
+                :placeholder="$t('Placeholder.Input')"
+                @keyup.enter="toSearch"
+              ></el-input>
+            </el-form-item>
+            <el-form-item style="margin-right: 10px">
+              <el-input
+                v-model.trim="search.merchantOrderNo"
+                class="crm-border-radius-no"
+                clearable
+                :placeholder="$t('global.p15')"
+                @keyup.enter="toSearch"
+              ></el-input>
+            </el-form-item>
+            <el-form-item style="margin-right: 10px">
+              <el-select
+                v-model="search.payoutCurrency"
+                class="crm-border-radius-no"
+                clearable
+                :placeholder="$t('global.p25')"
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="(item, index) in globalCurrenciesOpt"
+                  :key="index"
+                  :label="item.payoutCurrency"
+                  :value="item.payoutCurrency"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item style="margin-right: 10px">
+              <el-select
+                v-model="search.status"
+                class="crm-border-radius-no"
+                clearable
+                :placeholder="$t('global.p20')"
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="(item, key) in statusOpt"
+                  :key="key"
+                  :label="$t(item.label)"
+                  :value="item.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item style="margin-right: 10px">
+              <el-select
+                v-model="search.complianceStatus"
+                class="crm-border-radius-no"
+                clearable
+                :placeholder="$t('global.p21')"
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="(value, key) in complianceText"
+                  :key="key"
+                  :label="$t(value)"
+                  :value="key"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item style="margin-right: 10px">
+              <el-select
+                v-model="search.approveStatus"
+                class="crm-border-radius-no"
+                clearable
+                :placeholder="$t('global.p22')"
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="(value, key) in approvalText"
+                  :key="key"
+                  :label="$t(value)"
+                  :value="key"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-date-picker
+                v-model="search.date"
+                class="crm_date_pick crm-border-radius-no"
+                type="daterange"
+                align="right"
+                unlink-panels
+                value-format="yyyy-MM-dd"
+                range-separator="-"
+                :start-placeholder="$t('Placeholder.Start')"
+                :end-placeholder="$t('Placeholder.End')"
+              >
+              </el-date-picker>
+              <el-button
+                class="crm-border-radius-no crm-border-left-no"
+                :icon="Search"
+                @click="toSearch"
+              ></el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item>
+          <el-button
+            v-if="display['R-GlobalOrder-Export'] && display['R-GlobalOrder-Export'].show"
+            type="primary"
+            style="margin-left: 8px"
+            @click="exportAgents"
+            >{{ $t('Btn.Export') }}</el-button
+          >
+        </el-form-item>
+        <el-form-item>
+          <div class="search_action_btn">
+            <span
+              v-if="display['R-GlobalOrder-Add'] && display['R-GlobalOrder-Add'].show"
+              class="crm-cursor"
+              @click="addOrder"
+            >
+              <el-icon><Plus /></el-icon>
+              <span>{{ $t('Btn.Add') }}</span>
+            </span>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <!-- 列表 -->
+      <div class="business-mock-demo" style="margin: 30px 0">
+        <el-table :data="businessList" stripe style="margin-top: 20px; width: 100%">
+          <el-table-column prop="cId" :label="$t('Ucard.Business.item1')">
+            <template #default="scope">
+              <span v-if="scope.row.cId">{{ scope.row.cId }}</span>
+              <span v-else>--</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="merchantOrderNo" :label="$t('global.p15')" />
+          <el-table-column :label="$t('Ucard.Business.item5')">
+            <template #default="scope">
+              {{ scope.row.lastName }} {{ scope.row.firstName }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="email" :label="$t('Ucard.Business.item4')" />
+          <el-table-column :label="$t('Ucard.Business.item3')">
+            <template #default="scope"> {{ scope.row.areaCode }} {{ scope.row.mobile }} </template>
+          </el-table-column>
+          <el-table-column prop="deductionAmount" :label="$t('global.p16')">
+            <template #default="scope">
+              {{ scope.row.deductionAmount }}
+              <div>
+                {{ scope.row.sendCurrency }}
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column prop="deductionFee" :label="$t('global.p17')" />
+          <el-table-column prop="transferAmount" :label="$t('global.p19')">
+            <template #default="scope">
+              {{ scope.row.transferAmount }}
+              <div>
+                {{ scope.row.payoutCurrency }}
+              </div>
+            </template>
+          </el-table-column>
+
+          <el-table-column prop="status" :label="$t('global.p20')">
+            <template #default="scope">
+              <span :class="`state ${getStatusColor('status', scope.row.status)}`">{{
+                $t(statusText[scope.row.status])
+              }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="complianceStatus" :label="$t('global.p21')">
+            <template #default="scope">
+              <span
+                v-if="scope.row.complianceStatus"
+                :class="`state ${getStatusColor('compliance', scope.row.complianceStatus)}`"
+                >{{ $t(complianceText[scope.row.complianceStatus]) }}</span
+              >
+            </template>
+          </el-table-column>
+          <el-table-column prop="approveStatus" :label="$t('global.p22')">
+            <template #default="scope">
+              <span :class="`state ${getStatusColor('approve', scope.row.approveStatus)}`">{{
+                $t(approvalText[scope.row.approveStatus])
+              }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="deductionAccountStatus" :label="$t('global.p24')">
+            <template #default="scope">
+              <span
+                :class="`state ${getStatusColor('deduction', scope.row.deductionAccountStatus)}`"
+                >{{ $t(deductionAccountText[scope.row.deductionAccountStatus]) }}</span
+              >
+            </template>
+          </el-table-column>
+          <el-table-column prop="addTime" :label="$t('global.p23')" />
+
+          <el-table-column :label="$t('Ucard.Business.item20')" align="center">
+            <template #default="scope">
+              <el-dropdown trigger="click" @command="handleCommand">
+                <span class="el-dropdown-link crm-cursor">
+                  <i style="font-weight: bold; font-size: 20px" class="iconfont iconcaidan"></i>
+                </span>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item
+                      v-if="display['R-GlobalOrder-Single'].show"
+                      :command="{ type: '1', row: scope.row }"
+                    >
+                      <el-icon><View /></el-icon>
+                      <span>{{ $t('R-Business-Single') }}</span>
+                    </el-dropdown-item>
+                    <!-- 审批 -->
+                    <el-dropdown-item
+                      v-if="
+                        display['R-GlobalOrder-Approve'].show &&
+                        scope.row.deductionAccountStatus == 2 &&
+                        scope.row.approveStatus == 1 &&
+                        scope.row.status != 'cancel'
+                      "
+                      :command="{ type: '2', row: scope.row }"
+                    >
+                      <el-icon><EditPen /></el-icon>
+                      <span>{{ $t('R-Business-Approve') }}</span>
+                    </el-dropdown-item>
+                    <!-- 提交待补充资料 -->
+                    <el-dropdown-item
+                      v-if="
+                        display['R-GlobalOrder-Submit'].show &&
+                        scope.row.complianceStatus == complianceStatusEnum.PendingCheck
+                      "
+                      :command="{ type: '3', row: scope.row }"
+                    >
+                      <el-icon><EditPen /></el-icon>
+                      <span>
+                        {{ $t('R-GlobalOrder-Submit') }}
+                      </span>
+                    </el-dropdown-item>
+                    <!-- 取消订单 -->
+                    <el-dropdown-item
+                      v-if="
+                        display['R-GlobalOrder-Close'].show &&
+                        scope.row.approveStatus == 1 &&
+                        scope.row.status != 'cancel'
+                      "
+                      :command="{ type: '4', row: scope.row }"
+                    >
+                      <el-icon><Close /></el-icon>
+                      <span>
+                        {{ $t('Ucard.GlobalOrder.CancelOrder') }}
+                      </span>
+                    </el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+    <div v-if="pagerInfo.rowTotal" class="crm_pagination">
+      <div class="crm_page_total">
+        <span>{{ $t('Page.total.item1') }}</span>
+        <span>{{ pagerInfo.rowTotal }}</span>
+        <span>{{ $t('Page.total.item2') }}</span>
+      </div>
+      <el-pagination
+        class="page"
+        background
+        layout="sizes, prev, pager, next"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagerInfo.row"
+        :total="pagerInfo.rowTotal"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      >
+      </el-pagination>
+    </div>
+    <el-dialog
+      v-if="globalFormDialog"
+      v-model="globalFormDialog"
+      :title="$t('Ucard.Business.p1')"
+      width="1000px"
+      :close-on-click-modal="false"
+    >
+      <DynamicForm ref="dynamicFormRef" :fields="globalCurrenciesField" :global-form="globalForm">
+        <el-col v-if="isAdd" :span="8">
+          <el-form-item prop="cId" :label="$t('global.t1')">
+            <el-select
+              v-model="globalForm.cId"
+              filterable
+              :placeholder="$t('global.placeholder.p1')"
+              @change="selectOption"
+            >
+              <el-option
+                v-for="item in cardUserData"
+                :key="item.cId"
+                :label="
+                  item.cId +
+                  ' / ' +
+                  item.lastName +
+                  ' ' +
+                  item.firstName +
+                  ' / ' +
+                  item.areaCode +
+                  '' +
+                  item.mobile +
+                  ' / ' +
+                  item.email
+                "
+                :value="item.cId"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item prop="deductionAccount" :label="$t('global.t2')">
+            <el-select
+              v-model="globalForm.deductionAccount"
+              :disabled="!globalForm.cId || !cardAccountDropdown.length"
+              :placeholder="$t('global.placeholder.p2')"
+              @change="clearAmount"
+            >
+              <el-option
+                v-for="(item, index) in cardAccountDropdown"
+                :key="index"
+                :disabled="item.disabled"
+                :label="item.lable"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item prop="receiver" :label="$t('global.t6')">
+            <el-select
+              v-model="globalForm.receiver"
+              :disabled="!globalForm.cId"
+              filterable
+              :placeholder="$t('global.placeholder.p8')"
+              @change="selectReceiver"
+            >
+              <el-option
+                v-for="item in receiverList"
+                :key="item.id"
+                :label="`${item.receiverFirstName} ${item.receiverLastName} / ${item.receiverBankAccountNumber}`"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-row :gutter="20">
+            <el-col :span="8">
+              <el-form-item
+                :rules="rules['payoutCurrency']"
+                prop="payoutCurrency"
+                :label="$t('global.p1')"
+              >
+                <el-select
+                  v-model="globalForm.payoutCurrency"
+                  :disabled="!globalForm.cId || !currencyList.length"
+                  :placeholder="$t('global.placeholder.p3')"
+                  @change="selectOption1(1)"
+                >
+                  <el-option
+                    v-for="(item, index) in currencyList"
+                    :key="index"
+                    :label="item.payoutCurrency"
+                    :value="item.payoutCurrency"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item :rules="rules['payType']" prop="payType" :label="$t('global.p2')">
+                <el-select
+                  v-model="globalForm.payType"
+                  :disabled="!globalForm.payoutCurrency || !transferTypeList.length"
+                  :placeholder="$t('global.placeholder.p6')"
+                  @change="selectOption1(2)"
+                >
+                  <el-option
+                    v-for="(item, index) in transferTypeList"
+                    :key="index"
+                    :label="item.transferType"
+                    :value="item.transferTypeId"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item :rules="rules['payMethod']" prop="payMethod" :label="$t('global.p3')">
+                <el-select
+                  v-model="globalForm.payMethod"
+                  :disabled="!globalForm.payType || !payoutMethodList.length"
+                  :placeholder="$t('global.placeholder.p7')"
+                  @change="selectOption1(3)"
+                >
+                  <el-option
+                    v-for="(item, index) in payoutMethodList"
+                    :key="index"
+                    :label="item.payoutMethodValue"
+                    :value="item.payoutMethodId"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-col>
+
+        <el-col :span="8">
+          <el-form-item :rules="rules['amount']" prop="amount">
+            <template #label>
+              <span>
+                <span>{{ $t('global.t4') }}</span>
+                <!-- 提示输入值的大小 -->
+                <span v-if="exchangeRateData && exchangeRateData.maxQuota" class="quota-tip">{{
+                  `(${$t('Ucard.GlobalOrder.quoteTip')}  ${exchangeRateData.minQuota} - ${
+                    exchangeRateData.maxQuota
+                  })`
+                }}</span>
+              </span>
+            </template>
+            <el-input
+              v-model="globalForm.amount"
+              type="number"
+              :disabled="amountDisabled"
+              :min="exchangeRateData.minQuota ? exchangeRateData.minQuota : -Infinity"
+              :max="exchangeRateData.maxQuota ? exchangeRateData.maxQuota : Infinity"
+              :placeholder="$t('global.placeholder.p4')"
+              @change="globalExchangeRate"
+              @keydown="keyDown"
+            />
+            <span v-if="feeNum != undefined" class="fee-text">
+              {{ `${$t('Ucard.GlobalOrder.fee')}:${feeNum}` }}
+            </span>
+          </el-form-item>
+        </el-col>
+      </DynamicForm>
+
+      <template #footer>
+        <el-button @click="globalFormDialog = false">{{ $t('Ucard.Business.p32') }}</el-button>
+        <el-button
+          :loading="addLoading"
+          type="primary"
+          :disabled="isAdd && !isBusinessFormValid()"
+          @click="saveBusiness"
+          >{{ $t('Btn.Add') }}</el-button
+        >
+      </template>
+    </el-dialog>
+    <GlobalOrderDialog
+      v-if="detailDialog"
+      :visible="detailDialog"
+      :detail-data="detailData"
+      :type="detailData.type"
+      @close="detailDialog = false"
+      @update="searchFunc()"
+    ></GlobalOrderDialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, watch, onMounted, nextTick, inject } from 'vue'
+  import { useRouter } from 'vue-router'
+  import { ElMessage, ElMessageBox } from 'element-plus'
+  import { Search, View, EditPen, Close, Plus } from '@element-plus/icons-vue'
+  import _ from 'lodash'
+  import Config from '@/config/index'
+  import UcardService from '@/service/ucard'
+  import { exportExcel } from '@/utils/export'
+  import DynamicForm from './components/DynamicForm.vue'
+  import GlobalOrderDialog from './components/GlobalOrderDialog.vue'
+  import {
+    approvalText,
+    complianceStatusEnum,
+    deductionAccountText,
+    statusOpt,
+  } from '@/enum/card/globalOrder'
+  import { useI18n } from 'vue-i18n'
+
+  // 常量定义
+  const { Code } = Config
+  const Session = inject('session')
+  const pigeon = inject('pigeon')
+  const { t } = useI18n()
+
+  const complianceText = {
+    no_process: 'Ucard.GlobalOrder.no_process',
+    pending_check: 'Ucard.GlobalOrder.pending_check',
+    pending: 'Ucard.GlobalOrder.pending',
+    approved: 'Ucard.GlobalOrder.approved',
+    rejected: 'Ucard.GlobalOrder.rejected',
+  }
+  const statusText = {
+    null: 'Ucard.GlobalOrder.wait',
+    wait_process: 'Ucard.GlobalOrder.wait',
+    success: 'Ucard.GlobalOrder.success',
+    fail: 'Ucard.GlobalOrder.fail',
+    processing: 'Ucard.GlobalOrder.processing',
+    partner_processing: 'Ucard.GlobalOrder.partner',
+    cancel: 'Ucard.GlobalOrder.cancel',
+  }
+
+  // ref和reactive
+  const formRef = ref(null)
+  const dynamicFormRef = ref(null)
+  const router = useRouter()
+
+  // 响应式数据
+  const pictLoading = ref(false)
+  const addLoading = ref(false)
+  const globalFormDialog = ref(false)
+  const detailDialog = ref(false)
+  const isAdd = ref(true)
+
+  const search = reactive({
+    tag: 1,
+    cId: '',
+    email: '',
+    mobile: '',
+    merchantOrderNo: '',
+    payoutCurrency: '',
+    status: '',
+    complianceStatus: '',
+    approveStatus: '',
+    date: null,
+    startDate: '',
+    endDate: '',
+  })
+
+  const pagerInfo = reactive({ row: 10, current: 1, pageTotal: 0, rowTotal: 0 })
+
+  const businessList = ref([])
+  const globalForm = reactive({})
+  const businessForm = reactive({})
+
+  const cardUserData = ref([])
+  const cardAccountDropdown = ref([])
+  const receiverList = ref([])
+  const currencyList = ref([])
+  const globalCurrenciesOpt = ref([])
+  const globalCurrenciesField = ref([])
+  const globalCurrenciesDropdown = ref([])
+
+  const feeNum = ref(undefined)
+  const exchangeRateData = ref({})
+  const detailData = reactive({})
+
+  // 计算属性
+  const display = computed(() => JSON.parse(Session.Get('display', true)))
+  const AccessToken = computed(() => {
+    const token = Session.Get('access_token')
+    return token ? { 'Access-Token': token } : {}
+  })
+  const user = computed(() => JSON.parse(Session.Get('user', true)))
+
+  const transferTypeList = computed(() => {
+    return (
+      currencyList.value.find((item) => item.payoutCurrency == globalForm.payoutCurrency)?.types ||
+      []
+    )
+  })
+
+  const payoutMethodList = computed(() => {
+    return (
+      transferTypeList.value.find((item) => item.transferTypeId == globalForm.payType)?.methods ||
+      []
+    )
+  })
+
+  const amountDisabled = computed(() => {
+    const { payoutCurrency, payType, payMethod } = globalForm
+    return !payoutCurrency || !payType || !payMethod
+  })
+
+  const deductionAccountTextComputed = computed(() => deductionAccountText)
+
+  const rules = computed(() => ({
+    payoutCurrency: [
+      {
+        required: true,
+        message: t('global.placeholder.p3'),
+        trigger: 'change',
+      },
+    ],
+    payType: [
+      {
+        required: true,
+        message: t('global.placeholder.p6'),
+        trigger: 'change',
+      },
+    ],
+    payMethod: [
+      {
+        required: true,
+        message: t('global.placeholder.p7'),
+        trigger: 'change',
+      },
+    ],
+    amount: [
+      {
+        required: true,
+        message: t('global.placeholder.p4'),
+        trigger: 'change',
+      },
+    ],
+  }))
+
+  // 方法
+  const getStatusColor = (type, status) => {
+    let color = 'crm_state_blue'
+    if (type == 'status') {
+      switch (status) {
+        case null:
+        case 'wait_process':
+          color = 'crm_state_orange'
+          break
+        case 'success':
+          color = 'crm_state_blue'
+          break
+        case 'fail':
+          color = 'crm_state_red'
+          break
+        case 'processing':
+        case 'partner_processing':
+          color = 'crm_state_yellow'
+          break
+        case 'cancel':
+          color = 'crm_state_gray'
+          break
+      }
+    }
+    if (type == 'compliance') {
+      switch (status) {
+        case 'no_process':
+        case 'approved':
+          color = 'crm_state_blue'
+          break
+        case 'rejected':
+          color = 'crm_state_red'
+          break
+        case 'pending_check':
+          color = 'crm_state_orange'
+          break
+        case 'pending':
+          color = 'crm_state_yellow'
+          break
+      }
+    }
+    if (type == 'approve') {
+      switch (status) {
+        case 1:
+          color = 'crm_state_orange'
+          break
+        case 2:
+          color = 'crm_state_blue'
+          break
+        case 3:
+          color = 'crm_state_red'
+          break
+      }
+    }
+    if (type == 'deduction') {
+      switch (status) {
+        case '1':
+          color = 'crm_state_yellow'
+          break
+        case '2':
+          color = 'crm_state_blue'
+          break
+        case '3':
+          color = 'crm_state_red'
+          break
+        case '4':
+          color = 'crm_state_orange'
+          break
+        case '5':
+          color = 'crm_state_gray'
+          break
+        case '6':
+          color = 'crm_state_red'
+          break
+      }
+    }
+    return color
+  }
+
+  const divideNum = (quote, fee) => {
+    return _.floor(_.divide(quote, fee), 2)
+  }
+
+  const exportAgents = async () => {
+    exportExcel(pigeon, '/wasabi/global/order/list/export', { ...search }, 'Global_Order_List')
+  }
+
+  const addOrder = () => {
+    isAdd.value = true
+    globalFormDialog.value = true
+    clearForm()
+    cardUserList()
+  }
+
+  const clearForm = () => {
+    Object.keys(businessForm).forEach((key) => delete businessForm[key])
+    Object.keys(globalForm).forEach((key) => delete globalForm[key])
+    exchangeRateData.value = {}
+    feeNum.value = undefined
+  }
+
+  const cardUserList = async () => {
+    const res = await UcardService.cardUserList()
+    if (res.code === 200) {
+      cardUserData.value = res.data
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const clearAmount = () => {
+    globalForm.amount = undefined
+  }
+
+  const selectOption = async (item) => {
+    businessForm.deductionAccount = ''
+    globalForm.deductionAccount = undefined
+    globalForm.amount = undefined
+    globalForm.receiver = undefined
+
+    cardAccountDropdown.value = []
+    businessForm.cId = item
+    changeSelect(item)
+    await getAccountDropdown(item)
+    setData(globalCurrenciesField.value)
+    await getGlobalCurrenciesDropdown(item)
+    await getMostReceiverList(item)
+  }
+
+  const changeSelect = (cId) => {
+    const selectedUser = cardUserData.value.find((item) => item.cId === cId)
+    if (selectedUser) {
+      Object.assign(businessForm, selectedUser)
+    }
+  }
+
+  const selectOption1 = async (type) => {
+    if (type === 1) {
+      const list1 = currencyList.value.find(
+        (item) => item.payoutCurrency == globalForm.payoutCurrency
+      )?.types
+      if (list1 && list1.length == 1) {
+        globalForm.payType = list1[0].transferTypeId
+      } else {
+        globalForm.payType = undefined
+      }
+      const list2 = list1?.[0]?.methods || []
+      if (list2.length == 1) {
+        globalForm.payMethod = list2[0].payoutMethodId
+      } else {
+        globalForm.payMethod = undefined
+      }
+    } else if (type === 2) {
+      globalForm.payMethod = undefined
+    }
+    globalForm.amount = undefined
+    feeNum.value = undefined
+
+    const { payoutCurrency, payType, payMethod } = globalForm
+    if (!payoutCurrency || !payType || !payMethod) {
+      return
+    }
+
+    const row = globalCurrenciesDropdown.value.filter((currency) => {
+      return (
+        currency.payoutCurrency === payoutCurrency &&
+        currency.transferTypeId === payType &&
+        currency.payoutMethodId === payMethod
+      )
+    })
+
+    Object.assign(businessForm, {
+      fieldData: { ...row[0] },
+    })
+
+    await getGlobalCurrenciesField(row[0])
+
+    const res = await UcardService.globalExchangeRate({
+      uniqueId: businessForm.uniqueId,
+      country: businessForm.fieldData.country,
+      payoutCurrency: businessForm.fieldData.payoutCurrency,
+      transferTypeId: businessForm.fieldData.transferTypeId,
+      payoutMethodId: businessForm.fieldData.payoutMethodId,
+    })
+
+    if (res.code === 200) {
+      exchangeRateData.value = res.data
+    }
+  }
+
+  const getAllGlobalCurrencies = async () => {
+    const res = await UcardService.globalCurrenciesDropdown({})
+    if (res.code === 200 || res.code === 0) {
+      globalCurrenciesOpt.value = res.data || []
+    }
+  }
+
+  const getMostReceiverList = async (cId) => {
+    const data = cardUserData.value.find((item) => item.cId === cId)
+    const res = await UcardService.globalReceiverList({ uniqueId: data.uniqueId })
+    if (res.code === 200 || res.code === 0) {
+      receiverList.value = res.data || []
+    }
+  }
+
+  const getGlobalCurrenciesDropdown = async (cId) => {
+    const res = await UcardService.globalCurrenciesDropdown({ cId, code: '', status: 'online' })
+    if (res.code === 200 || res.code === 0) {
+      globalCurrenciesDropdown.value = res.data || []
+      const data = _.cloneDeep(res.data)
+
+      const array = Object.entries(
+        data.reduce((acc, item) => {
+          const {
+            payoutCurrency,
+            transferTypeValue,
+            transferTypeId,
+            payoutMethodId,
+            payoutMethodValue,
+          } = item
+
+          if (!acc[payoutCurrency]) {
+            acc[payoutCurrency] = {}
+          }
+
+          if (!acc[payoutCurrency][transferTypeValue]) {
+            acc[payoutCurrency][transferTypeValue] = {
+              transferTypeId,
+              methods: [],
+            }
+          }
+
+          acc[payoutCurrency][transferTypeValue].methods.push({
+            payoutMethodId,
+            payoutMethodValue,
+          })
+
+          return acc
+        }, {})
+      ).map(([payoutCurrency, typesObj]) => ({
+        payoutCurrency,
+        types: Object.entries(typesObj).map(([transferType, { transferTypeId, methods }]) => ({
+          transferType,
+          transferTypeId,
+          methods,
+        })),
+      }))
+      currencyList.value = array
+    }
+  }
+
+  const getGlobalCurrenciesField = async (row) => {
+    const res = await UcardService.globalCurrenciesField({
+      ...row,
+    })
+    if (res.code === 200) {
+      await setData(res.data || [])
+    }
+  }
+
+  const selectReceiver = (id) => {
+    const data = receiverList.value.find((item) => item.id === id)
+    Object.assign(globalForm, data)
+
+    nextTick(() => {
+      dynamicFormRef.value.globalFormRef.value.clearValidate()
+    })
+  }
+
+  const keyDown = (e) => {
+    console.log(e)
+  }
+
+  const globalExchangeRate = async (e) => {
+    feeNum.value = undefined
+    const { usedQuota, yearTransferAmountQuota, minQuota, maxQuota } = exchangeRateData.value
+
+    if (!/^(0|([1-9][0-9]*))(\.[\d]{1,2})?$/.test(e)) {
+      ElMessage.error(t('vaildate.amount.NonNegFormat'))
+      return
+    }
+
+    const amount = Number(e)
+    if (amount < minQuota || amount > maxQuota) {
+      ElMessage.error(t('global.validator.v14', { minQuota, maxQuota }))
+      return
+    }
+
+    if (yearTransferAmountQuota) {
+      const add = _.add(usedQuota, amount)
+      if (add > yearTransferAmountQuota) {
+        ElMessage.error(t('Ucard.GlobalOrder.rateTip'))
+        return
+      }
+    }
+
+    const res = await UcardService.globalExchangeRate({
+      amount: e,
+      uniqueId: businessForm.uniqueId,
+      country: businessForm.fieldData.country,
+      payoutCurrency: businessForm.fieldData.payoutCurrency,
+      transferTypeId: businessForm.fieldData.transferTypeId,
+      payoutMethodId: businessForm.fieldData.payoutMethodId,
+    })
+
+    if (res.code === 200) {
+      Object.assign(businessForm, {
+        exchangeRate: res.data,
+      })
+      feeNum.value = divideNum(res.data.deductionFee, businessForm.fieldData.exchangeRate)
+      await setData(globalCurrenciesField.value)
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const setData = async (data) => {
+    const { idType, idNumber } = businessForm
+    const senderIdType =
+      idType === 'PASSPORT' ? '1' : idType === 'GOVERNMENT_ISSUED_ID_CARD' ? '2' : ''
+    const senderIdNumber =
+      idType === 'PASSPORT' ? idNumber : idType === 'GOVERNMENT_ISSUED_ID_CARD' ? idNumber : ''
+
+    const senderData = {
+      senderFirstName: businessForm.firstName,
+      senderLastName: businessForm.lastName,
+      senderGender: businessForm.gender,
+      senderIdType,
+      senderIdNumber,
+      transferAmount: businessForm?.exchangeRate?.transferAmount?.toFixed(2) || '',
+      senderNationality: businessForm.nationality,
+      senderIdIssueCountry: businessForm.senderIdIssueCountry,
+      senderDateOfBirth: businessForm.birthday,
+      senderCountry: businessForm.country,
+      senderState: businessForm.senderState,
+      senderRegion: businessForm.senderRegion,
+      senderCity: businessForm.townEnName,
+      senderAddress: businessForm.address,
+      senderZipCode: businessForm.postCode,
+      senderMobileAreaCode: businessForm.areaCode,
+      senderMobileNumber: businessForm.mobile,
+      senderEmail: businessForm.email,
+      senderOccupation: businessForm.occupation,
+      transferType: businessForm.fieldData?.transferTypeId || '',
+      PayoutMethod: businessForm.fieldData?.payoutMethodId || '',
+    }
+
+    const mergedFields = await Promise.all(
+      data.map(async (field) => {
+        const { fieldName } = field
+        const key = Object.keys(senderData).find((k) => k.toLowerCase() === fieldName.toLowerCase())
+        const fixedValue = key ? senderData[key] : field.fixedValue
+
+        if (fieldName === 'transferAmount') {
+          field.disabled = true
+        }
+
+        if (['payoutCurrency', 'payoutMethod', 'transferType'].includes(fieldName)) {
+          field.hidden = true
+        }
+
+        if (field.fieldName === 'receiverBankCity') {
+          try {
+            const res = await UcardService.globalQueryBankCities({
+              payoutCurrency: businessForm.fieldData.payoutCurrency,
+              country: businessForm.fieldData.country,
+            })
+
+            if (res.code === 200 && Array.isArray(res.data)) {
+              field.availableDtos = res.data.map((item) => ({
+                value: item.bankCitiesValue,
+                valueId: item.bankCitiesKey,
+              }))
+            }
+          } catch (e) {
+            console.error('加载 receiverBankCity 城市数据失败:', e)
+          }
+        }
+
+        if (field.fieldType === 'select') {
+          if (fieldName.includes('receiver')) {
+            let rValue = globalForm[fieldName]
+            const label = field.availableDtos?.find((item) => item.valueId == rValue)?.value ?? null
+            globalForm[fieldName + 'Value'] = label
+          } else {
+            const label =
+              field.availableDtos?.find((item) => item.valueId == fixedValue)?.value ?? null
+            globalForm[fieldName + 'Value'] = label
+          }
+        }
+        return { ...field, fixedValue }
+      })
+    )
+    globalCurrenciesField.value = mergedFields
+  }
+
+  const getAccountDropdown = async (cId) => {
+    const res = await UcardService.cardAccountDropdown({ cId })
+    if (res.code === 200) {
+      cardAccountDropdown.value = res.data.map((item) => {
+        if (item.type == '1') {
+          item.lable = `${t('Ucard.GlobalOrder.cardNo')} - ${item.cardNumber} ${t(
+            'Ucard.GlobalOrder.bal'
+          )}: ${item.balance}`
+        } else {
+          item.lable = `${t('Ucard.GlobalOrder.bagBal')}: ${item.balance}`
+        }
+        item.value = item.cardNumber + ',' + item.type
+
+        item.disabled = item.balance == 0
+        return item
+      })
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const handleCommand = async (command) => {
+    if (command.type !== '4') {
+      const res = await UcardService.globalOrdersDetail({ id: command.row.id })
+      if (res.code === 200) {
+        Object.assign(detailData, {
+          ...res.data,
+          type: command.type,
+        })
+      }
+    }
+
+    switch (command.type) {
+      case '1':
+      case '2':
+      case '3':
+        detailDialog.value = true
+        break
+      case '4':
+        ElMessageBox.confirm(
+          $t('Ucard.GlobalOrder.ConfirmCancelOrder'),
+          $t('Ucard.GlobalOrder.CancelOrder'),
+          {
+            confirmButtonText: $t('Btn.Confirm'),
+            cancelButtonText: $t('Btn.Cancel'),
+            type: 'warning',
+          }
+        )
+          .then(async () => {
+            const res = await UcardService.globalCancelOrder({
+              id: command.row.id,
+              cId: user.value.cId,
+            })
+            if (res.code == Code.StatusOK) {
+              ElMessage.success(t('Ucard.GlobalOrder.CancelOrderSuccess'))
+              searchFunc()
+            } else {
+              ElMessage.error(res.msg)
+            }
+          })
+          .catch(() => {})
+        break
+    }
+  }
+
+  const toSearch = () => {
+    pagerInfo.current = 1
+    searchFunc()
+  }
+
+  const searchFunc = async () => {
+    if (!display.value['R-GlobalOrder-List']?.show) {
+      ElMessage.warning($t('Msg.NotDisplay'))
+      return
+    }
+
+    pictLoading.value = true
+    if (search.date == null) {
+      search.startDate = ''
+      search.endDate = ''
+    } else if (search.date.length == 0) {
+      search.startDate = ''
+      search.endDate = ''
+    } else {
+      search.startDate = search.date[0]
+      search.endDate = search.date[1]
+    }
+
+    try {
+      const params = {
+        ...search,
+        page: {
+          current: pagerInfo.current,
+          row: pagerInfo.row,
+        },
+      }
+      const res = await UcardService.globalOrdersList(params)
+      if (res.code === Code.StatusOK) {
+        businessList.value = res.data
+        pagerInfo.rowTotal = res.page?.rowTotal || 0
+        pagerInfo.pageTotal = res.page?.pageTotal || 0
+        ElMessage.success($t('Msg.SearchSuccess'))
+      } else {
+        ElMessage.error(res.msg || $t('Ucard.Business.ms2'))
+      }
+    } catch (error) {
+      console.error('Search error:', error)
+      ElMessage.error($t('Ucard.Business.ms2'))
+    } finally {
+      pictLoading.value = false
+    }
+  }
+
+  const handleSizeChange = (val) => {
+    pagerInfo.row = val
+    searchFunc()
+  }
+
+  const handleCurrentChange = (val) => {
+    pagerInfo.current = val
+    searchFunc()
+  }
+
+  const saveBusiness = async () => {
+    const valid = await dynamicFormRef.value.$refs.globalForm.validate()
+    const [cardNumber, deductionAccountType] = globalForm.deductionAccount.split(',')
+
+    if (valid) {
+      const otherData = {}
+      globalCurrenciesField.value
+        .map((item) => ({
+          fieldName: item.fieldName,
+          fieldType: item.fieldType,
+        }))
+        .forEach((item) => {
+          otherData[item.fieldName] = globalForm[item.fieldName]
+          if (item.fieldType == 'select') {
+            const key = item.fieldName + 'Value'
+            otherData[key] = globalForm[key]
+          }
+        })
+
+      const params = {
+        amount: globalForm.amount,
+        cId: globalForm.cId,
+        ...otherData,
+        cardNumber,
+        deductionAccountType,
+        uniqueId: businessForm.uniqueId,
+        ...businessForm.exchangeRate,
+      }
+
+      addLoading.value = true
+      const res = await UcardService.globalOrdersCreate(params)
+
+      if (res.code === Code.StatusOK) {
+        ElMessage.success(res.msg)
+        globalFormDialog.value = false
+        searchFunc()
+      } else {
+        ElMessage.error(res.msg)
+      }
+      addLoading.value = false
+    } else {
+      console.log('表单校验失败')
+    }
+  }
+
+  const accountOpen = (cId) => {
+    router.push({ name: 'R-CustomerDetail', params: { cId: cId } })
+  }
+
+  const isBusinessFormValid = () => {
+    const requiredFields = ['areaCode', 'mobile', 'email', 'lastName', 'firstName', 'address']
+    return requiredFields.every((field) => !!businessForm[field])
+  }
+
+  // 挂载
+  onMounted(() => {
+    getAllGlobalCurrencies()
+    searchFunc()
+  })
+
+  // 监听器
+  watch(
+    () => search.tag,
+    () => {
+      search.cId = ''
+      search.email = ''
+      search.mobile = ''
+    }
+  )
+
+  watch(
+    () => globalForm.amount,
+    (val) => {
+      if (!val) {
+        feeNum.value = undefined
+        delete businessForm.exchangeRate
+        setData(globalCurrenciesField.value)
+      }
+    }
+  )
+
+  watch(globalFormDialog, (val) => {
+    if (!val) {
+      Object.keys(globalForm).forEach((key) => delete globalForm[key])
+      Object.keys(businessForm).forEach((key) => delete businessForm[key])
+      globalCurrenciesField.value = []
+    }
+  })
+</script>
+
+<style scoped>
+  .fee-text {
+    font-size: 12px;
+    color: #666;
+    margin-top: 4px;
+    display: block;
+  }
+</style>
+
+<style scoped lang="scss">
+  @import 'index.scss';
+</style>
+<style lang="scss">
+  #review_Email {
+    .dialog_header_w {
+      .crm_search_down {
+        width: 400px;
+      }
+    }
+  }
+</style>

+ 14 - 0
src/views/card/CardIdTypeConfig/const.ts

@@ -0,0 +1,14 @@
+export const idTypeList = [
+  { name: 'card.Form.v4', value: 'PASSPORT' },
+  { name: 'card.Form.v3', value: 'HK_HKID' },
+  { name: 'card.Form.v5', value: 'DLN' },
+  { name: 'card.Form.v6', value: 'GOVERNMENT_ISSUED_ID_CARD' },
+]
+
+// 定义 select 选项数组
+export const idTypeFilterOptions = [
+  { value: 'PASSPORT', label: 'card.Form.v4' },
+  { value: 'HK_HKID', label: 'card.Form.v3' },
+  { value: 'DLN', label: 'card.Form.v5' },
+  { value: 'GOVERNMENT_ISSUED_ID_CARD', label: 'card.Form.v6' },
+]

+ 457 - 0
src/views/card/CardIdTypeConfig/index.vue

@@ -0,0 +1,457 @@
+<template>
+  <div
+    id="review_Email"
+    v-loading="pictLoading"
+    class="view"
+    element-loading-background="rgba(43, 48, 67, 0.65)"
+    element-loading-spinner="el-icon-loading"
+  >
+    <div class="crm_search">
+      <el-form ref="formRef" label-position="" :model="search" label-width="">
+        <el-row>
+          <el-col :span="24" :md="24" :lg="24">
+            <el-form-item style="margin-right: 10px">
+              <el-select
+                v-model.trim="search.code"
+                filterable
+                class="crm-border-radius-no"
+                clearable
+                :placeholder="t('card.vaildate.v6')"
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="item in countryCityList"
+                  :key="item.id"
+                  :label="lang == 'cn' ? item.cnName : item.enName"
+                  :value="item.code"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-select
+                v-model="search.idType"
+                class="crm-border-radius-no"
+                clearable
+                :placeholder="t('Placeholder.Choose')"
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="item in idTypeFilterOptions"
+                  :key="item.value"
+                  :label="t(item.label)"
+                  :value="item.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button class="crm-border-radius-no crm-border-left-no" @click="toSearch">
+                <el-icon><Search /></el-icon>
+              </el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item v-if="display['R-IdTypeConfig-Add'].show">
+          <div class="search_action_btn">
+            <span class="crm-cursor delete active" @click="approvalAll(1)">
+              {{ t('R-IdTypeConfig-Add') }}
+            </span>
+          </div>
+        </el-form-item>
+        <el-form-item v-if="display['R-IdTypeConfig-Delete'].show">
+          <div class="search_action_btn">
+            <span class="crm-cursor delete active" @click="deleteAll">
+              {{ t('R-IdTypeConfig-Delete') }}
+            </span>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <div class="card-mock-demo" style="margin: 30px 0">
+        <el-table
+          :data="mock_tableData"
+          stripe
+          style="margin-top: 20px; width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column align="left" type="selection" width="55"> </el-table-column>
+
+          <el-table-column prop="code" align="left" :label="t('card.Form.f7')">
+            <template #default="scope">
+              {{ lang == 'cn' ? scope.row.cnName : scope.row.enName }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="idType" align="left" :label="t('card.Form.f16')">
+            <template #default="scope">
+              {{ idTypeDisplay(scope.row.idType) }}
+            </template>
+          </el-table-column>
+
+          <el-table-column prop="" align="center" :label="t('Label.Action')">
+            <template #default="scope">
+              <el-dropdown trigger="click" @command="handleCommand">
+                <span class="el-dropdown-link crm-cursor">
+                  <i style="font-weight: bold; font-size: 20px" class="iconfont iconcaidan"></i>
+                </span>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item
+                      v-if="display['R-IdTypeConfig-Update'].show"
+                      :command="{ type: 2, row: scope.row }"
+                    >
+                      <el-icon><Edit /></el-icon>
+                      {{ t('R-IdTypeConfig-Update') }}
+                    </el-dropdown-item>
+                    <el-dropdown-item
+                      v-if="display['R-IdTypeConfig-Delete'].show"
+                      :command="{ type: 1, row: scope.row }"
+                    >
+                      <el-icon><Delete /></el-icon>
+                      {{ t('R-IdTypeConfig-Delete') }}
+                    </el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+    <PagePagination
+      :pager-info="pagerInfo"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+    <el-dialog
+      v-if="approvalAllDialog"
+      v-model="approvalAllDialog"
+      :title="t('R-IdTypeConfig')"
+      :rules="rules"
+      width="600px"
+    >
+      <el-form
+        ref="dialogFormRef"
+        :rules="rules"
+        :model="form"
+        label-width="130px"
+        label-position="right"
+        class="business-edit-form"
+      >
+        <el-form-item prop="country" :label="t('card.Form.f7')">
+          <el-select
+            v-model="form.country"
+            multiple
+            :disabled="type == 2"
+            :placeholder="t('card.vaildate.v6')"
+          >
+            <el-option
+              v-for="item in countryCityList"
+              :key="item.id"
+              :label="lang == 'cn' ? item.cnName : item.enName"
+              :value="item.code"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item prop="idType" :label="t('card.Form.f16')">
+          <el-select v-model="form.idType" multiple :placeholder="t('card.vaildate.v14')">
+            <el-option
+              v-for="(item, index) in idTypeList"
+              :key="index"
+              :label="t(item.name)"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="approvalAllDialog = false">
+            {{ t('Ucard.Business.p32') }}
+          </el-button>
+          <el-button type="primary" @click="idTypesConfigAdd">
+            {{ t('card.Btn.Confirm') }}
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, onMounted, watch, inject } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { ElMessage, ElMessageBox } from 'element-plus'
+  import Service from '@/service/ucard'
+  import Config from '@/config/index'
+  import PagePagination from '@/components/pagePagination'
+  import { idTypeFilterOptions, idTypeList } from '@/views/card/CardIdTypeConfig/const'
+  import { Delete, Edit, Search } from '@element-plus/icons-vue'
+
+  const { t } = useI18n()
+  const { Code } = Config
+
+  // 使用 inject 获取 session
+  const Session = inject('session')
+
+  // Refs
+  const formRef = ref(null)
+  const dialogFormRef = ref(null)
+  const pictLoading = ref(false)
+  const approvalAllDialog = ref(false)
+  const type = ref(1)
+  const multipleSelection = ref([])
+  const countryCityList = ref([])
+
+  // Reactive data
+  const search = reactive({
+    idType: '',
+    code: '',
+  })
+
+  const mock_tableData = ref([])
+
+  const pagerInfo = reactive({ row: 10, current: 1, pageTotal: 0, rowTotal: 0 })
+
+  const form = reactive({
+    country: null,
+    idType: null,
+  })
+
+  const rules = reactive({
+    idType: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'blur',
+      },
+    ],
+    country: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'blur',
+      },
+    ],
+  })
+  // Computed properties
+  const display = computed(() => {
+    const displayStr = Session?.Get('display', true)
+    return displayStr ? JSON.parse(displayStr) : {}
+  })
+
+  const user = computed(() => {
+    const userStr = Session?.Get('user', true)
+    return userStr ? JSON.parse(userStr) : {}
+  })
+
+  const lang = computed(() => Session?.Get('lang') || 'cn')
+
+  // Methods
+  const getCountryListForSelect = async () => {
+    const res = await Service.ucardCountryCity({ code: '' })
+    if (res.code === 200 || res.code === 0) {
+      countryCityList.value = res.data || []
+    }
+  }
+
+  const idTypeDisplay = (idTypeStr) => {
+    const types = idTypeStr.split(',').map((t) => t.trim())
+    return idTypeList
+      .filter((item) => types.includes(item.value))
+      .map((item) => t(item.name))
+      .join(',')
+  }
+
+  const handleCommand = (command) => {
+    switch (command.type) {
+      case 1:
+        multipleSelection.value = [command.row.id]
+        deleteAll()
+        break
+      case 2: {
+        const idType = command.row.idType.split(',').map((item) => item.trim())
+        const country = [command.row.code]
+        Object.assign(form, {
+          ...command.row,
+          country,
+          idType,
+        })
+        approvalAll(2)
+        break
+      }
+    }
+  }
+
+  //选择多项
+  const handleSelectionChange = (val) => {
+    multipleSelection.value = []
+    val.forEach((item) => {
+      multipleSelection.value.push(item.id)
+    })
+  }
+
+  const approvalAll = (approvalType) => {
+    type.value = approvalType
+    if (approvalType == 1) {
+      Object.assign(form, {
+        country: null,
+        idType: null,
+      })
+    }
+    approvalAllDialog.value = true
+  }
+
+  //批量删除
+  const deleteAll = async () => {
+    if (!multipleSelection.value.length) {
+      return
+    }
+
+    try {
+      await ElMessageBox.confirm(t('Msg.Delete'), t('Msg.SystemPrompt'), {
+        confirmButtonText: t('Btn.Confirm'),
+        cancelButtonText: t('Btn.Cancel'),
+        type: 'warning',
+      })
+
+      const res = await Service.idTypesConfigDelete({
+        ids: multipleSelection.value,
+      })
+      if (res.code == Code.StatusOK) {
+        ElMessage.success(t('Msg.DeleteSuccess'))
+        searchFunc()
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } catch (error) {
+      // 用户取消删除
+    }
+  }
+
+  // 添加
+  const idTypesConfigAdd = async () => {
+    try {
+      const valid = await dialogFormRef?.value.validate()
+      if (!valid) return
+
+      const result = countryCityList.value
+        .filter((item) => form.country.includes(item.code))
+        .map((item) => {
+          return {
+            enName: item.enName,
+            cnName: item.cnName,
+            code: item.code,
+            idType: form.idType.join(','),
+          }
+        })
+
+      const res = await Service.idTypesConfigAdd(result)
+      if (res.code == 200) {
+        Object.assign(form, {
+          country: null,
+          idType: null,
+        })
+        approvalAllDialog.value = false
+        toSearch()
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } catch (error) {
+      console.log(error)
+    }
+  }
+
+  const toSearch = () => {
+    pagerInfo.current = 1
+    searchFunc()
+  }
+
+  // 列表
+  const searchFunc = async () => {
+    pictLoading.value = true
+
+    if (!display.value['R-IdTypeConfig-Search']?.show) {
+      ElMessage.warning(t('Msg.NotDisplay'))
+      pictLoading.value = false
+      return
+    }
+
+    const res = await Service.idTypesConfigList({
+      ...search,
+      page: {
+        current: pagerInfo.current,
+        row: pagerInfo.row,
+      },
+    })
+
+    if (res.code == Code.StatusOK) {
+      mock_tableData.value = res.data
+      if (res.page != null) {
+        pagerInfo.rowTotal = res.page.rowTotal
+        pagerInfo.pageTotal = res.page.pageTotal
+      } else {
+        pagerInfo.rowTotal = 0
+      }
+      ElMessage.success(t('Msg.SearchSuccess'))
+    } else {
+      ElMessage.error(res.msg)
+    }
+    pictLoading.value = false
+  }
+
+  const handleSizeChange = (val) => {
+    pagerInfo.row = val
+    searchFunc()
+  }
+
+  const handleCurrentChange = (val) => {
+    pagerInfo.current = val
+    searchFunc()
+  }
+
+  onMounted(() => {
+    searchFunc()
+    getCountryListForSelect()
+  })
+</script>
+
+<style scoped lang="scss">
+  #review_Email {
+    .crm_search {
+      .search_action_btn {
+        .delete {
+          background-color: #a1a1a1;
+        }
+
+        .delete.active {
+          background-color: #368fec;
+        }
+      }
+    }
+
+    .el-table .state {
+      display: inline-block;
+      min-width: 80px;
+      max-width: 150px;
+      box-sizing: border-box;
+      line-height: 1.5;
+      border-radius: 2px;
+      padding: 2px 10px;
+      color: #ffffff;
+    }
+    .crm_switch {
+      :deep(.el-form-item__content) {
+        text-align: left;
+      }
+    }
+  }
+</style>
+<style lang="scss">
+  #review_Email {
+    .dialog_header_w {
+      .crm_search_down {
+        width: 400px;
+      }
+    }
+  }
+</style>

+ 409 - 0
src/views/card/CardOperate/index.vue

@@ -0,0 +1,409 @@
+<template>
+  <div
+    id="review_Email"
+    v-loading="pictLoading"
+    class="view"
+    element-loading-background="rgba(43, 48, 67, 0.65)"
+    element-loading-spinner="el-icon-loading"
+  >
+    <div class="crm_search">
+      <el-form ref="formRef" label-position="" :model="search" label-width="">
+        <el-row>
+          <el-col :span="24" :md="24" :lg="24">
+            <el-form-item>
+              <el-select
+                v-model="search.tag"
+                class="crm_search_down crm-border-radius-no"
+                :placeholder="$t('Placeholder.Choose')"
+              >
+                <el-option
+                  v-for="item in tagOptions"
+                  :key="item.value"
+                  :label="$t(item.label)"
+                  :value="item.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item style="margin-right: 10px">
+              <!-- 动态输入框 -->
+              <el-input
+                v-if="search.tag <= 5"
+                v-model.trim="search[searchFields[search.tag]]"
+                class="crm-border-left-no crm-border-radius-no"
+                clearable
+                :placeholder="$t('Placeholder.Input')"
+                @keyup.enter="toSearch"
+              />
+            </el-form-item>
+            <el-form-item style="margin-right: 10px">
+              <el-select
+                v-model="search.operateType"
+                filterable
+                clearable
+                class="crm-border-radius-no select_down"
+                :placeholder="$t('card.Form.f52')"
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="(value, key) in typeOpts"
+                  :key="key"
+                  :label="$t(value)"
+                  :value="key"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-date-picker
+                v-model="search.date"
+                class="crm_date_pick crm-border-radius-no"
+                type="daterange"
+                align="right"
+                unlink-panels
+                value-format="yyyy-MM-dd"
+                range-separator="-"
+                :start-placeholder="$t('Placeholder.Start')"
+                :end-placeholder="$t('Placeholder.End')"
+              >
+              </el-date-picker>
+            </el-form-item>
+            <el-form-item>
+              <el-button
+                class="crm-border-radius-no crm-border-left-no"
+                :icon="Search"
+                @click="toSearch"
+              >
+              </el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item>
+          <el-button
+            v-if="display['R-CardOperate-Export'] && display['R-CardOperate-Export'].show"
+            type="primary"
+            style="margin-left: 8px"
+            @click="exportAgents"
+            ><span>{{ $t('Btn.Export') }}</span></el-button
+          >
+        </el-form-item>
+      </el-form>
+
+      <div class="card-mock-demo" style="margin: 30px 0">
+        <el-table :data="mock_tableData" stripe style="margin-top: 20px; width: 100%">
+          <el-table-column prop="cId" align="left" :label="$t('Ucard.UserOrder.item1')">
+            <template #default="scope">
+              <span
+                v-if="scope.row.cId"
+                class="crm-text-underline"
+                style="cursor: pointer"
+                @click="accountOpen(scope.row.cId)"
+                >{{ scope.row.cId }}</span
+              >
+              <span v-else>--</span>
+            </template>
+          </el-table-column>
+          <el-table-column :label="$t('Ucard.UserOrder.item14')">
+            <template #default="scope">
+              {{ scope.row.lastName }}{{ scope.row.firstName }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="email" align="left" :label="$t('Ucard.UserOrder.item15')" />
+          <el-table-column prop="mobile" align="left" :label="$t('Ucard.UserOrder.item16')">
+            <template #default="scope">
+              <span>{{ scope.row.areaCode }} {{ scope.row.mobile }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="cardNumber" align="left" :label="$t('card.Form.f24')">
+            <template #default="scope">
+              <span>{{ scope.row.cardNumber || '--' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="operateType" align="left" :label="$t('card.Form.f52')">
+            <template #default="scope">
+              <span>{{ $t(typeOpts[scope.row.operateType]) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="operateUser" align="left" :label="$t('card.Status.v17')" />
+          <el-table-column prop="addTime" align="left" :label="$t('card.Form.f51')" />
+        </el-table>
+      </div>
+    </div>
+
+    <div v-if="pagerInfo.rowTotal" class="crm_pagination">
+      <div class="crm_page_total">
+        <span>{{ $t('Page.total.item1') }}</span>
+        <span>{{ pagerInfo.rowTotal }}</span>
+        <span>{{ $t('Page.total.item2') }}</span>
+      </div>
+      <el-pagination
+        class="page"
+        background
+        layout="sizes, prev, pager, next"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagerInfo.row"
+        :total="pagerInfo.rowTotal"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      >
+      </el-pagination>
+    </div>
+    <detailed-info-cid
+      :dialog-info-cid="dialogInfoCid"
+      :form-info="formInfo"
+      :is-trading="true"
+      @close="closeCidDialog"
+    />
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, watch, onMounted, inject } from 'vue'
+  import { useRouter } from 'vue-router'
+  import { useI18n } from 'vue-i18n'
+  import Service from '@/service/ucard'
+  import Config from '@/config/index'
+  import DetailedInfoCid from '@/views/components/DetailedInfoCid'
+  import { Search } from '@element-plus/icons-vue'
+  import { exportExcel } from '@/utils/export'
+
+  const { Code } = Config
+  const { t } = useI18n()
+  const router = useRouter()
+  const Session = inject('session')
+  const pigeon = inject('pigeon')
+
+  // 响应式数据
+  const pictLoading = ref(false)
+  const dialogInfoTradingAdd = ref(false)
+  const dialogInfoCid = ref(false)
+  const formInfo = ref({})
+  const formRef = ref(null)
+
+  // 搜索条件
+  const search = reactive({
+    tag: 1,
+    cId: '',
+    mobile: '',
+    email: '',
+    cardNumber: '',
+    operateType: '',
+    operateUser: '',
+    date: [],
+    startDate: '',
+    endDate: '',
+  })
+
+  // 搜索标签选项
+  const tagOptions = [
+    { value: 1, label: 'Label.CidAccount' },
+    { value: 2, label: 'Ucard.KycAuth.item2' },
+    { value: 3, label: 'Ucard.KycAuth.item3' },
+    { value: 4, label: 'card.Form.f24' },
+    { value: 5, label: 'card.Status.v17' },
+  ]
+
+  // 搜索字段映射
+  const searchFields = {
+    1: 'cId',
+    2: 'mobile',
+    3: 'email',
+    4: 'cardNumber',
+    5: 'operateUser',
+  }
+
+  const editor = ref('')
+  const addType = ref('')
+  const formList = ref({})
+  const myInfo = ref({})
+  const mock_tableData = ref([])
+  const pagerInfo = reactive({ row: 10, current: 1, pageTotal: 0, rowTotal: 0 })
+  const banksData = ref([])
+
+  // 操作类型选项
+  const typeOpts = ref({
+    1: 'card.Status.v1',
+    2: 'card.Status.v2',
+    3: 'card.Status.v3',
+    4: 'card.Status.v4',
+    5: 'card.Status.v5',
+    6: 'card.Status.v6',
+    7: 'card.Status.v7',
+    8: 'card.Status.v8',
+    9: 'card.Status.v9',
+    10: 'card.Status.v10',
+    11: 'card.Status.v11',
+    12: 'card.Status.v12',
+    13: 'card.Status.v13',
+    14: 'card.Status.v14',
+    15: 'card.Status.v15',
+    16: 'card.Status.v16',
+    17: 'R-Business-Btn2',
+    18: 'R-VirtualCard-Btn11',
+    19: 'card.Status.v19',
+    20: 'card.Status.v20',
+    21: 'card.Status.v21',
+    22: 'card.Status.v22',
+    23: 'card.Status.v23',
+  })
+
+  // 计算属性
+  const display = computed(() => {
+    try {
+      return JSON.parse(Session?.Get('display', true) || '{}')
+    } catch {
+      return {}
+    }
+  })
+
+  // 方法
+  const exportAgents = async () => {
+    if (search.date === null || search.date.length === 0) {
+      search.startDate = ''
+      search.endDate = ''
+    } else {
+      search.startDate = search.date[0]
+      search.endDate = search.date[1]
+    }
+
+    exportExcel(
+      pigeon, // 这里需要传递合适的上下文,可能需要调整
+      '/wasabi/card/operate/record/export',
+      { ...search },
+      'Card_Operate'
+    )
+  }
+
+  const closeAdd = (val) => {
+    dialogInfoTradingAdd.value = val
+  }
+
+  const closeDiaAdd = () => {
+    dialogInfoTradingAdd.value = false
+  }
+
+  const confirmToReload = () => {
+    closeDiaAdd()
+    searchFunc()
+  }
+
+  const toSearch = () => {
+    pagerInfo.current = 1
+    searchFunc()
+  }
+
+  // 列表查询
+  const searchFunc = async () => {
+    if (!display.value['R-CardOperate-Page']?.show) {
+      console.warn(t('Msg.NotDisplay'))
+      return
+    }
+
+    if (search.date === null || search.date.length === 0) {
+      search.startDate = ''
+      search.endDate = ''
+    } else {
+      search.startDate = search.date[0]
+      search.endDate = search.date[1]
+    }
+
+    pictLoading.value = true
+    try {
+      const res = await Service.operatePage({
+        ...search,
+        page: {
+          current: pagerInfo.current,
+          row: pagerInfo.row,
+        },
+      })
+
+      if (res.code === Code.StatusOK) {
+        mock_tableData.value = res.data || []
+        pagerInfo.rowTotal = res.page?.rowTotal || 0
+        pagerInfo.pageTotal = res.page?.pageTotal || 0
+        pigeon.MessageOK(t('Msg.SearchSuccess'))
+      } else {
+        pigeon.MessageError(res.msg)
+      }
+    } catch (error) {
+      pigeon.MessageError(t('Msg.SearchFailed'))
+    } finally {
+      pictLoading.value = false
+    }
+  }
+
+  const handleSizeChange = (val) => {
+    pagerInfo.row = val
+    searchFunc()
+  }
+
+  const handleCurrentChange = (val) => {
+    pagerInfo.current = val
+    searchFunc()
+  }
+
+  // 打开账户详情
+  const accountOpen = (cId) => {
+    router.push({ name: 'R-CustomerDetail', params: { cId: cId } })
+  }
+
+  const closeCidDialog = (val) => {
+    dialogInfoCid.value = val
+  }
+
+  // 监听器
+  watch(
+    () => search.tag,
+    () => {
+      // 清空所有搜索字段
+      search.cId = ''
+      search.mobile = ''
+      search.email = ''
+      search.cardNumber = ''
+      search.operateType = ''
+      search.operateUser = ''
+      search.date = []
+      search.startDate = ''
+      search.endDate = ''
+    }
+  )
+
+  // 生命周期
+  onMounted(() => {
+    searchFunc()
+  })
+</script>
+
+<style scoped lang="scss">
+  #review_Email {
+    .crm_search {
+      .search_action_btn {
+        .delete {
+          background-color: #a1a1a1;
+        }
+
+        .delete.active {
+          background-color: #368fec;
+        }
+      }
+    }
+
+    .el-table .state {
+      display: inline-block;
+      min-width: 80px;
+      max-width: 150px;
+      box-sizing: border-box;
+      line-height: 1.5;
+      border-radius: 2px;
+      padding: 2px 10px;
+      color: #ffffff;
+    }
+  }
+</style>
+<style lang="scss">
+  #review_Email {
+    .dialog_header_w {
+      .crm_search_down {
+        width: 400px;
+      }
+    }
+  }
+</style>

+ 3 - 1
src/views/login/index.vue

@@ -17,6 +17,7 @@
       <el-form-item prop="password">
         <el-input
           v-model.trim="params.password"
+          style="width: 100% !important"
           class="m-input"
           type="password"
           :prefix-icon="Key"
@@ -41,8 +42,8 @@
       :title="$t('getCode.item1')"
       center
       :close-on-click-modal="false"
-      class="dialog_header_w"
       v-model="dialogCheck"
+      class="dialog_header_w"
     >
       <div class="dia-content">
         <el-form
@@ -257,6 +258,7 @@
   // 登录
   const login = async () => {
     if (!dialogCheckFormRef.value) return
+    console.log('asd')
 
     const valid = await dialogCheckFormRef.value.validate()
     if (!valid) return

+ 1 - 22
src/views/page/Page.vue

@@ -59,20 +59,6 @@
 
   const display = computed(() => JSON.parse(Session.Get('display', true)))
 
-  // 计时器--高风险客户内转提醒
-  const beginTimer = () => {
-    interval.value = setInterval(() => {
-      closeId.value.forEach((item) => {
-        item.downTime = item.downTime - 1
-      })
-      closeId.value = closeId.value.filter((item) => {
-        return item.downTime > 0
-      })
-
-      getFinanceRiskList()
-    }, 60000)
-  }
-
   const getFinanceRiskList = async () => {
     try {
       const res = await Service.financeTransferRiskRemindList({})
@@ -132,14 +118,7 @@
   }
 
   // 生命周期
-  onMounted(() => {
-    setTimeout(() => {
-      if (display.value['R-InternalTransfer-remindSingle']?.show) {
-        getFinanceRiskList()
-        beginTimer()
-      }
-    }, 3000)
-  })
+  onMounted(() => {})
 
   onUnmounted(() => {
     if (interval.value) {

+ 453 - 0
src/views/system/GoogleEmail/index.vue

@@ -0,0 +1,453 @@
+<template>
+  <div
+    id="google"
+    v-loading="loading"
+    class="view"
+    :element-loading-background="'rgba(43, 48, 67, 0.65)'"
+    :element-loading-spinner="'el-icon-loading'"
+  >
+    <!-- 搜索表单 -->
+    <div class="crm_search">
+      <el-form ref="searchRef" :model="search">
+        <el-row>
+          <el-col :span="24" :md="24" :lg="24">
+            <el-form-item style="margin-right: 10px">
+              <el-input
+                v-model="search.groupEmail"
+                :placeholder="t('Placeholder.Input')"
+                clearable
+                style="margin-top: 5px"
+              >
+                <template #prepend>
+                  <span class="crm-cursor crm-border-radius-no">
+                    {{ t('Enter_info.GroupEmail') }}
+                  </span>
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item>
+              <el-input
+                v-model="search.groupName"
+                :placeholder="t('Placeholder.Input')"
+                clearable
+                class="crm-border-radius-no"
+                style="margin-top: 5px"
+              >
+                <template #prepend>
+                  <span class="crm-cursor crm-border-radius-no">
+                    {{ t('Enter_info.GroupName') }}
+                  </span>
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item>
+              <el-button
+                class="crm-border-radius-no crm-border-left-no"
+                :icon="Search"
+                @click="handleSearch"
+              ></el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item>
+          <el-button v-if="display['R-GoogleGroup-Add']?.show" @click="handleAdd">
+            {{ t('Btn.Add') }}
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 数据表格 -->
+    <el-table v-loading="loading" :data="tableData" stripe style="width: 100%">
+      <el-table-column :label="t('Enter_info.GroupEmail')">
+        <template #default="{ row }">
+          {{ row.groupEmail || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('Enter_info.GroupName')">
+        <template #default="{ row }">
+          {{ row.groupName || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('Label.Action')" width="120" align="center">
+        <template #default="{ row }">
+          <el-dropdown trigger="click" @command="handleCommand">
+            <span class="el-dropdown-link crm-cursor">
+              <i style="font-weight: bold; font-size: 20px" class="iconfont iconcaidan"></i>
+            </span>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item
+                  v-if="display['R-GoogleGroup-Update']?.show"
+                  :command="{ type: 'editor', row: row }"
+                >
+                  <el-icon><Edit /></el-icon>
+                  <span>{{ t('Btn.Editor') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-GoogleGroup-Delete']?.show"
+                  :command="{ type: 'delete', id: row.id }"
+                >
+                  <el-icon><Delete /></el-icon>
+                  <span>{{ t('Btn.Delete') }}</span>
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <div v-if="pagerInfo.rowTotal" class="crm_pagination">
+      <div class="crm_page_total">
+        <span>{{ t('Page.total.item1') }}</span>
+        <span>{{ pagerInfo.rowTotal }}</span>
+        <span>{{ t('Page.total.item2') }}</span>
+      </div>
+      <el-pagination
+        v-model:current-page="pagerInfo.current"
+        class="page"
+        background
+        layout="sizes, prev, pager, next"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagerInfo.row"
+        :total="pagerInfo.rowTotal"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      >
+      </el-pagination>
+    </div>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog
+      v-model="dialogVisible"
+      :title="dialogTitle"
+      width="500px"
+      :close-on-click-modal="false"
+      @close="handleDialogClose"
+    >
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+        <el-form-item :label="t('Enter_info.GroupName') + ':'" prop="groupName">
+          <el-input v-model.trim="form.groupName" :placeholder="t('Placeholder.Input')"></el-input>
+        </el-form-item>
+        <el-form-item :label="t('Enter_info.GroupEmail') + ':'" prop="groupEmail">
+          <el-input
+            v-model.trim="form.groupEmail"
+            type="textarea"
+            :placeholder="t('Placeholder.Input')"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleDialogClose">{{ t('Btn.Cancel') }}</el-button>
+          <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
+            {{ t('Btn.Confirm') }}
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, onMounted, nextTick, inject } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
+  import { Delete, Edit, Search } from '@element-plus/icons-vue'
+  import Service from '@/service/google'
+  import Config from '@/config'
+
+  const { t } = useI18n()
+  const { Code } = Config
+  const Session = inject('session')
+
+  // 响应式数据
+  const loading = ref(false)
+  const searchRef = ref(null)
+  const formRef = ref(null)
+  const submitLoading = ref(false)
+  const dialogVisible = ref(false)
+  const dialogTitle = ref('')
+  const isEdit = ref(false)
+
+  const search = reactive({
+    groupEmail: '',
+    groupName: '',
+  })
+
+  const tableData = ref([])
+
+  const pagerInfo = reactive({
+    current: 1,
+    row: 10,
+    rowTotal: 0,
+  })
+
+  const form = reactive({
+    id: '',
+    groupEmail: '',
+    groupName: '',
+  })
+
+  // 计算属性
+  const display = computed(() => {
+    return JSON.parse(Session.Get('display', true))
+  })
+
+  // 验证规则
+  const rules = reactive({
+    groupEmail: [
+      {
+        required: true,
+        message: t('Placeholder.Input'),
+        trigger: 'blur',
+      },
+      {
+        validator: validateEmails,
+        trigger: 'blur',
+      },
+    ],
+    groupName: [
+      {
+        required: true,
+        message: t('Placeholder.Input'),
+        trigger: 'blur',
+      },
+    ],
+  })
+
+  // 生命周期
+  onMounted(() => {
+    fetchData()
+  })
+
+  // 方法
+  // 获取数据
+  const fetchData = async () => {
+    loading.value = true
+    if (!display.value['R-GoogleGroup-Search']?.show) {
+      ElMessage.warning(t('Msg.NotDisplay'))
+      loading.value = false
+      return
+    }
+
+    try {
+      const params = {
+        ...search,
+        page: {
+          current: pagerInfo.current,
+          row: pagerInfo.row,
+        },
+      }
+      const res = await Service.googleGroupList(params)
+      if (res.code === Code.StatusOK) {
+        tableData.value = res.data || []
+        pagerInfo.rowTotal = res.data.total || 0
+      } else {
+        ElMessage.error(res.msg || t('Msg.SearchFail'))
+      }
+    } catch (error) {
+      console.error('获取数据失败:', error)
+      ElMessage.error(t('Msg.SystemError'))
+    } finally {
+      loading.value = false
+    }
+  }
+
+  // 搜索
+  const handleSearch = () => {
+    pagerInfo.current = 1
+    fetchData()
+  }
+
+  // 新增
+  const handleAdd = () => {
+    isEdit.value = false
+    dialogTitle.value = t('Btn.Add')
+    form.id = ''
+    form.groupEmail = ''
+    form.groupName = ''
+    dialogVisible.value = true
+    nextTick(() => {
+      if (formRef.value) {
+        formRef.value.clearValidate()
+      }
+    })
+  }
+
+  // 编辑
+  const handleEdit = (row) => {
+    isEdit.value = true
+    dialogTitle.value = t('Btn.Editor')
+    form.id = row.id
+    form.groupEmail = row.groupEmail
+    form.groupName = row.groupName
+    dialogVisible.value = true
+    nextTick(() => {
+      if (formRef.value) {
+        formRef.value.clearValidate()
+      }
+    })
+  }
+
+  // 删除
+  const handleDelete = async (id) => {
+    try {
+      await ElMessageBox.confirm(t('Msg.DeleteConfirm'), t('Msg.SystemPrompt'), {
+        confirmButtonText: t('Btn.Confirm'),
+        cancelButtonText: t('Btn.Cancel'),
+        type: 'warning',
+      })
+
+      const res = await Service.googleGroupDelete({ ids: [id] })
+      if (res.code === Code.StatusOK) {
+        ElMessage.success(t('Msg.DeleteSuccess'))
+        fetchData()
+      } else {
+        ElMessage.error(res.msg || t('Msg.DeleteFail'))
+      }
+    } catch (error) {
+      if (error !== 'cancel') {
+        console.error('删除失败:', error)
+        ElMessage.error(t('Msg.SystemError'))
+      }
+    }
+  }
+
+  // 下拉菜单命令处理
+  const handleCommand = (command) => {
+    if (command.type === 'editor') {
+      handleEdit(command.row)
+    } else if (command.type === 'delete') {
+      handleDelete(command.id)
+    }
+  }
+
+  // 分页处理
+  const handleCurrentChange = (val) => {
+    pagerInfo.current = val
+    fetchData()
+  }
+
+  const handleSizeChange = (val) => {
+    pagerInfo.row = val
+    pagerInfo.current = 1
+    fetchData()
+  }
+
+  // 弹窗关闭
+  const handleDialogClose = () => {
+    dialogVisible.value = false
+    form.id = ''
+    form.groupEmail = ''
+    form.groupName = ''
+    if (formRef.value) {
+      formRef.value.resetFields()
+    }
+  }
+
+  // 提交表单
+  const handleSubmit = async () => {
+    if (!formRef.value) return
+
+    const valid = await formRef.value.validate()
+    if (!valid) return
+
+    submitLoading.value = true
+    try {
+      let res
+      if (isEdit.value) {
+        // 编辑
+        res = await Service.googleGroupUpdate(form)
+        if (res.code === Code.StatusOK) {
+          ElMessage.success(t('Msg.ModifySuccess'))
+          handleDialogClose()
+          fetchData()
+        } else {
+          ElMessage.error(res.msg || t('Msg.ModifyFail'))
+        }
+      } else {
+        // 新增
+        res = await Service.googleGroupAdd(form)
+        if (res.code === Code.StatusOK) {
+          ElMessage.success(t('Msg.SaveSuccess'))
+          handleDialogClose()
+          fetchData()
+        } else {
+          ElMessage.error(res.msg || t('Msg.SaveFail'))
+        }
+      }
+    } catch (error) {
+      console.error('提交失败:', error)
+      ElMessage.error(t('Msg.SystemError'))
+    } finally {
+      submitLoading.value = false
+    }
+  }
+
+  // 自定义邮箱验证规则
+  const validateEmails = (rule, value, callback) => {
+    if (!value) {
+      callback(new Error(t('Placeholder.Input')))
+      return
+    }
+
+    // 检查是否包含中文逗号
+    if (value.includes(',')) {
+      callback(new Error('请使用英文逗号分隔多个邮箱地址'))
+      return
+    }
+
+    // 分割邮箱地址,只支持英文逗号,去除空格
+    const emails = value
+      .split(',')
+      .map((email) => email.trim())
+      .filter((email) => email)
+
+    // 邮箱格式正则表达式
+    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+
+    // 验证每个邮箱格式
+    for (const email of emails) {
+      if (!emailRegex.test(email)) {
+        callback(new Error(t('vaildate.email.format')))
+        return
+      }
+    }
+
+    callback()
+  }
+</script>
+<style lang="scss" scoped>
+  #google {
+    .crm_search {
+      background: #fff;
+      border-radius: 4px;
+    }
+
+    .el-table {
+      margin-bottom: 20px;
+    }
+
+    .crm_pagination {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      background: #fff;
+      padding: 20px;
+      border-radius: 4px;
+
+      .crm_page_total {
+        span {
+          margin-right: 5px;
+        }
+      }
+    }
+
+    .el-dropdown-link {
+      cursor: pointer;
+    }
+  }
+</style>

+ 567 - 0
src/views/user/userList/components/UserAdd.vue

@@ -0,0 +1,567 @@
+<template>
+  <div id="TradingDetailedInfoAdd" class="InfoBox" :class="{ active: dialogInfoTradingAdd }">
+    <div class="header">
+      <div>
+        <span v-if="editor" class="title">{{ $t('Label.EditorUser') }}</span>
+        <span v-else class="title">{{ $t('Label.AddUser') }}</span>
+      </div>
+      <span class="close crm-cursor" @click="close">
+        <el-icon><Close /></el-icon>
+      </span>
+    </div>
+    <el-form ref="formRef" :rules="rules" :model="form" label-width="150PX">
+      <el-form-item :label="$t('Label.UserNames') + ':'">
+        <el-input v-model.trim="form.username" :placeholder="$t('Placeholder.Input')"></el-input>
+      </el-form-item>
+      <el-form-item :label="$t('Label.Name') + ':'">
+        <el-input v-model="form.name" :placeholder="$t('Placeholder.Input')"></el-input>
+      </el-form-item>
+      <el-form-item :label="$t('Label.Email') + ':'">
+        <el-input v-model.trim="form.email" :placeholder="$t('Placeholder.Input')"></el-input>
+      </el-form-item>
+      <!--      <el-form-item :label="$t('Label.Phone') + ':'">-->
+      <!--        <el-input v-model.trim="form.phone" :placeholder="$t('Placeholder.Input')"></el-input>-->
+      <!--      </el-form-item>-->
+      <el-form-item :label="$t('Label.Pwd') + ':'">
+        <el-input v-model.trim="form.password"></el-input>
+      </el-form-item>
+      <el-form-item :label="$t('Label.RoleName') + ':'">
+        <el-select
+          v-model="form.roleId"
+          class="crm_search_down"
+          :placeholder="$t('Placeholder.Choose')"
+        >
+          <el-option
+            v-for="(item, index) in roleName"
+            :key="index"
+            :label="item.name"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="form.departmentId == '1'" :label="$t('Label.SalesSupervisor') + ':'">
+        <el-select
+          v-model="form.pid"
+          class="crm_search_down"
+          filterable
+          :placeholder="$t('Placeholder.Choose')"
+          @change="selectChange"
+        >
+          <el-option
+            v-for="(item, index) in userSales"
+            :key="index"
+            :label="item.name + '(' + item.ibNo + ')'"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="form.departmentId == '1'"
+        :label="$t('salaryPerformance.salary.item1') + ':'"
+      >
+        <el-select
+          v-model="form.salesType"
+          class="crm_search_down"
+          filterable
+          :placeholder="$t('Placeholder.Choose')"
+          @change="selectChangesalesType"
+        >
+          <el-option :label="$t('salaryPerformance.salary.item2')" :value="1"></el-option>
+          <el-option :label="$t('salaryPerformance.salary.item3')" :value="2"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="form.departmentId == '1'"
+        :label="$t('salaryPerformance.salary.item3') + ':'"
+      >
+        <el-select
+          v-model="form.leaderId"
+          class="crm_search_down"
+          filterable
+          :placeholder="$t('Placeholder.Choose')"
+          @change="selectChange"
+        >
+          <el-option
+            v-for="(item, index) in userSales"
+            :key="index"
+            :label="item.name + '(' + item.ibNo + ')'"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="form.departmentId == '1'"
+        :label="$t('salaryPerformance.salary.item4') + ':'"
+      >
+        <el-select
+          v-model="form.salesCommissionRuleId"
+          class="crm_search_down"
+          filterable
+          clearable
+          :placeholder="$t('Placeholder.Choose')"
+          @change="selectChange"
+        >
+          <el-option
+            v-for="(item, index) in GetRuleDropdownList"
+            :key="index"
+            :label="item.ruleName + '(' + item.kpiStandardName + ')'"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="form.departmentId == '1'"
+        :label="$t('salaryPerformance.salary.item5') + ':'"
+      >
+        <el-input v-model.trim="form.salaryLogin" :placeholder="$t('Placeholder.Input')"></el-input>
+      </el-form-item>
+      <el-form-item
+        v-if="form.departmentId == '1'"
+        prop="salaryPlatform"
+        :label="$t('salaryPerformance.salary.item6') + ':'"
+      >
+        <el-select
+          v-model="form.salaryPlatform"
+          class="crm_search_down"
+          filterable
+          :placeholder="$t('Placeholder.Choose')"
+          @change="selectChange"
+        >
+          <el-option label="MT4" value="MT4"></el-option>
+          <el-option label="MT5" value="MT5"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="$t('Label.Region') + ':'">
+        <el-select
+          v-model="form.groupId"
+          class="crm_search_down"
+          :placeholder="$t('Placeholder.Choose')"
+        >
+          <el-option
+            v-for="(item, index) in GetState"
+            :key="index"
+            :label="Session.Get('lang') == 'cn' ? item.name : item.enName"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="$t('Label.Nationality') + ':'">
+        <el-select
+          v-model="form.country"
+          class="crm_search_down"
+          filterable
+          remote
+          :placeholder="$t('Placeholder.Choose')"
+        >
+          <el-option
+            v-for="(item, index) in mock_options"
+            :key="index"
+            :label="item.enName"
+            :value="item.code"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="areaId" :label="$t('Area.item1') + ':'">
+        <el-select
+          v-model="form.areaId"
+          class="crm_search_down"
+          :placeholder="$t('Placeholder.Choose')"
+        >
+          <el-option
+            v-for="(item, index) in GetArea"
+            :key="index"
+            :label="item.name"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <!--      <el-form-item :label="$t('Label.Code') + ':'">-->
+      <!--        <el-input v-model.trim="form.ibNo" :placeholder="$t('Placeholder.Input')"></el-input>-->
+      <!--      </el-form-item>-->
+      <!-- <el-form-item :label="$t('Label.ibNoTemp') + ':'">
+        <el-input
+
+          v-model.trim="form.ibNoTemp"
+          :placeholder="$t('Placeholder.Input')"
+        ></el-input>
+      </el-form-item> -->
+      <el-form-item v-if="!editor" :label="$t('Enter_info.GroupEmail') + ':'">
+        <el-select
+          v-model="form.groupEmail"
+          class="crm_search_down"
+          :placeholder="$t('Placeholder.Choose')"
+        >
+          <el-option
+            v-for="(item, index) in groupEmailList"
+            :key="index"
+            :label="item.groupName"
+            :value="item.groupEmail"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="entryDate" :label="$t('Label.entryDate') + ':'">
+        <el-date-picker
+          v-model="form.entryDate"
+          type="date"
+          class="crm_search_down"
+          value-format="YYYY-MM-DD"
+          :placeholder="$t('Placeholder.Input')"
+        >
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item prop="pid" :label="$t('Label.SalesSupervisor1') + ':'">
+        <el-select
+          v-model="form.attachId"
+          class="crm_search_down"
+          filterable
+          :placeholder="$t('Placeholder.Choose')"
+        >
+          <el-option
+            v-for="(item, index) in userSales"
+            :key="index"
+            :label="item.name + '(' + item.ibNo + ')'"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="entryStatus" :label="$t('Label.EmploymentStatus') + ':'">
+        <el-select
+          v-model="form.entryStatus"
+          class="crm_search_down"
+          :placeholder="$t('Placeholder.Choose')"
+        >
+          <el-option :label="$t('State.employmentStatus1')" :value="1"></el-option>
+          <el-option :label="$t('State.employmentStatus2')" :value="2"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="$t('Label.LoginState') + ':'">
+        <el-switch
+          v-model="form.valid"
+          class="crm_switch"
+          :active-value="1"
+          :inactive-value="0"
+          :active-text="$t('Btn.Yes')"
+          :inactive-text="$t('Btn.No')"
+          active-color="#368FEC"
+          inactive-color="#EB3F57"
+        >
+        </el-switch>
+      </el-form-item>
+      <el-form-item :label="$t('Label.IpLimit') + ':'">
+        <el-switch
+          v-model="form.ipLimit"
+          class="crm_switch"
+          :active-value="1"
+          :inactive-value="0"
+          :active-text="$t('Btn.Yes')"
+          :inactive-text="$t('Btn.No')"
+          active-color="#368FEC"
+          inactive-color="#EB3F57"
+        >
+        </el-switch>
+      </el-form-item>
+      <el-form-item v-show="form.ipLimit" :label="$t('Label.limitIp') + ':'">
+        <el-input v-model.trim="form.limitIp" :placeholder="$t('Placeholder.Input')"></el-input>
+      </el-form-item>
+    </el-form>
+    <span class="btn crm-cursor" @click="confirm"
+      ><span>{{ $t('Btn.Confirm') }}</span></span
+    >
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed, watch, onMounted, inject } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import type { FormInstance, FormRules } from 'element-plus'
+  import ServiceSalary from '@/service/salaryPerformance'
+  import ServiceMarket from '@/service/marketing'
+  import ServiceUser from '@/service/user'
+  import Config from '@/config/index'
+  import { Close } from '@element-plus/icons-vue'
+
+  const { Code } = Config
+  const { t } = useI18n()
+  const Session = inject('session')
+  const pigeon = inject('pigeon')
+
+  interface Props {
+    dialogInfoTradingAdd: boolean
+    editor?: string
+    myInfo?: any
+    formList?: any
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    dialogInfoTradingAdd: false,
+    editor: '',
+    myInfo: null,
+    formList: null,
+  })
+
+  const emit = defineEmits<{
+    (e: 'update:dialogInfoTradingAdd', value: boolean): void
+    (e: 'confirmToReload'): void
+    (e: 'close'): void
+  }>()
+
+  // Refs
+  const formRef = ref<FormInstance>()
+
+  // Reactive data
+  const form = reactive({})
+  const roleName = ref([])
+  const userSales = ref([])
+  const GetRuleDropdownList = ref([])
+  const GetRuleDropdown = ref([])
+  const GetState = ref([])
+  const GetArea = ref([])
+  const mock_options = ref([])
+  const groupEmailList = ref([])
+  const rules = ref<FormRules>({
+    areaId: [
+      {
+        required: true,
+        message: t('Placeholder.Choose'),
+        trigger: 'change',
+      },
+    ],
+  })
+
+  // Computed
+  const GetRuleDropdownListComputed = computed(() => {
+    return GetRuleDropdown.value.filter((item: any) => item.salesType == form.salesType)
+  })
+
+  // Methods
+  const selectChangesalesType = (val: number) => {
+    form.salesCommissionRuleId = null
+    GetRuleDropdownList.value = GetRuleDropdown.value.filter((item: any) => item.salesType == val)
+  }
+  const getRoleList = async () => {
+    let res = await ServiceUser.roleNameList({})
+    if (res.code == Code.StatusOK) {
+      roleName.value = res.data
+    } else {
+      pigeon.MessageError(res.msg)
+    }
+  }
+  // 获取邮箱组别列表
+  const getGroupEmailList = async () => {
+    try {
+      let res = await ServiceUser.getGroupEmailList({})
+      if (res.code == Code.StatusOK) {
+        groupEmailList.value = res.data
+      } else {
+        // 使用 Element Plus 的消息提示或其他消息组件
+        pigeon.MessageError(res.msg || t('Msg.Error'))
+      }
+    } catch (error) {
+      console.error('获取邮箱组别列表失败:', error)
+      pigeon.MessageError(t('Msg.NetworkError'))
+    }
+  }
+  // 获取销售主管列表
+  const getRoleSuperiorList = async () => {
+    try {
+      let res = await ServiceUser.userSales({})
+      if (res.code == Code.StatusOK) {
+        userSales.value = res.data
+      } else {
+        pigeon.MessageError(res.msg || t('Msg.Error'))
+      }
+    } catch (error) {
+      console.error('获取销售主管列表失败:', error)
+      pigeon.MessageError(t('Msg.NetworkError'))
+    }
+  }
+
+  // 获取薪资绩效规则下拉
+  const getsalesCommissionRuleDropdown = async () => {
+    let res = await ServiceSalary.salesCommissionRuleDropdown({})
+    if (res.code == Code.StatusOK) {
+      GetRuleDropdown.value = res.data
+      form.salesType = Number(form.salesType) || null
+      GetRuleDropdownList.value = GetRuleDropdown.value.filter(
+        (item: any) => item.salesType == form.salesType
+      )
+    } else {
+      pigeon.MessageError(res.msg)
+    }
+  }
+
+  // 获取角色地区信息
+  const getDepartmentState = async () => {
+    try {
+      let res = await ServiceUser.userGroupGet({})
+      if (res.code == Code.StatusOK) {
+        GetState.value = res.data
+      } else {
+        pigeon.MessageError(res.msg || t('Msg.Error'))
+      }
+    } catch (error) {
+      console.error('获取角色地区信息失败:', error)
+      pigeon.MessageError(t('Msg.NetworkError'))
+    }
+  }
+
+  // 获取角色区域信息
+  const getArealListUP = async () => {
+    try {
+      let res = await ServiceUser.arealListUP({})
+      if (res.code == Code.StatusOK) {
+        GetArea.value = res.data
+      } else {
+        pigeon.MessageError(res.msg || t('Msg.Error'))
+      }
+    } catch (error) {
+      console.error('获取角色区域信息失败:', error)
+      pigeon.MessageError(t('Msg.NetworkError'))
+    }
+  }
+
+  // 获取国家列表
+  const getCountry = async () => {
+    try {
+      let res = await ServiceMarket.Country({})
+      if (res.code == Code.StatusOK) {
+        mock_options.value = res.data
+      } else {
+        pigeon.MessageError(res.msg || t('Msg.Error'))
+      }
+    } catch (error) {
+      console.error('获取国家列表失败:', error)
+      pigeon.MessageError(t('Msg.NetworkError'))
+    }
+  }
+
+  // 提交
+  const confirm = () => {
+    if (!formRef.value) return
+
+    formRef.value.validate((valid) => {
+      if (valid) {
+        toConfirm()
+      } else {
+        return false
+      }
+    })
+  }
+
+  const toConfirm = () => {
+    if (props.editor) {
+      userUpdate()
+    } else {
+      userAdd()
+    }
+  }
+
+  // 用户update
+  const userUpdate = async () => {
+    if (form.departmentId != 1) {
+      form.pid = null
+      form.salesType = null
+      form.salesCommissionRuleId = null
+      form.leaderId = null
+      form.salaryLogin = null
+      form.salaryPlatform = null
+    }
+
+    if (!form.ipLimit) {
+      form.limitIp = null
+    }
+
+    let res = await ServiceUser.userListUpdate({
+      ...form,
+    })
+
+    if (res.code == Code.StatusOK) {
+      emit('confirmToReload')
+      close()
+      formRef.value?.resetFields()
+      // 使用 $pigeon 的替代方式
+      console.log(t('Msg.Success'))
+    } else {
+      console.error(res.msg)
+    }
+  }
+
+  // 用户add
+  const userAdd = async () => {
+    if (form.departmentId != 1) {
+      form.pid = null
+      form.salesType = null
+      form.salesCommissionRuleId = null
+      form.leaderId = null
+      form.salaryLogin = null
+      form.salaryPlatform = null
+    }
+
+    if (!form.ipLimit) {
+      form.limitIp = null
+    }
+
+    let res = await ServiceUser.userListAdd({
+      ...form,
+    })
+
+    if (res.code == Code.StatusOK) {
+      emit('confirmToReload')
+      formRef.value?.resetFields()
+      console.log(t('Msg.Success'))
+    } else {
+      console.error(res.msg)
+    }
+  }
+
+  const close = () => {
+    emit('update:dialogInfoTradingAdd', false)
+    emit('close')
+  }
+
+  const selectChange = () => {
+    // 选择变更处理
+  }
+
+  // Lifecycle
+  onMounted(() => {
+    // 初始化数据
+    // getsalesCommissionRuleDropdown()
+  })
+
+  // Watch
+  watch(
+    () => props.formList,
+    (newVal) => {
+      if (newVal) {
+        Object.assign(form, newVal)
+      }
+    },
+    { immediate: true }
+  )
+  // Watch
+  watch(
+    () => props.dialogInfoTradingAdd,
+    (newVal) => {
+      if (!newVal && formRef.value) {
+        formRef.value?.resetFields()
+        Object.assign(form, {})
+      }
+      if (newVal) {
+        getDepartmentState()
+        getArealListUP()
+        getCountry()
+        getRoleList()
+        getRoleSuperiorList()
+        getsalesCommissionRuleDropdown()
+        getGroupEmailList()
+        if (!form.entryStatus) {
+          Object.assign(form, { entryStatus: 1 })
+        }
+      }
+    },
+    { immediate: true }
+  )
+</script>
+
+<style scoped lang="scss"></style>

+ 65 - 0
src/views/user/userList/index.scss

@@ -0,0 +1,65 @@
+#user_CustomerList {
+  .crm_search {
+    .search_action_btn {
+      .delete {
+        background-color: #a1a1a1;
+      }
+
+      .delete.active {
+        background-color: #368fec;
+      }
+    }
+  }
+
+  .crm_verified_info_mask_trading {
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(43, 48, 67, 0.65);
+    z-index: 88;
+  }
+
+  .googBox {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+
+    .googleSecretKey {
+      margin-top: 25px;
+      margin: 30px 0;
+      display: flex;
+      align-items: center;
+    }
+
+    .link {
+      width: 300px;
+      margin-left: 1em;
+      border: 1px solid rgb(245, 245, 245);
+      padding: 0 20px;
+      font-size: 14px;
+      color: #606266;
+      line-height: 32px;
+
+      &:focus {
+        outline: none;
+      }
+    }
+
+    .btn-copy {
+      margin-left: 20px;
+      padding: 4px 10px;
+      border-right: 4px;
+      background-color: rgba(84, 129, 214, 1);
+      color: #ffffff;
+      cursor: pointer;
+      transition: all 0.25s linear;
+
+      &:hover {
+        background-color: rgba(84, 129, 214, 0.8);
+      }
+    }
+  }
+}

+ 1272 - 0
src/views/user/userList/index.vue

@@ -0,0 +1,1272 @@
+<template>
+  <div
+    id="user_CustomerList"
+    v-loading="pictLoading"
+    class="view"
+    element-loading-background="rgba(43, 48, 67, 0.65)"
+    element-loading-spinner="el-icon-loading"
+  >
+    <div class="crm_search">
+      <el-form ref="formRef" label-position="" :model="search" label-width="">
+        <el-row>
+          <el-col :span="24" :md="24" :lg="24">
+            <el-form-item style="margin-right: 10px">
+              <el-select
+                v-model="search.roleId"
+                clearable
+                :placeholder="$t('Label.RoleName')"
+                @change="toSearch"
+              >
+                <el-option
+                  v-for="(item, index) in roleName"
+                  :key="index"
+                  :label="item.name"
+                  :value="item.id"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-select
+                v-model="search.tag"
+                class="crm_search_down crm-border-radius-no"
+                :placeholder="$t('Placeholder.Choose')"
+              >
+                <el-option :label="$t('Label.Email')" :value="1"></el-option>
+                <!--                <el-option :label="$t('Label.Code')" :value="2"></el-option>-->
+              </el-select>
+            </el-form-item>
+            <el-form-item style="margin-right: 10px">
+              <el-input
+                v-if="search.tag == 1"
+                v-model.trim="search.email"
+                class="crm-border-left-no crm-border-radius-no"
+                :placeholder="$t('Placeholder.Input')"
+                @keyup.enter="toSearch"
+              ></el-input>
+              <!--              <el-input-->
+              <!--                v-if="search.tag == 2"-->
+              <!--                v-model.trim="search.ibNo"-->
+              <!--                class="crm-border-left-no crm-border-radius-no"-->
+              <!--                :placeholder="$t('Placeholder.Input')"-->
+              <!--                @keyup.enter="toSearch"-->
+              <!--              ></el-input>-->
+            </el-form-item>
+            <el-form-item>
+              <el-date-picker
+                v-model="search.date"
+                class="crm_date_pick crm-border-radius-no"
+                type="daterange"
+                unlink-panels
+                value-format="YYYY-MM-DD"
+                range-separator="-"
+                :start-placeholder="$t('Placeholder.Start')"
+                :end-placeholder="$t('Placeholder.End')"
+                @input="getSearchDate"
+              >
+              </el-date-picker>
+              <el-button
+                class="crm-border-left-no crm-border-radius-no"
+                :icon="Search"
+                @click="toSearch"
+              ></el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item v-if="display['R-UserList-Add'].show">
+          <div class="search_action_btn">
+            <span class="crm-cursor" @click="addUser">
+              <el-icon><Plus /></el-icon>
+              <span>{{ $t('Btn.Add') }}</span>
+            </span>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+    <el-table :data="mock_tableData" stripe style="width: 100%">
+      <el-table-column prop="" align="left" :label="$t('Label.Name')">
+        <template #default="scope">
+          {{ scope.row.name || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Label.UserNames')">
+        <template #default="scope">
+          {{ scope.row.username || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="email" align="left" :label="$t('Label.Email')"> </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Label.RoleName')">
+        <template #default="scope">
+          {{ scope.row.roleName || '--' }}
+        </template>
+      </el-table-column>
+      <!--      <el-table-column prop="" align="left" :label="$t('Label.Code')">-->
+      <!--        <template #default="scope">-->
+      <!--          <span>{{ scope.row.ibNo || '&#45;&#45;' }}</span>-->
+      <!--        </template>-->
+      <!--      </el-table-column>-->
+      <!-- <el-table-column prop="" align="left" :label="$t('Label.ibNoTemp')">
+        <template #default="scope">
+          {{ scope.row.ibNoTemp || "--" }}
+        </template>
+      </el-table-column> -->
+      <el-table-column prop="" align="left" :label="$t('Label.Region')">
+        <template #default="scope">
+          {{
+            Session.Get('lang') == 'cn'
+              ? scope.row.groupName || '--'
+              : scope.row.groupEnName || '--'
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Label.Nationality')">
+        <template #default="scope">
+          {{
+            Session.Get('lang') == 'cn'
+              ? scope.row.countryName || '--'
+              : scope.row.countryEnName || '--'
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Area.item1')">
+        <template #default="scope">
+          {{ scope.row.areaName || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Label.LoginIp')">
+        <template #default="scope">
+          {{ scope.row.lastIp || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Label.IpLimit')">
+        <template #default="scope">
+          <span v-if="scope.row.ipLimit == 1" class="">{{ $t('Btn.Yes') }}</span>
+          <span v-if="!scope.row.ipLimit" class="">{{ $t('Btn.No') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Label.limitIp')">
+        <template #default="scope">
+          {{ scope.row.limitIp || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Label.CreationDate')">
+        <template #default="scope">
+          {{ scope.row.addTime || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="center" width="120" :label="$t('Label.State')">
+        <template #default="scope">
+          <span v-if="scope.row.valid == 1" class="state crm_state_blue">{{
+            $t('State.Normal')
+          }}</span>
+          <span v-if="scope.row.valid == 0" class="state crm_state_red">{{
+            $t('State.Freeze')
+          }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="center" :label="$t('Label.Action')">
+        <template #default="scope">
+          <el-dropdown trigger="click" @command="handleCommand">
+            <span class="el-dropdown-link crm-cursor">
+              <i style="font-weight: bold; font-size: 20px" class="iconfont iconcaidan"></i>
+            </span>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item
+                  v-if="display['R-UserList-Update'].show"
+                  :command="{ type: 'editor', row: scope.row }"
+                >
+                  <el-icon><Operation /></el-icon>
+                  <span>{{ $t('Btn.Editor') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-UserList-Delete'].show"
+                  :command="{ type: 'delete', id: scope.row.id }"
+                >
+                  <el-icon><Delete /></el-icon>
+                  <span>{{ $t('Btn.Delete') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-UserList-GroupSettings'].show"
+                  :command="{ type: 'group', row: scope.row }"
+                >
+                  <el-icon><Discount /></el-icon>
+                  <span>{{ $t('R-UserList-GroupSettings') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-UserList-Unseal'].show"
+                  :command="{ type: 'Unseal', row: scope.row }"
+                >
+                  <el-icon><CircleCheck /></el-icon>
+                  <span>{{ $t('R-UserList-Unseal') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-UserList-reSecretKey'].show"
+                  :command="{ type: 'reSecretKey', row: scope.row }"
+                >
+                  <el-icon><Refresh /></el-icon>
+                  <span>{{ $t('R-UserList-reSecretKey') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-UserList-rePassword'].show"
+                  :command="{ type: 'rePassword', row: scope.row }"
+                >
+                  <el-icon><Refresh /></el-icon>
+                  <span>{{ $t('R-UserList-rePassword') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-UserList-closeSecretKey'].show"
+                  :command="{ type: 'closeSecretKey', row: scope.row }"
+                >
+                  <el-icon><Delete /></el-icon>
+                  <span>{{ $t('R-UserList-closeSecretKey') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-UserList-secretKey'].show"
+                  :command="{ type: 'secretKey', row: scope.row }"
+                >
+                  <el-icon><Key /></el-icon><span>{{ $t('R-UserList-secretKey') }}</span>
+                </el-dropdown-item>
+                <!--                <el-dropdown-item-->
+                <!--                  v-if="display['R-Marketing-Extension-Btn'].show"-->
+                <!--                  :command="{ type: 'extension', row: scope.row }"-->
+                <!--                >-->
+                <!--                  <el-icon><Key /></el-icon><span>{{ $t('R-Marketing-Extension-Btn') }}</span>-->
+                <!--                </el-dropdown-item>-->
+                <!--                <el-dropdown-item-->
+                <!--                  v-if="-->
+                <!--                    display['R-UserList-AccountSetup'].show || display['R-UserList-Disabled'].show-->
+                <!--                  "-->
+                <!--                  :command="{ type: 'accountSetup', row: scope.row }"-->
+                <!--                >-->
+                <!--                  <el-icon><Setting /></el-icon><span>{{ $t('R-UserList-AccountSetup') }}</span>-->
+                <!--                </el-dropdown-item>-->
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div v-if="pagerInfo.rowTotal" class="crm_pagination">
+      <div class="crm_page_total">
+        <span>{{ $t('Page.total.item1') }}</span>
+        <span>{{ pagerInfo.rowTotal }}</span>
+        <span>{{ $t('Page.total.item2') }}</span>
+      </div>
+      <el-pagination
+        class="page"
+        background
+        layout="sizes, prev, pager, next"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagerInfo.row"
+        :total="pagerInfo.rowTotal"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      >
+      </el-pagination>
+    </div>
+
+    <!-- 开户链接 弹出 -->
+    <el-dialog
+      v-model="dialogCheck"
+      :title="$t('Dashboard.Profile.ConsumerShareLinks')"
+      center
+      custom-class="dialog_header_w"
+    >
+      <div class="dia-content">
+        <el-form
+          ref="dialogCheckFormRef"
+          :model="dialogCheck_form"
+          label-width="150px"
+          class="dialogCheck_form"
+        >
+          <el-form-item prop="" :label="$t('Label.Name') + ':'">
+            {{ dialogCheck_form.name }}
+          </el-form-item>
+          <el-form-item prop="" :label="$t('Label.UserNames') + ':'">
+            {{ dialogCheck_form.username }}
+          </el-form-item>
+          <el-form-item prop="" :label="$t('Label.Email') + ':'">
+            {{ dialogCheck_form.email }}
+          </el-form-item>
+          <el-form-item
+            v-if="dialogCheck_form.departmentId == 1"
+            :label="$t('Dashboard.Profile.AgentShareLink')"
+          >
+            <span
+              style="
+                display: block;
+                width: 100%;
+                word-break: break-all;
+                line-height: 1.5;
+                margin-top: 8px;
+              "
+              >{{ Host00 + '/#/signup/' + dialogCheck_form.id + '/F0/B0' }} (
+              <span>{{ $t('Label.OutsideCommission') }}</span> 0/
+              <span>{{ $t('Dashboard.Profile.Allow') }}</span> )</span
+            >
+            <span
+              style="
+                display: block;
+                width: 100%;
+                word-break: break-all;
+                line-height: 1.5;
+                margin-top: 8px;
+              "
+              >{{ Host00 + '/#/signup/' + dialogCheck_form.id + '/F1/B0' }} (
+              <span>{{ $t('Label.OutsideCommission') }}</span> 10/
+              <span>{{ $t('Dashboard.Profile.Allow') }}</span> )</span
+            >
+            <span
+              style="
+                display: block;
+                width: 100%;
+                word-break: break-all;
+                line-height: 1.5;
+                margin-top: 8px;
+              "
+              >{{ Host00 + '/#/signup/' + dialogCheck_form.id + '/F2/B0' }} (
+              <span>{{ $t('Label.OutsideCommission') }}</span> 20/
+              <span>{{ $t('Dashboard.Profile.Allow') }}</span> )</span
+            >
+            <span
+              style="
+                display: block;
+                width: 100%;
+                word-break: break-all;
+                line-height: 1.5;
+                margin-top: 8px;
+              "
+              >{{ Host00 + '/#/signup/' + dialogCheck_form.id + '/F0/B1' }} (
+              <span>{{ $t('Label.OutsideCommission') }}</span> 0/
+              <span>{{ $t('Dashboard.Profile.NotAllow') }}</span> )</span
+            >
+            <span
+              style="
+                display: block;
+                width: 100%;
+                word-break: break-all;
+                line-height: 1.5;
+                margin-top: 8px;
+              "
+              >{{ Host00 + '/#/signup/' + dialogCheck_form.id + '/F1/B1' }} (
+              <span>{{ $t('Label.OutsideCommission') }}</span> 10/
+              <span>{{ $t('Dashboard.Profile.NotAllow') }}</span> )</span
+            >
+            <span
+              style="
+                display: block;
+                width: 100%;
+                word-break: break-all;
+                line-height: 1.5;
+                margin-top: 8px;
+              "
+              >{{ Host00 + '/#/signup/' + dialogCheck_form.id + '/F2/B1' }} (
+              <span>{{ $t('Label.OutsideCommission') }}</span> 20/
+              <span>{{ $t('Dashboard.Profile.NotAllow') }}</span> )</span
+            >
+          </el-form-item>
+        </el-form>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="dialogCheck = false">{{ $t('Btn.Confirm') }}</el-button>
+          <el-button @click="dialogCheck = false">{{ $t('Btn.Cancel') }}</el-button>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!--组别设置 弹出 -->
+    <el-dialog v-model="dialogCheck1" :title="dialogCheckTit" center custom-class="dialog_header_w">
+      <div class="dia-content">
+        <el-form
+          ref="dialogCheckForm1Ref"
+          :model="dialogCheck_form1"
+          :rules="rules"
+          label-width="190px"
+          class="dialogCheck_form"
+        >
+          <el-form-item prop="" :label="$t('Label.UserNames') + ':'">
+            {{ dialogCheck_form1.username }}
+          </el-form-item>
+          <div>
+            <el-form-item
+              prop="selectLoginTypes"
+              :label="$t('news_add_field.Label.SelectAccountType') + ':'"
+            >
+              <el-select
+                v-model="dialogCheck_form1.selectLoginTypes"
+                class="crm_search_down"
+                multiple
+                :placeholder="$t('Placeholder.Choose')"
+                @change="selectChange"
+              >
+                <el-option :label="$t('AccountType.ClassicAccount')" value="1"></el-option>
+                <el-option :label="$t('AccountType.SeniorAccount')" value="2"></el-option>
+                <el-option :label="$t('AccountType.SpeedAccount')" value="5"></el-option>
+                <el-option :label="$t('AccountType.NewSpeedAccount')" value="6"></el-option>
+                <el-option :label="$t('AccountType.StandardAccount')" value="7"></el-option>
+                <el-option :label="$t('AccountType.CentAccount')" value="8"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              prop="excludeLoginTypes"
+              :label="$t('news_add_field.Label.ExclusionAccountType') + ':'"
+            >
+              <el-select
+                v-model="dialogCheck_form1.excludeLoginTypes"
+                class="crm_search_down"
+                multiple
+                :placeholder="$t('Placeholder.Choose')"
+                @change="selectChange"
+              >
+                <el-option :label="$t('AccountType.ClassicAccount')" value="1"></el-option>
+                <el-option :label="$t('AccountType.SeniorAccount')" value="2"></el-option>
+                <el-option :label="$t('AccountType.SpeedAccount')" value="5"></el-option>
+                <el-option :label="$t('AccountType.NewSpeedAccount')" value="6"></el-option>
+                <el-option :label="$t('AccountType.StandardAccount')" value="7"></el-option>
+                <el-option :label="$t('AccountType.CentAccount')" value="8"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              prop="selectGroupType"
+              :label="$t('news_add_field.Label.SelectGroupType') + ':'"
+            >
+              <el-select
+                v-model="dialogCheck_form1.selectGroupType"
+                class="crm_search_down"
+                clearable
+                :placeholder="$t('Placeholder.Choose')"
+                @change="selectChange"
+              >
+                <el-option
+                  v-for="item in groupType_options"
+                  :key="item.name"
+                  :label="Session.Get('lang') == 'cn' ? item.name : item.enName"
+                  :value="item.id"
+                >
+                </el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              prop="defaultGroupType"
+              :label="$t('news_add_field.Label.DefaultGroupType') + ':'"
+            >
+              <el-select
+                v-model="dialogCheck_form1.defaultGroupType"
+                class="crm_search_down"
+                clearable
+                :placeholder="$t('Placeholder.Choose')"
+                @change="selectChange"
+              >
+                <el-option
+                  v-for="item in groupType_options"
+                  :key="item.name"
+                  :label="Session.Get('lang') == 'cn' ? item.name : item.enName"
+                  :value="item.id"
+                >
+                </el-option>
+              </el-select>
+            </el-form-item>
+          </div>
+        </el-form>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="toVerified()">{{ $t('Btn.Confirm') }}</el-button>
+          <el-button @click="cancel">{{ $t('Btn.Cancel') }}</el-button>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!--解封 弹出 -->
+    <el-dialog
+      v-model="dialogCheckUnseal"
+      :title="dialogCheckTit"
+      center
+      width="400"
+      custom-class="dialog_header_w"
+    >
+      <div class="dia-content">
+        <el-form
+          ref="dialogCheckFormUnsealRef"
+          :model="dialogCheck_formUnseal"
+          :rules="rules"
+          label-width="160px"
+          class="dialogCheck_form"
+        >
+          <el-form-item prop="" :label="$t('Label.UserNames') + ':'">
+            {{ dialogCheck_formUnseal.loginName }}
+          </el-form-item>
+        </el-form>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="toVerifiedUnseal()">{{ $t('Btn.Confirm') }}</el-button>
+          <el-button @click="cancel">{{ $t('Btn.Cancel') }}</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!--秘钥 弹出 -->
+    <el-dialog
+      v-model="dialogCheckKey"
+      :title="dialogCheckKeyTit"
+      center
+      width="600"
+      custom-class="dialog_header_w"
+    >
+      <div class="dia-content">
+        <el-form
+          ref="dialogCheckKeyFormRef"
+          :model="dialogCheckKey_form"
+          :rules="rules"
+          label-width="150px"
+          class="dialogCheck_form"
+        >
+          <template v-if="dialogCheckKey_type != 4">
+            <el-form-item prop="" :label="$t('Label.Name') + ':'">
+              {{ dialogCheckKey_form.name }}
+            </el-form-item>
+            <el-form-item prop="" :label="$t('Label.UserNames') + ':'">
+              {{ dialogCheckKey_form.username }}
+            </el-form-item>
+            <div v-if="dialogCheckKey_type == 3" class="googBox">
+              <img :src="dialogCheckKey_key.qrUrl" alt="" width="150px" />
+              <div class="googleSecretKey">
+                <span
+                  ><span>{{ $t('getCode.item5') }}</span
+                  >:</span
+                >
+                <input
+                  id="ConsumerShareLink1"
+                  v-model="dialogCheckKey_key.secretKey"
+                  class="link"
+                  readonly="readonly"
+                />
+                <span class="btn-copy" @click="CopyShareLink('ConsumerShareLink1')">{{
+                  $t('Dashboard.Profile.Copy')
+                }}</span>
+              </div>
+            </div>
+          </template>
+          <template v-if="dialogCheckKey_type == 4">
+            <el-form-item prop="updateType" :label="$t('Label.Type') + ':'">
+              <el-select
+                v-model="dialogCheckKey_form.updateType"
+                class="crm_search_down"
+                clearable
+                :placeholder="$t('Placeholder.Choose')"
+                @change="selectChange"
+              >
+                <el-option :label="$t('vaildate.password.updateType1')" :value="1"></el-option>
+                <el-option :label="$t('vaildate.password.updateType2')" :value="2"></el-option>
+                <el-option :label="$t('vaildate.password.updateType3')" :value="3"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item prop="password" :label="$t('Label.Pwd') + ':'">
+              <el-input
+                v-model.trim="dialogCheckKey_form.password"
+                class="crm_search_down"
+                :placeholder="$t('Placeholder.Input')"
+              ></el-input>
+            </el-form-item>
+          </template>
+        </el-form>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button v-if="dialogCheckKey_type == 1" type="primary" @click="toreSecretKey()">{{
+            $t('Btn.Confirm')
+          }}</el-button>
+          <el-button v-if="dialogCheckKey_type == 2" type="primary" @click="tocloseSecretKey()">{{
+            $t('Btn.Confirm')
+          }}</el-button>
+          <el-button v-if="dialogCheckKey_type == 4" type="primary" @click="rePassword()">{{
+            $t('Btn.Confirm')
+          }}</el-button>
+          <el-button
+            v-if="dialogCheckKey_type == 3"
+            type="primary"
+            @click="dialogCheckKey = false"
+            >{{ $t('Btn.Confirm') }}</el-button
+          >
+          <el-button @click="dialogCheckKey = false">{{ $t('Btn.Cancel') }}</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!--开户设置 弹出 -->
+    <el-dialog
+      v-model="dialogCheckAccountSetup"
+      :title="$t('R-UserList-AccountSetup')"
+      center
+      custom-class="dialog_header_w"
+    >
+      <div class="dia-content">
+        <el-form
+          ref="dialogCheckAccountSetupFormRef"
+          :model="dialogCheckAccountSetup_form"
+          :rules="rules"
+          label-width="190px"
+          class="dialogCheck_form"
+        >
+          <el-form-item prop="" :label="$t('Label.UserNames') + ':'">
+            {{ dialogCheckAccountSetup_form.username }}
+          </el-form-item>
+          <el-form-item
+            prop="excludeShowLoginTypes"
+            :label="$t('news_add_field.Label.ExclusionAccountType') + ':'"
+          >
+            <el-select
+              v-model="dialogCheckAccountSetup_form.excludeShowLoginTypes"
+              class="crm_search_down"
+              multiple
+              :placeholder="$t('Placeholder.Choose')"
+              :disabled="hideAccountSetup ? false : hideDisabled"
+              @change="selectChange"
+            >
+              <el-option :label="$t('AccountType.ClassicAccount')" value="1"></el-option>
+              <el-option :label="$t('AccountType.SeniorAccount')" value="2"></el-option>
+              <el-option :label="$t('AccountType.SpeedAccount')" value="5"></el-option>
+              <el-option :label="$t('AccountType.NewSpeedAccount')" value="6"></el-option>
+              <el-option :label="$t('AccountType.StandardAccount')" value="7"></el-option>
+              <el-option :label="$t('AccountType.CentAccount')" value="8"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button
+            v-if="display['R-UserList-AccountSetup'].show"
+            type="primary"
+            @click="toVerifiedAccountSetup()"
+            >{{ $t('Btn.Confirm') }}</el-button
+          >
+          <el-button @click="cancelAccountSetup">{{ $t('Btn.Cancel') }}</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 账户新增/修改弹出 -->
+    <UserAdd
+      :dialog-info-trading-add="dialogInfoTradingAdd"
+      :editor="editor"
+      :form-list="formList"
+      @confirm-to-reload="confirmToReload"
+      @close-add="closeAdd"
+    ></UserAdd>
+    <div v-if="dialogInfoTradingAdd" class="crm_verified_info_mask" @click="closeDiaAdd"></div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, computed, onMounted, watch, inject } from 'vue'
+  import { ElMessage, ElMessageBox } from 'element-plus'
+  import Service from '@/service/user'
+  import ServiceEnter from '@/service/enter'
+  import Config from '@/config/index'
+  import UserAdd from './components/UserAdd.vue'
+  // import AttributionNumber from '@/views/global/AttributionNumber'
+  // import ConsumerShareLink from '@/views/global/ConsumerShareLink.vue'
+  import {
+    Delete,
+    Discount,
+    Key,
+    Operation,
+    Plus,
+    Refresh,
+    Search,
+    Setting,
+  } from '@element-plus/icons-vue'
+  import { useI18n } from 'vue-i18n'
+
+  const { Code, Host00 } = Config
+  const Session = inject('session')
+  const { t } = useI18n()
+
+  // Refs
+  const formRef = ref(null)
+  const dialogCheckFormRef = ref(null)
+  const dialogCheckForm1Ref = ref(null)
+  const dialogCheckFormUnsealRef = ref(null)
+  const dialogCheckKeyFormRef = ref(null)
+  const dialogCheckAccountSetupFormRef = ref(null)
+  const consumerShareLinkRef = ref(null)
+
+  // State
+  const pictLoading = ref(false)
+  const mock_tableData = ref([])
+  const search = ref({
+    tag: 1,
+    email: '',
+    roleId: '',
+    ibNo: '',
+    date: [],
+    startDate: '',
+    endDate: '',
+  })
+  const dialogInfoNumber = ref(false)
+  const formInfoNum = ref({})
+  const dialogInfo = ref(false)
+  const addType = ref('')
+  const formList = ref({})
+  const type = ref(1)
+  const dialogInfoTradingAdd = ref(false)
+  const btn = ref('')
+  const editor = ref('')
+  const dialogInfoTrading = ref(false)
+  const infoType = ref('')
+  const formInfo = ref({})
+  const roleName = ref([])
+
+  const dialogCheck = ref(false)
+  const dialogCheck_form = ref({})
+
+  const dialogCheck1 = ref(false)
+  const dialogCheck_form1 = ref({})
+  const dialogCheckTit = ref('')
+
+  const dialogCheckUnseal = ref(false)
+  const dialogCheck_formUnseal = ref({})
+
+  const groupType_options = ref({})
+
+  const dialogCheckKey = ref(false)
+  const dialogCheckKeyTit = ref('')
+  const dialogCheckKey_form = ref({
+    password: '',
+    updateType: 1,
+  })
+  const dialogCheckKey_type = ref(null)
+  const dialogCheckKey_key = ref({})
+
+  const dialogCheckAccountSetup = ref(false)
+  const dialogCheckAccountSetup_form = ref({})
+
+  const pagerInfo = ref({ row: 10, current: 1, pageTotal: 0, rowTotal: 0 })
+  const flag = ref(false)
+
+  // 验证规则
+  const rules = {
+    password: [
+      {
+        validator: (rule, value, callback) => {
+          if (Config.Pattern.Password1.test(value) || !value) {
+            callback()
+          } else {
+            callback(new Error(t('vaildate.password.same1')))
+          }
+        },
+        trigger: 'blur',
+      },
+    ],
+    updateType: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'change',
+      },
+    ],
+  }
+
+  // Computed
+  const hideDisabled = computed(() => {
+    return display.value['R-UserList-Disabled'].show
+  })
+
+  const hideAccountSetup = computed(() => {
+    return display.value['R-UserList-AccountSetup'].show
+  })
+
+  const display = computed(() => {
+    return JSON.parse(Session.Get('display', true))
+  })
+
+  // Methods
+  const parseStringToArray = (value) => {
+    if (!value) return []
+    if (typeof value === 'string') {
+      return value.split(',').filter((item) => item)
+    }
+    if (Array.isArray(value)) {
+      return value
+    }
+    return []
+  }
+
+  const closeDia = () => {
+    dialogInfoNumber.value = false
+  }
+
+  const close = (val) => {
+    dialogInfoNumber.value = val
+  }
+
+  const selectChange = () => {
+    // force update if needed
+  }
+
+  const CopyShareLink = (link) => {
+    let linkEle = document.getElementById(link)
+    linkEle.value = linkEle.value.split('--')[0]
+    linkEle.select()
+    document.execCommand('Copy')
+    ElMessage.success(t('Dashboard.Profile.CopySuccess'))
+  }
+
+  const handleCommand = (command) => {
+    if (command.type == 'editor') {
+      UserSingle(command.row.id)
+      editor.value = 1
+    } else if (command.type == 'delete') {
+      deleteUser(command.id)
+    } else if (command.type == 'group') {
+      if (dialogCheckForm1Ref.value) {
+        dialogCheckForm1Ref.value.resetFields()
+      }
+      dialogCheck_form1.value = {}
+      dialogCheckTit.value = t('R-UserList-GroupSettings')
+      dialogCheck_form1.value.id = command.row.id
+      dialogCheck_form1.value.username = command.row.username
+      dialogCheck_form1.value.selectLoginTypes = parseStringToArray(command.row.selectLoginTypes)
+      dialogCheck_form1.value.excludeLoginTypes = parseStringToArray(command.row.excludeLoginTypes)
+      dialogCheck_form1.value.selectGroupType = command.row.selectGroupType
+      dialogCheck_form1.value.defaultGroupType = command.row.defaultGroupType
+      customGroupType()
+    } else if (command.type == 'Unseal') {
+      if (dialogCheckFormUnsealRef.value) {
+        dialogCheckFormUnsealRef.value.resetFields()
+      }
+
+      dialogCheck_formUnseal.value = {}
+      dialogCheckTit.value = t('R-UserList-Unseal')
+      dialogCheck_formUnseal.value.loginName = command.row.username
+      dialogCheckUnseal.value = true
+    } else if (
+      command.type == 'reSecretKey' ||
+      command.type == 'rePassword' ||
+      command.type == 'closeSecretKey' ||
+      command.type == 'secretKey'
+    ) {
+      if (dialogCheckKeyFormRef.value) {
+        dialogCheckKeyFormRef.value.resetFields()
+      }
+
+      dialogCheckKey_form.value = command.row
+      if (command.type == 'reSecretKey') {
+        dialogCheckKeyTit.value = t('R-UserList-reSecretKey')
+        dialogCheckKey_type.value = 1
+      } else if (command.type == 'rePassword') {
+        dialogCheckKeyTit.value = t('R-UserList-rePassword')
+        dialogCheckKey_type.value = 4
+      } else if (command.type == 'closeSecretKey') {
+        dialogCheckKeyTit.value = t('R-UserList-closeSecretKey')
+        dialogCheckKey_type.value = 2
+      } else if (command.type == 'secretKey') {
+        dialogCheckKeyTit.value = t('R-UserList-secretKey')
+        dialogCheckKey_type.value = 3
+        togetsecretKey()
+      }
+      dialogCheckKey.value = true
+    } else if (command.type == 'extension') {
+      // getLink(command.row)
+    } else if (command.type == 'accountSetup') {
+      if (dialogCheckAccountSetupFormRef.value) {
+        dialogCheckAccountSetupFormRef.value.resetFields()
+      }
+      dialogCheckAccountSetup_form.value = {}
+      dialogCheckAccountSetup_form.value.id = command.row.id
+      dialogCheckAccountSetup_form.value.username = command.row.username
+      dialogCheckAccountSetup_form.value.excludeShowLoginTypes = parseStringToArray(
+        command.row.excludeShowLoginTypes
+      )
+      dialogCheckAccountSetup.value = true
+    }
+  }
+
+  const customGroupType = async () => {
+    let res = await Service.customGroupTypeList({})
+    if (res.code == Code.StatusOK) {
+      groupType_options.value = res.data
+      dialogCheck1.value = true
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const toVerified = async () => {
+    dialogCheckForm1Ref.value.validate(async (valid) => {
+      if (valid) {
+        if (flag.value) {
+          return
+        } else {
+          flag.value = true
+        }
+
+        let res = await Service.ibGroup({
+          id: dialogCheck_form1.value.id,
+          selectLoginTypes: dialogCheck_form1.value.selectLoginTypes || null,
+          excludeLoginTypes: dialogCheck_form1.value.excludeLoginTypes || null,
+          selectGroupType: dialogCheck_form1.value.selectGroupType || null,
+          defaultGroupType: dialogCheck_form1.value.defaultGroupType || null,
+        })
+        if (res.code == Code.StatusOK) {
+          ElMessage.success(t('Msg.ModifySuccess'))
+          cancel()
+          searchFunc()
+          flag.value = false
+        } else {
+          flag.value = false
+          ElMessage.error(res.msg)
+        }
+      } else {
+        return false
+      }
+    })
+  }
+
+  const toVerifiedAccountSetup = async () => {
+    dialogCheckAccountSetupFormRef.value.validate(async (valid) => {
+      if (valid) {
+        if (flag.value) {
+          return
+        } else {
+          flag.value = true
+        }
+
+        let res = await Service.userAccountSetup({
+          id: dialogCheckAccountSetup_form.value.id,
+          excludeShowLoginTypes: dialogCheckAccountSetup_form.value.excludeShowLoginTypes || null,
+        })
+        if (res.code == Code.StatusOK) {
+          ElMessage.success(t('Msg.ModifySuccess'))
+          cancelAccountSetup()
+          searchFunc()
+          flag.value = false
+        } else {
+          flag.value = false
+          ElMessage.error(res.msg)
+        }
+      } else {
+        return false
+      }
+    })
+  }
+
+  const cancelAccountSetup = () => {
+    dialogCheckAccountSetup.value = false
+    dialogCheckAccountSetupFormRef.value && dialogCheckAccountSetupFormRef.value.resetFields()
+  }
+
+  const cancel = () => {
+    dialogCheck1.value = false
+    dialogCheckForm1Ref.value && dialogCheckForm1Ref.value.resetFields()
+    dialogCheckUnseal.value = false
+    dialogCheckFormUnsealRef.value && dialogCheckFormUnsealRef.value.resetFields()
+    dialogCheckAccountSetup.value = false
+    dialogCheckAccountSetupFormRef.value && dialogCheckAccountSetupFormRef.value.resetFields()
+  }
+
+  const toVerifiedUnseal = async () => {
+    dialogCheckFormUnsealRef.value.validate(async (valid) => {
+      if (valid) {
+        if (flag.value) {
+          return
+        } else {
+          flag.value = true
+        }
+
+        let res = await Service.userUnseal({
+          loginName: dialogCheck_formUnseal.value.loginName,
+        })
+        if (res.code == Code.StatusOK) {
+          ElMessage.success(t('Msg.ModifySuccess'))
+          cancel()
+          searchFunc()
+          flag.value = false
+        } else {
+          flag.value = false
+          ElMessage.error(res.msg)
+        }
+      } else {
+        return false
+      }
+    })
+  }
+
+  const toreSecretKey = async () => {
+    if (flag.value) {
+      return
+    } else {
+      flag.value = true
+    }
+    let res = await Service.secretKeyRe({
+      id: dialogCheckKey_form.value.id,
+    })
+    if (res.code == Code.StatusOK) {
+      ElMessage.success(t('Msg.ModifySuccess'))
+      dialogCheckKey.value = false
+      searchFunc()
+      flag.value = false
+    } else {
+      flag.value = false
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const tocloseSecretKey = async () => {
+    if (flag.value) {
+      return
+    } else {
+      flag.value = true
+    }
+    let res = await Service.secretKeyClose({
+      id: dialogCheckKey_form.value.id,
+    })
+    if (res.code == Code.StatusOK) {
+      ElMessage.success(t('Msg.ModifySuccess'))
+      dialogCheckKey.value = false
+      searchFunc()
+      flag.value = false
+    } else {
+      flag.value = false
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const rePassword = async () => {
+    dialogCheckKeyFormRef.value.validate(async (valid) => {
+      if (valid) {
+        if (flag.value) {
+          return
+        } else {
+          flag.value = true
+        }
+        let res = await Service.rePassword({
+          id: dialogCheckKey_form.value.id,
+          password: dialogCheckKey_form.value.password,
+          updateType: dialogCheckKey_form.value.updateType,
+        })
+        if (res.code == Code.StatusOK) {
+          dialogCheckKey.value = false
+          flag.value = false
+          ElMessageBox.confirm(`${t('Label.Pwd')}:${res.data}`, t('Msg.SystemPrompt'), {
+            confirmButtonText: t('Btn.Confirm'),
+            cancelButtonText: t('Btn.Cancel'),
+          })
+            .then(() => {
+              searchFunc()
+            })
+            .catch(() => {})
+        } else {
+          ElMessage.error(res.msg)
+          flag.value = false
+        }
+      } else {
+        flag.value = false
+      }
+    })
+  }
+
+  const togetsecretKey = async () => {
+    dialogCheckKey_key.value = {}
+    if (flag.value) {
+      return
+    } else {
+      flag.value = true
+    }
+    let res = await Service.secretKeyGet({
+      id: dialogCheckKey_form.value.id,
+    })
+    if (res.code == Code.StatusOK) {
+      dialogCheckKey_key.value = res.data
+      flag.value = false
+    } else {
+      ElMessage.error(res.msg)
+      flag.value = false
+    }
+  }
+
+  const closeDiaAdd = () => {
+    dialogInfoTradingAdd.value = false
+    editor.value = ''
+  }
+
+  const closeAdd = (val) => {
+    dialogInfoTradingAdd.value = val
+  }
+
+  const addUser = () => {
+    editor.value = ''
+    dialogInfoTradingAdd.value = true
+  }
+
+  const confirmToReload = () => {
+    closeDiaAdd()
+    searchFunc()
+  }
+
+  const closeDiaTrading = () => {
+    dialogInfoTrading.value = false
+  }
+
+  const closeTrading = (val) => {
+    dialogInfoTrading.value = val
+  }
+
+  const openInfo = (val) => {
+    dialogInfo.value = val
+  }
+
+  const accountOpen = (form) => {
+    formInfo.value = form
+    dialogInfoTrading.value = true
+  }
+
+  const deleteUser = async (id) => {
+    ElMessageBox.confirm(t('Msg.Delete'), t('Msg.SystemPrompt'), {
+      confirmButtonText: t('Btn.Confirm'),
+      cancelButtonText: t('Btn.Cancel'),
+    })
+      .then(async () => {
+        let ids = []
+        ids.push(id)
+        let res = await Service.userListDelete({ ids: ids })
+        if (res.code == Code.StatusOK) {
+          ElMessage.success(t('Msg.DeleteSuccess'))
+          searchFunc()
+        } else {
+          ElMessage.error(res.msg)
+        }
+      })
+      .catch(() => {})
+  }
+
+  const UserSingle = async (id) => {
+    let res = await Service.userSingle({
+      id: id,
+    })
+    if (res.code == Code.StatusOK) {
+      formList.value = res.data
+      addType.value = 'user_CustomerList'
+      dialogInfoTradingAdd.value = true
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const getRoleList = async () => {
+    let res = await Service.roleNameList({})
+    if (res.code == Code.StatusOK) {
+      roleName.value = res.data
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const searchFunc = async () => {
+    pictLoading.value = true
+    if (!display.value['R-UserList-Search'].show) {
+      ElMessage.warning(t('Msg.NotDisplay'))
+      pictLoading.value = false
+      return
+    }
+    if (search.value.date == null) {
+      search.value.startDate = ''
+      search.value.endDate = ''
+    } else if (search.value.date.length == 0) {
+      search.value.startDate = ''
+      search.value.endDate = ''
+    } else {
+      search.value.startDate = search.value.date[0]
+      search.value.endDate = search.value.date[1]
+    }
+    let res = await Service.userList({
+      ...search.value,
+      page: {
+        current: pagerInfo.value.current,
+        row: pagerInfo.value.row,
+      },
+    })
+    if (res.code == Code.StatusOK) {
+      mock_tableData.value = res.data
+      if (res.page != null) {
+        pagerInfo.value.rowTotal = res.page.rowTotal
+        pagerInfo.value.pageTotal = res.page.pageTotal
+      } else {
+        pagerInfo.value.rowTotal = 0
+      }
+      ElMessage.success(t('Msg.SearchSuccess'))
+      pictLoading.value = false
+    } else {
+      ElMessage.error(res.msg)
+      pictLoading.value = false
+    }
+  }
+
+  const getSearchDate = (e) => {
+    search.value.date = e
+  }
+
+  const toSearch = () => {
+    pagerInfo.value.current = 1
+    searchFunc()
+  }
+
+  const handleSizeChange = (val) => {
+    pagerInfo.value.row = val
+    searchFunc()
+  }
+
+  const handleCurrentChange = (val) => {
+    pagerInfo.value.current = val
+    searchFunc()
+  }
+
+  const CopyShareLinks = async (link) => {
+    try {
+      await navigator.clipboard.writeText(link)
+      ElMessage.success(t('Dashboard.Profile.CopySuccess'))
+    } catch (err) {
+      copyText(link)
+    }
+  }
+
+  const copyText = (text) => {
+    const textarea = document.createElement('textarea')
+    textarea.value = text
+    document.body.appendChild(textarea)
+    textarea.select()
+    try {
+      document.execCommand('copy')
+      ElMessage.success(t('Dashboard.Profile.CopySuccess'))
+    } catch (err) {
+      // console.log(err)
+    }
+    document.body.removeChild(textarea)
+  }
+
+  // Lifecycle
+  onMounted(() => {
+    getRoleList()
+    searchFunc()
+  })
+
+  // Watchers
+  watch(
+    () => search.value.tag,
+    () => {
+      search.value.email = ''
+      search.value.ibNo = ''
+    }
+  )
+</script>
+<style scoped lang="scss">
+  @import 'index.scss';
+</style>
+<style lang="scss">
+  #user_CustomerList {
+    .dialog_header_w {
+      .crm_search_down {
+        width: 400px;
+      }
+    }
+  }
+</style>

+ 746 - 0
src/views/user/userRole/components/RoleAdd/index.vue

@@ -0,0 +1,746 @@
+<template>
+  <div
+    id="TradingDetailedInfoAdd"
+    v-loading="loading"
+    element-loading-background="rgba(122, 122, 122, 0.8)"
+    element-loading-text="Loading..."
+    class="InfoBox"
+    :class="{ active: dialogInfoTradingAdd }"
+  >
+    <div class="header">
+      <span v-if="editor" class="title">{{ $t('Label.EditorRole') }}</span>
+      <span v-else class="title">{{ $t('Label.AddRole') }}</span>
+      <span class="close crm-cursor" @click="close">
+        <el-icon><Close /></el-icon>
+      </span>
+    </div>
+    <el-form ref="formRef" :rules="rules" :model="form" label-width="100PX">
+      <el-form-item prop="name" :label="$t('Label.RoleName') + ':'">
+        <el-input
+          v-model="form.name"
+          :placeholder="$t('Placeholder.Input')"
+          @input="selectChange"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="departmentId" :label="$t('Label.RoleGroup') + ':'">
+        <el-select
+          v-model="form.departmentId"
+          class="crm_search_down"
+          :placeholder="$t('Placeholder.Choose')"
+          @change="selectChange"
+        >
+          <el-option
+            v-for="(item, index) in group"
+            :key="index"
+            :label="item.name"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item class="authorityBox" :label="$t('Label.permissions') + ':'">
+        <div
+          style="
+            border: 1px solid #dcdfe6;
+            padding: 5px 2px;
+            display: flex;
+            flex-wrap: wrap;
+            justify-content: flex-end;
+            width: 100%;
+          "
+        >
+          <el-button
+            v-if="role_system"
+            style="padding-right: 4px"
+            type="text"
+            @click="() => append('one')"
+          >
+            {{ $t('R-OneNodeAdd') }}
+          </el-button>
+          <el-tree
+            ref="treeRef"
+            style="width: 100%"
+            :data="roleTreeDate"
+            show-checkbox
+            :props="defaultProps"
+            node-key="tId"
+            :default-checked-keys="defaultSelect"
+            @check="handleCheck"
+          >
+            <template #default="{ node, data }">
+              <span class="custom-tree-node">
+                <span>{{ node.label }}</span>
+                <span v-if="role_system">
+                  <el-icon @click="() => append(data)"><CirclePlus /></el-icon>
+                  <el-icon @click="() => update(data)"><Edit /></el-icon>
+                  <el-icon @click="() => remove(data)"><Delete /></el-icon>
+                </span>
+              </span>
+            </template>
+          </el-tree>
+        </div>
+      </el-form-item>
+    </el-form>
+    <el-button :loading="addLoading" class="btn crm-cursor" @click="confirm">{{
+      $t('Btn.Confirm')
+    }}</el-button>
+    <el-dialog
+      v-model="dialogCheck"
+      :title="dialogCheck_form.title"
+      center
+      append-to-body
+      width="600"
+      custom-class="dialog_header_w"
+      @close="cancel"
+    >
+      <div class="dia-content">
+        <el-form
+          ref="dialogCheckFormRef"
+          :model="dialogCheck_form"
+          :rules="rules"
+          label-width="135px"
+          class="dialogCheck_form"
+        >
+          <el-form-item
+            v-if="dialogCheck_form.typeIndex1 != 3"
+            prop="name"
+            :label="$t('Label.AuthorityName') + ':'"
+          >
+            <el-input
+              v-model="dialogCheck_form.name"
+              style="width: 400px"
+              :placeholder="$t('Placeholder.Input')"
+            ></el-input>
+          </el-form-item>
+          <el-form-item
+            v-if="dialogCheck_form.typeIndex != 3 && dialogCheck_form.typeIndex1 != 3"
+            prop="link"
+            :label="$t('Label.Url') + ':'"
+          >
+            <el-input
+              v-model.trim="dialogCheck_form.link"
+              style="width: 400px"
+              :placeholder="$t('Placeholder.Input')"
+            ></el-input>
+          </el-form-item>
+          <el-form-item
+            v-if="dialogCheck_form.typeIndex != 3 && dialogCheck_form.typeIndex1 != 3"
+            prop="icon"
+            :label="$t('Label.Icon') + ':'"
+          >
+            <el-input
+              v-model.trim="dialogCheck_form.icon"
+              style="width: 400px"
+              :placeholder="$t('Placeholder.Input')"
+            ></el-input>
+          </el-form-item>
+          <el-form-item
+            v-if="dialogCheck_form.typeIndex != 3 && dialogCheck_form.typeIndex1 != 3"
+            prop="subIndex"
+            :label="$t('Label.SubIndex') + ':'"
+          >
+            <el-input
+              v-model.number.trim="dialogCheck_form.subIndex"
+              style="width: 400px"
+              :placeholder="$t('Placeholder.Input')"
+              @input="selectChange"
+            ></el-input>
+          </el-form-item>
+          <el-form-item
+            v-if="dialogCheck_form.typeIndex == 3 && dialogCheck_form.typeIndex1 != 3"
+            prop="action"
+            :label="$t('Label.Url') + ':'"
+          >
+            <el-input
+              v-model.trim="dialogCheck_form.action"
+              style="width: 400px"
+              :placeholder="$t('Placeholder.Input')"
+            ></el-input>
+          </el-form-item>
+          <div
+            v-if="dialogCheck_form.typeIndex1 == 3"
+            style="text-align: center; font-size: 20px; padding: 25px 0"
+          >
+            {{ $t('Msg.Delete') }}
+          </div>
+        </el-form>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button v-if="dialogCheck_form.typeIndex1 == 1" type="primary" @click="toAppend()">
+            {{ $t('Btn.Confirm') }}
+          </el-button>
+          <el-button v-if="dialogCheck_form.typeIndex1 == 2" type="primary" @click="toUpdate()">
+            {{ $t('Btn.Confirm') }}
+          </el-button>
+          <el-button v-if="dialogCheck_form.typeIndex1 == 3" type="primary" @click="toRemove()">
+            {{ $t('Btn.Confirm') }}
+          </el-button>
+          <el-button @click="cancel">{{ $t('Btn.Cancel') }}</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, onMounted, watch, inject } from 'vue'
+  import { CirclePlus, Edit, Delete, Close } from '@element-plus/icons-vue'
+  import { ElMessage, ElMessageBox } from 'element-plus'
+  import ServiceUser from '@/service/user'
+  import Config from '@/config/index'
+  import { useI18n } from 'vue-i18n'
+
+  const { Code } = Config
+  const { t } = useI18n()
+  const Session = inject('session')
+
+  // props
+  const props = defineProps({
+    dialogInfoTradingAdd: {
+      type: Boolean,
+      default: false,
+    },
+    editor: {
+      default: '',
+    },
+    myInfo: {
+      default: '',
+    },
+    formList: {
+      default: '',
+    },
+  })
+
+  // emits
+  const emit = defineEmits(['confirmToReload', 'closeAdd'])
+
+  // refs
+  const formRef = ref(null)
+  const treeRef = ref(null)
+  const dialogCheckFormRef = ref(null)
+  // 新增loading
+  const addLoading = ref(false)
+  const loading = ref(false)
+
+  // reactive data
+  const form = reactive({})
+  const group = ref([])
+  const roleTreeDate = ref([])
+  const defaultSelect = ref([])
+  const data_check = ref([])
+
+  const dialogCheck = ref(false)
+  const dialogCheck_form = reactive({
+    title: '',
+    typeIndex: null,
+    typeIndex1: null,
+    pid: null,
+    valid: null,
+    id: null,
+    subIndex: null,
+    name: '',
+    link: '',
+    icon: '',
+    action: '',
+    ids: [],
+    nodeId: null,
+    code: '',
+  })
+
+  // constants
+  const defaultProps = {
+    children: 'children',
+    label: 'name',
+  }
+
+  // computed
+  const role_system = computed(() => {
+    const user = JSON.parse(Session.Get('user', true) || '{}')
+    return user.departmentId == null && user.roleName == 'ROLE_SYSTEM'
+  })
+
+  // rules
+  const rules = reactive({
+    name: [
+      {
+        required: true,
+        message: t('Placeholder.Input'),
+        trigger: 'blur',
+      },
+    ],
+    link: [
+      {
+        required: true,
+        message: t('Placeholder.Input'),
+        trigger: 'blur',
+      },
+    ],
+    icon: [
+      {
+        required: true,
+        message: t('Placeholder.Input'),
+        trigger: 'blur',
+      },
+    ],
+    subIndex: [
+      {
+        required: true,
+        message: t('Placeholder.Input'),
+        trigger: 'blur',
+      },
+    ],
+    action: [
+      {
+        required: true,
+        message: t('Placeholder.Input'),
+        trigger: 'blur',
+      },
+    ],
+  })
+
+  // methods
+  const selectChange = () => {
+    // 这里可能需要强制更新
+  }
+
+  const append = (data) => {
+    console.log(data, 'append')
+    if (data == 'one') {
+      dialogCheck_form.title = t('R-OneNodeAdd')
+      dialogCheck_form.typeIndex = 1
+      dialogCheck_form.typeIndex1 = 1
+      dialogCheck_form.pid = null
+      dialogCheck_form.valid = 1
+    } else {
+      if (data.type == 0 && !data.pid) {
+        dialogCheck_form.title = t('R-TwoNodeAdd')
+        dialogCheck_form.typeIndex = 2
+        dialogCheck_form.typeIndex1 = 1
+        dialogCheck_form.pid = data.id
+        dialogCheck_form.valid = 1
+      } else {
+        dialogCheck_form.title = t('R-BtnAdd')
+        dialogCheck_form.typeIndex = 3
+        dialogCheck_form.typeIndex1 = 1
+        dialogCheck_form.nodeId = data.id
+        dialogCheck_form.valid = 1
+      }
+    }
+    dialogCheck.value = true
+  }
+
+  const update = (data) => {
+    if (data.type == 0 && !data.pid) {
+      dialogCheck_form.title = t('R-OneNodeAdd')
+      dialogCheck_form.typeIndex = 1
+      dialogCheck_form.typeIndex1 = 2
+      dialogCheck_form.pid = null
+      dialogCheck_form.valid = 1
+      dialogCheck_form.id = data.id
+      dialogCheck_form.subIndex = data.subIndex
+    } else {
+      if (data.type == 0 && data.pid) {
+        dialogCheck_form.title = t('R-TwoNodeAdd')
+        dialogCheck_form.typeIndex = 2
+        dialogCheck_form.typeIndex1 = 2
+        dialogCheck_form.pid = data.pid
+        dialogCheck_form.valid = 1
+        dialogCheck_form.id = data.id
+        dialogCheck_form.subIndex = data.subIndex
+      } else if (data.type == 1 && data.pid) {
+        dialogCheck_form.title = t('R-BtnAdd')
+        dialogCheck_form.typeIndex = 3
+        dialogCheck_form.typeIndex1 = 2
+        dialogCheck_form.nodeId = data.pid
+        dialogCheck_form.valid = 1
+        dialogCheck_form.id = data.id
+      }
+    }
+
+    dialogCheck_form.name = data.name || ''
+    dialogCheck_form.link = data.link || ''
+    dialogCheck_form.icon = data.icon || ''
+    dialogCheck_form.action = data.action || ''
+
+    dialogCheck.value = true
+  }
+
+  const remove = (data) => {
+    if (data.type == 0 && !data.pid) {
+      dialogCheck_form.title = t('R-OneNodeAdd')
+      dialogCheck_form.typeIndex = 1
+      dialogCheck_form.typeIndex1 = 3
+      dialogCheck_form.ids = [data.id]
+    } else {
+      if (data.type == 0 && data.pid) {
+        dialogCheck_form.title = t('R-TwoNodeAdd')
+        dialogCheck_form.typeIndex = 2
+        dialogCheck_form.typeIndex1 = 3
+        dialogCheck_form.ids = [data.id]
+      } else if (data.type == 1 && data.pid) {
+        dialogCheck_form.title = t('R-BtnAdd')
+        dialogCheck_form.typeIndex = 3
+        dialogCheck_form.typeIndex1 = 3
+        dialogCheck_form.ids = [data.id]
+      }
+    }
+    dialogCheck.value = true
+  }
+
+  const getDepartmentList = async () => {
+    try {
+      const res = await ServiceUser.departmentList({})
+      if (res.code == Code.StatusOK) {
+        group.value = res.data
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } catch (error) {
+      ElMessage.error(t('Msg.RequestError'))
+    }
+  }
+
+  const handleCheck = (data, checked) => {
+    data_check.value = []
+    const checkedNodes = [...checked.checkedNodes, ...checked.halfCheckedNodes]
+
+    checkedNodes.forEach((item) => {
+      const dataItem = {
+        name: item.code,
+        id: item.id,
+        pid: item.pid,
+        type: item.type,
+      }
+      data_check.value.push(dataItem)
+    })
+  }
+
+  const getDataCheck = () => {
+    const data_c = []
+    roleTreeDate.value.forEach((item) => {
+      if (item.show) {
+        data_c.push({
+          name: item.code,
+          id: item.id,
+          pid: item.pid,
+          type: item.type,
+        })
+      }
+      if (item.children?.length) {
+        item.children.forEach((item1) => {
+          if (item1.show) {
+            data_c.push({
+              name: item1.code,
+              id: item1.id,
+              pid: item1.pid,
+              type: item1.type,
+            })
+          }
+          const btnsArray = item1.btns ? Object.values(item1.btns) : []
+          btnsArray.forEach((item2) => {
+            if (item2.show) {
+              data_c.push({
+                name: item2.code,
+                id: item2.id,
+                pid: item2.pid,
+                type: item2.type,
+              })
+            }
+          })
+        })
+      }
+    })
+    data_check.value = data_c
+  }
+
+  const cancel = () => {
+    dialogCheck.value = false
+    dialogCheckFormRef.value?.resetFields()
+  }
+
+  const toAppend = async () => {
+    try {
+      const valid = await dialogCheckFormRef?.value.validate()
+      console.log(valid)
+      console.log(dialogCheck_form)
+      if (valid) {
+        if (dialogCheck_form.typeIndex == 1 || dialogCheck_form.typeIndex == 2) {
+          dialogCheck_form.subIndex = Number(dialogCheck_form.subIndex)
+          dialogCheck_form.code = dialogCheck_form.name
+
+          const res = await ServiceUser.authorityNodeAdd({
+            ...dialogCheck_form,
+          })
+
+          if (res.code == Code.StatusOK) {
+            ElMessage.success(res.msg)
+          } else {
+            ElMessage.error(res.msg)
+          }
+        }
+        cancel()
+        getRoleDetailAdd()
+      }
+    } catch (error) {
+      console.error('表单验证失败:', error)
+    }
+  }
+  const getRoleDetailUpdate = async () => {
+    // 复制表单数据
+    Object.assign(form, props.formList)
+
+    try {
+      const res = await ServiceUser.groupSingleUpdate({
+        id: props.formList?.id || form.id,
+      })
+
+      if (res.code === Code.StatusOK) {
+        // 处理角色树数据
+        const processedData = processRoleTreeData(res.data)
+        roleTreeDate.value = processedData.data
+        defaultSelect.value = processedData.select
+      } else {
+        ElMessage.error(res.msg || t('Msg.RequestError'))
+      }
+      loading.value = false
+    } catch (error) {
+      console.error('获取角色详情失败:', error)
+      ElMessage.error(t('Msg.RequestError'))
+    }
+  }
+  // 处理角色树数据的函数(可以独立出来复用)
+  const processRoleTreeData = (treeData) => {
+    const select = []
+
+    const processedData = treeData.map((item) => {
+      // 处理一级节点
+      const processedItem = {
+        ...item,
+        name: t(item.name),
+        children: item.children ? processChildrenNodes(item.children, select) : [],
+      }
+
+      return processedItem
+    })
+
+    return {
+      data: processedData,
+      select,
+    }
+  }
+  // 处理子节点
+  const processChildrenNodes = (children, select) => {
+    return children.map((child) => {
+      // 处理二级节点
+      const btnsArray = child.btns ? Object.values(child.btns) : []
+
+      // 处理三级节点(按钮节点)
+      const buttonNodes = btnsArray.map((btn) => {
+        const buttonNode = {
+          ...btn,
+          name: t(btn.name),
+        }
+
+        // 如果按钮节点显示,添加到选中列表中
+        if (buttonNode.show) {
+          select.push(buttonNode.tId)
+        }
+
+        return buttonNode
+      })
+
+      const processedChild = {
+        ...child,
+        name: t(child.name),
+        children: buttonNodes,
+      }
+
+      // 如果没有子节点且当前节点显示,添加到选中列表中
+      if (!buttonNodes.length && processedChild.show) {
+        select.push(processedChild.tId)
+      }
+
+      return processedChild
+    })
+  }
+
+  const toUpdate = async () => {
+    try {
+      const valid = await dialogCheckFormRef.value.validate()
+      if (valid) {
+        if (dialogCheck_form.typeIndex == 1 || dialogCheck_form.typeIndex == 2) {
+          dialogCheck_form.subIndex = Number(dialogCheck_form.subIndex)
+          dialogCheck_form.code = dialogCheck_form.name
+
+          const res = await ServiceUser.authorityNodeUpdate({
+            ...dialogCheck_form,
+          })
+
+          if (res.code == Code.StatusOK) {
+            ElMessage.success(res.msg)
+          } else {
+            ElMessage.error(res.msg)
+          }
+        }
+        cancel()
+        getRoleDetailAdd()
+      }
+    } catch (error) {
+      console.error('表单验证失败:', error)
+    }
+  }
+
+  const toRemove = async () => {
+    try {
+      const valid = await dialogCheckFormRef.value.validate()
+      if (valid) {
+        if (dialogCheck_form.typeIndex == 1 || dialogCheck_form.typeIndex == 2) {
+          const res = await ServiceUser.authorityNodeDelete({
+            ...dialogCheck_form,
+          })
+
+          if (res.code == Code.StatusOK) {
+            ElMessage.success(res.msg)
+          } else {
+            ElMessage.error(res.msg)
+          }
+        }
+        cancel()
+        getRoleDetailAdd()
+      }
+    } catch (error) {
+      console.error('表单验证失败:', error)
+    }
+  }
+
+  const getRoleDetailAdd = async () => {
+    try {
+      const res = await ServiceUser.groupSingleAdd({})
+      if (res.code == Code.StatusOK) {
+        const data = res.data.map((item) => {
+          return {
+            ...item,
+            name: t(item.name),
+            children:
+              item.children?.map((child) => ({
+                ...child,
+                name: t(child.name),
+                children: child.btns
+                  ? Object.values(child.btns).map((btn) => ({
+                      ...btn,
+                      name: t(btn.name),
+                    }))
+                  : [],
+              })) || [],
+          }
+        })
+        roleTreeDate.value = data
+        loading.value = false
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } catch (error) {
+      ElMessage.error(t('Msg.RequestError'))
+    }
+  }
+
+  const confirm = () => {
+    formRef.value.validate(async (valid) => {
+      if (valid) {
+        toConfirm()
+      } else {
+        return false
+      }
+    })
+  }
+  const toConfirm = () => {
+    addLoading.value = true
+    if (props.editor) {
+      roleUpdate()
+    } else {
+      roleAdd()
+    }
+  }
+  const roleAdd = async () => {
+    let res = await ServiceUser.roleListAdd({
+      data: data_check.value,
+      code: 'ROLE_USER',
+      ...form,
+    })
+    addLoading.value = false
+    if (res.code == Code.StatusOK) {
+      formRef.value.resetFields()
+      roleTreeDate.value = []
+      emit('confirmToReload')
+      emit('closeAdd', false)
+      ElMessage.success(t('Msg.Success'))
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+  const roleUpdate = async () => {
+    if (!data_check.value.length) {
+      getDataCheck()
+    }
+
+    let res = await ServiceUser.roleListUpdate({
+      data: data_check.value,
+      code: 'ROLE_USER',
+      ...form,
+    })
+    addLoading.value = false
+    if (res.code == Code.StatusOK) {
+      roleTreeDate.value = []
+      defaultSelect.value = []
+      formRef.value.resetFields()
+      emit('confirmToReload')
+      emit('closeAdd', false)
+      ElMessage.success(t('Msg.Success'))
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+  //提交成功后回调
+  const confirmToReload = () => {
+    emit('confirmToReload', true)
+  }
+
+  // watch for props changes
+  watch(
+    () => props.formList,
+    (newVal) => {
+      if (newVal) {
+        Object.assign(form, newVal)
+      }
+    },
+    { immediate: true }
+  )
+  watch(
+    () => props.dialogInfoTradingAdd,
+    async (newVal) => {
+      loading.value = true
+      if (newVal) {
+        if (props.editor) {
+          await getRoleDetailUpdate()
+        } else {
+          await getRoleDetailAdd()
+        }
+      }
+    }
+  )
+
+  // lifecycle
+  onMounted(async () => {
+    // await Promise.all([getDepartmentList(), getRoleDetailAdd()])
+    getDepartmentList()
+    console.log(props, 'props')
+
+    // 设置默认选中
+    if (props.editor && props.formList?.id) {
+      // 如果有编辑数据,设置默认选中项
+      // 这里需要根据实际逻辑设置 defaultSelect.value
+    }
+  })
+</script>
+
+<style scoped></style>

+ 7 - 0
src/views/user/userRole/const.ts

@@ -0,0 +1,7 @@
+export const departmentNames = {
+  1: 'User_info.Group.item1',
+  2: 'User_info.Group.item2',
+  3: 'User_info.Group.item3',
+  4: 'User_info.Group.item4',
+  5: 'User_info.Group.item5',
+}

+ 353 - 0
src/views/user/userRole/index.vue

@@ -0,0 +1,353 @@
+<template>
+  <div
+    id="user_Role"
+    v-loading="pictLoading"
+    class="view"
+    element-loading-background="rgba(43, 48, 67, 0.65)"
+    element-loading-spinner="el-icon-loading"
+  >
+    <div class="crm_search">
+      <el-form ref="formRef" label-position="" :model="search" label-width="">
+        <el-row>
+          <el-col :span="24" :md="24" :lg="24">
+            <el-form-item>
+              <el-date-picker
+                v-model="search.date"
+                class="crm_date_pick crm-border-radius-no"
+                type="daterange"
+                unlink-panels
+                value-format="yyyy-MM-dd"
+                range-separator="-"
+                :start-placeholder="$t('Placeholder.Start')"
+                :end-placeholder="$t('Placeholder.End')"
+              >
+              </el-date-picker>
+              <el-button
+                class="crm-border-radius-no crm-border-left-no"
+                :icon="Search"
+                @click="toSearch"
+              ></el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item v-if="display['R-Role-Add'].show">
+          <div class="search_action_btn">
+            <span class="crm-cursor" @click="addRole">
+              <el-icon><Plus /></el-icon>
+              <span>{{ $t('Btn.Add') }}</span>
+            </span>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+    <el-table :data="mock_tableData" stripe style="width: 100%">
+      <el-table-column prop="" align="left" :label="$t('Label.RoleName')">
+        <template #default="scope">
+          {{ scope.row.name || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Label.RoleGroup')">
+        <template #default="scope">
+          <span>{{
+            scope.row.departmentId ? $t(departmentNames[scope.row.departmentId]) : '--'
+          }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="left" :label="$t('Label.Date')">
+        <template #default="scope">
+          {{ scope.row.addTime || '--' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="" align="center" :label="$t('Label.Action')">
+        <template #default="scope">
+          <el-dropdown trigger="click" @command="handleCommand">
+            <span class="el-dropdown-link crm-cursor">
+              <i style="font-weight: bold; font-size: 20px" class="iconfont iconcaidan"></i>
+            </span>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item
+                  v-if="display['R-Role-Update'].show"
+                  :command="{ type: 'editor', row: scope.row }"
+                >
+                  <el-icon><Edit /></el-icon>
+                  <span>{{ $t('Btn.Editor') }}</span>
+                </el-dropdown-item>
+                <el-dropdown-item
+                  v-if="display['R-Role-Delete'].show"
+                  :command="{ type: 'delete', id: scope.row.id }"
+                >
+                  <el-icon><Delete /></el-icon>
+                  <span>{{ $t('Btn.Delete') }}</span>
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div v-if="pagerInfo.rowTotal" class="crm_pagination">
+      <div class="crm_page_total">
+        <span>{{ $t('Page.total.item1') }}</span>
+        <span>{{ pagerInfo.rowTotal }}</span>
+        <span>{{ $t('Page.total.item2') }}</span>
+      </div>
+      <el-pagination
+        class="page"
+        background
+        layout="sizes, prev, pager, next"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagerInfo.row"
+        :total="pagerInfo.rowTotal"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      >
+      </el-pagination>
+    </div>
+
+    <!-- 账户新增/修改弹出 -->
+    <RoleAdd
+      :dialog-info-trading-add="dialogInfoTradingAdd"
+      :editor="editor"
+      :form-list="formList"
+      @confirm-to-reload="confirmToReload"
+      @close-add="closeAdd"
+    >
+    </RoleAdd>
+    <div v-if="dialogInfoTradingAdd" class="crm_verified_info_mask" @click="closeDiaAdd"></div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, onMounted, inject } from 'vue'
+  import { Search, Edit, Delete, Plus } from '@element-plus/icons-vue'
+  import { ElMessage, ElMessageBox } from 'element-plus'
+  import Service from '@/service/user'
+  import Config from '@/config/index'
+  import RoleAdd from './components/RoleAdd'
+  import { departmentNames } from '@/views/user/userRole/const'
+  import { useI18n } from 'vue-i18n'
+
+  const { Code } = Config
+  const { t } = useI18n()
+  const Session = inject('session')
+
+  // ref 响应式数据
+  const pictLoading = ref(false)
+  const mock_tableData = ref([])
+  const dialogInfoTradingAdd = ref(false)
+  const editor = ref(null)
+  const addType = ref('')
+  const formRef = ref(null)
+
+  // reactive 响应式对象
+  const search = reactive({
+    date: [],
+    startDate: '',
+    endDate: '',
+  })
+
+  const formList = reactive({})
+
+  const pagerInfo = reactive({
+    row: 10,
+    current: 1,
+    pageTotal: 0,
+    rowTotal: 0,
+  })
+
+  // computed 计算属性
+  const display = computed(() => {
+    const displayData = Session.Get('display', true)
+    return displayData ? JSON.parse(displayData) : {}
+  })
+
+  // 方法
+  const handleCommand = (command) => {
+    if (command.type == 'editor') {
+      Object.assign(formList, {
+        id: command.row.id,
+        name: command.row.name,
+        departmentId: command.row.departmentId,
+      })
+      addType.value = 'user_Role'
+      dialogInfoTradingAdd.value = true
+      editor.value = 1
+    } else if (command.type == 'delete') {
+      deleteRole(command.id)
+    }
+  }
+
+  const closeDiaAdd = () => {
+    addType.value = ''
+    dialogInfoTradingAdd.value = false
+    editor.value = null
+  }
+
+  const closeAdd = (val) => {
+    addType.value = ''
+    editor.value = null
+    dialogInfoTradingAdd.value = val
+  }
+
+  const addRole = () => {
+    addType.value = 'user_Role'
+    dialogInfoTradingAdd.value = true
+    editor.value = null
+  }
+
+  const confirmToReload = () => {
+    addType.value = ''
+    dialogInfoTradingAdd.value = false
+    searchFunc()
+  }
+
+  const deleteRole = async (id) => {
+    try {
+      await ElMessageBox.confirm(t('Msg.Delete'), t('Msg.SystemPrompt'), {
+        confirmButtonText: t('Btn.Confirm'),
+        cancelButtonText: t('Btn.Cancel'),
+        type: 'warning',
+      })
+
+      const res = await Service.roleListDelete({ id })
+      if (res.code == Code.StatusOK) {
+        ElMessage.success(t('Msg.DeleteSuccess'))
+        searchFunc()
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } catch (error) {
+      // 用户取消删除
+      console.log('删除操作取消')
+    }
+  }
+
+  const searchFunc = async () => {
+    pictLoading.value = true
+
+    if (!display.value['R-Role-Search']?.show) {
+      ElMessage.warning(t('Msg.NotDisplay'))
+      pictLoading.value = false
+      return
+    }
+
+    // 处理日期
+    if (search.date == null || search.date.length == 0) {
+      search.startDate = ''
+      search.endDate = ''
+    } else {
+      search.startDate = search.date[0]
+      search.endDate = search.date[1]
+    }
+
+    try {
+      const res = await Service.roleList({
+        ...search,
+        page: {
+          current: pagerInfo.current,
+          row: pagerInfo.row,
+        },
+      })
+
+      if (res.code == Code.StatusOK) {
+        mock_tableData.value = res.data
+        if (res.page != null) {
+          pagerInfo.rowTotal = res.page.rowTotal
+          pagerInfo.pageTotal = res.page.pageTotal
+        } else {
+          pagerInfo.rowTotal = 0
+        }
+        ElMessage.success(t('Msg.SearchSuccess'))
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } catch (error) {
+      ElMessage.error(t('Msg.SearchError'))
+    } finally {
+      pictLoading.value = false
+    }
+  }
+
+  const toSearch = () => {
+    pagerInfo.current = 1
+    searchFunc()
+  }
+
+  const handleSizeChange = (val) => {
+    pagerInfo.row = val
+    searchFunc()
+  }
+
+  const handleCurrentChange = (val) => {
+    pagerInfo.current = val
+    searchFunc()
+  }
+
+  // 挂载生命周期
+  onMounted(() => {
+    searchFunc()
+  })
+</script>
+
+<style scoped lang="scss">
+  #user_Role {
+    .crm_search {
+      .search_action_btn {
+        .delete {
+          background-color: #a1a1a1;
+        }
+        .delete.active {
+          background-color: #368fec;
+        }
+      }
+      .chooseLang {
+        margin-top: 2px;
+        margin-right: 10px;
+        > span {
+          border: 1px solid #dcdfe6;
+          padding: 0 8px;
+          min-width: 100px;
+          display: inline-block;
+          height: 32px;
+          line-height: 32px;
+          box-sizing: border-box;
+          text-align: center;
+        }
+        span.active {
+          background-color: #368fec;
+          border-color: #368fec;
+          color: #ffffff;
+        }
+      }
+    }
+    .el-table .state {
+      display: inline-block;
+      min-width: 80px;
+      max-width: 150px;
+      box-sizing: border-box;
+      line-height: 1.5;
+      border-radius: 2px;
+      padding: 2px 10px;
+      color: #ffffff;
+    }
+    .crm_verified_info_mask_trading {
+      position: fixed;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: 100%;
+      background-color: rgba(43, 48, 67, 0.65);
+      z-index: 88;
+    }
+  }
+</style>
+<style lang="scss">
+  #user_Role {
+    .dialog_header_w {
+      .crm_search_down {
+        width: 400px;
+      }
+    }
+  }
+</style>