Files
smt/pages/logs/index.vue
T
nepiedg 031eef9643 aaa
2026-02-23 22:24:29 +08:00

705 lines
14 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 class="filters">
<view class="tabs">
<view
v-for="tab in tabs"
:key="tab.value"
class="tab"
:class="{ 'tab-active': currentTab === tab.value }"
@tap="currentTab = tab.value"
>
{{ tab.label }}
</view>
</view>
</view>
<!-- 记录列表 -->
<scroll-view
class="scroll-container"
scroll-y
:refresher-enabled="true"
:refresher-triggered="logsStore.refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
>
<!-- 骨架屏 -->
<view v-if="logsStore.loading && logsStore.logs.length === 0" class="skeleton">
<view v-for="i in 3" :key="i" class="skeleton-item">
<view class="skeleton-dot"></view>
<view class="skeleton-card">
<view class="skeleton-line skeleton-line-title"></view>
<view class="skeleton-line skeleton-line-text"></view>
<view class="skeleton-line skeleton-line-text short"></view>
</view>
</view>
</view>
<!-- 时间轴 -->
<view v-else-if="filteredLogs.length > 0" class="log-list">
<view v-for="(group, date) in groupedLogs" :key="date" class="log-group">
<view class="group-header">
<text class="group-title">{{ formatGroupTitle(date) }}</text>
<text class="group-count">{{ group.length }}条记录</text>
</view>
<view class="group-items">
<view v-for="log in group" :key="log.id" class="log-card" :class="log.type === 'resisted' ? 'log-card-resisted' : 'log-card-smoke'">
<view class="log-bar"></view>
<view class="log-icon" :class="log.type === 'resisted' ? 'icon-resisted' : 'icon-smoke'">
<text v-if="log.type === 'resisted'">🌿</text>
<text v-else>🚬</text>
</view>
<view class="log-main">
<view class="log-top">
<view class="log-time-tag">
<text class="log-time">{{ log.displayTime || '--:--' }}</text>
<text class="log-tag" :class="log.type === 'resisted' ? 'tag-resisted' : 'tag-smoke'">
{{ log.type === 'resisted' ? '已忍住' : '已抽烟' }}
</text>
</view>
<view class="log-right">
<text v-if="log.type === 'smoke'" class="count-pill">{{ log.num !== undefined && log.num !== null ? log.num : 0 }}</text>
<text v-else class="thumb-pill">👍</text>
</view>
</view>
<text
v-if="log.remark && typeof log.remark === 'string' && log.remark.trim() && log.remark.trim().length > 0"
class="log-desc"
>{{ log.remark.trim() }}</text>
<view class="log-meta-row">
<text
v-if="log.level !== undefined && log.level !== null"
class="level-text"
:class="levelClass(log.level)"
>烟瘾程度{{ levelLabel(log.level) }}</text>
<text v-if="log.interval" class="log-interval">距上次 {{ log.interval }}</text>
</view>
<view class="log-actions">
<text class="action-btn edit-btn" @tap.stop="handleEdit(log)">编辑</text>
<text class="action-btn delete-btn" @tap.stop="handleDelete(log)">删除</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="empty-icon">📝</text>
<text class="empty-text">暂无记录</text>
<text class="empty-hint">点击右下角按钮开始记录</text>
</view>
<!-- 加载更多 -->
<view v-if="logsStore.loading && logsStore.logs.length > 0" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="!logsStore.hasMore && logsStore.logs.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</scroll-view>
<!-- 浮动按钮 -->
<view class="fab" @tap="addLog">
<text class="fab-icon">+</text>
</view>
<!-- 编辑弹框 -->
<smoke-record-dialog
v-model:show="showEditDialog"
:type="editType"
:initial-data="editData"
@submit="handleUpdate"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useLogsStore } from '@/stores/logs'
import { useLogin } from '@/hooks/useLogin'
const { waitForLogin } = useLogin()
const logsStore = useLogsStore()
const tabs = [
{ label: '全部', value: 'all' },
{ label: '已抽烟', value: 'smoke' },
{ label: '已忍住', value: 'resisted' }
]
const currentTab = ref('all')
const showEditDialog = ref(false)
const editType = ref('smoke')
const editData = ref(null)
const editingLogId = ref(null)
// 筛选后的记录
const filteredLogs = computed(() => {
const logs = logsStore.formattedLogs
if (currentTab.value === 'all') {
return logs
}
return logs.filter(log => log.type === currentTab.value)
})
// 按日期分组
const groupedLogs = computed(() => {
return filteredLogs.value.reduce((groups, log) => {
const date = log.displayDate
if (!groups[date]) {
groups[date] = []
}
groups[date].push(log)
return groups
}, {})
})
// 本地日期 YYYY-MM-DD(避免 toISOString 用 UTC 导致日期差一天)
function localDateStr(d) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
// 格式化分组标题
function formatGroupTitle(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const today = new Date()
const yesterday = new Date(today)
yesterday.setDate(yesterday.getDate() - 1)
const todayStr = localDateStr(today)
const yesterdayStr = localDateStr(yesterday)
if (dateStr === todayStr) {
return '今天'
}
if (dateStr === yesterdayStr) {
return '昨天'
}
return `${date.getMonth() + 1}${date.getDate()}`
}
// 下拉刷新
async function onRefresh() {
await logsStore.fetchLogs(true, currentTab.value)
}
// 上拉加载
async function onLoadMore() {
await logsStore.loadMore()
}
// 新增记录
function addLog() {
uni.switchTab({ url: '/pages/index/index' })
}
// 编辑记录
function handleEdit(log) {
editingLogId.value = log.id
editType.value = log.type
editData.value = {
smoke_time: log.smoke_time?.split('T')[0] || '',
smoke_time_only: log.displayTime,
smoke_at: log.smoke_at,
remark: log.remark || '',
level: log.level ?? 2,
num: log.num ?? 1
}
showEditDialog.value = true
}
// 更新记录
async function handleUpdate(data) {
if (!editingLogId.value) return
const success = await logsStore.updateLog(editingLogId.value, data)
if (success) {
showEditDialog.value = false
editingLogId.value = null
editData.value = null
}
}
// 删除记录
function handleDelete(log) {
uni.showModal({
title: '确认删除',
content: `确定要删除这条${log.type === 'resisted' ? '忍住' : '抽烟'}记录吗?`,
confirmColor: '#EF4444',
success: async (res) => {
if (res.confirm) {
await logsStore.deleteLog(log.id)
}
}
})
}
// 初始化页面
async function initPage() {
try {
await waitForLogin()
await logsStore.fetchLogs(true, currentTab.value)
} catch (e) {
console.error('initPage error:', e)
}
}
onMounted(() => {
initPage()
})
function levelClass(level) {
const value = Number(level)
if (Number.isNaN(value)) return 'level-unknown'
if (value <= 1) return 'level-1'
if (value === 2) return 'level-2'
if (value === 3) return 'level-3'
if (value === 4) return 'level-4'
return 'level-5'
}
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 '极强'
}
watch(currentTab, async (value) => {
await logsStore.fetchLogs(true, value)
})
</script>
<style scoped>
.page {
min-height: 100vh;
background: linear-gradient(to bottom, #D1FAE5 0%, #F3FFF8 45%, #FFFFFF 100%);
box-sizing: border-box;
}
.filters {
display: flex;
align-items: center;
gap: 16rpx;
padding: 24rpx 32rpx 8rpx;
}
.tabs {
display: flex;
flex: 1;
background-color: #FFFFFF;
border-radius: 20rpx;
padding: 6rpx;
box-shadow: 0 8rpx 20rpx rgba(16, 185, 129, 0.08);
border: 2rpx solid #ECFDF3;
}
.tab {
flex: 1;
text-align: center;
padding: 14rpx 0;
border-radius: 16rpx;
font-size: 26rpx;
color: #6B7280;
transition: all 0.2s;
font-weight: 600;
}
.tab-active {
background-color: #10B981;
color: #0B2F23;
box-shadow: 0 6rpx 16rpx rgba(16, 185, 129, 0.25);
}
.filter-btn {
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #ECFDF3;
box-shadow: 0 6rpx 14rpx rgba(16, 185, 129, 0.1);
}
.filter-icon {
font-size: 32rpx;
}
.scroll-container {
height: calc(100vh - 10rpx);
padding: 0 32rpx 40rpx;
box-sizing: border-box;
}
/* 骨架屏 */
.skeleton {
padding-top: 24rpx;
}
.skeleton-item {
position: relative;
padding-left: 80rpx;
margin-bottom: 32rpx;
}
.skeleton-dot {
position: absolute;
left: 0;
top: 16rpx;
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 50%, #E5E7EB 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.skeleton-card {
background-color: #FFFFFF;
border-radius: 24rpx;
padding: 24rpx;
}
.skeleton-line {
height: 24rpx;
background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 50%, #E5E7EB 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.skeleton-line-title {
width: 60%;
height: 32rpx;
}
.skeleton-line-text {
width: 80%;
}
.skeleton-line-text.short {
width: 40%;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* 列表分组 */
.log-group {
margin-bottom: 32rpx;
}
.group-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
}
.group-title {
font-size: 30rpx;
font-weight: 700;
color: #0F172A;
}
.group-count {
font-size: 22rpx;
color: #64748B;
background-color: #E8FFF1;
padding: 6rpx 16rpx;
border-radius: 999rpx;
}
.group-items {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.log-card {
position: relative;
background-color: #FFFFFF;
border-radius: 24rpx;
padding: 24rpx 24rpx 20rpx 24rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.06);
display: flex;
gap: 20rpx;
overflow: hidden;
}
.log-card-resisted {
background-color: #F6FFFA;
border: 2rpx solid #E8FFF1;
}
.log-card-smoke {
background-color: #FFF7F5;
border: 2rpx solid #FFE4E6;
}
.log-bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 8rpx;
background-color: #10B981;
}
.log-card-smoke .log-bar {
background-color: #F97316;
}
.log-icon {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
flex-shrink: 0;
}
.icon-resisted {
background-color: rgba(16, 185, 129, 0.16);
color: #0F766E;
}
.icon-smoke {
background-color: rgba(249, 115, 22, 0.15);
color: #C2410C;
}
.log-main {
flex: 1;
min-width: 0;
}
.log-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 10rpx;
}
.log-time-tag {
display: flex;
align-items: center;
gap: 12rpx;
}
.log-time {
font-size: 30rpx;
font-weight: 700;
color: #0F172A;
}
.log-tag {
font-size: 22rpx;
padding: 6rpx 14rpx;
border-radius: 12rpx;
font-weight: 700;
}
.tag-smoke {
background-color: #FEE2E2;
color: #DC2626;
}
.tag-resisted {
background-color: #DCFCE7;
color: #16A34A;
}
.log-right {
display: flex;
align-items: center;
gap: 8rpx;
flex-shrink: 0;
}
.count-pill {
font-size: 22rpx;
color: #DC2626;
background-color: #FEE2E2;
padding: 6rpx 14rpx;
border-radius: 12rpx;
font-weight: 700;
}
.thumb-pill {
font-size: 24rpx;
background-color: #DCFCE7;
color: #16A34A;
padding: 6rpx 12rpx;
border-radius: 12rpx;
}
.log-desc {
font-size: 24rpx;
color: #475569;
line-height: 1.5;
margin-bottom: 10rpx;
}
.log-meta-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
flex-wrap: wrap;
}
.level-text {
font-size: 22rpx;
font-weight: 600;
}
.log-interval {
font-size: 22rpx;
color: #64748B;
background-color: #F1F5F9;
padding: 4rpx 12rpx;
border-radius: 999rpx;
}
.log-actions {
display: flex;
justify-content: flex-end;
gap: 16rpx;
margin-top: 12rpx;
}
.action-btn {
font-size: 22rpx;
padding: 6rpx 14rpx;
border-radius: 12rpx;
background-color: #FFFFFF;
border: 2rpx solid #E2E8F0;
color: #64748B;
}
.edit-btn {
color: #2563EB;
border-color: #DBEAFE;
background-color: #EFF6FF;
}
.delete-btn {
color: #DC2626;
border-color: #FECACA;
background-color: #FEF2F2;
}
.level-unknown {
color: #64748B;
}
.level-1 {
color: #16A34A;
}
.level-2 {
color: #0EA5E9;
}
.level-3 {
color: #F59E0B;
}
.level-4 {
color: #F97316;
}
.level-5 {
color: #EF4444;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 32rpx;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 32rpx;
color: #6B7280;
font-weight: 500;
margin-bottom: 12rpx;
}
.empty-hint {
font-size: 26rpx;
color: #9CA3AF;
}
/* 加载状态 */
.loading-more, .no-more {
text-align: center;
padding: 32rpx;
}
.loading-text, .no-more-text {
font-size: 24rpx;
color: #9CA3AF;
}
/* 浮动按钮 */
.fab {
position: fixed;
right: 32rpx;
bottom: 140rpx;
width: 96rpx;
height: 96rpx;
background-color: #10B981;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 28rpx rgba(16, 185, 129, 0.35);
transition: all 0.3s;
z-index: 100;
}
.fab:active {
transform: scale(0.95);
}
.fab-icon {
font-size: 48rpx;
color: #FFFFFF;
font-weight: 300;
}
</style>