13404089107
2026-04-24 55fe68b0b9de887c555fcce3fc7dbb0e6b17683f
feat: 切换API基础URL并优化微信登录功能

- 将BASE_URL从本地开发环境切换至线上生产环境
- 在manifest.json中添加payment配置项
- 重构微信登录逻辑,使用plus.oauth.getServices实现更稳定的授权流程
- 改进代码格式和缩进以提高可读性
3个文件已修改
1383 ■■■■■ 已修改文件
traditionalApp/config/index.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/manifest.json 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/login/login.vue 1086 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/config/index.js
@@ -1,6 +1,6 @@
export default {
    BASE_URL: 'http://192.168.110.188:48080',
    // BASE_URL: 'https://haizhentong.com',
    // BASE_URL: 'http://192.168.110.188:48080',
    BASE_URL: 'https://haizhentong.com',
    uploadUrl: "https://haizhentong.com/api/open/file/upload",
    imageUrl: 'http://221.182.45.100:11224/images/'
};
traditionalApp/manifest.json
@@ -1,147 +1,148 @@
{
    "sassImplementationName": "node-sass",
    "name": "海诊康道",
    "appid": "__UNI__1A90541",
    "description": "",
    "versionName": "1.0.1",
    "versionCode": 101,
    "transformPx": false,
    /* 5+App特有相关 */
    "app-plus": {
        "usingComponents": true,
        "nvueStyleCompiler": "uni-app",
        "compilerVersion": 3,
        "splashscreen": {
            "alwaysShowBeforeRender": true,
            "waiting": true,
            "autoclose": true,
            "delay": 0
        },
        /* 模块配置 */
        "modules": {
            "Barcode": {},
            "VideoPlayer": {},
            "Share": {},
            "OAuth": {}
        },
        /* 应用发布信息 */
        "distribute": {
            /* android打包配置 */
            "android": {
                "permissions": [
                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
                    "<uses-feature android:name=\"android.hardware.camera\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
                ],
                "minSdkVersion": 30,
                "targetSdkVersion": 29
            },
            /* ios打包配置 */
            "ios": {
                "dSYMs": false,
                "capabilities": {
                    "entitlements": {
                        "com.apple.developer.associated-domains": [
                            "applinks:static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com"
                        ]
                    }
                }
            },
            /* SDK配置 */
            "sdkConfigs": {
                "push": {},
                "share": {
                    "weixin": {
                        "appid": "wx074c1f0cf5a0ecf4",
                        "UniversalLinks": "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
                    }
                },
                "oauth": {
                    "weixin": {
                        "appid": "wx074c1f0cf5a0ecf4",
                        "UniversalLinks": "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
                    }
                }
            },
            "icons": {
                "android": {
                    "hdpi": "unpackage/res/icons/72x72.png",
                    "xhdpi": "unpackage/res/icons/96x96.png",
                    "xxhdpi": "unpackage/res/icons/144x144.png",
                    "xxxhdpi": "unpackage/res/icons/192x192.png"
                },
                "ios": {
                    "appstore": "unpackage/res/icons/1024x1024.png",
                    "ipad": {
                        "app": "unpackage/res/icons/76x76.png",
                        "app@2x": "unpackage/res/icons/152x152.png",
                        "notification": "unpackage/res/icons/20x20.png",
                        "notification@2x": "unpackage/res/icons/40x40.png",
                        "proapp@2x": "unpackage/res/icons/167x167.png",
                        "settings": "unpackage/res/icons/29x29.png",
                        "settings@2x": "unpackage/res/icons/58x58.png",
                        "spotlight": "unpackage/res/icons/40x40.png",
                        "spotlight@2x": "unpackage/res/icons/80x80.png"
                    },
                    "iphone": {
                        "app@2x": "unpackage/res/icons/120x120.png",
                        "app@3x": "unpackage/res/icons/180x180.png",
                        "notification@2x": "unpackage/res/icons/40x40.png",
                        "notification@3x": "unpackage/res/icons/60x60.png",
                        "settings@2x": "unpackage/res/icons/58x58.png",
                        "settings@3x": "unpackage/res/icons/87x87.png",
                        "spotlight@2x": "unpackage/res/icons/80x80.png",
                        "spotlight@3x": "unpackage/res/icons/120x120.png"
                    }
                }
            }
        }
    },
    /* 快应用特有相关 */
    "quickapp": {},
    /* 小程序特有相关 */
    "mp-weixin": {
        "appid": "wxa17e8d1331e50934",
        "setting": {
            "urlCheck": false,
            "minified": true
        },
        "usingComponents": true
    },
    "mp-alipay": {
        "usingComponents": true
    },
    "mp-baidu": {
        "usingComponents": true
    },
    "mp-toutiao": {
        "usingComponents": true
    },
    "uniStatistics": {
        "enable": false
    },
    "vueVersion": "3",
    "permission": {
        "scope.userLocation": {
            "desc": "下载文件"
        }
    },
    "h5": {
        "unipush": {
            "enable": false
        }
    },
    "_spaceID": "mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8"
}
    "sassImplementationName" : "node-sass",
    "name" : "海诊康道",
    "appid" : "__UNI__1A90541",
    "description" : "",
    "versionName" : "1.0.1",
    "versionCode" : 101,
    "transformPx" : false,
    /* 5+App特有相关 */
    "app-plus" : {
        "usingComponents" : true,
        "nvueStyleCompiler" : "uni-app",
        "compilerVersion" : 3,
        "splashscreen" : {
            "alwaysShowBeforeRender" : true,
            "waiting" : true,
            "autoclose" : true,
            "delay" : 0
        },
        /* 模块配置 */
        "modules" : {
            "Barcode" : {},
            "VideoPlayer" : {},
            "Share" : {},
            "OAuth" : {}
        },
        /* 应用发布信息 */
        "distribute" : {
            /* android打包配置 */
            "android" : {
                "permissions" : [
                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
                    "<uses-feature android:name=\"android.hardware.camera\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
                ],
                "minSdkVersion" : 30,
                "targetSdkVersion" : 29
            },
            /* ios打包配置 */
            "ios" : {
                "dSYMs" : false,
                "capabilities" : {
                    "entitlements" : {
                        "com.apple.developer.associated-domains" : [
                            "applinks:static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com"
                        ]
                    }
                }
            },
            /* SDK配置 */
            "sdkConfigs" : {
                "push" : {},
                "share" : {
                    "weixin" : {
                        "appid" : "wx074c1f0cf5a0ecf4",
                        "UniversalLinks" : "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
                    }
                },
                "oauth" : {
                    "weixin" : {
                        "appid" : "wx074c1f0cf5a0ecf4",
                        "UniversalLinks" : "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
                    }
                },
                "payment" : {}
            },
            "icons" : {
                "android" : {
                    "hdpi" : "unpackage/res/icons/72x72.png",
                    "xhdpi" : "unpackage/res/icons/96x96.png",
                    "xxhdpi" : "unpackage/res/icons/144x144.png",
                    "xxxhdpi" : "unpackage/res/icons/192x192.png"
                },
                "ios" : {
                    "appstore" : "unpackage/res/icons/1024x1024.png",
                    "ipad" : {
                        "app" : "unpackage/res/icons/76x76.png",
                        "app@2x" : "unpackage/res/icons/152x152.png",
                        "notification" : "unpackage/res/icons/20x20.png",
                        "notification@2x" : "unpackage/res/icons/40x40.png",
                        "proapp@2x" : "unpackage/res/icons/167x167.png",
                        "settings" : "unpackage/res/icons/29x29.png",
                        "settings@2x" : "unpackage/res/icons/58x58.png",
                        "spotlight" : "unpackage/res/icons/40x40.png",
                        "spotlight@2x" : "unpackage/res/icons/80x80.png"
                    },
                    "iphone" : {
                        "app@2x" : "unpackage/res/icons/120x120.png",
                        "app@3x" : "unpackage/res/icons/180x180.png",
                        "notification@2x" : "unpackage/res/icons/40x40.png",
                        "notification@3x" : "unpackage/res/icons/60x60.png",
                        "settings@2x" : "unpackage/res/icons/58x58.png",
                        "settings@3x" : "unpackage/res/icons/87x87.png",
                        "spotlight@2x" : "unpackage/res/icons/80x80.png",
                        "spotlight@3x" : "unpackage/res/icons/120x120.png"
                    }
                }
            }
        }
    },
    /* 快应用特有相关 */
    "quickapp" : {},
    /* 小程序特有相关 */
    "mp-weixin" : {
        "appid" : "wxa17e8d1331e50934",
        "setting" : {
            "urlCheck" : false,
            "minified" : true
        },
        "usingComponents" : true
    },
    "mp-alipay" : {
        "usingComponents" : true
    },
    "mp-baidu" : {
        "usingComponents" : true
    },
    "mp-toutiao" : {
        "usingComponents" : true
    },
    "uniStatistics" : {
        "enable" : false
    },
    "vueVersion" : "3",
    "permission" : {
        "scope.userLocation" : {
            "desc" : "下载文件"
        }
    },
    "h5" : {
        "unipush" : {
            "enable" : false
        }
    },
    "_spaceID" : "mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8"
}
traditionalApp/pages/login/login.vue
@@ -4,46 +4,33 @@
        <view class="title">欢迎登录</view>
        <view class="tabs">
            <view class="tab" :class="loginType === 'code' ? 'active' : 'normal'" @tap="switchLoginType('code')">验证码登录</view>
            <view class="tab" :class="loginType === 'password' ? 'active' : 'normal'" @tap="switchLoginType('password')">密码登录</view>
            <view class="tab" :class="loginType === 'code' ? 'active' : 'normal'" @tap="switchLoginType('code')">验证码登录
            </view>
            <view class="tab" :class="loginType === 'password' ? 'active' : 'normal'"
                @tap="switchLoginType('password')">密码登录</view>
        </view>
        <view class="form">
            <view class="input-group">
                <input
                    class="input"
                    type="number"
                    maxlength="11"
                    v-model="phone"
                    placeholder="输入手机号"
                    placeholder-class="placeholder-style"
                />
                <view
                    class="get-code-btn"
                    :class="{ disabled: countdown > 0 }"
                    v-if="loginType === 'code'"
                    @tap="handleSendCode"
                >
                <input class="input" type="number" maxlength="11" v-model="phone" placeholder="输入手机号"
                    placeholder-class="placeholder-style" />
                <view class="get-code-btn" :class="{ disabled: countdown > 0 }" v-if="loginType === 'code'"
                    @tap="handleSendCode">
                    {{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
                </view>
            </view>
            <view class="input-group">
                <input
                    class="input"
                    :type="loginType === 'code' ? 'number' : 'text'"
                    :password="loginType === 'password'"
                    :maxlength="loginType === 'code' ? 6 : 32"
                    v-model="credential"
                    :placeholder="loginType === 'code' ? '输入验证码' : '输入密码'"
                    placeholder-class="placeholder-style"
                />
                <input class="input" :type="loginType === 'code' ? 'number' : 'text'"
                    :password="loginType === 'password'" :maxlength="loginType === 'code' ? 6 : 32" v-model="credential"
                    :placeholder="loginType === 'code' ? '输入验证码' : '输入密码'" placeholder-class="placeholder-style" />
            </view>
        </view>
        <view class="login-btn" @tap="handleLogin">{{ loading ? '登录中...' : '登录' }}</view>
        <view class="action-links" :class="{ 'center': loginType === 'code', 'space-between': loginType === 'password' }">
        <view class="action-links"
            :class="{ 'center': loginType === 'code', 'space-between': loginType === 'password' }">
            <text class="register-link" @tap="goRegister">没有账号?点击注册</text>
            <text class="forgot-password" v-if="loginType === 'password'" @tap="goForgotPassword">忘记密码?</text>
        </view>
@@ -56,558 +43,609 @@
</template>
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app'
import { onUnmounted, ref } from 'vue'
import { passwordLogin, sendSmsCode, smsLogin, wechatLogin } from './service.js'
    import { onLoad } from '@dcloudio/uni-app'
    import { onUnmounted, ref } from 'vue'
    import { passwordLogin, sendSmsCode, smsLogin, wechatLogin } from './service.js'
const loginType = ref<'code' | 'password'>('code')
const phone = ref('')
const credential = ref('')
const countdown = ref(0)
const loading = ref(false)
const inviteUserId = ref('')
const inviteType = ref('')
    const loginType = ref<'code' | 'password'>('code')
    const phone = ref('')
    const credential = ref('')
    const countdown = ref(0)
    const loading = ref(false)
    const inviteUserId = ref('')
    const inviteType = ref('')
let countdownTimer: ReturnType<typeof setInterval> | null = null
    let countdownTimer : ReturnType<typeof setInterval> | null = null
interface AppLoginUser {
    id: number
    nickname: string
    phone: string
    avatar?: string
    type?: number
    status?: number
    clinicId?: number
    memberExpireAt?: string
}
interface AppLoginPayload {
    token: string
    tokenType: string
    expiresIn: number
    user: AppLoginUser
}
interface ApiResponse<T> {
    code: number
    msg?: string
    message?: string
    data: T
}
interface AppWechatLoginPayload {
    code?: string
    accessToken?: string
    openid?: string
    unionid?: string
}
interface WechatAuthResult {
    access_token?: string
    accessToken?: string
    openid?: string
    openId?: string
    unionid?: string
    unionId?: string
}
interface WechatLoginResult {
    code?: string
    authResult?: WechatAuthResult
}
/**
 * 页面加载时记录来源参数,便于后续扩展回跳逻辑。
 */
onLoad((options) => {
    inviteUserId.value = options?.inviteUserId || ''
    inviteType.value = options?.inviteType || ''
})
/**
 * 页面销毁时清理倒计时,避免重复定时器泄漏。
 */
onUnmounted(() => {
    clearCountdown()
})
/**
 * 切换登录方式时重置当前输入态。
 */
const switchLoginType = (type: 'code' | 'password') => {
    loginType.value = type
    credential.value = ''
}
/**
 * 关闭登录页,优先返回上一页,没有页面栈时回首页。
 */
const handleClose = () => {
    if (getCurrentPages().length > 1) {
        uni.navigateBack()
        return
    interface AppLoginUser {
        id : number
        nickname : string
        phone : string
        avatar ?: string
        type ?: number
        status ?: number
        clinicId ?: number
        memberExpireAt ?: string
    }
    uni.switchTab({
        url: '/pages/home/home'
    })
}
/**
 * 跳转到注册页。
 */
const goRegister = () => {
    uni.navigateTo({
        url: '/pages/register/register'
    })
}
/**
 * 跳转到忘记密码页。
 */
const goForgotPassword = () => {
    uni.navigateTo({
        url: '/pages/forgotPassword/forgotPassword'
    })
}
/**
 * 执行微信登录,优先将微信授权结果交给后端拉取昵称和头像。
 */
const handleWechatLogin = async () => {
    if (loading.value) {
        return
    interface AppLoginPayload {
        token : string
        tokenType : string
        expiresIn : number
        user : AppLoginUser
    }
    // #ifdef APP-PLUS
    loading.value = true
    try {
        const loginRes = await requestWechatAuthorization()
        const payload = buildWechatLoginPayload(loginRes)
        if (!payload) {
    interface ApiResponse<T> {
        code : number
        msg ?: string
        message ?: string
        data : T
    }
    interface AppWechatLoginPayload {
        code ?: string
        accessToken ?: string
        openid ?: string
        unionid ?: string
    }
    interface WechatAuthResult {
        access_token ?: string
        accessToken ?: string
        openid ?: string
        openId ?: string
        unionid ?: string
        unionId ?: string
    }
    interface WechatLoginResult {
        code ?: string
        authResult ?: WechatAuthResult
    }
    /**
     * 页面加载时记录来源参数,便于后续扩展回跳逻辑。
     */
    onLoad((options) => {
        inviteUserId.value = options?.inviteUserId || ''
        inviteType.value = options?.inviteType || ''
    })
    /**
     * 页面销毁时清理倒计时,避免重复定时器泄漏。
     */
    onUnmounted(() => {
        clearCountdown()
    })
    /**
     * 切换登录方式时重置当前输入态。
     */
    const switchLoginType = (type : 'code' | 'password') => {
        loginType.value = type
        credential.value = ''
    }
    /**
     * 关闭登录页,优先返回上一页,没有页面栈时回首页。
     */
    const handleClose = () => {
        if (getCurrentPages().length > 1) {
            uni.navigateBack()
            return
        }
        const res = await wechatLogin(payload)
        if (!isSuccessResponse(res)) {
        uni.switchTab({
            url: '/pages/home/home'
        })
    }
    /**
     * 跳转到注册页。
     */
    const goRegister = () => {
        uni.navigateTo({
            url: '/pages/register/register'
        })
    }
    /**
     * 跳转到忘记密码页。
     */
    const goForgotPassword = () => {
        uni.navigateTo({
            url: '/pages/forgotPassword/forgotPassword'
        })
    }
    /**
     * 执行微信登录,优先将微信授权结果交给后端拉取昵称和头像。
     */
    const handleWechatLogin = async () => {
        if (loading.value) {
            return
        }
        saveLoginSession(res.data)
        uni.showToast({
            title: '登录成功',
            icon: 'success'
        })
        setTimeout(() => {
            uni.switchTab({
                url: '/pages/home/home'
            })
        }, 300)
    } catch (error) {
        uni.showToast({
            title: '微信登录失败',
            icon: 'none'
        })
    } finally {
        loading.value = false
    }
    // #endif
    // #ifndef APP-PLUS
    uni.showToast({
        title: '请在App中使用微信登录',
        icon: 'none'
    })
    // #endif
}
/**
 * 拉起微信授权,获取后端换取用户资料所需的授权结果。
 */
const requestWechatAuthorization = () => {
    return new Promise<WechatLoginResult>((resolve, reject) => {
        uni.login({
            provider: 'weixin',
            success: (res) => {
                resolve(res as WechatLoginResult)
            },
            fail: (error) => {
                reject(error)
        // #ifdef APP-PLUS
        loading.value = true
        try {
            const loginRes = await requestWechatAuthorization()
            const payload = buildWechatLoginPayload(loginRes)
            if (!payload) {
                return
            }
            const res = await wechatLogin(payload)
            if (!isSuccessResponse(res)) {
                return
            }
            saveLoginSession(res.data)
            uni.showToast({
                title: '登录成功',
                icon: 'success'
            })
            setTimeout(() => {
                uni.switchTab({
                    url: '/pages/home/home'
                })
            }, 300)
        } catch (error: any) {
            const errorCode = error?.code
            const errorMessage = typeof error?.errMsg === 'string' ? error.errMsg : ''
            uni.showToast({
                title: errorCode === -7
                    ? '请重新云打包,并检查 manifest 中微信登录配置'
                    : (errorMessage || '微信登录失败'),
                icon: 'none'
            })
        } finally {
            loading.value = false
        }
        // #endif
        // #ifndef APP-PLUS
        uni.showToast({
            title: '请在App中使用微信登录',
            icon: 'none'
        })
    })
}
/**
 * 从 uni.login 返回结果中提取微信登录接口需要的参数。
 */
const buildWechatLoginPayload = (loginRes: WechatLoginResult) => {
    const authResult = loginRes?.authResult || {}
    const payload: AppWechatLoginPayload = {
        code: normalizeOptionalText(loginRes?.code),
        accessToken: normalizeOptionalText(authResult.access_token || authResult.accessToken),
        openid: normalizeOptionalText(authResult.openid || authResult.openId),
        unionid: normalizeOptionalText(authResult.unionid || authResult.unionId)
        // #endif
    }
    // 当前阶段优先兼容 APP 端直接返回的 accessToken/openid,后续补齐配置后也可走 code 模式。
    if ((payload.accessToken && payload.openid) || payload.code) {
        return payload
    /**
     * 拉起微信授权,获取后端换取用户资料所需的授权结果。
     */
    const requestWechatAuthorization = () => {
        return new Promise<WechatLoginResult>((resolve, reject) => {
            // #ifdef APP-PLUS
            plus.oauth.getServices((services) => {
                const weixinService = services.find((service) => service.id === 'weixin')
                if (!weixinService) {
                    reject({
                        code: -3,
                        errMsg: '当前环境不支持微信登录'
                    })
                    return
                }
                if (!weixinService.nativeClient) {
                    reject({
                        code: -8,
                        errMsg: '请先安装微信客户端'
                    })
                    return
                }
                weixinService.authorize((event : any) => {
                    const code = normalizeOptionalText(event?.code)
                    if (!code) {
                        reject({
                            code: -99,
                            errMsg: '微信授权未返回有效code'
                        })
                        return
                    }
                    resolve({
                        code
                    })
                }, (error : any) => {
                    reject({
                        code: error?.code,
                        errMsg: error?.message || '微信授权失败'
                    })
                }, {
                    scope: 'snsapi_userinfo',
                    state: 'login'
                })
            }, (error : any) => {
                reject({
                    code: error?.code,
                    errMsg: error?.message || '获取微信登录服务失败'
                })
            })
            return
            // #endif
            uni.login({
                provider: 'weixin',
                success: (res) => {
                    resolve(res as WechatLoginResult)
                },
                fail: (error) => {
                    reject(error)
                }
            })
        })
    }
    uni.showToast({
        title: '微信授权信息缺失',
        icon: 'none'
    })
    return null
}
    /**
     * 从 uni.login 返回结果中提取微信登录接口需要的参数。
     */
    const buildWechatLoginPayload = (loginRes : WechatLoginResult) => {
        const authResult = loginRes?.authResult || {}
        const payload : AppWechatLoginPayload = {
            code: normalizeOptionalText(loginRes?.code),
            accessToken: normalizeOptionalText(authResult.access_token || authResult.accessToken),
            openid: normalizeOptionalText(authResult.openid || authResult.openId),
            unionid: normalizeOptionalText(authResult.unionid || authResult.unionId)
        }
/**
 * 发送短信验证码并启动倒计时。
 */
const handleSendCode = async () => {
    if (countdown.value > 0 || loading.value) {
        return
    }
    const normalizedPhone = normalizePhone()
    if (!normalizedPhone) {
        return
    }
    const res = await sendSmsCode({ phone: normalizedPhone })
    if (!isSuccessResponse(res)) {
        return
    }
    startCountdown()
    uni.showToast({
        title: '验证码已发送',
        icon: 'success'
    })
}
        // 当前阶段优先兼容 APP 端直接返回的 accessToken/openid,后续补齐配置后也可走 code 模式。
        if ((payload.accessToken && payload.openid) || payload.code) {
            return payload
        }
/**
 * 执行登录动作,根据当前 tab 走验证码登录或密码登录。
 */
const handleLogin = async () => {
    if (loading.value) {
        return
        uni.showToast({
            title: '微信授权信息缺失',
            icon: 'none'
        })
        return null
    }
    const normalizedPhone = normalizePhone()
    if (!normalizedPhone) {
        return
    }
    loading.value = true
    try {
        const res = loginType.value === 'code'
            ? await loginWithCode(normalizedPhone)
            : await loginWithPassword(normalizedPhone)
        if (!res) {
    /**
     * 发送短信验证码并启动倒计时。
     */
    const handleSendCode = async () => {
        if (countdown.value > 0 || loading.value) {
            return
        }
        const normalizedPhone = normalizePhone()
        if (!normalizedPhone) {
            return
        }
        const res = await sendSmsCode({ phone: normalizedPhone })
        if (!isSuccessResponse(res)) {
            return
        }
        saveLoginSession(res.data)
        startCountdown()
        uni.showToast({
            title: '登录成功',
            title: '验证码已发送',
            icon: 'success'
        })
        setTimeout(() => {
            uni.switchTab({
                url: '/pages/home/home'
            })
        }, 300)
    } finally {
        loading.value = false
    }
}
/**
 * 使用短信验证码完成登录。
 */
const loginWithCode = (normalizedPhone: string) => {
    const normalizedCode = normalizeCode()
    if (!normalizedCode) {
        return null
    }
    return smsLogin({
        phone: normalizedPhone,
        code: normalizedCode
    })
}
/**
 * 使用手机号密码完成登录。
 */
const loginWithPassword = (normalizedPhone: string) => {
    const normalizedPassword = normalizePassword()
    if (!normalizedPassword) {
        return null
    }
    return passwordLogin({
        phone: normalizedPhone,
        password: normalizedPassword
    })
}
/**
 * 将登录响应写入本地缓存,供后续请求与页面读取。
 */
const saveLoginSession = (loginData: AppLoginPayload) => {
    if (!loginData) {
        return
    }
    const authToken = `${loginData.tokenType || 'Bearer'} ${loginData.token || ''}`.trim()
    uni.setStorageSync('token', authToken)
    uni.setStorageSync('appUserInfo', loginData.user || {})
    uni.setStorageSync('showLogoutModal', 1)
    // 先保留来源参数,后续如需登录后回跳可直接复用。
    uni.setStorageSync('loginRedirectMeta', {
        inviteUserId: inviteUserId.value,
        inviteType: inviteType.value
    })
}
/**
 * 校验手机号输入是否合法。
 */
const normalizePhone = () => {
    const normalizedPhone = (phone.value || '').trim()
    if (!/^1\d{10}$/.test(normalizedPhone)) {
        uni.showToast({
            title: '请输入正确的手机号',
            icon: 'none'
        })
        return ''
    }
    return normalizedPhone
}
/**
 * 校验验证码输入是否合法。
 */
const normalizeCode = () => {
    const normalizedCode = (credential.value || '').trim()
    if (!/^\d{6}$/.test(normalizedCode)) {
        uni.showToast({
            title: '请输入6位验证码',
            icon: 'none'
        })
        return ''
    }
    return normalizedCode
}
/**
 * 校验密码输入是否合法。
 */
const normalizePassword = () => {
    const normalizedPassword = (credential.value || '').trim()
    if (!normalizedPassword) {
        uni.showToast({
            title: '请输入密码',
            icon: 'none'
        })
        return ''
    }
    return normalizedPassword
}
/**
 * 将可选字符串参数归一化为空串或有效文本。
 */
const normalizeOptionalText = (value: unknown) => {
    if (typeof value !== 'string') {
        return ''
    }
    return value.trim()
}
/**
 * 判断接口响应是否为成功态。
 */
const isSuccessResponse = (response: ApiResponse<unknown> | undefined) => {
    return !!response && (response.code === 0 || response.code === 200)
}
/**
 * 启动 60 秒验证码发送倒计时。
 */
const startCountdown = () => {
    clearCountdown()
    countdown.value = 60
    countdownTimer = setInterval(() => {
        if (countdown.value <= 1) {
            clearCountdown()
    /**
     * 执行登录动作,根据当前 tab 走验证码登录或密码登录。
     */
    const handleLogin = async () => {
        if (loading.value) {
            return
        }
        countdown.value -= 1
    }, 1000)
}
/**
 * 清理验证码倒计时。
 */
const clearCountdown = () => {
    if (countdownTimer) {
        clearInterval(countdownTimer)
        countdownTimer = null
        const normalizedPhone = normalizePhone()
        if (!normalizedPhone) {
            return
        }
        loading.value = true
        try {
            const res = loginType.value === 'code'
                ? await loginWithCode(normalizedPhone)
                : await loginWithPassword(normalizedPhone)
            if (!res) {
                return
            }
            if (!isSuccessResponse(res)) {
                return
            }
            saveLoginSession(res.data)
            uni.showToast({
                title: '登录成功',
                icon: 'success'
            })
            setTimeout(() => {
                uni.switchTab({
                    url: '/pages/home/home'
                })
            }, 300)
        } finally {
            loading.value = false
        }
    }
    countdown.value = 0
}
    /**
     * 使用短信验证码完成登录。
     */
    const loginWithCode = (normalizedPhone : string) => {
        const normalizedCode = normalizeCode()
        if (!normalizedCode) {
            return null
        }
        return smsLogin({
            phone: normalizedPhone,
            code: normalizedCode
        })
    }
    /**
     * 使用手机号密码完成登录。
     */
    const loginWithPassword = (normalizedPhone : string) => {
        const normalizedPassword = normalizePassword()
        if (!normalizedPassword) {
            return null
        }
        return passwordLogin({
            phone: normalizedPhone,
            password: normalizedPassword
        })
    }
    /**
     * 将登录响应写入本地缓存,供后续请求与页面读取。
     */
    const saveLoginSession = (loginData : AppLoginPayload) => {
        if (!loginData) {
            return
        }
        const authToken = `${loginData.tokenType || 'Bearer'} ${loginData.token || ''}`.trim()
        uni.setStorageSync('token', authToken)
        uni.setStorageSync('appUserInfo', loginData.user || {})
        uni.setStorageSync('showLogoutModal', 1)
        // 先保留来源参数,后续如需登录后回跳可直接复用。
        uni.setStorageSync('loginRedirectMeta', {
            inviteUserId: inviteUserId.value,
            inviteType: inviteType.value
        })
    }
    /**
     * 校验手机号输入是否合法。
     */
    const normalizePhone = () => {
        const normalizedPhone = (phone.value || '').trim()
        if (!/^1\d{10}$/.test(normalizedPhone)) {
            uni.showToast({
                title: '请输入正确的手机号',
                icon: 'none'
            })
            return ''
        }
        return normalizedPhone
    }
    /**
     * 校验验证码输入是否合法。
     */
    const normalizeCode = () => {
        const normalizedCode = (credential.value || '').trim()
        if (!/^\d{6}$/.test(normalizedCode)) {
            uni.showToast({
                title: '请输入6位验证码',
                icon: 'none'
            })
            return ''
        }
        return normalizedCode
    }
    /**
     * 校验密码输入是否合法。
     */
    const normalizePassword = () => {
        const normalizedPassword = (credential.value || '').trim()
        if (!normalizedPassword) {
            uni.showToast({
                title: '请输入密码',
                icon: 'none'
            })
            return ''
        }
        return normalizedPassword
    }
    /**
     * 将可选字符串参数归一化为空串或有效文本。
     */
    const normalizeOptionalText = (value : unknown) => {
        if (typeof value !== 'string') {
            return ''
        }
        return value.trim()
    }
    /**
     * 判断接口响应是否为成功态。
     */
    const isSuccessResponse = (response : ApiResponse<unknown> | undefined) => {
        return !!response && (response.code === 0 || response.code === 200)
    }
    /**
     * 启动 60 秒验证码发送倒计时。
     */
    const startCountdown = () => {
        clearCountdown()
        countdown.value = 60
        countdownTimer = setInterval(() => {
            if (countdown.value <= 1) {
                clearCountdown()
                return
            }
            countdown.value -= 1
        }, 1000)
    }
    /**
     * 清理验证码倒计时。
     */
    const clearCountdown = () => {
        if (countdownTimer) {
            clearInterval(countdownTimer)
            countdownTimer = null
        }
        countdown.value = 0
    }
</script>
<style></style>
<style lang="scss" scoped>
.container {
    padding: 0 40rpx;
    padding-top: calc(var(--status-bar-height, 0px) + 151rpx);
    box-sizing: border-box;
    min-height: 100vh;
    background-color: #fff;
    background-image: url('https://mxcd.zhinenganguan.com/file/2026-03-26/6b53248a-b511-4ffd-84e7-e86f3ef0e774.png');
    background-size: 100% 100%;
    .container {
        padding: 0 40rpx;
        padding-top: calc(var(--status-bar-height, 0px) + 151rpx);
        box-sizing: border-box;
        min-height: 100vh;
        background-color: #fff;
        background-image: url('https://mxcd.zhinenganguan.com/file/2026-03-26/6b53248a-b511-4ffd-84e7-e86f3ef0e774.png');
        background-size: 100% 100%;
    background-repeat: no-repeat;
    position: relative;
}
.close-icon {
    position: absolute;
    left: 24rpx;
    top: calc(var(--status-bar-height, 0px) + 65rpx);
    width: 40rpx;
    height: 40rpx;
    z-index: 10;
    display: block;
}
.title {
    font-size: 68rpx;
    line-height: 84rpx;
    color: #010F07;
    font-weight: bold;
}
.tabs {
    margin-top: 64rpx;
    display: flex;
    align-items: center;
    .tab {
        flex: 1;
        height: 108rpx;
        line-height: 108rpx;
        text-align: center;
        border-radius: 16rpx;
        font-size: 32rpx;
        &.active {
            background-color: #C63520;
            color: #fff;
            margin-right: 30rpx;
        }
        &.normal {
            background-color: #ffffff;
            color: #677294;
            box-shadow: 0px 0px 44px 0px rgba(0, 0, 0, 0.06);
        }
        background-repeat: no-repeat;
        position: relative;
    }
}
.form {
    margin-top: 60rpx;
    .close-icon {
        position: absolute;
        left: 24rpx;
        top: calc(var(--status-bar-height, 0px) + 65rpx);
        width: 40rpx;
        height: 40rpx;
        z-index: 10;
        display: block;
    }
    .input-group {
        width: 670rpx;
        height: 108rpx;
        border-radius: 16rpx;
        border: 1px solid #EEEEEE;
    .title {
        font-size: 68rpx;
        line-height: 84rpx;
        color: #010F07;
        font-weight: bold;
    }
    .tabs {
        margin-top: 64rpx;
        display: flex;
        align-items: center;
        margin-bottom: 30rpx;
        box-sizing: border-box;
        .input {
        .tab {
            flex: 1;
            height: 100%;
            padding-left: 48rpx;
            font-size: 32rpx;
            color: #333;
        }
        .get-code-btn {
            width: 178rpx;
            height: 56rpx;
            line-height: 56rpx;
            height: 108rpx;
            line-height: 108rpx;
            text-align: center;
            background-color: #C63520;
            color: #fff;
            font-size: 26rpx;
            border-radius: 8rpx;
            margin-right: 24rpx;
            border-radius: 16rpx;
            font-size: 32rpx;
            &.disabled {
                background-color: #D9D9D9;
            &.active {
                background-color: #C63520;
                color: #fff;
                margin-right: 30rpx;
            }
            &.normal {
                background-color: #ffffff;
                color: #677294;
                box-shadow: 0px 0px 44px 0px rgba(0, 0, 0, 0.06);
            }
        }
    }
}
.placeholder-style {
    color: #868686;
    font-size: 32rpx;
}
    .form {
        margin-top: 60rpx;
.login-btn {
    width: 670rpx;
    height: 96rpx;
    line-height: 96rpx;
    text-align: center;
    background-color: #C63520;
    color: #fff;
    font-size: 36rpx;
    border-radius: 16rpx;
    margin-top: 48rpx;
}
        .input-group {
            width: 670rpx;
            height: 108rpx;
            border-radius: 16rpx;
            border: 1px solid #EEEEEE;
            display: flex;
            align-items: center;
            margin-bottom: 30rpx;
            box-sizing: border-box;
.action-links {
    margin-top: 88rpx;
    display: flex;
    width: 100%;
    box-sizing: border-box;
    padding: 0 20rpx;
            .input {
                flex: 1;
                height: 100%;
                padding-left: 48rpx;
                font-size: 32rpx;
                color: #333;
            }
    &.center {
            .get-code-btn {
                width: 178rpx;
                height: 56rpx;
                line-height: 56rpx;
                text-align: center;
                background-color: #C63520;
                color: #fff;
                font-size: 26rpx;
                border-radius: 8rpx;
                margin-right: 24rpx;
                &.disabled {
                    background-color: #D9D9D9;
                }
            }
        }
    }
    .placeholder-style {
        color: #868686;
        font-size: 32rpx;
    }
    .login-btn {
        width: 670rpx;
        height: 96rpx;
        line-height: 96rpx;
        text-align: center;
        background-color: #C63520;
        color: #fff;
        font-size: 36rpx;
        border-radius: 16rpx;
        margin-top: 48rpx;
    }
    .action-links {
        margin-top: 88rpx;
        display: flex;
        width: 100%;
        box-sizing: border-box;
        padding: 0 20rpx;
        &.center {
            justify-content: center;
        }
        &.space-between {
            justify-content: space-between;
        }
        .register-link,
        .forgot-password {
            font-size: 28rpx;
            color: #C63520;
        }
    }
    .wechat-btn {
        width: 400rpx;
        height: 88rpx;
        margin: 100rpx auto 0;
        display: flex;
        align-items: center;
        justify-content: center;
    }
        border: 2px solid #EEEEEE;
        border-radius: 16rpx;
        box-sizing: border-box;
    &.space-between {
        justify-content: space-between;
    }
        .wechat-icon {
            width: 56rpx;
            height: 56rpx;
            margin-right: 16rpx;
        }
    .register-link,
    .forgot-password {
        font-size: 28rpx;
        color: #C63520;
        .wechat-text {
            font-size: 30rpx;
            color: #333333;
        }
    }
}
.wechat-btn {
    width: 400rpx;
    height: 88rpx;
    margin: 100rpx auto 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px solid #EEEEEE;
    border-radius: 16rpx;
    box-sizing: border-box;
    .wechat-icon {
        width: 56rpx;
        height: 56rpx;
        margin-right: 16rpx;
    }
    .wechat-text {
        font-size: 30rpx;
        color: #333333;
    }
}
</style>