feat: 增加会员功能
This commit is contained in:
@@ -10,8 +10,11 @@
|
||||
编辑个人信息
|
||||
</view>
|
||||
<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-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 title="性别">
|
||||
<wd-radio-group v-model="profileStore.profileForm.gender" type="button">
|
||||
|
||||
@@ -40,6 +40,15 @@
|
||||
</view>
|
||||
</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 size="large" title="记录默认值" is-link @click="openDefaultPopup"/>
|
||||
<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 AboutPopup from "@/components/Profile/AboutPopup.vue";
|
||||
import { share } from '@/utils/share'
|
||||
import { generateUniqueId } from "@/utils";
|
||||
|
||||
const { onShareAppMessage } = share()
|
||||
onShareAppMessage()
|
||||
@@ -128,12 +138,30 @@ onShow(() => {
|
||||
appStore.points = getPoints()
|
||||
appStore.recordCount = getRecordCount()
|
||||
appStore.achievement = getAchievement()
|
||||
getUser()
|
||||
})
|
||||
|
||||
watch(() => profileStore.isRefresh, () => {
|
||||
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() {
|
||||
Object.assign(profileStore.profileForm, profile.value)
|
||||
if (!profileStore.profileForm.height) {
|
||||
@@ -144,6 +172,10 @@ function openProfile() {
|
||||
profileStore.profileForm.birthYear = 1990
|
||||
}
|
||||
|
||||
if (!profileStore.profileForm.id) {
|
||||
profileStore.profileForm.id = generateUniqueId()
|
||||
}
|
||||
|
||||
profileStore.showProfilePopup = true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type {IDefaultRecordValue, IProfile} from "@/type/profile";
|
||||
import type {IStats} from "@/type";
|
||||
import {countRecordsByMonth, getMaxRecord, getTotalCount} from "@/utils/record";
|
||||
import type { IDefaultRecordValue, IProfile, IUser } from "@/type/profile";
|
||||
import type { IStats } from "@/type";
|
||||
import { countRecordsByMonth, getMaxRecord, getTotalCount } from "@/utils/record";
|
||||
|
||||
export const useProfileStore = defineStore('profile', {
|
||||
state: () => ({
|
||||
@@ -15,6 +15,7 @@ export const useProfileStore = defineStore('profile', {
|
||||
showThemePopup: false,
|
||||
showAboutPopup: false,
|
||||
profileForm: {
|
||||
id: '',
|
||||
gender: '',
|
||||
height: 170,
|
||||
birthYear: 1990,
|
||||
@@ -23,6 +24,11 @@ export const useProfileStore = defineStore('profile', {
|
||||
goal: '',
|
||||
name: '',
|
||||
} as IProfile,
|
||||
user: {
|
||||
id: '',
|
||||
level: '普通用户',
|
||||
expiry_date: '无'
|
||||
} as IUser,
|
||||
themeColor: 'purple',
|
||||
defaultRecordValueForm: {
|
||||
weightValue: 70,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface IProfile {
|
||||
id: string
|
||||
name: string
|
||||
gender: 'male' | 'female' | ''
|
||||
birthYear: number | null
|
||||
@@ -11,6 +12,12 @@ export interface IProfile {
|
||||
goal: string
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
id: string
|
||||
level: string
|
||||
expiry_date: string
|
||||
}
|
||||
|
||||
export interface IOverview {
|
||||
type: string
|
||||
icon: string
|
||||
|
||||
@@ -5,3 +5,7 @@ export function formatDateTime(ts: string | number) {
|
||||
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())}`
|
||||
}
|
||||
|
||||
export function generateUniqueId() {
|
||||
return Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ const DEFAULT_RECORD_VALUE_KEY = 'user_default_record_value'
|
||||
|
||||
function getEmptyProfile(): IProfile {
|
||||
return {
|
||||
id: '',
|
||||
gender: '',
|
||||
height: null,
|
||||
birthYear: null,
|
||||
|
||||
@@ -155,9 +155,31 @@ function calcSportReport(records: IHealthRecord[]): SportReport | null {
|
||||
const filtered = records.filter(r => r.type === 'sport') as SportRecord[]
|
||||
if (filtered.length === 0) return null
|
||||
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> = {}
|
||||
sorted.forEach(r => {
|
||||
const cat = r.category || '其他'
|
||||
@@ -165,19 +187,11 @@ function calcSportReport(records: IHealthRecord[]): SportReport | null {
|
||||
})
|
||||
|
||||
// 最长连续不运动天数
|
||||
const allDates = new Set<string>()
|
||||
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())
|
||||
const sortedDates = [...allDaySet].sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
|
||||
let maxNoSport = 0
|
||||
let currentNoSport = 0
|
||||
sortedDates.forEach(date => {
|
||||
if (sportDates.has(date)) {
|
||||
if (sportDaySet.has(date)) {
|
||||
currentNoSport = 0
|
||||
} else {
|
||||
currentNoSport++
|
||||
@@ -186,12 +200,12 @@ function calcSportReport(records: IHealthRecord[]): SportReport | null {
|
||||
})
|
||||
|
||||
return {
|
||||
totalRecords: records.length,
|
||||
sportDays: sorted.length,
|
||||
frequency: percent(sorted.length, records.length),
|
||||
totalRecords: filtered.length,
|
||||
sportDays: sportDays,
|
||||
frequency: percent(sportDays, totalDays),
|
||||
totalDuration: totalDur,
|
||||
avgDailyDuration: round1(totalDur / records.length),
|
||||
maxSingleDuration: max(durations),
|
||||
avgDailyDuration: round1(totalDur / sportDays),
|
||||
maxSingleDuration: max(sorted.map(r => r.duration)),
|
||||
categoryDistribution: categoryDist,
|
||||
maxConsecutiveNoSport: maxNoSport,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user