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