code-chat-bug-analysis-task.md 38 KB

编译错误修复任务

任务描述

下面的错误是uni-app项目调试运行到 [android] 平台时的编译阶段出现的,你需要分析以下报错信息并修复相应的错误。

uni_modules

如果需要修复 `找不到uni_modules的模块` 错误时,请参考以下模块列表:
android-keeplive

lime-color lime-date-time-picker lime-dayuts lime-echart lime-icon lime-input lime-loading lime-overlay lime-picker lime-popup lime-shared lime-style lime-textarea lime-transition tq-encrypt tui-xechars uni-captcha uni-config-center uni-icons uni-id-common uni-id-pages uni-load-more uni-scss uni-sign-in uni-upgrade-center-app uts-openSchema uts-progressNotification

修复要求(必须严格遵守)

  1. 你的任务是仅修复编译错误,禁止修改与错误无关的代码。
  2. 简洁易懂,复杂的代码配上中文注释.
  3. 不使用变量和函数的声明提升,严格的在清晰的范围内和正确的顺序中使用变量和函数.
  4. 当生成某个平台专用代码时,应使用条件编译进行平台约束,避免干扰其他平台.
  5. 修复完成后,需要使用中文向用户说明修复了哪些错误、发生错误的原因以及修复错误的思路.

第 1 个错误

  • 文件路径 pages/login/index.uvue

  • 文件内容

    <template>
    <view class="login-page">
        <!-- 背景图 -->
        <image class="bg-image" src="/static/images/login/1.png" mode="aspectFill"></image>
    
        <scroll-view class="login-content">
            <!-- Logo 和标题 -->
            <view class="header">
                <image class="logo" src="/static/images/index/logo.png" mode="aspectFit"></image>
                <text class="title">EMCS</text>
            </view>
    
            <!-- 登录表单卡片 -->
            <view class="form-card">
                <!-- 账号输入 -->
                <view class="form-item">
                    <text class="label">账号</text>
                    <input class="input" type="text" placeholder="请输入您的账号" v-model="username" />
                </view>
    
                <!-- 密码输入 -->
                <view class="form-item">
                    <text class="label">密码</text>
                    <view class="input-wrapper">
                        <input class="input" :type="showPassword ? 'text' : 'password'" placeholder="请输入您的密码" v-model="password" />
                        <image class="eye-icon" :src="showPassword ? '/static/images/login/4.png' : '/static/images/login/3.png'" mode="aspectFit" @click="handleTogglePassword"></image>
                    </view>
                </view>
    
                <!-- 记住密码 (可点击区域切换) -->
                <view class="remember-box" @click="handleRememberChange" style="cursor: pointer;">
                    <checkbox :checked="rememberPassword" class="checkbox" />
                    <text class="remember-text" style="margin-left: 8px;">记住密码</text>
                </view>
    
                <!-- 登录按钮 -->
                <button class="login-btn"  @click="handleLogin">
                    <text class="login-btn-text">{{ loading ? "登录中..." : "登录" }}</text>
                </button>
            </view>
    
            <!-- 底部信息 -->
            <view class="footer">
                <text class="version">v{{ version }}</text>
                <text class="company">韫珠科技</text>
            </view>
        </scroll-view>
    		
    		<!-- 自定义选择器弹窗 -->
    		<view v-if="showPasswordPicker" class="picker-modal">
    		    <view class="modal-mask" @click="showPasswordPicker = false"></view>
    		    <view class="modal-content">
    		        <view class="modal-header">
    		            <text class="modal-title">修改密码</text>
    		            <text class="modal-close" @click="showPasswordPicker = false">取消</text>
    		        </view>
    				<view class="form-item-input">
    				    <text class="label-picker">新密码</text>
    				    <view class="view-input-picker">
    				        <input class="input-picker" :type="showNewPassword ? 'text' : 'password'" placeholder="请输入新密码" v-model="newpassword" />
    				        <image class="eye-icon-picker" :src="showNewPassword ? '/static/images/login/4.png' : '/static/images/login/3.png'" mode="aspectFit" @click="handleToggleNewPassword"></image>
    				    </view>
    				</view>
    				<view class="form-item-input">
    				    <text class="label-picker">确认密码</text>
    				    <view class="view-input-picker">
    				        <input class="input-picker" :type="showConfirmPassword ? 'text' : 'password'" placeholder="请输入确认密码" v-model="confirmpassword" />
    				        <image class="eye-icon-picker" :src="showConfirmPassword ? '/static/images/login/4.png' : '/static/images/login/3.png'" mode="aspectFit" @click="handleToggleConfirmPassword"></image>
    				    </view>
    				</view>
    				<view class="form-item-btn">
    					<button class="btn-primary"  @click="handleRestPassword">
    						确认
    					</button>
    				</view>
    		    </view>
    		</view>
    		
    		<!-- 下载进度遮罩(仅更新数据不重建,无闪烁) -->
    		<view v-if="isDownload" class="download-progress-mask">
    		    <view class="download-progress-box">
    		        <text class="download-progress-title">下载中...</text>
    		        <view class="download-progress-bar">
    		            <view class="download-progress-fill" :style="{ width: downloadProgress + '%' }"></view>
    		        </view>
    		        <text class="download-progress-text">{{ downloadProgress }}%</text>
    		    </view>
    		</view>
    		
    </view>
    </template>
    
    <script setup lang="uts">
    import { ref, computed, onMounted } from 'vue'
    import { loginByAccount, loginSSO, getUserInfo, resetPassword , getIsKey} from '../../api/auth/login'
    	import { getVersion } from '../../api/system/config.uts'
    	import { getBaseUrl } from '../../utils/request'
    	
    import {
        saveAccessToken,
        saveUserInfo,
        getRememberedAccount,
        saveRememberedAccount,
        clearRememberedAccount,
    		saveStoreIsKey,
    		getStoreIsKey
    } from '../../utils/storage'
    import { validatePassword } from '../../utils/validate'
    // @ts-ignore
    import manifest from '@/manifest.json'
    
    // 表单数据
    const username = ref<string>("")
    const password = ref<string>("")
    	const newpassword = ref<string>("")
    	const confirmpassword = ref<string>("")
    const rememberPassword = ref<boolean>(false)
    const showPassword = ref<boolean>(false)
    	const showNewPassword = ref<boolean>(false)
    	const showConfirmPassword = ref<boolean>(false)
    const loading = ref<boolean>(false)
    	const isDownload = ref<boolean>(false)
    	const downloadProgress = ref<number>(0)
    	const showPasswordPicker = ref<boolean>(false)
    
    // 版本号
    const manifestVersion = manifest.versionName as string | null
    const version = ref<string>(manifestVersion != null ? manifestVersion : '1.0.0')
    	const versionServer = ref<string>('1.0.0')
    	
    // 是否可以登录
    const canLogin = computed((): boolean => {
        return username.value.length > 0 && password.value.length > 0 && !loading.value
    })
    
    // 记住密码切换
    const handleRememberChange = (): void => {
        rememberPassword.value = !rememberPassword.value
    }
    
    // 切换密码显示/隐藏
    const handleTogglePassword = (): void => {
        showPassword.value = !showPassword.value
    }
    	
    	// 切换密码显示/隐藏
    	const handleToggleNewPassword = (): void => {
    	    showNewPassword.value = !showNewPassword.value
    	}
    	
    	// 切换密码显示/隐藏
    	const handleToggleConfirmPassword = (): void => {
    	    showConfirmPassword.value = !showConfirmPassword.value
    	}
    
    	const handleRestPassword = async (): Promise<void> => {
    		try {
    			if(newpassword.value != confirmpassword.value){
    				uni.showToast({
    					title: '两次输入的密码不一致',
    					icon: 'error',
    					duration: 3000
    				})
    				return;
    			}
    			await resetPassword(username.value, password.value, newpassword.value);
    			showPasswordPicker.value = false;
    			uni.showToast({
    				title: '修改成功,请重新登录',
    				icon: 'success',
    				duration: 3000
    			})
    			newpassword.value = ""
    			confirmpassword.value = "";
    		} catch (e: any) {
    		    uni.showToast({
    		        title: e.message ?? '密码修改失败',
    		        icon: 'none',
    		        duration: 2000
    		    })
    		}
    	};
    	
    	
    	const loginSuccess = async(result: any) : Promise<void> => {
    		// 提取 data 部分
    		const resultObj = result as UTSJSONObject
    		// const data = resultObj['data'] as UTSJSONObject
    				
    		const isInitPassword = resultObj["isInitPassword"] as boolean | null;
    		// console.log("================"+ code)
    		if(isInitPassword==true){
    			showPasswordPicker.value = true;
    			return
    		}
    		
    		uni.setStorageSync("login_key", "1")
    		
    		// 保存登录信息
    		saveAccessToken(resultObj['token'] as string)
    		
    		const userInfoJson = await getUserInfo();
    		console.log(userInfoJson);
    		
    		const userInfoObj = userInfoJson as UTSJSONObject
    		const userInfo = userInfoObj['user'] as UTSJSONObject
    		const deptInfo = userInfo['dept'] as UTSJSONObject
    		const permissions = userInfoObj['permissions'] as any[]
    		saveUserInfo({
    		    userId: userInfo['userId'],
    		    userName: userInfo['userName'],
    			nickName: userInfo['nickName'],
    			phone: userInfo['phonenumber'],
    			deptName: deptInfo['deptName'],
    			roleNames: userInfoObj['roleNames'],
    			permissions: permissions
    		})
    		
    		// 保存或清除记住的账号密码
    		if (rememberPassword.value) {
    		    saveRememberedAccount(username.value, password.value)
    		} else {
    		    clearRememberedAccount()
    		}
    
    		// 跳转到首页
    		/*setTimeout(() => {
    		    uni.redirectTo({
    		        url: '/pages/index/index'
    		    })
    		}, 1000)*/
    		setTimeout(() => {
            uni.redirectTo({
                url: '/pages/index/index'
            })
        }, 1000)
    	}
    	
    // 登录处理
    const handleLogin = async (): Promise<void> => {
    
        // 验证输入
        if (username.value.trim().length == 0) {
            uni.showToast({
                title: '请输入账号',
                icon: 'none',
                duration: 2000
            })
            return
        }
    
        if (password.value.trim().length == 0) {
            uni.showToast({
                title: '请输入密码',
                icon: 'none',
                duration: 2000
            })
            return
        }
    
        try {
            loading.value = true
    
    			/* const resultKey = await getIsKey();
    			const resultKeyObj = resultKey as UTSJSONObject
    			const isKey = resultKeyObj["data"] as string |'0'
    			saveStoreIsKey(isKey); */
    
            const result = await loginByAccount(username.value, password.value)
            loginSuccess(result);
    		
    			uni.showToast({
    				title: '登录成功',
    				icon: 'success',
    				duration: 3000
    			})
    		
        } catch (e: any) {
            uni.showToast({
                title: e.message ?? '登录失败',
                icon: 'none',
                duration: 2000
            })
        } finally {
            loading.value = false
        }
    }
    	
    	const handleLoginSSO = async (apptoken:string): Promise<void> => {
    		try {
    			/* const resultKey = await getIsKey();
    			const resultKeyObj = resultKey as UTSJSONObject
    			const isKey = resultKeyObj["data"] as string |'0'
    			saveStoreIsKey(isKey); */
    			
    			let result = await loginSSO(apptoken)
    			console.log('自动登录:', result)
    			loginSuccess(result);
    		} catch (e: any) {
    		    uni.showToast({
    		        title: e.message ?? '登录失败',
    		        icon: 'none',
    		        duration: 2000
    		    })
    		} finally {
    		    loading.value = false
    		}
    	}
    	
    	const compareVersion = (newVersion: string, currentVersion: string): boolean => {
    		const newVer = newVersion.split('.').map(item => parseInt(item))
    		const currentVer = currentVersion.split('.').map(item => parseInt(item))
    		
    		const maxLength = Math.max(newVer.length, currentVer.length)
    		
    		for (let i = 0; i < maxLength; i++) {
    			const newNum = i < newVer.length ? newVer[i] : 0
    			const currentNum = i < currentVer.length ? currentVer[i] : 0
    			
    			if (newNum > currentNum) {
    				return true
    			} else if (newNum < currentNum) {
    				return false
    			}
    		}
    		return false
    	}
    	
    	// 安装APK的单独函数
    	const installApkFile = (filePath: string): void => {
    	  uni.installApk({
    		filePath: filePath,
    		success: () => {
    				  console.log('安装成功');
    				  uni.showToast({
    					title: '安装成功',
    					icon: 'success',
    					duration: 3000
    				  });
    				},
    				fail: (error) => {
    				  console.error('安装失败:', error);
    				  uni.showToast({
    					title: '安装失败',
    					icon: 'none',
    					duration: 3000
    				  });
    				},
    		complete: (res) => {
    		  console.log('安装完成:', res);
    		}
    	  });
    	}
    	
    	const installApkWithProgress = (): void => {		
    	    // #ifdef APP-ANDROID
    			isDownload.value = true;
    			downloadProgress.value = 0;
    			let donwloadUrl = '';
    			donwloadUrl = getBaseUrl() + '/profile/app/emcs-release-'+versionServer.value+'.apk'
    		
    			// 下载APK
    			const downloadTask = uni.downloadFile({
    				url: donwloadUrl,
    				filePath: `${uni.env.USER_DATA_PATH}/${Date.now()}_test.apk`, // 使用时间戳防止重名
    				success: (downloadRes) => {
    					isDownload.value = false;
    					if (downloadRes.statusCode == 200) {
    					// 确认安装
    						uni.showModal({
    						  title: '安装提示',
    						  content: '下载完成,是否立即安装?',
    						  success: (modalRes) => {
    							if (modalRes.confirm) {
    							  installApkFile(downloadRes.tempFilePath);
    							}
    						  }
    						});
    					} else {
    						uni.showToast({
    						  title: '下载失败',
    						  icon: 'error',
    						  duration: 3000
    						});
    					  }
    				},
    				fail: (error) => {
    				  isDownload.value = false;
    				  uni.showToast({
    						title: '下载失败',
    						icon: 'none',
    						duration: 3000
    					  });
    				  console.error('下载失败:', error);
    				}
    			});
    			// 监听下载进度(更新页面内进度条,节流 5% 减轻渲染压力)
    			let lastProgress = -5;
    			downloadTask.onProgressUpdate((res) => {
    				const p = Math.round(res.progress);
    				if (p - lastProgress >= 5 || p >= 100) {
    					lastProgress = p;
    					downloadProgress.value = p;
    				}
    			});
    		// #endif
    		// #ifdef APP-HARMONY
    			uni.showToast({
    				title: '请登录PC端,扫描二维码下载并安装',
    				icon: 'none',
    				duration: 3000
    			});
    		// #endif
    		// #ifdef APP-IOS
    			uni.showToast({
    				title: '请登录PC端,扫描二维码下载并安装',
    				icon: 'none',
    				duration: 3000
    			});
    		// #endif
    	}
    	
    	const checkVersion = async (): Promise<void> => {
    		const versionJSON = await getVersion() as UTSJSONObject
    		versionServer.value = versionJSON['msg'] as string
    		const hasNewVersion = compareVersion(versionServer.value, version.value) // true
    		console.log("versionServer:"+versionServer.value)
    		console.log("hasNewVersion:"+hasNewVersion)
    		if(hasNewVersion){
    			installApkWithProgress();
    		}
    	}
    	
    	onLoad((options: UTSJSONObject | null) => {
    	    console.log('URL参数:', options)
    		 // 延迟一点执行,确保App.vue已保存到缓存
    		setTimeout(() => {
    			// 2. 从页面参数获取
    			if (options != null) {
    				const apptokenValue = options['apptoken']
    				if (apptokenValue != null) {
    					if (typeof apptokenValue == 'string') {
    						if(apptokenValue.length > 0){
    							const apptoken = apptokenValue as string
    							console.log('获取到自动登录apptoken:', apptoken)
    							handleLoginSSO(apptoken);
    						}
    					}
    				}
    			}
    		}, 500)
    	})
    
    // 初始化:加载记住的账号密码
    onMounted(() => {
    		checkVersion()
        const remembered = getRememberedAccount()
        if (remembered != null) {
            username.value = remembered['username'] as string
            password.value = remembered['password'] as string
            rememberPassword.value = true
                
            // 检查是否是从退出操作进入登录页
              const isLogout = uni.getStorageSync('isLogout')
              if (isLogout == null || isLogout == false || isLogout == '') {
                // 自动登录
                handleLogin()
            } else {
                // 清除退出标志
                uni.removeStorageSync('isLogout')
            }
        }
    })
    </script>
    
    <style lang="scss">
    .login-page {
        position: relative;
        flex: 1;
        padding-top: env(safe-area-inset-top);
    }
    
    .bg-image {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 0;
    }
    
    .login-content {
        position: relative;
        flex: 1;
        padding: 60rpx 40rpx;
        z-index: 1;
    }
    
    .header {
        align-items: center;
        margin-bottom: 80rpx;
    }
    
    .logo {
        height: 200rpx;
        margin-bottom: 40rpx;
    		border-radius:10px;
    }
    
    .title {
        font-size: 48rpx;
        color: #ffffff;
        font-weight: bold;
        // #ifndef APP-HARMONY
        text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
        // #endif
    
    }
    
    .form-card {
        background: rgba(255, 255, 255, 0.95);
        border-radius: 24rpx;
        padding: 50rpx 40rpx;
         // #ifndef APP-HARMONY
        box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
        // #endif
        margin-bottom: 100rpx;
    }
    
    .form-item {
        margin-bottom: 40rpx;
    }
    
    .label {
        font-size: 32rpx;
        color: #333333;
        font-weight: bold;
        margin-bottom: 20rpx;
    }
    
    .input-wrapper {
        position: relative;
        width: 100%;
    }
    
    .input {
        width: 100%;
        height: 90rpx;
        padding: 0 100rpx 0 30rpx;
        background-color: #f5f5f5;
        border-radius: 12rpx;
        font-size: 30rpx;
        color: #333333;
        border: 1rpx solid transparent;
    }
    
    .input:focus {
        border-color: #007aff;
        background-color: #ffffff;
    }
    
    .eye-icon {
        position: absolute;
        right: 30rpx;
        top: 50%;
        transform: translateY(-50%);
        width: 40rpx;
        height: 40rpx;
    }
    
    .remember-box {
        flex-direction: row;
        align-items: center;
        margin-bottom: 50rpx;
    }
    
    .checkbox {
        transform: scale(0.8);
    }
    
    .remember-text {
        font-size: 28rpx;
        color: #1677ff;
        margin-left: 12rpx;
    }
    
    .login-btn {
        width: 100%;
        height: 100rpx;
    		line-height: 100rpx;
        background-color: #0081ff;
        border-radius: 50rpx;
        color: #ffffff;
        // #ifndef APP-HARMONY
        box-shadow: 0 8rpx 16rpx rgba(0, 122, 255, 0.3);
        // #endif
    }
    
    .login-btn-text {
        font-size: 36rpx;
        color: #ffffff !important;
        font-weight: bold;
    }
    
    .footer {
        position: fixed;
        bottom: 60rpx;
        left: 0;
        right: 0;
        align-items: center;
        z-index: 2;
    }
    
    .version {
        font-size: 28rpx;
        color: #333333;
        margin-bottom: 15rpx;
    }
    
    .company {
        font-size: 24rpx;
        color: #333333;
    }
    	.picker-modal {
    	    position: fixed;
    	    top: 0;
    	    left: 0;
    	    right: 0;
    	    bottom: 0;
    	    z-index: 1000;
    	}
    	
    	.modal-mask {
    	    position: absolute;
    	    top: 0;
    	    left: 0;
    	    right: 0;
    	    bottom: 0;
    	    background-color: rgba(0, 0, 0, 0.5);
    	}
    	
    	.modal-content {
    	    position: absolute;
    	    bottom: 0;
    	    left: 0;
    	    right: 0;
    	    background-color: #ffffff;
    	    border-top-left-radius: 16rpx;
    	    border-top-right-radius: 16rpx;
    	    max-height: 1000rpx;
    	}
    	
    	.modal-header {
    	    flex-direction: row;
    	    justify-content: space-between;
    	    align-items: center;
    	    padding: 30rpx;
    	    border-bottom: 1rpx solid #f0f0f0;
    	}
    	
    	.modal-title {
    	    font-size: 32rpx;
    	    font-weight: bold;
    	    color: #333333;
    	}
    	
    	.modal-close {
    	    font-size: 28rpx;
    	    color: #007aff;
    	}
    	
    	.form-item-btn{
    		flex: 1;
    		align-items: center;
    		margin-bottom: 10px;
    	}
    	
    	.form-item-input{
    		flex: 1;
    		flex-direction: row;
    		background-color: #ffffff;
    		padding: 10px;
    	}
    	
    	.btn-primary {
    		z-index: 999;
    		font-size: 15px;
    		border-radius: 10rpx;
    		width: 100px;
    		padding: 5px;
    		background-color: #165DFF;
    		line-height: 45rpx;
    		color: #ffffff;
    		.btn-text{
    			color: #ffffff;
    			padding: 5px 15px;
    		}
    	}
    	.label-picker{
    		font-size: 32rpx;
    		color: #333333;
    		font-weight: bold;
    		width: 180rpx;
    		padding: 20rpx;
    	}
    	
    	.input-picker {
    	    width: 70%;
    	    height: 90rpx;
    	    padding: 0 100rpx 0 30rpx;
    	    background-color: #f5f5f5;
    	    border-radius: 12rpx;
    	    font-size: 30rpx;
    	    color: #333333;
    	    border: 1rpx solid transparent;
    	}
    	
    	.eye-icon-picker {
    	    position: absolute;
    	    right: 230rpx;
    	    top: 50%;
    	    transform: translateY(-50%);
    	    width: 40rpx;
    	    height: 40rpx;
    	}
    	
    	.download-progress-mask {
    	    position: fixed;
    	    top: 0;
    	    left: 0;
    	    right: 0;
    	    bottom: 0;
    	    background-color: rgba(0, 0, 0, 0.5);
    	    z-index: 999;
    	    align-items: center;
    	    justify-content: center;
    	}
    	.download-progress-box {
    	    width: 500rpx;
    	    padding: 60rpx;
    	    background-color: #ffffff;
    	    border-radius: 24rpx;
    	    align-items: center;
    	}
    	.download-progress-title {
    	    font-size: 32rpx;
    	    color: #333333;
    	    margin-bottom: 30rpx;
    	}
    	.download-progress-bar {
    	    width: 100%;
    	    height: 16rpx;
    	    background-color: #f0f0f0;
    	    border-radius: 8rpx;
    	    overflow: hidden;
    	    margin-bottom: 20rpx;
    	}
    	.download-progress-fill {
    	    height: 100%;
    	    background-color: #0081ff;
    	    border-radius: 8rpx;
    	    transition: width 0.2s ease;
    	}
    	.download-progress-text {
    	    font-size: 28rpx;
    	    color: #666666;
    	}
    </style>
    
    
  • 错误信息

    error: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'UTSJSONObject?'.‌
    at pages/login/index.uvue:127:28
    125|
    126|      // 版本号
    127|      const manifestVersion = manifest.versionName as string | null
    |                              ^
    128|      const version = ref<string>(manifestVersion != null ? manifestVersion : '1.0.0')
    129|   const versionServer = ref<string>('1.0.0')
    

