feat: refresh mini program home and stats experience
This commit is contained in:
+8
-4
@@ -1,6 +1,7 @@
|
|||||||
import { request } from './request'
|
import { request } from './request'
|
||||||
import { MINI_PROGRAM_ID } from '@/config'
|
import { MINI_PROGRAM_ID } from '@/config'
|
||||||
import { storage, SESSION_KEY, USER_KEY } from '@/utils/storage'
|
import pinia, { useUserStore } from '@/stores'
|
||||||
|
import { storage, SESSION_KEY, USER_KEY, USER_MODE_KEY } from '@/utils/storage'
|
||||||
|
|
||||||
const H5_DEBUG_SESSION_KEY = 'FxLFPHHBw49loODmRSvqdg=='
|
const H5_DEBUG_SESSION_KEY = 'FxLFPHHBw49loODmRSvqdg=='
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ export function applyH5DebugSession() {
|
|||||||
if (process.env.NODE_ENV === 'development' && storage.get(SESSION_KEY) !== H5_DEBUG_SESSION_KEY) {
|
if (process.env.NODE_ENV === 'development' && storage.get(SESSION_KEY) !== H5_DEBUG_SESSION_KEY) {
|
||||||
storage.set(SESSION_KEY, H5_DEBUG_SESSION_KEY)
|
storage.set(SESSION_KEY, H5_DEBUG_SESSION_KEY)
|
||||||
storage.remove(USER_KEY)
|
storage.remove(USER_KEY)
|
||||||
|
storage.remove(USER_MODE_KEY)
|
||||||
applied = true
|
applied = true
|
||||||
}
|
}
|
||||||
// #endif
|
// #endif
|
||||||
@@ -26,9 +28,11 @@ export async function login() {
|
|||||||
mini_program_id: MINI_PROGRAM_ID,
|
mini_program_id: MINI_PROGRAM_ID,
|
||||||
code: loginRes.code
|
code: loginRes.code
|
||||||
})
|
})
|
||||||
|
const userStore = useUserStore(pinia)
|
||||||
storage.set(SESSION_KEY, res.data.session_key)
|
userStore.setUser(res.data.user, res.data.session_key)
|
||||||
storage.set(USER_KEY, res.data.user)
|
if (res.data.user?.mode) {
|
||||||
|
userStore.setMode(res.data.user.mode)
|
||||||
|
}
|
||||||
|
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -262,10 +262,11 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transform: translateY(100%);
|
transform: translateY(100%);
|
||||||
transition: transform 0.3s ease-out;
|
transition: transform 0.3s ease-out;
|
||||||
padding-bottom: 16rpx;
|
padding-bottom: 0;
|
||||||
border-top: 2rpx solid rgba(255, 255, 255, 0.72);
|
border-top: 2rpx solid rgba(255, 255, 255, 0.72);
|
||||||
backdrop-filter: blur(28rpx);
|
backdrop-filter: blur(28rpx);
|
||||||
-webkit-backdrop-filter: blur(28rpx);
|
-webkit-backdrop-filter: blur(28rpx);
|
||||||
|
box-shadow: 0 20rpx 48rpx rgba(15, 23, 42, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-show {
|
.dialog-show {
|
||||||
@@ -308,7 +309,8 @@ export default {
|
|||||||
|
|
||||||
.dialog-body {
|
.dialog-body {
|
||||||
padding: 16rpx 32rpx 24rpx;
|
padding: 16rpx 32rpx 24rpx;
|
||||||
max-height: 62vh;
|
padding-bottom: 40rpx;
|
||||||
|
max-height: 58vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,6 +540,8 @@ export default {
|
|||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
padding: 16rpx 32rpx 32rpx;
|
padding: 16rpx 32rpx 32rpx;
|
||||||
|
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||||
|
background: linear-gradient(180deg, rgba(248, 250, 252, 0) 0%, rgba(248, 250, 252, 0.96) 28%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-btn-primary {
|
.dialog-btn-primary {
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/stats-calendar/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "日历详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/ai/index",
|
"path": "pages/ai/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
+431
-288
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,21 @@ function setupNavBar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isProfileCompleted(profileData) {
|
||||||
|
const profile = profileData?.profile
|
||||||
|
return profileData?.is_completed ||
|
||||||
|
(profile && profile.onboarding_completed_at) ||
|
||||||
|
(profile && profile.baseline_cigs_per_day > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goHome() {
|
||||||
|
uni.switchTab({ url: '/pages/index/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goOnboarding() {
|
||||||
|
uni.redirectTo({ url: '/pages/onboarding/index' })
|
||||||
|
}
|
||||||
|
|
||||||
async function selectMode(mode) {
|
async function selectMode(mode) {
|
||||||
if (submitting.value) return
|
if (submitting.value) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
@@ -78,20 +93,15 @@ async function selectMode(mode) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const profileData = await profileStore.saveProfile({ mode })
|
const profileData = await profileStore.saveProfile({ mode })
|
||||||
const profile = profileData.profile
|
if (!profileData.exists || !isProfileCompleted(profileData)) {
|
||||||
const isCompleted = profileData.is_completed ||
|
goOnboarding()
|
||||||
(profile && profile.onboarding_completed_at) ||
|
|
||||||
(profile && profile.baseline_cigs_per_day > 0)
|
|
||||||
|
|
||||||
if (!profileData.exists || !isCompleted) {
|
|
||||||
uni.redirectTo({ url: '/pages/onboarding/index' })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.switchTab({ url: '/pages/index/index' })
|
goHome()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('selectMode error:', e)
|
console.error('selectMode error:', e)
|
||||||
uni.switchTab({ url: '/pages/index/index' })
|
goHome()
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
@@ -104,15 +114,17 @@ onMounted(async () => {
|
|||||||
// New users go directly to onboarding (single-page flow)
|
// New users go directly to onboarding (single-page flow)
|
||||||
try {
|
try {
|
||||||
const profileData = await profileStore.fetchProfile()
|
const profileData = await profileStore.fetchProfile()
|
||||||
const profile = profileData?.profile
|
if (!profileData?.exists || !isProfileCompleted(profileData)) {
|
||||||
const isCompleted = profileData?.is_completed ||
|
goOnboarding()
|
||||||
(profile && profile.onboarding_completed_at) ||
|
return
|
||||||
(profile && profile.baseline_cigs_per_day > 0)
|
}
|
||||||
if (!profileData?.exists || !isCompleted) {
|
if (profileData?.profile?.mode || userStore.mode) {
|
||||||
uni.redirectTo({ url: '/pages/onboarding/index' })
|
goHome()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If fetch fails, let user choose mode normally
|
if (userStore.mode) {
|
||||||
|
goHome()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+88
-183
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
|
<view class="page-bg"></view>
|
||||||
<view class="nav-placeholder" :style="{ height: navBarHeight + 'px' }"></view>
|
<view class="nav-placeholder" :style="{ height: navBarHeight + 'px' }"></view>
|
||||||
|
|
||||||
<view class="section">
|
<view class="section">
|
||||||
@@ -18,32 +19,6 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="section">
|
<view class="section">
|
||||||
<text class="section-label">使用模式</text>
|
|
||||||
<view class="mode-card 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>
|
|
||||||
|
|
||||||
<text class="section-label">常用操作</text>
|
<text class="section-label">常用操作</text>
|
||||||
<view class="menu-list card">
|
<view class="menu-list card">
|
||||||
<view class="menu-item">
|
<view class="menu-item">
|
||||||
@@ -56,11 +31,11 @@
|
|||||||
<view class="menu-actions">
|
<view class="menu-actions">
|
||||||
<text class="menu-action" @tap.stop="previewSharePage">预览分享页</text>
|
<text class="menu-action" @tap.stop="previewSharePage">预览分享页</text>
|
||||||
<text class="menu-action-sep">·</text>
|
<text class="menu-action-sep">·</text>
|
||||||
<text class="menu-action" @tap.stop="handleRefreshShare">刷新分享链接</text>
|
<text class="menu-action" @tap.stop="handleRefreshShare">刷新</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<button class="share-btn" open-type="share" :disabled="shareLoading || !shareToken">
|
<button class="share-btn" open-type="share" :disabled="shareLoading || !shareToken">
|
||||||
{{ shareLoading ? '生成中' : '分享' }}
|
{{ shareLoading ? '生成中' : '去分享' }}
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -72,7 +47,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="menu-content">
|
<view class="menu-content">
|
||||||
<text class="menu-label">重新填写问卷</text>
|
<text class="menu-label">重新填写问卷</text>
|
||||||
<text class="menu-desc">修改吸烟基线与个人信息</text>
|
<text class="menu-desc">修改打卡模式、吸烟基线等个人信息</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-arrow">›</text>
|
<text class="menu-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -88,7 +63,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="menu-content">
|
<view class="menu-content">
|
||||||
<text class="menu-label">清除缓存</text>
|
<text class="menu-label">清除缓存</text>
|
||||||
<text class="menu-desc">仅清理本地缓存,不影响云端记录</text>
|
<text class="menu-desc">仅清理本地缓存,不影响云端数据</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-arrow">›</text>
|
<text class="menu-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -128,12 +103,7 @@ const { waitForLogin } = useLogin()
|
|||||||
const shareToken = ref('')
|
const shareToken = ref('')
|
||||||
const shareExpireAt = ref('')
|
const shareExpireAt = ref('')
|
||||||
const shareLoading = ref(false)
|
const shareLoading = ref(false)
|
||||||
const modeSaving = ref(false)
|
|
||||||
const navBarHeight = ref(0)
|
const navBarHeight = ref(0)
|
||||||
const modeOptions = [
|
|
||||||
{ value: 'quit', label: '戒烟打卡', desc: '按天记录今天没抽' },
|
|
||||||
{ value: 'record', label: '记录抽烟', desc: '按支数记录变化' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const userName = computed(() => userStore.user?.nickname || '戒烟用户')
|
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 userAvatar = computed(() => userStore.user?.avatar_url || 'https://linghu-wmr.oss-cn-beijing.aliyuncs.com/smt/avatar.png')
|
||||||
@@ -214,25 +184,6 @@ function previewSharePage() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
function goOnboarding() {
|
||||||
uni.navigateTo({ url: '/pages/onboarding/index' })
|
uni.navigateTo({ url: '/pages/onboarding/index' })
|
||||||
}
|
}
|
||||||
@@ -285,11 +236,21 @@ onShow(async () => {
|
|||||||
.page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: linear-gradient(180deg, #E6F7F2 0%, #F0FBF7 40%, #FAFFFE 100%);
|
background-color: #F5F7F6;
|
||||||
padding: 0 28rpx 0;
|
padding: 0 32rpx 0;
|
||||||
box-sizing: border-box;
|
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,
|
.nav-placeholder,
|
||||||
.section,
|
.section,
|
||||||
.version {
|
.version {
|
||||||
@@ -298,37 +259,36 @@ onShow(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 36rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 0 14rpx 6rpx;
|
margin: 0 0 16rpx 16rpx;
|
||||||
font-size: 26rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1a5c45;
|
color: #666666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: #FFFFFF;
|
||||||
border-radius: 24rpx;
|
border-radius: 32rpx;
|
||||||
padding: 24rpx;
|
padding: 32rpx;
|
||||||
border: 1.5rpx solid rgba(52, 200, 160, 0.14);
|
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
|
||||||
box-shadow: 0 4rpx 18rpx rgba(52, 200, 160, 0.07);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-section {
|
.user-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24rpx;
|
gap: 32rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 120rpx;
|
width: 128rpx;
|
||||||
height: 120rpx;
|
height: 128rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 4rpx solid rgba(52, 200, 160, 0.16);
|
background-color: #F0F4F2;
|
||||||
background-color: rgba(52, 200, 160, 0.06);
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-copy {
|
.user-copy {
|
||||||
@@ -341,197 +301,137 @@ onShow(async () => {
|
|||||||
.user-name {
|
.user-name {
|
||||||
font-size: 38rpx;
|
font-size: 38rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #0D3D2E;
|
color: #1A1A1A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-desc {
|
.user-desc {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 8rpx;
|
margin-top: 8rpx;
|
||||||
font-size: 25rpx;
|
font-size: 26rpx;
|
||||||
line-height: 1.5;
|
color: #999999;
|
||||||
color: #52806E;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-meta {
|
.user-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 14rpx;
|
gap: 16rpx;
|
||||||
margin-top: 16rpx;
|
margin-top: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-pill {
|
.user-pill {
|
||||||
padding: 14rpx 26rpx;
|
padding: 8rpx 20rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
background: rgba(52, 200, 160, 0.12);
|
background: #E8F5F0;
|
||||||
border: 1.5rpx solid rgba(52, 200, 160, 0.18);
|
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1a8c62;
|
color: #10B981;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-pill-muted {
|
.user-pill-muted {
|
||||||
background: rgba(52, 200, 160, 0.06);
|
background: #F5F5F5;
|
||||||
color: #7aA898;
|
color: #999999;
|
||||||
}
|
|
||||||
|
|
||||||
.mode-card {
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-list {
|
.menu-list {
|
||||||
display: flex;
|
padding: 8rpx 32rpx;
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20rpx;
|
gap: 24rpx;
|
||||||
padding: 6rpx 0;
|
padding: 24rpx 0;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-divider {
|
.menu-divider {
|
||||||
margin: 16rpx 0;
|
height: 1px;
|
||||||
height: 2rpx;
|
background: #F0F0F0;
|
||||||
background: rgba(52, 200, 160, 0.12);
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon {
|
.menu-icon {
|
||||||
width: 64rpx;
|
width: 72rpx;
|
||||||
height: 64rpx;
|
height: 72rpx;
|
||||||
border-radius: 18rpx;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 1.5rpx solid rgba(52, 200, 160, 0.18);
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon-accent {
|
.menu-icon-accent {
|
||||||
background: rgba(52, 200, 160, 0.12);
|
background: #E8F5F0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon-muted {
|
.menu-icon-muted {
|
||||||
background: rgba(52, 200, 160, 0.06);
|
background: #F5F5F5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-glyph {
|
.menu-glyph {
|
||||||
font-size: 24rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1a8c62;
|
}
|
||||||
|
|
||||||
|
.menu-icon-accent .menu-glyph {
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon-muted .menu-glyph {
|
||||||
|
color: #999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-content {
|
.menu-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4rpx;
|
gap: 6rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
color: #0D3D2E;
|
color: #1A1A1A;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-desc {
|
.menu-desc {
|
||||||
font-size: 25rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.4;
|
||||||
color: #52806E;
|
color: #999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-actions {
|
.menu-actions {
|
||||||
margin-top: 6rpx;
|
margin-top: 8rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8rpx;
|
gap: 12rpx;
|
||||||
font-size: 22rpx;
|
font-size: 24rpx;
|
||||||
color: #1a8c62;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-action {
|
.menu-action {
|
||||||
color: #1a8c62;
|
color: #10B981;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-action-sep {
|
.menu-action-sep {
|
||||||
color: #7aA898;
|
color: #D4D4D4;
|
||||||
}
|
|
||||||
|
|
||||||
.menu-arrow {
|
|
||||||
font-size: 36rpx;
|
|
||||||
color: #7aA898;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-value {
|
|
||||||
font-size: 24rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a8c62;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-switch {
|
|
||||||
display: flex;
|
|
||||||
gap: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-switch-item {
|
|
||||||
flex: 1;
|
|
||||||
padding: 22rpx 20rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.72);
|
|
||||||
border: 2rpx solid rgba(52, 200, 160, 0.1);
|
|
||||||
box-shadow: 0 2rpx 10rpx rgba(52, 200, 160, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-switch-item-active {
|
|
||||||
background: rgba(52, 200, 160, 0.09);
|
|
||||||
border-color: rgba(52, 200, 160, 0.45);
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(52, 200, 160, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-switch-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0D3D2E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-switch-desc {
|
|
||||||
display: block;
|
|
||||||
margin-top: 8rpx;
|
|
||||||
font-size: 22rpx;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #7aA898;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-hint {
|
|
||||||
display: block;
|
|
||||||
margin-top: 16rpx;
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #1a8c62;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-btn {
|
.share-btn {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 16rpx 28rpx;
|
padding: 12rpx 32rpx;
|
||||||
line-height: 1.4;
|
line-height: 1.5;
|
||||||
font-size: 24rpx;
|
font-size: 26rpx;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
background: linear-gradient(180deg, #3DD9AE 0%, #34C8A0 100%);
|
background: #10B981;
|
||||||
box-shadow: 0 12rpx 28rpx rgba(52, 200, 160, 0.28);
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-btn[disabled] {
|
.share-btn[disabled] {
|
||||||
background: #9CC5B5;
|
background: #A7F3D0;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,15 +439,20 @@ onShow(async () => {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-arrow {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #CCCCCC;
|
||||||
|
}
|
||||||
|
|
||||||
.version {
|
.version {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 22rpx;
|
font-size: 24rpx;
|
||||||
color: #7aA898;
|
color: #B0B0B0;
|
||||||
margin-top: 28rpx;
|
margin-top: 40rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-safe {
|
.bottom-safe {
|
||||||
height: calc(32rpx + env(safe-area-inset-bottom));
|
height: calc(40rpx + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,593 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view class="month-bar">
|
||||||
|
<view class="month-arrow" @tap="changeMonth(-1)">‹</view>
|
||||||
|
<view class="month-copy">
|
||||||
|
<text class="month-title">{{ monthTitle }}</text>
|
||||||
|
<text class="month-subtitle">{{ monthSubtitle }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="month-arrow" :class="{ 'month-arrow-disabled': !canGoNextMonth }" @tap="changeMonth(1)">›</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="summary-row">
|
||||||
|
<view class="summary-chip">
|
||||||
|
<text class="summary-chip-label">本月已抽</text>
|
||||||
|
<view class="summary-chip-value-row">
|
||||||
|
<text class="summary-chip-value">{{ monthSmokeTotal }}</text>
|
||||||
|
<text class="summary-chip-unit">支</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="summary-chip summary-chip-soft">
|
||||||
|
<text class="summary-chip-label">本月忍住</text>
|
||||||
|
<view class="summary-chip-value-row">
|
||||||
|
<text class="summary-chip-value">{{ monthResistedTotal }}</text>
|
||||||
|
<text class="summary-chip-unit">次</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="summary-chip summary-chip-soft">
|
||||||
|
<text class="summary-chip-label">记录天数</text>
|
||||||
|
<view class="summary-chip-value-row">
|
||||||
|
<text class="summary-chip-value">{{ activeDayCount }}</text>
|
||||||
|
<text class="summary-chip-unit">天</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="calendar-legend">
|
||||||
|
<view class="legend-item">
|
||||||
|
<view class="legend-dot legend-dot-smoke"></view>
|
||||||
|
<text class="legend-text">已抽</text>
|
||||||
|
</view>
|
||||||
|
<view class="legend-item">
|
||||||
|
<view class="legend-dot legend-dot-resisted"></view>
|
||||||
|
<text class="legend-text">忍住</text>
|
||||||
|
</view>
|
||||||
|
<text class="legend-tip">点击灰色日期可切换到对应月份</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="weekday-row">
|
||||||
|
<text v-for="item in weekdayNames" :key="item" class="weekday-item">{{ item }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="calendar-grid">
|
||||||
|
<view
|
||||||
|
v-for="item in calendarDays"
|
||||||
|
:key="item.key"
|
||||||
|
class="calendar-cell"
|
||||||
|
:class="{
|
||||||
|
'calendar-cell-muted': !item.isCurrentMonth,
|
||||||
|
'calendar-cell-selected': item.date === selectedDate,
|
||||||
|
'calendar-cell-today': item.isToday,
|
||||||
|
'calendar-cell-disabled': item.isFuture
|
||||||
|
}"
|
||||||
|
@tap="selectDay(item)"
|
||||||
|
>
|
||||||
|
<view class="calendar-cell-top">
|
||||||
|
<text class="calendar-day">{{ item.day }}</text>
|
||||||
|
<view v-if="item.isToday" class="calendar-today-dot"></view>
|
||||||
|
</view>
|
||||||
|
<view class="calendar-count-wrap">
|
||||||
|
<text class="calendar-count-value">{{ item.smokeCount }}</text>
|
||||||
|
<text class="calendar-count-unit">支</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-safe"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { useLogin } from '@/hooks/useLogin'
|
||||||
|
import * as api from '@/api'
|
||||||
|
|
||||||
|
const { waitForLogin } = useLogin()
|
||||||
|
|
||||||
|
const weekdayNames = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
|
const currentMonth = ref(startOfMonth(new Date()))
|
||||||
|
const selectedDate = ref(formatDate(new Date()))
|
||||||
|
const monthLogs = ref([])
|
||||||
|
const todayText = formatDate(new Date())
|
||||||
|
|
||||||
|
const monthTitle = computed(() => {
|
||||||
|
const date = currentMonth.value
|
||||||
|
return `${date.getFullYear()} 年 ${date.getMonth() + 1} 月`
|
||||||
|
})
|
||||||
|
|
||||||
|
const monthSubtitle = computed(() => {
|
||||||
|
return `共 ${monthLogs.value.length} 条记录`
|
||||||
|
})
|
||||||
|
|
||||||
|
const monthSummaryMap = computed(() => buildDailySummaryMap(monthLogs.value))
|
||||||
|
const canGoNextMonth = computed(() => {
|
||||||
|
return formatDate(startOfMonth(currentMonth.value)) < formatDate(startOfMonth(new Date()))
|
||||||
|
})
|
||||||
|
|
||||||
|
const calendarDays = computed(() => {
|
||||||
|
const monthStart = startOfMonth(currentMonth.value)
|
||||||
|
const monthEnd = endOfMonth(currentMonth.value)
|
||||||
|
const gridStart = startOfWeek(monthStart)
|
||||||
|
const gridEnd = endOfWeek(monthEnd)
|
||||||
|
const result = []
|
||||||
|
const summaryMap = monthSummaryMap.value
|
||||||
|
|
||||||
|
for (let date = new Date(gridStart); date <= gridEnd; date = addDays(date, 1)) {
|
||||||
|
const dateKey = formatDate(date)
|
||||||
|
const summary = summaryMap.get(dateKey) || { smokeCount: 0, resistedCount: 0 }
|
||||||
|
result.push({
|
||||||
|
key: dateKey,
|
||||||
|
date: dateKey,
|
||||||
|
day: date.getDate(),
|
||||||
|
isCurrentMonth: date.getMonth() === monthStart.getMonth(),
|
||||||
|
isToday: dateKey === todayText,
|
||||||
|
isFuture: dateKey > todayText,
|
||||||
|
smokeCount: summary.smokeCount,
|
||||||
|
resistedCount: summary.resistedCount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const monthSmokeTotal = computed(() => {
|
||||||
|
return monthLogs.value.reduce((total, item) => {
|
||||||
|
return normalizeLogType(item) === 'resisted' ? total : total + (Number(item.num) || 0)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const monthResistedTotal = computed(() => {
|
||||||
|
return monthLogs.value.reduce((total, item) => {
|
||||||
|
return normalizeLogType(item) === 'resisted' ? total + 1 : total
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeDayCount = computed(() => monthSummaryMap.value.size)
|
||||||
|
|
||||||
|
async function fetchMonthLogs() {
|
||||||
|
const start = formatDate(startOfMonth(currentMonth.value))
|
||||||
|
const end = formatDate(endOfMonth(currentMonth.value))
|
||||||
|
try {
|
||||||
|
await waitForLogin()
|
||||||
|
const res = await api.getLogs({
|
||||||
|
page: 1,
|
||||||
|
page_size: 200,
|
||||||
|
type: 'all',
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
})
|
||||||
|
monthLogs.value = res.data?.items || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error('fetchMonthLogs error:', e)
|
||||||
|
monthLogs.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initPage(dateText) {
|
||||||
|
const baseDate = parseDate(dateText) || new Date()
|
||||||
|
currentMonth.value = startOfMonth(baseDate)
|
||||||
|
selectedDate.value = formatDate(baseDate)
|
||||||
|
await fetchMonthLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeMonth(offset) {
|
||||||
|
const next = new Date(currentMonth.value)
|
||||||
|
next.setMonth(next.getMonth() + offset)
|
||||||
|
if (offset > 0 && formatDate(startOfMonth(next)) > formatDate(startOfMonth(new Date()))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentMonth.value = startOfMonth(next)
|
||||||
|
selectedDate.value = formatDate(startOfMonth(next))
|
||||||
|
await fetchMonthLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectDay(item) {
|
||||||
|
if (item.isFuture) return
|
||||||
|
if (!item.isCurrentMonth) {
|
||||||
|
currentMonth.value = startOfMonth(parseDate(item.date))
|
||||||
|
selectedDate.value = item.date
|
||||||
|
await fetchMonthLogs()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedDate.value = item.date
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(async (options) => {
|
||||||
|
await initPage(options?.date)
|
||||||
|
})
|
||||||
|
|
||||||
|
function buildDailySummaryMap(logs) {
|
||||||
|
const map = new Map()
|
||||||
|
logs.forEach(log => {
|
||||||
|
const dateKey = resolveLogDate(log)
|
||||||
|
if (!dateKey) return
|
||||||
|
const current = map.get(dateKey) || { smokeCount: 0, resistedCount: 0 }
|
||||||
|
if (normalizeLogType(log) === 'resisted') {
|
||||||
|
current.resistedCount += 1
|
||||||
|
} else {
|
||||||
|
current.smokeCount += Number(log.num) || 0
|
||||||
|
}
|
||||||
|
map.set(dateKey, current)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLogDate(log) {
|
||||||
|
if (typeof log?.smoke_time === 'string' && log.smoke_time) {
|
||||||
|
return log.smoke_time.split('T')[0]
|
||||||
|
}
|
||||||
|
if (log?.smoke_at) {
|
||||||
|
return formatDate(new Date(log.smoke_at))
|
||||||
|
}
|
||||||
|
if (log?.createtime) {
|
||||||
|
return formatDate(typeof log.createtime === 'number' ? new Date(log.createtime * 1000) : new Date(log.createtime))
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLogType(log) {
|
||||||
|
const rawType = log?.type
|
||||||
|
if (typeof rawType === 'string') {
|
||||||
|
const value = rawType.toLowerCase()
|
||||||
|
if (value === 'resisted' || value === 'resist') return 'resisted'
|
||||||
|
if (value === 'smoke' || value === 'log_smoke') return 'smoke'
|
||||||
|
}
|
||||||
|
if (typeof rawType === 'number') {
|
||||||
|
if (rawType === 0) return 'resisted'
|
||||||
|
if (rawType === 1) return 'smoke'
|
||||||
|
}
|
||||||
|
if (log?.num === 0) return 'resisted'
|
||||||
|
return 'smoke'
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDate(value) {
|
||||||
|
if (!value) return null
|
||||||
|
const date = new Date(`${value}T00:00:00`)
|
||||||
|
if (Number.isNaN(date.getTime())) return null
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date) {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${d}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfMonth(date) {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function endOfMonth(date) {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth() + 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfWeek(date) {
|
||||||
|
const result = new Date(date)
|
||||||
|
result.setDate(result.getDate() - result.getDay())
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function endOfWeek(date) {
|
||||||
|
const result = new Date(date)
|
||||||
|
result.setDate(result.getDate() + (6 - result.getDay()))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(date, offset) {
|
||||||
|
const result = new Date(date)
|
||||||
|
result.setDate(result.getDate() + offset)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #F6F8F6 0%, #EFF4F1 52%, #E9F0EC 100%);
|
||||||
|
padding: 20rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.68);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #52806E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(251, 253, 252, 0.94) 100%);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 32rpx;
|
||||||
|
padding: 22rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
border: 1rpx solid rgba(15, 23, 42, 0.06);
|
||||||
|
box-shadow:
|
||||||
|
0 20rpx 44rpx rgba(15, 23, 42, 0.06),
|
||||||
|
0 6rpx 14rpx rgba(15, 23, 42, 0.03),
|
||||||
|
inset 0 1rpx 0 rgba(255, 255, 255, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 240rpx;
|
||||||
|
height: 240rpx;
|
||||||
|
left: -72rpx;
|
||||||
|
top: -72rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(52, 200, 160, 0.14) 0%, rgba(52, 200, 160, 0) 72%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-bar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-arrow {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f7faf8 100%);
|
||||||
|
border: 2rpx solid rgba(15, 23, 42, 0.06);
|
||||||
|
box-shadow: 0 14rpx 28rpx rgba(15, 23, 42, 0.07);
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #1a8c62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-arrow-disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-copy {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-chip {
|
||||||
|
padding: 16rpx 14rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: linear-gradient(180deg, rgba(240, 252, 248, 0.96) 0%, rgba(247, 251, 249, 0.96) 100%);
|
||||||
|
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||||
|
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-chip-soft {
|
||||||
|
background: linear-gradient(180deg, #fbfcfc 0%, #f7faf8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-chip-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #7aA898;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-chip-value-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-chip-value {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-chip-unit {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-legend {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
padding: 12rpx 14rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: linear-gradient(180deg, #fafcfb 0%, #f5f8f6 100%);
|
||||||
|
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot-smoke {
|
||||||
|
background: #1a8c62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot-resisted {
|
||||||
|
background: #a7d9c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
font-size: 21rpx;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-tip {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-row {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
padding: 0 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-item {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #7aA898;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell {
|
||||||
|
min-height: 138rpx;
|
||||||
|
padding: 14rpx 10rpx 12rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f7faf8 100%);
|
||||||
|
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1rpx 0 rgba(255, 255, 255, 0.92),
|
||||||
|
0 8rpx 18rpx rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-selected {
|
||||||
|
background: linear-gradient(180deg, rgba(240, 252, 248, 0.98) 0%, rgba(247, 252, 249, 0.96) 100%);
|
||||||
|
border-color: rgba(52, 200, 160, 0.24);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1rpx 0 rgba(255, 255, 255, 0.96),
|
||||||
|
0 12rpx 22rpx rgba(29, 163, 111, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-today {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 2rpx rgba(26, 140, 98, 0.16),
|
||||||
|
0 10rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-muted {
|
||||||
|
opacity: 0.42;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #123329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-today-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #1a8c62;
|
||||||
|
background: #1a8c62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-count-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-count-value {
|
||||||
|
font-size: 34rpx;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #14936d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-count-unit {
|
||||||
|
font-size: 18rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-safe {
|
||||||
|
height: calc(28rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+53
-12
@@ -23,7 +23,7 @@
|
|||||||
<text class="insight-glyph">{{ insightEmoji }}</text>
|
<text class="insight-glyph">{{ insightEmoji }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="insight-content">
|
<view class="insight-content">
|
||||||
<text class="insight-title">每周洞察</text>
|
<text class="insight-title">阶段洞察</text>
|
||||||
<text class="insight-desc">{{ insightText }}</text>
|
<text class="insight-desc">{{ insightText }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -33,23 +33,20 @@
|
|||||||
<view class="card-header">
|
<view class="card-header">
|
||||||
<view>
|
<view>
|
||||||
<text class="card-title">吸烟趋势</text>
|
<text class="card-title">吸烟趋势</text>
|
||||||
<text class="card-sub">{{ trendRangeText }}</text>
|
<text class="card-sub">{{ weeklyTrendRangeText }}</text>
|
||||||
</view>
|
|
||||||
<view class="status-chip" :class="statusChipClass">
|
|
||||||
<text class="status-arrow" :class="statusIconClass">{{ statusArrow }}</text>
|
|
||||||
<text class="status-text">{{ statusText }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<view class="card-link" @tap="goCalendarDetail">日历详情</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 日均数据 -->
|
<!-- 日均数据 -->
|
||||||
<view class="avg-row">
|
<view class="avg-row">
|
||||||
<text class="avg-value">{{ averageCount }}</text>
|
<text class="avg-value">{{ weeklyAverageCount }}</text>
|
||||||
<text class="avg-unit">支/天</text>
|
<text class="avg-unit">支/天</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 日历格式趋势 -->
|
<!-- 日历格式趋势 -->
|
||||||
<view v-if="trendItems.length > 0" class="cal-grid">
|
<view v-if="weeklyTrendItems.length > 0" class="cal-grid">
|
||||||
<view v-for="(item, index) in trendItems" :key="index" class="cal-cell" :class="{ 'cal-cell-today': item.isHighlight }">
|
<view v-for="(item, index) in weeklyTrendItems" :key="index" class="cal-cell" :class="{ 'cal-cell-today': item.isHighlight }">
|
||||||
<text class="cal-weekday">{{ item.weekday }}</text>
|
<text class="cal-weekday">{{ item.weekday }}</text>
|
||||||
<text class="cal-date">{{ item.date }}</text>
|
<text class="cal-date">{{ item.date }}</text>
|
||||||
<view class="cal-dot" :class="calDotClass(item.count)">
|
<view class="cal-dot" :class="calDotClass(item.count)">
|
||||||
@@ -164,7 +161,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { onShareAppMessage } from '@dcloudio/uni-app'
|
import { onShareAppMessage, onShow } from '@dcloudio/uni-app'
|
||||||
import { useLogin } from '@/hooks/useLogin'
|
import { useLogin } from '@/hooks/useLogin'
|
||||||
import * as api from '@/api'
|
import * as api from '@/api'
|
||||||
|
|
||||||
@@ -179,6 +176,7 @@ const tabs = [
|
|||||||
|
|
||||||
const currentTab = ref('week')
|
const currentTab = ref('week')
|
||||||
const statsData = ref(null)
|
const statsData = ref(null)
|
||||||
|
const weeklyStatsData = ref(null)
|
||||||
|
|
||||||
const WEEKDAY_NAMES = ['日', '一', '二', '三', '四', '五', '六']
|
const WEEKDAY_NAMES = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
|
|
||||||
@@ -217,6 +215,13 @@ const trendRangeText = computed(() => {
|
|||||||
return formatRangeText(start, end)
|
return formatRangeText(start, end)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const weeklyTrendRangeText = computed(() => {
|
||||||
|
const start = weeklyStatsData.value?.start
|
||||||
|
const end = weeklyStatsData.value?.end
|
||||||
|
if (!start || !end) return '固定展示最近 7 天'
|
||||||
|
return `${formatRangeText(start, end)} · 固定展示最近 7 天`
|
||||||
|
})
|
||||||
|
|
||||||
const statusText = computed(() => {
|
const statusText = computed(() => {
|
||||||
if (changePercent.value === null) return '暂无对比'
|
if (changePercent.value === null) return '暂无对比'
|
||||||
const sign = changePercent.value > 0 ? '+' : ''
|
const sign = changePercent.value > 0 ? '+' : ''
|
||||||
@@ -244,8 +249,14 @@ const averageCount = computed(() => {
|
|||||||
return Number(avg) || 0
|
return Number(avg) || 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const trendItems = computed(() => {
|
const weeklyAverageCount = computed(() => {
|
||||||
const trend = statsData.value?.trend
|
const avg = weeklyStatsData.value?.daily_average
|
||||||
|
if (avg === undefined || avg === null) return 0
|
||||||
|
return Number(avg) || 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const weeklyTrendItems = computed(() => {
|
||||||
|
const trend = weeklyStatsData.value?.trend
|
||||||
if (!trend || !Array.isArray(trend) || trend.length === 0) return []
|
if (!trend || !Array.isArray(trend) || trend.length === 0) return []
|
||||||
return trend.map((item, index) => {
|
return trend.map((item, index) => {
|
||||||
const count = Number(item.count) || 0
|
const count = Number(item.count) || 0
|
||||||
@@ -425,11 +436,31 @@ async function fetchStats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchWeeklyStats() {
|
||||||
|
try {
|
||||||
|
await waitForLogin()
|
||||||
|
const res = await api.getStats({ range: 'week' })
|
||||||
|
weeklyStatsData.value = res.data
|
||||||
|
} catch (e) {
|
||||||
|
console.error('fetchWeeklyStats error:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCalendarDetail() {
|
||||||
|
uni.navigateTo({ url: '/pages/stats-calendar/index' })
|
||||||
|
}
|
||||||
|
|
||||||
watch(currentTab, () => { fetchStats() })
|
watch(currentTab, () => { fetchStats() })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setupNavBar()
|
setupNavBar()
|
||||||
fetchStats()
|
fetchStats()
|
||||||
|
fetchWeeklyStats()
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
fetchStats()
|
||||||
|
fetchWeeklyStats()
|
||||||
})
|
})
|
||||||
|
|
||||||
onShareAppMessage(() => {
|
onShareAppMessage(() => {
|
||||||
@@ -558,6 +589,16 @@ onShareAppMessage(() => {
|
|||||||
margin-bottom: 16rpx;
|
margin-bottom: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-link {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(52, 200, 160, 0.08);
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a8c62;
|
||||||
|
}
|
||||||
|
|
||||||
.card-title-row {
|
.card-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user