Просмотр исходного кода

Merge branch 'admin_dev' into admin_dev_ocr

# Conflicts:
#	config/index.ts
#	pages/mine/improveImmediately.vue
ljc 4 дней назад
Родитель
Сommit
a5db074586

+ 26 - 17
App.vue

@@ -4,7 +4,7 @@
   import config from '@/config'
   import { useMouseTooltip } from '@/utils/useMouseTooltip'
   const { t, locale } = useI18n()
-  const { Host80 } = config
+  // 勿解构 Host80,动态域名下需每次读取 config.Host80
 
   import {
     onLoad,
@@ -17,6 +17,11 @@
   import useGlobalStore from '@/stores/use-global-store'
   import { userToken } from '@/composables/config'
   import { useAppUpdate } from '@/hooks/useAppUpdate'
+  import {
+    whenDomainReady,
+    refreshDynamicDomainOnShow,
+    stopDomainHeartbeat,
+  } from '@/utils/dynamicDomain'
 
   const { checkUpdate } = useAppUpdate()
   const globalStore = useGlobalStore()
@@ -30,6 +35,10 @@
     updateRoute()
     // checkUpdate()
     handleSignupRoute(options)
+
+    // #ifdef APP-PLUS
+    refreshDynamicDomainOnShow()
+    // #endif
   })
 
   // App.vue 或你的初始化文件中
@@ -82,20 +91,18 @@
     // initTheme()
 
     // #ifdef APP-PLUS
