feat: 落地 Warm Kitchen Ledger 原型,宽屏左导航 + 双栏布局

新增 ledger.css 设计令牌与桌面端布局,AppRail 左导航、PageHead 桌面页头、ResultsColumn 常驻结果栏;做菜/菜谱/发现/我的/设置/帮助/关于各页做桌面窄屏双形态适配,窄屏仍走原有底部标签栏与弹层交互。
This commit is contained in:
toor
2026-10-22 03:30:56 +08:00
committed by trae-solo-agent
parent 7049eb02a8
commit 7408591fac
18 changed files with 3604 additions and 383 deletions
+235 -31
View File
@@ -5,6 +5,7 @@ import { CUISINE_GROUPS, DIFFICULTY_LEVELS, METHOD_GROUPS } from '@yunyoujun/coo
import { useEmojiAnimation } from '~/composables/animation'
import { useIncompatibleFoods } from '~/composables/incompatible-foods'
import { meat, staple, tools, vegetable } from '~/data/food'
import { pantryOptions } from '~/data/pantry'
import { trackCookEvent } from '~/utils/analytics'
import { yunleHaptic } from '~/utils/yunleHost'
@@ -17,6 +18,8 @@ const rStore = useRecipeStore()
const selected = computed(() => rStore.selectedStuff)
const pantryOpen = ref(false)
const avoidOpen = ref(false)
/** 桌面端「筛选条件」默认收起 */
const filtersOpen = ref(false)
// 食物相克检测
const { warningMessage, hasWarning, checkIncompatibility } = useIncompatibleFoods()
@@ -54,6 +57,8 @@ function clickTool(item: StuffItem) {
rStore.clickTool(item)
void yunleHaptic('light')
}
const filterCount = computed(() => rStore.methodGroups.length + rStore.cuisines.length + rStore.difficulty.length)
</script>
<template>
@@ -71,7 +76,7 @@ function clickTool(item: StuffItem) {
/>
</IngredientSection>
<IngredientSection title="厨具(可选)" :note="rStore.curTool ? '只看用它做的菜' : ''">
<IngredientSection title="厨具(可选)" :note="rStore.curTool ? '只看用它做的菜' : ''" grid>
<IngredientChip
v-for="item in tools"
:key="item.name"
@@ -82,39 +87,130 @@ function clickTool(item: StuffItem) {
/>
</IngredientSection>
<IngredientSection title="做法(可选)" :note="rStore.methodGroups.length ? '只看这些做法' : ''">
<IngredientChip
v-for="group in METHOD_GROUPS"
:key="group.label"
:label="group.label"
:selected="rStore.methodGroups.includes(group.label)"
@click="rStore.toggleMethodGroup(group.label)"
/>
</IngredientSection>
<div class="choose-food-filters">
<button
type="button"
class="ledger-filter-bar"
:aria-expanded="filtersOpen"
@click="filtersOpen = !filtersOpen"
>
<span>筛选条件</span>
<span v-if="filterCount" class="badge-count">{{ filterCount }}</span>
<ion-icon class="caret" :icon="ioniconsChevronDown" aria-hidden="true" />
</button>
<IngredientSection title="菜系(可选)" :note="rStore.cuisines.length ? '只看这些菜系' : ''">
<IngredientChip
v-for="group in CUISINE_GROUPS"
:key="group.label"
:label="group.label"
:selected="rStore.cuisines.includes(group.label)"
@click="rStore.toggleCuisine(group.label)"
/>
</IngredientSection>
<div class="ledger-filter-body" :class="{ 'is-open': filtersOpen }">
<div class="ledger-filter-group">
<h3>
做法<span class="ledger-filter-hint">(可选)</span>
</h3>
<div class="ledger-chips">
<IngredientChip
v-for="group in METHOD_GROUPS"
:key="group.label"
:label="group.label"
:selected="rStore.methodGroups.includes(group.label)"
@click="rStore.toggleMethodGroup(group.label)"
/>
</div>
</div>
<IngredientSection title="难度(可选)" :note="rStore.difficulty.length ? '没标难度的菜会被隐藏' : ''">
<IngredientChip
v-for="level in DIFFICULTY_LEVELS"
:key="level"
:label="level"
:selected="rStore.difficulty.includes(level)"
@click="rStore.toggleDifficulty(level)"
/>
</IngredientSection>
<div class="ledger-filter-group">
<h3>
菜系<span class="ledger-filter-hint">(可选)</span>
</h3>
<div class="ledger-chips">
<IngredientChip
v-for="group in CUISINE_GROUPS"
:key="group.label"
:label="group.label"
:selected="rStore.cuisines.includes(group.label)"
@click="rStore.toggleCuisine(group.label)"
/>
</div>
</div>
<h2 class="cook-group-header">
调料和忌口
</h2>
<div class="ledger-filter-group">
<h3>
难度<span class="ledger-filter-hint">(可选)</span>
</h3>
<div class="ledger-chips">
<IngredientChip
v-for="level in DIFFICULTY_LEVELS"
:key="level"
:label="level"
:selected="rStore.difficulty.includes(level)"
@click="rStore.toggleDifficulty(level)"
/>
</div>
</div>
<div v-if="filterCount || rStore.curTool" class="ledger-active-filters">
<button v-if="rStore.curTool" type="button" class="ledger-tag-chip" @click="rStore.toggleTools(rStore.curTool)">
厨具:{{ rStore.curTool }}<ion-icon :icon="ioniconsClose" aria-hidden="true" />
</button>
<button v-for="label in rStore.methodGroups" :key="label" type="button" class="ledger-tag-chip" @click="rStore.toggleMethodGroup(label)">
做法:{{ label }}<ion-icon :icon="ioniconsClose" aria-hidden="true" />
</button>
<button v-for="label in rStore.cuisines" :key="label" type="button" class="ledger-tag-chip" @click="rStore.toggleCuisine(label)">
菜系:{{ label }}<ion-icon :icon="ioniconsClose" aria-hidden="true" />
</button>
<button v-for="level in rStore.difficulty" :key="level" type="button" class="ledger-tag-chip is-accent" @click="rStore.toggleDifficulty(level)">
难度:{{ level }}<ion-icon :icon="ioniconsClose" aria-hidden="true" />
</button>
</div>
</div>
</div>
<!-- 桌面端:调料和忌口直接铺在卡片里 -->
<section class="ledger-desktop-block choose-food-seasoning">
<div class="ledger-section-head">
<h2>调料和忌口</h2>
<span class="hint">常备调料算作已有</span>
</div>
<div class="ledger-card ledger-card-pad">
<div class="choose-food-stack">
<div>
<h3 class="choose-food-sub-title">
家里常备
</h3>
<div class="ledger-chips">
<IngredientChip
v-for="name in pantryOptions"
:key="name"
:label="name"
:selected="rStore.selectedPantry.includes(name)"
@click="rStore.togglePantry(name)"
/>
</div>
</div>
<div>
<h3 class="choose-food-sub-title">
忌口
</h3>
<div class="ledger-chips">
<span v-for="name in rStore.selectedAvoid" :key="name" class="ledger-tag-chip is-accent">
{{ name }}
<button type="button" class="choose-food-tag-remove" :aria-label="`移除忌口${name}`" @click="rStore.toggleAvoid(name)">
<ion-icon :icon="ioniconsClose" aria-hidden="true" />
</button>
</span>
<button type="button" class="ledger-btn is-ghost is-sm" @click="avoidOpen = true">
选择忌口
</button>
</div>
</div>
<div class="choose-food-toggle">
<span class="label">不吃辣</span>
<ion-toggle v-model="rStore.noSpicy" justify="space-between" />
</div>
</div>
</div>
</section>
<!-- 移动端:调料和忌口用 iOS 分组列表 -->
<ion-list :inset="true" class="choose-food-pantry">
<ion-item :button="true" @click="pantryOpen = true">
<ion-label>家里常备</ion-label>
@@ -172,6 +268,114 @@ function clickTool(item: StuffItem) {
text-overflow: ellipsis;
font-size: 17px;
}
/* 筛选条件:窄屏是一组 iOS 分组,桌面收进一张可折叠的卡片 */
.ledger-filter-bar {
display: none;
}
.ledger-filter-body {
display: block;
}
.ledger-filter-group {
margin-bottom: 8px;
}
.ledger-filter-group h3 {
margin: 24px 0 0;
padding: 0 calc(16px + 20px) 6px;
font-size: 13px;
font-weight: 400;
line-height: 18px;
color: var(--ion-color-step-600, #6d6d72);
}
.ledger-filter-group .ledger-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0 16px;
padding: 12px;
border-radius: 10px;
background: var(--ion-item-background, #fff);
}
.ledger-active-filters {
display: none;
}
@media (min-width: 1024px) {
/* 桌面端用原型的筛选卡片 */
.choose-food-filters {
margin-bottom: 24px;
border: 1px solid var(--ledger-border);
border-radius: var(--ledger-r-lg);
background: var(--ledger-surface);
box-shadow: var(--ledger-sh-sm);
overflow: hidden;
}
.ledger-filter-bar {
display: flex;
}
.ledger-filter-body {
display: none;
padding: 0 16px 16px;
}
.ledger-filter-body.is-open {
display: block;
}
.ledger-filter-group h3 {
margin: 16px 0 8px;
padding: 0;
color: var(--ledger-text-sub);
font: 500 var(--ledger-t-13) / 1.3 var(--ledger-font-body);
}
.ledger-filter-group .ledger-chips {
margin: 0;
padding: 0;
background: transparent;
}
.ledger-filter-hint {
display: none;
}
.ledger-active-filters {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--ledger-border);
}
/* 桌面端不再需要 iOS 分组列表 */
.choose-food-pantry {
display: none;
}
.choose-food-stack > * + * {
margin-top: 12px;
}
.choose-food-sub-title {
margin: 0 0 8px;
color: var(--ledger-text-sub);
font: 500 var(--ledger-t-13) / 1.3 var(--ledger-font-body);
}
.choose-food-toggle {
display: flex;
align-items: center;
gap: 12px;
min-height: 40px;
}
.choose-food-toggle .label {
flex: 1;
font-size: var(--ledger-t-14);
}
.choose-food-tag-remove {
display: inline-flex;
align-items: center;
padding: 0;
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
}
}
</style>
<style>