Files
PartsInquiry/frontend/pages/my/index.vue
2025-10-08 19:15:20 +08:00

367 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="me">
<view class="card user" v-if="isLoggedIn">
<image class="avatar" :src="avatarDisplay" mode="aspectFill" @error="onAvatarError" />
<view class="meta">
<text class="name">{{ shopName }}</text>
<text class="phone">{{ emailDisplay }}</text>
<text class="role">老板</text>
</view>
</view>
<view v-else class="card user guest">
<image class="avatar" src="/static/icons/icons8-login-50.png" mode="aspectFill" />
<view class="meta">
<text class="name">未登录</text>
<text class="phone">登录后同步数据</text>
<text class="role">访客</text>
</view>
<button class="login-entry" @click="goLogin">登录</button>
</view>
<!-- VIP 卡片置于会员与订单分组上方 -->
<view v-if="isLoggedIn" class="card vip" :class="{ active: vipIsVip }">
<view class="vip-row">
<text class="vip-badge">{{ vipIsVip ? 'VIP' : '非VIP' }}</text>
<text class="vip-title">会员状态</text>
</view>
<view class="vip-meta">
<view class="item">
<text class="label">开始</text>
<text class="value">{{ vipStartDisplay }}</text>
</view>
<view class="item">
<text class="label">结束</text>
<text class="value">{{ vipEndDisplay }}</text>
</view>
</view>
</view>
<view class="group">
<view class="group-title">会员与订单</view>
<view class="cell" @click="goVip">
<view class="cell-left">
<text>VIP会员</text>
<text v-if="vipIsVip" class="vip-tag">已开通</text>
<text v-else class="vip-tag pending">待开通</text>
</view>
<text class="arrow"></text>
</view>
<view class="cell" @click="goMyOrders">
<text>我的订单</text>
<text class="arrow"></text>
</view>
</view>
<view class="group">
<view class="group-title">设置中心</view>
<view class="cell" @click="editProfile">
<text>账号与安全</text>
<text class="desc">修改头像姓名密码电话</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goAbout">
<text>关于与协议</text>
<text class="arrow"></text>
</view>
<view v-if="isLoggedIn" class="cell danger" @click="logout">
<text>退出登录</text>
</view>
</view>
</view>
</template>
<script>
import { get } from '../../common/http.js'
import { API_BASE_URL } from '../../common/config.js'
function normalizeAvatar(url) {
if (!url) return '/static/icons/icons8-mitt-24.png'
const s = String(url)
if (/^https?:\/\//i.test(s)) return s
if (!API_BASE_URL) return s
if (s.startsWith('/')) return `${API_BASE_URL}${s}`
return `${API_BASE_URL}/${s}`
}
export default {
data() {
return {
avatarUrl: '/static/icons/icons8-mitt-24.png',
shopName: '未登录',
mobile: '',
pendingJsCode: '',
logging: false,
vipIsVip: false,
vipStart: '',
vipEnd: ''
}
},
onShow() {
this.fetchProfile()
this.loadVip()
try {
if (uni.getStorageSync('TOKEN')) {
// 已登录时刷新资料并隐藏登录卡片
this.$forceUpdate && this.$forceUpdate()
}
} catch(e) {}
},
computed: {
isLoggedIn() { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } },
avatarDisplay() { return normalizeAvatar(this.avatarUrl) },
emailDisplay() {
if (!this.isLoggedIn) return ''
const e = String(uni.getStorageSync('USER_EMAIL') || '')
if (!e) return '未绑定邮箱'
const at = e.indexOf('@')
if (at > 1) {
const name = e.slice(0, at)
const domain = e.slice(at)
return (name.length <= 2 ? name[0] + '*' : name.slice(0,2) + '***') + domain
}
return e
},
vipStartDisplay() { return this.formatDisplay(this.vipStart) },
vipEndDisplay() { return this.formatDisplay(this.vipEnd) }
},
methods: {
// 登录相关方法已移除
async fetchProfile() {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
this.shopName = '未登录'
this.avatarUrl = '/static/icons/icons8-mitt-24.png'
this.mobile = ''
return
}
try {
const profile = await get('/api/user/me')
const latestAvatar = profile?.avatarUrl || ''
if (latestAvatar) {
const bust = `${latestAvatar}${latestAvatar.includes('?') ? '&' : '?'}t=${Date.now()}`
this.avatarUrl = bust
try {
uni.setStorageSync('USER_AVATAR_RAW', latestAvatar)
uni.setStorageSync('USER_AVATAR', latestAvatar)
} catch(_){}
} else {
const cached = uni.getStorageSync('USER_AVATAR') || ''
this.avatarUrl = cached || '/static/icons/icons8-mitt-24.png'
}
const storeName = profile?.name || uni.getStorageSync('SHOP_NAME') || '未命名店铺'
this.shopName = storeName
const phone = profile?.phone || uni.getStorageSync('USER_MOBILE') || ''
this.mobile = phone
// 保存邮箱到本地存储
const email = profile?.email || ''
try {
if (email) {
uni.setStorageSync('USER_EMAIL', email)
} else {
uni.removeStorageSync('USER_EMAIL')
}
} catch(_){}
} catch(e) {
try {
const storeName = uni.getStorageSync('SHOP_NAME') || ''
const avatar = uni.getStorageSync('USER_AVATAR') || ''
const phone = uni.getStorageSync('USER_MOBILE') || ''
if (storeName) this.shopName = storeName
if (avatar) this.avatarUrl = avatar
this.mobile = phone
} catch(_){}
}
},
async loadVip() {
try {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
// 未登录:不读取本地缓存,直接复位为默认值并隐藏卡片(由 v-if 控制)
this.vipIsVip = false
this.vipStart = ''
this.vipEnd = ''
return
}
const data = await get('/api/vip/status')
const active = !!data?.isVip
this.vipIsVip = active
this.vipEnd = data?.expireAt || ''
// 根据结束时间动态计算开始时间(固定退 1 个月)
let computedStart = ''
const exp = this.vipEnd
if (exp) {
const m = String(exp).match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?/)
if (m) {
const y = Number(m[1])
const mo = Number(m[2]) - 1
const da = Number(m[3])
const hh = Number(m[4] || '0')
const mm = Number(m[5] || '0')
const ss = Number(m[6] || '0')
const startDate = new Date(y, mo - 1, da, hh, mm, ss)
const y2 = startDate.getFullYear()
const m2 = (startDate.getMonth() + 1).toString().padStart(2, '0')
const d2 = startDate.getDate().toString().padStart(2, '0')
const h2 = startDate.getHours().toString().padStart(2, '0')
const i2 = startDate.getMinutes().toString().padStart(2, '0')
computedStart = `${y2}-${m2}-${d2} ${h2}:${i2}`
}
}
// 写入到本地与内存
this.vipStart = computedStart
// 同步到本地,便于其他页面读取
try {
uni.setStorageSync('USER_VIP_IS_VIP', String(active))
uni.setStorageSync('USER_VIP_END', this.vipEnd)
if (this.vipStart) uni.setStorageSync('USER_VIP_START', this.vipStart); else uni.removeStorageSync('USER_VIP_START')
} catch(_) {}
} catch(e) {
// 网络异常回退到缓存
try {
const isVip = String(uni.getStorageSync('USER_VIP_IS_VIP') || 'false').toLowerCase() === 'true'
this.vipIsVip = isVip
this.vipStart = uni.getStorageSync('USER_VIP_START') || ''
this.vipEnd = uni.getStorageSync('USER_VIP_END') || ''
} catch(_) {}
}
},
formatDisplay(value) {
if (!value) return '-'
const s = String(value)
// 仅显示“YYYY-MM-DD”
const m = s.match(/^(\d{4}-\d{2}-\d{2})/)
if (m) return m[1]
const d = new Date(s)
if (!isNaN(d.getTime())) {
const y = d.getFullYear()
const mo = String(d.getMonth() + 1).padStart(2, '0')
const da = String(d.getDate()).padStart(2, '0')
return `${y}-${mo}-${da}`
}
return s
},
startLogin() {
if (this.logging) return
this.logging = true
const tryOnce = async () => ({})
uni.login({ provider: 'weixin', success: async (res) => {
this.pendingJsCode = res.code || ''
if (!this.pendingJsCode) { this.logging = false; return uni.showToast({ title: '获取登录code失败', icon: 'none' }) }
try {
await tryOnce()
} catch(e) {
const msg = (e && e.message) || ''
if (msg.includes('40163') || msg.toLowerCase().includes('been used')) {
// 40163换新 code 再试一次
uni.login({ provider: 'weixin', success: async (r2) => {
const fresh = r2.code || ''
if (!fresh) { this.logging = false; return }
try {
await tryOnce()
} finally { this.logging = false }
} })
return
}
} finally {
this.logging = false
}
}, fail: () => { this.logging = false; uni.showToast({ title: '微信登录失败', icon: 'none' }) } })
},
goLogin(){ uni.navigateTo({ url: '/pages/auth/login' }) },
onGetPhoneNumber(e) {
if (this.logging) return
this.logging = true
const phoneCode = ''
// 为避免 40163code been used此处重新获取一次 jsCode
uni.login({ provider: 'weixin', success: (res) => {
const jsCode = res.code || ''
if (!jsCode) { this.logging = false; return uni.showToast({ title: '获取登录code失败', icon: 'none' }) }
Promise.resolve().finally(() => { this.logging = false })
}, fail: () => { this.logging = false; uni.showToast({ title: '微信登录失败', icon: 'none' }) } })
},
goSmsLogin(){ uni.navigateTo({ url: '/pages/my/sms-login' }) },
onAvatarError() {
this.avatarUrl = '/static/icons/icons8-mitt-24.png'
},
goVip() { uni.navigateTo({ url: '/pages/my/vip' }) },
goMyOrders() { uni.navigateTo({ url: '/pages/my/orders' }) },
editProfile() { uni.navigateTo({ url: '/pages/my/security' }) },
goAbout() { uni.navigateTo({ url: '/pages/my/about' }) },
logout() {
try {
uni.removeStorageSync('TOKEN')
uni.removeStorageSync('LOGINED')
uni.removeStorageSync('LOGIN_PHONE')
uni.removeStorageSync('DEFAULT_USER_ID')
uni.setStorageSync('ENABLE_DEFAULT_USER', 'false')
uni.removeStorageSync('USER_AVATAR')
uni.removeStorageSync('USER_AVATAR_RAW')
uni.removeStorageSync('USER_NAME')
uni.removeStorageSync('USER_MOBILE')
uni.removeStorageSync('USER_EMAIL')
uni.removeStorageSync('SHOP_NAME')
uni.removeStorageSync('USER_VIP_IS_VIP')
uni.removeStorageSync('USER_VIP_START')
uni.removeStorageSync('USER_VIP_END')
uni.showToast({ title: '已清理本地信息', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
} catch(e) { uni.reLaunch({ url: '/pages/index/index' }) }
}
}
}
</script>
<style lang="scss">
.me { padding: 24rpx; }
.card.login { display: flex; flex-direction: column; gap: 16rpx; padding: 22rpx; background: $uni-bg-color-grey; border-radius: 16rpx; margin-bottom: 24rpx; }
.login-title { font-size: 28rpx; font-weight: 700; }
.login-btn { }
.login-btn.minor { background: $uni-bg-color-hover; color: $uni-text-color; }
.hint { font-size: 22rpx; color: $uni-text-color-grey; }
.card.user { display: flex; gap: 18rpx; padding: 22rpx; background: $uni-bg-color-grey; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.16); align-items: center; }
.card.user.guest { justify-content: space-between; }
.card.user.guest .meta { flex: 1; }
.card.user.guest .login-entry { padding: 12rpx 30rpx; border-radius: 999rpx; background: $uni-color-primary; color: #fff; font-size: 28rpx; font-weight: 600; }
.avatar { width: 120rpx; height: 120rpx; border-radius: 60rpx; background: $uni-bg-color-hover; }
.meta { display: flex; flex-direction: column; gap: 6rpx; }
.name { font-size: 34rpx; font-weight: 700; color: $uni-text-color; }
.phone { font-size: 26rpx; color: $uni-text-color-grey; }
.role { font-size: 22rpx; color: $uni-text-color-grey; }
/* VIP 卡片样式 */
.card.vip { margin-top: 24rpx; padding: 22rpx; background: $uni-bg-color-grey; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.12); }
.card.vip.active { border: 1rpx solid rgba(255, 208, 0, 0.6); background-image: radial-gradient(60% 60% at 80% 0%, rgba(255, 214, 0, 0.08), transparent 60%); }
.vip-row { display: flex; align-items: center; gap: 12rpx; margin-bottom: 10rpx; }
.vip-badge { background: #f1c40f; color: #111; font-weight: 800; padding: 2rpx 10rpx; border-radius: 8rpx; font-size: 22rpx; }
.vip-title { font-size: 28rpx; font-weight: 700; color: $uni-text-color; }
.vip-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 8rpx 16rpx; }
.vip-meta .item { display: flex; align-items: center; gap: 10rpx; }
.vip-meta .label { width: 80rpx; color: $uni-text-color-grey; font-size: 24rpx; }
.vip-meta .value { color: $uni-text-color; font-size: 26rpx; word-break: break-all; }
.group { margin-top: 24rpx; background: $uni-bg-color-grey; border-radius: 16rpx; overflow: hidden; }
.group-title { padding: 18rpx 22rpx; font-size: 26rpx; color: $uni-text-color-grey; background: $uni-bg-color-hover; }
.cell { display: flex; align-items: center; padding: 26rpx 22rpx; border-top: 1rpx solid $uni-border-color; color: $uni-text-color; gap: 18rpx; }
.cell-left { display:flex; align-items:center; gap: 14rpx; }
.vip-tag { padding: 4rpx 12rpx; border-radius: 999rpx; background: rgba(76,141,255,0.15); color: $uni-color-primary; font-size: 22rpx; }
.vip-tag.pending { background: rgba(76,141,255,0.06); color: #99a2b3; }
.cell .desc { margin-left: auto; margin-right: 8rpx; font-size: 22rpx; color: $uni-text-color-grey; }
.cell .arrow { margin-left: auto; color: #99a2b3; }
.cell.danger { color: #dd524d; justify-content: center; font-weight: 700; }
/* 简易对话框样式 */
.dialog-mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; z-index: 999; }
.dialog { width: 600rpx; background: #fff; border-radius: 16rpx; padding: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.2); }
.dialog-title { font-size: 30rpx; font-weight: 700; margin-bottom: 16rpx; }
.dialog-input { width: 100%; height: 72rpx; padding: 0 16rpx; border: 1rpx solid $uni-border-color; border-radius: 10rpx; background: #fff; color: $uni-text-color; }
.dialog-actions { display: flex; gap: 16rpx; margin-top: 18rpx; justify-content: flex-end; }
.dialog-btn { padding: 16rpx 22rpx; border-radius: 10rpx; }
.dialog-btn.cancel { background: $uni-bg-color-hover; color: $uni-text-color; }
.dialog-btn.confirm { background: #2979ff; color: #fff; }
</style>