fix: 精油编辑统一保存到 DB + 导出 Excel 增加功效列
Some checks failed
Test / unit-test (push) Successful in 7s
Test / build-check (push) Successful in 4s
PR Preview / teardown-preview (pull_request) Has been skipped
Test / e2e-test (push) Failing after 1m12s
PR Preview / test (pull_request) Successful in 7s
PR Preview / deploy-preview (pull_request) Successful in 19s

- 新增 oil_cards 表,持久化知识卡片(功效/用法/方法/注意/emoji)
- POST /api/oils 扩展接受 card_* 字段,在同一事务里 upsert oil_cards
- GET /api/oil-cards 返回全部卡片
- 前端 getOilCard 优先查 DB,再 fallback 静态表
- saveEditOil 统一走 saveOil,不再分两套保存
- 精油价目 Excel 导出增加「功效」列
- 首次部署自动从静态 OIL_CARDS 播种到 oil_cards 表

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 10:56:30 +00:00
parent 8f004a02cd
commit c04bb53ddd
5 changed files with 147 additions and 18 deletions

View File

@@ -1,5 +1,6 @@
// Oil knowledge cards - usage guides for common essential oils
// Ported from original vanilla JS implementation
import { useOilsStore } from '../stores/oils'
export const OIL_CARDS = {
'野橘': { emoji: '🍊', en: 'Wild Orange', effects: '安抚镇静、驱散负能量、紧张情绪和压力\n抗氧化、消炎、抗病毒、增强免疫\n提振食欲刺激胆汁分泌促进消化\n促进循环', usage: '日常香薰,提振情绪、舒压,营造愉悦氛围\n饮水加入 1至2 滴,护肝抗病毒\n泡澡时滴 3至4 滴,消除疲劳,放松身心\n扫地、拖地时加入 3至5 滴,清新空气\n涂抹腹部促进消化\n涂抹肝区帮助肝脏排毒\n口腔溃疡时加入水中漱口', method: '🔹香薰 🔸内用 🔺涂抹', caution: '轻微光敏,白天涂抹注意防晒' },
@@ -33,6 +34,17 @@ export const OIL_CARD_ALIAS = {
}
export function getOilCard(name) {
// Check DB-persisted cards first (via store)
try {
const store = useOilsStore()
if (store.oilCards[name]) return store.oilCards[name]
// Check alias in store too
const aliased = OIL_CARD_ALIAS[name]
if (aliased && store.oilCards[aliased]) return store.oilCards[aliased]
} catch {
// Store may not be initialized yet, fall through to static data
}
// Fall back to static OIL_CARDS
if (OIL_CARDS[name]) return OIL_CARDS[name]
if (OIL_CARD_ALIAS[name] && OIL_CARDS[OIL_CARD_ALIAS[name]]) return OIL_CARDS[OIL_CARD_ALIAS[name]]
const base = name.replace(/呵护$/, '')

View File

@@ -16,6 +16,7 @@ export const VOLUME_DROPS = {
export const useOilsStore = defineStore('oils', () => {
const oils = ref({})
const oilsMeta = ref({})
const oilCards = ref({})
// Getters
const oilNames = computed(() =>
@@ -79,9 +80,28 @@ export const useOilsStore = defineStore('oils', () => {
}
oils.value = newOils
oilsMeta.value = newMeta
// Also fetch oil cards from DB
try {
const cards = await api.get('/api/oil-cards')
const newCards = {}
for (const card of cards) {
newCards[card.name] = {
emoji: card.emoji || '',
en: card.en || '',
effects: card.effects || '',
usage: card.usage || '',
method: card.method || '',
caution: card.caution || '',
}
}
oilCards.value = newCards
} catch {
// oil_cards table may not exist yet on older backends
}
}
async function saveOil(name, bottlePrice, dropCount, retailPrice, enName = null, unit = null) {
async function saveOil(name, bottlePrice, dropCount, retailPrice, enName = null, unit = null, card = null) {
const payload = {
name,
bottle_price: bottlePrice,
@@ -90,6 +110,13 @@ export const useOilsStore = defineStore('oils', () => {
en_name: enName,
}
if (unit) payload.unit = unit
if (card) {
payload.card_emoji = card.emoji ?? null
payload.card_effects = card.effects ?? null
payload.card_usage = card.usage ?? null
payload.card_method = card.method ?? null
payload.card_caution = card.caution ?? null
}
await api.post('/api/oils', payload)
await loadOils()
}
@@ -138,6 +165,7 @@ export const useOilsStore = defineStore('oils', () => {
return {
oils,
oilsMeta,
oilCards,
oilNames,
pricePerDrop,
calcCost,

View File

@@ -442,7 +442,7 @@ import { useAuthStore } from '../stores/auth'
import { useUiStore } from '../stores/ui'
import { useRecipesStore } from '../stores/recipes'
import { oilEn } from '../composables/useOilTranslation'
import { getOilCard, setOilCard } from '../composables/useOilCards'
import { getOilCard } from '../composables/useOilCards'
import { showConfirm } from '../composables/useDialog'
import { api } from '../composables/useApi'
import { parseOilProductPaste } from '../composables/useOilProductPaste'
@@ -816,26 +816,24 @@ async function saveEditOil() {
}
const finalDropCount = editUnit.value !== 'drop' ? editProductAmount.value : dropCount
const finalUnit = editUnit.value !== 'drop' ? editProductUnit.value : null
// Build card payload if any card content provided
const hasCard = editCardEffects.value.trim() || editCardUsage.value.trim()
const cardPayload = hasCard ? {
emoji: editCardEmoji.value || '🌿',
effects: editCardEffects.value.trim(),
usage: editCardUsage.value.trim(),
method: editCardMethod.value.trim(),
caution: editCardCaution.value.trim(),
} : null
await oils.saveOil(
newName || oldName,
editBottlePrice.value,
finalDropCount,
editRetailPrice.value,
editOilEnName.value.trim() || null,
finalUnit
finalUnit,
cardPayload
)
// Save knowledge card if any content provided
const finalName = newName || oldName
if (editCardEffects.value.trim() || editCardUsage.value.trim()) {
setOilCard(finalName, {
emoji: editCardEmoji.value || '🌿',
en: editOilEnName.value.trim() || '',
effects: editCardEffects.value.trim(),
usage: editCardUsage.value.trim(),
method: editCardMethod.value.trim(),
caution: editCardCaution.value.trim(),
})
}
cardVersion.value++ // trigger re-render for card badges
ui.showToast('已更新')
editingOilName.value = null
@@ -906,6 +904,7 @@ async function exportExcel() {
const vol = volumeLabel(meta.dropCount, name)
const unit = oilPriceUnit(name)
const ppdNum = oils.pricePerDrop(name)
const card = getOilCard(name)
rows.push({
'精油': name,
'英文名': en,
@@ -913,12 +912,13 @@ async function exportExcel() {
'零售价': meta.retailPrice != null ? Number(meta.retailPrice.toFixed(2)) : '',
'容量': vol,
'单价': ppdNum ? `¥${ppdNum.toFixed(2)}/${unit}` : '',
'功效': card?.effects || '',
'状态': meta.isActive === false ? '下架' : '在售',
})
}
const ws = XLSX.utils.json_to_sheet(rows)
ws['!cols'] = [{ wch: 16 }, { wch: 28 }, { wch: 10 }, { wch: 10 }, { wch: 12 }, { wch: 16 }, { wch: 8 }]
ws['!cols'] = [{ wch: 16 }, { wch: 28 }, { wch: 10 }, { wch: 10 }, { wch: 12 }, { wch: 16 }, { wch: 40 }, { wch: 8 }]
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, '精油价目表')
XLSX.writeFile(wb, `精油价目表${dateStr}.xlsx`)