feat: 增加会员功能

This commit is contained in:
2026-09-05 16:19:56 +08:00
parent c1592cdc5b
commit dea55bec15
7 changed files with 88 additions and 21 deletions

View File

@@ -10,8 +10,11 @@
编辑个人信息 编辑个人信息
</view> </view>
<wd-form :model="profileStore.profileForm" layout="horizontal" title-width="80" size="large" border> <wd-form :model="profileStore.profileForm" layout="horizontal" title-width="80" size="large" border>
<wd-form-item title="唯一标识">
<text selectable>{{ profileStore.profileForm.id }}</text>
</wd-form-item>
<wd-form-item title="昵称"> <wd-form-item title="昵称">
<wd-input v-model="profileStore.profileForm.name" placeholder="请输入昵称" /> <wd-input v-model="profileStore.profileForm.name" :maxlength="20" show-word-limit placeholder="请输入昵称" />
</wd-form-item> </wd-form-item>
<wd-form-item title="性别"> <wd-form-item title="性别">
<wd-radio-group v-model="profileStore.profileForm.gender" type="button"> <wd-radio-group v-model="profileStore.profileForm.gender" type="button">

View File

@@ -40,6 +40,15 @@
</view> </view>
</wd-card> </wd-card>
<wd-card title="会员等级" type="rectangle">
<view class="flex items-center justify-between">
<view class="mt-2 space-y-1 text-gray-600">
<view> 当前会员{{ profileStore.user.level }}</view>
<view>📅 到期日期{{ profileStore.user.expiry_date }}</view>
</view>
</view>
</wd-card>
<wd-cell-group> <wd-cell-group>
<wd-cell size="large" title="记录默认值" is-link @click="openDefaultPopup"/> <wd-cell size="large" title="记录默认值" is-link @click="openDefaultPopup"/>
<wd-cell size="large" title="今日概览" is-link @click="openOverviewPopup"/> <wd-cell size="large" title="今日概览" is-link @click="openOverviewPopup"/>
@@ -91,6 +100,7 @@ import { getTheme, getThemePrimaryColor } from "@/utils/themes";
import ThemePopup from "@/components/Profile/ThemePopup.vue"; import ThemePopup from "@/components/Profile/ThemePopup.vue";
import AboutPopup from "@/components/Profile/AboutPopup.vue"; import AboutPopup from "@/components/Profile/AboutPopup.vue";
import { share } from '@/utils/share' import { share } from '@/utils/share'
import { generateUniqueId } from "@/utils";
const { onShareAppMessage } = share() const { onShareAppMessage } = share()
onShareAppMessage() onShareAppMessage()
@@ -128,12 +138,30 @@ onShow(() => {
appStore.points = getPoints() appStore.points = getPoints()
appStore.recordCount = getRecordCount() appStore.recordCount = getRecordCount()
appStore.achievement = getAchievement() appStore.achievement = getAchievement()
getUser()
}) })
watch(() => profileStore.isRefresh, () => { watch(() => profileStore.isRefresh, () => {
profile.value = getProfile() profile.value = getProfile()
}) })
function getUser() {
const profile = getProfile()
if (profile.id) {
wx.request({
url: `https://cxx0822.s.3q.hair/health-api/user/${profile.id}`,
method: 'GET',
header: { 'Content-Type': 'application/json' },
success: (res) => {
profileStore.user = res.data
},
fail: (err) => {
console.error('请求失败:', err);
}
});
}
}
function openProfile() { function openProfile() {
Object.assign(profileStore.profileForm, profile.value) Object.assign(profileStore.profileForm, profile.value)
if (!profileStore.profileForm.height) { if (!profileStore.profileForm.height) {
@@ -144,6 +172,10 @@ function openProfile() {
profileStore.profileForm.birthYear = 1990 profileStore.profileForm.birthYear = 1990
} }
if (!profileStore.profileForm.id) {
profileStore.profileForm.id = generateUniqueId()
}
profileStore.showProfilePopup = true profileStore.showProfilePopup = true
} }

View File

@@ -1,7 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type {IDefaultRecordValue, IProfile} from "@/type/profile"; import type { IDefaultRecordValue, IProfile, IUser } from "@/type/profile";
import type {IStats} from "@/type"; import type { IStats } from "@/type";
import {countRecordsByMonth, getMaxRecord, getTotalCount} from "@/utils/record"; import { countRecordsByMonth, getMaxRecord, getTotalCount } from "@/utils/record";
export const useProfileStore = defineStore('profile', { export const useProfileStore = defineStore('profile', {
state: () => ({ state: () => ({
@@ -15,6 +15,7 @@ export const useProfileStore = defineStore('profile', {
showThemePopup: false, showThemePopup: false,
showAboutPopup: false, showAboutPopup: false,
profileForm: { profileForm: {
id: '',
gender: '', gender: '',
height: 170, height: 170,
birthYear: 1990, birthYear: 1990,
@@ -23,6 +24,11 @@ export const useProfileStore = defineStore('profile', {
goal: '', goal: '',
name: '', name: '',
} as IProfile, } as IProfile,
user: {
id: '',
level: '普通用户',
expiry_date: '无'
} as IUser,
themeColor: 'purple', themeColor: 'purple',
defaultRecordValueForm: { defaultRecordValueForm: {
weightValue: 70, weightValue: 70,

View File

@@ -1,4 +1,5 @@
export interface IProfile { export interface IProfile {
id: string
name: string name: string
gender: 'male' | 'female' | '' gender: 'male' | 'female' | ''
birthYear: number | null birthYear: number | null
@@ -11,6 +12,12 @@ export interface IProfile {
goal: string goal: string
} }
export interface IUser {
id: string
level: string
expiry_date: string
}
export interface IOverview { export interface IOverview {
type: string type: string
icon: string icon: string

View File

@@ -5,3 +5,7 @@ export function formatDateTime(ts: string | number) {
const pad = (n: number) => String(n).padStart(2, '0') const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
} }
export function generateUniqueId() {
return Date.now() + '-' + Math.random().toString(36).slice(2, 6);
}

View File

@@ -9,6 +9,7 @@ const DEFAULT_RECORD_VALUE_KEY = 'user_default_record_value'
function getEmptyProfile(): IProfile { function getEmptyProfile(): IProfile {
return { return {
id: '',
gender: '', gender: '',
height: null, height: null,
birthYear: null, birthYear: null,

View File

@@ -155,9 +155,31 @@ function calcSportReport(records: IHealthRecord[]): SportReport | null {
const filtered = records.filter(r => r.type === 'sport') as SportRecord[] const filtered = records.filter(r => r.type === 'sport') as SportRecord[]
if (filtered.length === 0) return null if (filtered.length === 0) return null
const sorted = sortByTime(filtered) const sorted = sortByTime(filtered)
const durations = sorted.map(r => r.duration)
const totalDur = durations.reduce((s, v) => s + v, 0)
// 按天去重,统计运动天数
const sportDaySet = new Set<string>()
sorted.forEach(r => {
sportDaySet.add(getDateStringFromTimestamp(r.timestamp))
})
const sportDays = sportDaySet.size
// 按天聚合时长
const dayDurationMap: Record<string, number> = {}
sorted.forEach(r => {
const day = getDateStringFromTimestamp(r.timestamp)
dayDurationMap[day] = (dayDurationMap[day] || 0) + r.duration
})
const dailyDurations = Object.values(dayDurationMap)
const totalDur = dailyDurations.reduce((s, v) => s + v, 0)
// 所有记录涉及的天数(去重)
const allDaySet = new Set<string>()
records.forEach(r => {
allDaySet.add(getDateStringFromTimestamp(r.timestamp))
})
const totalDays = allDaySet.size
// 分类分布(按记录条数,这个没问题)
const categoryDist: Record<string, number> = {} const categoryDist: Record<string, number> = {}
sorted.forEach(r => { sorted.forEach(r => {
const cat = r.category || '其他' const cat = r.category || '其他'
@@ -165,19 +187,11 @@ function calcSportReport(records: IHealthRecord[]): SportReport | null {
}) })
// 最长连续不运动天数 // 最长连续不运动天数
const allDates = new Set<string>() const sortedDates = [...allDaySet].sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
records.forEach(r => {
allDates.add(getDateStringFromTimestamp(r.timestamp))
})
const sportDates = new Set<string>()
sorted.forEach(r => {
sportDates.add(getDateStringFromTimestamp(r.timestamp))
})
const sortedDates = [...allDates].sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
let maxNoSport = 0 let maxNoSport = 0
let currentNoSport = 0 let currentNoSport = 0
sortedDates.forEach(date => { sortedDates.forEach(date => {
if (sportDates.has(date)) { if (sportDaySet.has(date)) {
currentNoSport = 0 currentNoSport = 0
} else { } else {
currentNoSport++ currentNoSport++
@@ -186,12 +200,12 @@ function calcSportReport(records: IHealthRecord[]): SportReport | null {
}) })
return { return {
totalRecords: records.length, totalRecords: filtered.length,
sportDays: sorted.length, sportDays: sportDays,
frequency: percent(sorted.length, records.length), frequency: percent(sportDays, totalDays),
totalDuration: totalDur, totalDuration: totalDur,
avgDailyDuration: round1(totalDur / records.length), avgDailyDuration: round1(totalDur / sportDays),
maxSingleDuration: max(durations), maxSingleDuration: max(sorted.map(r => r.duration)),
categoryDistribution: categoryDist, categoryDistribution: categoryDist,
maxConsecutiveNoSport: maxNoSport, maxConsecutiveNoSport: maxNoSport,
} }