You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

775 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="profile-container" :class="{ 'wechat-browser': isWeixinBrowser }">
<!-- 顶部个人信息 -->
<view class="profile-header">
<view class="profile-avatar">
<image src="/static/avatar.png" mode="aspectFill" />
</view>
<view class="profile-info" @click="openEditPopup">
<view class="profile-name">Hi, {{ displayName }}</view>
<view class="profile-contact">
手机号{{ userInfo && userInfo.phone ? userInfo.phone : '-' }}
</view>
</view>
<view class="profile-setting" @click="openEditPopup">
<image class="setting-icon" src="/static/icon_setting.png" mode="aspectFit" />
</view>
</view>
<!-- 功能卡片1 -->
<view class="profile-card first-card">
<view class="profile-item" v-for="item in mainList" :key="item.text" @click="onItemClick(item)">
<image :src="item.icon" class="profile-list-icon" mode="aspectFit" />
<text class="item-text">{{ item.text }}</text>
<image src="/static/right_arrow.png" class="profile-arrow-icon" mode="aspectFit" />
</view>
</view>
<!-- 功能卡片2 -->
<view class="profile-card">
<view class="profile-item" v-for="item in helpList" :key="item.text" @click="onItemClick(item)">
<image :src="item.icon" class="profile-list-icon" mode="aspectFit" />
<text class="item-text">{{ item.text }}</text>
<image src="/static/right_arrow.png" class="profile-arrow-icon" mode="aspectFit" />
</view>
</view>
<!-- 编辑信息弹窗 -->
<view v-if="showEditPopup" class="edit-popup-mask" @click="closeEditPopup">
<view class="edit-popup" @click.stop>
<view class="edit-popup-header">
<text>{{ isEditMode ? '更新信息' : '个人信息' }}</text>
<text class="edit-popup-close" @click="closeEditPopup">×</text>
</view>
<view class="edit-popup-content">
<!-- 展示模式 -->
<template v-if="!isEditMode">
<view class="edit-field">
<text class="edit-label">姓名</text>
<text class="edit-value">{{ userInfo?userInfo.name:'-' }}</text>
</view>
<view class="edit-field">
<text class="edit-label">开票抬头类型</text>
<text class="edit-value">{{ getInvoiceTitleTypeLabel(userInfo?userInfo.payer_type:'') }}</text>
</view>
<view class="edit-field" v-if="userInfo&&userInfo.payer_type == 2">
<text class="edit-label">单位名称</text>
<text class="edit-value">{{ userInfo?userInfo.company_name:'-' }}</text>
</view>
<view class="edit-field">
<text class="edit-label">手机号</text>
<text class="edit-value">{{ userInfo?userInfo.phone:'-' }}</text>
</view>
<view class="edit-field" v-if="userInfo && userInfo.backup_phone">
<text class="edit-label">备用手机号</text>
<text class="edit-value">{{ userInfo.backup_phone }}</text>
</view>
<view class="edit-field">
<text class="edit-label">证件号</text>
<text class="edit-value">{{ userInfo?userInfo.id_card:'-' }}</text>
</view>
</template>
<!-- 编辑模式 -->
<template v-else>
<view class="edit-field">
<text class="edit-label">姓名</text>
<input class="edit-input" v-model="editForm.name" placeholder="请输入姓名" />
</view>
<view class="edit-field">
<text class="edit-label">开票抬头类型</text>
<view class="payer-type-group">
<view
class="payer-type-item"
:class="{ active: Number(editForm.payer_type) === 1 }"
@click="editForm.payer_type = 1"
>
船名
</view>
<view
class="payer-type-item"
:class="{ active: Number(editForm.payer_type) === 2 }"
@click="editForm.payer_type = 2"
>
单位
</view>
</view>
</view>
<view class="edit-field" v-if="Number(editForm.payer_type) === 2">
<text class="edit-label">单位名称</text>
<input
class="edit-input"
v-model="editForm.company_name"
placeholder="请输入单位名称"
/>
</view>
<view class="edit-field">
<text class="edit-label">手机号</text>
<input
class="edit-input"
v-model="editForm.phone"
type="number"
maxlength="11"
placeholder="请输入手机号"
/>
</view>
<view class="edit-field">
<text class="edit-label">验证码</text>
<view class="verify-code-row">
<input
class="edit-input verify-code-input"
v-model="editForm.phone_code"
type="number"
maxlength="6"
placeholder="请输入验证码"
/>
<button
class="send-code-btn"
:disabled="!canSendCode || countdown > 0"
@click="sendSmsCode"
>
{{ countdown > 0 ? countdown + '秒后重发' : '获取验证码' }}
</button>
</view>
<text class="hint-text">您稍后还可以验证一个备用手机号</text>
</view>
<view class="edit-field">
<text class="edit-label">证件号</text>
<input
class="edit-input"
v-model="editForm.id_card"
placeholder="请输入身份证号"
maxlength="18"
/>
</view>
</template>
</view>
<view class="edit-popup-footer">
<button v-if="!isEditMode" class="edit-submit-btn" @click="enterEditMode">编辑</button>
<button v-else class="edit-submit-btn" @click="submitUserInfo">保存</button>
</view>
</view>
</view>
</view>
</template>
<script>
import { API } from '@/config/index.js'
export default {
data() {
return {
isWeixinBrowser: false,
userInfo: null,
editForm: {
name: '',
payer_type: 1, // 开票抬头类型1-船号2-单位
company_name: '', // 单位名称
phone: '',
phone_code: '', // 主手机号验证码
id_card: ''
},
showEditPopup: false,
isEditMode: false, // 是否处于编辑模式
canSendCode: true, // 是否可以发送验证码
countdown: 0, // 倒计时秒数
countdownTimer: null, // 倒计时定时器
mainList: [
{ text: '个人信息', icon: '/static/icon_my1.png' },
{ text: '我的预约', icon: '/static/icon_my1.png' },
// { text: '我的购票', icon: '/static/icon_my2.png' },
{ text: '我的发票', icon: '/static/icon_my3.png' },
{ text: '船舶信息', icon: '/static/icon_my4.png' },
],
helpList: [
{ text: '联系客服', icon: '/static/icon_my5.png' },
{ text: '帮助中心', icon: '/static/icon_my6.png' },
{ text: '关于我们', icon: '/static/icon_my7.png' },
]
}
},
onLoad() {
// #ifdef H5
this.isWeixinBrowser = /MicroMessenger/i.test(navigator.userAgent)
// #endif
},
computed: {
displayName() {
return (this.userInfo && this.userInfo.name) ? this.userInfo.name : '微信用户'
},
payerTypeLabel() {
const map = {
1: '个人',
2: '单位'
}
const type = Number(this.userInfo ? (this.userInfo.payer_type ?? this.userInfo.payerType) : undefined)
return map[type] || '-'
}
},
onShow() {
this.fetchUserInfo()
},
onUnload() {
// 清理倒计时定时器
if (this.countdownTimer) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
}
},
methods: {
getInvoiceTitleTypeLabel(type) {
const map = {
1: '船名',
2: '单位'
}
return map[Number(type)] || '-'
},
async fetchUserInfo() {
const token = uni.getStorageSync('token')
if (!token) {
this.userInfo = null
return
}
try {
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_USER_INFO}?token=${token}`,
method: 'POST',
success: resolve,
fail: reject
})
})
console.log(res);
if (res.data) {
const data = res.data || {}
const invoiceTitleType = Number(data.payer_type ?? data.invoiceTitleType ?? 1)
this.userInfo = {
...data,
payer_type: invoiceTitleType
}
this.editForm = {
name: data.name || '',
payer_type: invoiceTitleType,
company_name: data.company_name || data.companyName || '',
phone: data.phone || '',
phone_code: '',
id_card: data.id_card || ''
}
} else {
this.userInfo = null
this.resetEditForm()
}
} catch (e) {
console.error('获取用户信息失败:', e)
this.userInfo = null
this.resetEditForm()
}
},
resetEditForm() {
this.editForm = {
name: '',
payer_type: 1,
company_name: '',
phone: '',
phone_code: '',
id_card: ''
}
},
openEditPopup() {
if (this.userInfo) {
this.editForm = {
name: this.userInfo.name || '',
payer_type: Number(this.userInfo.payer_type ?? this.userInfo.invoiceTitleType) || 1,
company_name: this.userInfo.company_name || this.userInfo.companyName || '',
phone: this.userInfo.phone || '',
phone_code: '',
id_card: this.userInfo.id_card || ''
}
}
this.isEditMode = false // 默认展示模式
this.showEditPopup = true
},
enterEditMode() {
this.isEditMode = true
},
closeEditPopup() {
this.showEditPopup = false
this.isEditMode = false
// 清理倒计时定时器
if (this.countdownTimer) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
}
this.countdown = 0
},
validateForm() {
if (!this.editForm.name) {
uni.showToast({ title: '请输入姓名', icon: 'none' })
return false
}
if (![1, 2].includes(Number(this.editForm.payer_type))) {
uni.showToast({ title: '请选择开票抬头类型', icon: 'none' })
return false
}
// 如果是单位类型,需要填写单位名称
if (Number(this.editForm.payer_type) === 2 && !this.editForm.company_name) {
uni.showToast({ title: '请输入单位名称', icon: 'none' })
return false
}
const phoneReg = /^1\d{10}$/
if (!phoneReg.test(this.editForm.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return false
}
// 验证码校验
if (!this.editForm.phone_code || this.editForm.phone_code.length !== 6) {
uni.showToast({ title: '请输入6位验证码', icon: 'none' })
return false
}
const idReg = /^(?:\d{15}|\d{17}[\dXx])$/
if (!idReg.test(this.editForm.id_card)) {
uni.showToast({ title: '请输入正确的证件号', icon: 'none' })
return false
}
return true
},
async submitUserInfo() {
if (!this.validateForm()) return
const token = uni.getStorageSync('token')
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
try {
uni.showLoading({ title: '提交中...' })
const requestData = {
name: this.editForm.name,
payer_type: Number(this.editForm.payer_type),
phone: this.editForm.phone,
phone_code: this.editForm.phone_code,
id_card: this.editForm.id_card
}
// 如果是单位类型,添加单位名称
if (Number(this.editForm.payer_type) === 2) {
requestData.company_name = this.editForm.company_name
}
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.UPDATE_USER_INFO}?token=${token}`,
method: 'POST',
data: requestData,
success: resolve,
fail: reject
})
})
uni.hideLoading()
if (res.data && res.data.errcode === 0) {
uni.showToast({ title: '更新成功', icon: 'success' })
this.closeEditPopup()
// 先重新获取用户信息
await this.fetchUserInfo()
// 不立即刷新页面,而是提示是否验证备用手机号
setTimeout(() => {
uni.showModal({
title: '提示',
content: '是否需要立即验证备用手机号码?',
confirmText: '需要',
cancelText: '稍后再说',
success: (modalRes) => {
if (modalRes.confirm) {
// 跳转到备用手机号验证页面
uni.navigateTo({
url: '/pages/index/verify_backup_phone',
success: () => {
console.log('跳转成功')
},
fail: (err) => {
console.error('跳转失败:', err)
uni.showToast({ title: '跳转失败,请重试', icon: 'none' })
}
})
} else {
// 稍后再说,用户信息已经刷新过了
console.log('用户选择稍后再说')
}
}
})
}, 500)
} else {
uni.showToast({ title: res.data.errmsg || '更新失败', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
console.error('更新用户信息失败:', e)
uni.showToast({ title: '更新失败', icon: 'none' })
}
},
// 发送短信验证码
async sendSmsCode() {
// 验证手机号
const phoneReg = /^1\d{10}$/
if (!phoneReg.test(this.editForm.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
if (!this.canSendCode || this.countdown > 0) {
return
}
const token = uni.getStorageSync('token')
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
try {
uni.showLoading({ title: '发送中...' })
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.PROFILE_SEND_PHONE_CODE}?token=${token}`,
method: 'POST',
data: {
phone: this.editForm.phone,
type: 'phone'
},
success: resolve,
fail: reject
})
})
uni.hideLoading()
if (res.data && res.data.errcode === 0) {
uni.showToast({ title: '验证码已发送', icon: 'success' })
// 开始倒计时
this.canSendCode = false
this.countdown = 60
this.countdownTimer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
this.canSendCode = true
}
}, 1000)
} else {
uni.showToast({
title: (res.data && res.data.errmsg) || '发送失败',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '发送失败', icon: 'none' })
}
},
onItemClick(item) {
if (item.text === '个人信息') {
this.openEditPopup()
}
else if (item.text === '船舶信息') {
uni.navigateTo({ url: '/pages/index/ship_manage' });
}
else if (item.text === '我的预约') {
uni.switchTab({ url: '/pages/order/index' });
}
else if (item.text === '我的购票') {
uni.switchTab({ url: '/pages/order/index' });
}
else if (item.text === '我的发票') {
uni.navigateTo({ url: '/pages/index/invoice_manage' });
}
else if (item.text === '关于我们') {
uni.navigateTo({ url: '/pages/my/about' });
}
else {
uni.showToast({ title: item.text, icon: 'none' })
}
}
}
}
</script>
<style lang="scss">
.profile-container {
min-height: 100vh;
background: linear-gradient(180deg, #cbe6ff 0%, #f6faff 100%);
padding-top: 88rpx;
padding-bottom: 100rpx;
}
.wechat-browser {
margin-top: -88rpx;
}
.profile-header {
display: flex;
align-items: center;
padding: 40rpx 30rpx 20rpx 30rpx;
position: relative;
margin-top: 44rpx;
}
.profile-avatar image {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: #fff;
border: 2rpx solid #e0e0e0;
}
.profile-info {
flex: 1;
margin-left: 24rpx;
}
.profile-name {
font-size: 36rpx;
font-weight: bold;
color: #222;
}
.profile-id {
font-size: 24rpx;
color: #7a8599;
margin-top: 8rpx;
}
.profile-contact {
font-size: 24rpx;
color: #7a8599;
margin-top: 8rpx;
}
.profile-setting {
position: absolute;
right: 30rpx;
top: 40rpx;
}
.setting-placeholder {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
color: #888;
font-size: 28rpx;
font-weight: bold;
margin-top: 25rpx;
}
.profile-card {
background: #fff;
border-radius: 24rpx;
margin: 20rpx 20rpx 0 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03);
overflow: hidden;
}
.profile-item {
display: flex;
align-items: center;
padding: 32rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
font-size: 30rpx;
color: #222;
position: relative;
}
.profile-item:last-child {
border-bottom: none;
}
.profile-item .iconfont {
font-size: 40rpx;
margin-right: 24rpx;
color: #3b82f6;
}
.profile-item .icon-arrow-right {
margin-left: auto;
color: #c0c4cc;
font-size: 32rpx;
}
.item-text {
flex: 1;
}
.tabbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 100rpx;
background: #fff;
display: flex;
border-top: 1rpx solid #f0f0f0;
z-index: 10;
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #888;
font-size: 24rpx;
}
.tabbar-item .iconfont {
font-size: 40rpx;
margin-bottom: 4rpx;
}
.tabbar-item.active {
color: #3b82f6;
}
.setting-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e0e0e0;
margin-top: 25rpx;
}
.profile-list-icon {
width: 40rpx;
height: 40rpx;
margin-right: 24rpx;
vertical-align: middle;
}
.first-card {
margin-bottom: 38rpx;
}
.profile-arrow-icon {
width: 38rpx;
height: 38rpx;
margin-left: auto;
display: block;
}
.edit-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
justify-content: center;
align-items: flex-end;
z-index: 9999;
}
.edit-popup {
background: #fff;
width: 100%;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
padding: 32rpx;
box-sizing: border-box;
max-height: 80vh;
overflow-y: auto;
}
.edit-popup-header {
display: flex;
justify-content: center;
position: relative;
font-size: 32rpx;
font-weight: 600;
color: #222;
}
.edit-popup-close {
position: absolute;
right: 0;
top: 0;
font-size: 44rpx;
color: #999;
padding: 0 16rpx;
}
.edit-popup-content {
margin-top: 32rpx;
}
.edit-field {
margin-bottom: 28rpx;
}
.edit-label {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
}
.edit-input {
width: 100%;
height: 80rpx;
border-radius: 12rpx;
border: 1rpx solid #e5e6eb;
padding: 0 24rpx;
font-size: 28rpx;
box-sizing: border-box;
background: #fafafa;
}
.payer-type-group {
display: flex;
gap: 20rpx;
}
.payer-type-item {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
border: 1rpx solid #e5e6eb;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #666;
}
.payer-type-item.active {
border-color: #3b7cff;
color: #3b7cff;
background: #edf3ff;
}
.edit-popup-footer {
margin-top: 12rpx;
}
.edit-submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
background: linear-gradient(90deg, #3b7cff 0%, #5bb6ff 100%);
color: #fff;
font-size: 32rpx;
font-weight: 500;
border: none;
}
.edit-submit-btn::after {
border: none;
}
.edit-value {
font-size: 28rpx;
color: #222;
padding: 20rpx 0;
}
.verify-code-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.verify-code-input {
flex: 1;
}
.send-code-btn {
min-width: 180rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 12rpx;
background: #3b7cff;
color: #fff;
font-size: 24rpx;
border: none;
padding: 0 24rpx;
white-space: nowrap;
}
.send-code-btn::after {
border: none;
}
.send-code-btn:disabled {
background: #ccc;
color: #999;
}
.hint-text {
display: block;
font-size: 24rpx;
color: #999;
margin-top: 12rpx;
}
</style>