Files
smt/pages/share/index.vue
T
2026-03-10 23:00:37 +08:00

585 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<view v-if="loading" class="state-wrap">
<text class="state-text">加载分享数据中...</text>
</view>
<view v-else-if="errorText" class="state-wrap">
<text class="state-text">{{ errorText }}</text>
<button class="retry-btn" @tap="reload">重新加载</button>
</view>
<view v-else>
<view class="owner-card">
<image class="avatar" :src="owner.avatar_url || defaultAvatar" mode="aspectFill"></image>
<view class="owner-main">
<text class="owner-name">{{ owner.nickname || '戒烟用户' }}</text>
<text class="owner-desc">分享了自己的戒烟记录只读</text>
</view>
</view>
<view class="overview-grid">
<view class="overview-item">
<text class="overview-label">今日吸烟</text>
<text class="overview-value">{{ overview.today_count || 0 }}</text>
</view>
<view class="overview-item">
<text class="overview-label">今日忍住</text>
<text class="overview-value">{{ overview.resisted_count || 0 }}</text>
</view>
<view class="overview-item">
<text class="overview-label">连续记录</text>
<text class="overview-value">{{ overview.streak_days || 0 }}</text>
</view>
<view class="overview-item">
<text class="overview-label">较昨日变化</text>
<text class="overview-value" :class="overview.exceeded_yesterday ? 'warning' : 'success'">
{{ overview.exceeded_yesterday ? '+' : '-' }}{{ overview.reduced_from_yesterday || 0 }}
</text>
</view>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">统计报表</text>
<view class="range-tabs">
<view
v-for="item in rangeTabs"
:key="item.value"
class="range-item"
:class="{ active: range === item.value }"
@tap="switchRange(item.value)"
>
{{ item.label }}
</view>
</view>
</view>
<view class="stats-row">
<view class="stats-block">
<text class="stats-key">日均支数</text>
<text class="stats-val">{{ stats.daily_average || 0 }}</text>
</view>
<view class="stats-block">
<text class="stats-key">变化幅度</text>
<text class="stats-val">{{ stats.change_percent || 0 }}%</text>
</view>
<view class="stats-block">
<text class="stats-key">范围忍住</text>
<text class="stats-val">{{ stats.resisted_total || 0 }}</text>
</view>
</view>
<view class="trend-wrap">
<view v-for="(item, index) in stats.trend || []" :key="index" class="trend-item">
<text class="trend-label">{{ item.label }}</text>
<view class="trend-bar-bg">
<view class="trend-bar" :style="{ width: trendWidth(item.count) + '%' }"></view>
</view>
<text class="trend-count">{{ item.count }}</text>
</view>
</view>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">记录详情只读</text>
<view class="range-tabs">
<view
v-for="item in logTypeTabs"
:key="item.value"
class="range-item"
:class="{ active: logType === item.value }"
@tap="switchLogType(item.value)"
>
{{ item.label }}
</view>
</view>
</view>
<view v-if="logs.length === 0" class="empty-box">
<text class="empty-text">暂无记录</text>
</view>
<view v-else class="log-list">
<view v-for="item in logs" :key="item.id" class="log-item" @tap="showDetail(item)">
<view class="log-top">
<text class="log-time">{{ displayTime(item) }}</text>
<text class="log-type" :class="resolveType(item) === 'resisted' ? 'resisted' : 'smoke'">
{{ resolveType(item) === 'resisted' ? '已忍住' : '已抽烟' }}
</text>
</view>
<view class="log-meta">
<text>数量{{ item.num ?? 0 }}</text>
<text>强度{{ levelLabel(item.level) }}</text>
</view>
<text class="log-remark">{{ (item.remark && String(item.remark).trim()) || '无备注' }}</text>
</view>
</view>
<button
v-if="logs.length < total"
class="load-more"
:disabled="loadingMore"
@tap="loadMore"
>
{{ loadingMore ? '加载中...' : '加载更多' }}
</button>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad, onShareAppMessage } from '@dcloudio/uni-app'
import { getShareData } from '@/api'
const defaultAvatar = 'https://linghu-wmr.oss-cn-beijing.aliyuncs.com/smt/avatar.png'
const loading = ref(true)
const loadingMore = ref(false)
const errorText = ref('')
const shareToken = ref('')
const range = ref('week')
const logType = ref('all')
const owner = ref({})
const overview = ref({})
const stats = ref({})
const logs = ref([])
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const rangeTabs = [
{ label: '周', value: 'week' },
{ label: '月', value: 'month' },
{ label: '年', value: 'year' }
]
const logTypeTabs = [
{ label: '全部', value: 'all' },
{ label: '已抽烟', value: 'smoke' },
{ label: '已忍住', value: 'resisted' }
]
const maxTrend = computed(() => {
const values = (stats.value?.trend || []).map((item) => Number(item.count) || 0)
const max = Math.max(...values, 0)
return max <= 0 ? 1 : max
})
function trendWidth(count) {
const n = Number(count) || 0
return Math.max(8, Math.round((n / maxTrend.value) * 100))
}
function resolveType(item) {
if ((item?.level || 0) === 0 && (item?.num || 0) === 0) {
return 'resisted'
}
return 'smoke'
}
function levelLabel(level) {
const value = Number(level)
if (Number.isNaN(value)) return '未知'
if (value <= 1) return '轻微'
if (value === 2) return '中等'
if (value === 3) return '明显'
if (value === 4) return '强烈'
return '极强'
}
function displayTime(item) {
if (item?.smoke_at) {
return String(item.smoke_at).replace('T', ' ').slice(0, 19)
}
if (item?.smoke_time) {
return String(item.smoke_time).slice(0, 10)
}
if (item?.createtime) {
const ts = Number(item.createtime)
if (!Number.isNaN(ts) && ts > 0) {
const d = new Date(ts * 1000)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const h = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
const s = String(d.getSeconds()).padStart(2, '0')
return `${y}-${m}-${day} ${h}:${mm}:${s}`
}
}
return '--'
}
async function fetchShare(resetLogs = false) {
if (!shareToken.value) {
errorText.value = '分享参数缺失'
loading.value = false
return
}
if (resetLogs) {
page.value = 1
logs.value = []
}
const params = {
range: range.value,
type: logType.value,
page: page.value,
page_size: pageSize.value
}
try {
const res = await getShareData(shareToken.value, params)
const payload = res.data || {}
owner.value = payload.owner || {}
overview.value = payload.overview || {}
stats.value = payload.stats || {}
const logPayload = payload.logs || {}
const items = logPayload.items || []
if (resetLogs) {
logs.value = items
} else {
logs.value = [...logs.value, ...items]
}
total.value = Number(logPayload.total || 0)
page.value = Number(logPayload.page || page.value)
} catch (e) {
console.error('fetchShare error:', e)
errorText.value = e?.message || '分享已失效或不可访问'
} finally {
loading.value = false
loadingMore.value = false
}
}
async function switchRange(next) {
if (range.value === next) return
range.value = next
loading.value = true
errorText.value = ''
await fetchShare(true)
}
async function switchLogType(next) {
if (logType.value === next) return
logType.value = next
loading.value = true
errorText.value = ''
await fetchShare(true)
}
async function loadMore() {
if (loadingMore.value || logs.value.length >= total.value) return
loadingMore.value = true
page.value += 1
await fetchShare(false)
}
function showDetail(item) {
uni.showModal({
title: '记录详情',
showCancel: false,
content: [
`时间:${displayTime(item)}`,
`类型:${resolveType(item) === 'resisted' ? '已忍住' : '已抽烟'}`,
`数量:${item.num ?? 0}`,
`强度:${levelLabel(item.level)}`,
`备注:${(item.remark && String(item.remark).trim()) || '无备注'}`
].join('\n')
})
}
async function reload() {
loading.value = true
errorText.value = ''
await fetchShare(true)
}
onLoad(async (options) => {
shareToken.value = String(options?.share_token || '').trim()
await fetchShare(true)
})
onShareAppMessage(() => {
return {
title: '戒烟助手 - 查看我的戒烟记录',
path: shareToken.value
? `pages/share/index?share_token=${shareToken.value}`
: 'pages/index/index'
}
})
</script>
<style scoped>
.page {
min-height: 100vh;
padding: 24rpx;
box-sizing: border-box;
background: #f5f7fa;
}
.state-wrap {
padding: 120rpx 40rpx;
text-align: center;
}
.state-text {
font-size: 28rpx;
color: #6b7280;
}
.retry-btn {
margin-top: 24rpx;
font-size: 26rpx;
background: #10b981;
color: #fff;
border: none;
border-radius: 16rpx;
}
.owner-card {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx;
background: #ffffff;
border-radius: 20rpx;
}
.avatar {
width: 90rpx;
height: 90rpx;
border-radius: 50%;
background: #e5e7eb;
}
.owner-main {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.owner-name {
font-size: 32rpx;
font-weight: 700;
color: #111827;
}
.owner-desc {
font-size: 24rpx;
color: #6b7280;
}
.overview-grid {
margin-top: 20rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.overview-item {
padding: 20rpx;
background: #fff;
border-radius: 16rpx;
}
.overview-label {
font-size: 22rpx;
color: #6b7280;
}
.overview-value {
margin-top: 10rpx;
display: block;
font-size: 34rpx;
font-weight: 700;
color: #111827;
}
.overview-value.success {
color: #10b981;
}
.overview-value.warning {
color: #ef4444;
}
.section {
margin-top: 20rpx;
padding: 20rpx;
background: #fff;
border-radius: 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #111827;
}
.range-tabs {
display: flex;
gap: 8rpx;
}
.range-item {
padding: 8rpx 18rpx;
font-size: 22rpx;
color: #6b7280;
border-radius: 999rpx;
background: #f3f4f6;
}
.range-item.active {
background: #10b981;
color: #fff;
}
.stats-row {
margin-top: 18rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
}
.stats-block {
padding: 14rpx;
border-radius: 14rpx;
background: #f8fafc;
}
.stats-key {
font-size: 20rpx;
color: #6b7280;
}
.stats-val {
display: block;
margin-top: 8rpx;
font-size: 30rpx;
font-weight: 700;
color: #111827;
}
.trend-wrap {
margin-top: 18rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.trend-item {
display: grid;
grid-template-columns: 1.5fr 3fr 0.8fr;
align-items: center;
gap: 10rpx;
}
.trend-label,
.trend-count {
font-size: 22rpx;
color: #374151;
}
.trend-bar-bg {
height: 16rpx;
border-radius: 999rpx;
background: #e5e7eb;
overflow: hidden;
}
.trend-bar {
height: 100%;
background: linear-gradient(90deg, #34d399, #10b981);
}
.empty-box {
padding: 40rpx 0;
text-align: center;
}
.empty-text {
font-size: 24rpx;
color: #6b7280;
}
.log-list {
margin-top: 14rpx;
display: flex;
flex-direction: column;
gap: 14rpx;
}
.log-item {
padding: 18rpx;
border-radius: 14rpx;
background: #f8fafc;
}
.log-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.log-time {
font-size: 24rpx;
font-weight: 600;
color: #111827;
}
.log-type {
font-size: 20rpx;
padding: 6rpx 12rpx;
border-radius: 999rpx;
}
.log-type.smoke {
background: #ffedd5;
color: #c2410c;
}
.log-type.resisted {
background: #dcfce7;
color: #047857;
}
.log-meta {
margin-top: 10rpx;
display: flex;
gap: 18rpx;
font-size: 22rpx;
color: #4b5563;
}
.log-remark {
margin-top: 8rpx;
font-size: 22rpx;
color: #6b7280;
}
.load-more {
margin-top: 16rpx;
font-size: 24rpx;
border-radius: 14rpx;
border: none;
background: #ecfdf5;
color: #047857;
}
</style>