新增 ledger.css 设计令牌与桌面端布局,AppRail 左导航、PageHead 桌面页头、ResultsColumn 常驻结果栏;做菜/菜谱/发现/我的/设置/帮助/关于各页做桌面窄屏双形态适配,窄屏仍走原有底部标签栏与弹层交互。
195 lines
7.1 KiB
Vue
195 lines
7.1 KiB
Vue
<script lang="ts" setup>
|
||
import type { RecipeMatch } from '@yunyoujun/cook'
|
||
import type { SearchMode } from '~/composables/store/recipe'
|
||
import type { RecipeItem } from '~/types'
|
||
import { uniqueStuff } from '@yunyoujun/cook/recommend'
|
||
import { getEmojisFromStuff } from '~/utils'
|
||
|
||
/**
|
||
* 桌面端常驻的结果栏(原型 .cook-results):左栏选食材,右栏一直显示能做的菜。
|
||
* 窄屏由 ResultsPanel 底部弹层承担,这里只在 >=1024px 渲染。
|
||
*/
|
||
const emit = defineEmits<{ detail: [recipe: RecipeItem, match?: RecipeMatch] }>()
|
||
|
||
const rStore = useRecipeStore()
|
||
|
||
const groupLabels = ['齐全', '差一种', '差两种']
|
||
const groups = computed(() => groupLabels.map((label, missing) => ({
|
||
label,
|
||
items: rStore.recipeMatches.filter(match => match.missingStuff.length === missing),
|
||
})))
|
||
|
||
const active = ref(0)
|
||
watch(() => rStore.recipeMatches, () => {
|
||
if (!groups.value[active.value]?.items.length)
|
||
active.value = Math.max(0, groups.value.findIndex(group => group.items.length))
|
||
})
|
||
|
||
const toolMode = computed(() => !rStore.selectedStuff.length && Boolean(rStore.curTool))
|
||
const matches = computed(() => groups.value[active.value]?.items ?? [])
|
||
const toolRecipes = computed(() => rStore.toolRecipes)
|
||
const count = computed(() => rStore.resultCount)
|
||
const hasSelection = computed(() => rStore.selectedStuff.length > 0 || Boolean(rStore.curTool))
|
||
|
||
const modeOptions: { value: SearchMode, label: string }[] = [
|
||
{ value: 'loose', label: '全部推荐' },
|
||
{ value: 'strict', label: '用上所有已选' },
|
||
{ value: 'survival', label: '只看食材齐全' },
|
||
]
|
||
|
||
function onModeChange(event: Event) {
|
||
const value = (event.target as HTMLSelectElement).value as SearchMode
|
||
rStore.setMode(modeOptions.some(option => option.value === value) ? value : 'loose')
|
||
}
|
||
|
||
function addSuggestion(name: string) {
|
||
rStore.addStuff(name)
|
||
}
|
||
|
||
// 结果行和移动端保持一致:缩略图 + 菜名 + 已有食材 + 还缺什么 + 厨具
|
||
function thumbOf(recipe: RecipeItem) {
|
||
const stuff = uniqueStuff(recipe.stuff)
|
||
return recipe.tags?.includes('杂烩') ? '🍲' : getEmojisFromStuff(stuff)[0] ?? '🍽️'
|
||
}
|
||
|
||
function ingredientsOf(recipe: RecipeItem, missing?: string[]) {
|
||
return uniqueStuff(recipe.stuff).filter(name => !missing?.includes(name)).join(' · ')
|
||
}
|
||
|
||
function toolOf(recipe: RecipeItem) {
|
||
return recipe.tools?.[0] === '一口大锅' ? '炒锅' : recipe.tools?.[0]
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="ledger-cook-results" aria-label="可以做的菜">
|
||
<div class="ledger-results-head">
|
||
<h2 class="ledger-results-title">
|
||
{{ hasSelection ? '能做的菜' : '选好食材,看看能做什么' }}
|
||
</h2>
|
||
<p class="ledger-results-sub">
|
||
{{ hasSelection ? '厨具可选,不选也能找菜谱' : '先挑几样手边的食材' }}
|
||
</p>
|
||
|
||
<div v-if="!toolMode" class="ledger-results-toolbar">
|
||
<div class="ledger-segment" role="tablist" aria-label="按缺料分组">
|
||
<button
|
||
v-for="(group, index) in groups"
|
||
:key="group.label"
|
||
type="button"
|
||
role="tab"
|
||
:aria-selected="active === index"
|
||
:class="{ 'is-on': active === index }"
|
||
@click="active = index"
|
||
>
|
||
{{ group.label }}<span class="n">{{ group.items.length }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<select class="ledger-select-pill" aria-label="匹配范围" :value="rStore.curMode" @change="onModeChange">
|
||
<option v-for="option in modeOptions" :key="option.value" :value="option.value">
|
||
{{ option.label }}
|
||
</option>
|
||
</select>
|
||
|
||
<span class="ledger-results-count">{{ count }} 道</span>
|
||
</div>
|
||
<div v-else class="ledger-results-toolbar">
|
||
<span class="ledger-results-count">{{ count }} 道</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ledger-results-scroll">
|
||
<div v-if="rStore.unlockSuggestions.length && !toolMode" class="ledger-unlock">
|
||
<span class="ledger-unlock-label">再补一样</span>
|
||
<div class="ledger-unlock-row">
|
||
<button
|
||
v-for="item in rStore.unlockSuggestions"
|
||
:key="item.name"
|
||
type="button"
|
||
class="ledger-unlock-chip"
|
||
@click="addSuggestion(item.name)"
|
||
>
|
||
<ion-icon :icon="ioniconsAdd" aria-hidden="true" />{{ item.name }}<span>多 {{ item.count }} 道</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="rStore.isSearching" class="ledger-results-scroll" aria-busy="true">
|
||
<div v-for="i in 6" :key="i" class="ledger-dish-row" aria-hidden="true">
|
||
<span class="ledger-dish-thumb" />
|
||
<span class="ledger-dish-main">
|
||
<span class="ledger-skeleton-line" />
|
||
<span class="ledger-skeleton-line is-short" />
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="toolMode" class="ledger-list">
|
||
<button
|
||
v-for="recipe in toolRecipes"
|
||
:key="`${recipe.bv}-${recipe.name}`"
|
||
type="button"
|
||
class="ledger-dish-row"
|
||
@click="emit('detail', recipe)"
|
||
>
|
||
<span class="ledger-dish-thumb" aria-hidden="true">{{ thumbOf(recipe) }}</span>
|
||
<span class="ledger-dish-main">
|
||
<span class="ledger-dish-name">{{ recipe.name }}</span>
|
||
<span class="ledger-dish-meta">{{ ingredientsOf(recipe) }}</span>
|
||
</span>
|
||
<span class="ledger-dish-tool">{{ toolOf(recipe) }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div v-else-if="matches.length" class="ledger-list">
|
||
<button
|
||
v-for="match in matches"
|
||
:key="match.sourceIndex"
|
||
type="button"
|
||
class="ledger-dish-row"
|
||
@click="emit('detail', match.recipe, match)"
|
||
>
|
||
<span class="ledger-dish-thumb" aria-hidden="true">{{ thumbOf(match.recipe) }}</span>
|
||
<span class="ledger-dish-main">
|
||
<span class="ledger-dish-name">{{ match.recipe.name }}</span>
|
||
<span class="ledger-dish-meta">
|
||
<span v-if="match.missingStuff.length" class="ledger-dish-missing">缺 {{ match.missingStuff.join('、') }}</span>
|
||
<span>{{ ingredientsOf(match.recipe, match.missingStuff) }}</span>
|
||
</span>
|
||
</span>
|
||
<span class="ledger-dish-tool">{{ toolOf(match.recipe) }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div v-else class="ledger-results-empty">
|
||
<h3>{{ hasSelection ? '这一组暂时没有菜' : '还没选食材' }}</h3>
|
||
<p>{{ hasSelection ? '换个分组,或者再加几样食材' : '左边挑几样手边的食材,这里就会列出能做的菜' }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="ledger-results-foot">
|
||
只核对菜谱里记录的食材,西红柿算番茄、鸡翅算鸡肉;常备调料只在菜谱写明时算作已有。用量和调味以视频为准。
|
||
</p>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.ledger-results-empty {
|
||
padding: 48px 24px;
|
||
text-align: center;
|
||
color: var(--ledger-text-sub);
|
||
}
|
||
|
||
.ledger-results-empty h3 {
|
||
margin: 0 0 6px;
|
||
font: 600 var(--ledger-t-18) / 1.4 var(--ledger-font-display);
|
||
color: var(--ledger-text);
|
||
}
|
||
|
||
.ledger-results-empty p {
|
||
margin: 0;
|
||
font-size: var(--ledger-t-13);
|
||
}
|
||
</style>
|