feat: 精油知识卡片加品牌元素 + 导出PDF修复
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 6s
PR Preview / deploy-preview (pull_request) Successful in 8s
Test / e2e-test (push) Has been cancelled

- 知识卡片: QR码(右上角)+品牌名+背景图+Logo
- 导出PDF: volumeLabel传name参数修复,单价列适配单位
- 导入api模块修复品牌数据加载

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 13:53:24 +00:00
parent ccf96d54cd
commit 8a49938929

View File

@@ -181,9 +181,16 @@
<!-- Oil Knowledge Card Modal --> <!-- Oil Knowledge Card Modal -->
<div v-if="activeCard && activeCardName" class="modal-overlay" @mousedown.self="closeOilModal"> <div v-if="activeCard && activeCardName" class="modal-overlay" @mousedown.self="closeOilModal">
<div class="oil-card-modal"> <div class="oil-card-modal" style="position:relative;overflow:hidden">
<div class="oil-card-header"> <!-- Brand background -->
<div class="oil-card-header-content"> <div v-if="brand.brand_bg" style="position:absolute;inset:0;width:100%;height:100%;background-size:cover;background-position:center;opacity:0.08;pointer-events:none;z-index:0" :style="{ backgroundImage: `url('${brand.brand_bg}')` }"></div>
<!-- QR code -->
<div v-if="brand.qr_code" style="position:absolute;top:16px;right:16px;display:flex;flex-direction:column;gap:3px;z-index:3" :style="{ alignItems: (brand.brand_align === 'left' ? 'flex-start' : brand.brand_align === 'right' ? 'flex-end' : 'center') }">
<img :src="brand.qr_code" crossorigin="anonymous" style="width:48px;height:48px;object-fit:cover;border-radius:6px;box-shadow:0 2px 6px rgba(0,0,0,0.1)" />
<div v-if="brand.brand_name" style="font-size:7px;color:rgba(255,255,255,0.8);line-height:1.3;max-width:60px;white-space:pre-line;text-align:center">{{ brand.brand_name }}</div>
</div>
<div class="oil-card-header" style="position:relative;z-index:1">
<div class="oil-card-header-content" :style="brand.qr_code ? 'padding-right:70px' : ''">
<span class="oil-card-emoji">{{ activeCard.emoji }}</span> <span class="oil-card-emoji">{{ activeCard.emoji }}</span>
<div> <div>
<h2 class="oil-card-title">{{ activeCardName }}</h2> <h2 class="oil-card-title">{{ activeCardName }}</h2>
@@ -196,7 +203,7 @@
</div> </div>
</div> </div>
</div> </div>
<button class="btn-close btn-close-light" @click="closeOilModal"></button> <button class="btn-close btn-close-light" @click="closeOilModal" style="z-index:4"></button>
</div> </div>
<!-- Method badges --> <!-- Method badges -->
<div class="oil-card-methods"> <div class="oil-card-methods">
@@ -227,6 +234,10 @@
<h4 class="oil-card-caution-title"> 注意事项</h4> <h4 class="oil-card-caution-title"> 注意事项</h4>
<p>{{ activeCard.caution }}</p> <p>{{ activeCard.caution }}</p>
</div> </div>
<!-- Logo -->
<div v-if="brand.brand_logo" style="padding-top:8px">
<img :src="brand.brand_logo" crossorigin="anonymous" style="height:24px;opacity:0.7" />
</div>
<div style="text-align:center;padding-top:12px"> <div style="text-align:center;padding-top:12px">
<button class="btn btn-outline btn-sm" @click="saveCardImage(activeCardName)">💾 保存图片</button> <button class="btn btn-outline btn-sm" @click="saveCardImage(activeCardName)">💾 保存图片</button>
</div> </div>
@@ -408,12 +419,19 @@ import { useRecipesStore } from '../stores/recipes'
import { oilEn } from '../composables/useOilTranslation' import { oilEn } from '../composables/useOilTranslation'
import { getOilCard, setOilCard } from '../composables/useOilCards' import { getOilCard, setOilCard } from '../composables/useOilCards'
import { showConfirm } from '../composables/useDialog' import { showConfirm } from '../composables/useDialog'
import { api } from '../composables/useApi'
const auth = useAuthStore() const auth = useAuthStore()
const oils = useOilsStore() const oils = useOilsStore()
const recipeStore = useRecipesStore() const recipeStore = useRecipesStore()
const ui = useUiStore() const ui = useUiStore()
// Brand data for card
const brand = ref({})
async function loadBrand() {
try { brand.value = await api.get('/api/brand') } catch {}
}
// Modal states // Modal states
const showDilution = ref(false) const showDilution = ref(false)
const showContra = ref(false) const showContra = ref(false)
@@ -637,6 +655,7 @@ async function openOilDetail(name) {
activeCardName.value = name activeCardName.value = name
activeCard.value = card activeCard.value = card
selectedOilName.value = null selectedOilName.value = null
loadBrand()
// Pre-generate card image for instant save // Pre-generate card image for instant save
oilCardImageUrl.value = null oilCardImageUrl.value = null
await nextTick() await nextTick()
@@ -825,8 +844,9 @@ function exportPDF() {
const en = getEnglishName(name) const en = getEnglishName(name)
const bp = meta.bottlePrice != null ? '¥' + meta.bottlePrice.toFixed(0) : '--' const bp = meta.bottlePrice != null ? '¥' + meta.bottlePrice.toFixed(0) : '--'
const rp = meta.retailPrice != null ? '¥' + meta.retailPrice.toFixed(0) : '--' const rp = meta.retailPrice != null ? '¥' + meta.retailPrice.toFixed(0) : '--'
const vol = volumeLabel(meta.dropCount) const vol = volumeLabel(meta.dropCount, name)
const ppd = oils.pricePerDrop(name) ? '¥' + oils.pricePerDrop(name).toFixed(2) : '--' const unit = oilPriceUnit(name)
const ppd = oils.pricePerDrop(name) ? '¥' + oils.pricePerDrop(name).toFixed(2) + '/' + unit : '--'
rows += `<tr> rows += `<tr>
<td>${name}</td> <td>${name}</td>
<td>${en}</td> <td>${en}</td>
@@ -858,7 +878,7 @@ function exportPDF() {
<h1>doTERRA 精油价目表 ${dateStr}</h1> <h1>doTERRA 精油价目表 ${dateStr}</h1>
<table> <table>
<thead> <thead>
<tr><th>精油</th><th>英文名</th><th>会员价</th><th>零售价</th><th>容量</th><th>单价/滴</th></tr> <tr><th>精油</th><th>英文名</th><th>会员价</th><th>零售价</th><th>容量</th><th>单价</th></tr>
</thead> </thead>
<tbody>${rows}</tbody> <tbody>${rows}</tbody>
</table> </table>