-    uni.getSystemInfo({
-      success: res => {       // android且非谷歌渠道才执行
-        if(res.platform=="android"){
-          // 区分是否谷歌打包:用渠道名/manifest自定义参数
-          let channel = plus.runtime.channel;
-          if(channel != "google"){
-            // 仅国内APK执行代码
-            checkUpdate()
-            // console.log('普通APK专属逻辑')
+    whenDomainReady().then(() => {
+      uni.getSystemInfo({
+        success: (res) => {
+          if (res.platform === 'android') {
+            const channel = plus.runtime.channel
+            if (channel !== 'google') {
+              checkUpdate()
+            }
           }
-        }     }
+        },
+      })
     })
-    // checkWgtUpdate()
-
     // #endif
 
     // #ifdef H5
@@ -299,12 +306,11 @@
 
   // 检测版本号更新
   const checkWgtUpdate = async () => {
-    console.log(Host80)
+    await whenDomainReady()
     try {
-      console.log(Host80)
       const currentVersion = await getCurrentVersion()
       const res = await uni.request({
-        url: `${Host80}/wgt/list.json?_t=${Date.now()}`,
+        url: `${config.Host80}/wgt/list.json?_t=${Date.now()}`,
         method: 'GET',
         timeout: 5000,
       })
@@ -332,7 +338,7 @@
   // 下载并安装
   const downloadAndInstall = (version) => {
     //TODO: 需要根据版本来确定url
-    const url = `${Host80}/wgt/CwgApp_${version}.wgt`
+    const url = `${config.Host80}/wgt/CwgApp_${version}.wgt`
     console.log(url, 'downloadurl')
 
     uni.downloadFile({
@@ -421,6 +427,9 @@
     // #endif
   })
   onUnmounted(() => {
+    // #ifdef APP-PLUS
+    stopDomainHeartbeat()
+    // #endif
     // #ifdef H5
     window.removeEventListener('hashchange', handleSignupRoute)
     if (windowWidth.value < 700) return

+ 56 - 45
components/cwg-file-picker-wrapper.vue

@@ -1,7 +1,7 @@
 <template>
   <view class="file-picker-wrapper">
     <!-- 只读模式:仅展示 -->
-    <view v-if="readonly" class="file-list readonly-list">
+    <view v-if="readonly && !noFileList" class="file-list readonly-list">
       <view v-for="(file, index) in innerFileList" :key="index" class="file-item readonly-item"
         @click="previewFile(file, index)">
         <image v-if="isImage(file)" :src="file.url || file.path" mode="aspectFill" class="file-thumb"
@@ -16,52 +16,55 @@
 
     <!-- 正常模式:宫格上传(完全对齐官方 upload-image 样式) -->
     <view v-else class="uni-file-picker__container">
-      <view class="file-picker__box" v-for="(item, index) in innerFileList" :key="index"
-        :style="typeof boxStyle === 'object' ? boxStyle : { cssText: boxStyle }" >
-        <view class="file-picker__box-content " :style="borderStyle" >
-          <!-- 图片 -->
-          <image v-if="isImage(item)" class="file-image cursor-pointer" :data-tooltip="tooltipText(item)" :src="item.url || item.path" mode="aspectFill"
-            :style="imgStyle" @click.stop="previewFile(item, index)" />
-
-          <!-- 视频 → 显示第一帧 + 播放图标 -->
-          <view v-else-if="isVideo(item)" class="file-cover video-box cursor-pointer" :data-tooltip="tooltipText(item)" @click.stop="previewFile(item, index)">
-            <image :src="item.url || item.path" class="file-image" mode="aspectFill" :style="imgStyle" />
-            <view class="video-play-icon">▶</view>
-          </view>
-
-          <!-- 其他文件(PDF/Word/Excel) -->
-          <view v-else class="file-cover file-box cursor-pointer" :data-tooltip="tooltipText(item)" :class="getFileClass(item.name)"
-            @click.stop="previewFile(item, index)">
-            <text class="file-big-icon">{{ getFileIcon(item.name) }}</text>
-            <text class="file-ext-name">{{ getFileExt(item.name) }}</text>
-          </view>
-
-          <!-- 删除 -->
-          <view v-if="delIcon" class="icon-del-box cursor-pointer" :data-tooltip="t('vu.tooltip.t17')" @click.stop="deleteFile(index)">
-            <view class="icon-del"></view>
-            <view class="icon-del rotate"></view>
-          </view>
-
-          <!-- 进度 -->
-          <view v-if="item.status === 'uploading'" class="file-picker__progress">
-            <progress class="file-picker__progress-item" :percent="item.progress" stroke-width="4"
-              backgroundColor="#EBEBEB" />
-          </view>
-
-          <!-- 失败重试 -->
-          <view v-if="item.status === 'error'" class="file-picker__mask" @click.stop="reUploadFile(index)">
-            点击重试
+      <template v-if="!noFileList">
+        <view class="file-picker__box" v-for="(item, index) in innerFileList" :key="index"
+              :style="typeof boxStyle === 'object' ? boxStyle : { cssText: boxStyle }" >
+          <view class="file-picker__box-content " :style="borderStyle" >
+            <!-- 图片 -->
+            <image v-if="isImage(item)" class="file-image cursor-pointer" :data-tooltip="tooltipText(item)" :src="item.url || item.path" mode="aspectFill"
+                   :style="imgStyle" @click.stop="previewFile(item, index)" />
+
+            <!-- 视频 → 显示第一帧 + 播放图标 -->
+            <view v-else-if="isVideo(item)" class="file-cover video-box cursor-pointer" :data-tooltip="tooltipText(item)" @click.stop="previewFile(item, index)">
+              <image :src="item.url || item.path" class="file-image" mode="aspectFill" :style="imgStyle" />
+              <view class="video-play-icon">▶</view>
+            </view>
+
+            <!-- 其他文件(PDF/Word/Excel) -->
+            <view v-else class="file-cover file-box cursor-pointer" :data-tooltip="tooltipText(item)" :class="getFileClass(item.name)"
+                  @click.stop="previewFile(item, index)">
+              <text class="file-big-icon">{{ getFileIcon(item.name) }}</text>
+              <text class="file-ext-name">{{ getFileExt(item.name) }}</text>
+            </view>
+
+            <!-- 删除 -->
+            <view v-if="delIcon" class="icon-del-box cursor-pointer" :data-tooltip="t('vu.tooltip.t17')" @click.stop="deleteFile(index)">
+              <view class="icon-del"></view>
+              <view class="icon-del rotate"></view>
+            </view>
+
+            <!-- 进度 -->
+            <view v-if="item.status === 'uploading'" class="file-picker__progress">
+              <progress class="file-picker__progress-item" :percent="item.progress" stroke-width="4"
+                        backgroundColor="#EBEBEB" />
+            </view>
+
+            <!-- 失败重试 -->
+            <view v-if="item.status === 'error'" class="file-picker__mask" @click.stop="reUploadFile(index)">
+              点击重试
+            </view>
           </view>
         </view>
-      </view>
-
+      </template>
       <!-- 添加按钮 -->
-      <view v-if="innerFileList?.length < limit" class="file-picker__box cursor-pointer"
-        :style="typeof boxStyle === 'object' ? boxStyle : { cssText: boxStyle }" :data-tooltip="t('vu.tooltip.t10')" data-placement="top">
-        <view class="file-picker__box-content  is-add" :style="borderStyle" @click="handleChoose">
-          <cwg-icon name="icon_add" class="upload-icon" :size="24" />
+      <slot v-if="innerFileList?.length < limit" name="add-button" :handleChoose="handleChoose">
+        <view class="file-picker__box cursor-pointer"
+          :style="typeof boxStyle === 'object' ? boxStyle : { cssText: boxStyle }" :data-tooltip="t('vu.tooltip.t10')" data-placement="top">
+          <view class="file-picker__box-content is-add" :style="borderStyle" @click="handleChoose">
+            <cwg-icon name="icon_add" class="upload-icon" :size="24" />
+          </view>
         </view>
-      </view>
+      </slot>
     </view>
   </view>
   <cop-chooseFile :trigger="triggerFlag" accept="*" @receiveRenderFile="handleFile">
@@ -73,6 +76,7 @@ import { ref, watch, nextTick, computed } from 'vue'
 import config from '@/config'
 import { useI18n } from 'vue-i18n';
 import { userToken } from '@/composables/config'
+import { whenDomainReady } from '@/utils/dynamicDomain'
 import copChooseFile from '@/uni_modules/cop-chooseFile/components/cop-chooseFile/cop-chooseFile.vue'
 const { t, locale } = useI18n();
 
@@ -110,6 +114,11 @@ const props = defineProps({
     type: Boolean,
     default: true
   },
+  // 不展示文件列表
+  noFileList: {
+    type: Boolean,
+    default: false
+  },
   action: {
     type: String,
     default: ''
@@ -484,7 +493,8 @@ const startUpload = async () => {
   }
 }
 
-const uploadFile = (fileItem) => {
+const uploadFile = async (fileItem) => {
+  await whenDomainReady()
   return new Promise((resolve) => {
     innerFileList.value.push({ ...fileItem, status: 'uploading', progress: 0 })
     console.log(innerFileList.value,'upload231')
@@ -549,7 +559,8 @@ const uploadFile = (fileItem) => {
   })
 }
 
-const uploadBase64 = (fileItem) => {
+const uploadBase64 = async (fileItem) => {
+  await whenDomainReady()
   return new Promise((resolve) => {
     innerFileList.value.push({
       ...fileItem,

+ 1 - 2
composables/useFilters.ts

@@ -43,8 +43,7 @@ export function useFilters(): Filters {
         }
 
         // let value1 = Number(value).toFixed(2)
-      let value1 = numberDecimal(value,decimalPlaces);
-      console.log(value1)
+        let value1 = numberDecimal(value,decimalPlaces);
         if (value1.indexOf('-') > -1) {
             value1 = value1.split('-')[1];
             let num = value1.split('.');

+ 12 - 0
config/domainConstants.ts

@@ -0,0 +1,12 @@
+/** 远程域名配置(根域名列表:primary + backup) */
+export const DOMAIN_CONFIG_URL =
+  'https://www.cwgvu-git.com/cwg/dynamic-domain/raw/master/config.json'
+
+/** 首次启动解析超时后仍放行业务请求 */
+export const DOMAIN_INIT_TIMEOUT = 12000
+/** 探测单个域名超时 */
+export const DOMAIN_PROBE_TIMEOUT = 5000
+/** 可用性探测路径 */
+export const DOMAIN_PROBE_PATH = '/custom/ping'
+/** App 前台运行期间心跳检测间隔 */
+export const DOMAIN_HEARTBEAT_INTERVAL = 10 * 60 * 1000

+ 47 - 0
config/domainState.ts

@@ -0,0 +1,47 @@
+/** 代码内置默认根域名(远程 primary/backup 均不可用时回退) */
+export const DEFAULT_DOMAIN = {
+  ht: 'https:',
+  ho: 'cwgbroker',
+  dt: 'club',
+} as const
+
+export function getDefaultRootDomain() {
+  return `${DEFAULT_DOMAIN.ho}.${DEFAULT_DOMAIN.dt}`
+}
+
+/** APP 动态域名运行时状态(可被 dynamicDomain 更新) */
+let ht: string = DEFAULT_DOMAIN.ht
+let ho: string = DEFAULT_DOMAIN.ho
+let dt: string = DEFAULT_DOMAIN.dt
+
+export function getDomainParts() {
+  return { ht, ho, dt }
+}
+
+export function setDomainParts(parts: { ho: string; dt: string; ht?: string }) {
+  if (parts.ho) ho = parts.ho
+  if (parts.dt) dt = parts.dt
+  if (parts.ht) ht = parts.ht
+}
+
+export function resetToDefaultDomain() {
+  ht = DEFAULT_DOMAIN.ht
+  ho = DEFAULT_DOMAIN.ho
+  dt = DEFAULT_DOMAIN.dt
+}
+
+export function buildHostUrls() {
+  return {
+    HostWs: `wss://ws.${ho}.${dt}`,
+    Host80: `${ht}//secure.${ho}.${dt}`,
+    Host00: `${ht}//ucard.${ho}.${dt}`,
+    Host85: `${ht}//ucard.${ho}.${dt}`,
+    Host04: `${ht}//pay.${ho}.${dt}`,
+    Host90: `${ht}//data.${ho}.${dt}`,
+    HostShop: `${ht}//shopcustom.${ho}.${dt}`,
+    HostShopImg: `${ht}//shopmanager.${ho}.${dt}`,
+    Host87: `${ht}//followup.${ho}.${dt}`,
+    Host05: `${ht}//file.${ho}.${dt}`,
+    HostEnter: `${ht}//ad.${ho}.${dt}`,
+  }
+}

+ 50 - 31
config/index.ts

@@ -1,35 +1,14 @@
+import { getDomainParts, setDomainParts, buildHostUrls } from './domainState'
+
 // #ifdef H5
-let [p, h] = [window.location.protocol, window.location.host];
-let isIP = /^\d+\.\d+\.\d+\.\d+:\d+$/.test(h);
-let [ho, dt] = isIP ? ['44a5c8109e4', 'com'] : h.split('.').slice(-2);
-// let [ho, dt] = isIP ? ['cwgbroker', 'club'] : h.split('.').slice(-2);
-let ht = p == 'http:' ? 'https:' : p;
-console.log(ho, dt, ht, 1009);
-// #else
-let ht = 'https:';
-let ho = 'cwgbroker'//'cwgbroker'; // 默认主域名或可根据实际APP环境配置
-let dt = 'club'//'club'; // 默认域名后缀
+const [p, h] = [window.location.protocol, window.location.host]
+const isIP = /^\d+\.\d+\.\d+\.\d+:\d+$/.test(h)
+const [h5Ho, h5Dt] = isIP ? ['44a5c8109e4', 'com'] : h.split('.').slice(-2)
+const h5Ht = p == 'http:' ? 'https:' : p
+setDomainParts({ ho: h5Ho, dt: h5Dt, ht: h5Ht })
 // #endif
 
-const config = {
-
-  HostWs: "wss://ws." + ho + "." + dt,
-  Host80: ht + "//secure." + ho + "." + dt,
-  Host00: ht + "//ucard." + ho + "." + dt,
-  Host85: ht + "//ucard." + ho + "." + dt,
-  Host04: ht + "//pay." + ho + "." + dt,
-  // Host80: 'http://192.168.0.32:8000',
-  // Host00: 'http://192.168.0.32:8000',
-  // Host85: 'http://192.168.0.32:8000',
-  // Host04: 'http://192.168.0.32:8004',
-  Host90: ht + "//data." + ho + "." + dt,
-  HostShop: ht + "//shopcustom." + ho + "." + dt,
-  HostShopImg: ht + "//shopmanager." + ho + "." + dt,
-  Host87: ht + "//followup." + ho + "." + dt,
-  Host05: ht + "//file." + ho + "." + dt,
-  HostEnter: ht + "//ad." + ho + "." + dt,
-  ho,
-  host: ho,
+const staticConfig = {
   Code: {
     StatusOK: 200,
     StatusFail: 400,
@@ -48,5 +27,45 @@ const config = {
     nonnegative: /^\d+(\.\d{1,2})?$/, // 非负数(最多两位小数)
     englishName: /^[^\u4E00-\u9FA5]+$/,
   },
-};
-export default config;
+}
+
+const hostKeys = [
+  'HostWs',
+  'Host80',
+  'Host00',
+  'Host85',
+  'Host04',
+  'Host90',
+  'HostShop',
+  'HostShopImg',
+  'Host87',
+  'Host05',
+  'HostEnter',
+] as const
+
+type HostKey = (typeof hostKeys)[number]
+
+const config: Record<string, unknown> = {
+  ...staticConfig,
+  get ho() {
+    return getDomainParts().ho
+  },
+  get host() {
+    return getDomainParts().ho
+  },
+}
+
+hostKeys.forEach((key) => {
+  Object.defineProperty(config, key, {
+    enumerable: true,
+    configurable: true,
+    get() {
+      return buildHostUrls()[key as HostKey]
+    },
+  })
+})
+
+export default config as typeof staticConfig & Record<HostKey, string> & {
+  ho: string
+  host: string
+}

+ 5 - 5
hooks/useAppUpdate.ts

@@ -5,6 +5,7 @@ import useUserStore from '@/stores/use-user-store'
 import { ucardApi } from '@/api/ucard'
 import { userToken } from '@/composables/config'
 import config from '@/config'
+import { whenDomainReady } from '@/utils/dynamicDomain'
 import { trim } from 'lodash'
 
 // ================== 类型声明 ==================
@@ -212,9 +213,7 @@ function removeStorageSync(key: string): boolean {
 
 export function useAppUpdate() {
   const { t } = useI18n()
-  const { Host80 } = config
-  // 测试环境地址
-  // const Host80 = 'https://secure.44a5c8109e4.com/'
+  // 勿解构 Host80,动态域名下需每次读取 getter
   const userStore = useUserStore()
   const progress = useProgress()
 
@@ -244,9 +243,10 @@ export function useAppUpdate() {
     checking.value = true
 
     try {
+      await whenDomainReady()
 
       const res = await uni.request({
-        url: `${Host80}/wgt/list.json?_t=${Date.now()}`,
+        url: `${config.Host80}/wgt/list.json?_t=${Date.now()}`,
         method: 'GET',
         timeout: 5000,
       })
@@ -288,7 +288,7 @@ export function useAppUpdate() {
       // }
 
       // 显示更新提示
-      const url = `${Host80}/wgt/CwgApp_${latestVersion}.wgt`
+      const url = `${config.Host80}/wgt/CwgApp_${latestVersion}.wgt`
       console.log('url',url)
       showForceUpdate({ version: latestVersion, forceUpdate: true, wgtUrl: url })
     } catch (error) {

+ 4 - 0
main.js

@@ -8,6 +8,10 @@ import vEllipsis from './directives/v-ellipsis'
 import { createI18n } from "vue-i18n";
 import { routeInterceptor } from '@/utils/routeInterceptor.js'
 import { lang } from '@/composables/config'
+// #ifdef APP-PLUS
+import { initDynamicDomain } from '@/utils/dynamicDomain'
+initDynamicDomain()
+// #endif
 // import './static/js/jsvm_all.js'
 import { watch } from "vue";
 import vT from './directives/v-t'

+ 2 - 2
manifest.json

@@ -2,8 +2,8 @@
     "name" : "CWG Center",
     "appid" : "__UNI__F1BC8D2",
     "description" : "应用描述",
-    "versionName" : "1.1.3",
-    "versionCode" : 3,
+    "versionName" : "1.1.5",
+    "versionCode" : 5,
     "transformPx" : false,
     "app-plus" : {
         "console" : false,

+ 1 - 2
pages/common/chat.vue

@@ -15,10 +15,9 @@ import { onLoad } from '@dcloudio/uni-app'
 import useGlobalStore from '@/stores/use-global-store'
 const globalStore = useGlobalStore()
 import Config from '@/config/index'
-const { Host80 } = Config
 import getWebBase from '@/utils/webBase'
 const webBase = getWebBase()
-const fileUrl = `${Host80}/iframe/livechat.html`
+const fileUrl = computed(() => `${Config.Host80}/iframe/livechat.html`)
 const statusBarHeight = computed(() => globalStore.statusBarHeight)
 
 const webviewStyles = computed(() => ({

+ 1 - 2
pages/common/webview.vue

@@ -13,7 +13,6 @@
 import { ref, computed } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import Config from '@/config/index'
-const { Host80 } = Config
 import getWebBase from '@/utils/webBase'
 const webBase = getWebBase()
 import { useI18n } from 'vue-i18n'
@@ -53,7 +52,7 @@ onLoad((options) => {
 
   // ✅ 核心修复:PDF 只编码一次,不再二次编码
   if (fileType.value === 'PDF') {
-    fileUrl.value = `${Host80}${webBase}/iframe/pdf.html?pdf=${realUrl}&title=${title.value}`
+    fileUrl.value = `${Config.Host80}${webBase}/iframe/pdf.html?pdf=${realUrl}&title=${title.value}`
   } else {
     fileUrl.value = realUrl
   }

+ 0 - 2
pages/customer/create-account.vue

@@ -619,8 +619,6 @@ onLoad((e) => {
     dome.value = e.server == 'demo'
     cativeIndex.value = e.server
     isOpenAccount.value = e.id
-    const host = window?.location?.host || '';
-    ho.value = host.split('.')[1] || '';
 
     getExcludeShowLogin();
     getMustData(isOpenAccount.value, dome.value);

+ 7 - 4
pages/ib/agent-transfer.vue

@@ -598,11 +598,14 @@ watch(loginValue, (newVal) => {
     }
 })
 // 监听 loginValue 变化
-watch(form.depositLogin, (newVal) => {
-    if (newVal) {
-        togetActivity()
+watch(
+    () => form.depositLogin,
+    (newVal) => {
+        if (newVal) {
+            togetActivity()
+        }
     }
-})
+)
 
 // 监听 agree6 变化
 watch(() => form.agree6, (newVal) => {

+ 17 - 9
pages/launch/index.vue

@@ -1,15 +1,23 @@
+<template>
+  <view class="launch-page" />
+</template>
+
 <script setup>
 import { onLoad } from '@dcloudio/uni-app'
 import { userToken } from '@/composables/config'
+
 onLoad(() => {
-    if (!userToken.value) {
-        uni.reLaunch({
-            url: '/pages/login/index'
-        })
-    } else {
-        uni.reLaunch({
-            url: '/pages/customer/index'
-        })
-    }
+  if (!userToken.value) {
+    uni.reLaunch({ url: '/pages/login/index' })
+  } else {
+    uni.reLaunch({ url: '/pages/customer/index' })
+  }
 })
 </script>
+
+<style scoped>
+.launch-page {
+  min-height: 100vh;
+  background-color: #fff;
+}
+</style>

+ 5 - 9
pages/login/index.vue

@@ -188,7 +188,7 @@
                           <ul class="pwd">
                             <li :class="{ fit: rule1 }" v-t="'signup.form.rules.1st'"></li>
                             <li :class="{ fit: rule2 }" v-t="'signup.form.rules.2nd'"></li>
-                            <li :class="{ fit: rule3 }" v-t="'signup.form.rules.3rd'"></li>
+                            <li :class="{ fit: rule3 }" v-t="'signup.form.rules.4rd'"></li>
                           </ul>
                         </uni-forms-item>
                       </uni-col>
@@ -428,12 +428,11 @@ const rule1 = computed(() => {
 const rule2 = computed(() => {
   return /^(?=.*?[a-z])(?=.*?[A-Z]).*$/.test(formData.value.password)
 })
-
 const rule3 = computed(() => {
-  return /^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?!.*([~!@&%$^\(\)#_]).*\1.*\1)[A-Za-z0-9~!@&%$^\(\)#_]{8,16}$/.test(
-    formData.value.password,
-  )
-})
+    return /^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[~!@&%$^*./\\(\\)\\+\\=#_-])[A-Za-z0-9~!@&%$^*./\\(\\)\\+\\=#_-]{8,16}$/.test(
+        formData.value.password,
+    );
+});
 
 const isAgeValid = computed(() => {
   if (!formData.value.birthDate) {
@@ -865,9 +864,6 @@ onMounted(() => {
   // 注册表单初始化
   getCountry()
   getCountryMsg()
-
-  const hostParts = window.location.host.split('.')
-  ho.value = hostParts.length > 1 ? hostParts[1] : ''
   globalStore.setMode(ls.get('mode') || 'customer');
 })
 const inputType = ref('password')

+ 0 - 3
pages/login/regist.vue

@@ -580,9 +580,6 @@ const getLoginInfo = async () => {
 onMounted(() => {
   getCountry()
   getCountryMsg()
-
-  const hostParts = window.location.host.split('.')
-  ho.value = hostParts.length > 1 ? hostParts[1] : ''
 })
 
 </script>

+ 27 - 35
pages/mine/improveImmediately.vue

@@ -478,34 +478,24 @@
               </view>
               <uni-row class="demo-uni-row uni-row1" :gutter="20">
                 <uni-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-                  <cwg-file-picker :limit="9" :multiple="true" :editable="true" :fileMediatype="'all'"
-                                   uploadUrl="/custom/file/upload/10" :baseUrl="updateUrl" :imageWidth="100"
-                                   :imageHeight="60"
-                                   :showPreviewDelete="false" :canDelete="false" :uploadError="false"
-                                   :showProgress="false"
-                                   :image-styles="imageStyle" @update:modelValue="(val) => handleFileUpdate(val, '10')"
-                                   custom-class="fileOther" :showError="false">
-                    <button type="primary" class="btn btn-danger waves-effect waves-light">{{ t('Btn.Upload') }}
-                    </button>
-                  </cwg-file-picker>
+                  <cwg-file-picker-wrapper :limit="9" :multiple="true" :fileMediatype="'all'"
+                    uploadUrl="/custom/file/upload/10" :baseUrl="updateUrl" :imageWidth="100" :imageHeight="60" noFileList
+                    :showPreviewDelete="false" :canDelete="false" :uploadError="false" :showProgress="false"
+                    :image-styles="imageStyle" @update:modelValue="(val) => handleFileUpdate(val, '10')"
+                    custom-class="fileOther" :showError="false">
+                    <template #add-button="{ handleChoose }">
+                      <button type="primary" class="btn btn-danger waves-effect waves-light btn-upload" @click="handleChoose">{{ t('Btn.Upload') }}</button>
+                    </template>
+
+                  </cwg-file-picker-wrapper>
                   <view class="fileList">
                     <view id="files" v-for="(item, index) in fileListOthers" :key="index">
-
                       <cwg-file-picker-wrapper customClass="list_upload" v-model="item.path"
-                                               :editable="item.status != 2" :limit="1" :fileMediatype="'all'"
-                                               :uploadUrl="'/custom/file/upload/10/' + item.id" :baseUrl="updateUrl"
-                                               :uploadError="false"
-                                               :showProgress="false" :imageWidth="200" :imageHeight="150"
-                                               :showPreviewDelete="true"
-                                               :disablePreview="true" :image-styles="imageStyle" :canChoose="true"
-                                               @update:modelValue="(val) => handleFileUpdate(val, '1')">
-                        <view class="file-item">
-                          <image v-if="!isPdf(item.path) || !isPdf(item.againPath)" class="avatar"
-                                 :src="updateUrl + (item.againPath || item.path)"></image>
-                          <view v-else>
-                            <image class="icon" :src="icon_doc" />
-                          </view>
-                        </view>
+                        :disabled="item.status == 2" :limit="1" :fileMediatype="'all'"
+                        :uploadUrl="'/custom/file/upload/10/' + item.id" :baseUrl="updateUrl" :uploadError="false"
+                        :showProgress="false" :imageWidth="200" :imageHeight="150" :showPreviewDelete="true"
+                        :disablePreview="true" :image-styles="imageStyle" :canChoose="true"
+                        @update:modelValue="(val) => handleFileUpdate(val, '1')">
                       </cwg-file-picker-wrapper>
                       <view class="options">
                         <u-button style="margin-right: 20px;" :disabled="item.status == 2" @click.stop="showFile(item)">
@@ -1083,12 +1073,11 @@
   }
 
 
-  function handleFileUpdate(value, type) {
-    console.log(value, type, 'uplaod')
-    if (type == 10) {
-      uni.showToast({ title: t('card.New1.d5'), icon: 'none' })
-      getCustomFileList()
-    }
+function handleFileUpdate(value, type) {
+  console.log(value, type, 'uplaod')
+  if (type == 10) {
+    // uni.showToast({ title: t('card.New1.d5'), icon: 'none' })
+    getCustomFileList();
   }
 
   function handleOcr(value, type) {
@@ -1908,9 +1897,12 @@ watch(
       margin-bottom: px2rpx(5);
     }
 
-    .back {
-      font-size: px2rpx(12);
-      color: var(--bs-heading-color);
-    }
+  .back {
+    font-size: px2rpx(12);
+    color: var(--bs-heading-color);
   }
+}
+.btn-upload{
+  width: px2rpx(120);
+}
 </style>

+ 315 - 0
utils/dynamicDomain.js

@@ -0,0 +1,315 @@
+import {
+  getDomainParts,
+  setDomainParts,
+  getDefaultRootDomain,
+  DEFAULT_DOMAIN,
+} from '@/config/domainState'
+import {
+  DOMAIN_CONFIG_URL,
+  DOMAIN_INIT_TIMEOUT,
+  DOMAIN_PROBE_PATH,
+  DOMAIN_PROBE_TIMEOUT,
+  DOMAIN_HEARTBEAT_INTERVAL,
+} from '@/config/domainConstants'
+
+let resolving = null
+let domainReady = false
+let domainReadyPromise = null
+let domainReadyResolve = null
+let initTimeoutId = null
+let heartbeatTimer = null
+
+function createDomainReadyPromise() {
+  domainReadyPromise = new Promise((resolve) => {
+    domainReadyResolve = resolve
+  })
+}
+
+function markDomainReady() {
+  if (domainReady) return
+  domainReady = true
+  if (initTimeoutId) {
+    clearTimeout(initTimeoutId)
+    initTimeoutId = null
+  }
+  domainReadyResolve?.()
+}
+
+function buildActiveSecureUrl() {
+  const { ht, ho, dt } = getDomainParts()
+  return `${ht}//secure.${ho}.${dt}`
+}
+
+function notifyDomainChanged(parts) {
+  try {
+    uni.$emit('domain-changed', {
+      ...parts,
+      activeSecureUrl: parts.secureUrl || buildActiveSecureUrl(),
+    })
+  } catch (e) {
+    // ignore
+  }
+}
+
+/** 业务请求需等待域名就绪(仅 APP) */
+export function whenDomainReady() {
+  // #ifndef APP-PLUS
+  return Promise.resolve()
+  // #endif
+  if (domainReady) return Promise.resolve()
+  if (!domainReadyPromise) createDomainReadyPromise()
+  return domainReadyPromise
+}
+
+/** 当前域名状态(调试用) */
+export function getDomainStatus() {
+  return {
+    ready: domainReady,
+    parts: getDomainParts(),
+    activeSecureUrl: buildActiveSecureUrl(),
+    defaultRoot: getDefaultRootDomain(),
+    defaultSecureUrl: `https://secure.${DEFAULT_DOMAIN.ho}.${DEFAULT_DOMAIN.dt}`,
+  }
+}
+
+/**
+ * 解析根域名配置项
+ * 支持:cwgbroker.club / secure.cwgbroker.club / https://secure.cwgbroker.club
+ */
+export function parseRootDomain(input) {
+  if (!input || typeof input !== 'string') {
+    throw new Error('无效的域名')
+  }
+  let host = input.trim()
+  host = host.replace(/^https?:\/\//i, '')
+  host = host.split('/')[0]
+  host = host.replace(/^secure\./i, '')
+
+  const parts = host.split('.')
+  if (parts.length < 2) {
+    throw new Error(`域名格式不正确: ${input}`)
+  }
+
+  const dt = parts[parts.length - 1]
+  const ho = parts[parts.length - 2]
+  const ht = 'https:'
+
+  return {
+    ho,
+    dt,
+    ht,
+    root: `${ho}.${dt}`,
+    secureUrl: `${ht}//secure.${ho}.${dt}`,
+  }
+}
+
+/** @deprecated 兼容旧调用 */
+export function parseSecureUrl(url) {
+  return parseRootDomain(url)
+}
+
+function parseResponseData(data) {
+  if (!data) return null
+  if (typeof data === 'string') {
+    try {
+      return JSON.parse(data)
+    } catch (e) {
+      return null
+    }
+  }
+  return data
+}
+
+function requestJson(url, timeout = 8000) {
+  return new Promise((resolve, reject) => {
+    uni.request({
+      url,
+      method: 'GET',
+      timeout,
+      success: (res) => {
+        if (res.statusCode !== 200) {
+          reject(new Error(`请求失败: ${res.statusCode}`))
+          return
+        }
+        const body = parseResponseData(res.data)
+        if (!body) {
+          reject(new Error('配置解析失败'))
+          return
+        }
+        resolve(body)
+      },
+      fail: (err) => reject(err || new Error('网络请求失败')),
+    })
+  })
+}
+
+/** 探测 secure 子域是否可用 */
+export function probeSecureUrl(secureUrl, timeout = DOMAIN_PROBE_TIMEOUT) {
+  const url = `${secureUrl.replace(/\/$/, '')}${DOMAIN_PROBE_PATH}?_t=${Date.now()}`
+  return new Promise((resolve) => {
+    uni.request({
+      url,
+      method: 'GET',
+      timeout,
+      success: (res) => resolve(res.statusCode === 200),
+      fail: () => resolve(false),
+    })
+  })
+}
+
+async function pickActiveRootDomain(remoteConfig) {
+  const candidates = []
+  if (remoteConfig?.primary) {
+    candidates.push({ root: remoteConfig.primary, priority: 0 })
+  }
+  ;(remoteConfig?.backup || []).forEach((root, index) => {
+    if (root) candidates.push({ root, priority: index + 1 })
+  })
+
+  if (!candidates.length) {
+    throw new Error('远程域名配置为空')
+  }
+
+  const checks = await Promise.all(
+    candidates.map(async (item) => {
+      const parsed = parseRootDomain(item.root)
+      return {
+        ...item,
+        parsed,
+        ok: await probeSecureUrl(parsed.secureUrl),
+      }
+    })
+  )
+
+  const winner = checks
+    .filter((item) => item.ok)
+    .sort((a, b) => a.priority - b.priority)[0]
+  if (!winner) {
+    console.warn('[dynamicDomain] primary/backup 均不可用,回退代码默认域名')
+    return getDefaultRootDomain()
+  }
+
+  return winner.root
+}
+
+function applyRootDomain(root, meta = {}) {
+  const parsed = parseRootDomain(root)
+  const before = getDomainParts()
+  setDomainParts(parsed)
+
+  const changed = before.ho !== parsed.ho || before.dt !== parsed.dt
+  if (changed) {
+    notifyDomainChanged(parsed)
+    console.log('[dynamicDomain] 域名已切换', {
+      from: `${before.ho}.${before.dt}`,
+      to: parsed.root,
+      secureUrl: parsed.secureUrl,
+      version: meta.version,
+      fallback: meta.fallback || false,
+    })
+  } else {
+    console.log('[dynamicDomain] 域名可用', parsed.secureUrl)
+  }
+
+  return parsed
+}
+
+/** primary/backup 均不可用或拉配置失败时,回退代码内置默认域名 */
+function applyDefaultDomain(meta = {}) {
+  return applyRootDomain(getDefaultRootDomain(), { ...meta, fallback: true })
+}
+
+async function fetchRemoteConfig() {
+  const data = await requestJson(`${DOMAIN_CONFIG_URL}?_t=${Date.now()}`)
+  if (!data?.primary) {
+    throw new Error('远程配置缺少 primary 字段')
+  }
+  return data
+}
+
+/** 拉取配置并探测可用根域名 */
+export async function resolveDynamicDomain() {
+  if (resolving) return resolving
+
+  resolving = (async () => {
+    try {
+      const remoteConfig = await fetchRemoteConfig()
+      const activeRoot = await pickActiveRootDomain(remoteConfig)
+      return applyRootDomain(activeRoot, { version: remoteConfig.version })
+    } catch (error) {
+      console.warn('[dynamicDomain] 解析失败,回退代码默认域名', error)
+      return applyDefaultDomain()
+    } finally {
+      resolving = null
+    }
+  })()
+
+  return resolving
+}
+
+function scheduleInitTimeout() {
+  if (initTimeoutId) return
+  initTimeoutId = setTimeout(() => {
+    console.warn(`[dynamicDomain] 初始化超过 ${DOMAIN_INIT_TIMEOUT}ms,放行业务请求`)
+    markDomainReady()
+  }, DOMAIN_INIT_TIMEOUT)
+}
+
+/** 启动时解析域名(main.js 调用) */
+export function initDynamicDomain() {
+  // #ifndef APP-PLUS
+  return Promise.resolve(null)
+  // #endif
+
+  if (!domainReadyPromise) createDomainReadyPromise()
+  try {
+    uni.removeStorageSync('app-dynamic-domain')
+  } catch (e) {
+    // ignore
+  }
+  scheduleInitTimeout()
+
+  return resolveDynamicDomain()
+    .finally(() => {
+      markDomainReady()
+      startDomainHeartbeat()
+      console.log('[dynamicDomain] 域名就绪', buildActiveSecureUrl())
+    })
+}
+
+/** 每次 App 进入前台时重新检测 */
+export function refreshDynamicDomainOnShow() {
+  // #ifndef APP-PLUS
+  return Promise.resolve(null)
+  // #endif
+
+  if (!domainReady) {
+    return whenDomainReady()
+  }
+
+  return resolveDynamicDomain().catch((e) => {
+    console.warn('[dynamicDomain] 前台刷新失败', e)
+  })
+}
+
+/** App 运行期间定时心跳检测(10 分钟) */
+export function startDomainHeartbeat() {
+  // #ifndef APP-PLUS
+  return
+  // #endif
+
+  stopDomainHeartbeat()
+  heartbeatTimer = setInterval(() => {
+    console.log('[dynamicDomain] 心跳检测')
+    resolveDynamicDomain().catch((e) => {
+      console.warn('[dynamicDomain] 心跳检测失败', e)
+    })
+  }, DOMAIN_HEARTBEAT_INTERVAL)
+}
+
+export function stopDomainHeartbeat() {
+  if (heartbeatTimer) {
+    clearInterval(heartbeatTimer)
+    heartbeatTimer = null
+  }
+}

+ 1 - 2
utils/pdf.js

@@ -1,5 +1,4 @@
 import Config from '@/config/index'
-const { Host80 } = Config
 import getWebBase from '@/utils/webBase'
 const webBase = getWebBase()
 
@@ -10,7 +9,7 @@ export function openLocalPdf(fileName, title, type = 'pdf') {
 
   // 拼接地址
   if (type === 'pdf') {
-    targetUrl = `${Host80}/${fileName}`
+    targetUrl = `${Config.Host80}/${fileName}`
   } else if (type === 'pdf1') {
     targetUrl = fileName
   }

+ 25 - 19
utils/request.js

@@ -2,9 +2,10 @@
 import { showLoading, hideLoading } from '@/hooks/useLoading'
 import config1 from "@/config";
 import ls from "@/utils/store2";
+import { whenDomainReady } from '@/utils/dynamicDomain';
 
-const baseUrl = config1.Host85;
 const timeout = 60000;
+const getHost = (type = 'Host80') => config1[type] || config1.Host80;
 // 不加loading
 const urlLoading = ['/list', '/page', '/field/params', '/dropdown', '/single', '/detail']
 import { CLIENT, lang, userToken, shopToken } from "@/composables/config";
@@ -87,10 +88,10 @@ const responseInterceptor = (response, options = {}) => {
       if (ignore401) {
         return Promise.reject({
           ...data,
-          code: 401,
+          code: 600,
         });
       }
-      
+
       // 4. 提示并跳转登录页(防抖/防重复跳转处理)
       if (!isRedirectingToLogin) {
         isRedirectingToLogin = true;
@@ -98,14 +99,14 @@ const responseInterceptor = (response, options = {}) => {
           title: "登录已过期,请重新登录",
           icon: "none",
         });
-        
+
         uni.$emit('logout');
-        
+
         setTimeout(() => {
           uni.reLaunch({
             url: LOGIN_PAGE_PATH,
             success: () => {
-              uni.setStorageSync('logoutToSystem',1)
+              uni.setStorageSync('logoutToSystem', 1)
               ls.set('mode', 'customer');
               // globalStore.setMode('customer');
               // uni.clearStorageSync()
@@ -117,10 +118,10 @@ const responseInterceptor = (response, options = {}) => {
           });
         }, 1500);
       }
-      
+
       return Promise.reject({
         ...data,
-        code: 401,
+        code: 600,
         message: "登录已过期,请重新登录",
       });
     }
@@ -136,10 +137,11 @@ const responseInterceptor = (response, options = {}) => {
       return Promise.reject(data);
     }
   } else {
-    uni.showToast({
-      title: `网络错误: ${statusCode}`,
-      icon: "none",
-    });
+    // uni.showToast({
+    //   title: `网络错误: ${statusCode}`,
+    //   icon: "none",
+    // });
+    console.log('接口错误error:', error)
     return Promise.reject(response);
   }
 };
@@ -147,6 +149,7 @@ const responseInterceptor = (response, options = {}) => {
 // 错误处理
 const errorHandler = (error) => {
   uni.hideLoading();
+  console.log('请求失败抛出error:', error)
   uni.showToast({
     title: "网络异常,请稍后重试",
     icon: "none",
@@ -155,9 +158,10 @@ const errorHandler = (error) => {
 };
 
 // 核心请求函数
-export const request = (options) => {
-  // const host = config1[options.type || 'Host85'] || '';
-  const host = config1[options.type || 'Host80'] || '';
+export const request = async (options) => {
+  await whenDomainReady();
+  const host = getHost(options.type || 'Host80');
+
   // 合并配置
   const config = {
     ...options,
@@ -209,11 +213,12 @@ export const request = (options) => {
  * @param {Function} [options.onProgressUpdate] - 进度监听函数(可选)
  * @returns {Promise} - 上传结果
  */
-export const upload = (options) => {
+export const upload = async (options) => {
+  await whenDomainReady();
   // 1. 处理基础配置
   const uploadConfig = {
     ...options,
-    url: `${baseUrl}${options.url}`, // 完整上传接口地址
+    url: `${getHost(options.type || 'Host85')}${options.url}`, // 完整上传接口地址
     timeout,
     name: options.name || "file", // 后端接收文件的字段名(默认file)
     filePath: options.filePath, // 图片临时路径
@@ -278,7 +283,8 @@ export const upload = (options) => {
  * @param {Object} [header] - 额外请求头
  * @param {boolean} [checkCode=true] - 是否走响应码检查(默认走)
  */
-export const uploadFile = (url, file, data = {}, header = {}, checkCode = true) => {
+export const uploadFile = async (url, file, data = {}, header = {}, checkCode = true) => {
+  await whenDomainReady();
   return new Promise((resolve, reject) => {
     try {
       // 提取文件路径
@@ -287,7 +293,7 @@ export const uploadFile = (url, file, data = {}, header = {}, checkCode = true)
         filePath = file.path || file.url || file.tempFilePath || file.filePath || file;
       }
 
-      const finalUrl = `${baseUrl}${url}`;
+      const finalUrl = `${getHost('Host85')}${url}`;
 
       // 构建 headers,优先使用传入 header
       const headers = {