610 lines
13 KiB
Vue
610 lines
13 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="page-bg"></view>
|
||
<view class="nav-placeholder" :style="{ height: navBarHeight + 'px' }"></view>
|
||
|
||
<view class="section">
|
||
<text class="section-label">当前账号</text>
|
||
<view class="user-section card">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
|
||
</button>
|
||
<!-- #endif -->
|
||
<!-- #ifdef H5 -->
|
||
<image class="avatar" :src="userAvatar" mode="aspectFill" @tap="onChooseAvatarH5"></image>
|
||
<!-- #endif -->
|
||
<view class="user-copy">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<input
|
||
type="nickname"
|
||
class="nickname-input"
|
||
:value="userStore.user?.nickname || ''"
|
||
placeholder="点击设置昵称"
|
||
@blur="onNicknameChange"
|
||
/>
|
||
<!-- #endif -->
|
||
<!-- #ifdef H5 -->
|
||
<text class="user-name">{{ userName }}</text>
|
||
<!-- #endif -->
|
||
<text class="user-desc">点击头像或昵称可修改</text>
|
||
<view class="user-meta">
|
||
<text class="user-pill">{{ modeText }}</text>
|
||
<text class="user-pill user-pill-muted">{{ shareToken ? '分享已启用' : '分享未生成' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="section-label">常用操作</text>
|
||
<view class="menu-list card">
|
||
<view class="menu-item">
|
||
<view class="menu-icon menu-icon-accent">
|
||
<text class="menu-glyph">享</text>
|
||
</view>
|
||
<view class="menu-content">
|
||
<text class="menu-label">分享戒烟记录</text>
|
||
<text class="menu-desc">{{ shareDesc }}</text>
|
||
<view class="menu-actions">
|
||
<text class="menu-action" @tap.stop="previewSharePage">预览分享页</text>
|
||
<text class="menu-action-sep">·</text>
|
||
<text class="menu-action" @tap.stop="handleRefreshShare">刷新</text>
|
||
</view>
|
||
</view>
|
||
<button class="share-btn" open-type="share" :disabled="shareLoading || !shareToken">
|
||
{{ shareLoading ? '生成中' : '去分享' }}
|
||
</button>
|
||
</view>
|
||
|
||
<view class="menu-divider"></view>
|
||
|
||
<view class="menu-item" @tap="goSupervisor">
|
||
<view class="menu-icon menu-icon-accent">
|
||
<text class="menu-glyph">督</text>
|
||
</view>
|
||
<view class="menu-content">
|
||
<text class="menu-label">监督人</text>
|
||
<text class="menu-desc">邀请朋友监督你,或查看你监督的人</text>
|
||
</view>
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="menu-divider"></view>
|
||
|
||
<view class="menu-item" @tap="goNSTI">
|
||
<view class="menu-icon menu-icon-nsti">
|
||
<text class="menu-glyph">测</text>
|
||
</view>
|
||
<view class="menu-content">
|
||
<text class="menu-label">赛博尼古丁测试</text>
|
||
<text class="menu-desc">{{ nstiDesc }}</text>
|
||
</view>
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="menu-divider"></view>
|
||
|
||
<view class="menu-item" @tap="goOnboarding">
|
||
<view class="menu-icon menu-icon-accent">
|
||
<text class="menu-glyph">问</text>
|
||
</view>
|
||
<view class="menu-content">
|
||
<text class="menu-label">重新填写问卷</text>
|
||
<text class="menu-desc">修改打卡模式、吸烟基线等个人信息</text>
|
||
</view>
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="section-label">更多设置</text>
|
||
<view class="menu-list card">
|
||
<view class="menu-item" @tap="clearCache">
|
||
<view class="menu-icon menu-icon-muted">
|
||
<text class="menu-glyph">清</text>
|
||
</view>
|
||
<view class="menu-content">
|
||
<text class="menu-label">清除缓存</text>
|
||
<text class="menu-desc">仅清理本地缓存,不影响云端数据</text>
|
||
</view>
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="menu-divider"></view>
|
||
|
||
<view class="menu-item" @tap="copyInfo">
|
||
<view class="menu-icon menu-icon-muted">
|
||
<text class="menu-glyph">邮</text>
|
||
</view>
|
||
<view class="menu-content">
|
||
<text class="menu-label">意见反馈</text>
|
||
<text class="menu-desc">复制反馈邮箱,发送使用建议或问题</text>
|
||
</view>
|
||
<text class="menu-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<text class="version">版本 1.0.0</text>
|
||
<view class="bottom-safe"></view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref, onMounted } from 'vue'
|
||
import { onShareAppMessage, onShow } from '@dcloudio/uni-app'
|
||
import { useProfileStore } from '@/stores/profile'
|
||
import { useUserStore } from '@/stores/user'
|
||
import { useLogin } from '@/hooks/useLogin'
|
||
import { createShare } from '@/api/smoke'
|
||
import { updateUserProfile, uploadFile } from '@/api/auth'
|
||
import { getLatestNSTIResult } from '@/utils/nsti'
|
||
|
||
const profileStore = useProfileStore()
|
||
const userStore = useUserStore()
|
||
const { waitForLogin } = useLogin()
|
||
|
||
const shareToken = ref('')
|
||
const shareExpireAt = ref('')
|
||
const shareLoading = ref(false)
|
||
const navBarHeight = ref(0)
|
||
const latestNSTIResult = ref(null)
|
||
|
||
const userName = computed(() => userStore.user?.nickname || '戒烟用户')
|
||
const userAvatar = computed(() => userStore.user?.avatar_url || 'https://linghu-wmr.oss-cn-beijing.aliyuncs.com/smt/avatar.png')
|
||
const modeText = computed(() => {
|
||
if (userStore.mode === 'quit') return '戒烟打卡'
|
||
if (userStore.mode === 'record') return '记录抽烟'
|
||
return '未选择'
|
||
})
|
||
const nstiDesc = computed(() => latestNSTIResult.value
|
||
? `你上次测出:${latestNSTIResult.value.name}`
|
||
: '10 道题,测测你是哪一种抽象戒烟人格')
|
||
|
||
const shareDesc = computed(() => {
|
||
if (!shareToken.value) {
|
||
return shareLoading.value ? '正在生成分享信息...' : '先生成分享令牌后即可分享给朋友'
|
||
}
|
||
return `有效期至 ${formatExpire(shareExpireAt.value)},仅查看权限`
|
||
})
|
||
|
||
const sharePath = computed(() => {
|
||
if (!shareToken.value) {
|
||
return 'pages/index/index'
|
||
}
|
||
return `pages/share/index?share_token=${shareToken.value}`
|
||
})
|
||
|
||
function setupNavBar() {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
const statusBarH = systemInfo.statusBarHeight || 0
|
||
try {
|
||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||
navBarHeight.value = menuBtn.bottom + (menuBtn.top - statusBarH)
|
||
} catch (e) {
|
||
navBarHeight.value = statusBarH + 44
|
||
}
|
||
}
|
||
|
||
function formatExpire(value) {
|
||
if (!value) return '--'
|
||
const d = new Date(value)
|
||
if (Number.isNaN(d.getTime())) return value
|
||
const y = d.getFullYear()
|
||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||
const day = String(d.getDate()).padStart(2, '0')
|
||
const hh = String(d.getHours()).padStart(2, '0')
|
||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||
return `${y}-${m}-${day} ${hh}:${mm}`
|
||
}
|
||
|
||
async function prepareShareToken(showToast = false) {
|
||
if (shareLoading.value) return
|
||
shareLoading.value = true
|
||
try {
|
||
const res = await createShare({ days: 7 })
|
||
shareToken.value = res.data?.share_token || ''
|
||
shareExpireAt.value = res.data?.expire_at || ''
|
||
if (showToast) {
|
||
uni.showToast({ title: '分享链接已刷新', icon: 'success' })
|
||
}
|
||
} catch (e) {
|
||
console.error('prepareShareToken error:', e)
|
||
if (showToast) {
|
||
uni.showToast({ title: '生成分享失败', icon: 'none' })
|
||
}
|
||
} finally {
|
||
shareLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleRefreshShare() {
|
||
prepareShareToken(true)
|
||
}
|
||
|
||
function previewSharePage() {
|
||
if (!shareToken.value) {
|
||
uni.showToast({ title: '分享令牌尚未生成', icon: 'none' })
|
||
return
|
||
}
|
||
uni.navigateTo({
|
||
url: `/pages/share/index?share_token=${shareToken.value}`
|
||
})
|
||
}
|
||
|
||
async function onChooseAvatar(e) {
|
||
const avatarUrl = e.detail.avatarUrl
|
||
if (!avatarUrl) return
|
||
try {
|
||
uni.showLoading({ title: '上传头像...' })
|
||
const uploadRes = await uploadFile(avatarUrl)
|
||
await doUpdateProfile({ avatar_url: uploadRes.url })
|
||
} catch (err) {
|
||
console.error('头像上传失败:', err)
|
||
uni.showToast({ title: '头像更新失败', icon: 'none' })
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
|
||
function onChooseAvatarH5() {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sizeType: ['compressed'],
|
||
sourceType: ['album'],
|
||
success: async (res) => {
|
||
const tempPath = res.tempFilePaths[0]
|
||
try {
|
||
uni.showLoading({ title: '上传头像...' })
|
||
const uploadRes = await uploadFile(tempPath)
|
||
await doUpdateProfile({ avatar_url: uploadRes.url })
|
||
} catch (err) {
|
||
console.error('头像上传失败:', err)
|
||
uni.showToast({ title: '头像更新失败', icon: 'none' })
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
async function onNicknameChange(e) {
|
||
const nickname = (e.detail.value || '').trim()
|
||
if (!nickname || nickname === userStore.user?.nickname) return
|
||
try {
|
||
await doUpdateProfile({ nickname })
|
||
} catch (err) {
|
||
uni.showToast({ title: '昵称更新失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function doUpdateProfile(data) {
|
||
try {
|
||
const res = await updateUserProfile(data)
|
||
userStore.updateUser(res.data)
|
||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||
} catch (e) {
|
||
console.error('doUpdateProfile error:', e)
|
||
uni.showToast({ title: '修改失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
function goOnboarding() {
|
||
uni.navigateTo({ url: '/pages/onboarding/index' })
|
||
}
|
||
|
||
function goNSTI() {
|
||
uni.navigateTo({ url: '/pages/nsti/index' })
|
||
}
|
||
|
||
function goSupervisor() {
|
||
uni.navigateTo({ url: '/pages/supervisor/index' })
|
||
}
|
||
|
||
function clearCache() {
|
||
uni.showModal({
|
||
title: '清除缓存',
|
||
content: '将清除本地缓存数据,不会影响云端记录',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
try {
|
||
uni.clearStorageSync()
|
||
uni.showToast({ title: '缓存已清除', icon: 'success' })
|
||
} catch (e) {
|
||
uni.showToast({ title: '清除失败', icon: 'none' })
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function copyInfo() {
|
||
uni.setClipboardData({
|
||
data: '806669289@qq.com',
|
||
success: () => {
|
||
uni.showToast({ title: '反馈邮箱已复制', icon: 'success' })
|
||
}
|
||
})
|
||
}
|
||
|
||
onShareAppMessage(() => {
|
||
return {
|
||
title: `${userName.value}的戒烟记录(仅查看)`,
|
||
path: sharePath.value
|
||
}
|
||
})
|
||
|
||
onMounted(() => {
|
||
setupNavBar()
|
||
})
|
||
|
||
onShow(async () => {
|
||
await waitForLogin()
|
||
await profileStore.fetchProfile()
|
||
await prepareShareToken(false)
|
||
latestNSTIResult.value = getLatestNSTIResult()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
position: relative;
|
||
background-color: #F5F7F6;
|
||
padding: 0 32rpx 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.page-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 500rpx;
|
||
background: linear-gradient(180deg, #DDF3EB 0%, #F5F7F6 100%);
|
||
z-index: 0;
|
||
}
|
||
|
||
.nav-placeholder,
|
||
.section,
|
||
.version {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 36rpx;
|
||
}
|
||
|
||
.section-label {
|
||
display: block;
|
||
margin: 0 0 16rpx 16rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #666666;
|
||
}
|
||
|
||
.card {
|
||
background: #FFFFFF;
|
||
border-radius: 32rpx;
|
||
padding: 32rpx;
|
||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
|
||
}
|
||
|
||
.user-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 32rpx;
|
||
}
|
||
|
||
.avatar-btn {
|
||
padding: 0;
|
||
margin: 0;
|
||
background: transparent;
|
||
border: none;
|
||
line-height: 1;
|
||
width: 128rpx;
|
||
height: 128rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.avatar-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.avatar {
|
||
width: 128rpx;
|
||
height: 128rpx;
|
||
border-radius: 50%;
|
||
background-color: #F0F4F2;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.user-copy {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.nickname-input {
|
||
font-size: 38rpx;
|
||
font-weight: 700;
|
||
color: #1A1A1A;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 0;
|
||
height: 56rpx;
|
||
line-height: 56rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 38rpx;
|
||
font-weight: 700;
|
||
color: #1A1A1A;
|
||
}
|
||
|
||
.user-desc {
|
||
display: block;
|
||
margin-top: 8rpx;
|
||
font-size: 26rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.user-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.user-pill {
|
||
padding: 8rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
background: #E8F5F0;
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
color: #10B981;
|
||
}
|
||
|
||
.user-pill-muted {
|
||
background: #F5F5F5;
|
||
color: #999999;
|
||
}
|
||
|
||
.menu-list {
|
||
padding: 8rpx 32rpx;
|
||
}
|
||
|
||
.menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24rpx;
|
||
padding: 24rpx 0;
|
||
background: transparent;
|
||
}
|
||
|
||
.menu-divider {
|
||
height: 1px;
|
||
background: #F0F0F0;
|
||
margin: 0;
|
||
}
|
||
|
||
.menu-icon {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.menu-icon-accent {
|
||
background: #E8F5F0;
|
||
}
|
||
|
||
.menu-icon-muted {
|
||
background: #F5F5F5;
|
||
}
|
||
|
||
.menu-icon-nsti {
|
||
background: linear-gradient(135deg, #FFE0F1 0%, #E6FFDE 100%);
|
||
}
|
||
|
||
.menu-glyph {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.menu-icon-accent .menu-glyph {
|
||
color: #10B981;
|
||
}
|
||
|
||
.menu-icon-nsti .menu-glyph {
|
||
color: #C2187A;
|
||
}
|
||
|
||
.menu-icon-muted .menu-glyph {
|
||
color: #999999;
|
||
}
|
||
|
||
.menu-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.menu-label {
|
||
font-size: 30rpx;
|
||
color: #1A1A1A;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.menu-desc {
|
||
font-size: 24rpx;
|
||
line-height: 1.4;
|
||
color: #999999;
|
||
}
|
||
|
||
.menu-actions {
|
||
margin-top: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.menu-action {
|
||
color: #10B981;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.menu-action-sep {
|
||
color: #D4D4D4;
|
||
}
|
||
|
||
.share-btn {
|
||
margin: 0;
|
||
padding: 12rpx 32rpx;
|
||
line-height: 1.5;
|
||
font-size: 26rpx;
|
||
border: none;
|
||
border-radius: 999rpx;
|
||
color: #FFFFFF;
|
||
background: #10B981;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.share-btn[disabled] {
|
||
background: #A7F3D0;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.share-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.menu-arrow {
|
||
font-size: 36rpx;
|
||
color: #CCCCCC;
|
||
}
|
||
|
||
.version {
|
||
display: block;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: #B0B0B0;
|
||
margin-top: 40rpx;
|
||
}
|
||
|
||
.bottom-safe {
|
||
height: calc(40rpx + env(safe-area-inset-bottom));
|
||
}
|
||
</style>
|