Files
toor 7408591fac feat: 落地 Warm Kitchen Ledger 原型,宽屏左导航 + 双栏布局
新增 ledger.css 设计令牌与桌面端布局,AppRail 左导航、PageHead 桌面页头、ResultsColumn 常驻结果栏;做菜/菜谱/发现/我的/设置/帮助/关于各页做桌面窄屏双形态适配,窄屏仍走原有底部标签栏与弹层交互。
2026-10-22 03:30:56 +08:00

246 lines
6.6 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import type { IonSearchbarCustomEvent, SearchbarInputEventDetail } from '@ionic/core'
import { uniqueStuff } from '@yunyoujun/cook/recommend'
import { meat, staple, vegetable } from '~/data/food'
import { yunleHaptic } from '~/utils/yunleHost'
/**
* 输入框添加食材 + 已选食材标签(可横向滚动,点一下移除)
*/
const props = defineProps<{ selected: string[] }>()
const emit = defineEmits<{ add: [name: string], remove: [name: string] }>()
const query = ref('')
const message = ref('')
const foods = [...vegetable, ...meat, ...staple]
const suggestions = computed(() => {
const text = query.value.trim()
if (!text)
return []
return foods
.filter(item => !props.selected.includes(item.name) && (item.name.includes(text) || item.alias?.includes(text)))
.slice(0, 6)
})
function onInput(ev: IonSearchbarCustomEvent<SearchbarInputEventDetail>) {
query.value = ev.detail.value ?? ''
message.value = ''
}
function add(text = query.value) {
const names = uniqueStuff([text])
if (!names.length)
return
const fresh = names.filter(name => !props.selected.includes(name))
fresh.forEach(name => emit('add', name))
message.value = fresh.length ? '' : '已经选过了'
if (fresh.length)
void yunleHaptic('light')
query.value = ''
}
function remove(name: string) {
emit('remove', name)
void yunleHaptic('light')
}
</script>
<template>
<div class="ingredient-input">
<ion-searchbar
:value="query"
class="ingredient-input-field"
placeholder="添加食材,如西红柿、鸡蛋"
aria-label="添加食材"
enterkeyhint="done"
:search-icon="ioniconsAddCircleOutline"
show-clear-button="focus"
@ion-input="onInput"
@keydown.enter.prevent="add()"
/>
<div v-if="suggestions.length" class="ingredient-input-row ingredient-input-suggestions" role="listbox" aria-label="食材建议">
<button v-for="item in suggestions" :key="item.name" type="button" class="ingredient-suggestion" role="option" @click="add(item.name)">
<span aria-hidden="true">{{ item.emoji }}</span> {{ item.name }}
</button>
</div>
<p v-else-if="message" class="ingredient-input-message" role="status">
{{ message }}
</p>
<p v-if="!selected.length" class="ingredient-input-empty">
还没选食材 —— 下面挑几样,或者直接搜名字
</p>
<TransitionGroup v-if="selected.length" tag="div" name="ingredient-token" class="ingredient-input-row ingredient-input-tokens" aria-label="已选食材">
<button v-for="name in selected" :key="name" type="button" class="ingredient-token" :aria-label="`移除${name}`" @click="remove(name)">
{{ name }}<ion-icon :icon="ioniconsClose" aria-hidden="true" />
</button>
</TransitionGroup>
</div>
</template>
<style scoped>
.ingredient-input-field {
padding-inline: 12px;
padding-bottom: 0;
}
.ingredient-input-row {
position: relative;
display: flex;
gap: 8px;
padding: 10px 16px 2px;
overflow-x: auto;
scrollbar-width: none;
}
.ingredient-input-row::-webkit-scrollbar {
display: none;
}
.ingredient-token,
.ingredient-suggestion {
display: inline-flex;
flex: none;
align-items: center;
gap: 4px;
height: 30px;
padding: 0 10px 0 12px;
border-radius: 15px;
font-size: 15px;
font-weight: 500;
white-space: nowrap;
}
.ingredient-token {
background: rgba(var(--ion-color-primary-rgb), 0.14);
color: var(--ion-color-primary);
}
.ingredient-token ion-icon {
font-size: 15px;
opacity: 0.7;
}
.ingredient-suggestion {
padding-inline: 12px;
background: var(--cook-fill);
color: var(--ion-text-color, #000);
font-weight: 400;
}
.ingredient-token:active,
.ingredient-suggestion:active {
opacity: 0.6;
}
.ingredient-input-message {
margin: 0;
padding: 8px 32px 0;
font-size: 13px;
color: var(--ion-color-medium);
}
.ingredient-input-empty {
display: none;
}
.ingredient-token-enter-active,
.ingredient-token-leave-active {
transition:
opacity 0.2s ease,
transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ingredient-token-enter-from,
.ingredient-token-leave-to {
opacity: 0;
transform: scale(0.8);
}
.ingredient-token-leave-active {
position: absolute;
}
.ingredient-token-move {
transition: transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@media (min-width: 1024px) {
/* 桌面端:已选食材在上面的托盘里,搜索框在下面 */
.ingredient-input {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.ingredient-input-field {
order: 3;
padding-inline: 0;
padding-bottom: 0;
--background: var(--ledger-surface);
--box-shadow: inset 0 0 0 1px var(--ledger-border);
--border-radius: var(--ledger-r-pill);
--color: var(--ledger-text);
--icon-color: var(--ledger-text-mute);
--placeholder-color: var(--ledger-text-mute);
--placeholder-opacity: 1;
--height: 44px;
border-radius: var(--ledger-r-pill);
background: var(--ledger-surface);
}
.ingredient-input-suggestions {
order: 4;
flex-wrap: wrap;
padding: 0;
overflow: visible;
}
.ingredient-input-message {
order: 5;
padding: 0;
}
.ingredient-input-tokens {
order: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
min-height: 62px;
padding: 12px;
overflow: visible;
border: 1px solid var(--ledger-border);
border-radius: var(--ledger-r-lg);
background: var(--ledger-surface);
box-shadow: var(--ledger-sh-sm);
}
.ingredient-input-empty {
order: 2;
display: flex;
align-items: center;
min-height: 62px;
padding: 12px;
margin: 0;
border: 1px solid var(--ledger-border);
border-radius: var(--ledger-r-lg);
background: var(--ledger-surface);
box-shadow: var(--ledger-sh-sm);
color: var(--ledger-text-mute);
font-size: var(--ledger-t-13);
}
.ingredient-token {
height: 34px;
padding: 0 8px 0 12px;
border-radius: var(--ledger-r-pill);
background: var(--ledger-primary-soft);
color: var(--ledger-primary-ink);
font-size: var(--ledger-t-14);
}
.ingredient-suggestion {
height: 34px;
border: 1px solid var(--ledger-border);
border-radius: var(--ledger-r-pill);
background: var(--ledger-surface);
color: var(--ledger-text);
font-size: var(--ledger-t-14);
}
}
@media (prefers-reduced-motion: reduce) {
.ingredient-token-enter-active,
.ingredient-token-leave-active,
.ingredient-token-move {
transition: opacity 0.15s ease;
}
.ingredient-token-enter-from,
.ingredient-token-leave-to {
transform: none;
}
}
</style>