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
+72
View File
@@ -0,0 +1,72 @@
import { defineStore } from 'pinia'
import { getDashboard, getNextSmokeTime } from '@/api/smoke'
export const useDashboardStore = defineStore('dashboard', {
state: () => ({
todayCount: 0,
minutesSinceLast: 0,
weekly: [],
nextSmokeTime: null,
lastFetchTime: 0,
cacheExpiry: 30 * 1000,
loading: false
}),
getters: {
isCacheValid: (state) => {
return Date.now() - state.lastFetchTime < state.cacheExpiry
}
},
actions: {
async fetchDashboard(forceRefresh = false) {
if (!forceRefresh && this.isCacheValid) {
return
}
this.loading = true
try {
const res = await getDashboard()
this.todayCount = res.data.today_count || 0
this.minutesSinceLast = res.data.minutes_since_last || 0
this.weekly = res.data.weekly || []
this.lastFetchTime = Date.now()
} catch (e) {
console.error('fetchDashboard error:', e)
throw e
} finally {
this.loading = false
}
},
async fetchNextSmokeTime() {
try {
const res = await getNextSmokeTime()
this.nextSmokeTime = res.data
return res.data
} catch (e) {
console.error('fetchNextSmokeTime error:', e)
throw e
}
},
setDashboard(data) {
this.todayCount = data.today_count || 0
this.minutesSinceLast = data.minutes_since_last || 0
this.weekly = data.weekly || []
this.lastFetchTime = Date.now()
},
setNextSmokeTime(data) {
this.nextSmokeTime = data
},
incrementTodayCount() {
this.todayCount++
},
resetTimer() {
this.minutesSinceLast = 0
}
}
})
+10
View File
@@ -0,0 +1,10 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
export * from './user'
export * from './dashboard'
export * from './profile'
export * from './logs'
+315
View File
@@ -0,0 +1,315 @@
import { defineStore } from 'pinia'
import * as api from '@/api'
export const useLogsStore = defineStore('logs', {
state: () => ({
logs: [], // 记录列表
total: 0, // 总条数
page: 1, // 当前页
pageSize: 20, // 每页数量
hasMore: true, // 是否有更多
loading: false, // 加载状态
refreshing: false, // 刷新状态
queryType: 'all' // 当前筛选类型
}),
getters: {
// 按日期分组
groupedByDate: (state) => {
const groups = {}
state.logs.forEach(log => {
const date = log.smoke_time?.split('T')[0] || ''
if (!groups[date]) {
groups[date] = []
}
groups[date].push(log)
})
return groups
},
// 抽烟记录数量
smokeCount: (state) => {
return state.logs.filter(log => normalizeLogType(log) === 'smoke').length
},
// 忍住记录数量
resistedCount: (state) => {
return state.logs.filter(log => normalizeLogType(log) === 'resisted').length
},
// 格式化记录列表(按时间倒序,最新的在前)
formattedLogs: (state) => {
if (!state.logs || state.logs.length === 0) {
return []
}
// 获取时间戳的辅助函数(统一处理 smoke_at / smoke_time / createtime
const getTime = (log) => {
if (log.smoke_at) {
return new Date(log.smoke_at).getTime()
}
if (log.smoke_time) {
return new Date(log.smoke_time).getTime()
}
if (log.createtime) {
return typeof log.createtime === 'number'
? log.createtime * 1000
: new Date(log.createtime).getTime()
}
return 0
}
// 先按时间正序(最早在前)计算「距上次抽烟」的时间间隔,
// 再按时间倒序用于页面展示,保证间隔只和上一次「抽烟」记录有关
const logsAsc = [...state.logs].sort((a, b) => {
const timeA = getTime(a)
const timeB = getTime(b)
return timeA - timeB
})
const intervalById = new Map()
let lastSmokeTime = null
logsAsc.forEach((log) => {
const type = normalizeLogType(log)
const currentTime = getTime(log)
let interval = ''
// 已存在「上次抽烟」时间,计算与其的间隔
if (lastSmokeTime !== null && currentTime > lastSmokeTime) {
const diff = currentTime - lastSmokeTime
const hours = Math.floor(diff / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
if (hours > 0) {
interval = `${hours}小时${minutes}`
} else if (minutes > 0) {
interval = `${minutes}分钟`
} else {
interval = '刚刚'
}
}
intervalById.set(log.id, interval)
// 仅当当前记录是「抽烟」时,更新「上次抽烟时间」
if (type === 'smoke' && currentTime > 0) {
lastSmokeTime = currentTime
}
})
// 再按时间倒序排序用于展示
const sortedLogs = [...state.logs].sort((a, b) => {
const timeA = getTime(a)
const timeB = getTime(b)
return timeB - timeA // 倒序:最新的在前
})
return sortedLogs.map((log) => {
const type = normalizeLogType(log)
const interval = intervalById.get(log.id) || ''
// 获取显示日期(用本地日期,避免 UTC 导致差一天)
let displayDate = ''
if (log.smoke_time) {
displayDate = log.smoke_time.split('T')[0]
} else if (log.createtime) {
const date = typeof log.createtime === 'number'
? new Date(log.createtime * 1000)
: new Date(log.createtime)
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
displayDate = `${y}-${m}-${d}`
}
return {
...log,
type,
interval,
displayTime: formatLogTime(log.smoke_at || log.smoke_time || log.createtime),
displayDate
}
})
}
},
actions: {
// 获取记录列表
async fetchLogs(refresh = false, type) {
if (this.loading) return
this.loading = true
if (refresh) {
this.refreshing = true
this.page = 1
this.logs = []
this.queryType = type || 'all'
}
try {
const res = await api.getLogs({
page: this.page,
page_size: this.pageSize,
type: this.queryType
})
if (res.data) {
let newLogs = res.data.items || []
// 按时间倒序排序(最新的在前)
newLogs = newLogs.sort((a, b) => {
const timeA = new Date(a.smoke_at || a.smoke_time || (a.createtime ? a.createtime * 1000 : 0)).getTime()
const timeB = new Date(b.smoke_at || b.smoke_time || (b.createtime ? b.createtime * 1000 : 0)).getTime()
return timeB - timeA
})
if (refresh) {
this.logs = newLogs
} else {
// 合并并去重(按 id
const existingIds = new Set(this.logs.map(log => log.id))
const uniqueNewLogs = newLogs.filter(log => !existingIds.has(log.id))
this.logs = [...this.logs, ...uniqueNewLogs]
// 再次排序确保顺序
this.logs.sort((a, b) => {
const timeA = new Date(a.smoke_at || a.smoke_time || (a.createtime ? a.createtime * 1000 : 0)).getTime()
const timeB = new Date(b.smoke_at || b.smoke_time || (b.createtime ? b.createtime * 1000 : 0)).getTime()
return timeB - timeA
})
}
this.total = res.data.total || 0
this.hasMore = newLogs.length >= this.pageSize
}
} catch (e) {
console.error('fetchLogs error:', e)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
this.loading = false
this.refreshing = false
}
},
// 加载更多
async loadMore() {
if (!this.hasMore || this.loading) return
this.page++
await this.fetchLogs(false)
},
// 删除记录
async deleteLog(id) {
try {
await api.deleteLog(id)
// 乐观更新:先从列表中移除
const index = this.logs.findIndex(log => log.id === id)
if (index > -1) {
this.logs.splice(index, 1)
this.total--
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
return true
} catch (e) {
console.error('deleteLog error:', e)
uni.showToast({
title: '删除失败',
icon: 'none'
})
// 失败时刷新列表恢复数据
await this.fetchLogs(true)
return false
}
},
// 更新记录
async updateLog(id, data) {
try {
await api.updateLog(id, data)
// 更新本地数据
const index = this.logs.findIndex(log => log.id === id)
if (index > -1) {
this.logs[index] = {
...this.logs[index],
...data
}
}
uni.showToast({
title: '更新成功',
icon: 'success'
})
return true
} catch (e) {
console.error('updateLog error:', e)
uni.showToast({
title: '更新失败',
icon: 'none'
})
return false
}
},
// 清空列表
clearLogs() {
this.logs = []
this.total = 0
this.page = 1
this.hasMore = true
this.queryType = 'all'
}
}
})
// 辅助函数:格式化时间
function formatLogTime(timeStr) {
if (!timeStr) return '--:--'
let date
if (typeof timeStr === 'number') {
// 如果是时间戳(秒)
date = new Date(timeStr * 1000)
} else if (typeof timeStr === 'string') {
// 如果是字符串
date = new Date(timeStr)
} else {
return '--:--'
}
// 检查日期是否有效
if (isNaN(date.getTime())) {
return '--:--'
}
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
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'
}
+66
View File
@@ -0,0 +1,66 @@
import { defineStore } from 'pinia'
import { storage, PROFILE_KEY } from '@/utils/storage'
import { getProfile, updateProfile } from '@/api/profile'
import { useUserStore } from '@/stores/user'
export const useProfileStore = defineStore('profile', {
state: () => ({
exists: false,
isCompleted: false,
profile: storage.get(PROFILE_KEY),
awakeMinutes: 960,
baselineIntervalMinutes: 60
}),
getters: {
needOnboarding: (state) => !state.exists || !state.isCompleted
},
actions: {
async fetchProfile() {
try {
const res = await getProfile()
const userStore = useUserStore()
this.exists = res.data.exists
this.awakeMinutes = res.data.awake_minutes || 960
this.baselineIntervalMinutes = res.data.baseline_interval_minutes || 60
if (res.data.profile) {
this.profile = res.data.profile
storage.set(PROFILE_KEY, res.data.profile)
if (res.data.profile.mode) {
userStore.setMode(res.data.profile.mode)
}
this.isCompleted = res.data.is_completed ||
!!res.data.profile.onboarding_completed_at ||
res.data.profile.baseline_cigs_per_day > 0
} else {
this.isCompleted = res.data.is_completed
}
return res.data
} catch (e) {
console.error('fetchProfile error:', e)
throw e
}
},
async saveProfile(data) {
try {
const res = await updateProfile(data)
const userStore = useUserStore()
this.exists = res.data.exists
this.isCompleted = res.data.is_completed
this.profile = res.data.profile
storage.set(PROFILE_KEY, res.data.profile)
if (res.data.profile?.mode) {
userStore.setMode(res.data.profile.mode)
}
return res.data
} catch (e) {
console.error('saveProfile error:', e)
throw e
}
}
}
})
+37
View File
@@ -0,0 +1,37 @@
import { defineStore } from 'pinia'
import { storage, USER_KEY, SESSION_KEY, USER_MODE_KEY, QUIT_CHECKIN_KEY } from '@/utils/storage'
export const useUserStore = defineStore('user', {
state: () => ({
user: storage.get(USER_KEY),
sessionKey: storage.get(SESSION_KEY),
isLoggedIn: !!storage.get(SESSION_KEY),
mode: storage.get(USER_MODE_KEY)
}),
actions: {
setUser(user, sessionKey) {
this.user = user
this.sessionKey = sessionKey
this.isLoggedIn = true
storage.set(USER_KEY, user)
storage.set(SESSION_KEY, sessionKey)
},
setMode(mode) {
this.mode = mode
storage.set(USER_MODE_KEY, mode)
},
logout() {
this.user = null
this.sessionKey = null
this.isLoggedIn = false
this.mode = null
storage.remove(USER_KEY)
storage.remove(SESSION_KEY)
storage.remove(USER_MODE_KEY)
storage.remove(QUIT_CHECKIN_KEY)
}
}
})