diff --git a/docs/nsti.md b/docs/nsti.md
new file mode 100644
index 0000000..ab88880
--- /dev/null
+++ b/docs/nsti.md
@@ -0,0 +1,55 @@
+# NSTI 戒烟人格测试
+
+## 概览
+
+`NSTI` 是为 `smt` 小程序新增的一组轻量娱乐化测试页面,用 10 道题和 16 种抽象人格帮助用户以更低心理压力进入戒烟主题。
+
+当前实现为纯前端方案,不依赖后端接口:
+
+- 题库和人格画像:`src/utils/nsti-data.js`
+- 结果算法与本地存储:`src/utils/nsti.js`
+- 测试首页:`src/pages/nsti/index.vue`
+- 答题页:`src/pages/nsti/test.vue`
+- 结果页:`src/pages/nsti/result.vue`
+- 入口位置:`src/pages/profile/index.vue`
+
+## 页面流转
+
+1. 用户从“我的”页进入 `NSTI 戒烟人格测试`
+2. 在 `pages/nsti/index` 查看测试说明和最近结果
+3. 进入 `pages/nsti/test` 完成 10 道题
+4. 本地计算结果后跳转 `pages/nsti/result`
+5. 结果页可重新测试、分享文案、进入戒烟主流程
+
+## 数据与存储
+
+使用本地缓存保存测试结果和草稿:
+
+- `nsti_latest_result`:最近一次结果
+- `nsti_history`:历史结果列表,最多保留 12 条
+- `nsti_draft`:中途退出后的答题草稿
+
+对应常量定义在 `src/utils/storage.js`。
+
+## 结果算法
+
+当前算法采用“维度计数 + 人格权重”的混合方案:
+
+1. 每道题的 A/B/C/D 选项分别代表一种行为维度
+2. 每个选项同时会给 1-2 个具体人格加权
+3. 提交后先统计维度分布,再统计人格得分
+4. 根据前两高维度组合给对应人格一个额外加权
+5. 取最终得分最高的人格作为结果,若分数相同则优先更抽象的人格
+
+这种做法兼顾了:
+
+- 题目抽象风格和直觉作答体验
+- 16 型人格的差异化结果
+- 后续扩展为后台配置时的可维护性
+
+## 后续可扩展方向
+
+- 把题库和人格画像迁移到后台配置
+- 记录测试完成率、分享率、人格分布
+- 把人格结果与首页激励文案、AI 建议、戒烟计划联动
+- 增加人格历史对比和“再次测试看变化”
diff --git a/src/api/auth.js b/src/api/auth.js
index 325fa99..e6ffe67 100644
--- a/src/api/auth.js
+++ b/src/api/auth.js
@@ -2,6 +2,7 @@ import { request } from './request'
import { MINI_PROGRAM_ID } from '@/config'
import pinia, { useUserStore } from '@/stores'
import { storage, SESSION_KEY, USER_KEY, USER_MODE_KEY } from '@/utils/storage'
+import { BASE_URL } from '@/config'
const H5_DEBUG_SESSION_KEY = 'FxLFPHHBw49loODmRSvqdg=='
@@ -62,3 +63,83 @@ export function logout() {
storage.remove(SESSION_KEY)
storage.remove(USER_KEY)
}
+
+export async function updateUserProfile(data) {
+ const res = await request.put('/auth/profile', data)
+ const user = storage.get(USER_KEY)
+ if (user && res.data) {
+ if (res.data.nickname) user.nickname = res.data.nickname
+ if (res.data.avatar_url) user.avatar_url = res.data.avatar_url
+ storage.set(USER_KEY, user)
+ }
+ return res
+}
+
+export { updateUserProfile as updateProfile }
+
+export function getUploadToken(filename) {
+ return request.post('/common/upload/oss/token', { filename })
+}
+
+export function downloadMiniProgramTestCode(params = {}) {
+ const sessionKey = storage.get(SESSION_KEY)
+ const path = encodeURIComponent(params.path || 'pages/nsti/test?resume=0')
+ const width = params.width || 280
+ return new Promise((resolve, reject) => {
+ uni.downloadFile({
+ url: `${BASE_URL}/auth/mini-program-test-code?path=${path}&width=${width}`,
+ header: {
+ Authorization: sessionKey ? `Bearer ${sessionKey}` : ''
+ },
+ success: (res) => {
+ if (res.statusCode === 200 && res.tempFilePath) {
+ resolve(res.tempFilePath)
+ return
+ }
+ reject(new Error(`下载小程序码失败: ${res.statusCode || 'unknown'}`))
+ },
+ fail: (err) => reject(err)
+ })
+ })
+}
+
+export async function uploadFile(filePath) {
+ const ext = filePath.split('.').pop() || 'jpg'
+ const tokenRes = await getUploadToken(`avatar.${ext}`)
+ const data = tokenRes.data
+
+ let uploadUrl = (data.upload_url || '').replace(/\/$/, '')
+ if (!uploadUrl || uploadUrl.indexOf('aliyuncs.com') === -1) {
+ uploadUrl = (data.cdn_domain || '').replace(/\/$/, '')
+ if (uploadUrl && uploadUrl.indexOf('http') !== 0) {
+ uploadUrl = 'https://' + uploadUrl
+ }
+ }
+
+ return new Promise((resolve, reject) => {
+ uni.uploadFile({
+ url: uploadUrl,
+ filePath,
+ name: 'file',
+ formData: {
+ key: data.key,
+ OSSAccessKeyId: data.oss_access_key_id,
+ policy: data.oss_policy,
+ Signature: data.oss_signature
+ },
+ success: (res) => {
+ const code = res.statusCode || 0
+ if (code === 200 || code === 204 || (code >= 200 && code < 300)) {
+ const cdnDomain = (data.cdn_domain || '').replace(/\/$/, '')
+ const fileUrl = cdnDomain.startsWith('http')
+ ? `${cdnDomain}/${data.key}`
+ : `https://${cdnDomain}/${data.key}`
+ resolve({ url: fileUrl, key: data.key })
+ } else {
+ reject(new Error('上传失败: ' + (res.data || code)))
+ }
+ },
+ fail: (err) => reject(new Error(err.errMsg || '上传失败'))
+ })
+ })
+}
diff --git a/src/api/index.js b/src/api/index.js
index 58e033c..d532ff2 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -1,3 +1,3 @@
export * from './auth'
export * from './smoke'
-export * from './profile'
+export { getProfile as getSmokeProfile, updateProfile as updateSmokeProfile } from './profile'
diff --git a/src/api/request.js b/src/api/request.js
index 23898ca..7d46a21 100644
--- a/src/api/request.js
+++ b/src/api/request.js
@@ -13,12 +13,12 @@ function isInvalidToken(res) {
export const request = {
async request(options) {
const sessionKey = storage.get(SESSION_KEY)
- // 测试
const isRetryAfter401 = options._retryAfter401 === true
+ const baseUrl = options.baseUrl || BASE_URL
return new Promise((resolve, reject) => {
uni.request({
- url: BASE_URL + options.url,
+ url: baseUrl + options.url,
method: options.method || 'GET',
data: options.data,
header: {
diff --git a/src/api/smoke.js b/src/api/smoke.js
index 62ab0bc..505478f 100644
--- a/src/api/smoke.js
+++ b/src/api/smoke.js
@@ -1,4 +1,7 @@
import { request } from './request'
+import { BASE_URL } from '@/config'
+
+const BASE_URL_V2 = BASE_URL.replace('/v1', '/v2')
export function getDashboard(params = {}) {
return request.get('/smoke/dashboard', params)
@@ -88,3 +91,44 @@ export function getQuitPlanDays(planId) {
export function resetQuitPlan() {
return request.post('/smoke/quit-plan/reset')
}
+
+// 成就系统 API
+export function getAchievementThemes() {
+ return request.get('/smoke/achievement/themes')
+}
+
+export function getAchievement() {
+ return request.get('/smoke/achievement')
+}
+
+// V2 戒烟打卡 API
+export function getQuitCheckinHome() {
+ return request.request({ url: '/checkin/home', method: 'GET', baseUrl: BASE_URL_V2 })
+}
+
+export function quitCheckin(data = {}) {
+ return request.request({ url: '/checkin/check', method: 'POST', data, baseUrl: BASE_URL_V2 })
+}
+
+export function upsertQuitCheckinProfile(data) {
+ return request.request({ url: '/profile', method: 'POST', data, baseUrl: BASE_URL_V2 })
+}
+
+// 预设梦想目标
+export function listDreamPresets() {
+ return request.request({ url: '/dream-presets', method: 'GET', baseUrl: BASE_URL_V2 })
+}
+
+// 梦想目标 API
+export function listRewardGoals(status = 'all') {
+ return request.request({ url: '/reward-goals', method: 'GET', data: { status }, baseUrl: BASE_URL_V2 })
+}
+
+export function createRewardGoal(data) {
+ return request.request({ url: '/reward-goals', method: 'POST', data, baseUrl: BASE_URL_V2 })
+}
+
+export function updateRewardGoal(id, data) {
+ return request.request({ url: `/reward-goals/${id}`, method: 'PUT', data, baseUrl: BASE_URL_V2 })
+}
+
diff --git a/src/config/index.js b/src/config/index.js
index 406c6f1..733f32a 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -5,6 +5,7 @@ const ENV = {
},
production: {
BASE_URL: 'https://wx.nepiedg.top/api/v1',
+ // BASE_URL: 'http://192.168.31.73:8080/api/v1',
MINI_PROGRAM_ID: 2
}
}
diff --git a/src/pages.json b/src/pages.json
index f2a4d62..dcb5a56 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -54,6 +54,24 @@
"navigationBarTitleText": "戒烟分享"
}
},
+ {
+ "path": "pages/nsti/index",
+ "style": {
+ "navigationBarTitleText": "赛博尼古丁测试"
+ }
+ },
+ {
+ "path": "pages/nsti/test",
+ "style": {
+ "navigationBarTitleText": "人格测试"
+ }
+ },
+ {
+ "path": "pages/nsti/result",
+ "style": {
+ "navigationBarTitleText": "测试结果"
+ }
+ },
{
"path": "pages/profile/index",
"style": {
@@ -71,6 +89,13 @@
"style": {
"navigationStyle": "custom"
}
+ },
+ {
+ "path": "pages/dream-goals/index",
+ "style": {
+ "navigationStyle": "default",
+ "navigationBarTitleText": "梦想清单"
+ }
}
],
"globalStyle": {
diff --git a/src/pages/dream-goals/index.vue b/src/pages/dream-goals/index.vue
new file mode 100644
index 0000000..091d08a
--- /dev/null
+++ b/src/pages/dream-goals/index.vue
@@ -0,0 +1,626 @@
+
+
+
+
+ 戒烟已省下
+
+ ¥
+ {{ savedMoneyYuan }}
+
+ 每天省下的钱,离梦想更近一步
+
+
+
+
+ 进行中
+ 已实现
+
+
+
+
+
+
+
+
+ 🎯
+ {{ currentTab === 'active' ? '还没有梦想目标' : '还没有已实现的目标' }}
+ 添加一个
+
+
+
+
+
+
+ {{ goal.cover_image.replace('icon:', '') }}
+ 🎁
+
+
+ {{ goal.title }}
+ ¥{{ (goal.target_amount_cent / 100).toFixed(0) }}
+
+
+
+
+ {{ getEtaText(goal) }}
+
+
+ ✓ 已实现
+
+
+
+ 已买到
+
+
+
+
+
+
+ +
+
+
+
+
+
+ 添加梦想目标
+
+
+ 我想买
+
+
+
+
+ 价格(元)
+
+
+
+
+ 选择图标
+
+ 加载中...
+
+
+ 暂无图标
+
+
+
+
+ {{ (icon.cover_image || '').replace('icon:', '') || '🎁' }}
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index 719005c..3d687d2 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -21,8 +21,7 @@
无烟旅程
- 今天也在慢慢变好
- {{ quitEncouragement }}
+ {{ quitEncouragement }}
{{ todayChecked ? '今日已完成' : '等待打卡' }}
@@ -53,6 +52,25 @@
+
+
+
+ {{ achievementData.theme_icon }}
+
+ {{ achievementData.current?.name || '--' }}
+ {{ achievementData.theme_name }}
+
+
+
+
+
+
+
+ 距「{{ achievementData.next.name }}」还需 {{ achievementData.next.required_days - achievementData.days }} 天
+
+ 已达最高等级
+
+
-
-
-
-
+
-
- {{ milestone.label }}
+
+ {{ ms.name }}
+ {{ ms.percent }}%
+
+
+
+
-
- 今日提醒
- {{ healthTip }}
+
+
+ 🎯
+
+ 梦想清单
+ {{ activeGoalText }}
+
+
+ ›
+
+
+
+ 💡
+ {{ healthTip }}
@@ -212,6 +247,28 @@
💡
{{ recordHealthTip }}
+
+
+
+
+
+ {{ achievementData.current?.name || '--' }}
+ 第 {{ achievementData.days }} 天
+
+
+
+
+
+ 距下一级「{{ achievementData.next.name }}」还需 {{ achievementData.next.required_days - achievementData.days }} 天
+
+
+ 已达最高等级
+
+
+
@@ -244,6 +301,8 @@ const showDialog = ref(false)
const dialogType = ref('smoke')
const homeData = ref(null)
const pageReady = ref(false)
+const achievementData = ref(null)
+const quitHomeData = ref(null)
const quitState = ref(defaultQuitState())
let timerInterval = null
@@ -288,35 +347,59 @@ const nextSmokeTimeText = computed(() => {
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
})
-// 戒烟模式计算
+// 戒烟模式计算 — 优先使用 V2 API 数据
+const quitSummary = computed(() => quitHomeData.value?.summary || {})
+const quitDailyStatus = computed(() => quitHomeData.value?.daily_status || {})
+
const baselineCigsPerDay = computed(() => profileStore.profile?.baseline_cigs_per_day || 10)
const packPriceYuan = computed(() => (profileStore.profile?.pack_price_cent || 2500) / 100)
const quitDays = computed(() => {
+ if (quitSummary.value.current_streak_days !== undefined) {
+ return quitSummary.value.current_streak_days
+ }
if (!quitState.value.lastCheckinDate) return 0
const gap = diffDays(quitState.value.lastCheckinDate, formatDate(new Date()))
if (gap > 1) return 0
return Number(quitState.value.streakDays || 0)
})
-const todayChecked = computed(() => quitState.value.lastCheckinDate === formatDate(new Date()))
-const todayCheckinTime = computed(() => formatClock(quitState.value.lastCheckinAt))
+const todayChecked = computed(() => {
+ if (quitDailyStatus.value.status) {
+ return quitDailyStatus.value.status === 'checked_in'
+ }
+ return quitState.value.lastCheckinDate === formatDate(new Date())
+})
+
+const todayCheckinTime = computed(() => {
+ if (quitDailyStatus.value.checkin_at) {
+ return formatClock(quitDailyStatus.value.checkin_at)
+ }
+ return formatClock(quitState.value.lastCheckinAt)
+})
const savedMoney = computed(() => {
- const total = (quitDays.value * baselineCigsPerDay.value / 20) * packPriceYuan.value
- return Math.round(total)
+ if (quitSummary.value.saved_money_cent !== undefined) {
+ return Math.round(quitSummary.value.saved_money_cent / 100)
+ }
+ return Math.round((quitDays.value * baselineCigsPerDay.value / 20) * packPriceYuan.value)
})
const avoidedCigs = computed(() => {
+ if (quitSummary.value.avoided_cigs !== undefined) {
+ return quitSummary.value.avoided_cigs
+ }
return quitDays.value * baselineCigsPerDay.value
})
const lifeSaved = computed(() => {
- // 每支烟减少约11分钟生命,换算成小时
- return Math.round(quitDays.value * baselineCigsPerDay.value * 11 / 60)
+ return Math.round(avoidedCigs.value * 11 / 60)
})
const healthProgress = computed(() => {
+ if (quitSummary.value.health_recovery_percent !== undefined) {
+ return quitSummary.value.health_recovery_percent
+ }
if (quitDays.value >= 365) return 100
if (quitDays.value >= 180) return 85
if (quitDays.value >= 90) return 70
@@ -361,6 +444,30 @@ const healthMilestones = computed(() => [
{ days: 365, label: '1年' }
])
+const healthMilestoneItems = computed(() => {
+ const milestones = [
+ { name: '血压心率恢复', minutes: 20 },
+ { name: '一氧化碳排出', minutes: 480 },
+ { name: '尼古丁代谢完', minutes: 4320 },
+ { name: '味觉嗅觉恢复', minutes: 43200 },
+ { name: '血液循环改善', minutes: 129600 },
+ { name: '肺功能提升', minutes: 525600 },
+ ]
+ const minutes = quitDays.value * 1440
+ return milestones.map(m => ({
+ name: m.name,
+ percent: Math.min(Math.round((minutes / m.minutes) * 100), 100)
+ }))
+})
+
+const activeGoalText = computed(() => {
+ const goal = quitHomeData.value?.goal
+ if (!goal) return '设定一个小目标,攒钱实现它'
+ const remaining = goal.target_amount_cent - (goal.current_amount_cent || 0)
+ if (remaining <= 0) return `「${goal.title}」已攒够!`
+ return `「${goal.title}」还差 ¥${Math.round(remaining / 100)}`
+})
+
const todayCountPercent = computed(() => {
if (!dailyTarget.value || dailyTarget.value <= 0) return todayCount.value > 0 ? 100 : 0
const percent = Math.round((todayCount.value / dailyTarget.value) * 100)
@@ -518,21 +625,73 @@ async function handleSubmit(submitData) {
}
}
-function handleQuitCheckin() {
+async function handleQuitCheckin() {
if (todayChecked.value) {
uni.showToast({ title: '今天已经打过卡', icon: 'none' })
return
}
- const today = formatDate(new Date())
- const previousDate = quitState.value.lastCheckinDate
- let streakDays = 1
- if (previousDate) {
- const gap = diffDays(previousDate, today)
- if (gap === 1) streakDays = Number(quitState.value.streakDays || 0) + 1
- else if (gap === 0) streakDays = Number(quitState.value.streakDays || 0)
+ try {
+ const today = formatDate(new Date())
+ const res = await api.quitCheckin({ date: today })
+ applyQuitHomeData(res.data)
+ saveQuitState({
+ lastCheckinDate: today,
+ lastCheckinAt: new Date().toISOString(),
+ streakDays: res.data?.summary?.current_streak_days || 0
+ })
+ uni.showToast({ title: '打卡成功', icon: 'success' })
+ } catch (e) {
+ console.error('handleQuitCheckin error:', e)
+ uni.showToast({ title: '打卡失败,请重试', icon: 'none' })
}
- saveQuitState({ lastCheckinDate: today, lastCheckinAt: new Date().toISOString(), streakDays })
- uni.showToast({ title: '打卡成功', icon: 'success' })
+}
+
+function gotoDreamGoals() {
+ uni.navigateTo({ url: '/pages/dream-goals/index' })
+}
+
+function applyQuitHomeData(data) {
+ if (!data) return
+ quitHomeData.value = data
+ if (data.daily_status?.status === 'checked_in' && data.daily_status?.checkin_at) {
+ saveQuitState({
+ lastCheckinDate: data.daily_status.date,
+ lastCheckinAt: data.daily_status.checkin_at,
+ streakDays: data.summary?.current_streak_days || 0
+ })
+ }
+}
+
+async function fetchQuitHomeData() {
+ try {
+ const res = await api.getQuitCheckinHome()
+ applyQuitHomeData(res.data)
+ } catch (e) {
+ const msg = e?.message || ''
+ if (msg.includes('基础资料')) {
+ await ensureQuitProfile()
+ try {
+ const res = await api.getQuitCheckinHome()
+ applyQuitHomeData(res.data)
+ } catch (retryErr) {
+ console.error('fetchQuitHomeData retry error:', retryErr)
+ loadQuitState()
+ }
+ } else {
+ console.error('fetchQuitHomeData error:', e)
+ loadQuitState()
+ }
+ }
+}
+
+async function ensureQuitProfile() {
+ const profile = profileStore.profile
+ if (!profile) return
+ await api.upsertQuitCheckinProfile({
+ quit_start_date: formatDate(new Date()),
+ pack_price_cent: profile.pack_price_cent || 2500,
+ baseline_cigs_per_day: profile.baseline_cigs_per_day || 10
+ })
}
async function ensureProfileReady() {
@@ -548,13 +707,23 @@ async function ensureProfileReady() {
return true
}
+async function fetchAchievement() {
+ try {
+ const res = await api.getAchievement()
+ achievementData.value = res.data?.achievement || null
+ } catch (e) {
+ console.error('fetchAchievement error:', e)
+ }
+}
+
async function refreshCurrentMode() {
if (!userStore.mode) return
const profileReady = await ensureProfileReady()
if (!profileReady) return
+ fetchAchievement()
if (isQuitMode.value) {
stopTimer()
- loadQuitState()
+ await fetchQuitHomeData()
return
}
await fetchRecordHomeData()
@@ -698,8 +867,7 @@ onShareAppMessage(() => ({
.quit-hero-card,
.quit-checkin-card,
-.quit-health-card,
-.quit-motivation-card {
+.quit-health-card {
position: relative;
overflow: hidden;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(251, 253, 252, 0.94) 100%);
@@ -714,8 +882,7 @@ onShareAppMessage(() => ({
}
.quit-hero-card::before,
-.quit-health-card::before,
-.quit-motivation-card::before {
+.quit-health-card::before {
content: '';
position: absolute;
width: 240rpx;
@@ -744,7 +911,7 @@ onShareAppMessage(() => ({
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
- margin-bottom: 26rpx;
+ margin-bottom: 20rpx;
}
.quit-hero-copy {
@@ -765,20 +932,12 @@ onShareAppMessage(() => ({
}
.quit-hero-title {
- display: block;
- margin-top: 16rpx;
- font-size: 42rpx;
- line-height: 1.2;
- font-weight: 800;
- color: #111827;
-}
-
-.quit-hero-subtitle {
display: block;
margin-top: 10rpx;
- font-size: 24rpx;
- line-height: 1.6;
- color: #6b7280;
+ font-size: 28rpx;
+ line-height: 1.5;
+ font-weight: 700;
+ color: #14936d;
}
.quit-hero-chip {
@@ -966,10 +1125,10 @@ onShareAppMessage(() => ({
.quit-health-header {
display: flex;
- align-items: flex-start;
+ align-items: center;
justify-content: space-between;
gap: 16rpx;
- margin-bottom: 18rpx;
+ margin-bottom: 20rpx;
}
.quit-health-title {
@@ -979,101 +1138,238 @@ onShareAppMessage(() => ({
color: #111827;
}
-.quit-health-subtitle {
- display: block;
- margin-top: 8rpx;
- font-size: 22rpx;
- line-height: 1.5;
- color: #6b7280;
+.quit-health-badge {
+ font-size: 21rpx;
+ padding: 8rpx 16rpx;
+ border-radius: 999rpx;
+ background: rgba(240, 252, 248, 0.94);
+ border: 1rpx solid rgba(15, 23, 42, 0.05);
+ color: #14936d;
+ font-weight: 600;
}
-.quit-health-value {
- font-size: 34rpx;
- font-weight: 800;
+.quit-health-badge-done {
+ background: linear-gradient(135deg, #10B981, #14936d);
+ color: #fff;
+ border-color: transparent;
+}
+
+.quit-milestone-list {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.quit-ms-item {
+ position: relative;
+}
+
+.quit-ms-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8rpx;
+}
+
+.quit-ms-name {
+ font-size: 24rpx;
+ color: #374151;
+ font-weight: 500;
+}
+
+.quit-ms-pct {
+ font-size: 22rpx;
+ color: #9ca3af;
+ font-weight: 600;
+}
+
+.quit-ms-pct-done {
color: #14936d;
}
-.quit-health-bar {
- position: relative;
- z-index: 1;
- height: 14rpx;
+.quit-ms-bar {
+ height: 10rpx;
background: rgba(15, 23, 42, 0.06);
border-radius: 999rpx;
overflow: hidden;
}
-.quit-health-bar-fill {
+.quit-ms-fill {
height: 100%;
- background: linear-gradient(90deg, #31c18b 0%, #14936d 100%);
border-radius: 999rpx;
transition: width 0.5s ease;
}
-.quit-health-milestones {
- display: flex;
- justify-content: space-between;
- gap: 8rpx;
- margin-top: 22rpx;
+.quit-ms-fill-done {
+ background: linear-gradient(90deg, #31c18b, #14936d);
}
-.quit-milestone-item {
+.quit-ms-fill-pending {
+ background: rgba(52, 200, 160, 0.3);
+}
+
+.quit-ach-inline {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16rpx;
+ margin-top: 20rpx;
+ padding: 18rpx 20rpx;
+ border-radius: 22rpx;
+ background: linear-gradient(180deg, #f5fbf9 0%, #eef6f2 100%);
+ border: 1rpx solid rgba(15, 23, 42, 0.05);
+ position: relative;
+ z-index: 1;
+}
+
+.quit-ach-left {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ flex-shrink: 0;
+}
+
+.quit-ach-icon {
+ font-size: 36rpx;
+ line-height: 1;
+}
+
+.quit-ach-info {
display: flex;
flex-direction: column;
- align-items: center;
- gap: 8rpx;
- flex: 1;
}
-.quit-milestone-dot {
- width: 16rpx;
- height: 16rpx;
- border-radius: 50%;
- background: rgba(15, 23, 42, 0.08);
- border: 2rpx solid rgba(15, 23, 42, 0.1);
+.quit-ach-rank {
+ font-size: 28rpx;
+ font-weight: 800;
+ color: #14936d;
+ line-height: 1.2;
}
-.quit-milestone-done .quit-milestone-dot {
- background: #31c18b;
- border-color: #14936d;
-}
-
-.quit-milestone-text {
+.quit-ach-theme {
font-size: 20rpx;
color: #9ca3af;
+ margin-top: 2rpx;
}
-.quit-milestone-done .quit-milestone-text {
+.quit-ach-right {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.quit-ach-progress-area {
+ width: 100%;
+ max-width: 280rpx;
+}
+
+.quit-ach-bar {
+ height: 8rpx;
+ background: rgba(16, 185, 129, 0.12);
+ border-radius: 999rpx;
+ overflow: hidden;
+}
+
+.quit-ach-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #10B981, #34D399);
+ border-radius: 999rpx;
+ transition: width 0.5s ease;
+}
+
+.quit-ach-hint {
+ display: block;
+ margin-top: 6rpx;
+ font-size: 20rpx;
+ color: #9ca3af;
+ text-align: right;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.quit-ach-max {
+ font-size: 20rpx;
color: #14936d;
font-weight: 600;
}
-.quit-motivation-card {
- padding: 24rpx;
-}
-
-.quit-motivation-label {
- position: relative;
- z-index: 1;
- display: inline-flex;
+.quit-dream-entry {
+ display: flex;
align-items: center;
- padding: 8rpx 18rpx;
- border-radius: 999rpx;
- background: rgba(240, 252, 248, 0.94);
- border: 1rpx solid rgba(15, 23, 42, 0.05);
- font-size: 20rpx;
- font-weight: 600;
- color: #6b7280;
+ justify-content: space-between;
+ padding: 22rpx 24rpx;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(251, 253, 252, 0.94) 100%);
+ border-radius: 24rpx;
+ border: 1rpx solid rgba(15, 23, 42, 0.06);
+ box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
}
-.quit-motivation-text {
- position: relative;
- z-index: 1;
+.quit-dream-left {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ flex: 1;
+ min-width: 0;
+}
+
+.quit-dream-icon {
+ font-size: 36rpx;
+ flex-shrink: 0;
+}
+
+.quit-dream-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.quit-dream-title {
display: block;
- margin-top: 16rpx;
font-size: 28rpx;
- line-height: 1.7;
- font-weight: 600;
- color: #374151;
+ font-weight: 700;
+ color: #111827;
+}
+
+.quit-dream-desc {
+ display: block;
+ margin-top: 4rpx;
+ font-size: 22rpx;
+ color: #9ca3af;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.quit-dream-arrow {
+ font-size: 36rpx;
+ color: #d1d5db;
+ flex-shrink: 0;
+ font-weight: 300;
+}
+
+.quit-tip-bar {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ padding: 14rpx 18rpx;
+ border-radius: 16rpx;
+ background: rgba(240, 252, 248, 0.6);
+ border: 1rpx solid rgba(15, 23, 42, 0.03);
+}
+
+.quit-tip-icon {
+ font-size: 22rpx;
+ flex-shrink: 0;
+}
+
+.quit-tip-text {
+ font-size: 20rpx;
+ line-height: 1.5;
+ color: #9ca3af;
}
/* ===== 记录模式 ===== */
@@ -1497,4 +1793,93 @@ onShareAppMessage(() => ({
color: #4b5563;
line-height: 1.5;
}
+
+.record-achievement-card {
+ margin-top: 20rpx;
+ padding: 24rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 251, 249, 0.94) 100%);
+ border: 1rpx solid rgba(15, 23, 42, 0.06);
+ box-shadow: 0 6rpx 14rpx rgba(15, 23, 42, 0.03);
+}
+
+.ach-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16rpx;
+}
+
+.ach-title {
+ font-size: 26rpx;
+ font-weight: 700;
+ color: #111827;
+}
+
+.ach-theme-name {
+ font-size: 22rpx;
+ color: #6b7280;
+ padding: 6rpx 14rpx;
+ background: rgba(240, 252, 248, 0.94);
+ border-radius: 999rpx;
+ border: 1rpx solid rgba(15, 23, 42, 0.05);
+}
+
+.ach-body {
+ position: relative;
+ z-index: 1;
+}
+
+.ach-current {
+ display: flex;
+ align-items: baseline;
+ gap: 12rpx;
+ margin-bottom: 16rpx;
+}
+
+.ach-rank {
+ font-size: 40rpx;
+ font-weight: 800;
+ color: #14936d;
+ font-family: 'DIN Alternate', -apple-system, sans-serif;
+}
+
+.ach-days {
+ font-size: 24rpx;
+ color: #6b7280;
+}
+
+.ach-progress-wrap {
+ margin-bottom: 4rpx;
+}
+
+.ach-progress-bar {
+ height: 12rpx;
+ background: rgba(16, 185, 129, 0.12);
+ border-radius: 6rpx;
+ overflow: hidden;
+ margin-bottom: 10rpx;
+}
+
+.ach-progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #10B981, #34D399);
+ border-radius: 6rpx;
+ transition: width 0.5s ease;
+}
+
+.ach-next-hint {
+ font-size: 22rpx;
+ color: #6b7280;
+}
+
+.ach-max {
+ margin-top: 4rpx;
+}
+
+.ach-max-text {
+ font-size: 22rpx;
+ color: #14936d;
+ font-weight: 600;
+}
diff --git a/src/pages/logs/index.vue b/src/pages/logs/index.vue
index 132d764..25960cb 100644
--- a/src/pages/logs/index.vue
+++ b/src/pages/logs/index.vue
@@ -1,27 +1,7 @@
-
- 记录历史
- 按时间查看抽烟和忍住记录,随时回看自己的变化轨迹。
-
-
- {{ filteredLogs.length }}
- 当前筛选
-
-
- {{ smokeCount }}
- 抽烟
-
-
- {{ resistedCount }}
- 忍住
-
-
-
-
-
- 筛选记录
-
+
+
{
return logs.filter(log => log.type === currentTab.value)
})
-const smokeCount = computed(() => filteredLogs.value.filter(log => log.type === 'smoke').length)
-const resistedCount = computed(() => filteredLogs.value.filter(log => log.type === 'resisted').length)
-
// 按日期分组
const groupedLogs = computed(() => {
return filteredLogs.value.reduce((groups, log) => {
@@ -320,120 +297,63 @@ onShareAppMessage(() => {
min-height: 100vh;
display: flex;
flex-direction: column;
- background: linear-gradient(180deg, #E6F7F2 0%, #F0FBF7 40%, #FAFFFE 100%);
- padding: 24rpx 28rpx 0;
+ background-color: #F5F7F6;
+ padding: 0 32rpx;
box-sizing: border-box;
}
-.hero-card,
-.section,
-.filters,
-.scroll-container,
-.fab {
+.filters-sticky {
position: relative;
- z-index: 1;
-}
-
-.card {
- background: rgba(255, 255, 255, 0.88);
- border-radius: 24rpx;
- border: 1.5rpx solid rgba(52, 200, 160, 0.14);
- box-shadow: 0 4rpx 18rpx rgba(52, 200, 160, 0.07);
- padding: 24rpx;
-}
-
-.hero-card {
- margin-bottom: 20rpx;
-}
-
-.hero-title {
- display: block;
- font-size: 38rpx;
- line-height: 1.2;
- font-weight: 700;
- color: #0D3D2E;
-}
-
-.hero-subtitle {
- display: block;
- margin-top: 10rpx;
- font-size: 25rpx;
- line-height: 1.5;
- color: #52806E;
-}
-
-.hero-stats {
- display: flex;
- gap: 16rpx;
- margin-top: 24rpx;
-}
-
-.hero-stat {
- flex: 1;
- background: rgba(52, 200, 160, 0.06);
- border: 1.5rpx solid rgba(52, 200, 160, 0.1);
- border-radius: 20rpx;
- padding: 18rpx 14rpx;
-}
-
-.hero-stat-value {
- display: block;
- font-size: 40rpx;
- font-weight: 700;
- color: #0D3D2E;
-}
-
-.hero-stat-label {
- display: block;
- margin-top: 8rpx;
- font-size: 22rpx;
- color: #7aA898;
-}
-
-.section {
- margin-bottom: 20rpx;
-}
-
-.section-label {
- display: block;
- margin: 0 0 14rpx 6rpx;
- font-size: 26rpx;
- font-weight: 600;
- color: #1a5c45;
+ height: 120rpx;
+ flex-shrink: 0;
+ z-index: 20;
}
.filters {
- padding: 24rpx;
+ position: fixed;
+ left: 32rpx;
+ right: 32rpx;
+ z-index: 50;
+ margin: 12rpx 0 0;
}
.tabs {
display: flex;
- background: rgba(255, 255, 255, 0.82);
- border-radius: 22rpx;
+ background: #FFFFFF;
+ border-radius: 24rpx;
padding: 6rpx;
- border: 1.5rpx solid rgba(52, 200, 160, 0.14);
- box-shadow: 0 4rpx 16rpx rgba(52, 200, 160, 0.07);
+ box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
}
.tab {
flex: 1;
text-align: center;
- padding: 14rpx 0;
- border-radius: 16rpx;
- font-size: 24rpx;
+ padding: 16rpx 0;
+ border-radius: 20rpx;
+ font-size: 26rpx;
font-weight: 600;
- color: #7aA898;
+ color: #999999;
}
.tab-active {
- background: #FFFFFF;
- color: #0D3D2E;
- box-shadow: 0 4rpx 12rpx rgba(52, 200, 160, 0.12);
+ background: #10B981;
+ color: #FFFFFF;
+}
+
+.section-label {
+ display: block;
+ margin: 0 0 18rpx 6rpx;
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #666666;
}
.scroll-container {
flex: 1;
min-height: 0;
+ position: relative;
+ z-index: 0;
+ padding-top: 0;
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
@@ -452,7 +372,7 @@ onShareAppMessage(() => {
width: 64rpx;
height: 64rpx;
border-radius: 18rpx;
- background: linear-gradient(90deg, rgba(52, 200, 160, 0.08) 25%, rgba(52, 200, 160, 0.18) 50%, rgba(52, 200, 160, 0.08) 75%);
+ background: linear-gradient(90deg, #EEEEEE 25%, #F5F5F5 50%, #EEEEEE 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
flex-shrink: 0;
@@ -460,16 +380,15 @@ onShareAppMessage(() => {
.skeleton-card {
flex: 1;
- background: rgba(255, 255, 255, 0.88);
+ background: #FFFFFF;
border-radius: 24rpx;
padding: 24rpx;
- border: 1.5rpx solid rgba(52, 200, 160, 0.14);
- box-shadow: 0 4rpx 18rpx rgba(52, 200, 160, 0.07);
+ box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
}
.skeleton-line {
height: 24rpx;
- background: linear-gradient(90deg, rgba(52, 200, 160, 0.08) 25%, rgba(52, 200, 160, 0.18) 50%, rgba(52, 200, 160, 0.08) 75%);
+ background: linear-gradient(90deg, #EEEEEE 25%, #F5F5F5 50%, #EEEEEE 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;
@@ -508,42 +427,40 @@ onShareAppMessage(() => {
.group-title {
font-size: 28rpx;
font-weight: 700;
- color: #0D3D2E;
+ color: #1A1A1A;
}
.group-count {
font-size: 22rpx;
- color: #7aA898;
- background-color: rgba(52, 200, 160, 0.06);
+ color: #999999;
+ background-color: #F0F0F0;
padding: 6rpx 16rpx;
border-radius: 999rpx;
- border: 1.5rpx solid rgba(52, 200, 160, 0.14);
}
.group-items {
display: flex;
flex-direction: column;
- gap: 20rpx;
+ gap: 16rpx;
}
.log-card {
position: relative;
- background: rgba(255, 255, 255, 0.88);
+ background: #FFFFFF;
border-radius: 24rpx;
padding: 24rpx 24rpx 20rpx 24rpx;
- box-shadow: 0 4rpx 18rpx rgba(52, 200, 160, 0.07);
+ box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
display: flex;
gap: 20rpx;
overflow: hidden;
- border: 1.5rpx solid rgba(52, 200, 160, 0.14);
}
.log-card-resisted {
- background: linear-gradient(135deg, rgba(245, 255, 251, 0.96), rgba(255, 255, 255, 0.88));
+ background: #FFFFFF;
}
.log-card-smoke {
- background: linear-gradient(135deg, rgba(255, 250, 244, 0.96), rgba(255, 255, 255, 0.88));
+ background: #FFFFFF;
}
.log-bar {
@@ -552,11 +469,11 @@ onShareAppMessage(() => {
top: 0;
bottom: 0;
width: 8rpx;
- background-color: #34C8A0;
+ background-color: #10B981;
}
.log-card-smoke .log-bar {
- background-color: #B45309;
+ background-color: #F59E0B;
}
.log-icon {
@@ -572,13 +489,13 @@ onShareAppMessage(() => {
}
.icon-resisted {
- background-color: rgba(52, 200, 160, 0.12);
- color: #1a8c62;
+ background-color: #E8F5F0;
+ color: #10B981;
}
.icon-smoke {
- background-color: rgba(251, 191, 36, 0.14);
- color: #B45309;
+ background-color: #FEF3C7;
+ color: #D97706;
}
.log-main {
@@ -603,7 +520,7 @@ onShareAppMessage(() => {
.log-time {
font-size: 30rpx;
font-weight: 700;
- color: #0D3D2E;
+ color: #1A1A1A;
}
.log-tag {
@@ -614,13 +531,13 @@ onShareAppMessage(() => {
}
.tag-smoke {
- background-color: rgba(251, 191, 36, 0.14);
- color: #B45309;
+ background-color: #FEF3C7;
+ color: #D97706;
}
.tag-resisted {
- background-color: rgba(52, 200, 160, 0.12);
- color: #1a8c62;
+ background-color: #E8F5F0;
+ color: #10B981;
}
.log-right {
@@ -632,8 +549,8 @@ onShareAppMessage(() => {
.count-pill {
font-size: 22rpx;
- color: #B45309;
- background-color: rgba(251, 191, 36, 0.14);
+ color: #D97706;
+ background-color: #FEF3C7;
padding: 8rpx 16rpx;
border-radius: 999rpx;
font-weight: 600;
@@ -641,15 +558,15 @@ onShareAppMessage(() => {
.thumb-pill {
font-size: 24rpx;
- background-color: rgba(52, 200, 160, 0.12);
- color: #1a8c62;
+ background-color: #E8F5F0;
+ color: #10B981;
padding: 8rpx 14rpx;
border-radius: 999rpx;
}
.log-desc {
font-size: 25rpx;
- color: #52806E;
+ color: #666666;
line-height: 1.5;
margin-bottom: 10rpx;
}
@@ -669,11 +586,10 @@ onShareAppMessage(() => {
.log-interval {
font-size: 22rpx;
- color: #7aA898;
- background-color: rgba(52, 200, 160, 0.06);
+ color: #999999;
+ background-color: #F5F5F5;
padding: 6rpx 14rpx;
border-radius: 999rpx;
- border: 1.5rpx solid rgba(52, 200, 160, 0.12);
}
.log-actions {
@@ -687,29 +603,24 @@ onShareAppMessage(() => {
font-size: 22rpx;
padding: 8rpx 16rpx;
border-radius: 999rpx;
- background-color: rgba(255, 255, 255, 0.9);
- border: 1.5rpx solid rgba(52, 200, 160, 0.18);
- color: #7aA898;
}
.edit-btn {
color: #2563EB;
- border-color: rgba(37, 99, 235, 0.18);
background-color: rgba(37, 99, 235, 0.08);
}
.delete-btn {
color: #DC2626;
- border-color: rgba(220, 38, 38, 0.16);
- background-color: rgba(220, 38, 38, 0.08);
+ background-color: rgba(220, 38, 38, 0.06);
}
.level-unknown {
- color: #7aA898;
+ color: #999999;
}
.level-1 {
- color: #1a8c62;
+ color: #10B981;
}
.level-2 {
@@ -734,36 +645,35 @@ onShareAppMessage(() => {
align-items: center;
justify-content: center;
padding: 120rpx 32rpx;
- border-radius: 24rpx;
- border: 2rpx dashed rgba(52, 200, 160, 0.2);
- background: transparent;
+ border-radius: 32rpx;
+ background: #FFFFFF;
+ box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
}
.empty-icon {
width: 112rpx;
height: 112rpx;
border-radius: 36rpx;
- background: rgba(52, 200, 160, 0.06);
- border: 1.5rpx solid rgba(52, 200, 160, 0.14);
+ background: #E8F5F0;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
font-weight: 700;
- color: #7aA898;
+ color: #10B981;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 32rpx;
- color: #0D3D2E;
+ color: #1A1A1A;
font-weight: 600;
margin-bottom: 12rpx;
}
.empty-hint {
font-size: 24rpx;
- color: #7aA898;
+ color: #999999;
}
.loading-more, .no-more {
@@ -773,7 +683,7 @@ onShareAppMessage(() => {
.loading-text, .no-more-text {
font-size: 24rpx;
- color: #7aA898;
+ color: #999999;
}
.fab {
@@ -782,12 +692,12 @@ onShareAppMessage(() => {
bottom: 140rpx;
width: 96rpx;
height: 96rpx;
- background: linear-gradient(180deg, #3DD9AE 0%, #34C8A0 100%);
+ background: #10B981;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
- box-shadow: 0 12rpx 28rpx rgba(52, 200, 160, 0.28);
+ box-shadow: 0 8rpx 24rpx rgba(16, 185, 129, 0.25);
transition: all 0.3s;
z-index: 100;
}
diff --git a/src/pages/nsti/index.vue b/src/pages/nsti/index.vue
new file mode 100644
index 0000000..49c43d9
--- /dev/null
+++ b/src/pages/nsti/index.vue
@@ -0,0 +1,417 @@
+
+
+
+
+
+ 赛博尼古丁测试
+ 10 道题,测出你的抽象戒烟人格
+
+ 不是说教,也不是审判。我们用一点抽象和一点幽默,把戒烟这件事先聊轻一点。
+
+
+
+
+
+
+
+
+
+ 你最近一次测出
+ {{ latestTime }}
+
+
+
+ {{ latestResult.emoji }}
+
+
+ {{ latestResult.name }}
+ “{{ latestResult.catchphrase }}”
+ {{ latestResult.difficultyText }}
+
+
+
+
+
+
+ 你会看到什么
+
+
+ 01
+
+ 抽象人格结果
+ 16 种戒烟人格画像,结果页可直接分享。
+
+
+
+ 02
+
+ 个性化建议
+ 每种人格都给你一套更贴合状态的起步动作。
+
+
+
+ 03
+
+ 宽容失败的语气
+ 不把复吸当羞耻,而是把它看成下一次优化的线索。
+
+
+
+
+
+
+
+ 部分人格预览
+ 完整测试共 16 型
+
+
+
+ {{ item.emoji }}
+ {{ item.name }}
+ “{{ item.catchphrase }}”
+
+
+
+
+
+ 说明
+
+ 本测试偏娱乐和行为洞察,不构成医疗建议。真正想开始戒烟时,我们也会把你带回到实际可执行的打卡和计划里。
+
+
+
+
+
+
+
+
diff --git a/src/pages/nsti/result.vue b/src/pages/nsti/result.vue
new file mode 100644
index 0000000..6af1a08
--- /dev/null
+++ b/src/pages/nsti/result.vue
@@ -0,0 +1,768 @@
+
+
+
+
+
+
+
+ 赛博尼古丁测试结果
+ {{ result.emoji }} {{ result.name }}
+ “{{ result.catchphrase }}”
+
+
+ 戒烟难度
+ {{ difficultyStars }}
+
+
+ 同类人群
+ {{ result.peerCount }} 人
+
+
+
+
+
+ 人格画像
+ {{ result.description }}
+
+ {{ tag }}
+
+
+
+
+
+ 你的戒烟驱动
+ {{ completedTime }}
+
+
+
+
+ {{ item.key }} · {{ item.label }}
+ {{ item.percentage }}%
+
+
+
+
+ {{ item.description }}
+
+
+
+
+
+ 专属建议
+
+
+ {{ index + 1 }}
+ {{ item }}
+
+
+
+
+
+ 现在就能开始的 3 步
+
+
+
+ {{ item }}
+
+
+
+
+
+ 和你最像的另外两型
+
+
+
+
+
+
+ {{ item.emoji }} {{ item.name }}
+ {{ item.catchphrase }}
+
+
+ {{ item.percentage }}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 本测试仅供娱乐与行为洞察,不构成医疗建议。如需专业戒烟支持,请咨询医生。
+
+
+
+
+
+
diff --git a/src/pages/nsti/test.vue b/src/pages/nsti/test.vue
new file mode 100644
index 0000000..bd5d698
--- /dev/null
+++ b/src/pages/nsti/test.vue
@@ -0,0 +1,367 @@
+
+
+
+
+ 赛博尼古丁测试
+ 已完成 {{ answeredCount }}/{{ questions.length }}
+
+
+
+
+
+
+
+
+ 第 {{ currentIndex + 1 }} / {{ questions.length }} 题
+ {{ currentQuestion.subtitle }}
+
+ {{ currentQuestion.title }}
+
+
+
+ {{ option.key }}
+
+ {{ option.text }}
+
+ {{ selectedIndex === index ? '已选' : '选择' }}
+
+
+
+
+
+ 作答建议
+ 按第一直觉选就好,不用追求“正确”。这个测试更想看你平时最真实的惯性。
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/onboarding/index.vue b/src/pages/onboarding/index.vue
index 7f3c78b..40a5cbf 100644
--- a/src/pages/onboarding/index.vue
+++ b/src/pages/onboarding/index.vue
@@ -17,13 +17,20 @@
:class="{ 'mode-switch-item-active': formData.mode === item.value }"
@tap="selectMode(item.value)"
>
- {{ item.icon }}
{{ item.label }}
{{ item.desc }}
+
+
+ 戒烟开始日期
+
+ {{ formData.quit_date || '请选择日期' }}
+
+
+
@@ -98,14 +105,41 @@
type="digit"
v-model="priceYuan"
class="inline-field"
- placeholder="25"
- placeholder-style="color: #9CC5B5"
+ placeholder="25"
+ placeholder-style="color: #CCCCCC"
/>
元/包
+
+
+ 成就称号风格
+ 打卡越久,称号越高
+
+
+
+
+ {{ level.name }} →
+
+
+
+
+
@@ -123,6 +157,7 @@ import { onShareAppMessage } from '@dcloudio/uni-app'
import { useProfileStore } from '@/stores/profile'
import { useUserStore } from '@/stores/user'
import { useLogin } from '@/hooks/useLogin'
+import * as api from '@/api'
const profileStore = useProfileStore()
const userStore = useUserStore()
@@ -130,12 +165,18 @@ const { waitForLogin } = useLogin()
const navBarHeight = ref(0)
const modeSaving = ref(false)
+const achievementThemes = ref([])
const modeOptions = [
- { value: 'quit', label: '戒烟打卡', desc: '按天打卡,坚持戒烟', icon: '🌿' },
- { value: 'record', label: '记录抽烟', desc: '按支数记录变化趋势', icon: '📊' }
+ { value: 'quit', label: '戒烟打卡', desc: '按天打卡,坚持戒烟' },
+ { value: 'record', label: '记录抽烟', desc: '按支数记录变化趋势' }
]
+function todayStr() {
+ const d = new Date()
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
+}
+
const formData = ref({
mode: 'record',
baseline_cigs_per_day: 10,
@@ -144,7 +185,9 @@ const formData = ref({
smoke_motivations: [],
wake_up_time: '07:30',
sleep_time: '23:00',
- pack_price_cent: 2500
+ pack_price_cent: 2500,
+ quit_date: todayStr(),
+ achievement_theme_id: null
})
const priceYuan = ref('25')
@@ -201,6 +244,10 @@ async function selectMode(mode) {
}
}
+function onQuitDateChange(e) {
+ formData.value.quit_date = e.detail.value
+}
+
function onWakeTimeChange(e) {
formData.value.wake_up_time = e.detail.value
}
@@ -238,6 +285,17 @@ onMounted(async () => {
}
await waitForLogin()
+
+ try {
+ const res = await api.getAchievementThemes()
+ achievementThemes.value = res.data?.themes || []
+ if (achievementThemes.value.length > 0 && !formData.value.achievement_theme_id) {
+ formData.value.achievement_theme_id = achievementThemes.value[0].id
+ }
+ } catch (e) {
+ console.error('loadAchievementThemes error:', e)
+ }
+
try {
const profileData = await profileStore.fetchProfile()
if (profileData?.profile) {
@@ -253,7 +311,9 @@ onMounted(async () => {
smoke_motivations: Array.isArray(profile.smoke_motivations) ? profile.smoke_motivations : formData.value.smoke_motivations,
wake_up_time: profile.wake_up_time || formData.value.wake_up_time,
sleep_time: profile.sleep_time || formData.value.sleep_time,
- pack_price_cent: profile.pack_price_cent || formData.value.pack_price_cent
+ pack_price_cent: profile.pack_price_cent || formData.value.pack_price_cent,
+ quit_date: profile.quit_date ? profile.quit_date.split('T')[0] : formData.value.quit_date,
+ achievement_theme_id: profile.achievement_theme_id || formData.value.achievement_theme_id
}
if (profile.pack_price_cent) {
priceYuan.value = String((profile.pack_price_cent / 100).toFixed(2)).replace(/\.00$/, '')
@@ -275,56 +335,58 @@ onShareAppMessage(() => {