feat(calendar): 更新日历接口并优化记录流程

- 将GetRecords接口替换为GetCalendar接口获取日历数据
- 重构首页日历数据显示逻辑,支持年月维度的数据获取
- 优化打卡记录流程,合并选酒和记量步骤为单一添加酒水步骤
- 移除登录页面的游客模式选项,简化首次启动逻辑
- 调整引导页逻辑,移除登录跳转改为直接进入首页
- 优化记录页面UI布局,添加状态栏安全距离适配
- 新增酒水列表管理功能,支持添加和删除已选酒水
- 更新依赖包HaveADrink至v1.0.3版本,同步API变更
This commit is contained in:
cg
2026-07-17 23:51:43 +08:00
parent a73bd79eb5
commit adf6b684b9
13 changed files with 520 additions and 451 deletions
+243 -331
View File
@@ -1,7 +1,7 @@
<template>
<view :class="themeClass" class="page-container record-page safe-bottom">
<!-- 顶部进度 -->
<view class="step-header">
<view class="step-header" :style="{ paddingTop: (statusBarHeight + 12) + 'px' }">
<view class="step-back" @click="handleBack">
<text class="step-back-arrow"></text>
</view>
@@ -25,8 +25,8 @@
<text class="step-subtitle">{{ stepSubtitles[currentStep - 1] }}</text>
</view>
<!-- Step 1: 选酒 -->
<view v-if="currentStep === 1" class="step-content">
<!-- Step 1: 添加酒水选酒+记量合一 -->
<scroll-view v-if="currentStep === 1" class="step-content" scroll-y enhanced :show-scrollbar="false">
<!-- 酒类选择网格 -->
<view class="category-grid">
<view
@@ -41,8 +41,9 @@
</view>
</view>
<!-- 品牌选择 -->
<view v-if="currentDrink.category" class="brand-section">
<!-- 品牌 + 用量选酒后展示 -->
<view v-if="currentDrink.category" class="drink-form card">
<!-- 品牌选择 -->
<text class="section-label">品牌</text>
<view class="brand-tags">
<view
@@ -65,7 +66,7 @@
</view>
<!-- 产品名称 -->
<text class="section-label" style="margin-top: 24rpx;">产品名称</text>
<text class="section-label" style="margin-top: 16rpx;">产品名称</text>
<view class="input-row">
<input
class="input-field"
@@ -74,22 +75,31 @@
placeholder-class="input-placeholder"
/>
</view>
</view>
</view>
<!-- Step 2: 记量 -->
<view v-if="currentStep === 2" class="step-content">
<view class="amount-section card">
<view class="amount-row">
<input
class="amount-input text-num"
type="digit"
v-model="amountStr"
placeholder="0"
placeholder-class="input-placeholder"
@input="onAmountChange"
/>
<text class="amount-unit">{{ currentDrink.unit === 'ml' ? 'ml' : currentDrink.unit }}</text>
<view class="divider"></view>
<!-- 用量 + 度数 一行 -->
<view class="amount-degree-row">
<view class="amount-inline">
<input
class="amount-input text-num"
type="digit"
v-model="amountStr"
placeholder="0"
placeholder-class="input-placeholder"
@input="onAmountChange"
/>
<text class="amount-unit">{{ currentDrink.unit === 'ml' ? 'ml' : currentDrink.unit }}</text>
</view>
<view class="degree-inline">
<input
class="degree-input text-num"
type="digit"
v-model="degreeStr"
@input="onAmountChange"
/>
<text class="degree-symbol">°</text>
</view>
</view>
<!-- 单位切换 -->
@@ -105,60 +115,53 @@
</view>
</view>
<!-- 酒精度数 -->
<view class="degree-row">
<text class="section-label">酒精度数</text>
<view class="degree-input-wrap">
<input
class="degree-input text-num"
type="digit"
v-model="degreeStr"
@input="onAmountChange"
/>
<text class="degree-symbol">°</text>
</view>
</view>
<view class="divider"></view>
<!-- 标准杯显示 -->
<view class="standard-cup-result">
<view class="standard-cup-label-row">
<text class="standard-cup-label">本次饮酒量</text>
<view class="cup-info-btn" @click="showCupInfo = true">
<text class="cup-info-icon">?</text>
</view>
<view class="cup-row">
<text class="cup-label"></text>
<text class="cup-value text-num text-amber">{{ currentCups }} 标准杯</text>
<view class="cup-info-btn" @click="showCupInfo = true">
<text class="cup-info-icon">?</text>
</view>
<text class="standard-cup-value text-num text-amber">{{ currentCups }} 标准杯</text>
</view>
</view>
<!-- 已添加的酒水列表 -->
<view v-if="drinks.length > 0" class="added-drinks">
<text class="section-label">已添加的酒水</text>
<view class="added-drinks-header">
<text class="section-label">已添加的酒水</text>
<text class="added-count">{{ drinks.length }}</text>
</view>
<view v-for="(d, i) in drinks" :key="i" class="added-drink-item card">
<view class="flex-between">
<text>{{ getCatEmoji(d.category) }} {{ d.brand }} {{ d.product }}</text>
<text class="text-amber text-num">{{ d.standardCups }}</text>
<text>{{ getCatEmoji(d.category) }} {{ d.brand }} {{ d.product }} · {{ d.amount }}{{ d.unit }}</text>
<view class="added-drink-right">
<text class="text-amber text-num">{{ d.standardCups }}</text>
<view class="added-drink-remove" @click="removeDrink(i)">
<text class="remove-icon"></text>
</view>
</view>
</view>
</view>
<!-- 累计标准杯 -->
<view class="total-cups-bar">
<text class="text-caption">本次累计</text>
<text class="total-cups-value text-num text-amber">{{ totalCups }} 标准杯</text>
</view>
</view>
<!-- 累计标准杯 -->
<view class="total-cups-bar">
<text class="text-caption">本次累计</text>
<text class="total-cups-value text-num text-amber">{{ totalCups }} 标准杯</text>
</view>
<!-- 健康提醒 -->
<view v-if="totalCups > 3" class="health-warning card">
<view v-if="drinks.length > 0 && totalCups > 3" class="health-warning card">
<text class="health-warning-icon"></text>
<text class="health-warning-text">今日已饮用 {{ totalCups }} 标准杯建议每日不超过2标准杯</text>
</view>
<view v-else class="health-hint">
<text class="text-tiny">建议每日不超过2标准杯中国居民膳食指南</text>
<view v-else-if="drinks.length > 0" class="health-hint">
<text class="text-tiny">💡 可继续添加更多酒水或点击下一步</text>
</view>
</view>
<!-- 底部占位 -->
<view style="height: 180rpx;"></view>
</scroll-view>
<!-- 标准杯说明弹窗 -->
<view v-if="showCupInfo" class="cup-info-overlay" @click="showCupInfo = false">
@@ -198,8 +201,8 @@
</view>
</view>
<!-- Step 3: 配餐 -->
<view v-if="currentStep === 3" class="step-content">
<!-- Step 2: 配餐 -->
<view v-if="currentStep === 2" class="step-content">
<view class="food-grid">
<view
v-for="food in foodCategories"
@@ -224,8 +227,8 @@
</view>
</view>
<!-- Step 4: 感受 -->
<view v-if="currentStep === 4" class="step-content">
<!-- Step 3: 感受 -->
<view v-if="currentStep === 3" class="step-content">
<view class="feeling-grid">
<view
v-for="f in feelings"
@@ -244,8 +247,8 @@
</view>
</view>
<!-- Step 5: 照片 -->
<view v-if="currentStep === 5" class="step-content">
<!-- Step 4: 照片 -->
<view v-if="currentStep === 4" class="step-content">
<view class="photo-grid">
<view
v-for="(photo, index) in photos"
@@ -267,39 +270,6 @@
</view>
</view>
<!-- Step 6: 确认保存 -->
<view v-if="currentStep === 6" class="step-content">
<!-- 记录预览 -->
<view class="preview-section card">
<text class="section-label">记录预览</text>
<view class="preview-drinks">
<view v-for="(d, i) in allDrinks" :key="i" class="preview-item">
<text>{{ getCatEmoji(d.category) }} {{ d.brand }} {{ d.product }} · {{ d.amount }}{{ d.unit }}</text>
<text class="text-amber text-num">{{ d.standardCups }}</text>
</view>
</view>
<view class="divider"></view>
<view class="preview-meta">
<text v-if="selectedFood !== 'none'" class="preview-line">🍲 {{ getFoodName() }}</text>
<text v-if="selectedFeeling" class="preview-line">{{ getFeelingEmoji() }} {{ getFeelingName() }}</text>
<text class="preview-line">📍 {{ backfillDate || '今天' }}</text>
</view>
<view class="preview-total">
<text class="text-caption">总计</text>
<text class="text-amber text-num" style="font-size: 34rpx;">{{ totalCups }} 标准杯</text>
</view>
</view>
<!-- 保存后提示 -->
<view class="save-hint card">
<text class="save-hint-icon"></text>
<view class="save-hint-text">
<text class="save-hint-title">保存后可生成酒局卡片</text>
<text class="save-hint-desc">卡片可保存到相册发朋友圈或分享给好友</text>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<button
@@ -309,6 +279,14 @@
>上一步</button>
<view v-else class="btn-placeholder"></view>
<!-- Step 1 显示+ 添加按钮 -->
<button
v-if="currentStep === 1"
class="btn-add-drink"
:disabled="!canAddDrink"
@click="addDrinkToList"
>+ 添加</button>
<button
v-if="currentStep < totalSteps"
class="btn-primary"
@@ -322,11 +300,6 @@
style="flex: none; width: 55%;"
>保存并生成卡片</button>
</view>
<!-- 添加更多酒水按钮(Step 1) -->
<view v-if="currentStep === 1 && drinks.length > 0" class="add-more-bar">
<button class="btn-ghost" @click="resetCurrentDrink">+ 再添加一种酒</button>
</view>
</view>
</template>
@@ -343,11 +316,13 @@ import themeMixin from '../../common/theme-mixin'
export default {
mixins: [themeMixin],
data() {
const sysInfo = uni.getSystemInfoSync()
return {
statusBarHeight: sysInfo.statusBarHeight || 25,
currentStep: 1,
totalSteps: 6,
stepTitles: ['选择酒水', '记录用量', '配餐', '感受', '照片', '确认保存'],
stepSubtitles: ['喝了什么酒?', '喝了多少?', '配了什么菜?', '感觉如何?', '拍张照片吧', '保存后可生成分享卡片'],
totalSteps: 4,
stepTitles: ['添加酒水', '配餐', '感受', '照片'],
stepSubtitles: ['喝了什么酒?喝了多少?', '配了什么菜?', '感觉如何?', '拍张照片吧'],
drinkCategories: DRINK_CATEGORIES,
units: DRINK_UNITS,
@@ -359,22 +334,20 @@ export default {
currentDrink: { category: '', brand: '', product: '', amount: 0, unit: 'ml', degree: 0 },
drinks: [],
brandList: [],
// Step 2
amountStr: '',
degreeStr: '',
// Step 3
// Step 2
selectedFood: '',
foodName: '',
// Step 4
// Step 3
selectedFeeling: '',
// Step 5
// Step 4
photos: [],
// Step 6
// Step 5
visibility: 'friends',
// 补打卡日期
@@ -390,30 +363,17 @@ export default {
return calcStandardCups(ml, parseFloat(this.degreeStr) || 0)
},
totalCups() {
const addedCups = this.drinks.reduce((sum, d) => sum + d.standardCups, 0)
return Math.round((addedCups + this.currentCups) * 100) / 100
return this.drinks.reduce((sum, d) => sum + d.standardCups, 0)
},
allDrinks() {
const all = [...this.drinks]
if (this.amountStr && this.currentDrink.category) {
const ml = unitToMl(parseFloat(this.amountStr) || 0, this.currentDrink.unit)
all.push({
...this.currentDrink,
amount: parseFloat(this.amountStr) || 0,
degree: parseFloat(this.degreeStr) || 0,
standardCups: calcStandardCups(ml, parseFloat(this.degreeStr) || 0)
})
}
return all
canAddDrink() {
return this.currentDrink.category && this.currentDrink.brand && parseFloat(this.amountStr) > 0
},
canProceed() {
switch (this.currentStep) {
case 1: return this.currentDrink.category && this.currentDrink.brand
case 2: return parseFloat(this.amountStr) > 0
case 3: return true // 配餐可选
case 4: return true // 感受可选
case 5: return true // 照片可选
case 6: return true
case 1: return this.drinks.length > 0
case 2: return true // 配餐可选
case 3: return true // 感受可选
case 4: return true // 照片可选
default: return true
}
}
@@ -486,20 +446,8 @@ export default {
removePhoto(index) {
this.photos.splice(index, 1)
},
resetCurrentDrink() {
// 保存当前酒水到列表
if (this.amountStr && this.currentDrink.category) {
const ml = unitToMl(parseFloat(this.amountStr) || 0, this.currentDrink.unit)
this.drinks.push({
...this.currentDrink,
amount: parseFloat(this.amountStr) || 0,
degree: parseFloat(this.degreeStr) || 0,
standardCups: calcStandardCups(ml, parseFloat(this.degreeStr) || 0)
})
}
this.currentDrink = { category: '', brand: '', product: '', amount: 0, unit: 'ml', degree: 0 }
this.amountStr = ''
this.degreeStr = ''
removeDrink(index) {
this.drinks.splice(index, 1)
},
handleBack() {
if (this.currentStep > 1) {
@@ -532,31 +480,28 @@ export default {
if (this.currentStep > 1) this.currentStep--
},
nextStep() {
// Step 1 -> 2: 保存酒水基础信息
if (this.currentStep === 1) {
if (!this.degreeStr) this.degreeStr = String(this.currentDrink.degree)
}
// Step 2 -> 3: 保存饮用量
if (this.currentStep === 2) {
const ml = unitToMl(parseFloat(this.amountStr) || 0, this.currentDrink.unit)
this.drinks.push({
...this.currentDrink,
amount: parseFloat(this.amountStr) || 0,
degree: parseFloat(this.degreeStr) || 0,
standardCups: calcStandardCups(ml, parseFloat(this.degreeStr) || 0)
})
// 重置当前输入
this.currentDrink = { ...this.currentDrink, amount: 0 }
this.amountStr = ''
}
if (this.currentStep < this.totalSteps) this.currentStep++
},
addDrinkToList() {
if (!this.canAddDrink) return
const ml = unitToMl(parseFloat(this.amountStr) || 0, this.currentDrink.unit)
this.drinks.push({
...this.currentDrink,
amount: parseFloat(this.amountStr) || 0,
degree: parseFloat(this.degreeStr) || 0,
standardCups: calcStandardCups(ml, parseFloat(this.degreeStr) || 0)
})
// 重置表单,准备添加下一种
this.currentDrink = { category: '', brand: '', product: '', amount: 0, unit: 'ml', degree: 0 }
this.amountStr = ''
this.degreeStr = ''
this.brandList = []
},
async publishRecord() {
const allDrinks = this.allDrinks.length > 0 ? this.allDrinks : this.drinks
const recordData = {
date: this.backfillDate || formatDate(new Date(), 'YYYY-MM-DD'),
mode: 'drank',
drinks: allDrinks.map(d => ({
drinks: this.drinks.map(d => ({
category: d.category,
brand: d.brand,
product: d.product || '',
@@ -580,7 +525,7 @@ export default {
uni.hideLoading()
// 跳转到卡片页,传递记录ID
uni.redirectTo({
uni.navigateTo({
url: `/pages/card/card?recordId=${saved.id}`
})
} catch (e) {
@@ -589,7 +534,7 @@ export default {
uni.showToast({ title: e.msg || '保存失败,已本地保存', icon: 'none', duration: 2000 })
// 降级:本地保存
const saved = addRecord(recordData)
uni.redirectTo({
uni.navigateTo({
url: `/pages/card/card?recordId=${saved.id}`
})
}
@@ -612,6 +557,7 @@ export default {
align-items: center;
gap: $sp-md;
padding: $sp-md 0;
padding-top: 0;
}
.step-back {
@@ -718,8 +664,10 @@ export default {
color: $text-primary;
}
.brand-section {
margin-top: $sp-lg;
/* Step 1: 添加酒水(合并选酒+记量) */
.drink-form {
margin-bottom: $sp-lg;
padding: $sp-lg;
}
.section-label {
@@ -734,7 +682,7 @@ export default {
display: flex;
flex-wrap: wrap;
gap: $sp-sm;
margin-bottom: $sp-lg;
margin-bottom: $sp-md;
}
.input-row {
@@ -754,102 +702,6 @@ export default {
color: $text-tertiary;
}
/* Step 2: 记量 */
.amount-section {
margin-bottom: $sp-lg;
}
.amount-row {
display: flex;
align-items: baseline;
gap: $sp-sm;
margin-bottom: $sp-xl;
}
.amount-input {
font-size: $fs-3xl;
font-weight: $fw-black;
color: $text-primary;
flex: 1;
background: transparent;
border: none;
padding: 0;
height: 100rpx;
line-height: 100rpx;
}
.amount-unit {
font-size: $fs-lg;
color: $text-secondary;
}
.unit-switcher {
display: flex;
gap: $sp-sm;
margin-bottom: $sp-xl;
flex-wrap: wrap;
}
.unit-btn {
padding: $sp-sm $sp-lg;
background: $bg-elevated;
border-radius: $radius-full;
font-size: $fs-sm;
color: $text-secondary;
border: 2rpx solid transparent;
transition: all $duration-fast $ease-out;
&.unit-active {
background: $amber-glow;
color: $amber;
border-color: rgba(232,168,56,0.3);
}
}
.degree-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.degree-input-wrap {
display: flex;
align-items: center;
gap: $sp-xs;
}
.degree-input {
width: 120rpx;
text-align: center;
background: $bg-elevated;
border-radius: $radius-md;
padding: $sp-sm;
color: $text-primary;
font-size: $fs-lg;
}
.degree-symbol {
font-size: $fs-lg;
color: $text-secondary;
}
.standard-cup-result {
display: flex;
justify-content: space-between;
align-items: center;
}
.standard-cup-label {
font-size: $fs-base;
color: $text-secondary;
}
.standard-cup-label-row {
display: flex;
align-items: center;
gap: $sp-sm;
}
.cup-info-btn {
width: 36rpx;
height: 36rpx;
@@ -866,10 +718,96 @@ export default {
font-weight: $fw-bold;
}
.standard-cup-value {
font-size: $fs-xl;
.amount-degree-row {
display: flex;
align-items: center;
gap: $sp-lg;
margin-bottom: $sp-md;
}
.amount-inline {
display: flex;
align-items: baseline;
gap: $sp-sm;
flex: 1;
}
.degree-inline {
display: flex;
align-items: center;
gap: $sp-xs;
}
.cup-row {
display: flex;
align-items: center;
gap: $sp-xs;
padding: $sp-sm 0;
}
.cup-label {
font-size: $fs-sm;
color: $text-tertiary;
}
.cup-value {
font-size: $fs-md;
font-weight: $fw-bold;
}
.amount-input {
font-size: $fs-3xl;
font-weight: $fw-black;
color: $text-primary;
flex: 1;
background: transparent;
border: none;
padding: 0;
height: 80rpx;
line-height: 80rpx;
}
.amount-unit {
font-size: $fs-md;
color: $text-secondary;
}
.unit-switcher {
display: flex;
gap: $sp-sm;
margin-bottom: $sp-md;
flex-wrap: wrap;
}
.unit-btn {
padding: $sp-xs $sp-md;
background: $bg-elevated;
border-radius: $radius-full;
font-size: $fs-xs;
color: $text-secondary;
border: 2rpx solid transparent;
transition: all $duration-fast $ease-out;
&.unit-active {
background: $amber-glow;
color: $amber;
border-color: rgba(232,168,56,0.3);
}
}
.degree-input {
width: 100rpx;
text-align: center;
background: $bg-elevated;
border-radius: $radius-md;
padding: $sp-sm;
color: $text-primary;
font-size: $fs-lg;
}
.degree-symbol {
font-size: $fs-lg;
color: $text-secondary;
}
/* 标准杯说明弹窗 */
@@ -1245,67 +1183,6 @@ export default {
margin-top: $sp-xs;
}
.preview-section {
margin-bottom: $sp-lg;
}
.preview-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: $sp-sm 0;
}
.preview-meta {
margin-bottom: $sp-md;
}
.preview-line {
display: block;
font-size: $fs-base;
padding: $sp-xs 0;
color: $text-secondary;
}
.preview-total {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: $sp-md;
border-top: 2rpx solid var(--border-faint, rgba(255, 255, 255, 0.08));
}
/* 保存提示卡片 */
.save-hint {
display: flex;
align-items: flex-start;
gap: $sp-md;
margin-top: $sp-lg;
padding: $sp-lg;
background: rgba(232, 168, 56, 0.06);
border: 2rpx solid rgba(232, 168, 56, 0.15);
}
.save-hint-icon {
font-size: $fs-xl;
flex-shrink: 0;
}
.save-hint-title {
display: block;
font-size: $fs-base;
font-weight: $fw-bold;
color: $text-primary;
margin-bottom: $sp-xs;
}
.save-hint-desc {
display: block;
font-size: $fs-sm;
color: $text-secondary;
line-height: $lh-loose;
}
/* 底部操作栏 */
.bottom-actions {
position: fixed;
@@ -1319,7 +1196,7 @@ export default {
background: linear-gradient(to top, $bg-base 80%, transparent);
z-index: 50;
.btn-secondary, .btn-primary {
.btn-secondary, .btn-primary, .btn-add-drink {
flex: 1;
}
}
@@ -1328,8 +1205,43 @@ export default {
flex: 1;
}
.add-more-bar {
padding: $sp-lg 0;
text-align: center;
.added-drinks-header {
display: flex;
align-items: center;
gap: $sp-sm;
margin-bottom: $sp-md;
.section-label {
margin-bottom: 0;
}
}
.added-count {
font-size: $fs-xs;
color: $amber;
background: $amber-glow;
padding: 2rpx $sp-sm;
border-radius: $radius-full;
}
.added-drink-right {
display: flex;
align-items: center;
gap: $sp-md;
}
.added-drink-remove {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius-full;
background: var(--overlay-mask, rgba(255, 255, 255, 0.08));
}
.remove-icon {
font-size: $fs-xs;
color: $text-tertiary;
}
</style>