This commit is contained in:
2026-02-07 16:11:40 +08:00
parent 7998e702a1
commit 71314c5b12
63 changed files with 33553 additions and 0 deletions

793
src/pages/profile/index.vue Normal file
View File

@@ -0,0 +1,793 @@
<template>
<view class="profile-page">
<!-- 用户信息区域 -->
<view class="user-info">
<view class="avatar-section">
<view class="avatar-container" @click="previewAvatar">
<image v-if="userInfo.driverInfo?.data?.driverHead"
:src="getImageUrl(userInfo.driverInfo.data.driverHead)" :key="avatarKey"
class="avatar-image" mode="aspectFill" />
<view v-else class="avatar-placeholder">
<My size="60" color="#666" />
</view>
</view>
<view class="user-details">
<text class="username">{{ userInfo.driverInfo?.data?.driverName || userInfo.username || '点击登录' }}</text>
<text class="phone-number">{{ userInfo.driverInfo?.data?.driverTel || '未设置' }}</text>
</view>
</view>
</view>
<!-- 功能菜单区域 -->
<view class="menu-section">
<!-- 我的车辆 -->
<!-- <view class="menu-item" @click="goToMyVehicles">
<view class="menu-icon">
<Cart size="40" color="#666" />
</view>
<text class="menu-text">我的车辆</text>
<view class="menu-arrow">
<Right size="32" color="#999" />
</view>
</view> -->
<!-- 历史运送车次 -->
<view class="menu-item" @click="goToTrips">
<view class="menu-icon">
<Order size="40" color="#666" />
</view>
<view class="menu-content">
<text class="menu-text">历史运送车次</text>
</view>
<view class="menu-arrow">
<Right size="32" color="#999" />
</view>
</view>
<!-- 修改头像 -->
<view class="menu-item" @click="changeAvatar" :class="{ 'disabled': isUploadingAvatar }">
<view class="menu-icon">
<My size="40" color="#666" />
</view>
<view class="menu-content">
<text class="menu-text">{{ isUploadingAvatar ? '头像上传中...' : '修改头像' }}</text>
</view>
<view class="menu-arrow" v-if="!isUploadingAvatar">
<Right size="32" color="#999" />
</view>
<view class="menu-loading" v-else>
<text class="loading-text">...</text>
</view>
</view>
<!-- 实名认证 -->
<view class="menu-item" @click="goToIdentity">
<view class="menu-icon">
<My size="40" color="#666" />
</view>
<view class="menu-content">
<text class="menu-text">实名认证</text>
<view v-if="driverStatus" class="status-badge" :class="statusClass">
<text class="status-text">{{ statusText }}</text>
</view>
</view>
<view class="menu-arrow">
<Right size="32" color="#999" />
</view>
</view>
<!-- 修改密码 -->
<view class="menu-item" @click="changePassword">
<view class="menu-icon">
<Setting size="40" color="#666" />
</view>
<view class="menu-content">
<text class="menu-text">修改密码</text>
</view>
<view class="menu-arrow">
<Right size="32" color="#999" />
</view>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-section">
<nut-button type="danger" size="large" @click="handleLogout" v-if="userInfo.isLogin">
退出登录
</nut-button>
<nut-button type="primary" size="large" @click="login" v-else>
立即登录
</nut-button>
</view>
<!-- Toast 提示 -->
<nut-toast v-model:visible="showToast" :msg="toastMsg" />
<!-- 头像预览弹窗 -->
<nut-popup v-model:visible="showAvatarPreview" position="center">
<view class="avatar-preview-dialog">
<view class="preview-title">头像预览</view>
<view class="avatar-preview">
<image v-if="userInfo.driverInfo?.data?.driverHead || userInfo.avatar"
:src="userInfo.driverInfo?.data?.driverHead ? getImageUrl(userInfo.driverInfo.data.driverHead) : userInfo.avatar"
class="preview-avatar-image" mode="aspectFill" />
<view v-else class="preview-avatar-placeholder">
<My size="100" color="#666" />
</view>
</view>
<view class="preview-buttons">
<nut-button @click="showAvatarPreview = false">关闭</nut-button>
<nut-button type="primary" @click="selectNewAvatar" :disabled="isUploadingAvatar">
{{ isUploadingAvatar ? '上传中...' : '选择新头像' }}
</nut-button>
</view>
</view>
</nut-popup>
<!-- 修改密码弹窗 -->
<nut-popup v-model:visible="showPasswordDialog" position="center">
<view class="password-dialog">
<view class="dialog-title">修改密码</view>
<nut-form>
<nut-form-item label="原密码">
<nut-input v-model="passwordForm.oldPassword" type="password" placeholder="请输入原密码" />
</nut-form-item>
<nut-form-item label="新密码">
<nut-input v-model="passwordForm.newPassword" type="password" placeholder="请输入新密码" />
</nut-form-item>
<nut-form-item label="确认密码">
<nut-input v-model="passwordForm.confirmPassword" type="password" placeholder="请再次输入新密码" />
</nut-form-item>
</nut-form>
<view class="dialog-buttons">
<nut-button @click="showPasswordDialog = false">取消</nut-button>
<nut-button type="primary" @click="submitPassword">确认</nut-button>
</view>
</view>
</nut-popup>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, computed } from 'vue'
import { Button, Toast, Avatar, Popup, Form, FormItem, Input } from '@nutui/nutui-taro'
import { Cart, Order, Setting, Right, My } from '@nutui/icons-vue'
import Taro, { useDidShow } from '@tarojs/taro'
import { getUserInfo, logout, isLoggedIn, saveUserInfo, checkLoginAndRedirect } from '../../utils/auth.js'
import { driverAPI } from '../../api/index'
import { API_CONFIG } from '../../utils/request.js'
// 用户信息
const userInfo = reactive({
isLogin: false,
username: '',
userId: '',
phone: '',
avatar: ''
})
// 页面加载时检查登录状态
onMounted(() => {
// 检查登录状态,未登录则跳转到登录页
if (!checkLoginAndRedirect()) {
return
}
// 已登录,获取用户信息
const storedUserInfo = getUserInfo()
if (storedUserInfo) {
Object.assign(userInfo, storedUserInfo)
}
})
// 页面显示时重新获取用户信息
useDidShow(() => {
// 检查登录状态,未登录则跳转到登录页
if (!checkLoginAndRedirect()) {
return
}
// 已登录,重新获取用户信息
const storedUserInfo = getUserInfo()
if (storedUserInfo) {
Object.assign(userInfo, storedUserInfo)
// 如果头像有变化,强制重新渲染
if (storedUserInfo.driverInfo?.data?.driverHead || storedUserInfo.avatar) {
avatarKey.value++
}
} else {
Object.assign(userInfo, {
isLogin: false,
username: '',
userId: '',
phone: '',
avatar: ''
})
}
// 刷新司机数据以获取最新的认证状态
refreshDriverData()
})
// Toast 相关
const showToast = ref(false)
const toastMsg = ref('')
// 头像预览弹窗
const showAvatarPreview = ref(false)
// 修改密码弹窗
const showPasswordDialog = ref(false)
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
// 头像上传状态
const isUploadingAvatar = ref(false)
// 头像key用于强制重新渲染
const avatarKey = ref(0)
// 司机状态相关
const driverStatus = computed(() => {
return userInfo.driverInfo?.data?.driverStatus
})
const statusText = computed(() => {
const status = driverStatus.value
if (status === '0') return '待认证'
if (status === '1') return '正常'
if (status === '2') return '冻结'
return '未知状态'
})
// 获取图片完整URL
const getImageUrl = (path) => {
if (!path) return ''
// 如果已经是完整URL直接返回
if (path.startsWith('http')) {
return path
}
// 拼接API地址
return `${API_CONFIG.BASE_URL}api${path.startsWith('/') ? path : '/' + path}`
}
const statusClass = computed(() => {
const status = driverStatus.value
if (status === '0') return 'status-pending'
if (status === '1') return 'status-normal'
if (status === '2') return 'status-frozen'
return 'status-unknown'
})
// 显示提示
const showMessage = (msg) => {
toastMsg.value = msg
showToast.value = true
}
// 刷新司机数据
const refreshDriverData = async () => {
try {
const storedUserInfo = getUserInfo()
if (!storedUserInfo) {
console.error('无法获取用户信息')
return
}
// 获取用户ID
const userId = storedUserInfo.userId || storedUserInfo.id || storedUserInfo.user?.id
if (!userId) {
console.error('无法获取用户ID')
return
}
// 调用API获取最新的司机信息
const response = await driverAPI.getDriverInfo(userId)
if (response.statusCode === 200 && response.data) {
if (response.data.code === 0 && response.data.data) {
// 更新本地用户信息中的司机数据
const updatedUserInfo = {
...storedUserInfo,
driverInfo: {
...storedUserInfo.driverInfo,
data: response.data.data
}
}
// 保存更新后的用户信息
saveUserInfo(updatedUserInfo)
// 更新当前页面的用户信息
Object.assign(userInfo, updatedUserInfo)
} else {
console.error('获取司机信息失败:', response.data)
}
} else {
console.error('获取司机信息API调用失败:', response)
}
} catch (error) {
console.error('刷新司机数据失败:', error)
}
}
// 预览头像
const previewAvatar = () => {
if (!userInfo.isLogin) {
showMessage('请先登录')
return
}
showAvatarPreview.value = true
}
// 更换头像
const changeAvatar = () => {
if (!userInfo.isLogin) {
showMessage('请先登录')
return
}
selectNewAvatar()
}
// 选择新头像
const selectNewAvatar = () => {
showAvatarPreview.value = false
if (isUploadingAvatar.value) {
showMessage('头像上传中,请稍候...')
return
}
Taro.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
// 将图片转换为base64
Taro.getFileSystemManager().readFile({
filePath: tempFilePath,
encoding: 'base64',
success: (fileRes) => {
const base64Data = `data:image/jpeg;base64,${fileRes.data}`
// 上传头像
uploadAvatar(base64Data)
},
fail: (error) => {
console.error('读取图片文件失败:', error)
showMessage('头像处理失败,请重试')
}
})
},
fail: (error) => {
console.error('选择图片失败:', error)
showMessage('选择头像失败,请重试')
}
})
}
// 上传头像
const uploadAvatar = async (base64Data) => {
if (isUploadingAvatar.value) return
isUploadingAvatar.value = true
showMessage('正在上传头像...')
try {
// 获取当前用户的司机ID
const storedUserInfo = getUserInfo()
const driverId = storedUserInfo?.driverId || storedUserInfo?.id || userInfo.userId
if (!driverId) {
console.error('未找到司机ID无法上传头像')
showMessage('用户信息不完整,请重新登录')
return
}
const updateData = {
id: driverId,
driverHead: base64Data
}
const updateResponse = await driverAPI.updateDriverHead(updateData)
if (updateResponse.statusCode === 200 && updateResponse.data) {
if (updateResponse.data.code === 0) {
// 获取服务器返回的图片路径
const imagePath = updateResponse.data.data || updateResponse.data.path || updateResponse.data.url
if (imagePath) {
// 更新driverInfo.data.driverHead字段为服务器返回的路径
if (userInfo.driverInfo && userInfo.driverInfo.data) {
userInfo.driverInfo.data.driverHead = imagePath
}
// 强制触发响应式更新
await nextTick()
// 强制重新渲染头像
avatarKey.value++
// 更新本地存储的用户信息
if (storedUserInfo) {
// 同时更新driverInfo中的头像字段
if (storedUserInfo.driverInfo && storedUserInfo.driverInfo.data) {
storedUserInfo.driverInfo.data.driverHead = imagePath
}
Taro.setStorageSync('userInfo', storedUserInfo)
}
showMessage('头像更新成功')
} else {
console.error('服务器未返回图片路径')
showMessage('头像上传失败:未获取到图片路径')
}
} else {
const errorMsg = updateResponse.data?.msg || updateResponse.data?.message || '头像上传失败'
console.error('头像上传业务失败:', updateResponse.data)
showMessage(errorMsg)
}
} else {
const errorMsg = updateResponse.data?.msg || updateResponse.data?.message || '头像上传失败'
showMessage(errorMsg)
}
} catch (error) {
console.error('头像上传失败:', error)
showMessage('头像上传失败,请重试')
} finally {
isUploadingAvatar.value = false
}
}
// 我的车辆
const goToMyVehicles = () => {
showMessage('跳转到我的车辆页面')
// 这里可以跳转到车辆页面
// Taro.navigateTo({ url: '/pages/vehicle/index' })
}
// 历史运送车次
const goToTrips = () => {
Taro.navigateTo({ url: '/pages/history/index' })
}
// 实名认证
const goToIdentity = () => {
Taro.navigateTo({ url: '/pages/realname/index' })
}
// 修改密码
const changePassword = () => {
Taro.navigateTo({ url: '/pages/password/index' })
}
// 提交密码修改
const submitPassword = () => {
if (!passwordForm.oldPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
showMessage('请填写完整信息')
return
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
showMessage('两次输入的密码不一致')
return
}
if (passwordForm.newPassword.length < 6) {
showMessage('密码长度不能少于6位')
return
}
// 这里调用修改密码的API
showMessage('密码修改成功')
showPasswordDialog.value = false
// 清空表单
passwordForm.oldPassword = ''
passwordForm.newPassword = ''
passwordForm.confirmPassword = ''
}
// 登录
const login = () => {
Taro.navigateTo({ url: '/pages/login/index' })
}
// 退出登录
const handleLogout = () => {
logout() // 使用认证工具中的退出登录函数
showMessage('已退出登录')
}
</script>
<style>
.profile-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 用户信息区域 */
.user-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 60rpx 40rpx;
color: white;
}
.avatar-section {
display: flex;
align-items: center;
}
.avatar-container {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
overflow: hidden;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-image {
width: 100%;
height: 100%;
border-radius: 50%;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.user-details {
margin-left: 40rpx;
flex: 1;
}
.username {
display: block;
font-size: 48rpx;
font-weight: bold;
margin-bottom: 16rpx;
}
.user-id {
display: block;
font-size: 28rpx;
opacity: 0.8;
margin-bottom: 12rpx;
}
.phone-number {
display: block;
font-size: 28rpx;
opacity: 0.8;
}
/* 状态徽章 */
.status-badge {
display: inline-block;
padding: 8rpx 16rpx;
border-radius: 20rpx;
margin-top: 10rpx;
font-size: 24rpx;
font-weight: bold;
width: fit-content;
max-width: 200rpx;
}
.status-pending {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
border: 1rpx solid rgba(255, 193, 7, 0.3);
}
.status-normal {
background: rgba(40, 167, 69, 0.2);
color: #28a745;
border: 1rpx solid rgba(40, 167, 69, 0.3);
}
.status-frozen {
background: rgba(220, 53, 69, 0.2);
color: #dc3545;
border: 1rpx solid rgba(220, 53, 69, 0.3);
}
.status-unknown {
background: rgba(108, 117, 125, 0.2);
color: #6c757d;
border: 1rpx solid rgba(108, 117, 125, 0.3);
}
.status-text {
font-size: 24rpx;
font-weight: bold;
}
/* 功能菜单区域 */
.menu-section {
background: white;
margin: 40rpx;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.1);
}
.menu-item {
display: flex;
align-items: center;
padding: 40rpx;
border-bottom: 2rpx solid #f0f0f0;
transition: background-color 0.3s;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background-color: #f8f8f8;
}
.menu-item.disabled {
opacity: 0.6;
pointer-events: none;
}
.menu-icon {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
border-radius: 16rpx;
margin-right: 30rpx;
}
.menu-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.menu-text {
font-size: 32rpx;
color: #333;
}
.menu-arrow {
color: #ccc;
}
.menu-loading {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
}
.loading-text {
font-size: 32rpx;
color: #667eea;
animation: loading 1s infinite;
}
@keyframes loading {
0%,
20% {
opacity: 0;
}
50% {
opacity: 1;
}
80%,
100% {
opacity: 0;
}
}
/* 退出登录区域 */
.logout-section {
padding: 40rpx;
margin-top: 40rpx;
}
/* 头像预览弹窗 */
.avatar-preview-dialog {
background: white;
border-radius: 24rpx;
padding: 40rpx;
width: 500rpx;
max-width: 90vw;
text-align: center;
}
.preview-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 30rpx;
color: #333;
}
.avatar-preview {
margin-bottom: 40rpx;
display: flex;
justify-content: center;
align-items: center;
}
.preview-avatar-image {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
}
.preview-avatar-placeholder {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
background: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
}
.preview-buttons {
display: flex;
justify-content: space-between;
gap: 20rpx;
}
.preview-buttons .nut-button {
flex: 1;
}
/* 修改密码弹窗 */
.password-dialog {
background: white;
border-radius: 24rpx;
padding: 40rpx;
width: 600rpx;
max-width: 90vw;
}
.dialog-title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
margin-bottom: 40rpx;
color: #333;
}
.dialog-buttons {
display: flex;
justify-content: space-between;
margin-top: 40rpx;
gap: 20rpx;
}
.dialog-buttons .nut-button {
flex: 1;
}
</style>