feat: refresh UI and add vite ci workflow

This commit is contained in:
你çšnepiedg
2026-03-18 19:24:51 +08:00
parent 31e504a997
commit 55f5c216bd
50 changed files with 13304 additions and 437 deletions
+618
View File
@@ -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>