feat: 套装方案对比与导出功能
All checks were successful
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 4s
PR Preview / teardown-preview (pull_request) Has been skipped
Test / e2e-test (push) Successful in 2m58s
PR Preview / test (pull_request) Successful in 6s
PR Preview / deploy-preview (pull_request) Successful in 14s

- 新增套装方案对比页面(KitExport.vue),支持4个套装(芳香调理/家庭医生/居家呵护/全精油)的配方匹配和成本对比
- 按各精油原瓶价占比分摊套装总价,计算每滴套装成本
- 支持设置配方售价,自动计算利润率
- Excel导出完整版(含成分明细)和简版,含横向对比sheet
- 抽取套装配置到共享config/kits.js,Inventory页复用
- 修复库存页模糊匹配bug(牛至错误匹配到牛至呵护)
- 修正全精油套装列表(补芫荽叶/加州胡椒/罗马洋甘菊/道格拉斯冷杉/西班牙鼠尾草,修正广藿香/斯里兰卡肉桂皮名称)
- 所有套装加入椰子油

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 22:22:22 +00:00
parent d3e824be5b
commit cbf7294688
6 changed files with 645 additions and 18 deletions

View File

@@ -0,0 +1,136 @@
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))
// Sum of bottle prices for all oils in kit
let totalBottlePrice = 0
const oilBottlePrices = {}
for (const name of resolved) {
const meta = oils.oilsMeta[name]
const bp = meta ? meta.bottlePrice : 0
oilBottlePrices[name] = bp
totalBottlePrice += bp
}
if (totalBottlePrice === 0) return {}
// Proportional allocation
const perDrop = {}
for (const name of resolved) {
const meta = oils.oilsMeta[name]
const bp = oilBottlePrices[name]
const kitCostForOil = (bp / totalBottlePrice) * kit.price
const drops = meta ? meta.dropCount : 1
perDrop[name] = drops > 0 ? kitCostForOil / drops : 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
const kitAnalysis = computed(() => {
return KITS.map(kit => {
const perDrop = calcKitPerDrop(kit)
const recipes = getKitRecipes(kit)
return {
...kit,
perDrop,
recipes,
recipeCount: recipes.length,
}
})
})
// Cross-kit comparison: all recipes that at least one kit can make
const crossComparison = computed(() => {
const analysis = kitAnalysis.value
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) {
// Find recipe info from any kit that has it
let recipe = null
const costs = {}
for (const ka of analysis) {
const found = ka.recipes.find(r => r._id === id)
if (found) {
if (!recipe) recipe = found
costs[ka.id] = found.kitCost
} else {
costs[ka.id] = null // kit can't make this recipe
}
}
rows.push({
id,
name: recipe.name,
tags: recipe.tags,
ingredients: recipe.ingredients,
originalCost: recipe.originalCost,
costs, // { aroma: 12.3, family: null, home3988: 10.8, full: 9.5 }
})
}
rows.sort((a, b) => a.name.localeCompare(b.name, 'zh'))
return rows
})
return {
KITS,
resolveOilName,
calcKitPerDrop,
canMakeRecipe,
calcRecipeCostWithKit,
getKitRecipes,
kitAnalysis,
crossComparison,
}
}