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
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:
136
frontend/src/composables/useKitCost.js
Normal file
136
frontend/src/composables/useKitCost.js
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user