feat: refresh UI and add vite ci workflow
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="page-glow page-glow-a"></view>
|
||||
<view class="page-glow page-glow-b"></view>
|
||||
<view class="nav-placeholder" :style="{ height: navBarHeight + 'px' }"></view>
|
||||
|
||||
<view class="page-header">
|
||||
<text class="header-eyebrow">Account</text>
|
||||
<text class="header-title">个人中心</text>
|
||||
<text class="header-subtitle">模式切换、分享与基础设置</text>
|
||||
</view>
|
||||
|
||||
<view class="user-section">
|
||||
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
|
||||
<view class="user-copy">
|
||||
<text class="user-name">{{ userName }}</text>
|
||||
<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 class="section">
|
||||
<view class="mode-card">
|
||||
<view class="mode-card-header">
|
||||
<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>
|
||||
</view>
|
||||
<view class="mode-switch">
|
||||
<view
|
||||
v-for="item in modeOptions"
|
||||
:key="item.value"
|
||||
class="mode-switch-item"
|
||||
:class="{ 'mode-switch-item-active': userStore.mode === item.value }"
|
||||
@tap="changeMode(item.value)"
|
||||
>
|
||||
<text class="mode-switch-title">{{ item.label }}</text>
|
||||
<text class="mode-switch-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="mode-hint">当前:{{ modeText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-list">
|
||||
<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="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">
|
||||
<view class="menu-list">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { onShareAppMessage, onShow } from '@dcloudio/uni-app'
|
||||
import * as api from '@/api'
|
||||
import { useProfileStore } from '@/stores/profile'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useLogin } from '@/hooks/useLogin'
|
||||
|
||||
const profileStore = useProfileStore()
|
||||
const userStore = useUserStore()
|
||||
const { waitForLogin } = useLogin()
|
||||
|
||||
const shareToken = ref('')
|
||||
const shareExpireAt = ref('')
|
||||
const shareLoading = ref(false)
|
||||
const modeSaving = ref(false)
|
||||
const navBarHeight = ref(0)
|
||||
const modeOptions = [
|
||||
{ value: 'quit', label: '戒烟打卡', desc: '按天记录今天没抽' },
|
||||
{ value: 'record', label: '记录抽烟', desc: '按支数记录变化' }
|
||||
]
|
||||
|
||||
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 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 api.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 changeMode(nextMode) {
|
||||
if (!nextMode || nextMode === userStore.mode || modeSaving.value) return
|
||||
modeSaving.value = true
|
||||
try {
|
||||
uni.showLoading({ title: '切换中...' })
|
||||
await profileStore.saveProfile({ mode: nextMode })
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '模式已切换', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}, 250)
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '切换失败', icon: 'none' })
|
||||
} finally {
|
||||
modeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goOnboarding() {
|
||||
uni.navigateTo({ url: '/pages/onboarding/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)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(52, 200, 160, 0.16), transparent 30%),
|
||||
radial-gradient(circle at top right, rgba(255, 255, 255, 0.92), transparent 22%),
|
||||
linear-gradient(180deg, #edf2f8 0%, #f5f7fb 38%, #fbfdff 100%);
|
||||
padding: 0 24rpx 168rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(24rpx);
|
||||
opacity: 0.72;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page-glow-a {
|
||||
top: 100rpx;
|
||||
left: -140rpx;
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
background: rgba(52, 200, 160, 0.15);
|
||||
}
|
||||
|
||||
.page-glow-b {
|
||||
top: 340rpx;
|
||||
right: -120rpx;
|
||||
width: 320rpx;
|
||||
height: 320rpx;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
}
|
||||
|
||||
.nav-placeholder,
|
||||
.page-header,
|
||||
.user-section,
|
||||
.section,
|
||||
.version {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 24rpx 6rpx 18rpx;
|
||||
}
|
||||
|
||||
.header-eyebrow {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 42rpx;
|
||||
line-height: 1.18;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.user-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 12rpx 28rpx 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.66);
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 16rpx 42rpx rgba(15, 23, 42, 0.08);
|
||||
backdrop-filter: blur(24rpx);
|
||||
-webkit-backdrop-filter: blur(24rpx);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.82);
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.user-copy {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.user-desc {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.user-pill {
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(52, 200, 160, 0.12);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.64);
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #17795c;
|
||||
}
|
||||
|
||||
.user-pill-muted {
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.mode-card {
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
border-radius: 32rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.66);
|
||||
box-shadow: 0 16rpx 42rpx rgba(15, 23, 42, 0.08);
|
||||
backdrop-filter: blur(24rpx);
|
||||
-webkit-backdrop-filter: blur(24rpx);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.mode-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.66);
|
||||
box-shadow: 0 16rpx 42rpx rgba(15, 23, 42, 0.08);
|
||||
backdrop-filter: blur(24rpx);
|
||||
-webkit-backdrop-filter: blur(24rpx);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
margin: 0 24rpx;
|
||||
height: 2rpx;
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.menu-icon-accent {
|
||||
background: rgba(52, 200, 160, 0.14);
|
||||
}
|
||||
|
||||
.menu-icon-muted {
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.menu-glyph {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
font-size: 30rpx;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-desc {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.menu-actions {
|
||||
margin-top: 6rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #1aa37a;
|
||||
}
|
||||
|
||||
.menu-action {
|
||||
color: #1aa37a;
|
||||
}
|
||||
|
||||
.menu-action-sep {
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 36rpx;
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
.menu-value {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #1aa37a;
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
padding: 8rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(247, 249, 252, 0.92);
|
||||
border: 2rpx solid rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.mode-switch-item {
|
||||
flex: 1;
|
||||
padding: 22rpx 18rpx;
|
||||
border-radius: 20rpx;
|
||||
background: transparent;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.mode-switch-item-active {
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border-color: rgba(255, 255, 255, 0.76);
|
||||
box-shadow: 0 8rpx 18rpx rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.mode-switch-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.mode-switch-desc {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.mode-hint {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #1aa37a;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
margin: 0;
|
||||
padding: 12rpx 22rpx;
|
||||
line-height: 1.4;
|
||||
font-size: 24rpx;
|
||||
border: none;
|
||||
border-radius: 999rpx;
|
||||
color: #FFFFFF;
|
||||
background: linear-gradient(180deg, #32c59d 0%, #1aa37a 100%);
|
||||
box-shadow: 0 12rpx 28rpx rgba(26, 163, 122, 0.2);
|
||||
}
|
||||
|
||||
.share-btn[disabled] {
|
||||
background: #98a2b3;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.version {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #98a2b3;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user