Fix recipe detail overlay: layout, buttons, price columns
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 12s
Test / e2e-test (push) Failing after 1m7s

- Remove "卡片预览" tab text, use top bar with action buttons
- Move language toggle (中文/English) to top of card view
- Fix favorite button: check recipe _id before toggling
- Fix save-to-diary: match API fields (name, source_recipe_id)
- Use custom translations in card rendering (getCardOilName/getCardRecipeName)
- Swap price columns: cost first, retail strikethrough after
- Add retail strikethrough price in total cost bar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 21:41:49 +00:00
parent d68f5b35ee
commit 4655040153

View File

@@ -1,32 +1,41 @@
<template>
<div class="detail-overlay" @click.self="$emit('close')">
<div class="detail-panel">
<!-- Mode toggle tabs -->
<div class="detail-mode-tabs">
<button
class="mode-tab"
:class="{ active: viewMode === 'card' }"
@click="viewMode = 'card'"
>卡片预览</button>
<button
v-if="canEditThisRecipe"
class="mode-tab"
:class="{ active: viewMode === 'editor' }"
@click="viewMode = 'editor'"
>编辑</button>
<button class="detail-close-btn" @click="$emit('close')"></button>
<!-- Top bar: close button only (no "卡片预览" text) -->
<div class="detail-top-bar">
<div class="card-top-actions" v-if="viewMode === 'card' && authStore.isLoggedIn">
<button class="action-btn action-btn-fav" @click="handleToggleFavorite">
{{ isFav ? ' 已收藏' : ' 收藏' }}
</button>
<button v-if="!recipe._diary_id" class="action-btn action-btn-diary" @click="saveToDiary">
📔 存为我的
</button>
</div>
<div class="top-bar-spacer" v-else></div>
<div class="top-bar-right">
<button
v-if="canEditThisRecipe && viewMode === 'card'"
class="action-btn action-btn-sm"
@click="viewMode = 'editor'"
>编辑</button>
<button class="detail-close-btn" @click="$emit('close')"></button>
</div>
</div>
<!-- ==================== CARD VIEW ==================== -->
<div v-if="viewMode === 'card'" class="detail-card-view">
<!-- Top action buttons -->
<div class="card-top-actions">
<button class="action-btn action-btn-fav" @click="handleToggleFavorite">
{{ isFav ? ' 已收藏' : ' 收藏' }}
</button>
<button class="action-btn action-btn-diary" @click="saveToDiary">
📔 存为我的
</button>
<!-- Language toggle (at top, matching main branch) -->
<div class="card-lang-toggle">
<button
class="lang-btn"
:class="{ active: cardLang === 'zh' }"
@click="switchLang('zh')"
>中文</button>
<button
class="lang-btn"
:class="{ active: cardLang === 'en' }"
@click="switchLang('en')"
>English</button>
</div>
<!-- Card image (rendered by html2canvas) -->
@@ -56,7 +65,7 @@
{{ cardLang === 'en' ? 'doTERRA · Gifts of the Earth' : 'doTERRA · 来自大地的礼物' }}
</div>
<div class="card-title">
{{ cardLang === 'en' ? recipeNameEn(recipe.name) : recipe.name }}
{{ getCardRecipeName() }}
</div>
<div class="card-divider"></div>
@@ -64,17 +73,18 @@
<ul class="card-ingredients">
<li v-for="(ing, i) in cardIngredients" :key="i">
<span class="card-oil-name">
{{ cardLang === 'en' ? (oilEn(ing.oil) || ing.oil) : ing.oil }}
{{ getCardOilName(ing.oil) }}
</span>
<span class="card-oil-drops">
{{ ing.drops }} {{ cardLang === 'en' ? 'drops' : '滴' }}
</span>
<span class="card-oil-cost">
<template v-if="hasRetailForOil(ing.oil) && retailPerDrop(ing.oil) > oilsStore.pricePerDrop(ing.oil)">
<span class="card-retail-strike">{{ oilsStore.fmtPrice(retailPerDrop(ing.oil) * ing.drops) }}</span>
</template>
{{ oilsStore.fmtPrice(oilsStore.pricePerDrop(ing.oil) * ing.drops) }}
</span>
<span
v-if="hasRetailForOil(ing.oil) && retailPerDrop(ing.oil) > oilsStore.pricePerDrop(ing.oil)"
class="card-retail-strike"
>{{ oilsStore.fmtPrice(retailPerDrop(ing.oil) * ing.drops) }}</span>
</li>
</ul>
@@ -89,7 +99,10 @@
<div class="card-total-label">
{{ cardLang === 'en' ? 'Total Cost' : '配方总成本' }}
</div>
<div class="card-total-price">{{ priceInfo.cost }}</div>
<div class="card-total-price">
{{ priceInfo.cost }}
<span v-if="priceInfo.hasRetail" class="card-total-retail">{{ priceInfo.retail }}</span>
</div>
</div>
<!-- Date -->
@@ -104,20 +117,6 @@
<img :src="cardImageUrl" class="card-rendered-image" />
</div>
<!-- Language toggle -->
<div class="card-lang-toggle">
<button
class="lang-btn"
:class="{ active: cardLang === 'zh' }"
@click="switchLang('zh')"
>中文</button>
<button
class="lang-btn"
:class="{ active: cardLang === 'en' }"
@click="switchLang('en')"
>English</button>
</div>
<!-- Bottom action buttons -->
<div class="card-bottom-actions">
<button class="action-btn" @click="saveImage">💾 保存图片</button>
@@ -539,9 +538,14 @@ async function handleToggleFavorite() {
ui.openLogin()
return
}
if (!recipe.value._id) {
ui.showToast('该配方无法收藏')
return
}
const wasFav = isFav.value
try {
await recipesStore.toggleFavorite(recipe.value._id)
ui.showToast(isFav.value ? '已取消收藏' : '已收藏')
ui.showToast(wasFav ? '已取消收藏' : '已收藏')
} catch (e) {
ui.showToast('操作失败')
}
@@ -553,15 +557,16 @@ async function saveToDiary() {
ui.openLogin()
return
}
const name = prompt('保存为我的配方,名称:', recipe.value.name)
if (!name) return
try {
await api.post('/api/diary', {
recipe_id: recipe.value._id,
recipe_name: recipe.value.name,
ingredients: recipe.value.ingredients,
note: recipe.value.note,
tags: recipe.value.tags,
name,
source_recipe_id: recipe.value._id || null,
ingredients: recipe.value.ingredients.map(i => ({ oil: i.oil, drops: i.drops })),
note: recipe.value.note || '',
})
ui.showToast('已存为我的配方')
ui.showToast('已保存到「我的配方日记」')
} catch (e) {
ui.showToast('保存失败: ' + (e?.message || '未知错误'))
}
@@ -808,58 +813,52 @@ async function saveRecipe() {
position: relative;
}
/* Mode tabs */
.detail-mode-tabs {
/* Top bar */
.detail-top-bar {
display: flex;
align-items: center;
border-bottom: 1px solid var(--border, #e0d4c0);
padding: 0 16px;
gap: 4px;
justify-content: space-between;
padding: 12px 16px;
position: sticky;
top: 0;
background: #fff;
z-index: 10;
border-radius: 18px 18px 0 0;
border-bottom: 1px solid var(--border, #e0d4c0);
gap: 8px;
}
.mode-tab {
padding: 14px 16px;
font-size: 14px;
font-weight: 500;
color: var(--text-light, #9a8570);
cursor: pointer;
border: none;
background: none;
border-bottom: 2px solid transparent;
font-family: inherit;
transition: color 0.2s;
.top-bar-spacer {
flex: 1;
}
.mode-tab:hover {
color: var(--sage-dark, #5a7d5e);
}
.mode-tab.active {
color: var(--sage-dark, #5a7d5e);
border-bottom-color: var(--sage, #7a9e7e);
.top-bar-right {
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
}
.detail-close-btn {
margin-left: auto;
background: none;
background: rgba(0, 0, 0, 0.06);
border: none;
font-size: 20px;
font-size: 14px;
color: var(--text-light, #9a8570);
cursor: pointer;
padding: 8px;
padding: 0;
width: 28px;
height: 28px;
border-radius: 50%;
transition: all 0.2s;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.detail-close-btn:hover {
color: var(--text-dark, #2c2416);
background: rgba(0, 0, 0, 0.05);
background: rgba(0, 0, 0, 0.1);
}
/* ==================== CARD VIEW ==================== */
@@ -869,8 +868,8 @@ async function saveRecipe() {
.card-top-actions {
display: flex;
gap: 10px;
margin-bottom: 16px;
gap: 6px;
flex-wrap: wrap;
}
.action-btn-fav {
@@ -1024,17 +1023,19 @@ async function saveRecipe() {
}
.card-oil-cost {
width: 90px;
width: 60px;
text-align: right;
color: var(--text-light, #9a8570);
font-size: 12px;
}
.card-retail-strike {
width: 55px;
text-align: right;
text-decoration: line-through;
opacity: 0.5;
margin-right: 4px;
font-size: 11px;
color: var(--text-light, #9a8570);
font-size: 10px;
opacity: 0.6;
}
.card-dilution {
@@ -1077,6 +1078,14 @@ async function saveRecipe() {
font-weight: 700;
}
.card-total-retail {
text-decoration: line-through;
opacity: 0.6;
font-size: 13px;
font-weight: 400;
margin-left: 6px;
}
.card-footer {
margin-top: 16px;
text-align: center;
@@ -1101,20 +1110,28 @@ async function saveRecipe() {
.card-lang-toggle {
display: flex;
justify-content: center;
gap: 8px;
margin: 16px 0;
gap: 0;
margin-bottom: 12px;
border: 1.5px solid var(--border, #e0d4c0);
border-radius: 10px;
overflow: hidden;
align-self: center;
width: fit-content;
margin-left: auto;
margin-right: auto;
}
.lang-btn {
padding: 6px 18px;
border: 1.5px solid var(--border, #e0d4c0);
border-radius: 20px;
padding: 6px 16px;
border: none;
border-radius: 0;
background: #fff;
font-size: 13px;
font-size: 12px;
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
color: var(--text-mid, #5a4a35);
color: var(--text-mid, #5a4a35);
}
.lang-btn.active {
@@ -1124,7 +1141,7 @@ async function saveRecipe() {
}
.lang-btn:hover:not(.active) {
border-color: var(--sage, #7a9e7e);
background: var(--sage-mist, #eef4ee);
}
/* Card bottom actions */