Implement login functionality and UI updates across the application. Added silent login process in App.vue, updated styles for various components, and integrated smoke record dialog. Enhanced onboarding and profile pages with improved layouts and user experience. Updated manifest and configuration files for deployment. Added easycom configuration for component auto-import.

This commit is contained in:
nepiedg
2026-01-25 14:48:20 +08:00
parent c883ae7b17
commit 661f39dfd7
24 changed files with 4569 additions and 572 deletions
+390 -139
View File
@@ -1,5 +1,6 @@
<template>
<view class="page container">
<view class="page">
<!-- 筛选标签 -->
<view class="tabs">
<view
v-for="tab in tabs"
@@ -12,51 +13,110 @@
</view>
</view>
<view class="timeline">
<view v-for="(group, date) in groupedLogs" :key="date" class="timeline-group">
<view class="timeline-date">
<text class="timeline-date-badge">{{ formatDate(date) }}</text>
<!-- 记录列表 -->
<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 class="timeline-items">
<view
v-for="log in group"
:key="log.id"
class="timeline-item"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<view class="timeline-line"></view>
<view class="timeline-dot" :class="log.type === 'resisted' ? 'dot-green' : 'dot-smoke'">
<text v-if="log.type === 'resisted'">🛡</text>
<text v-else>🚬</text>
</view>
<view class="timeline-content card">
<view class="log-header">
<text class="log-type">{{ log.type === 'resisted' ? '已忍住' : '已抽烟' }}</text>
<text v-if="log.badge" class="log-badge" :class="log.badge.class">{{ log.badge.text }}</text>
<!-- 时间轴 -->
<view v-else-if="filteredLogs.length > 0" class="timeline">
<view v-for="(group, date) in groupedLogs" :key="date" class="timeline-group">
<view class="timeline-date">
<text class="timeline-date-badge">{{ formatDate(date) }}</text>
</view>
<view class="timeline-items">
<view v-for="(log, logIndex) in group" :key="log.id" class="timeline-item">
<view class="timeline-line" v-if="logIndex < group.length - 1"></view>
<view class="timeline-dot" :class="log.type === 'resisted' ? 'dot-green' : 'dot-smoke'">
<text v-if="log.type === 'resisted'">💪</text>
<text v-else>🚬</text>
</view>
<text class="log-time">{{ log.time }}</text>
<view v-if="log.reason" class="log-reason">
<text class="reason-icon">😫</text>
<text class="reason-text">{{ log.reason }}</text>
<!-- 记录卡片 -->
<view class="timeline-content" :class="log.type === 'resisted' ? 'content-green' : 'content-red'">
<view class="log-header">
<text class="log-type">{{ log.type === 'resisted' ? '想抽忍住了' : '记录抽烟' }}</text>
<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 class="log-time-row">
<text class="log-time">{{ log.displayTime || '--:--' }}</text>
<text v-if="log.interval" class="log-interval">距上次 {{ log.interval }}</text>
</view>
<view v-if="log.type === 'smoke'" class="log-meta">
<text class="meta-item">数量: {{ log.num !== undefined && log.num !== null ? log.num : 0 }} </text>
<text v-if="log.level !== undefined && log.level !== null" class="meta-item">等级: {{ log.level }}</text>
</view>
<view v-if="log.remark && typeof log.remark === 'string' && log.remark.trim() && log.remark.trim().length > 0" class="log-remark">
<text class="remark-text">{{ log.remark.trim() }}</text>
</view>
</view>
<text v-if="log.interval" class="log-interval">间隔 {{ log.interval }}</text>
</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 } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useLogsStore } from '@/stores/logs'
import { useLogin } from '@/hooks/useLogin'
const { waitForLogin } = useLogin()
const logsStore = useLogsStore()
const tabs = [
{ label: '全部', value: 'all' },
@@ -65,124 +125,237 @@ const tabs = [
]
const currentTab = ref('all')
const showEditDialog = ref(false)
const editType = ref('smoke')
const editData = ref(null)
const editingLogId = ref(null)
const logs = ref([
{
id: 1,
date: '2026-01-25',
time: '4:20 PM',
type: 'resisted',
reason: '压力大',
badge: { text: '成功', class: 'badge-success' }
},
{
id: 2,
date: '2026-01-25',
time: '1:15 PM',
type: 'smoke',
reason: '无聊',
interval: '1小时30分'
},
{
id: 3,
date: '2026-01-25',
time: '11:45 AM',
type: 'smoke',
reason: '晨间习惯',
badge: { text: '今日第一支', class: 'badge-info' }
},
{
id: 4,
date: '2026-01-24',
time: '10:30 PM',
type: 'smoke',
reason: '压力大',
interval: '4小时12分'
// 筛选后的记录
const filteredLogs = computed(() => {
const logs = logsStore.formattedLogs
if (currentTab.value === 'all') {
return logs
}
])
return logs.filter(log => log.type === currentTab.value)
})
// 按日期分组
const groupedLogs = computed(() => {
const filtered = currentTab.value === 'all'
? logs.value
: logs.value.filter(l => l.type === currentTab.value)
return filtered.reduce((groups, log) => {
if (!groups[log.date]) {
groups[log.date] = []
return filteredLogs.value.reduce((groups, log) => {
const date = log.displayDate
if (!groups[date]) {
groups[date] = []
}
groups[log.date].push(log)
groups[date].push(log)
return groups
}, {})
})
// 格式化日期显示
function formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const today = new Date()
const yesterday = new Date(today)
yesterday.setDate(yesterday.getDate() - 1)
if (dateStr === today.toISOString().split('T')[0]) {
return `今天, ${date.getMonth() + 1}${date.getDate()}`
const todayStr = today.toISOString().split('T')[0]
const yesterdayStr = yesterday.toISOString().split('T')[0]
if (dateStr === todayStr) {
return `今天 ${date.getMonth() + 1}${date.getDate()}`
}
if (dateStr === yesterday.toISOString().split('T')[0]) {
return `昨天, ${date.getMonth() + 1}${date.getDate()}`
if (dateStr === yesterdayStr) {
return `昨天 ${date.getMonth() + 1}${date.getDate()}`
}
return `${date.getMonth() + 1}${date.getDate()}`
}
// 下拉刷新
async function onRefresh() {
await logsStore.fetchLogs(true)
}
// 上拉加载
async function onLoadMore() {
await logsStore.loadMore()
}
// 新增记录
function addLog() {
uni.switchTab({ url: '/pages/index/index' })
}
function onTouchStart() {}
function onTouchMove() {}
function onTouchEnd() {}
// 编辑记录
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)
} catch (e) {
console.error('initPage error:', e)
}
}
onMounted(() => {
initPage()
})
</script>
<style scoped>
.page {
padding-bottom: 180rpx;
min-height: 100vh;
background: linear-gradient(to bottom, #D1FAE5 0%, #F0FDF4 50%, #FFFFFF 100%);
box-sizing: border-box;
}
.tabs {
display: flex;
gap: 16rpx;
margin-bottom: 32rpx;
padding: 32rpx 32rpx 0;
margin-bottom: 24rpx;
}
.tab {
padding: 16rpx 32rpx;
border-radius: 32rpx;
font-size: 28rpx;
color: var(--color-text-secondary);
background-color: var(--color-bg-card);
color: #6B7280;
background-color: #FFFFFF;
border: 2rpx solid #E5E7EB;
transition: all 0.3s;
}
.tab-active {
background-color: var(--color-primary);
color: var(--color-bg);
background-color: #10B981;
color: #FFFFFF;
border-color: #10B981;
font-weight: 600;
}
.timeline-group {
.scroll-container {
height: calc(100vh - 140rpx);
padding: 0 32rpx 200rpx;
}
/* 骨架屏 */
.skeleton {
padding-top: 24rpx;
}
.skeleton-item {
position: relative;
padding-left: 80rpx;
margin-bottom: 32rpx;
}
.timeline-date {
.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; }
}
/* 时间轴 */
.timeline-group {
margin-bottom: 32rpx;
}
.timeline-date {
margin-bottom: 16rpx;
}
.timeline-date-badge {
font-size: 24rpx;
color: var(--color-primary);
background-color: rgba(74, 222, 128, 0.1);
color: #059669;
background-color: #D1FAE5;
padding: 8rpx 20rpx;
border-radius: 16rpx;
font-weight: 500;
}
.timeline-items {
position: relative;
padding-left: 60rpx;
padding-left: 80rpx;
}
.timeline-item {
@@ -192,20 +365,16 @@ function onTouchEnd() {}
.timeline-line {
position: absolute;
left: -36rpx;
top: 48rpx;
left: -44rpx;
top: 56rpx;
bottom: -24rpx;
width: 4rpx;
background-color: var(--color-border);
}
.timeline-item:last-child .timeline-line {
display: none;
background-color: #E5E7EB;
}
.timeline-dot {
position: absolute;
left: -48rpx;
left: -60rpx;
top: 16rpx;
width: 48rpx;
height: 48rpx;
@@ -215,98 +384,180 @@ function onTouchEnd() {}
justify-content: center;
font-size: 24rpx;
z-index: 1;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.dot-green {
background-color: var(--color-primary);
.dot-green {
background-color: #10B981;
}
.dot-smoke {
background-color: var(--color-bg-card-light);
.dot-smoke {
background-color: #EF4444;
}
.timeline-content {
background-color: #FFFFFF;
border-radius: 24rpx;
padding: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
border-left: 4rpx solid;
transition: all 0.3s;
overflow: hidden;
}
.content-green {
border-left-color: #10B981;
}
.content-red {
border-left-color: #EF4444;
}
.log-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 8rpx;
justify-content: space-between;
margin-bottom: 12rpx;
}
.log-type {
font-size: 30rpx;
font-weight: 600;
color: #1F2937;
}
.log-badge {
font-size: 22rpx;
padding: 4rpx 12rpx;
.log-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 8rpx;
transition: all 0.3s;
}
.badge-success {
background-color: rgba(74, 222, 128, 0.2);
color: var(--color-primary);
.edit-btn {
background-color: #EFF6FF;
color: #3B82F6;
}
.badge-info {
background-color: rgba(96, 165, 250, 0.2);
color: #60A5FA;
.delete-btn {
background-color: #FEE2E2;
color: #EF4444;
}
.log-time {
font-size: 26rpx;
color: var(--color-text-secondary);
display: block;
.log-time-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.log-reason {
display: inline-flex;
align-items: center;
gap: 8rpx;
background-color: var(--color-bg);
padding: 8rpx 16rpx;
border-radius: 16rpx;
margin-bottom: 8rpx;
}
.reason-icon {
font-size: 24rpx;
}
.reason-text {
font-size: 24rpx;
color: var(--color-text-secondary);
.log-time {
font-size: 28rpx;
color: #1F2937;
font-weight: 500;
}
.log-interval {
font-size: 24rpx;
color: var(--color-text-muted);
display: block;
text-align: right;
color: #9CA3AF;
}
.log-meta {
display: flex;
gap: 16rpx;
margin-bottom: 12rpx;
flex-wrap: wrap;
}
.meta-item {
font-size: 24rpx;
color: #6B7280;
background-color: #F9FAFB;
padding: 6rpx 12rpx;
border-radius: 8rpx;
white-space: nowrap;
}
.log-remark {
background-color: #F9FAFB;
padding: 12rpx 16rpx;
border-radius: 12rpx;
margin-bottom: 0;
margin-top: 8rpx;
}
.remark-text {
font-size: 26rpx;
color: #374151;
line-height: 1.6;
word-break: break-word;
}
/* 空状态 */
.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: var(--color-primary);
background-color: #10B981;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 32rpx rgba(74, 222, 128, 0.4);
box-shadow: 0 8rpx 24rpx rgba(16, 185, 129, 0.4);
transition: all 0.3s;
z-index: 100;
}
.fab:active {
transform: scale(0.95);
}
.fab-icon {
font-size: 48rpx;
color: var(--color-bg);
color: #FFFFFF;
font-weight: 300;
}
</style>