feat(logs): add quick record flow and reason tags
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
- ✅ 从底部弹出动画效果
|
||||
- ✅ 半屏展示,优化用户体验
|
||||
- ✅ 支持两种模式:抽烟记录 / 忍住记录
|
||||
- ✅ 快捷标签、多选原因与补充备注
|
||||
- ✅ `quickMode` 快速记录模式
|
||||
- ✅ 完整的表单功能
|
||||
- ✅ 已配置 easycom 自动导入
|
||||
|
||||
@@ -58,6 +60,8 @@ function handleSubmit(data) {
|
||||
|------|------|--------|------|
|
||||
| show | Boolean | false | 控制弹框显示/隐藏(支持 v-model) |
|
||||
| type | String | 'smoke' | 记录类型:'smoke'(抽烟) 或 'resisted'(忍住) |
|
||||
| initialData | Object | null | 编辑模式下的初始值 |
|
||||
| quickMode | Boolean | false | 是否启用快速记录模式,默认隐藏高级项 |
|
||||
|
||||
## 🎪 Events
|
||||
|
||||
@@ -72,6 +76,7 @@ function handleSubmit(data) {
|
||||
{
|
||||
smoke_time: "2025-01-25", // 日期
|
||||
smoke_at: "2025-01-25 14:30:00", // 完整时间
|
||||
reason_tags: ["stress"], // 原因标签(可选)
|
||||
remark: "压力大", // 备注(可选)
|
||||
level: 2, // 烟瘾等级 1-5
|
||||
num: 3 // 数量(忍住时为0)
|
||||
@@ -90,6 +95,7 @@ function handleSubmit(data) {
|
||||
<smoke-record-dialog
|
||||
v-model:show="showDialog"
|
||||
type="smoke"
|
||||
:quick-mode="true"
|
||||
@submit="onSmokeSubmit"
|
||||
/>
|
||||
</view>
|
||||
@@ -179,3 +185,4 @@ async function onResistedSubmit(data) {
|
||||
3. 已配置 easycom,无需手动导入
|
||||
4. 提交后弹框会自动关闭
|
||||
5. 表单数据会在打开时自动初始化为当前时间
|
||||
6. 当后端尚未消费 `reason_tags` 时,组件会把已选标签合并进 `remark`,避免信息丢失
|
||||
|
||||
@@ -3,84 +3,130 @@
|
||||
<view class="dialog-container" :class="{ 'dialog-show': showAnimation }" @tap.stop>
|
||||
<view class="dialog-handle"></view>
|
||||
<view class="dialog-header">
|
||||
<text class="dialog-title">{{ title }}</text>
|
||||
<view>
|
||||
<text class="dialog-title">{{ title }}</text>
|
||||
<text v-if="quickModeActive" class="dialog-subtitle">{{ quickModeSummary }}</text>
|
||||
</view>
|
||||
<view class="dialog-close" @tap="close">×</view>
|
||||
</view>
|
||||
|
||||
<view class="dialog-body">
|
||||
<view class="form-row">
|
||||
<picker class="picker-card" mode="date" :value="formData.smoke_time" @change="onDateChange">
|
||||
<view class="input-card">
|
||||
<text class="input-label">日期</text>
|
||||
<view class="input-value-row">
|
||||
<view class="input-icon input-icon-date"></view>
|
||||
<text class="input-value">{{ formData.smoke_time || '选择日期' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
<picker class="picker-card" mode="time" :value="formData.smoke_time_only" @change="onTimeChange">
|
||||
<view class="input-card">
|
||||
<text class="input-label">时间</text>
|
||||
<view class="input-value-row">
|
||||
<view class="input-icon input-icon-time"></view>
|
||||
<text class="input-value">{{ formData.smoke_time_only || '选择时间' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="section-card section-card-counter" v-if="type === 'smoke'">
|
||||
<view class="section-left">
|
||||
<view class="section-icon"></view>
|
||||
<text class="section-title">抽烟数量</text>
|
||||
<view v-if="quickModeActive" class="quick-banner">
|
||||
<view class="quick-banner-chip">
|
||||
<text class="quick-banner-chip-label">默认时间</text>
|
||||
<text class="quick-banner-chip-value">{{ formData.smoke_time_only }}</text>
|
||||
</view>
|
||||
<view class="counter">
|
||||
<view class="counter-btn" @tap="decreaseNum">-</view>
|
||||
<text class="counter-value">{{ formData.num }}</text>
|
||||
<view class="counter-btn" @tap="increaseNum">+</view>
|
||||
<view class="quick-banner-chip" v-if="type === 'smoke'">
|
||||
<text class="quick-banner-chip-label">默认数量</text>
|
||||
<text class="quick-banner-chip-value">{{ formData.num }} 支</text>
|
||||
</view>
|
||||
<text class="quick-banner-tip">先选原因就能快速保存,需要时再展开高级项。</text>
|
||||
</view>
|
||||
|
||||
<view class="section-card">
|
||||
<view class="level-header">
|
||||
<text class="section-title">{{ type === 'smoke' ? '烟瘾程度' : '忍住强度' }}</text>
|
||||
<view class="level-badge">Level {{ formData.level }}</view>
|
||||
<view class="section-heading">
|
||||
<text class="section-title">{{ reasonSectionTitle }}</text>
|
||||
<text class="section-caption">可多选</text>
|
||||
</view>
|
||||
<slider
|
||||
class="level-slider"
|
||||
:value="formData.level"
|
||||
:min="1"
|
||||
:max="5"
|
||||
:step="1"
|
||||
activeColor="#22C55E"
|
||||
backgroundColor="#E5E7EB"
|
||||
block-color="#22C55E"
|
||||
:block-size="20"
|
||||
@change="onLevelChange"
|
||||
/>
|
||||
<view class="level-scale">
|
||||
<text class="level-scale-text">无感</text>
|
||||
<text class="level-scale-text">强烈</text>
|
||||
<view class="reason-chip-grid">
|
||||
<view
|
||||
v-for="item in quickReasonOptions"
|
||||
:key="item.key"
|
||||
class="reason-chip"
|
||||
:class="{ 'reason-chip-active': isReasonSelected(item.key) }"
|
||||
@tap="toggleReason(item.key)"
|
||||
>
|
||||
<text class="reason-chip-text">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="remark-section">
|
||||
<text class="section-title">备注(可选)</text>
|
||||
<view class="section-heading">
|
||||
<text class="section-title">{{ remarkTitle }}</text>
|
||||
<text class="section-caption">{{ remarkCaption }}</text>
|
||||
</view>
|
||||
<view class="remark-card">
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="formData.remark"
|
||||
:placeholder="type === 'smoke' ? '记录此时的心情或诱因,如压力大、应酬...' : '记录抵抗心得或诱因...'"
|
||||
:placeholder="remarkPlaceholder"
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="quickModeActive" class="advanced-toggle" @tap="showAdvanced = !showAdvanced">
|
||||
<view>
|
||||
<text class="advanced-toggle-title">{{ showAdvanced ? '收起高级设置' : '展开高级设置' }}</text>
|
||||
<text class="advanced-toggle-desc">修改时间、数量和强度</text>
|
||||
</view>
|
||||
<text class="advanced-toggle-arrow">{{ showAdvanced ? '⌃' : '⌄' }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="showAdvanced || !quickModeActive" class="advanced-fields">
|
||||
<view class="form-row">
|
||||
<picker class="picker-card" mode="date" :value="formData.smoke_time" @change="onDateChange">
|
||||
<view class="input-card">
|
||||
<text class="input-label">日期</text>
|
||||
<view class="input-value-row">
|
||||
<view class="input-icon input-icon-date"></view>
|
||||
<text class="input-value">{{ formData.smoke_time || '选择日期' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
<picker class="picker-card" mode="time" :value="formData.smoke_time_only" @change="onTimeChange">
|
||||
<view class="input-card">
|
||||
<text class="input-label">时间</text>
|
||||
<view class="input-value-row">
|
||||
<view class="input-icon input-icon-time"></view>
|
||||
<text class="input-value">{{ formData.smoke_time_only || '选择时间' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="section-card section-card-counter" v-if="type === 'smoke'">
|
||||
<view class="section-left">
|
||||
<view class="section-icon"></view>
|
||||
<text class="section-title">抽烟数量</text>
|
||||
</view>
|
||||
<view class="counter">
|
||||
<view class="counter-btn" @tap="decreaseNum">-</view>
|
||||
<text class="counter-value">{{ formData.num }}</text>
|
||||
<view class="counter-btn" @tap="increaseNum">+</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card">
|
||||
<view class="level-header">
|
||||
<text class="section-title">{{ type === 'smoke' ? '烟瘾程度' : '忍住强度' }}</text>
|
||||
<view class="level-badge">Level {{ formData.level }}</view>
|
||||
</view>
|
||||
<slider
|
||||
class="level-slider"
|
||||
:value="formData.level"
|
||||
:min="1"
|
||||
:max="5"
|
||||
:step="1"
|
||||
activeColor="#22C55E"
|
||||
backgroundColor="#E5E7EB"
|
||||
block-color="#22C55E"
|
||||
:block-size="20"
|
||||
@change="onLevelChange"
|
||||
/>
|
||||
<view class="level-scale">
|
||||
<text class="level-scale-text">无感</text>
|
||||
<text class="level-scale-text">强烈</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="dialog-footer">
|
||||
<view class="dialog-btn-primary" @tap="submit">
|
||||
<view class="btn-icon"></view>
|
||||
<text class="btn-text">保存记录</text>
|
||||
<text class="btn-text">{{ quickModeActive ? '快速保存' : '保存记录' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -88,6 +134,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getReasonOptions, normalizeReasonTags, getReasonLabels } from '@/config/smoke-reasons'
|
||||
|
||||
export default {
|
||||
name: 'SmokeRecordDialog',
|
||||
props: {
|
||||
@@ -102,16 +150,22 @@ export default {
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
quickMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAnimation: false,
|
||||
showAdvanced: false,
|
||||
formData: {
|
||||
smoke_time: '',
|
||||
smoke_time_only: '',
|
||||
smoke_at: '',
|
||||
remark: '',
|
||||
reason_tags: [],
|
||||
level: 2,
|
||||
num: 1
|
||||
}
|
||||
@@ -119,7 +173,40 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return '添加记录'
|
||||
if (this.type === 'resisted') {
|
||||
return this.quickModeActive ? '记下这次忍住' : '添加忍住记录'
|
||||
}
|
||||
return this.quickModeActive ? '快速记录一根' : '添加抽烟记录'
|
||||
},
|
||||
quickModeActive() {
|
||||
return this.quickMode && !this.initialData
|
||||
},
|
||||
quickModeSummary() {
|
||||
return this.type === 'smoke'
|
||||
? '默认按当前时间和 1 支记录,可直接选原因后保存'
|
||||
: '默认按当前时间记录,可直接选原因后保存'
|
||||
},
|
||||
quickReasonOptions() {
|
||||
return getReasonOptions(this.type)
|
||||
},
|
||||
reasonSectionTitle() {
|
||||
return this.type === 'smoke' ? '这次为什么会抽?' : '这次是怎么扛住的?'
|
||||
},
|
||||
remarkTitle() {
|
||||
return this.formData.reason_tags.includes('other') ? '补充说明' : '补充备注'
|
||||
},
|
||||
remarkCaption() {
|
||||
return this.formData.reason_tags.includes('other') ? '已选其他,建议补充一点细节' : '可选'
|
||||
},
|
||||
remarkPlaceholder() {
|
||||
if (this.type === 'smoke') {
|
||||
return this.formData.reason_tags.includes('other')
|
||||
? '写下这次抽烟的具体原因...'
|
||||
: '还想补充当时的场景或心情,可以写在这里'
|
||||
}
|
||||
return this.formData.reason_tags.includes('other')
|
||||
? '写下这次忍住的具体方法或感受...'
|
||||
: '可以补充这次是怎么撑过去的'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -136,35 +223,41 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
initFormData() {
|
||||
// 如果有初始数据(编辑模式),使用初始数据
|
||||
this.showAdvanced = !this.quickModeActive
|
||||
if (this.initialData) {
|
||||
this.formData = {
|
||||
smoke_time: this.initialData.smoke_time || '',
|
||||
smoke_time_only: this.initialData.smoke_time_only || '',
|
||||
smoke_at: this.initialData.smoke_at || '',
|
||||
remark: this.initialData.remark || '',
|
||||
reason_tags: normalizeReasonTags(this.initialData.reason_tags),
|
||||
level: this.initialData.level ?? 2,
|
||||
num: this.initialData.num ?? 1
|
||||
}
|
||||
} else {
|
||||
// 新建模式,使用当前本地时间(不用 toISOString,避免 UTC 导致日期差一天)
|
||||
const now = new Date()
|
||||
const y = now.getFullYear()
|
||||
const m = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(now.getDate()).padStart(2, '0')
|
||||
const dateStr = `${y}-${m}-${d}`
|
||||
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||
const datetimeStr = `${dateStr} ${timeStr}:00`
|
||||
|
||||
this.formData = {
|
||||
smoke_time: dateStr,
|
||||
smoke_time_only: timeStr,
|
||||
smoke_at: datetimeStr,
|
||||
remark: '',
|
||||
level: 2,
|
||||
num: this.type === 'smoke' ? 1 : 0
|
||||
num: this.resolveInitialNum(this.initialData)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const y = now.getFullYear()
|
||||
const m = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(now.getDate()).padStart(2, '0')
|
||||
const dateStr = `${y}-${m}-${d}`
|
||||
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||
const datetimeStr = `${dateStr} ${timeStr}:00`
|
||||
|
||||
this.formData = {
|
||||
smoke_time: dateStr,
|
||||
smoke_time_only: timeStr,
|
||||
smoke_at: datetimeStr,
|
||||
remark: '',
|
||||
reason_tags: [],
|
||||
level: 2,
|
||||
num: this.type === 'smoke' ? 1 : 0
|
||||
}
|
||||
},
|
||||
resolveInitialNum(initialData) {
|
||||
if (this.type !== 'smoke') return 0
|
||||
return initialData.num ?? 1
|
||||
},
|
||||
handleMaskClick() {
|
||||
this.close()
|
||||
@@ -197,6 +290,17 @@ export default {
|
||||
onLevelChange(e) {
|
||||
this.formData.level = e.detail.value
|
||||
},
|
||||
isReasonSelected(reasonKey) {
|
||||
return this.formData.reason_tags.includes(reasonKey)
|
||||
},
|
||||
toggleReason(reasonKey) {
|
||||
const exists = this.isReasonSelected(reasonKey)
|
||||
if (exists) {
|
||||
this.formData.reason_tags = this.formData.reason_tags.filter(item => item !== reasonKey)
|
||||
return
|
||||
}
|
||||
this.formData.reason_tags = [...this.formData.reason_tags, reasonKey]
|
||||
},
|
||||
isTimeValid() {
|
||||
const dateStr = this.formData.smoke_time
|
||||
const timeStr = this.formData.smoke_time_only
|
||||
@@ -221,6 +325,17 @@ export default {
|
||||
|
||||
return true
|
||||
},
|
||||
buildRemark() {
|
||||
const customRemark = (this.formData.remark || '').trim()
|
||||
const reasonLabels = getReasonLabels(this.formData.reason_tags, this.type).filter(label => label !== '其他')
|
||||
if (!reasonLabels.length) {
|
||||
return customRemark
|
||||
}
|
||||
if (!customRemark) {
|
||||
return reasonLabels.join('、')
|
||||
}
|
||||
return `${reasonLabels.join('、')};${customRemark}`
|
||||
},
|
||||
submit() {
|
||||
if (!this.isTimeValid()) {
|
||||
return
|
||||
@@ -229,7 +344,8 @@ export default {
|
||||
const submitData = {
|
||||
smoke_time: this.formData.smoke_time,
|
||||
smoke_at: this.formData.smoke_at,
|
||||
remark: this.formData.remark,
|
||||
remark: this.buildRemark(),
|
||||
reason_tags: [...this.formData.reason_tags],
|
||||
level: this.formData.level,
|
||||
num: this.type === 'smoke' ? this.formData.num : 0
|
||||
}
|
||||
@@ -283,17 +399,27 @@ export default {
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx 16rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.dialog-subtitle {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
@@ -301,10 +427,11 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 44rpx;
|
||||
color: #98A2B3;
|
||||
color: #98a2b3;
|
||||
line-height: 1;
|
||||
background-color: rgba(255, 255, 255, 0.78);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
@@ -314,6 +441,151 @@ export default {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.quick-banner {
|
||||
padding: 18rpx 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, rgba(232, 249, 241, 0.96) 0%, rgba(246, 251, 249, 0.96) 100%);
|
||||
border: 2rpx solid rgba(49, 193, 139, 0.14);
|
||||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.quick-banner-chip {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
padding: 10rpx 14rpx;
|
||||
margin-right: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.quick-banner-chip-label {
|
||||
font-size: 20rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.quick-banner-chip-value {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.quick-banner-tip {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background-color: rgba(255, 255, 255, 0.84);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 10rpx 24rpx rgba(15, 23, 42, 0.05);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.section-caption {
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.reason-chip-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.reason-chip {
|
||||
padding: 16rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(247, 249, 252, 0.92);
|
||||
border: 2rpx solid rgba(226, 232, 240, 0.9);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.reason-chip-active {
|
||||
background: linear-gradient(180deg, rgba(223, 250, 234, 0.96) 0%, rgba(209, 250, 229, 0.96) 100%);
|
||||
border-color: rgba(26, 163, 122, 0.22);
|
||||
box-shadow: 0 10rpx 18rpx rgba(26, 163, 122, 0.12);
|
||||
}
|
||||
|
||||
.reason-chip-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.remark-section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.remark-card {
|
||||
margin-top: 16rpx;
|
||||
background-color: rgba(255, 255, 255, 0.78);
|
||||
border-radius: 20rpx;
|
||||
padding: 8rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 180rpx;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
border: 2rpx solid transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.advanced-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 22rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 22rpx;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 8rpx 18rpx rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.advanced-toggle-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.advanced-toggle-desc {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.advanced-toggle-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #14936d;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
@@ -337,7 +609,7 @@ export default {
|
||||
|
||||
.input-label {
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -352,7 +624,7 @@ export default {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 8rpx;
|
||||
background-color: #DCFCE7;
|
||||
background-color: #dcfce7;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -363,7 +635,7 @@ export default {
|
||||
top: 8rpx;
|
||||
width: 20rpx;
|
||||
height: 16rpx;
|
||||
border: 2rpx solid #22C55E;
|
||||
border: 2rpx solid #22c55e;
|
||||
border-top-width: 6rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
@@ -375,7 +647,7 @@ export default {
|
||||
top: 8rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border: 2rpx solid #22C55E;
|
||||
border: 2rpx solid #22c55e;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@@ -386,7 +658,7 @@ export default {
|
||||
top: 12rpx;
|
||||
width: 2rpx;
|
||||
height: 8rpx;
|
||||
background-color: #22C55E;
|
||||
background-color: #22c55e;
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
@@ -396,15 +668,6 @@ export default {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background-color: rgba(255, 255, 255, 0.84);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 10rpx 24rpx rgba(15, 23, 42, 0.05);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-card-counter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -425,7 +688,7 @@ export default {
|
||||
.section-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background-color: #DCFCE7;
|
||||
background-color: #dcfce7;
|
||||
border-radius: 16rpx;
|
||||
position: relative;
|
||||
}
|
||||
@@ -438,7 +701,7 @@ export default {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 6rpx;
|
||||
border: 3rpx solid #22C55E;
|
||||
border: 3rpx solid #22c55e;
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
@@ -456,7 +719,7 @@ export default {
|
||||
background-color: rgba(247, 249, 252, 0.92);
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
border: 2rpx solid #F1F5F9;
|
||||
border: 2rpx solid #f1f5f9;
|
||||
margin-top: 0;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
@@ -511,31 +774,7 @@ export default {
|
||||
|
||||
.level-scale-text {
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.remark-section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.remark-card {
|
||||
margin-top: 16rpx;
|
||||
background-color: rgba(255, 255, 255, 0.78);
|
||||
border-radius: 20rpx;
|
||||
padding: 8rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 180rpx;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
border: 2rpx solid transparent;
|
||||
box-sizing: border-box;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
@@ -570,14 +809,14 @@ export default {
|
||||
top: 18rpx;
|
||||
width: 12rpx;
|
||||
height: 6rpx;
|
||||
border-left: 4rpx solid #FFFFFF;
|
||||
border-bottom: 4rpx solid #FFFFFF;
|
||||
border-left: 4rpx solid #ffffff;
|
||||
border-bottom: 4rpx solid #ffffff;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
const SMOKE_REASON_OPTIONS = [
|
||||
{ key: 'stress', label: '压力大' },
|
||||
{ key: 'after_meal', label: '饭后习惯' },
|
||||
{ key: 'social', label: '社交应酬' },
|
||||
{ key: 'bored', label: '无聊' },
|
||||
{ key: 'low_mood', label: '情绪低落' },
|
||||
{ key: 'after_drink', label: '喝酒后' },
|
||||
{ key: 'triggered', label: '看到别人抽' },
|
||||
{ key: 'morning', label: '早起习惯' },
|
||||
{ key: 'other', label: '其他' }
|
||||
]
|
||||
|
||||
const RESISTED_REASON_OPTIONS = [
|
||||
{ key: 'distracted', label: '转移注意' },
|
||||
{ key: 'walked', label: '出去走走' },
|
||||
{ key: 'water', label: '喝水缓解' },
|
||||
{ key: 'deep_breath', label: '深呼吸' },
|
||||
{ key: 'snack', label: '吃点东西' },
|
||||
{ key: 'chat', label: '找人聊聊' },
|
||||
{ key: 'busy', label: '让自己忙起来' },
|
||||
{ key: 'willpower', label: '硬撑过去了' },
|
||||
{ key: 'other', label: '其他' }
|
||||
]
|
||||
|
||||
const REASON_MAP = {
|
||||
smoke: SMOKE_REASON_OPTIONS,
|
||||
resisted: RESISTED_REASON_OPTIONS
|
||||
}
|
||||
|
||||
export function getReasonOptions(type = 'smoke') {
|
||||
return REASON_MAP[type] || REASON_MAP.smoke
|
||||
}
|
||||
|
||||
export function normalizeReasonTags(value) {
|
||||
if (!value) return []
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter(item => typeof item === 'string' && item.trim())
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return []
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.filter(item => typeof item === 'string' && item.trim())
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parse failure and fallback to comma split
|
||||
}
|
||||
return trimmed
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function getReasonLabelMap(type = 'smoke') {
|
||||
return getReasonOptions(type).reduce((acc, item) => {
|
||||
acc[item.key] = item.label
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function getReasonLabels(tags, type = 'smoke') {
|
||||
const labelMap = getReasonLabelMap(type)
|
||||
return normalizeReasonTags(tags).map(tag => labelMap[tag] || tag)
|
||||
}
|
||||
@@ -189,6 +189,17 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="quit-slip-entry" :style="hpVisualStyle" @tap="openSmokeDialog">
|
||||
<view class="quit-slip-copy">
|
||||
<text class="quit-slip-title">如果刚刚没扛住,先记下来</text>
|
||||
<text class="quit-slip-desc">用快速记录留下时间和诱因,比“等会再说”更能帮你找回节奏。</text>
|
||||
</view>
|
||||
<view class="quit-slip-action">
|
||||
<text class="quit-slip-action-main">快速记录</text>
|
||||
<text class="quit-slip-action-sub">一根 / 当前时间</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="quit-dream-entry" @tap="gotoDreamGoals">
|
||||
<view class="quit-dream-left">
|
||||
<text class="quit-dream-icon">🎯</text>
|
||||
@@ -372,9 +383,9 @@
|
||||
</view>
|
||||
|
||||
<smoke-record-dialog
|
||||
v-if="!isQuitMode"
|
||||
v-model:show="showDialog"
|
||||
:type="dialogType"
|
||||
:quick-mode="true"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</view>
|
||||
@@ -918,20 +929,30 @@ async function handleSubmit(submitData) {
|
||||
try {
|
||||
if (dialogType.value === 'smoke') {
|
||||
await api.createLog(submitData)
|
||||
timerBaseSeconds.value = 0
|
||||
timerSeconds.value = 0
|
||||
startTimer()
|
||||
uni.showToast({ title: '记录成功', icon: 'success' })
|
||||
if (isQuitMode.value) {
|
||||
await fetchQuitHomeData()
|
||||
uni.showToast({ title: '已记下这次波动', icon: 'success' })
|
||||
} else {
|
||||
timerBaseSeconds.value = 0
|
||||
timerSeconds.value = 0
|
||||
startTimer()
|
||||
uni.showToast({ title: '记录成功', icon: 'success' })
|
||||
}
|
||||
} else {
|
||||
await api.createResistedLog({
|
||||
smoke_time: submitData.smoke_time,
|
||||
smoke_at: submitData.smoke_at,
|
||||
remark: submitData.remark,
|
||||
reason_tags: submitData.reason_tags,
|
||||
level: submitData.level,
|
||||
num: submitData.num
|
||||
})
|
||||
uni.showToast({ title: '已记下这次忍住', icon: 'success' })
|
||||
}
|
||||
if (isQuitMode.value) {
|
||||
await fetchAchievement()
|
||||
return
|
||||
}
|
||||
await fetchRecordHomeData()
|
||||
} catch (e) {
|
||||
console.error('handleSubmit error:', e)
|
||||
@@ -1567,6 +1588,60 @@ onShareAppMessage(() => ({
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.quit-slip-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, var(--hp-soft, #def7ec) 0%, rgba(255, 255, 255, 0.96) 100%);
|
||||
border: 1rpx solid rgba(15, 23, 42, 0.06);
|
||||
box-shadow: 0 10rpx 24rpx rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.quit-slip-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quit-slip-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.quit-slip-desc {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.quit-slip-action {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(180deg, var(--hp-accent, #14936d) 0%, var(--hp-deep, #0f766e) 100%);
|
||||
box-shadow: 0 14rpx 26rpx rgba(20, 147, 109, 0.2);
|
||||
}
|
||||
|
||||
.quit-slip-action-main {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.quit-slip-action-sub {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 19rpx;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.quit-milestone-list {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@@ -70,6 +70,14 @@
|
||||
class="log-desc"
|
||||
>{{ log.remark.trim() }}</text>
|
||||
|
||||
<view v-if="log.reasonLabels && log.reasonLabels.length > 0" class="reason-tag-row">
|
||||
<text
|
||||
v-for="label in log.reasonLabels"
|
||||
:key="`${log.id}-${label}`"
|
||||
class="reason-tag"
|
||||
>{{ label }}</text>
|
||||
</view>
|
||||
|
||||
<view class="log-meta-row">
|
||||
<text
|
||||
v-if="log.level !== undefined && log.level !== null"
|
||||
@@ -219,6 +227,7 @@ function handleEdit(log) {
|
||||
smoke_time_only: log.displayTime,
|
||||
smoke_at: log.smoke_at,
|
||||
remark: log.remark || '',
|
||||
reason_tags: log.reasonTags || log.reason_tags || [],
|
||||
level: log.level ?? 2,
|
||||
num: log.num ?? 1
|
||||
}
|
||||
@@ -576,6 +585,23 @@ onShareAppMessage(() => {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.reason-tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.reason-tag {
|
||||
padding: 8rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(223, 250, 234, 0.82);
|
||||
border: 1rpx solid rgba(26, 163, 122, 0.16);
|
||||
color: #0f766e;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import * as api from '@/api'
|
||||
import { normalizeReasonTags, getReasonLabels } from '@/config/smoke-reasons'
|
||||
|
||||
export const useLogsStore = defineStore('logs', {
|
||||
state: () => ({
|
||||
@@ -126,6 +127,8 @@ export const useLogsStore = defineStore('logs', {
|
||||
return {
|
||||
...log,
|
||||
type,
|
||||
reasonTags: normalizeReasonTags(log.reason_tags),
|
||||
reasonLabels: getReasonLabels(log.reason_tags, type),
|
||||
interval,
|
||||
displayTime: formatLogTime(log.smoke_at || log.smoke_time || log.createtime),
|
||||
displayDate
|
||||
|
||||
Reference in New Issue
Block a user