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 @@ + + + + + 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 }} 天 + + 已达最高等级 + + - 健康恢复 - 身体正在按自己的节奏修复 + 健康恢复里程碑 + + + {{ healthProgress >= 100 ? '已达成' : `${healthProgress}%` }} - {{ healthProgress }}% - - - - + - - {{ milestone.label }} + + {{ ms.name }} + {{ ms.percent }}% + + + + - - 今日提醒 - {{ healthTip }} + + + 🎯 + + 梦想清单 + {{ activeGoalText }} + + + + + + + 💡 + {{ healthTip }} @@ -212,6 +247,28 @@ 💡 {{ recordHealthTip }} + + + + {{ achievementData.theme_icon }} 成就称号 + {{ achievementData.theme_name }} + + + + {{ 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 @@