Files
hejiu/pages/record/record.vue
T
cg dd3c670021 feat: add guest mode and optimize login experience
针对微信审核要求新增游客浏览模式,用户可无需登录直接浏览应用内容,仅在使用发布、点赞、记录等需要身份的功能时才弹窗引导登录。具体修改包括:
1. 新增游客态判断工具函数与登录守卫逻辑
2. 为登录页添加游客直接进入入口
3. 为首页、酒友圈、个人页、动态页、记录页添加游客适配逻辑
4. 优化登录态失效处理逻辑,不再强制跳转登录页
5. 调整部分页面的返回逻辑适配分享直接进入场景
6. 为各页面添加游客引导UI与交互
2026-09-10 21:43:46 +08:00

1475 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view :class="themeClass" class="page-container record-page safe-bottom">
<!-- 顶部进度 -->
<view class="step-header" :style="{ paddingTop: (statusBarHeight + 12) + 'px' }">
<view class="step-back" @click="handleBack">
<text class="step-back-arrow">‹</text>
</view>
<view class="step-progress">
<view
v-for="i in totalSteps"
:key="i"
class="step-dot"
:class="{
'step-dot-active': i === currentStep,
'step-dot-done': i < currentStep
}"
></view>
</view>
<text class="step-label">{{ currentStep }}/{{ totalSteps }}</text>
</view>
<!-- 步骤标题 -->
<view class="step-title-area">
<text class="step-title">{{ stepTitles[currentStep - 1] }}</text>
<text class="step-subtitle">{{ stepSubtitles[currentStep - 1] }}</text>
</view>
<!-- Step 1: 添加酒水(选酒+记量合一) -->
<scroll-view v-if="currentStep === 1" class="step-content" scroll-y enhanced :show-scrollbar="false">
<!-- 酒类选择网格 -->
<view class="category-grid">
<view
v-for="cat in drinkCategories"
:key="cat.id"
class="category-item"
:class="{ 'category-active': currentDrink.category === cat.id }"
@click="selectCategory(cat)"
>
<image v-if="isIconPath(cat.icon)" :src="cat.icon" class="category-icon-img" mode="aspectFit"></image>
<text v-else class="category-emoji">{{ cat.emoji }}</text>
<text class="category-name">{{ cat.name }}</text>
</view>
</view>
<!-- 品牌 + 用量(选酒后展示) -->
<view v-if="currentDrink.category" class="drink-form card">
<!-- 品牌选择 -->
<text class="section-label">品牌</text>
<!-- 啤酒:分组tab(全部/国产/国际/麦啤/精酿)+ 组内品牌标签,输入框可实时搜索 -->
<scroll-view
v-if="currentDrink.category === 'beer' && !beerKeyword"
class="beer-brand-tabs"
scroll-x
enhanced
:show-scrollbar="false"
>
<view
v-for="tab in beerGroups"
:key="tab.id"
class="beer-tab"
:class="{ 'beer-tab-active': activeBeerGroup === tab.id }"
@click="selectBeerGroup(tab.id)"
>
<text>{{ tab.name }}</text>
</view>
</scroll-view>
<view class="brand-tags">
<view
v-for="brand in visibleBrands"
:key="brand.name + brand.product"
class="tag"
:class="{ 'tag-active': currentDrink.brand === brand.name && currentDrink.product === brand.product }"
@click="selectBrand(brand)"
>
<!-- 同品牌多产品时展示产品名以便区分 -->
<text>{{ getBrandTagLabel(brand) }}</text>
</view>
<!-- 搜索无结果提示 -->
<view v-if="currentDrink.category === 'beer' && beerKeyword && visibleBrands.length === 0" class="brand-empty">
<text>没找到相关啤酒,可直接在下方输入品牌</text>
</view>
<!-- 更多:默认只展示前几个品牌,点击展开全部(啤酒为分组tab无需更多) -->
<view v-if="currentDrink.category !== 'beer' && brandList.length > maxVisibleBrands" class="tag tag-more" @click="showAllBrands = !showAllBrands">
<text>{{ showAllBrands ? '收起 ▴' : '更多 ▾' }}</text>
</view>
</view>
<view class="input-row">
<input
class="input-field"
v-model="currentDrink.brand"
:placeholder="currentDrink.category === 'beer' ? '搜索或输入其他品牌' : '或输入其他品牌'"
placeholder-class="input-placeholder"
/>
</view>
<!-- 产品名称 -->
<text class="section-label" style="margin-top: 16rpx;">产品名称</text>
<view class="input-row">
<input
class="input-field"
v-model="currentDrink.product"
placeholder="如:飞天茅台53度"
placeholder-class="input-placeholder"
/>
</view>
<view class="divider"></view>
<!-- 用量 + 度数 一行 -->
<view class="amount-degree-row">
<view class="amount-inline" :class="{ 'amount-focus-wrap': amountFocused }">
<input
class="amount-input text-num"
type="digit"
v-model="amountStr"
placeholder="输入用量"
placeholder-class="amount-placeholder"
@input="onAmountChange"
@focus="amountFocused = true"
@blur="amountFocused = false"
/>
<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"
placeholder="度数"
placeholder-class="input-placeholder"
@input="onAmountChange"
/>
<text class="degree-symbol">°</text>
</view>
</view>
<!-- 单位切换 -->
<view class="unit-switcher">
<view
v-for="u in units"
:key="u.id"
class="unit-btn"
:class="{ 'unit-active': currentDrink.unit === u.id }"
@click="currentDrink.unit = u.id; onAmountChange()"
>
<text>{{ u.name }}</text>
<text v-if="u.id !== 'ml'" class="unit-ml">≈{{ u.toMl }}ml</text>
</view>
</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>
</view>
</view>
<!-- 已添加的酒水列表 -->
<view v-if="drinks.length > 0" class="added-drinks">
<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">
<view class="added-drink-name">
<image v-if="isIconPath(getCatIcon(d.category))" :src="getCatIcon(d.category)" class="inline-icon-img" mode="aspectFit"></image>
<text v-else>{{ getCatEmoji(d.category) }}</text>
<text>{{ d.brand }} {{ d.product }} · {{ d.customAmount || d.amount }}{{ getUnitLabel(d.customUnit || d.unit) }}</text>
</view>
<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 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-if="drinks.length > 0" class="health-hint">
<text class="text-tiny">💡 可继续添加更多酒水,或点击下一步</text>
</view>
<!-- 底部占位 -->
<view style="height: 180rpx;"></view>
</scroll-view>
<!-- 标准杯说明弹窗 -->
<view v-if="showCupInfo" class="cup-info-overlay" @click="showCupInfo = false">
<view class="cup-info-card card-elevated" @click.stop>
<view class="cup-info-header">
<text class="cup-info-title">什么是「标准杯」?</text>
<text class="cup-info-close" @click="showCupInfo = false">✕</text>
</view>
<view class="cup-info-body">
<text class="cup-info-desc">标准杯是国际通用的饮酒量度量单位,方便你比较不同酒类的实际酒精摄入量。</text>
<view class="cup-info-formula">
<text class="cup-info-formula-text">1 标准杯 = 10g 纯酒精</text>
</view>
<text class="cup-info-subtitle">常见换算参考:</text>
<view class="cup-info-table">
<view class="cup-info-row">
<text class="cup-info-cell">🥃 白酒 (52°) 1两</text>
<text class="cup-info-cell cup-info-val">≈ 2.1 杯</text>
</view>
<view class="cup-info-row">
<text class="cup-info-cell">🍺 啤酒 (5°) 1瓶</text>
<text class="cup-info-cell cup-info-val">≈ 2.0 杯</text>
</view>
<view class="cup-info-row">
<text class="cup-info-cell">🍷 红酒 (13°) 1杯</text>
<text class="cup-info-cell cup-info-val">≈ 1.5 杯</text>
</view>
<view class="cup-info-row">
<text class="cup-info-cell">🥂 威士忌 (40°) 1shot</text>
<text class="cup-info-cell cup-info-val">≈ 1.0 杯</text>
</view>
</view>
<view class="cup-info-tip">
<text class="cup-info-tip-text">💡 中国居民膳食指南建议:成年人每日饮酒不超过 2 标准杯</text>
</view>
</view>
</view>
</view>
<!-- Step 2: 配餐 -->
<view v-if="currentStep === 2" class="step-content">
<view class="food-grid">
<view
v-for="food in foodCategories"
:key="food.id"
class="food-item"
:class="{ 'food-active': selectedFood === food.id }"
@click="selectedFood = food.id"
>
<text class="food-emoji">{{ food.emoji }}</text>
<text class="food-name">{{ food.name }}</text>
</view>
</view>
<view v-if="selectedFood && selectedFood !== 'none'" class="food-detail">
<text class="section-label">具体吃了什么</text>
<input
class="input-field"
v-model="foodName"
placeholder="如:毛肚、黄喉、肥牛..."
placeholder-class="input-placeholder"
/>
</view>
</view>
<!-- Step 3: 感受 -->
<view v-if="currentStep === 3" class="step-content">
<view class="feeling-grid">
<view
v-for="f in feelings"
:key="f.id"
class="feeling-item"
:class="{ 'feeling-active': selectedFeeling === f.id }"
@click="selectedFeeling = f.id"
>
<text class="feeling-emoji">{{ f.emoji }}</text>
<text class="feeling-name">{{ f.name }}</text>
<text class="feeling-desc">{{ f.desc }}</text>
</view>
</view>
<view class="feeling-skip">
<text class="text-caption">不想记录感受?可以直接下一步</text>
</view>
</view>
<!-- Step 4: 照片 -->
<view v-if="currentStep === 4" class="step-content">
<view class="photo-grid">
<view
v-for="(photo, index) in photos"
:key="index"
class="photo-item"
>
<image :src="photo" mode="aspectFill" class="photo-img"></image>
<view class="photo-remove" @click="removePhoto(index)">
<text>✕</text>
</view>
</view>
<view v-if="photos.length < 6" class="photo-add" @click="choosePhoto">
<text class="photo-add-icon">📷</text>
<text class="photo-add-text">拍照/相册</text>
</view>
</view>
<view class="photo-tip">
<text class="text-caption">最多选择6张照片,记录今夜的精彩瞬间</text>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<button
v-if="currentStep > 1"
class="btn-secondary"
@click="prevStep"
>上一步</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"
@click="nextStep"
:disabled="!canProceed"
>下一步</button>
<button
v-else
class="btn-cta"
@click="publishRecord"
style="flex: none; width: 55%;"
>保存并生成卡片</button>
</view>
</view>
</template>
<script>
import {
DRINK_CATEGORIES, DRINK_UNITS, BRANDS, BEER_BRAND_GROUPS, FOOD_CATEGORIES,
FEELINGS, VISIBILITY_OPTIONS
} from '../../common/constants'
import { calcStandardCups, unitToMl, formatDate, getCatIcon, isIconPath } from '../../common/utils'
import client, { isLoggedIn } from '../../common/api'
import themeMixin from '../../common/theme-mixin'
export default {
mixins: [themeMixin],
data() {
const sysInfo = uni.getSystemInfoSync()
return {
statusBarHeight: sysInfo.statusBarHeight || 25,
currentStep: 1,
totalSteps: 4,
stepTitles: ['添加酒水', '配餐', '感受', '照片'],
stepSubtitles: ['喝了什么酒?喝了多少?', '配了什么菜?', '感觉如何?', '拍张照片吧'],
drinkCategories: DRINK_CATEGORIES,
units: DRINK_UNITS,
foodCategories: FOOD_CATEGORIES,
feelings: FEELINGS,
visibilityOptions: VISIBILITY_OPTIONS,
// Step 1
currentDrink: { category: '', brand: '', product: '', amount: 0, unit: 'ml', degree: 0 },
drinks: [],
brandList: [],
showAllBrands: false,
maxVisibleBrands: 8,
// 啤酒两级结构:分组tab列表 + 当前激活分组
beerGroups: BEER_BRAND_GROUPS,
activeBeerGroup: 'all',
// 点选标签填入的品牌名(不触发搜索过滤,区别于手动输入)
lastBeerSelection: '',
amountStr: '',
amountFocused: false,
degreeStr: '',
// Step 2
selectedFood: '',
foodName: '',
// Step 3
selectedFeeling: '',
// Step 4
photos: [],
// Step 5
visibility: 'friends',
// 补打卡日期
backfillDate: '',
// 标准杯说明弹窗
showCupInfo: false
}
},
computed: {
// 啤酒搜索关键词:品牌输入框内容兼作实时搜索(点选标签填入的值不算搜索)
beerKeyword() {
const v = (this.currentDrink.brand || '').trim()
if (v && v === this.lastBeerSelection) return ''
return v
},
// 品牌列表默认只展示前 maxVisibleBrands 个,点“更多”后展开全部
// 啤酒走两级结构:搜索优先,其次按分组tab过滤
visibleBrands() {
if (this.currentDrink.category === 'beer') {
const kw = this.beerKeyword.toLowerCase()
if (kw) {
return this.brandList.filter(b => (b.name + b.product).toLowerCase().includes(kw))
}
if (this.activeBeerGroup === 'all') return this.brandList
return this.brandList.filter(b => b.group === this.activeBeerGroup)
}
return this.showAllBrands ? this.brandList : this.brandList.slice(0, this.maxVisibleBrands)
},
currentCups() {
const ml = unitToMl(parseFloat(this.amountStr) || 0, this.currentDrink.unit)
return calcStandardCups(ml, parseFloat(this.degreeStr) || 0)
},
totalCups() {
return this.drinks.reduce((sum, d) => sum + d.standardCups, 0)
},
canAddDrink() {
return this.currentDrink.category && this.currentDrink.brand && parseFloat(this.amountStr) > 0
},
canProceed() {
switch (this.currentStep) {
case 1: return this.drinks.length > 0
case 2: return true // 配餐可选
case 3: return true // 感受可选
case 4: return true // 照片可选
default: return true
}
}
},
onLoad(options) {
if (options && options.date) {
this.backfillDate = options.date
}
// 登录守卫:记录功能需要登录(首页入口已拦截,此处兜底处理直接进入的场景)
if (!isLoggedIn()) {
uni.showModal({
title: '登录提示',
content: '记录饮酒需要登录后使用,是否立即登录?',
confirmText: '去登录',
cancelText: '再看看',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/login' })
} else {
uni.navigateBack({
fail: () => {
uni.switchTab({ url: '/pages/index/index' })
}
})
}
}
})
}
},
onShareAppMessage() {
return {
title: '碰盏日记 - 记录每一杯酒',
path: '/pages/index/index'
}
},
onShareTimeline() {
return {
title: '碰盏日记 - 记录每一杯酒',
query: ''
}
},
onBackPress() {
if (this.currentStep > 1 || this.hasFormData()) {
uni.showModal({
title: '放弃记录?',
content: '当前填写的内容将不会保存',
confirmText: '继续填写',
cancelText: '放弃',
success: (res) => {
if (res.cancel) {
uni.navigateBack()
}
}
})
return true // 阻止默认返回
}
return false
},
methods: {
selectCategory(cat) {
this.currentDrink.category = cat.id
this.currentDrink.degree = cat.defaultDegree
this.degreeStr = String(cat.defaultDegree)
this.brandList = BRANDS[cat.id] || []
this.showAllBrands = false
// 啤酒默认展示全部分组
this.activeBeerGroup = 'all'
this.lastBeerSelection = ''
},
selectBeerGroup(id) {
this.activeBeerGroup = id
},
getBrandTagLabel(brand) {
// 同品牌多产品时展示 品牌·产品,否则只展品牌名
return this.brandList.filter(b => b.name === brand.name).length > 1 ? brand.name + '·' + brand.product : brand.name
},
selectBrand(brand) {
this.currentDrink.brand = brand.name
this.currentDrink.product = brand.product
this.currentDrink.degree = brand.degree
this.degreeStr = String(brand.degree)
// 啤酒:记录点选值,避免输入框填入后误触发搜索过滤
if (this.currentDrink.category === 'beer') {
this.lastBeerSelection = brand.name
}
},
onAmountChange() {
// 响应式计算
},
getCatEmoji(catId) {
const cat = this.drinkCategories.find(c => c.id === catId)
return cat ? cat.emoji : '🥃'
},
getCatIcon,
isIconPath,
getUnitLabel(unitId) {
if (!unitId) return ''
const u = DRINK_UNITS.find(u => u.id === unitId)
return u ? u.name : unitId
},
getFoodName() {
const food = this.foodCategories.find(f => f.id === this.selectedFood)
if (!food) return ''
return this.foodName ? `${food.name}·${this.foodName}` : food.name
},
getFeelingEmoji() {
const f = this.feelings.find(f => f.id === this.selectedFeeling)
return f ? f.emoji : ''
},
getFeelingName() {
const f = this.feelings.find(f => f.id === this.selectedFeeling)
return f ? f.name : ''
},
choosePhoto() {
uni.chooseImage({
count: 6 - this.photos.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.photos = [...this.photos, ...res.tempFilePaths]
}
})
},
removePhoto(index) {
this.photos.splice(index, 1)
},
removeDrink(index) {
this.drinks.splice(index, 1)
},
handleBack() {
if (this.currentStep > 1) {
this.prevStep()
} else if (this.hasFormData()) {
uni.showModal({
title: '放弃记录?',
content: '当前填写的内容将不会保存',
confirmText: '继续填写',
cancelText: '放弃',
success: (res) => {
if (res.cancel) {
uni.navigateBack()
}
}
})
} else {
uni.navigateBack()
}
},
hasFormData() {
return this.drinks.length > 0 ||
this.currentDrink.brand ||
this.currentDrink.category ||
this.selectedFood ||
this.selectedFeeling ||
this.photos.length > 0
},
prevStep() {
if (this.currentStep > 1) this.currentStep--
},
nextStep() {
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),
customAmount: parseFloat(this.amountStr) || 0,
customUnit: this.currentDrink.unit
})
// 重置表单,准备添加下一种
this.currentDrink = { category: '', brand: '', product: '', amount: 0, unit: 'ml', degree: 0 }
this.amountStr = ''
this.degreeStr = ''
this.brandList = []
this.showAllBrands = false
this.activeBeerGroup = 'all'
this.lastBeerSelection = ''
},
async publishRecord() {
const recordData = {
date: this.backfillDate || formatDate(new Date(), 'YYYY-MM-DD'),
mode: 'drank',
drinks: this.drinks.map(d => ({
category: d.category,
brand: d.brand,
product: d.product || '',
amount: d.amount,
unit: d.unit,
degree: d.degree,
standardCups: d.standardCups,
customAmount: d.customAmount || d.amount,
customUnit: d.customUnit || d.unit
})),
food: this.selectedFood ? { category: this.selectedFood, name: this.foodName || '' } : null,
feeling: this.selectedFeeling || null,
photos: this.photos,
visibility: 'friends',
quote: ''
}
try {
uni.showLoading({ title: '保存中...' })
// 调用后端创建记录
const resp = await client.CreateRecord(recordData)
const saved = resp.data || resp
uni.hideLoading()
// 跳转到卡片页,传递记录ID
uni.navigateTo({
url: `/pages/card/card?recordId=${saved.id}`
})
} catch (e) {
uni.hideLoading()
console.warn('创建记录失败,降级本地保存:', e)
uni.showToast({ title: e.msg || '保存失败,已本地保存', icon: 'none', duration: 2000 })
// 降级:本地保存
recordData.id = `record_${Date.now()}`
recordData.createdAt = new Date().toISOString()
let localRecords = []
try { localRecords = JSON.parse(uni.getStorageSync('drink_records') || '[]') } catch (e) { /* */ }
localRecords.unshift(recordData)
uni.setStorageSync('drink_records', JSON.stringify(localRecords))
const saved = recordData
uni.navigateTo({
url: `/pages/card/card?recordId=${saved.id}`
})
}
}
}
}
</script>
<style lang="scss" scoped>
.record-page {
padding: $sp-lg;
color: $text-primary;
display: flex;
flex-direction: column;
}
/* 步骤头部 */
.step-header {
display: flex;
align-items: center;
gap: $sp-md;
padding: $sp-md 0;
padding-top: 0;
}
.step-back {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius-full;
background: $bg-card;
}
.step-back-arrow {
font-size: $fs-xl;
color: $text-secondary;
font-weight: $fw-bold;
}
.step-progress {
flex: 1;
display: flex;
gap: $sp-xs;
}
.step-dot {
flex: 1;
height: 6rpx;
border-radius: $radius-full;
background: $bg-elevated;
transition: all $duration-base $ease-out;
&.step-dot-done {
background: $amber;
}
&.step-dot-active {
background: $amber-light;
}
}
.step-label {
font-size: $fs-sm;
color: $text-tertiary;
font-family: $font-num;
}
.step-title-area {
margin-bottom: $sp-xl;
}
.step-title {
display: block;
font-size: $fs-2xl;
font-weight: $fw-black;
color: $text-primary;
margin-bottom: $sp-xs;
}
.step-subtitle {
font-size: $fs-sm;
color: $text-secondary;
}
.step-content {
flex: 1;
}
/* Step 1: 酒类选择 */
.category-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-md;
margin-bottom: $sp-xl;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
padding: $sp-lg $sp-sm;
background: $bg-card;
border-radius: $radius-lg;
border: 2rpx solid transparent;
transition: all $duration-fast $ease-out;
&.category-active {
border-color: $amber;
background: $amber-glow;
}
&:active {
transform: scale(0.95);
}
}
.category-emoji {
font-size: 56rpx;
margin-bottom: $sp-sm;
}
.category-icon-img {
width: 56rpx;
height: 56rpx;
margin-bottom: $sp-sm;
}
.category-name {
font-size: $fs-sm;
font-weight: $fw-medium;
color: $text-primary;
}
/* Step 1: 添加酒水(合并选酒+记量) */
.drink-form {
margin-bottom: $sp-lg;
padding: $sp-lg;
}
.section-label {
display: block;
font-size: $fs-sm;
font-weight: $fw-medium;
color: $text-secondary;
margin-bottom: $sp-md;
}
.brand-tags {
display: flex;
flex-wrap: wrap;
gap: $sp-sm;
margin-bottom: $sp-md;
}
/* 啤酒品牌分组tab:横向滚动,点击切换组内品牌标签 */
.beer-brand-tabs {
white-space: nowrap;
width: 100%;
margin-bottom: $sp-md;
}
.beer-tab {
display: inline-flex;
align-items: center;
padding: $sp-xs $sp-lg;
margin-right: $sp-sm;
border-radius: $radius-full;
font-size: $fs-sm;
font-weight: $fw-medium;
color: $text-secondary;
background: $bg-elevated;
border: 2rpx solid var(--border-faint, rgba(255, 255, 255, 0.08));
transition: all 0.2s ease;
&.beer-tab-active {
color: $amber;
background: $amber-glow;
border-color: $amber;
}
&:active {
transform: scale(0.95);
}
}
/* 啤酒搜索无结果提示 */
.brand-empty {
font-size: $fs-sm;
color: $text-tertiary;
padding: $sp-xs 0;
}
/* 更多/收起按钮:虚线边框与普通品牌标签区分 */
.tag-more {
border: 2rpx dashed var(--border-faint, rgba(255, 255, 255, 0.2));
background: transparent;
color: $amber;
}
.input-row {
margin-bottom: $sp-sm;
}
.input-field {
background: $bg-elevated;
border-radius: $radius-md;
padding: $sp-md $sp-lg;
color: $text-primary;
font-size: $fs-base;
border: 2rpx solid var(--border-faint, rgba(255, 255, 255, 0.08));
}
.input-placeholder {
color: $text-tertiary;
}
.cup-info-btn {
width: 36rpx;
height: 36rpx;
background: $bg-elevated;
border-radius: $radius-full;
display: flex;
align-items: center;
justify-content: center;
}
.cup-info-icon {
font-size: $fs-xs;
color: $text-tertiary;
font-weight: $fw-bold;
}
.amount-degree-row {
display: flex;
align-items: center;
gap: $sp-lg;
margin-bottom: $sp-md;
}
.amount-inline {
display: flex;
align-items: center;
gap: $sp-sm;
flex: 1;
background: $bg-elevated;
border: 2rpx solid var(--border-faint, rgba(255, 255, 255, 0.12));
border-radius: $radius-md;
padding: $sp-xs $sp-lg;
transition: border-color $duration-fast $ease-out;
}
/* 聚焦时高亮边框,强化可输入感知(小程序不支持 :has(),由容器类控制) */
.amount-focus-wrap {
border-color: $amber;
}
.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-2xl;
font-weight: $fw-black;
color: $text-primary;
flex: 1;
background: transparent;
border: none;
padding: 0;
height: 72rpx;
line-height: 72rpx;
}
.amount-placeholder {
color: $text-tertiary;
font-size: $fs-base;
font-weight: $fw-normal;
}
.amount-unit {
font-size: $fs-md;
color: $text-secondary;
flex-shrink: 0;
}
.unit-switcher {
display: flex;
gap: $sp-sm;
margin-bottom: $sp-md;
flex-wrap: wrap;
}
.unit-btn {
display: flex;
align-items: baseline;
gap: 6rpx;
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);
.unit-ml {
color: rgba(232, 168, 56, 0.6);
}
}
}
.unit-ml {
font-size: 20rpx;
color: $text-tertiary;
font-family: $font-num;
}
.degree-input {
width: 120rpx;
text-align: center;
background: $bg-elevated;
border-radius: $radius-md;
border: 2rpx solid var(--border-faint, rgba(255, 255, 255, 0.12));
padding: $sp-sm;
height: 72rpx;
color: $text-primary;
font-size: $fs-lg;
}
.degree-symbol {
font-size: $fs-lg;
color: $text-secondary;
}
/* 标准杯说明弹窗 */
.cup-info-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--overlay-modal, rgba(0, 0, 0, 0.7));
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
padding: $sp-xl;
}
.cup-info-card {
width: 100%;
max-width: 620rpx;
padding: $sp-xl;
border-radius: $radius-xl;
}
.cup-info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $sp-lg;
}
.cup-info-title {
font-size: $fs-lg;
font-weight: $fw-bold;
color: $text-primary;
}
.cup-info-close {
font-size: $fs-lg;
color: $text-tertiary;
padding: $sp-xs;
}
.cup-info-body {}
.cup-info-desc {
display: block;
font-size: $fs-sm;
color: $text-secondary;
line-height: $lh-loose;
margin-bottom: $sp-lg;
}
.cup-info-formula {
background: $amber-glow;
border-radius: $radius-md;
padding: $sp-md;
text-align: center;
margin-bottom: $sp-lg;
}
.cup-info-formula-text {
font-size: $fs-md;
font-weight: $fw-bold;
color: $amber;
font-family: $font-num;
}
.cup-info-subtitle {
display: block;
font-size: $fs-sm;
font-weight: $fw-medium;
color: $text-secondary;
margin-bottom: $sp-md;
}
.cup-info-table {
margin-bottom: $sp-lg;
}
.cup-info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: $sp-sm 0;
border-bottom: 2rpx solid var(--border-micro, rgba(255, 255, 255, 0.05));
}
.cup-info-cell {
font-size: $fs-sm;
color: $text-primary;
}
.cup-info-val {
color: $amber;
font-weight: $fw-bold;
font-family: $font-num;
}
.cup-info-tip {
background: rgba(94, 198, 160, 0.1);
border-radius: $radius-md;
padding: $sp-md;
}
.cup-info-tip-text {
font-size: $fs-xs;
color: $mint;
line-height: $lh-loose;
}
.added-drinks {
margin-bottom: $sp-lg;
}
.added-drink-item {
padding: $sp-md;
margin-bottom: $sp-sm;
}
.total-cups-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: $sp-md $sp-lg;
background: $bg-card;
border-radius: $radius-lg;
margin-bottom: $sp-md;
}
.total-cups-value {
font-size: $fs-xl;
font-weight: $fw-bold;
}
.health-warning {
display: flex;
align-items: center;
gap: $sp-md;
padding: $sp-md;
background: rgba(255, 107, 107, 0.1);
border: 2rpx solid rgba(255, 107, 107, 0.2);
}
.health-warning-icon {
font-size: $fs-xl;
}
.health-warning-text {
font-size: $fs-sm;
color: $coral;
flex: 1;
}
.health-hint {
text-align: center;
padding: $sp-sm;
}
/* Step 3: 配餐 */
.food-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-md;
margin-bottom: $sp-xl;
}
.food-item {
display: flex;
flex-direction: column;
align-items: center;
padding: $sp-lg $sp-sm;
background: $bg-card;
border-radius: $radius-lg;
border: 2rpx solid transparent;
transition: all $duration-fast $ease-out;
&.food-active {
border-color: $amber;
background: $amber-glow;
}
&:active {
transform: scale(0.95);
}
}
.food-emoji {
font-size: 56rpx;
margin-bottom: $sp-sm;
}
.food-name {
font-size: $fs-sm;
font-weight: $fw-medium;
color: $text-primary;
}
.food-detail {
margin-top: $sp-lg;
}
/* Step 4: 感受 */
.feeling-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $sp-lg;
margin-bottom: $sp-xl;
}
.feeling-item {
display: flex;
flex-direction: column;
align-items: center;
padding: $sp-xl $sp-lg;
background: $bg-card;
border-radius: $radius-xl;
border: 3rpx solid transparent;
transition: all $duration-base $ease-bounce;
&.feeling-active {
border-color: $amber;
background: $amber-glow;
transform: scale(1.03);
}
&:active {
transform: scale(0.96);
}
}
.feeling-emoji {
font-size: 80rpx;
margin-bottom: $sp-md;
}
.feeling-name {
font-size: $fs-lg;
font-weight: $fw-bold;
color: $text-primary;
margin-bottom: $sp-xs;
}
.feeling-desc {
font-size: $fs-xs;
color: $text-secondary;
}
.feeling-skip {
text-align: center;
padding: $sp-lg;
}
/* Step 5: 照片 */
.photo-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-md;
margin-bottom: $sp-lg;
}
.photo-item {
aspect-ratio: 1;
border-radius: $radius-lg;
overflow: hidden;
position: relative;
}
.photo-img {
width: 100%;
height: 100%;
}
.photo-remove {
position: absolute;
top: $sp-sm;
right: $sp-sm;
width: 44rpx;
height: 44rpx;
background: var(--overlay-mask, rgba(0, 0, 0, 0.6));
border-radius: $radius-full;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: $fs-sm;
}
.photo-add {
aspect-ratio: 1;
background: $bg-card;
border-radius: $radius-lg;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 3rpx dashed var(--border-dashed, rgba(255, 255, 255, 0.12));
&:active {
background: $bg-card-alt;
}
}
.photo-add-icon {
font-size: $fs-3xl;
color: $text-tertiary;
margin-bottom: $sp-sm;
opacity: 0.5;
}
.photo-add-text {
font-size: $fs-xs;
color: $text-tertiary;
}
.photo-tip {
text-align: center;
color: $text-secondary;
}
/* Step 6: 发布 */
.visibility-section {
margin-bottom: $sp-xl;
}
.visibility-options {
display: flex;
flex-direction: column;
gap: $sp-sm;
}
.visibility-item {
display: flex;
align-items: center;
gap: $sp-md;
padding: $sp-lg;
background: $bg-card;
border-radius: $radius-lg;
border: 2rpx solid transparent;
transition: all $duration-fast $ease-out;
&.visibility-active {
border-color: $amber;
background: $amber-glow;
}
}
.visibility-radio {
width: 32rpx;
height: 32rpx;
border-radius: $radius-full;
border: 3rpx solid $text-tertiary;
transition: all $duration-fast $ease-out;
&.radio-checked {
border-color: $amber;
background: $amber;
box-shadow: inset 0 0 0 6rpx $bg-base;
}
}
.visibility-name {
display: block;
font-size: $fs-base;
font-weight: $fw-medium;
color: $text-primary;
}
.visibility-desc {
display: block;
font-size: $fs-xs;
color: $text-secondary;
margin-top: $sp-xs;
}
/* 底部操作栏 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: $sp-md;
padding: $sp-lg;
padding-bottom: calc(env(safe-area-inset-bottom) + $sp-lg);
background: linear-gradient(to top, $bg-base 80%, transparent);
z-index: 50;
.btn-secondary, .btn-primary, .btn-add-drink {
flex: 1;
}
}
.btn-placeholder {
flex: 1;
}
.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-name {
display: flex;
align-items: center;
gap: $sp-xs;
flex: 1;
min-width: 0;
}
.inline-icon-img {
width: 36rpx;
height: 36rpx;
flex-shrink: 0;
}
.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>