feat: enhance achievement display and update API documentation
This commit is contained in:
+56
-2
@@ -139,7 +139,6 @@ curl -X POST 'http://127.0.0.1:8080/api/v1/smoke/logs' \
|
||||
"summary": {
|
||||
"today_count": 3,
|
||||
"daily_target": 10,
|
||||
"resisted_count": 1,
|
||||
"reduced_from_yesterday": 2,
|
||||
"exceeded_yesterday": false
|
||||
},
|
||||
@@ -158,7 +157,6 @@ curl -X POST 'http://127.0.0.1:8080/api/v1/smoke/logs' \
|
||||
- `timer.suggestion_source`:建议来源(`default`/`ai`)。
|
||||
- `summary.today_count`:今日吸烟支数累加。
|
||||
- `summary.daily_target`:每日目标。
|
||||
- `summary.resisted_count`:今日忍住次数。
|
||||
- `summary.reduced_from_yesterday`:与昨日的绝对差值(非负)。
|
||||
- `summary.exceeded_yesterday`:是否比昨天多。
|
||||
- `daily_summary`:当天已缓存的 AI 日总结;无缓存时为 `null`。
|
||||
@@ -372,3 +370,59 @@ AI 生成说明:
|
||||
- `money.expected_total`:按“统计周期内有记录的天数”×`baseline_cigs_per_day` 计算;不统计无日志的天数。
|
||||
- `money.saved_cent`:按 `max(expected_total - actual_total, 0)` 计算,避免出现负值。
|
||||
- `health.available=false`:表示无历史记录。
|
||||
|
||||
## 15) 成就主题与当前称号
|
||||
|
||||
`GET /api/v1/smoke/achievement/themes`
|
||||
|
||||
返回 onboarding 可选择的称号主题。每个主题包含 `icon/name/key/levels`,前端在“重填问卷”中展示。
|
||||
|
||||
`GET /api/v1/smoke/achievement`
|
||||
|
||||
返回当前用户所选主题下的当前等级、下一等级和进度。记录抽烟模式使用“少抽积分”进阶;戒烟打卡模式仍使用连续记录天数进阶。
|
||||
|
||||
记录模式少抽积分算法:
|
||||
- 统计窗口:从 `onboarding_completed_at` 到今天,最多回看最近 90 天。
|
||||
- 只统计有记录的日期,避免无记录日期被误判为 0 根。
|
||||
- 每日基础分:`max(0, baseline_cigs_per_day - 当日抽烟支数)`。
|
||||
- 当日少抽比例 `>=30%` 额外 `+1` 分。
|
||||
- 当日少抽比例 `>=60%` 额外 `+2` 分。
|
||||
- 当日抽烟支数为 `0` 额外 `+3` 分。
|
||||
- 总积分用于匹配所选主题的等级阈值;`fa_achievement_level.required_days` 在记录模式下作为 `required_score` 使用。
|
||||
|
||||
成功响应示例:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"achievement": {
|
||||
"theme_id": 1,
|
||||
"theme_name": "意志骑士",
|
||||
"theme_key": "knight",
|
||||
"theme_icon": "🛡️",
|
||||
"metric_type": "score",
|
||||
"score": 48,
|
||||
"metrics": {
|
||||
"baseline_cigs_per_day": 10,
|
||||
"scored_days": 7,
|
||||
"total_reduced_cigs": 36,
|
||||
"today_reduced_cigs": 4,
|
||||
"today_reduce_percent": 40,
|
||||
"stable_days": 6
|
||||
},
|
||||
"current": {
|
||||
"name": "见习骑士",
|
||||
"icon": "🛡️",
|
||||
"required_score": 30
|
||||
},
|
||||
"next": {
|
||||
"name": "白银骑士",
|
||||
"icon": "⚔️",
|
||||
"required_score": 80
|
||||
},
|
||||
"progress": 0.36
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+105
-69
@@ -129,17 +129,28 @@
|
||||
<view class="record-bio-status-card">
|
||||
<view class="record-bio-breath-line"></view>
|
||||
<view class="record-bio-profile-row">
|
||||
<view class="record-bio-avatar"><text>{{ recordAvatarText }}</text></view>
|
||||
<view class="record-bio-avatar-medal">
|
||||
<image class="record-bio-avatar-bg" src="/static/achievements/theme-medallion.png" mode="aspectFill" />
|
||||
<text class="record-bio-avatar-icon">{{ recordTitle.icon }}</text>
|
||||
</view>
|
||||
<view class="record-bio-profile-main">
|
||||
<text class="record-bio-rank">节奏观察者 <text class="record-bio-level">{{ recordLevelText }}</text></text>
|
||||
<text class="record-bio-subtitle">{{ recordStatusLabel }} · 今日 {{ todayCount }}/{{ dailyTarget || '-' }} 根</text>
|
||||
<text class="record-bio-rank">{{ recordTitle.name }} <text class="record-bio-level">{{ recordTitle.level }}</text></text>
|
||||
<text class="record-bio-subtitle">{{ recordTitle.description }}</text>
|
||||
</view>
|
||||
<view class="record-bio-target-pill">
|
||||
<text class="record-bio-target-icon">◎</text>
|
||||
<text class="record-bio-target-value">{{ recordControlScore }}%</text>
|
||||
<text class="record-bio-target-value">少 {{ recordReducedCigs }} 根</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-bio-title-panel">
|
||||
<view class="record-bio-title-copy">
|
||||
<text class="record-bio-title-kicker">REDUCTION TITLE</text>
|
||||
<text class="record-bio-title-name">{{ recordTitle.badge }}</text>
|
||||
</view>
|
||||
<text class="record-bio-title-hint">{{ recordNextTitleHint }}</text>
|
||||
</view>
|
||||
|
||||
<view class="record-bio-hp-box">
|
||||
<view class="record-bio-hp-track">
|
||||
<view class="record-bio-hp-fill" :style="{ width: recordControlScore + '%' }"></view>
|
||||
@@ -208,23 +219,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="achievementData" class="record-bio-achievement-card">
|
||||
<view class="record-bio-section-head">
|
||||
<view>
|
||||
<text class="record-bio-section-title">{{ achievementData.theme_icon }} {{ achievementCardTitle }}</text>
|
||||
<text class="record-bio-section-subtitle">{{ achievementData.theme_name }} · 第 {{ achievementData.days }} 天</text>
|
||||
</view>
|
||||
<text class="record-bio-section-chip">{{ achievementData.current?.name || '--' }}</text>
|
||||
</view>
|
||||
<view v-if="achievementData.next" class="record-bio-ach-progress">
|
||||
<view class="record-bio-ach-bar">
|
||||
<view class="record-bio-ach-fill" :style="{ width: (achievementData.progress * 100) + '%' }"></view>
|
||||
</view>
|
||||
<text class="record-bio-ach-hint">距下一级「{{ achievementData.next.name }}」还需 {{ achievementData.next.required_days - achievementData.days }} 天</text>
|
||||
</view>
|
||||
<text v-else class="record-bio-ach-hint">已达最高等级</text>
|
||||
</view>
|
||||
|
||||
<view class="record-bio-tip-card">
|
||||
<text class="record-bio-tip-icon">✦</text>
|
||||
<text class="record-bio-tip-text">{{ recordHealthTip }}</text>
|
||||
@@ -321,12 +315,12 @@ const packPriceYuan = computed(() => (profileStore.profile?.pack_price_cent || 2
|
||||
// ========== 记录模式 ==========
|
||||
|
||||
const todayCount = computed(() => homeSummary.value.today_count ?? 0)
|
||||
const resistedCount = computed(() => homeSummary.value.resisted_count ?? 0)
|
||||
const dailyTarget = computed(() => {
|
||||
const target = homeSummary.value.daily_target
|
||||
if (target != null) return target
|
||||
return profileStore.profile?.baseline_cigs_per_day || 0
|
||||
})
|
||||
const recordReducedCigs = computed(() => Math.max(0, (dailyTarget.value || 0) - todayCount.value))
|
||||
const changeText = computed(() => {
|
||||
const reduced = homeSummary.value.reduced_from_yesterday
|
||||
if (reduced == null) return '较昨日暂无对比'
|
||||
@@ -382,11 +376,9 @@ const recordRhythmText = computed(() => {
|
||||
return changeText.value
|
||||
})
|
||||
|
||||
const achievementCardTitle = computed(() => isQuitMode.value ? '无烟等级' : '长期记录等级')
|
||||
|
||||
const reducePercent = computed(() => {
|
||||
if (dailyTarget.value <= 0) return 0
|
||||
return Math.round(Math.max(0, dailyTarget.value - todayCount.value) / dailyTarget.value * 100)
|
||||
return Math.round(recordReducedCigs.value / dailyTarget.value * 100)
|
||||
})
|
||||
|
||||
const recordHealthTip = computed(() => {
|
||||
@@ -411,8 +403,6 @@ const recordStatusLabel = computed(() => {
|
||||
return '需要降速'
|
||||
})
|
||||
|
||||
const recordAvatarText = computed(() => achievementData.value?.theme_icon || '控')
|
||||
const recordLevelText = computed(() => `Lv.${Math.max(1, Math.floor((achievementData.value?.days || 0) / 7) + 1)}`)
|
||||
const recordBioBuffText = computed(() => nextSmokeTimeText.value ? `下一次建议 ${nextSmokeTimeText.value}` : '正在建立控烟节奏')
|
||||
const recordControlRingStyle = computed(() => {
|
||||
const angle = Math.round(recordControlScore.value * 3.6)
|
||||
@@ -429,11 +419,32 @@ const recordYesterdayText = computed(() => {
|
||||
|
||||
const recordBioHealthItems = computed(() => [
|
||||
{ name: '今日目标', value: `${todayCount.value}/${dailyTarget.value || '-'} 根` },
|
||||
{ name: '今日忍住', value: `${resistedCount.value} 次` },
|
||||
{ name: '较昨日', value: recordYesterdayText.value },
|
||||
{ name: '建议时间', value: nextSmokeTimeText.value || '--:--' }
|
||||
])
|
||||
|
||||
const recordTitle = computed(() => {
|
||||
const current = achievementData.value?.current
|
||||
const metrics = achievementData.value?.metrics || {}
|
||||
return {
|
||||
icon: current?.icon || achievementData.value?.theme_icon || '◎',
|
||||
name: current?.name || '节奏观察者',
|
||||
level: achievementData.value?.score !== undefined ? `积分 ${achievementData.value.score}` : '积分 0',
|
||||
badge: achievementData.value?.theme_name || '成就称号',
|
||||
description: metrics.scored_days > 0
|
||||
? `累计少抽 ${metrics.total_reduced_cigs || 0} 根,稳定记录 ${metrics.stable_days || 0} 天。`
|
||||
: '完成记录后,会按少抽数量解锁称号。'
|
||||
}
|
||||
})
|
||||
|
||||
const recordNextTitleHint = computed(() => {
|
||||
const next = achievementData.value?.next
|
||||
if (!next) return achievementData.value?.current ? '已达当前最高称号' : '记录后开始进阶'
|
||||
const score = Number(achievementData.value?.score) || 0
|
||||
const required = Number(next.required_score ?? next.required_days) || 0
|
||||
return `距「${next.name}」还差 ${Math.max(0, required - score)} 分`
|
||||
})
|
||||
|
||||
// ========== 戒烟模式 ==========
|
||||
|
||||
// 服务端连续无烟天数优先,失败时从本地缓存推算
|
||||
@@ -2757,7 +2768,6 @@ onShareAppMessage(() => ({
|
||||
.record-bio-status-card,
|
||||
.record-bio-console-card,
|
||||
.record-bio-health-card,
|
||||
.record-bio-achievement-card,
|
||||
.record-bio-tip-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -2791,16 +2801,28 @@ onShareAppMessage(() => ({
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.record-bio-avatar {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 28rpx;
|
||||
.record-bio-avatar-medal {
|
||||
position: relative;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
flex-shrink: 0;
|
||||
@include flex-center;
|
||||
background: linear-gradient(135deg, #FBBF24, #67E8F9);
|
||||
box-shadow: 0 12rpx 28rpx rgba(217, 119, 6, 0.14);
|
||||
color: #ffffff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.record-bio-avatar-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 30rpx;
|
||||
box-shadow: 0 12rpx 28rpx rgba(15, 118, 110, 0.12);
|
||||
}
|
||||
|
||||
.record-bio-avatar-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.record-bio-profile-main {
|
||||
@@ -2847,6 +2869,50 @@ onShareAppMessage(() => ({
|
||||
color: #0F766E;
|
||||
}
|
||||
|
||||
.record-bio-title-panel {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
border-radius: 26rpx;
|
||||
background:
|
||||
radial-gradient(circle at 12% 30%, rgba(103, 232, 249, 0.2), transparent 34%),
|
||||
linear-gradient(135deg, rgba(255, 251, 235, 0.82), rgba(236, 253, 245, 0.72));
|
||||
border: 1rpx solid rgba(251, 191, 36, 0.18);
|
||||
}
|
||||
|
||||
.record-bio-title-copy {
|
||||
min-width: 0;
|
||||
@include flex-col;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.record-bio-title-kicker {
|
||||
font-size: 18rpx;
|
||||
font-weight: 900;
|
||||
letter-spacing: 1.2rpx;
|
||||
color: #0891B2;
|
||||
}
|
||||
|
||||
.record-bio-title-name {
|
||||
font-size: 27rpx;
|
||||
line-height: 1.25;
|
||||
font-weight: 900;
|
||||
color: #0F766E;
|
||||
}
|
||||
|
||||
.record-bio-title-hint {
|
||||
flex-shrink: 0;
|
||||
max-width: 280rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
font-weight: 800;
|
||||
text-align: right;
|
||||
color: #B45309;
|
||||
}
|
||||
|
||||
.record-bio-hp-box {
|
||||
margin-top: 30rpx;
|
||||
padding: 18rpx;
|
||||
@@ -3022,8 +3088,7 @@ onShareAppMessage(() => ({
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.record-bio-health-card,
|
||||
.record-bio-achievement-card {
|
||||
.record-bio-health-card {
|
||||
padding: 28rpx;
|
||||
border-radius: 34rpx;
|
||||
}
|
||||
@@ -3132,35 +3197,6 @@ onShareAppMessage(() => ({
|
||||
color: #0F766E;
|
||||
}
|
||||
|
||||
.record-bio-achievement-card {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.78), rgba(250, 245, 255, 0.5));
|
||||
}
|
||||
|
||||
.record-bio-ach-progress {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.record-bio-ach-bar {
|
||||
height: 14rpx;
|
||||
border-radius: 999rpx;
|
||||
overflow: hidden;
|
||||
background: rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.record-bio-ach-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, var(--record-lavender), var(--record-clear));
|
||||
}
|
||||
|
||||
.record-bio-ach-hint {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.5;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.record-bio-tip-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
Reference in New Issue
Block a user