Przeglądaj źródła

feat:ib-仪表盘 pamm列表,申请账户,调整交易账户

ljc 2 miesięcy temu
rodzic
commit
88f10968a9

+ 21 - 0
pages.json

@@ -268,6 +268,27 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/ib/settingPammManager",
+      "style": {
+        "navigationBarTitleText": "",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/ib/openAccount",
+      "style": {
+        "navigationBarTitleText": "",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/ib/openPammManager",
+      "style": {
+        "navigationBarTitleText": "",
+        "navigationStyle": "custom"
+      }
+    },
     {
       "path": "pages/analytics/analystViews",
       "style": {

+ 4 - 0
pages/ib/agentList.vue

@@ -240,6 +240,10 @@
     dialogInfoTradingAdd.value = true
     dialogInfoTradingAddTcAgree.value = false
   }
+
+  onMounted(()=>{
+    // dialogInfoTradingAddTc.value = true
+  })
 </script>
 <style lang="scss" scoped>
   @import "@/uni.scss";

+ 311 - 3
pages/ib/index.vue

@@ -2,7 +2,7 @@
   <cwg-page-wrapper class="create-page" :isHeaderFixed="true" :bgColor="'#f8f9f9'">
     <cwg-header :title="t('Home.page_ib.item1')" />
     <uni-row class="demo-uni-row uni-row1">
-      <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+      <uni-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
         <view class="dashboard-container">
           <!-- 余额卡片 -->
           <view class="card balance-card">
@@ -102,6 +102,92 @@
                     </view>-->
         </view>
       </uni-col>
+      <uni-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
+        <view class="dashboard-container">
+          <view class="card mam-card">
+            <view class="card-header">
+              <view class="header-left">
+                <text class="header-title">{{ t('Ib.Index.MAMList') }}</text>
+              </view>
+              <view class="header-right" v-if="showAddMamAccount">
+                <cwg-dropdown :menu-list="addMamAccountMenus" @menuClick="handleAddMamAccountMenuClick">
+                  <button type="primary" class="add-mam-btn">
+                    <cwg-icon name="icon_add" :size="18" color="#fff" />
+                    <text class="btn-text">{{ t('Custom.Index.AddAccount') }}</text>
+                  </button>
+                </cwg-dropdown>
+              </view>
+            </view>
+
+            <cwg-tabel
+              ref="mamTableRef"
+              :columns="mamColumns"
+              :mobilePrimaryFields="mamMobilePrimaryFields"
+              :queryParams="mamSearch"
+              :api="mamListApi"
+              :show-operation="false"
+              :showPagination="true"
+            >
+              <template #mamAccount="{ row }">
+                <view v-if="row.type == 1 || row.type == 2">
+                  <text>{{ row.login || '-' }}</text>
+                </view>
+                <view v-else-if="row.type == 3">
+                  <view class="mam-line">
+                    <text>{{ t('Ib.PammManager.ownerId') }}:</text>
+                    <text>{{ row.ownerId || '--' }}</text>
+                  </view>
+                  <view class="mam-line">
+                    <text>{{ t('Ib.PammManager.accountId') }}:</text>
+                    <text>{{ row.accountId || '--' }}</text>
+                  </view>
+                  <view class="mam-line">
+                    <text>{{ t('Ib.PammManager.percent') }}:</text>
+                    <text>{{ (row.percent ?? '--') + '%' }}</text>
+                  </view>
+                </view>
+              </template>
+
+              <template #mamType="{ row }">
+                <text>{{ formatMamType(row.type) }}</text>
+              </template>
+
+              <template #loginType="{ row }">
+                <text>{{ formatAccountType(row.accountType) }}</text>
+              </template>
+
+              <template #leverage="{ row }">
+                <text>{{ row.leverage ? '1:' + row.leverage : '-' }}</text>
+              </template>
+
+              <template #balance="{ row }">
+                <text>{{ numberFormat(row.balance || 0) }}</text>
+              </template>
+
+              <template #equity="{ row }">
+                <text>{{ numberFormat(row.equity || 0) }}</text>
+              </template>
+
+              <template #operation="{ row }">
+                <view class="mam-ops">
+                  <view v-if="row.type == 1 || row.type == 2" class="mam-op" @click.stop="toSettings(row)">
+                    <text>{{ t('Ib.Index.Settings') }}</text>
+                  </view>
+                  <view v-if="row.type == 3" class="mam-op" @click.stop="toSettings(row)">
+                    <text>{{ t('Ib.PammManager.btn1') }}</text>
+                  </view>
+                  <view v-if="row.type == 3" class="mam-op" @click.stop="toDialogPercent(row)">
+                    <text>{{ t('Ib.PammManager.percent') }}</text>
+                  </view>
+                  <view class="mam-op" @click.stop="toDialogSubs(row)">
+                    <text>{{ t('blockchain.item1') }}</text>
+                  </view>
+                </view>
+              </template>
+            </cwg-tabel>
+          </view>
+        </view>
+      </uni-col>
     </uni-row>
 
     <!-- 二维码弹窗 -->
@@ -277,6 +363,34 @@
         </view>
       </view>
     </cwg-popup>
+    <!-- 调整收益分成弹窗 -->
+    <cwg-popup 
+      :visible="dialogPercent" 
+      :title="t('Ib.PammManager.percent')" 
+      @close="closeDialogPercent" 
+      @confirm="applyPercent"
+    >
+      <view class="dia-content custom-dialog-content">
+        <uni-forms :model="dialogPercentData" label-width="150" label-position="left">
+          <uni-forms-item :label="t('Ib.PammManager.ownerId') + ':'">
+            <text class="info-text">{{ dialogPercentData.oldOwnerId || "--" }}</text>
+          </uni-forms-item>
+          <uni-forms-item :label="t('Ib.PammManager.accountId') + ':'">
+            <text class="info-text">{{ dialogPercentData.oldAccountId || "--" }}</text>
+          </uni-forms-item>
+          <uni-forms-item :label="t('Ib.PammManager.percent') + ':'">
+            <text class="info-text">{{ dialogPercentData.oldPercent }}%</text>
+          </uni-forms-item>
+          <uni-forms-item :label="t('Ib.PammManager.percentNew') + ':'" name="percent" required>
+            <uni-easyinput 
+              v-model="dialogPercentData.percent" 
+              :placeholder="t('placeholder.input')" 
+            />
+          </uni-forms-item>
+        </uni-forms>
+      </view>
+    </cwg-popup>
+
   </cwg-page-wrapper>
 </template>
 
@@ -290,6 +404,7 @@
   import { useStorage } from '@/hooks/useStorage'
   import QrCode from '@/components/QrCode.vue'
   import { useFilters } from '@/composables/useFilters'
+  import { isAfterJuly28 } from '@/utils/dateUtils'
 
   const { t } = useI18n()
   const router = useRouter()
@@ -311,6 +426,15 @@
   const spreadList = ref([])
   const excludeShowLoginTypes = ref([])
   const pammManagerValid = ref()
+  const dialogPercent = ref(false)
+  const dialogPercentData = ref({
+    oldPercent: '',
+    mamListId: '',
+    oldOwnerId: '',
+    oldAccountId: '',
+    percent: ''
+  })
+  const isActionLoading = ref(false)
   const menuList = ref([
     { label: t('Custom.Index.Withdrawals'), type: 1 },
     { label: t('Custom.Index.Transfer'), type: 2 },
@@ -663,6 +787,149 @@
     }
   }
 
+  const mamTableRef = ref(null)
+  const mamSearch = ref({})
+  const mamListApi = (params) => ibApi.MamList(params)
+
+  const showAddMamAccount = computed(() => {
+    return !!(
+      pammManagerValid.value?.mamValid ||
+      pammManagerValid.value?.pammValid ||
+      pammManagerValid.value?.pammManagerValid
+    )
+  })
+
+  const addMamAccountMenus = computed(() => {
+    const list = []
+    if (pammManagerValid.value?.mamValid) list.push({ label: 'MAM', type: 1 })
+    if (pammManagerValid.value?.pammValid) list.push({ label: 'Money Manager', type: 2 })
+    if (pammManagerValid.value?.pammManagerValid) list.push({ label: t('Ib.PammManager.title'), type: 3 })
+    return list
+  })
+
+  const handleAddMamAccountMenuClick = ({ value }) => {
+    toNewAccount(value.type)
+  }
+
+  const formatMamType = (type) => {
+    if (type == 1) return 'MAM'
+    if (type == 2) return 'Money Manager'
+    if (type == 3) return t('Ib.PammManager.title1')
+    return '--'
+  }
+
+  const formatAccountType = (accountType) => {
+    if (accountType == 1) return t('AccountType.ClassicAccount')
+    if (accountType == 2) return t('AccountType.SeniorAccount')
+    if (accountType == 3 && !isAfterJuly28()) return t('AccountType.AgencyAccount')
+    if (accountType == 5) return t('AccountType.SpeedAccount')
+    if (accountType == 6) return t('AccountType.SpeedAccount')
+    if (accountType == 7) return t('AccountType.StandardAccount')
+    if (accountType == 8) return t('AccountType.CentAccount')
+    return '--'
+  }
+
+  const mamColumns = computed(() => [
+    { prop: 'mamAccount', label: t('Ib.Index.MAMAccount'), align: 'center', slot: 'mamAccount' },
+    { prop: 'type', label: t('Label.Type'), align: 'center', slot: 'mamType' },
+    { prop: 'accountType', label: t('Ib.Index.LoginType'), align: 'center', slot: 'loginType' },
+    { prop: 'platform', label: t('Ib.Index.Platform'), align: 'center' },
+    { prop: 'currency', label: t('Ib.Index.Currency'), align: 'center' },
+    { prop: 'leverage', label: t('Ib.Index.Leverage'), align: 'center', slot: 'leverage' },
+    { prop: 'balance', label: t('Ib.Index.Balance'), align: 'center', slot: 'balance' },
+    { prop: 'equity', label: t('Ib.Index.Equity'), align: 'center', slot: 'equity' },
+    { prop: 'commission', label: t('Ib.Index.Commission'), align: 'center' },
+    { prop: 'operation', label: t('Ib.Index.Operation'), align: 'center', slot: 'operation' },
+  ])
+
+  const mamMobilePrimaryFields = computed(() => [
+    { prop: 'mamAccount', label: t('Ib.Index.MAMAccount'), align: 'center', slot: 'mamAccount' },
+    { prop: 'type', label: t('Label.Type'), align: 'center', slot: 'mamType' },
+    { prop: 'platform', label: t('Ib.Index.Platform'), align: 'center' },
+    { prop: 'more', type: 'more', width: 20, align: 'right' },
+  ])
+
+  const toNewAccount = (type) => {
+    if (type == 3) {
+      router.push({
+        path: '/pages/ib/openPammManager',
+        query: {
+          type: type,
+        },
+      })
+    } else {
+      router.push({
+        path: '/pages/ib/openAccount',
+        query: {
+          type: type,
+        },
+      })
+    }
+  }
+
+  const toSettings = (row) => {
+    router.push({
+      path: '/pages/ib/settingPammManager',
+      query: { login: row.accountId, id: row.id },
+    })
+  }
+
+  const toDialogPercent = (row) => {
+    dialogPercentData.value.oldPercent = row.percent
+    dialogPercentData.value.mamListId = row.id
+    dialogPercentData.value.oldOwnerId = row.ownerId
+    dialogPercentData.value.oldAccountId = row.accountId
+    dialogPercentData.value.percent = ''
+    dialogPercent.value = true
+  }
+
+  const closeDialogPercent = () => {
+    dialogPercent.value = false
+  }
+
+  const applyPercent = async () => {
+    if (isActionLoading.value) return
+    
+    if (!dialogPercentData.value.percent) {
+      uni.showToast({ title: t('placeholder.input'), icon: 'none' })
+      return
+    }
+    
+    isActionLoading.value = true
+    try {
+      const res = await ibApi.applyPercent({
+        mamListId: dialogPercentData.value.mamListId,
+        percent: dialogPercentData.value.percent,
+      })
+      if (res.code === Code.StatusOK) {
+        uni.showToast({ title: res.msg, icon: 'success' })
+        dialogPercent.value = false
+        // Refresh MAM list
+        mamTableRef.value?.refreshTable()
+      } else {
+        uni.showToast({ title: res.msg, icon: 'none' })
+      }
+    } catch (error) {
+      uni.showToast({ title: t('Msg.Fail'), icon: 'none' })
+    } finally {
+      isActionLoading.value = false
+    }
+  }
+
+  const toDialogSubs = (row) => {
+    if (row?.type == 3) {
+      router.push({
+        path: '/pages/ib/transfer',
+        query: { tab: 'pammSubs', id: row.id },
+      })
+      return
+    }
+    router.push({
+      path: '/pages/ib/transfer',
+      query: { tab: 'mamSubs', id: row.id },
+    })
+  }
+
   onMounted(() => {
     // 初始化数据
     getIbData()
@@ -675,7 +942,7 @@
   @import "@/uni.scss";
 
   .dashboard-container {
-    min-height: 100vh;
+    min-height: 10vh;
     box-sizing: border-box;
   }
 
@@ -689,6 +956,15 @@
     box-shadow: 0 px2rpx(4) px2rpx(12) rgba(0, 0, 0, 0.2);
   }
 
+  .custom-dialog-content {
+    padding: px2rpx(20);
+    
+    .info-text {
+      color: #333;
+      font-size: px2rpx(14);
+      line-height: px2rpx(36);
+    }
+  }
   .card-header {
     display: flex;
     justify-content: space-between;
@@ -871,4 +1147,36 @@
     align-items: center;
     gap: 16rpx;
   }
-</style>
+
+  .mam-card {
+    .add-mam-btn {
+      display: flex;
+      align-items: center;
+      gap: 8rpx;
+      height: px2rpx(32);
+      line-height: px2rpx(32);
+      padding: 0 px2rpx(12);
+      margin: 0;
+    }
+  }
+
+  .mam-line {
+    display: flex;
+    justify-content: center;
+    gap: 6rpx;
+    line-height: 1.5;
+  }
+
+  .mam-ops {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: center;
+    gap: 10rpx;
+  }
+
+  .mam-op {
+    color: #2b5aed;
+    font-size: px2rpx(12);
+    line-height: 1.4;
+  }
+</style>

+ 400 - 0
pages/ib/openAccount.vue

@@ -0,0 +1,400 @@
+<template>
+  <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
+    <cwg-header :title="params.type === 1 ? t('Ib.NewAccount.TitleM') : t('Ib.NewAccount.TitleP')">
+      <template #right>
+        <view class="header-btn" @click="backIndex">
+          <cwg-icon name="icon_back" :size="18" />
+          <text>{{ t('Ib.Settings.Title') }}</text>
+        </view>
+      </template>
+    </cwg-header>
+
+    <view class="main-content account-content" v-if="showPage">
+      <view class="box" v-if="isAllLoginTypesHidden">
+        <view style="margin: 20px 0; font-size: 14px; line-height: 1.7;">
+          <text>{{ t('news_add_field.OpenAccount.Des3') }}</text>
+        </view>
+      </view>
+      <view class="box" v-else>
+        <view class="tit">
+          <cwg-icon name="icon_arrow_right" :size="16" />
+          <text>{{ t('Ib.NewAccount.Title1') }}</text>
+        </view>
+        <view class="des">{{ t('Ib.NewAccount.Des1') }}</view>
+        
+        <view class="b-card">
+          <uni-forms ref="formRef" :model="params" :rules="rules" label-position="top">
+            <uni-row :gutter="20">
+              <uni-col :xs="24" :sm="12" :md="8">
+                <uni-forms-item :label="t('Ib.NewAccount.Platform')" name="platform" required>
+                  <cwg-combox 
+                    v-model:value="params.platform" 
+                    :options="[{ text: 'MT4', value: 'MT4' }]" 
+                    :placeholder="t('placeholder.choose')" 
+                  />
+                </uni-forms-item>
+              </uni-col>
+              <uni-col :xs="24" :sm="12" :md="8">
+                <uni-forms-item :label="t('Ib.NewAccount.LoginType')" name="accountType" required>
+                  <cwg-combox 
+                    v-model:value="params.accountType" 
+                    :options="accountTypeOptions" 
+                    :placeholder="t('placeholder.choose')" 
+                    @change="onAccountTypeChange"
+                  />
+                </uni-forms-item>
+              </uni-col>
+              <uni-col :xs="24" :sm="12" :md="8">
+                <uni-forms-item :label="t('Ib.NewAccount.Lever')" name="leverage" required>
+                  <cwg-combox 
+                    v-model:value="params.leverage" 
+                    :options="formatOptions(optionsLev)" 
+                    :placeholder="t('placeholder.choose')" 
+                  />
+                </uni-forms-item>
+              </uni-col>
+              <uni-col :xs="24" :sm="12" :md="8">
+                <uni-forms-item :label="t('Ib.NewAccount.Currency')" name="currency" required>
+                  <cwg-combox 
+                    v-model:value="params.currency" 
+                    :options="formatOptions(optionsCur)" 
+                    :placeholder="t('placeholder.choose')" 
+                  />
+                </uni-forms-item>
+              </uni-col>
+              <uni-col :xs="24" :sm="12" :md="8">
+                <uni-forms-item :label="t('Ib.NewAccount.Commission')" name="commission" required>
+                  <cwg-combox 
+                    v-model:value="params.commission" 
+                    :options="formatOptions(optionsCom)" 
+                    :placeholder="t('placeholder.choose')" 
+                  />
+                </uni-forms-item>
+              </uni-col>
+              <uni-col :span="24">
+                <button class="s-btn" type="primary" @click="newAccount">{{ t('Ib.NewAccount.Btn') }}</button>
+              </uni-col>
+            </uni-row>
+          </uni-forms>
+        </view>
+      </view>
+
+      <view class="box">
+        <view style="margin: 20px 0; font-size: 14px; line-height: 1.7;">
+          <template v-if="params.type === 1">
+            <view style="font-weight: bold; margin-bottom: 5px;">{{ t('Ib.NewAccount.Dec1') }}</view>
+            <view>{{ t('Ib.NewAccount.Dec2') }}</view>
+          </template>
+          <template v-if="params.type === 2">
+            <view style="font-weight: bold; margin-bottom: 5px;">{{ t('Ib.NewAccount.Dec3') }}</view>
+            <view>{{ t('Ib.NewAccount.Dec4') }}</view>
+          </template>
+        </view>
+      </view>
+
+      <view class="box" v-if="params.accountType === 6">
+        <view style="margin: 20px 0; font-size: 14px; line-height: 1.7;">
+          <view>
+            <text>{{ t('Ib.NewAccount.Dec5') }}</text>
+            <text 
+              class="crm-cursor" 
+              style="color: #368FEC;" 
+              @click="openLeverageMargin"
+            >{{ t('Ib.NewAccount.Dec6') }}</text>
+            <text>{{ t('Ib.NewAccount.Dec7') }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 提交结果弹窗 -->
+    <cwg-popup :visible="dialogCheck" :showClose="false" :showFooters="false">
+      <view class="result-dialog">
+        <view v-if="dialogVisible" class="icon-wrap">
+          <cwg-icon name="icon_success" :size="50" color="#67C23A" />
+          <view class="result-text">{{ t('ApplicationDialog.Des1') }}</view>
+          <view class="result-sub-text">{{ t('ApplicationDialog.Des12') }}</view>
+        </view>
+        <view v-else class="icon-wrap">
+          <cwg-icon name="icon_warning" :size="50" color="#E6A23C" />
+          <view class="result-text">{{ RES }}</view>
+        </view>
+        <view class="dialog-footer">
+          <button type="primary" @click="closeDia">{{ t('Btn.Confirm') }}</button>
+          <button @click="closeDia">{{ t('Btn.Cancel') }}</button>
+        </view>
+      </view>
+    </cwg-popup>
+
+  </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { useI18n } from 'vue-i18n'
+import { ibApi } from '@/service/ib'
+import { isAfterJuly28 } from '@/utils/dateUtils'
+import Config from '@/config/index'
+import { lang } from '@/composables/config'
+
+const { t } = useI18n()
+const { Code } = Config
+
+const pictLoading = ref(false)
+const showPage = ref(false)
+const showLogin = ref<string[]>([])
+const showData = ref<any[]>([])
+
+const flag = ref(false)
+const RES = ref('')
+const dialogCheck = ref(false)
+const dialogVisible = ref(false)
+
+const optionsLev = ref<string[]>([])
+const optionsCur = ref<string[]>([])
+const optionsCom = ref<string[]>([])
+
+const formRef = ref()
+
+const params = reactive({
+  type: 1,
+  currency: '',
+  commission: '',
+  leverage: '',
+  accountType: '' as string | number,
+  platform: '',
+})
+
+const rules = {
+  currency: { rules: [{ required: true, errorMessage: t('vaildate.select.empty') }] },
+  commission: { rules: [{ required: true, errorMessage: t('vaildate.select.empty') }] },
+  leverage: { rules: [{ required: true, errorMessage: t('vaildate.select.empty') }] },
+  accountType: { rules: [{ required: true, errorMessage: t('vaildate.select.empty') }] },
+  platform: { rules: [{ required: true, errorMessage: t('vaildate.select.empty') }] },
+}
+
+// 是否全部类型被隐藏
+const isAllLoginTypesHidden = computed(() => {
+  const hiddenStr = showLogin.value.join(',')
+  return hiddenStr.includes('1') && hiddenStr.includes('2') && hiddenStr.includes('3') && 
+         hiddenStr.includes('6') && hiddenStr.includes('7') && hiddenStr.includes('8')
+})
+
+// 动态计算下拉类型选项
+const accountTypeOptions = computed(() => {
+  const list = []
+  if (!showLogin.value.includes('1')) list.push({ value: 1, text: t('AccountType.ClassicAccount') })
+  if (!showLogin.value.includes('2')) list.push({ value: 2, text: t('AccountType.SeniorAccount') })
+  if (!showLogin.value.includes('3') && !isAfterJuly28()) list.push({ value: 3, text: t('AccountType.AgencyAccount') })
+  if (!showLogin.value.includes('6')) list.push({ value: 6, text: t('AccountType.SpeedAccount') })
+  if (!showLogin.value.includes('7')) list.push({ value: 7, text: t('AccountType.StandardAccount') })
+  if (!showLogin.value.includes('8')) list.push({ value: 8, text: t('AccountType.CentAccount') })
+  return list
+})
+
+// 格式化普通数组为 combox 所需的 [{text, value}] 格式
+const formatOptions = (arr: string[]) => {
+  return (arr || []).map(item => ({ text: String(item), value: item }))
+}
+
+// 监听账户类型切换以回填下拉框
+const onAccountTypeChange = (type: number) => {
+  params.leverage = ''
+  params.currency = ''
+  params.commission = ''
+  
+  const target = showData.value.find(item => item.type == type)
+  if (target) {
+    optionsLev.value = target.leverage || []
+    optionsCur.value = target.currency || []
+    optionsCom.value = target.commission || []
+  }
+}
+
+// 获取必填配置项
+const getMustData = async () => {
+  const res = await ibApi.MamApplyData({})
+  if (res.code === Code.StatusOK) {
+    showData.value = res.data || []
+  } else {
+    uni.showToast({ title: res.msg, icon: 'none' })
+  }
+}
+
+// 获取排除类型
+const getExcludeShowLogin = async () => {
+  pictLoading.value = true
+  try {
+    const res = await ibApi.excludeShowLogin({})
+    if (res.code === Code.StatusOK) {
+      showLogin.value = res.data?.excludeShowLoginTypes || []
+      showPage.value = true
+    } else {
+      uni.showToast({ title: res.msg, icon: 'none' })
+    }
+  } finally {
+    pictLoading.value = false
+  }
+}
+
+// 提交申请
+const newAccount = async () => {
+  try {
+    await formRef.value?.validate()
+  } catch (error) {
+    return
+  }
+
+  if (flag.value) return
+  flag.value = true
+
+  try {
+    const res = await ibApi.MamApplyAdd({ ...params })
+    if (res.code === Code.StatusOK) {
+      dialogCheck.value = true
+      dialogVisible.value = true
+    } else {
+      RES.value = res.msg
+      dialogCheck.value = true
+      dialogVisible.value = false
+    }
+  } catch (err) {
+    RES.value = t('Msg.Fail')
+    dialogCheck.value = true
+    dialogVisible.value = false
+  } finally {
+    flag.value = false
+  }
+}
+
+// 关闭弹窗
+const closeDia = () => {
+  formRef.value?.clearValidate()
+  // 也可以选择重置表单: Object.assign(params, { currency: '', commission: '', leverage: '', accountType: '', platform: '' })
+  dialogCheck.value = false
+  dialogVisible.value = false
+}
+
+const backIndex = () => {
+  uni.navigateBack({ delta: 1 })
+}
+
+const openLeverageMargin = () => {
+  const url = ['cn', 'zhHant'].includes(lang.value) 
+    ? 'https://www.cwgvu.com/cn/leveragemargin' 
+    : 'https://www.cwgvu.com/en/leveragemargin'
+  // #ifdef H5
+  window.open(url, '_blank')
+  // #endif
+  // #ifndef H5
+  uni.showToast({ title: t('Msg.NotSupportedInApp'), icon: 'none' })
+  // #endif
+}
+
+onLoad((options: any) => {
+  if (options && options.type) {
+    params.type = Number(options.type)
+  } else {
+    params.type = 1
+  }
+  getExcludeShowLogin()
+  getMustData()
+})
+</script>
+
+<style lang="scss" scoped>
+@import "@/uni.scss";
+
+.header-btn {
+  display: flex;
+  align-items: center;
+  font-size: px2rpx(14);
+  color: #333;
+  cursor: pointer;
+  
+  text {
+    margin-left: px2rpx(5);
+  }
+}
+
+.main-content {
+  padding: px2rpx(10);
+  min-height: calc(100vh - 100px);
+}
+
+.box {
+  padding: px2rpx(15) px2rpx(20);
+  background-color: #fff;
+  border-radius: px2rpx(4);
+  margin-bottom: px2rpx(20);
+  
+  .tit {
+    display: flex;
+    align-items: center;
+    font-size: px2rpx(16);
+    font-weight: bold;
+    margin-bottom: px2rpx(10);
+    
+    text {
+      margin-left: px2rpx(5);
+    }
+  }
+  
+  .des {
+    font-size: px2rpx(14);
+    font-weight: bold;
+    line-height: 1.5;
+    margin-bottom: px2rpx(20);
+    color: #333;
+  }
+}
+
+.b-card {
+  margin-top: px2rpx(15);
+}
+
+.s-btn {
+  margin-top: px2rpx(10);
+  min-width: px2rpx(120);
+}
+
+.crm-cursor {
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.result-dialog {
+  text-align: center;
+  padding: px2rpx(20);
+  
+  .icon-wrap {
+    margin-bottom: px2rpx(20);
+  }
+  
+  .result-text {
+    font-size: px2rpx(16);
+    font-weight: bold;
+    margin-top: px2rpx(20);
+    margin-bottom: px2rpx(10);
+  }
+  
+  .result-sub-text {
+    font-size: px2rpx(12);
+    color: #666;
+    margin-bottom: px2rpx(20);
+  }
+  
+  .dialog-footer {
+    display: flex;
+    justify-content: center;
+    gap: px2rpx(15);
+    margin-top: px2rpx(30);
+    
+    button {
+      min-width: px2rpx(100);
+      margin: 0;
+    }
+  }
+}
+</style>

+ 318 - 0
pages/ib/openPammManager.vue

@@ -0,0 +1,318 @@
+<template>
+  <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
+    <cwg-header :title="t('Ib.PammManager.title')">
+      <template #right>
+        <view class="header-btn" @click="backIndex">
+          <cwg-icon name="icon_back" :size="18" />
+          <text>{{ t('Ib.Settings.Title') }}</text>
+        </view>
+      </template>
+    </cwg-header>
+
+    <view class="main-content account-content">
+      <view class="box">
+        <view class="tit"></view>
+        <view class="des">{{ t('Ib.PammManager.tips4') }}</view>
+        
+        <view class="b-card">
+          <uni-forms ref="formRef" label-width="120" :model="params" :rules="rules" label-position="top">
+            <uni-row :gutter="20">
+              <uni-col :xs="24" :sm="12">
+                <uni-forms-item :label="t('Ib.PammManager.accountId')" name="accountId" required>
+                  <cwg-combox 
+                    v-model:value="params.accountId" 
+                    :options="accountIdOptions" 
+                    :placeholder="t('placeholder.choose')" 
+                  />
+                </uni-forms-item>
+              </uni-col>
+              <uni-col :xs="24" :sm="12">
+                <uni-forms-item :label="t('Ib.PammManager.ownerId')" name="ownerId" required>
+                  <cwg-combox 
+                    v-model:value="params.ownerId" 
+                    :options="ownerIdOptions" 
+                    :placeholder="t('placeholder.choose')" 
+                  />
+                </uni-forms-item>
+              </uni-col>
+              <uni-col :xs="24" :sm="12">
+                <uni-forms-item :label="t('Ib.PammManager.percent')" name="percent" required>
+                  <uni-easyinput 
+                    v-model.trim="params.percent" 
+                    :placeholder="t('placeholder.input')" 
+                  />
+                </uni-forms-item>
+              </uni-col>
+              <uni-col :span="24">
+                <button class="s-btn" type="primary" @click="newAccount">{{ t('Ib.PammManager.btn') }}</button>
+              </uni-col>
+            </uni-row>
+          </uni-forms>
+        </view>
+      </view>
+
+      <view class="box">
+        <view style="margin: 20px 0; font-size: 14px; line-height: 1.7;">
+          <view style="font-weight: bold; margin-bottom: 5px;">{{ t('Ib.PammManager.tips2') }}</view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 提交结果弹窗 -->
+    <cwg-popup :visible="dialogCheck" :showClose="false" :showFooters="false">
+      <view class="result-dialog">
+        <view v-if="dialogVisible" class="icon-wrap">
+          <cwg-icon name="icon_success" :size="50" color="#67C23A" />
+          <view class="result-text">{{ t('ApplicationDialog.Des1') }}</view>
+          <view class="result-sub-text">{{ t('ApplicationDialog.Des12') }}</view>
+        </view>
+        <view v-else class="icon-wrap">
+          <cwg-icon name="icon_warning" :size="50" color="#E6A23C" />
+          <view class="result-text">{{ t('ApplicationDialog.Des43') }}</view>
+          <view class="result-text" style="color: #e6a23c; margin: 10px 0;">{{ RES }}</view>
+          <view class="result-sub-text">{{ t('ApplicationDialog.Des45') }}</view>
+        </view>
+        <view class="dialog-footer">
+          <button type="primary" @click="dialogVisible ? backIndex() : closeDia()">{{ t('Btn.Confirm') }}</button>
+          <button @click="dialogVisible ? backIndex() : closeDia()">{{ t('Btn.Cancel') }}</button>
+        </view>
+      </view>
+    </cwg-popup>
+
+  </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { useI18n } from 'vue-i18n'
+import { ibApi } from '@/service/ib'
+import Config from '@/config/index'
+
+const { t } = useI18n()
+const { Code } = Config
+
+const pictLoading = ref(false)
+const showData = ref<any[]>([])
+
+const flag = ref(false)
+const RES = ref('')
+const dialogCheck = ref(false)
+const dialogVisible = ref(false)
+
+const formRef = ref()
+
+const params = reactive({
+  type: null as number | null,
+  ownerId: null as string | null,
+  accountId: null as string | null,
+  percent: '',
+})
+
+// 验证规则
+const rules = {
+  ownerId: { rules: [{ required: true, errorMessage: t('vaildate.select.empty') }] },
+  accountId: { rules: [{ required: true, errorMessage: t('vaildate.select.empty') }] },
+  percent: {
+    rules: [
+      { required: true, errorMessage: t('vaildate.input.empty') },
+      {
+        validateFunction: (rule: any, value: any, data: any, callback: any) => {
+          if (!value && value !== 0) {
+            callback(t('vaildate.input.empty'))
+          } else if (!/^[0-9]+([.]{1}[0-9]{1,2})?$/.test(String(value))) {
+            callback('0.00-100.00')
+          } else {
+            return true
+          }
+        }
+      }
+    ]
+  },
+}
+
+// 动态计算下拉选项 (处理 disabled 逻辑)
+const accountIdOptions = computed(() => {
+  return showData.value.map(item => {
+    // disabled="(item.login == params.ownerId) || item.equity!=0"
+    const isDisabled = (item.login === params.ownerId) || (Number(item.equity) !== 0)
+    return {
+      text: item.login,
+      value: item.login,
+      disable: isDisabled
+    }
+  })
+})
+
+const ownerIdOptions = computed(() => {
+  return showData.value.map(item => {
+    // disabled="item.login == params.accountId"
+    const isDisabled = item.login === params.accountId
+    return {
+      text: item.login,
+      value: item.login,
+      disable: isDisabled
+    }
+  })
+})
+
+// 获取账号集合
+const getMustData = async () => {
+  pictLoading.value = true
+  try {
+    const res = await ibApi.mamApplyPammManagerLogins({})
+    if (res.code === Code.StatusOK) {
+      showData.value = res.data || []
+    } else {
+      uni.showToast({ title: res.msg, icon: 'none' })
+    }
+  } finally {
+    pictLoading.value = false
+  }
+}
+
+// 提交申请
+const newAccount = async () => {
+  try {
+    await formRef.value?.validate()
+  } catch (error) {
+    return
+  }
+
+  if (flag.value) return
+  flag.value = true
+
+  try {
+    const res = await ibApi.mamApplyPammManagerAdd({ ...params })
+    if (res.code === Code.StatusOK) {
+      dialogCheck.value = true
+      dialogVisible.value = true
+    } else {
+      RES.value = res.msg
+      dialogCheck.value = true
+      dialogVisible.value = false
+    }
+  } catch (err) {
+    RES.value = t('Msg.Fail')
+    dialogCheck.value = true
+    dialogVisible.value = false
+  } finally {
+    flag.value = false
+  }
+}
+
+// 关闭弹窗
+const closeDia = () => {
+  formRef.value?.clearValidate()
+  dialogCheck.value = false
+  dialogVisible.value = false
+}
+
+// 成功后返回
+const backIndex = () => {
+  formRef.value?.clearValidate()
+  dialogCheck.value = false
+  dialogVisible.value = false
+  uni.navigateBack({ delta: 1 })
+}
+
+onLoad((options: any) => {
+  if (options && options.type) {
+    params.type = Number(options.type)
+  } else {
+    params.type = 1
+  }
+  getMustData()
+})
+</script>
+
+<style lang="scss" scoped>
+@import "@/uni.scss";
+
+.header-btn {
+  display: flex;
+  align-items: center;
+  font-size: px2rpx(14);
+  color: #333;
+  cursor: pointer;
+  
+  text {
+    margin-left: px2rpx(5);
+  }
+}
+
+.main-content {
+  padding: px2rpx(10);
+  min-height: calc(100vh - 100px);
+}
+
+.box {
+  padding: px2rpx(15) px2rpx(20);
+  background-color: #fff;
+  border-radius: px2rpx(4);
+  margin-bottom: px2rpx(20);
+  
+  .tit {
+    display: flex;
+    align-items: center;
+    font-size: px2rpx(16);
+    font-weight: bold;
+    margin-bottom: px2rpx(10);
+    
+    text {
+      margin-left: px2rpx(5);
+    }
+  }
+  
+  .des {
+    font-size: px2rpx(14);
+    font-weight: bold;
+    line-height: 1.5;
+    margin-bottom: px2rpx(20);
+    color: #333;
+  }
+}
+
+.b-card {
+  margin-top: px2rpx(15);
+}
+
+.s-btn {
+  margin-top: px2rpx(10);
+  min-width: px2rpx(120);
+}
+
+.result-dialog {
+  text-align: center;
+  padding: px2rpx(20);
+  
+  .icon-wrap {
+    margin-bottom: px2rpx(20);
+  }
+  
+  .result-text {
+    font-size: px2rpx(16);
+    font-weight: bold;
+    margin-top: px2rpx(20);
+    margin-bottom: px2rpx(10);
+  }
+  
+  .result-sub-text {
+    font-size: px2rpx(12);
+    color: #666;
+    margin-bottom: px2rpx(20);
+  }
+  
+  .dialog-footer {
+    display: flex;
+    justify-content: center;
+    gap: px2rpx(15);
+    margin-top: px2rpx(30);
+    
+    button {
+      min-width: px2rpx(100);
+      margin: 0;
+    }
+  }
+}
+</style>

+ 462 - 0
pages/ib/settingPammManager.vue

@@ -0,0 +1,462 @@
+<template>
+  <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
+    <cwg-header :title="t('Ib.PammManager.title1') + ' - ' + row.login">
+      <template #right>
+        <view class="header-btn" @click="backIndex">
+          <cwg-icon name="icon_back" :size="18" />
+          <text>{{ t('Ib.Settings.Title') }}</text>
+        </view>
+      </template>
+    </cwg-header>
+
+    <view class="main-content account-content">
+      <view class="tips-text">{{ t('Ib.PammManager.tips3') }}</view>
+      
+      <view class="tab-group">
+        <view class="tab-item" :class="{ active: subs === '1' }" @click="tableSearch('1')">
+          {{ t('Ib.Settings.Hang') }}
+        </view>
+        <view class="tab-item" :class="{ active: subs === '2' }" @click="tableSearch('2')">
+          {{ t('Ib.Settings.Undo') }}
+        </view>
+      </view>
+
+      <!-- 穿梭框 (模拟 el-transfer) -->
+      <view class="transfer-container">
+        <!-- 左侧面板 (未选择) -->
+        <view class="transfer-panel">
+          <view class="transfer-header">
+            <checkbox :checked="isAllLeftChecked" @click="toggleAllLeft" style="transform:scale(0.7)"/>
+            <text>{{ subs === '1' ? t('Ib.Settings.NotHang') : t('Ib.Settings.NotUndo') }}</text>
+            <text class="count">{{ leftListData.length }}</text>
+          </view>
+          <view class="transfer-search">
+            <uni-easyinput prefixIcon="search" v-model="searchLeft" :placeholder="t('placeholder.input')" />
+          </view>
+          <scroll-view scroll-y class="transfer-list">
+            <checkbox-group @change="onLeftChange">
+              <label class="transfer-item" v-for="item in filteredLeftList" :key="item.key">
+                <checkbox :value="item.key" :checked="leftChecked.includes(item.key)" style="transform:scale(0.7)"/>
+                <text>{{ item.label }}</text>
+              </label>
+            </checkbox-group>
+            <view v-if="filteredLeftList.length === 0" class="empty-text">{{ t('Documentary.tradingCenter.item143') }}</view>
+          </scroll-view>
+        </view>
+
+        <!-- 中间操作按钮 -->
+        <view class="transfer-actions">
+          <button class="action-btn right" :disabled="leftChecked.length === 0" @click="moveToRight">
+            <text>></text>
+          </button>
+          <button class="action-btn left" :disabled="rightChecked.length === 0" @click="moveToLeft">
+            <text><</text>
+          </button>
+        </view>
+
+        <!-- 右侧面板 (已选择) -->
+        <view class="transfer-panel">
+          <view class="transfer-header">
+            <checkbox :checked="isAllRightChecked" @click="toggleAllRight" style="transform:scale(0.7)"/>
+            <text>{{ subs === '1' ? t('Ib.Settings.HaveHang') : t('Ib.Settings.HaveUndo') }}</text>
+            <text class="count">{{ rightListData.length }}</text>
+          </view>
+          <view class="transfer-search">
+            <uni-easyinput prefixIcon="search" v-model="searchRight" :placeholder="t('placeholder.input')" />
+          </view>
+          <scroll-view scroll-y class="transfer-list">
+            <checkbox-group @change="onRightChange">
+              <label class="transfer-item" v-for="item in filteredRightList" :key="item.key">
+                <checkbox :value="item.key" :checked="rightChecked.includes(item.key)" style="transform:scale(0.7)"/>
+                <text>{{ item.label }}</text>
+              </label>
+            </checkbox-group>
+            <view v-if="filteredRightList.length === 0" class="empty-text">{{ t('Documentary.tradingCenter.item143') }}</view>
+          </scroll-view>
+        </view>
+      </view>
+
+      <view class="submit-bar">
+        <button type="primary" class="submit-btn" @click="applySubmit">{{ t('Btn.Confirm') }}</button>
+      </view>
+    </view>
+
+    <!-- 提交结果弹窗 -->
+    <cwg-popup :visible="dialogCheck" :showClose="false" :showFooters="false">
+      <view class="result-dialog">
+        <view v-if="dialogVisible" class="icon-wrap">
+          <cwg-icon name="icon_success" :size="50" color="#67C23A" />
+          <view class="result-text">{{ t('ApplicationDialog.Des1') }}</view>
+        </view>
+        <view v-else class="icon-wrap">
+          <cwg-icon name="icon_warning" :size="50" color="#E6A23C" />
+          <view class="result-text">{{ RES }}</view>
+        </view>
+        <view class="dialog-footer">
+          <button type="primary" @click="closeDia">{{ t('Btn.Confirm') }}</button>
+          <button @click="closeDia">{{ t('Btn.Cancel') }}</button>
+        </view>
+      </view>
+    </cwg-popup>
+  </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { useI18n } from 'vue-i18n'
+import { ibApi } from '@/service/ib'
+import Config from '@/config/index'
+
+const { t } = useI18n()
+const { Code } = Config
+
+const row = ref({ login: '', mamId: '' })
+const subs = ref('1')
+
+const transData1 = ref<any[]>([]) // 挂入的数据源
+const transValue1 = ref<string[]>([]) // 挂入的选中项
+
+const transData = ref<any[]>([]) // 撤销的数据源
+const transValue = ref<string[]>([]) // 撤销的选中项
+
+const flag = ref(false)
+const RES = ref('')
+const dialogCheck = ref(false)
+const dialogVisible = ref(false)
+
+// 穿梭框搜索和选择状态
+const searchLeft = ref('')
+const searchRight = ref('')
+const leftChecked = ref<string[]>([])
+const rightChecked = ref<string[]>([])
+
+// 根据 subs (挂入/撤销) 动态绑定当前的数据源
+const currentData = computed(() => subs.value === '1' ? transData1.value : transData.value)
+const currentValue = computed({
+  get: () => subs.value === '1' ? transValue1.value : transValue.value,
+  set: (val) => {
+    if (subs.value === '1') transValue1.value = val
+    else transValue.value = val
+  }
+})
+
+// 左侧列表(全集 - 已选)
+const leftListData = computed(() => currentData.value.filter(item => !currentValue.value.includes(item.key)))
+// 右侧列表(已选)
+const rightListData = computed(() => currentData.value.filter(item => currentValue.value.includes(item.key)))
+
+// 搜索过滤
+const filteredLeftList = computed(() => leftListData.value.filter(item => item.label.toLowerCase().includes(searchLeft.value.toLowerCase())))
+const filteredRightList = computed(() => rightListData.value.filter(item => item.label.toLowerCase().includes(searchRight.value.toLowerCase())))
+
+const isAllLeftChecked = computed(() => filteredLeftList.value.length > 0 && leftChecked.value.length === filteredLeftList.value.length)
+const isAllRightChecked = computed(() => filteredRightList.value.length > 0 && rightChecked.value.length === filteredRightList.value.length)
+
+// 勾选事件
+const onLeftChange = (e: any) => { leftChecked.value = e.detail.value }
+const onRightChange = (e: any) => { rightChecked.value = e.detail.value }
+
+// 全选/反选
+const toggleAllLeft = () => {
+  if (isAllLeftChecked.value) leftChecked.value = []
+  else leftChecked.value = filteredLeftList.value.map(item => item.key)
+}
+
+const toggleAllRight = () => {
+  if (isAllRightChecked.value) rightChecked.value = []
+  else rightChecked.value = filteredRightList.value.map(item => item.key)
+}
+
+// 移动选中项
+const moveToRight = () => {
+  currentValue.value = [...currentValue.value, ...leftChecked.value]
+  leftChecked.value = []
+}
+
+const moveToLeft = () => {
+  currentValue.value = currentValue.value.filter(key => !rightChecked.value.includes(key))
+  rightChecked.value = []
+}
+
+// 切换 Tabs (挂入 / 撤销)
+const tableSearch = (val: string) => {
+  subs.value = val
+  searchLeft.value = ''
+  searchRight.value = ''
+  leftChecked.value = []
+  rightChecked.value = []
+}
+
+// 获取挂入数据
+const getSubs = async () => {
+  const res = await ibApi.MamSubsInfo({ mamId: Number(row.value.mamId), type: 1 })
+  if (res.code === Code.StatusOK) {
+    transData1.value = (res.data || []).map((item: string) => ({ key: item, label: item }))
+  } else {
+    uni.showToast({ title: res.msg, icon: 'none' })
+  }
+}
+
+// 获取撤销数据
+const getSubs1 = async () => {
+  const res = await ibApi.MamSubsInfo({ mamId: Number(row.value.mamId), type: 2 })
+  if (res.code === Code.StatusOK) {
+    transData.value = (res.data || []).map((item: string) => ({ key: item, label: item }))
+  } else {
+    uni.showToast({ title: res.msg, icon: 'none' })
+  }
+}
+
+// 提交申请
+const applySubmit = async () => {
+  if (flag.value) return
+  flag.value = true
+
+  const type = subs.value === '1' ? 1 : 2
+  const selectedSubs = currentValue.value
+
+  if (!selectedSubs || !selectedSubs.length) {
+    flag.value = false
+    uni.showToast({ title: t('placeholder.choose'), icon: 'none' })
+    return
+  }
+
+  try {
+    const res = await ibApi.MamSubsApply({
+      mamId: Number(row.value.mamId),
+      type,
+      subs: selectedSubs
+    })
+
+    if (res.code === Code.StatusOK) {
+      dialogCheck.value = true
+      dialogVisible.value = true
+    } else {
+      RES.value = res.msg
+      dialogCheck.value = true
+      dialogVisible.value = false
+    }
+  } catch (error) {
+    RES.value = t('Msg.Fail')
+    dialogCheck.value = true
+    dialogVisible.value = false
+  } finally {
+    flag.value = false
+  }
+}
+
+// 关闭结果弹窗
+const closeDia = () => {
+  dialogCheck.value = false
+  dialogVisible.value = false
+  // 刷新数据并清空选中
+  if (subs.value === '1') getSubs()
+  else getSubs1()
+  currentValue.value = []
+}
+
+// 返回
+const backIndex = () => {
+  uni.navigateBack({ delta: 1 })
+}
+
+onLoad((options: any) => {
+  if (options) {
+    row.value.login = options.login || ''
+    row.value.mamId = options.id || ''
+  }
+  getSubs()
+  getSubs1()
+})
+</script>
+
+<style lang="scss" scoped>
+@import "@/uni.scss";
+
+.header-btn {
+  display: flex;
+  align-items: center;
+  font-size: px2rpx(14);
+  color: #333;
+  cursor: pointer;
+  
+  text {
+    margin-left: px2rpx(5);
+  }
+}
+
+.main-content {
+  padding: px2rpx(20);
+  background-color: #fff;
+  border-radius: px2rpx(8);
+  margin-top: px2rpx(15);
+  min-height: calc(100vh - 100px);
+}
+
+.tips-text {
+  margin-bottom: px2rpx(20);
+  font-size: px2rpx(16);
+  line-height: 1.7;
+  font-weight: bold;
+}
+
+.tab-group {
+  display: flex;
+  margin-bottom: px2rpx(20);
+  
+  .tab-item {
+    height: px2rpx(32);
+    line-height: px2rpx(32);
+    padding: 0 px2rpx(20);
+    text-align: center;
+    border: 1px solid #dcdfe6;
+    background-color: #fff;
+    font-size: px2rpx(14);
+    cursor: pointer;
+    
+    &:first-child {
+      border-radius: px2rpx(4) 0 0 px2rpx(4);
+    }
+    
+    &:last-child {
+      border-radius: 0 px2rpx(4) px2rpx(4) 0;
+      border-left: none;
+    }
+    
+    &.active {
+      font-weight: bold;
+      background-color: var(--color-error);
+      color: #fff;
+      border-color: var(--color-error);
+    }
+  }
+}
+
+/* 穿梭框样式 */
+.transfer-container {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: px2rpx(20);
+}
+
+.transfer-panel {
+  flex: 1;
+  border: 1px solid #ebeef5;
+  border-radius: px2rpx(4);
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+  height: px2rpx(350);
+}
+
+.transfer-header {
+  height: px2rpx(40);
+  line-height: px2rpx(40);
+  background: #f5f7fa;
+  margin: 0;
+  padding: 0 px2rpx(15);
+  border-bottom: 1px solid #ebeef5;
+  box-sizing: border-box;
+  color: #000;
+  display: flex;
+  align-items: center;
+  font-size: px2rpx(14);
+  
+  .count {
+    margin-left: auto;
+    color: #909399;
+    font-size: px2rpx(12);
+  }
+}
+
+.transfer-search {
+  padding: px2rpx(10);
+}
+
+.transfer-list {
+  flex: 1;
+  overflow: hidden;
+  padding: px2rpx(10);
+}
+
+.transfer-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: px2rpx(10);
+  font-size: px2rpx(14);
+  color: #606266;
+}
+
+.empty-text {
+  text-align: center;
+  color: #909399;
+  font-size: px2rpx(14);
+  margin-top: px2rpx(50);
+}
+
+.transfer-actions {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  padding: 0 px2rpx(15);
+  
+  .action-btn {
+    width: px2rpx(32);
+    height: px2rpx(32);
+    border-radius: px2rpx(4);
+    background-color: var(--color-error);
+    color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: px2rpx(10);
+    padding: 0;
+    font-size: px2rpx(16);
+    
+    &[disabled] {
+      background-color: #f5f7fa;
+      border: 1px solid #ebeef5;
+      color: #c0c4cc;
+      cursor: not-allowed;
+    }
+  }
+}
+
+.submit-bar {
+  margin-top: px2rpx(20);
+  
+  .submit-btn {
+    width: 100%;
+  }
+}
+
+.result-dialog {
+  text-align: center;
+  padding: px2rpx(20);
+  
+  .icon-wrap {
+    margin-bottom: px2rpx(20);
+  }
+  
+  .result-text {
+    font-size: px2rpx(16);
+    font-weight: bold;
+    margin-top: px2rpx(10);
+    line-height: 1.5;
+  }
+  
+  .dialog-footer {
+    display: flex;
+    justify-content: center;
+    gap: px2rpx(15);
+    margin-top: px2rpx(30);
+    
+    button {
+      min-width: px2rpx(100);
+      margin: 0;
+    }
+  }
+}
+</style>