feat: 横向对比改为会员阶梯式排列
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 4s
PR Preview / deploy-preview (pull_request) Successful in 12s
Test / e2e-test (push) Has been cancelled

- 列按套装可做配方数从少到多排(小套装左,大套装右)
- 行按可用套装数从多到少排(所有套装都能做的在上,独占的在下)
- 形成左上到右下的阶梯视觉效果
- 有值格子绿色背景,无值格子灰色,区分更明显

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 23:18:15 +00:00
parent 750b247b5b
commit ccd3607e35
2 changed files with 26 additions and 14 deletions

View File

@@ -74,7 +74,7 @@ export function useKitCost() {
.sort((a, b) => a.name.localeCompare(b.name, 'zh')) .sort((a, b) => a.name.localeCompare(b.name, 'zh'))
} }
// Build full analysis for all kits // Build full analysis for all kits, sorted by recipe count ascending (fewest recipes first)
const kitAnalysis = computed(() => { const kitAnalysis = computed(() => {
return KITS.map(kit => { return KITS.map(kit => {
const perDrop = calcKitPerDrop(kit) const perDrop = calcKitPerDrop(kit)
@@ -85,12 +85,17 @@ export function useKitCost() {
recipes, recipes,
recipeCount: recipes.length, recipeCount: recipes.length,
} }
}) }).sort((a, b) => a.recipeCount - b.recipeCount)
}) })
// Cross-kit comparison: all recipes that at least one kit can make // 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 crossComparison = computed(() => {
const analysis = kitAnalysis.value 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() const allRecipeIds = new Set()
for (const ka of analysis) { for (const ka of analysis) {
for (const r of ka.recipes) allRecipeIds.add(r._id) for (const r of ka.recipes) allRecipeIds.add(r._id)
@@ -98,16 +103,21 @@ export function useKitCost() {
const rows = [] const rows = []
for (const id of allRecipeIds) { for (const id of allRecipeIds) {
// Find recipe info from any kit that has it
let recipe = null let recipe = null
const costs = {} const costs = {}
for (const ka of analysis) { 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) const found = ka.recipes.find(r => r._id === id)
if (found) { if (found) {
if (!recipe) recipe = found if (!recipe) recipe = found
costs[ka.id] = found.kitCost costs[ka.id] = found.kitCost
availCount++
if (i < smallestKitIdx) smallestKitIdx = i
} else { } else {
costs[ka.id] = null // kit can't make this recipe costs[ka.id] = null
} }
} }
rows.push({ rows.push({
@@ -116,14 +126,15 @@ export function useKitCost() {
tags: recipe.tags, tags: recipe.tags,
ingredients: recipe.ingredients, ingredients: recipe.ingredients,
originalCost: recipe.originalCost, originalCost: recipe.originalCost,
costs, // { aroma: 12.3, family: null, home3988: 10.8, full: 9.5 } costs,
availCount,
smallestKitIdx,
}) })
} }
// Sort by number of kits that can make the recipe (fewest first), then by name // Staircase sort: most available first, then by smallest kit that has it, then by name
rows.sort((a, b) => { rows.sort((a, b) => {
const aCount = Object.values(a.costs).filter(v => v != null).length if (a.availCount !== b.availCount) return b.availCount - a.availCount
const bCount = Object.values(b.costs).filter(v => v != null).length if (a.smallestKitIdx !== b.smallestKitIdx) return a.smallestKitIdx - b.smallestKitIdx
if (aCount !== bCount) return aCount - bCount
return a.name.localeCompare(b.name, 'zh') return a.name.localeCompare(b.name, 'zh')
}) })
return rows return rows

View File

@@ -84,7 +84,7 @@
<tbody> <tbody>
<tr v-for="row in crossComparison" :key="row.id"> <tr v-for="row in crossComparison" :key="row.id">
<td class="td-name">{{ row.name }}</td> <td class="td-name">{{ row.name }}</td>
<td v-for="ka in kitAnalysis" :key="ka.id" class="td-kit-cost"> <td v-for="ka in kitAnalysis" :key="ka.id" :class="row.costs[ka.id] != null ? 'td-kit-available' : 'td-kit-na'">
<template v-if="row.costs[ka.id] != null">{{ fmtPrice(row.costs[ka.id]) }}</template> <template v-if="row.costs[ka.id] != null">{{ fmtPrice(row.costs[ka.id]) }}</template>
<template v-else><span class="na"></span></template> <template v-else><span class="na"></span></template>
</td> </td>
@@ -404,8 +404,9 @@ async function exportExcel(mode) {
.td-cost.original { color: #999; font-weight: 400; } .td-cost.original { color: #999; font-weight: 400; }
.td-profit { font-weight: 600; color: #4a9d7e; } .td-profit { font-weight: 600; color: #4a9d7e; }
.td-profit.negative { color: #ef5350; } .td-profit.negative { color: #ef5350; }
.td-kit-cost { font-weight: 500; } .td-kit-available { font-weight: 500; color: #4a9d7e; background: #f0faf5; }
.na { color: #ccc; } .td-kit-na { background: #fafafa; }
.na { color: #ddd; }
.price-input-wrap { .price-input-wrap {
display: inline-flex; align-items: center; gap: 2px; font-size: 13px; color: #3e3a44; display: inline-flex; align-items: center; gap: 2px; font-size: 13px; color: #3e3a44;