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
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:
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user