feat: enhance achievement display and update API documentation
This commit is contained in:
+57
-3
@@ -136,10 +136,9 @@ curl -X POST 'http://127.0.0.1:8080/api/v1/smoke/logs' \
|
|||||||
"next_suggested_clock": "10:30",
|
"next_suggested_clock": "10:30",
|
||||||
"suggestion_source": "default"
|
"suggestion_source": "default"
|
||||||
},
|
},
|
||||||
"summary": {
|
"summary": {
|
||||||
"today_count": 3,
|
"today_count": 3,
|
||||||
"daily_target": 10,
|
"daily_target": 10,
|
||||||
"resisted_count": 1,
|
|
||||||
"reduced_from_yesterday": 2,
|
"reduced_from_yesterday": 2,
|
||||||
"exceeded_yesterday": false
|
"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`)。
|
- `timer.suggestion_source`:建议来源(`default`/`ai`)。
|
||||||
- `summary.today_count`:今日吸烟支数累加。
|
- `summary.today_count`:今日吸烟支数累加。
|
||||||
- `summary.daily_target`:每日目标。
|
- `summary.daily_target`:每日目标。
|
||||||
- `summary.resisted_count`:今日忍住次数。
|
|
||||||
- `summary.reduced_from_yesterday`:与昨日的绝对差值(非负)。
|
- `summary.reduced_from_yesterday`:与昨日的绝对差值(非负)。
|
||||||
- `summary.exceeded_yesterday`:是否比昨天多。
|
- `summary.exceeded_yesterday`:是否比昨天多。
|
||||||
- `daily_summary`:当天已缓存的 AI 日总结;无缓存时为 `null`。
|
- `daily_summary`:当天已缓存的 AI 日总结;无缓存时为 `null`。
|
||||||
@@ -372,3 +370,59 @@ AI 生成说明:
|
|||||||
- `money.expected_total`:按“统计周期内有记录的天数”×`baseline_cigs_per_day` 计算;不统计无日志的天数。
|
- `money.expected_total`:按“统计周期内有记录的天数”×`baseline_cigs_per_day` 计算;不统计无日志的天数。
|
||||||
- `money.saved_cent`:按 `max(expected_total - actual_total, 0)` 计算,避免出现负值。
|
- `money.saved_cent`:按 `max(expected_total - actual_total, 0)` 计算,避免出现负值。
|
||||||
- `health.available=false`:表示无历史记录。
|
- `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-status-card">
|
||||||
<view class="record-bio-breath-line"></view>
|
<view class="record-bio-breath-line"></view>
|
||||||
<view class="record-bio-profile-row">
|
<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">
|
<view class="record-bio-profile-main">
|
||||||
<text class="record-bio-rank">节奏观察者 <text class="record-bio-level">{{ recordLevelText }}</text></text>
|
<text class="record-bio-rank">{{ recordTitle.name }} <text class="record-bio-level">{{ recordTitle.level }}</text></text>
|
||||||
<text class="record-bio-subtitle">{{ recordStatusLabel }} · 今日 {{ todayCount }}/{{ dailyTarget || '-' }} 根</text>
|
<text class="record-bio-subtitle">{{ recordTitle.description }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="record-bio-target-pill">
|
<view class="record-bio-target-pill">
|
||||||
<text class="record-bio-target-icon">◎</text>
|
<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>
|
</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-box">
|
||||||
<view class="record-bio-hp-track">
|
<view class="record-bio-hp-track">
|
||||||
<view class="record-bio-hp-fill" :style="{ width: recordControlScore + '%' }"></view>
|
<view class="record-bio-hp-fill" :style="{ width: recordControlScore + '%' }"></view>
|
||||||
@@ -208,23 +219,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</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">
|
<view class="record-bio-tip-card">
|
||||||
<text class="record-bio-tip-icon">✦</text>
|
<text class="record-bio-tip-icon">✦</text>
|
||||||
<text class="record-bio-tip-text">{{ recordHealthTip }}</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 todayCount = computed(() => homeSummary.value.today_count ?? 0)
|
||||||
const resistedCount = computed(() => homeSummary.value.resisted_count ?? 0)
|
|
||||||
const dailyTarget = computed(() => {
|
const dailyTarget = computed(() => {
|
||||||
const target = homeSummary.value.daily_target
|
const target = homeSummary.value.daily_target
|
||||||
if (target != null) return target
|
if (target != null) return target
|
||||||
return profileStore.profile?.baseline_cigs_per_day || 0
|
return profileStore.profile?.baseline_cigs_per_day || 0
|
||||||
})
|
})
|
||||||
|
const recordReducedCigs = computed(() => Math.max(0, (dailyTarget.value || 0) - todayCount.value))
|
||||||
const changeText = computed(() => {
|
const changeText = computed(() => {
|
||||||
const reduced = homeSummary.value.reduced_from_yesterday
|
const reduced = homeSummary.value.reduced_from_yesterday
|
||||||
if (reduced == null) return '较昨日暂无对比'
|
if (reduced == null) return '较昨日暂无对比'
|
||||||
@@ -382,11 +376,9 @@ const recordRhythmText = computed(() => {
|
|||||||
return changeText.value
|
return changeText.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const achievementCardTitle = computed(() => isQuitMode.value ? '无烟等级' : '长期记录等级')
|
|
||||||
|
|
||||||
const reducePercent = computed(() => {
|
const reducePercent = computed(() => {
|
||||||
if (dailyTarget.value <= 0) return 0
|
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(() => {
|
const recordHealthTip = computed(() => {
|
||||||
@@ -411,8 +403,6 @@ const recordStatusLabel = computed(() => {
|
|||||||
return '需要降速'
|
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 recordBioBuffText = computed(() => nextSmokeTimeText.value ? `下一次建议 ${nextSmokeTimeText.value}` : '正在建立控烟节奏')
|
||||||
const recordControlRingStyle = computed(() => {
|
const recordControlRingStyle = computed(() => {
|
||||||
const angle = Math.round(recordControlScore.value * 3.6)
|
const angle = Math.round(recordControlScore.value * 3.6)
|
||||||
@@ -429,11 +419,32 @@ const recordYesterdayText = computed(() => {
|
|||||||
|
|
||||||
const recordBioHealthItems = computed(() => [
|
const recordBioHealthItems = computed(() => [
|
||||||
{ name: '今日目标', value: `${todayCount.value}/${dailyTarget.value || '-'} 根` },
|
{ name: '今日目标', value: `${todayCount.value}/${dailyTarget.value || '-'} 根` },
|
||||||
{ name: '今日忍住', value: `${resistedCount.value} 次` },
|
|
||||||
{ name: '较昨日', value: recordYesterdayText.value },
|
{ name: '较昨日', value: recordYesterdayText.value },
|
||||||
{ name: '建议时间', value: nextSmokeTimeText.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-status-card,
|
||||||
.record-bio-console-card,
|
.record-bio-console-card,
|
||||||
.record-bio-health-card,
|
.record-bio-health-card,
|
||||||
.record-bio-achievement-card,
|
|
||||||
.record-bio-tip-card {
|
.record-bio-tip-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -2791,16 +2801,28 @@ onShareAppMessage(() => ({
|
|||||||
gap: 18rpx;
|
gap: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-bio-avatar {
|
.record-bio-avatar-medal {
|
||||||
width: 76rpx;
|
position: relative;
|
||||||
height: 76rpx;
|
width: 96rpx;
|
||||||
border-radius: 28rpx;
|
height: 96rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
@include flex-center;
|
@include flex-center;
|
||||||
background: linear-gradient(135deg, #FBBF24, #67E8F9);
|
}
|
||||||
box-shadow: 0 12rpx 28rpx rgba(217, 119, 6, 0.14);
|
|
||||||
color: #ffffff;
|
.record-bio-avatar-bg {
|
||||||
font-size: 34rpx;
|
position: absolute;
|
||||||
font-weight: 900;
|
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 {
|
.record-bio-profile-main {
|
||||||
@@ -2847,6 +2869,50 @@ onShareAppMessage(() => ({
|
|||||||
color: #0F766E;
|
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 {
|
.record-bio-hp-box {
|
||||||
margin-top: 30rpx;
|
margin-top: 30rpx;
|
||||||
padding: 18rpx;
|
padding: 18rpx;
|
||||||
@@ -3022,8 +3088,7 @@ onShareAppMessage(() => ({
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-bio-health-card,
|
.record-bio-health-card {
|
||||||
.record-bio-achievement-card {
|
|
||||||
padding: 28rpx;
|
padding: 28rpx;
|
||||||
border-radius: 34rpx;
|
border-radius: 34rpx;
|
||||||
}
|
}
|
||||||
@@ -3132,35 +3197,6 @@ onShareAppMessage(() => ({
|
|||||||
color: #0F766E;
|
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 {
|
.record-bio-tip-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
Reference in New Issue
Block a user