698 lines
15 KiB
Plaintext
698 lines
15 KiB
Plaintext
<template>
|
||
<view class="page-root">
|
||
<scroll-view class="settings-scroll" direction="vertical" :scroll-y="true">
|
||
<view class="page-inner">
|
||
<view class="section-wrap account-section">
|
||
<text class="section-title">管理我的账户</text>
|
||
<view class="account-card" @click="goToProfile">
|
||
<image class="account-avatar" :src="userAvatar" mode="aspectFill" />
|
||
<view class="account-main">
|
||
<view class="account-name-row">
|
||
<text class="account-name">{{ getDisplayName() }}</text>
|
||
<text class="account-badge">当前登录</text>
|
||
</view>
|
||
<text class="account-subline">{{ getAccountSubtitle() }}</text>
|
||
</view>
|
||
<view class="account-edit" @click.stop="goToProfile">
|
||
<text class="edit-icon">✎</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-wrap">
|
||
<text class="section-title">用户设置</text>
|
||
<view class="menu-card">
|
||
<view class="menu-row" @click="goToAddressList">
|
||
<view class="menu-left">
|
||
<view class="menu-icon-shell menu-icon-shell-address">
|
||
<image class="menu-icon-image icon-address" src="/static/consumer/location.png" mode="aspectFit" />
|
||
</view>
|
||
<text class="menu-title">我的收货地址</text>
|
||
</view>
|
||
<view class="menu-right">
|
||
<text class="menu-right-desc">管理我的地址</text>
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="menu-row" @click="openAccountSecurity">
|
||
<view class="menu-left">
|
||
<view class="menu-icon-shell menu-icon-shell-security">
|
||
<image class="menu-icon-image icon-security" src="/static/consumer/setting.png" mode="aspectFit" />
|
||
</view>
|
||
<text class="menu-title">账户与安全</text>
|
||
</view>
|
||
<view class="menu-right">
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-wrap">
|
||
<text class="section-title">功能设置</text>
|
||
<view class="menu-card">
|
||
<view class="menu-row" @click="openPrivacySettings">
|
||
<view class="menu-left">
|
||
<view class="menu-icon-shell menu-icon-shell-privacy">
|
||
<image class="menu-icon-image icon-privacy" src="/static/consumer/privacy_setting.png" mode="aspectFit" />
|
||
</view>
|
||
<text class="menu-title">隐私设置</text>
|
||
</view>
|
||
<view class="menu-right">
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="menu-row" @click="openDefaultHomeSettings">
|
||
<view class="menu-left">
|
||
<view class="menu-icon-shell menu-icon-shell-home">
|
||
<image class="menu-icon-image icon-home" src="/static/consumer/default_index.png" mode="aspectFit" />
|
||
</view>
|
||
<text class="menu-title">默认主页</text>
|
||
</view>
|
||
<view class="menu-right">
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="menu-row" @click="openSeniorVersionSettings">
|
||
<view class="menu-left">
|
||
<view class="menu-icon-shell menu-icon-shell-senior">
|
||
<image class="menu-icon-image icon-senior" src="/static/consumer/change.png" mode="aspectFit" />
|
||
</view>
|
||
<text class="menu-title">长辈版本</text>
|
||
</view>
|
||
<view class="menu-right">
|
||
<text class="menu-right-desc">未开启</text>
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="menu-row" @click="contactService">
|
||
<view class="menu-left">
|
||
<view class="menu-icon-shell menu-icon-shell-service">
|
||
<image class="menu-icon-image icon-service" src="/static/consumer/bell.png" mode="aspectFit" />
|
||
</view>
|
||
<text class="menu-title">联系客服</text>
|
||
</view>
|
||
<view class="menu-right">
|
||
<text class="menu-right-desc">在线客服</text>
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="menu-row menu-row-last" @click="feedback">
|
||
<view class="menu-left">
|
||
<view class="menu-icon-shell menu-icon-shell-feedback">
|
||
<image class="menu-icon-image icon-feedback" src="/static/consumer/feedback.png" mode="aspectFit" />
|
||
</view>
|
||
<text class="menu-title">建议反馈</text>
|
||
</view>
|
||
<view class="menu-right">
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="logout-wrap">
|
||
<button class="logout-button" @click="showLogoutConfirm">退出登录</button>
|
||
</view>
|
||
|
||
<view class="bottom-safe"></view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { onBackPress, onShow } from '@dcloudio/uni-app'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import { goToLogin } from '@/utils/utils.uts'
|
||
import { logout as logoutStore } from '@/utils/store.uts'
|
||
|
||
type UserInfoLite = {
|
||
id: string
|
||
phone: string | null
|
||
email: string | null
|
||
nickname: string | null
|
||
avatar_url: string | null
|
||
}
|
||
|
||
const userInfo = ref<UserInfoLite>({
|
||
id: '',
|
||
phone: null,
|
||
email: null,
|
||
nickname: null,
|
||
avatar_url: null
|
||
})
|
||
const userAvatar = ref<string>('/static/consumer/defaul_picture.png')
|
||
const isLoggingOut = ref<boolean>(false)
|
||
|
||
onBackPress((_options): boolean => {
|
||
uni.switchTab({
|
||
url: '/pages/main/profile'
|
||
})
|
||
return true
|
||
})
|
||
|
||
const createEmptyUserInfo = (): UserInfoLite => {
|
||
return {
|
||
id: '',
|
||
phone: null,
|
||
email: null,
|
||
nickname: null,
|
||
avatar_url: null
|
||
}
|
||
}
|
||
|
||
const normalizeAvatar = (avatarUrl: string | null): string => {
|
||
if (avatarUrl == null || avatarUrl == '') {
|
||
return '/static/consumer/defaul_picture.png'
|
||
}
|
||
return avatarUrl
|
||
}
|
||
|
||
const loadUserInfo = (): void => {
|
||
const storedUser = uni.getStorageSync('userInfo')
|
||
if (storedUser != null && storedUser != '') {
|
||
let rawUser: UTSJSONObject | null = null
|
||
if (storedUser instanceof UTSJSONObject) {
|
||
rawUser = storedUser as UTSJSONObject
|
||
} else if (typeof storedUser == 'string') {
|
||
const storedText = storedUser as string
|
||
if (storedText != '') {
|
||
try {
|
||
rawUser = JSON.parse(storedText) as UTSJSONObject
|
||
} catch (e) {
|
||
console.error('[settings] 解析 userInfo 失败', e)
|
||
}
|
||
}
|
||
} else {
|
||
rawUser = JSON.parse(JSON.stringify(storedUser)) as UTSJSONObject
|
||
}
|
||
|
||
if (rawUser != null) {
|
||
const nextInfo: UserInfoLite = {
|
||
id: rawUser.getString('id') ?? '',
|
||
phone: rawUser.getString('phone'),
|
||
email: rawUser.getString('email'),
|
||
nickname: rawUser.getString('nickname'),
|
||
avatar_url: rawUser.getString('avatar_url')
|
||
}
|
||
userInfo.value = nextInfo
|
||
userAvatar.value = normalizeAvatar(nextInfo.avatar_url)
|
||
return
|
||
}
|
||
}
|
||
|
||
userInfo.value = createEmptyUserInfo()
|
||
userAvatar.value = '/static/consumer/defaul_picture.png'
|
||
}
|
||
|
||
const clearAuthStorage = (): void => {
|
||
const keys: Array<string> = [
|
||
'userInfo',
|
||
'user_id',
|
||
'access_token',
|
||
'refresh_token',
|
||
'token',
|
||
'currentUser',
|
||
'current_user',
|
||
'user',
|
||
'auth_user',
|
||
'supabase.auth.token'
|
||
]
|
||
|
||
for (let i = 0; i < keys.length; i++) {
|
||
try {
|
||
uni.removeStorageSync(keys[i])
|
||
} catch (e) {
|
||
console.error('[settings] 清理登录态失败', keys[i], e)
|
||
}
|
||
}
|
||
}
|
||
|
||
const resetLocalUserInfo = (): void => {
|
||
userInfo.value = createEmptyUserInfo()
|
||
userAvatar.value = '/static/consumer/defaul_picture.png'
|
||
}
|
||
|
||
const getStoredUserId = (): string => {
|
||
if (userInfo.value.id != null && userInfo.value.id != '') {
|
||
return userInfo.value.id
|
||
}
|
||
|
||
const cachedId = uni.getStorageSync('user_id') as string | null
|
||
if (cachedId != null && cachedId != '') {
|
||
return cachedId
|
||
}
|
||
|
||
return ''
|
||
}
|
||
|
||
const maskPhone = (phone: string): string => {
|
||
if (phone == null || phone == '') {
|
||
return ''
|
||
}
|
||
|
||
if (phone.length < 7) {
|
||
return phone
|
||
}
|
||
|
||
return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4)
|
||
}
|
||
|
||
const shortUserId = (id: string): string => {
|
||
if (id == null || id == '') {
|
||
return ''
|
||
}
|
||
|
||
if (id.length <= 12) {
|
||
return id
|
||
}
|
||
|
||
return id.substring(0, 7) + '...' + id.substring(id.length - 5)
|
||
}
|
||
|
||
const getDisplayName = (): string => {
|
||
const nickname = userInfo.value.nickname
|
||
if (nickname != null && nickname.trim() != '') {
|
||
return nickname.trim()
|
||
}
|
||
|
||
return '用户'
|
||
}
|
||
|
||
const getAccountSubtitle = (): string => {
|
||
const phone = userInfo.value.phone
|
||
if (phone != null && phone != '') {
|
||
return '手机号:' + maskPhone(phone)
|
||
}
|
||
|
||
const userId = userInfo.value.id
|
||
if (userId != null && userId != '') {
|
||
return '账号ID:' + shortUserId(userId)
|
||
}
|
||
|
||
return '账号信息待完善'
|
||
}
|
||
|
||
const showComingSoonToast = (): void => {
|
||
uni.showToast({
|
||
title: '功能建设中',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
const goToProfile = (): void => {
|
||
uni.navigateTo({
|
||
url: '/pages/user/profile'
|
||
})
|
||
}
|
||
|
||
const goToAddressList = (): void => {
|
||
const userId = getStoredUserId()
|
||
if (userId == '') {
|
||
goToLogin('/pages/mall/consumer/address-list')
|
||
return
|
||
}
|
||
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/address-list'
|
||
})
|
||
}
|
||
|
||
const openAccountSecurity = (): void => {
|
||
showComingSoonToast()
|
||
}
|
||
|
||
const openPrivacySettings = (): void => {
|
||
showComingSoonToast()
|
||
}
|
||
|
||
const openDefaultHomeSettings = (): void => {
|
||
showComingSoonToast()
|
||
}
|
||
|
||
const openSeniorVersionSettings = (): void => {
|
||
showComingSoonToast()
|
||
}
|
||
|
||
const contactService = (): void => {
|
||
const userId = getStoredUserId()
|
||
if (userId == '') {
|
||
goToLogin('/pages/mall/consumer/chat')
|
||
return
|
||
}
|
||
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/chat'
|
||
})
|
||
}
|
||
|
||
const feedback = (): void => {
|
||
showComingSoonToast()
|
||
}
|
||
|
||
const executeLogout = async (): Promise<void> => {
|
||
if (isLoggingOut.value) {
|
||
return
|
||
}
|
||
|
||
isLoggingOut.value = true
|
||
uni.showLoading({
|
||
title: '正在退出登录...'
|
||
})
|
||
|
||
try {
|
||
logoutStore()
|
||
|
||
try {
|
||
await supa.signOut()
|
||
} catch (signOutError) {
|
||
console.error('[settings] supa.signOut failed:', signOutError)
|
||
}
|
||
|
||
clearAuthStorage()
|
||
resetLocalUserInfo()
|
||
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '退出成功',
|
||
icon: 'success',
|
||
duration: 1200
|
||
})
|
||
|
||
uni.$emit('authChanged', { loggedIn: false })
|
||
|
||
setTimeout((): void => {
|
||
uni.switchTab({
|
||
url: '/pages/main/profile'
|
||
})
|
||
}, 1000)
|
||
} catch (e) {
|
||
console.error('[settings] 退出登录失败', e)
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '退出失败,请稍后重试',
|
||
icon: 'none',
|
||
duration: 1500
|
||
})
|
||
} finally {
|
||
isLoggingOut.value = false
|
||
}
|
||
}
|
||
|
||
const showLogoutConfirm = (): void => {
|
||
if (isLoggingOut.value) {
|
||
return
|
||
}
|
||
|
||
uni.showModal({
|
||
title: '退出登录',
|
||
content: '确定要退出当前账号吗?',
|
||
confirmText: '退出',
|
||
cancelText: '取消',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
executeLogout()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadUserInfo()
|
||
})
|
||
|
||
onShow(() => {
|
||
loadUserInfo()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-root {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #f5f5f7;
|
||
}
|
||
|
||
.settings-scroll {
|
||
flex: 1;
|
||
height: 0;
|
||
width: 100%;
|
||
background-color: #f5f5f7;
|
||
}
|
||
|
||
.page-inner {
|
||
width: 100%;
|
||
min-height: 100%;
|
||
padding: 14rpx 0 24rpx 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.section-wrap {
|
||
margin-bottom: 14rpx;
|
||
}
|
||
|
||
.account-section {
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.section-title {
|
||
display: block;
|
||
padding: 0 16px;
|
||
margin-bottom: 7px;
|
||
font-size: 13px;
|
||
color: #999999;
|
||
line-height: 18px;
|
||
}
|
||
|
||
.account-card {
|
||
margin: 0 16px;
|
||
padding: 14px;
|
||
border-radius: 14px;
|
||
background-color: #ffffff;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.account-avatar {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: 26px;
|
||
background-color: #edf0f3;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.account-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-left: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
|
||
.account-name-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.account-name {
|
||
max-width: 100%;
|
||
font-size: 16px;
|
||
color: #222222;
|
||
font-weight: 600;
|
||
lines: 1;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.account-badge {
|
||
margin-left: 8px;
|
||
padding: 0 8px;
|
||
height: 20px;
|
||
line-height: 20px;
|
||
border-radius: 999px;
|
||
background-color: #eef1f4;
|
||
color: #6f7682;
|
||
font-size: 11px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.account-subline {
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
color: #999999;
|
||
lines: 1;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.account-edit {
|
||
margin-left: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.edit-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
font-size: 18px;
|
||
color: #b5b5b5;
|
||
line-height: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.menu-card {
|
||
margin: 0 16px;
|
||
background-color: #ffffff;
|
||
border-radius: 14px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.menu-row {
|
||
height: 54px;
|
||
padding: 0 14px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
box-sizing: border-box;
|
||
border-bottom-width: 1px;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: #f1f2f4;
|
||
}
|
||
|
||
.menu-row-last {
|
||
border-bottom-width: 0;
|
||
}
|
||
|
||
.menu-left {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.menu-icon-shell {
|
||
width: 26px;
|
||
height: 26px;
|
||
background-color: transparent;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.menu-icon-image {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.menu-title {
|
||
margin-left: 12px;
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 15px;
|
||
color: #222222;
|
||
line-height: 22px;
|
||
font-weight: 400;
|
||
lines: 1;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.menu-right {
|
||
margin-left: 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.menu-right-desc {
|
||
font-size: 13px;
|
||
color: #999999;
|
||
line-height: 20px;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.menu-arrow {
|
||
margin-left: 6px;
|
||
font-size: 16px;
|
||
color: #b5b5b5;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.icon-address {
|
||
width: 21px;
|
||
height: 21px;
|
||
}
|
||
|
||
.icon-security {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.icon-privacy {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.icon-home {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.icon-senior {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.icon-service {
|
||
width: 19px;
|
||
height: 19px;
|
||
}
|
||
|
||
.icon-feedback {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.logout-wrap {
|
||
margin: 12px 16px 0 16px;
|
||
padding-bottom: 24rpx;
|
||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.logout-button {
|
||
width: 100%;
|
||
height: 52px;
|
||
line-height: 52px;
|
||
border-radius: 12px;
|
||
background-color: #ffffff;
|
||
color: #333333;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
}
|
||
|
||
.logout-button::after {
|
||
border: none;
|
||
}
|
||
|
||
.bottom-safe {
|
||
height: 12px;
|
||
}
|
||
</style>
|