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
套装配置支持 bottleCount 字段指定某种油的瓶数倍率 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
160 lines
5.2 KiB
JavaScript
160 lines
5.2 KiB
JavaScript
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,
|
||
}
|
||
}
|