feat: enhance achievement display and update API documentation

This commit is contained in:
nepiedg
2026-04-26 23:18:25 +08:00
parent 374168d959
commit 895f5e6d09
3 changed files with 162 additions and 72 deletions
+56 -2
View File
@@ -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
View File
@@ -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