|
|
@@ -15,8 +15,8 @@
|
|
|
@overview="switchView('family')"
|
|
|
@select="switchMember" />
|
|
|
|
|
|
- <!-- Tab 切换行:今日食谱 | 适合 | 戒备 | 全部 -->
|
|
|
- <view class="filter-bar">
|
|
|
+ <!-- Tab 切换行:今日食谱 | 适合 | 戒备 | 全部(只读分享模式下隐藏,避免切到空食材区) -->
|
|
|
+ <view class="filter-bar" v-if="!shareMode">
|
|
|
<view class="filter-item" :class="{'filter-active': currentTab === 'recipe'}" @tap="switchTab('recipe')">
|
|
|
<text>🍽️ 今日食谱</text>
|
|
|
</view>
|
|
|
@@ -161,10 +161,10 @@
|
|
|
<text class="recipe-loading-text">正在加载今日食谱...</text>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 失败态:接口异常 → 明确提示 + 重试入口,文案与空态区分(AC-16 / AC-38) -->
|
|
|
+ <!-- 失败态:接口异常 → 明确提示 + 重试入口,文案与空态区分(AC-16 / AC-38);只读分享模式下文案改分享语义 -->
|
|
|
<view class="empty-state" v-else-if="recipeError">
|
|
|
<view class="empty-icon-wrap"><text class="empty-icon">⚠️</text></view>
|
|
|
- <text class="empty-title">今日食谱加载失败</text>
|
|
|
+ <text class="empty-title">{{ shareMode ? '菜谱加载失败' : '今日食谱加载失败' }}</text>
|
|
|
<text class="empty-sub">{{ recipeError }}</text>
|
|
|
<view class="generate-btn" @tap="retryRecommendation">
|
|
|
<text>点击重试</text>
|
|
|
@@ -174,19 +174,25 @@
|
|
|
<!-- 无推荐状态 / 视角级空态(AC-04;不回退展示其它视角食谱) -->
|
|
|
<view class="empty-state" v-else-if="!recommendation">
|
|
|
<view class="empty-icon-wrap"><text class="empty-icon">🍽️</text></view>
|
|
|
- <text class="empty-title">今日菜单空着</text>
|
|
|
- <text class="empty-sub">点击生成今日三餐食谱</text>
|
|
|
- <view class="generate-btn" @tap="generateRecipe">
|
|
|
- <text>生成食谱</text>
|
|
|
- </view>
|
|
|
+ <block v-if="shareMode">
|
|
|
+ <text class="empty-title">菜谱不存在或已失效</text>
|
|
|
+ <text class="empty-sub">分享链接可能已过期</text>
|
|
|
+ </block>
|
|
|
+ <block v-else>
|
|
|
+ <text class="empty-title">今日菜单空着</text>
|
|
|
+ <text class="empty-sub">点击生成今日三餐食谱</text>
|
|
|
+ <view class="generate-btn" @tap="generateRecipe">
|
|
|
+ <text>生成食谱</text>
|
|
|
+ </view>
|
|
|
+ </block>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 整份空态:所有餐次菜数为 0(删除后可能出现,AC-35) -->
|
|
|
+ <!-- 整份空态:所有餐次菜数为 0(删除后可能出现,AC-35);只读分享模式下隐藏重新生成入口 -->
|
|
|
<view class="empty-state" v-else-if="allMealsEmpty">
|
|
|
<view class="empty-icon-wrap"><text class="empty-icon">🍽️</text></view>
|
|
|
<text class="empty-title">今天的菜品都删完啦</text>
|
|
|
- <text class="empty-sub">点击重新生成今日三餐食谱</text>
|
|
|
- <view class="generate-btn" @tap="regenerate">
|
|
|
+ <text class="empty-sub" v-if="!shareMode">点击重新生成今日三餐食谱</text>
|
|
|
+ <view class="generate-btn" @tap="regenerate" v-if="!shareMode">
|
|
|
<text>重新生成</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
@@ -204,6 +210,16 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
+ <!-- 分享操作区:只读分享模式显示"这是朋友分享的菜谱",本人模式显示分享按钮 -->
|
|
|
+ <view class="share-bar">
|
|
|
+ <text v-if="shareMode" class="share-mode-tip">📤 朋友分享的今日食谱</text>
|
|
|
+ <view v-else class="share-btn-wrap">
|
|
|
+ <button class="share-btn" open-type="share">
|
|
|
+ <text class="share-btn-text">📤 分享菜谱</text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
<!-- 菜品列表 -->
|
|
|
<view class="meal-section" v-for="(meal, index) in meals" :key="index">
|
|
|
<view class="meal-header">
|
|
|
@@ -236,7 +252,7 @@
|
|
|
<text class="method-text">🍳 {{ dish.method }}</text>
|
|
|
</view>
|
|
|
<!-- D1①:收敛为单一「🗑 删除」;按钮组合由 dishActions 数据位驱动,便于回退 -->
|
|
|
- <view class="dish-actions" v-if="dishActions === 'delete-only'">
|
|
|
+ <view class="dish-actions" v-if="dishActions === 'delete-only' && !shareMode">
|
|
|
<view class="action-btn delete-btn"
|
|
|
:class="{'action-btn-disabled': isDeleting(meal.name, dIndex)}"
|
|
|
@tap="deleteDish(meal.name, dIndex)">🗑 删除</view>
|
|
|
@@ -275,8 +291,8 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 操作按钮 -->
|
|
|
- <view class="action-area">
|
|
|
+ <!-- 操作按钮:只读分享模式下隐藏本人操作 -->
|
|
|
+ <view class="action-area" v-if="!shareMode">
|
|
|
<view class="btn-row">
|
|
|
<view class="btn-secondary" @tap="regenerate">重新生成</view>
|
|
|
<view class="btn-primary" @tap="completeRecipe" v-if="recommendation.status !== 'completed'">
|
|
|
@@ -322,15 +338,16 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { getDietRecommendation, generateDietRecommendation, completeDietRecommendation, regenerateDietRecommendation, updateDietRecommendation } from '@/utils/api.js'
|
|
|
+import { getDietRecommendation, generateDietRecommendation, completeDietRecommendation, regenerateDietRecommendation, updateDietRecommendation, getShareDietRecommendation } from '@/utils/api.js'
|
|
|
import config from '@/config.js'
|
|
|
import MemberSelector from '@/components/MemberSelector.vue'
|
|
|
import { memberIdOf } from '@/utils/memberNormalize.js'
|
|
|
+import shareMixin from '@/components/share-mixin.js'
|
|
|
|
|
|
import fontScaleMixin from '@/mixins/font-scale.js'
|
|
|
|
|
|
export default {
|
|
|
- mixins: [fontScaleMixin],
|
|
|
+ mixins: [fontScaleMixin, shareMixin],
|
|
|
components: { MemberSelector },
|
|
|
data() {
|
|
|
return {
|
|
|
@@ -358,13 +375,16 @@ export default {
|
|
|
// ===== 共餐人选择(需求7:家庭总览生成食谱先选共餐人)=====
|
|
|
showParticipantPicker: false,
|
|
|
pendingParticipantIds: [],
|
|
|
- // ===== 今日食谱视角隔离 / 删除交互 / 空·加载·失败三态(本轮增量)=====
|
|
|
+// ===== 今日食谱视角隔离 / 删除交互 / 空·加载·失败三态(本轮增量)=====
|
|
|
recipeLoading: false,
|
|
|
recipeError: '',
|
|
|
deleting: false,
|
|
|
deletingKey: '',
|
|
|
// D1① 可回退数据位:'delete-only'(当前)/ 'replace-and-delete'(备选,改此值 + 模板分支即可)
|
|
|
- dishActions: 'delete-only'
|
|
|
+ dishActions: 'delete-only',
|
|
|
+ // ===== 菜谱分享(id 参数进入只读模式,跨家庭可看,跳过本人成员/家庭/食材数据加载)=====
|
|
|
+ shareMode: false,
|
|
|
+ sharedId: null
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -475,12 +495,20 @@ export default {
|
|
|
onLoad: function(options) {
|
|
|
var opts = options || {}
|
|
|
this.currentMemberId = opts.memberId ? parseInt(opts.memberId) : null
|
|
|
+ // 菜谱分享只读模式:带 id 进入 → 按 id 加载该份菜谱(跨家庭可看),跳过本人成员/家庭/食材数据加载
|
|
|
+ this.shareMode = !!(opts.id && !opts.memberId)
|
|
|
+ if (this.shareMode) {
|
|
|
+ this.sharedId = parseInt(opts.id)
|
|
|
+ this.loadSharedRecommendation(this.sharedId)
|
|
|
+ return
|
|
|
+ }
|
|
|
this.loadRecommendation()
|
|
|
this.loadMembers()
|
|
|
this.loadFamilyData()
|
|
|
this.loadFoods()
|
|
|
},
|
|
|
onShow: function() {
|
|
|
+ if (this.shareMode) return
|
|
|
this.loadRecommendation()
|
|
|
},
|
|
|
methods: {
|
|
|
@@ -514,6 +542,7 @@ export default {
|
|
|
self.parseMenu(d.menu)
|
|
|
var ns = d.nutritionSummary
|
|
|
self.summary = ns ? (typeof ns === 'string' ? JSON.parse(ns) : ns) : {}
|
|
|
+ self.setShareInfo('今日食谱 ' + (d.date || today), '/pages/diet/recommendation?id=' + d.id)
|
|
|
} else {
|
|
|
self.recommendation = null
|
|
|
self.meals = []
|
|
|
@@ -527,7 +556,7 @@ export default {
|
|
|
self.recipeError = (err && err.message) || '加载失败,请重试'
|
|
|
})
|
|
|
},
|
|
|
- // 视角键生成(K1 可回退的唯一落点):家庭总览固定 'family';个人/成员 'member:<id>';
|
|
|
+// 视角键生成(K1 可回退的唯一落点):家庭总览固定 'family';个人/成员 'member:<id>';
|
|
|
// loadMembers 未完成、memberList 为空且无 currentMemberId 时返回 null 且不发请求。
|
|
|
// 后端把 view_key 当不透明字符串,语义全在此方法一处。
|
|
|
buildViewKey: function() {
|
|
|
@@ -542,8 +571,38 @@ export default {
|
|
|
},
|
|
|
// 失败态重试:按当前视角重新加载,不重置用户当前视角
|
|
|
retryRecommendation: function() {
|
|
|
+ if (this.shareMode) {
|
|
|
+ this.loadSharedRecommendation(this.sharedId)
|
|
|
+ return
|
|
|
+ }
|
|
|
this.loadRecommendation()
|
|
|
},
|
|
|
+ // 分享只读模式:按 id 加载该份菜谱(后端 getShareDetail,跨家庭可看,无 familyId 归属校验)
|
|
|
+ loadSharedRecommendation: function(id) {
|
|
|
+ var self = this
|
|
|
+ if (!id) return
|
|
|
+ self.recipeLoading = true
|
|
|
+ getShareDietRecommendation({ id: id }).then(function(res) {
|
|
|
+ self.recipeLoading = false
|
|
|
+ var d = res && res.data
|
|
|
+ if (d && d.id) {
|
|
|
+ self.recommendation = d
|
|
|
+ self.parseMenu(d.menu)
|
|
|
+ var ns = d.nutritionSummary
|
|
|
+ self.summary = ns ? (typeof ns === 'string' ? JSON.parse(ns) : ns) : {}
|
|
|
+ self.setShareInfo('今日食谱 ' + (d.date || ''), '/pages/diet/recommendation?id=' + id)
|
|
|
+ } else {
|
|
|
+ self.recipeLoading = false
|
|
|
+ self.recommendation = null
|
|
|
+ self.meals = []
|
|
|
+ self.summary = {}
|
|
|
+ uni.showToast({ title: (d && d.message) || '菜谱不存在或已失效', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.recipeLoading = false
|
|
|
+ uni.showToast({ title: '加载分享菜谱失败', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
parseMenu: function(menuJson) {
|
|
|
try {
|
|
|
var menu = (typeof menuJson === 'string') ? JSON.parse(menuJson || '{}') : (menuJson || {})
|
|
|
@@ -1275,6 +1334,42 @@ export default {
|
|
|
display: block;
|
|
|
margin-top: 4rpx;
|
|
|
}
|
|
|
+/* ===== 菜谱分享栏(本人模式分享按钮 / 只读模式提示条)===== */
|
|
|
+.share-bar {
|
|
|
+ padding: 24rpx $uni-space-page 0;
|
|
|
+}
|
|
|
+.share-btn-wrap {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.share-btn {
|
|
|
+ width: 320rpx;
|
|
|
+ padding: 18rpx 0;
|
|
|
+ background: linear-gradient(135deg, #ff8c42, #ffb26b);
|
|
|
+ border-radius: 999rpx;
|
|
|
+ border: none;
|
|
|
+ line-height: 1.4;
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+ box-shadow: 0 6rpx 20rpx rgba(255, 140, 66, 0.25);
|
|
|
+}
|
|
|
+.share-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+.share-btn-text {
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.share-mode-tip {
|
|
|
+ display: block;
|
|
|
+ text-align: center;
|
|
|
+ font-size: calc(26rpx * var(--font-scale, 1));
|
|
|
+ color: $brand-primary-deep;
|
|
|
+ background: rgba(255, 140, 66, 0.08);
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 20rpx 0;
|
|
|
+}
|
|
|
+
|
|
|
.action-area {
|
|
|
padding: 40rpx $uni-space-page;
|
|
|
}
|