Files
oil-formula-calculator/frontend/src/composables/useKitCost.js
Hera Zhao 9fe6eeaf29
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 12s
Test / e2e-test (push) Failing after 3m40s
feat: 全精油椰子油按实际295mL(2.57瓶)计算成本和可用量
套装配置支持 bottleCount 字段指定某种油的瓶数倍率

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 00:00:45 +00:00

160 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { computed } from 'vue'
import { useOilsStore } from '../stores/oils'
import { useRecipesStore } from '../stores/recipes'
import { KITS } from '../config/kits'
/**
* 套装成本分摊与配方匹配
*
* 分摊逻辑:按各精油原瓶价占比分摊套装总价
* 某精油套装内成本 = (该油原瓶价 / 套装内所有油原瓶价之和) × 套装总价
* 套装内每滴成本 = 套装内成本 / 该油滴数
*/
export function useKitCost() {
const oils = useOilsStore()
const recipeStore = useRecipesStore()
// Resolve kit oil name to system oil name (handles 牛至→西班牙牛至 etc.)
function resolveOilName(kitOilName) {
if (oils.oilsMeta[kitOilName]) return kitOilName
// Try finding system oil that ends with kit name
return oils.oilNames.find(n => n.endsWith(kitOilName) && n !== kitOilName) || kitOilName
}
// Calculate per-drop costs for a kit
function calcKitPerDrop(kit) {
const resolved = kit.oils.map(name => resolveOilName(name))
const bc = kit.bottleCount || {} // e.g. { '椰子油': 2.57 }
// Sum of bottle prices for all oils in kit (accounting for multiple bottles)
let totalBottlePrice = 0
const oilBottlePrices = {}
for (const name of resolved) {
const meta = oils.oilsMeta[name]
const count = bc[name] || 1
const bp = meta ? meta.bottlePrice * count : 0
oilBottlePrices[name] = bp
totalBottlePrice += bp
}
if (totalBottlePrice === 0) return {}
// Proportional allocation — kit accessories treated as freebies,
// so oil cost = min(kit price, sum of bottle prices)
const effectivePrice = Math.min(kit.price, totalBottlePrice)
const perDrop = {}
for (const name of resolved) {
const meta = oils.oilsMeta[name]
const count = bc[name] || 1
const bp = oilBottlePrices[name]
const kitCostForOil = (bp / totalBottlePrice) * effectivePrice
const totalDrops = meta ? meta.dropCount * count : 1
perDrop[name] = totalDrops > 0 ? kitCostForOil / totalDrops : 0
}
return perDrop
}
// Check if a recipe can be made with a kit
function canMakeRecipe(kit, recipe) {
const resolvedSet = new Set(kit.oils.map(name => resolveOilName(name)))
return recipe.ingredients.every(ing => resolvedSet.has(ing.oil))
}
// Calculate recipe cost using kit pricing
function calcRecipeCostWithKit(kitPerDrop, recipe) {
return recipe.ingredients.reduce((sum, ing) => {
const ppd = kitPerDrop[ing.oil] || 0
return sum + ppd * ing.drops
}, 0)
}
// Get all matching recipes for a kit
function getKitRecipes(kit) {
const perDrop = calcKitPerDrop(kit)
return recipeStore.recipes
.filter(r => canMakeRecipe(kit, r))
.map(r => ({
...r,
kitCost: calcRecipeCostWithKit(perDrop, r),
originalCost: oils.calcCost(r.ingredients),
}))
.sort((a, b) => a.name.localeCompare(b.name, 'zh'))
}
// Build full analysis for all kits, sorted by recipe count ascending (fewest recipes first)
const kitAnalysis = computed(() => {
return KITS.map(kit => {
const perDrop = calcKitPerDrop(kit)
const recipes = getKitRecipes(kit)
return {
...kit,
perDrop,
recipes,
recipeCount: recipes.length,
}
}).sort((a, b) => a.recipeCount - b.recipeCount)
})
// Cross-kit comparison: membership-tier style
// Columns: kits ordered by recipe count (fewest→most, from kitAnalysis)
// Rows: recipes available to most kits at top, exclusive recipes at bottom (staircase pattern)
const crossComparison = computed(() => {
const analysis = kitAnalysis.value
// Kit order for staircase: index in sorted analysis (0 = smallest kit)
const kitOrder = analysis.map(ka => ka.id)
const allRecipeIds = new Set()
for (const ka of analysis) {
for (const r of ka.recipes) allRecipeIds.add(r._id)
}
const rows = []
for (const id of allRecipeIds) {
let recipe = null
const costs = {}
let availCount = 0
// Track which kit columns have this recipe (by index in sorted order)
let smallestKitIdx = kitOrder.length
for (let i = 0; i < analysis.length; i++) {
const ka = analysis[i]
const found = ka.recipes.find(r => r._id === id)
if (found) {
if (!recipe) recipe = found
costs[ka.id] = found.kitCost
availCount++
if (i < smallestKitIdx) smallestKitIdx = i
} else {
costs[ka.id] = null
}
}
rows.push({
id,
name: recipe.name,
tags: recipe.tags,
volume: recipe.volume,
ingredients: recipe.ingredients,
originalCost: recipe.originalCost,
costs,
availCount,
smallestKitIdx,
})
}
// Staircase sort: most available first, then by smallest kit that has it, then by name
rows.sort((a, b) => {
if (a.availCount !== b.availCount) return b.availCount - a.availCount
if (a.smallestKitIdx !== b.smallestKitIdx) return a.smallestKitIdx - b.smallestKitIdx
return a.name.localeCompare(b.name, 'zh')
})
return rows
})
return {
KITS,
resolveOilName,
calcKitPerDrop,
canMakeRecipe,
calcRecipeCostWithKit,
getKitRecipes,
kitAnalysis,
crossComparison,
}
}