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>
186 lines
4.6 KiB
JavaScript
186 lines
4.6 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { api } from '../composables/useApi'
|
|
|
|
export const DROPS_PER_ML = 18.6
|
|
|
|
export const VOLUME_DROPS = {
|
|
'单次': null,
|
|
'2.5': 46,
|
|
'5': 93,
|
|
'10': 186,
|
|
'15': 280,
|
|
'115': 2146,
|
|
}
|
|
|
|
export const useOilsStore = defineStore('oils', () => {
|
|
const oils = ref({})
|
|
const oilsMeta = ref({})
|
|
const oilCards = ref({})
|
|
|
|
// Getters
|
|
const oilNames = computed(() =>
|
|
Object.keys(oils.value).sort((a, b) => a.localeCompare(b, 'zh'))
|
|
)
|
|
|
|
function pricePerDrop(name) {
|
|
return oils.value[name] || 0
|
|
}
|
|
|
|
function calcCost(ingredients) {
|
|
return ingredients.reduce((sum, ing) => {
|
|
return sum + pricePerDrop(ing.oil) * ing.drops
|
|
}, 0)
|
|
}
|
|
|
|
function calcRetailCost(ingredients) {
|
|
return ingredients.reduce((sum, ing) => {
|
|
const meta = oilsMeta.value[ing.oil]
|
|
if (meta && meta.retailPrice && meta.dropCount) {
|
|
return sum + (meta.retailPrice / meta.dropCount) * ing.drops
|
|
}
|
|
return sum + pricePerDrop(ing.oil) * ing.drops
|
|
}, 0)
|
|
}
|
|
|
|
function fmtPrice(n) {
|
|
return '¥ ' + n.toFixed(2)
|
|
}
|
|
|
|
function fmtCostWithRetail(ingredients) {
|
|
const cost = calcCost(ingredients)
|
|
const retail = calcRetailCost(ingredients)
|
|
const costStr = fmtPrice(cost)
|
|
const anyRetail = ingredients.some(i => {
|
|
const m = oilsMeta.value[i.oil]
|
|
return m && m.retailPrice && m.dropCount
|
|
})
|
|
if (anyRetail && retail > 0) {
|
|
return { cost: costStr, retail: fmtPrice(retail), hasRetail: true }
|
|
}
|
|
return { cost: costStr, retail: null, hasRetail: false }
|
|
}
|
|
|
|
// Actions
|
|
async function loadOils() {
|
|
const data = await api.get('/api/oils')
|
|
const newOils = {}
|
|
const newMeta = {}
|
|
for (const oil of data) {
|
|
const ppd = oil.drop_count ? oil.bottle_price / oil.drop_count : 0
|
|
newOils[oil.name] = ppd
|
|
newMeta[oil.name] = {
|
|
bottlePrice: oil.bottle_price,
|
|
dropCount: oil.drop_count,
|
|
retailPrice: oil.retail_price ?? null,
|
|
isActive: oil.is_active !== 0,
|
|
enName: oil.en_name ?? null,
|
|
unit: oil.unit || 'drop',
|
|
}
|
|
}
|
|
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, card = null) {
|
|
const payload = {
|
|
name,
|
|
bottle_price: bottlePrice,
|
|
drop_count: dropCount,
|
|
retail_price: retailPrice,
|
|
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()
|
|
}
|
|
|
|
async function deleteOil(name) {
|
|
await api.delete(`/api/oils/${encodeURIComponent(name)}`)
|
|
delete oils.value[name]
|
|
delete oilsMeta.value[name]
|
|
}
|
|
|
|
const UNIT_LABELS = {
|
|
drop: { zh: '滴', en: 'drop', enPlural: 'drops' },
|
|
ml: { zh: 'ml', en: 'ml', enPlural: 'ml' },
|
|
g: { zh: 'g', en: 'g', enPlural: 'g' },
|
|
capsule: { zh: '颗', en: 'capsule', enPlural: 'capsules' },
|
|
}
|
|
|
|
function getUnit(name) {
|
|
const meta = oilsMeta.value[name]
|
|
return (meta && meta.unit) || 'drop'
|
|
}
|
|
|
|
function isDropUnit(name) {
|
|
return getUnit(name) === 'drop'
|
|
}
|
|
|
|
function isMlUnit(name) {
|
|
return getUnit(name) === 'ml'
|
|
}
|
|
|
|
function isPortionUnit(name) {
|
|
return !isDropUnit(name)
|
|
}
|
|
|
|
function unitLabel(name, lang = 'zh') {
|
|
const u = UNIT_LABELS[getUnit(name)] || UNIT_LABELS.drop
|
|
return lang === 'en' ? u.en : u.zh
|
|
}
|
|
|
|
function unitLabelPlural(name, count, lang = 'zh') {
|
|
const u = UNIT_LABELS[getUnit(name)] || UNIT_LABELS.drop
|
|
if (lang === 'en') return count === 1 ? u.en : u.enPlural
|
|
return u.zh
|
|
}
|
|
|
|
return {
|
|
oils,
|
|
oilsMeta,
|
|
oilCards,
|
|
oilNames,
|
|
pricePerDrop,
|
|
calcCost,
|
|
calcRetailCost,
|
|
fmtPrice,
|
|
fmtCostWithRetail,
|
|
loadOils,
|
|
saveOil,
|
|
deleteOil,
|
|
getUnit,
|
|
isDropUnit,
|
|
isMlUnit,
|
|
isPortionUnit,
|
|
unitLabel,
|
|
unitLabelPlural,
|
|
}
|
|
})
|