第 2 个错误

  • 文件路径 pages/profile/index.uvue

  • 文件内容

    <template>
    <view class="page-container">
        <!-- 背景图 -->
        <image class="bg-image" src="/static/images/profile/1.png" mode="widthFix"></image>
    
        <scroll-view class="page-content" style="flex: 1">
            <!-- 用户信息头部 -->
            <view class="header">
                <image class="avatar" src="/static/images/login/2.png" mode="aspectFill"></image>
                <text class="user-name">{{ nickName }}({{ userName }})</text>
            </view>
    
            <!-- 信息卡片区域 -->
            <view class="info-cards">
                <!-- 电话卡片 -->
                <view class="info-card">
                    <image class="card-icon" src="/static/images/profile/2.png" mode="aspectFit"></image>
                    <text class="card-label">电话</text>
                    <text class="card-value">{{ userPhone }}</text>
                </view>
    
                <!-- 部门卡片 -->
                <view class="info-card">
                    <image class="card-icon" src="/static/images/profile/3.png" mode="aspectFit"></image>
                    <text class="card-label">部门</text>
                    <text class="card-value">{{ userDept }}</text>
                </view>
            </view>
    
            <!-- 通讯录 -->
            <view class="contact-item">
                <text class="contact-text">{{userRole}}</text>
            </view>
    			<view class="menu-section">
    				<view class="menu-item" @click="handlePassword()">
    					<image class="menu-icon" src="/static/images/profile/7.png" mode="aspectFit"></image>
    					<text class="menu-title">修改密码</text>
    					<text class="menu-arrow">›</text>
    				</view>
    				
    				<view class="menu-item">
    					<image class="menu-icon" src="/static/images/profile/5.png" mode="aspectFit"></image>
    					<text class="menu-title">版本:{{version}}</text>
    					<text class="menu-title-download" v-if="isNewVersion" @click="handleVersionClick()">升级</text>
    				</view>
    			</view> 
            <!-- 功能菜单 -->
            <!-- <view class="menu-section">
                <view v-for="(item, index) in menuList" :key="index" class="menu-item" @click="handleMenuClick(item)">
                    <image class="menu-icon" src="/static/images/profile/7.png" mode="aspectFit"></image>
                    <text class="menu-title">修改密码</text>
                    <text class="menu-arrow">›</text>
                </view>
            </view> -->
    
            <!-- 退出 -->
            <view class="logout-wrapper">
                <text class="logout-btn" @click="handleLogout">退出</text>
            </view>
        </scroll-view>
    		
    		<!-- 下载进度遮罩(仅更新数据不重建,无闪烁) -->
    		<view v-if="isDownload" class="download-progress-mask">
    		    <view class="download-progress-box">
    		        <text class="download-progress-title">下载中...</text>
    		        <view class="download-progress-bar">
    		            <view class="download-progress-fill" :style="{ width: downloadProgress + '%' }"></view>
    		        </view>
    		        <text class="download-progress-text">{{ downloadProgress }}%</text>
    		    </view>
    		</view>
    		<custom-tabbar :current="3" />
    </view>
    </template>
    
    <script setup lang="uts">
    import { ref, onMounted } from 'vue'
    import { getUserInfo, getStoreIsKey } from '../../utils/storage'
    import { logout as logoutApi } from '../../api/auth/logout'
    import { useAuth } from '../../composables/useAuth'
    	import { encryptAES, decryptAES } from '../../utils/crypto'
    	import { getIsKey } from '../../api/auth/login'
    	import { getConfigKey } from '../../api/system/config.uts'
    	import { getBaseUrl } from '../../utils/request'
    	
    // @ts-ignore
    import manifest from '@/manifest.json'
    
    // 用户信息
    const userName = ref<string>('')
    const userPhone = ref<string>('')
    const userDept = ref<string>('')
    	const userRole = ref<string>('')
    const isNewVersion = ref<boolean>(false)
    	const nickName = ref<string>('')
    	const isDownload = ref<boolean>(false)
    	const downloadProgress = ref<number>(0)
    	
    // 版本信息
    const manifestVersion = manifest.versionName as string | null
    const version = ref<string>(manifestVersion != null ? manifestVersion : '1.0.0')
    	const versionServer = ref<string>('1.0.0')
    			
    // 认证管理
    const auth = useAuth()
    const clearAuth = auth.logout
    const getCurrentAccessToken = auth.getCurrentAccessToken
    
    // 菜单列表
    type MenuItem = {
        id: number
        title: string
        icon: string
        path?: string
        action?: string
    }
    
    const menuList = ref<MenuItem[]>([
        {
            id: 1,
            title: '修改密码',
            icon: '/static/images/profile/7.png',
            path: '/pages/profile/password/index'
        },
    		{
            id: 2,
            title: '关于版本',
            icon: '/static/images/profile/5.png',
            action: 'version'
        },
        {
            id: 3,
            title: '关于我们',
            icon: '/static/images/profile/6.png',
            path: '/pages/profile/about/index'
        },
        {
            id: 4,
            title: '通用设置',
            icon: '/static/images/profile/8.png',
            path: '/pages/profile/settings/index'
        }
    ])
    
    // 通讯录点击
    const handleContactClick = (): void => {
        uni.showToast({
            title: '功能开发中',
            icon: 'none'
        })
    }
    
    	const compareVersion = (newVersion: string, currentVersion: string): boolean => {
    		const newVer = newVersion.split('.').map(item => parseInt(item))
    		const currentVer = currentVersion.split('.').map(item => parseInt(item))
    		
    		const maxLength = Math.max(newVer.length, currentVer.length)
    		
    		for (let i = 0; i < maxLength; i++) {
    			const newNum = i < newVer.length ? newVer[i] : 0
    			const currentNum = i < currentVer.length ? currentVer[i] : 0
    			
    			if (newNum > currentNum) {
    				return true
    			} else if (newNum < currentNum) {
    				return false
    			}
    		}
    		return false
    	}
    	
    	const checkVersion = async (): Promise<void> => {
    		const versionJSON = await getConfigKey("emcs.android.version") as UTSJSONObject
    		versionServer.value = versionJSON['msg'] as string
    		const hasNewVersion = compareVersion(versionServer.value, version.value) // true
    		console.log("versionServer:"+versionServer.value)
    		console.log("hasNewVersion:"+hasNewVersion)
    		if(hasNewVersion){
    			isNewVersion.value = true
    		}
    	}
    
    	const handlePassword = (): void =>{
    		uni.navigateTo({
    		    url:"/pages/profile/password/index",
    		    fail: (err: any) => {
    		        uni.showToast({
    		            title: '功能开发中',
    		            icon: 'none'
    		        })
    		    }
    		})
    	}
    
    // 菜单点击
    const handleMenuClick = (item: MenuItem): void => {
        if (item.action == 'version') {
            uni.showModal({
                title: '版本信息',
                content: `当前版本:v${version.value}`,
                showCancel: false
            })
            return
        }
    
        if (item.path != null && item.path.length > 0) {
            uni.navigateTo({
                url: item.path,
                fail: (err: any) => {
                    uni.showToast({
                        title: '功能开发中',
                        icon: 'none'
                    })
                }
            })
        }
    }
    
    // 执行退出登录(必须在 handleLogout 之前定义)
    const doLogout = async (): Promise<void> => {
        try {
            await logoutApi()
        } catch (e: any) {
            console.log('退出登录接口调用失败', e)
        } finally {
    			uni.setStorageSync("login_key", "0")
            clearAuth()
        }
    }
    
    // 退出登录
    const handleLogout = (): void => {
        uni.showModal({
            title: '提示',
            content: '确定要退出登录吗?',
            success: (res) => {
                // 提取属性(any 类型不能直接访问属性)
                const confirm = res.confirm as boolean
    
                if (confirm) {
                    // 使用异步函数包装
                    doLogout()
                }
            }
        })
    }
    
    	// 安装APK的单独函数
    	const installApkFile = (filePath: string): void => {
    	  uni.installApk({
    		filePath: filePath,
    		success: () => {
    		  console.log('安装成功');
    		  uni.showToast({
    			title: '安装成功',
    			icon: 'success'
    		  });
    		},
    		fail: (error) => {
    		  console.error('安装失败:', error);
    		  uni.showToast({
    			title: '安装失败',
    			icon: 'none'
    		  });
    		},
    		complete: (res) => {
    		  console.log('安装完成:', res);
    		}
    	  });
    	}
    	
    	
    	
    	const installApkWithProgress = (): void => {		
    	    // #ifdef APP-ANDROID
    			isDownload.value = true;
    			downloadProgress.value = 0;
    			let donwloadUrl = '';
    			donwloadUrl = getBaseUrl() + '/profile/app/emcs-release-'+versionServer.value+'.apk'
    		
    			// 下载APK
    			const downloadTask = uni.downloadFile({
    				url: donwloadUrl,
    				filePath: `${uni.env.USER_DATA_PATH}/${Date.now()}_test.apk`, // 使用时间戳防止重名
    				success: (downloadRes) => {
    					isDownload.value = false;
    					if (downloadRes.statusCode == 200) {
    					// 确认安装
    						uni.showModal({
    						  title: '安装提示',
    						  content: '下载完成,是否立即安装?',
    						  success: (modalRes) => {
    							if (modalRes.confirm) {
    							  installApkFile(downloadRes.tempFilePath);
    							}
    						  }
    						});
    					} else {
    						uni.showToast({
    						  title: '下载失败',
    						  icon: 'error'
    						});
    					}
    				},
    				fail: (error) => {
    				  isDownload.value = false;
    				  uni.showToast({
    					title: '下载失败',
    					icon: 'none'
    				  });
    				  console.error('下载失败:', error);
    				}
    			});
    	
    			// 监听下载进度(更新页面内进度条,节流 5% 减轻渲染压力)
    			let lastProgress = -5;
    			downloadTask.onProgressUpdate((res) => {
    				const p = Math.round(res.progress);
    				if (p - lastProgress >= 5 || p >= 100) {
    					lastProgress = p;
    					downloadProgress.value = p;
    				}
    			});
    		// #endif
    		// #ifdef APP-HARMONY
    			uni.showToast({
    				title: '请登录PC端,扫描二维码下载并安装',
    				icon: 'none'
    			});
    		// #endif
    		// #ifdef APP-IOS
    			uni.showToast({
    				title: '请登录PC端,扫描二维码下载并安装',
    				icon: 'none'
    			});
    		// #endif
    	}
    	
    	// 菜单点击
    	const handleVersionClick = (): void => {
    	    installApkWithProgress();
    	}
    	
    // 初始化
    onMounted(() => {
    		checkVersion();
        const userInfo = getUserInfo()
        if (userInfo != null) {
            const nickNameRaw = userInfo['nickName'] as string | null
    			const phone = userInfo['phone'] as string | null
    			const userNameRaw = userInfo['userName'] as string | null
    			
    			userName.value = userNameRaw ?? ''
    			nickName.value = nickNameRaw ?? ''
    			if (phone != null && phone.length > 0) {
    				userPhone.value = phone
    			}
                
            const deptName = userInfo['deptName'] as string | null
            if (deptName != null && deptName.length > 0) {
                userDept.value = deptName
            }
    			
    			const roleNames = userInfo['roleNames'] as string | null
    			if (roleNames != null && roleNames.length > 0) {
    			    userRole.value = roleNames
    			}
        }
    })
    </script>
    
    <style lang="scss">
    .page-container {
        position: relative;
        flex: 1;
        background: #f5f8fb;
        padding-top: env(safe-area-inset-top);
    }
    
    .bg-image {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 0;
    }
    
    .page-content {
        position: relative;
        flex: 1;
        padding-bottom: 100rpx;
        z-index: 1;
    }
    
    .header {
        padding: 80rpx 30rpx 40rpx;
        flex-direction: row;
        align-items: center;
    
        .avatar {
            width: 120rpx;
            height: 120rpx;
            border: 1rpx solid #ccc;
            border-radius: 60rpx;
            background-color: #fff;
            margin-right: 30rpx;
        }
    
        .user-name {
            font-size: 40rpx;
            color: #ffffff;
            font-weight: bold;
        }
    }
    
    .info-cards {
        padding: 0 30rpx;
        flex-direction: row;
        justify-content: space-between;
        margin-bottom: 30rpx;
    
        .info-card {
            width: 330rpx;
            background-color: #ffffff;
            border-radius: 24rpx;
            padding: 30rpx 24rpx;
                
            /* #ifndef APP-HARMONY */
            box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
            /* #endif */
    
            .card-icon {
                width: 48rpx;
                height: 48rpx;
                margin-bottom: 16rpx;
            }
    
            .card-label {
                font-size: 26rpx;
                color: #666666;
                margin-bottom: 12rpx;
            }
    
            .card-value {
                font-size: 32rpx;
                color: #333333;
                font-weight: bold;
            }
        }
    }
    
    .contact-item {
        margin: 0 30rpx 30rpx;
        padding: 30rpx;
        background-color: #ffffff;
        border-radius: 24rpx;
        flex-direction: row;
        align-items: center;
            
        /* #ifndef APP-HARMONY */
        box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
        /* #endif */
    
        .contact-icon {
            width: 48rpx;
            height: 48rpx;
            margin-right: 24rpx;
        }
    
        .contact-text {
            flex: 1;
            font-size: 32rpx;
            color: #333333;
        }
    
        .contact-arrow {
            font-size: 48rpx;
            color: #cccccc;
            line-height: 48rpx;
        }
    }
    
    .menu-section {
        margin: 0 30rpx 30rpx;
        background-color: #ffffff;
        border-radius: 24rpx;
        overflow: hidden;
            
        /* #ifndef APP-HARMONY */
        box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
        /* #endif */
    
        .menu-item {
            flex-direction: row;
            align-items: center;
            padding: 30rpx;
            border-bottom: 1rpx solid #f0f0f0;
    
            &:last-child {
                border-bottom: none;
            }
    
            .menu-icon {
                width: 48rpx;
                height: 48rpx;
                margin-right: 24rpx;
            }
    
            .menu-title {
                flex: 1;
                font-size: 32rpx;
                color: #333333;
            }
    			
    			.menu-title-download {
    			    font-size: 30rpx;
    			    color: #5500ff;
    			}
    
            .menu-arrow {
                font-size: 48rpx;
                color: #cccccc;
                line-height: 48rpx;
            }
        }
    }
    
    .logout-wrapper {
        margin: 0 30rpx 30rpx;
        background-color: #ffffff;
        border-radius: 24rpx;
        overflow: hidden;
        align-items: center;
        justify-content: center;
        height: 100rpx;
            
        /* #ifndef APP-HARMONY */
        box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
        /* #endif */
    
        .logout-btn {
            font-size: 32rpx;
            color: #666666;
            padding: 20rpx 0;
        }
    }
    	
    	.download-progress-mask {
    	    position: fixed;
    	    top: 0;
    	    left: 0;
    	    right: 0;
    	    bottom: 0;
    	    background-color: rgba(0, 0, 0, 0.5);
    	    z-index: 999;
    	    align-items: center;
    	    justify-content: center;
    	}
    	.download-progress-box {
    	    width: 500rpx;
    	    padding: 60rpx;
    	    background-color: #ffffff;
    	    border-radius: 24rpx;
    	    align-items: center;
    	}
    	.download-progress-title {
    	    font-size: 32rpx;
    	    color: #333333;
    	    margin-bottom: 30rpx;
    	}
    	.download-progress-bar {
    	    width: 100%;
    	    height: 16rpx;
    	    background-color: #f0f0f0;
    	    border-radius: 8rpx;
    	    overflow: hidden;
    	    margin-bottom: 20rpx;
    	}
    	.download-progress-fill {
    	    height: 100%;
    	    background-color: #0081ff;
    	    border-radius: 8rpx;
    	    transition: width 0.2s ease;
    	}
    	.download-progress-text {
    	    font-size: 28rpx;
    	    color: #666666;
    	}
    </style>
    
    
  • 错误信息

    error: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'UTSJSONObject?'.‌
    at pages/profile/index.uvue:100:28
    98 |
    99 |      // 版本信息
    100|      const manifestVersion = manifest.versionName as string | null
    |                              ^
    101|      const version = ref<string>(manifestVersion != null ? manifestVersion : '1.0.0')
    102|   const versionServer = ref<string>('1.0.0')