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
+221 -54
View File
@@ -7,6 +7,7 @@ import { yunleHaptic } from '~/utils/yunleHost'
const { isDark, toggleDark } = useDarkMode()
const app = useAppStore()
const rStore = useRecipeStore()
definePageMeta({
layout: 'child',
@@ -14,6 +15,15 @@ definePageMeta({
const importInput = ref<HTMLInputElement>()
const modeOptions = [
{ value: 'loose', label: '全部推荐' },
{ value: 'strict', label: '用上所有已选' },
{ value: 'survival', label: '只看食材齐全' },
] as const
const pantryText = computed(() => rStore.selectedPantry.join('、') || '无')
const avoidText = computed(() => rStore.selectedAvoid.join('、') || '无')
function exportText(): string {
return JSON.stringify(exportLocalUserData())
}
@@ -79,62 +89,219 @@ async function importFromPaste(): Promise<void> {
</ion-header>
<ion-content :fullscreen="true">
<h2 class="cook-group-header">
外观
</h2>
<ion-list :inset="true">
<ion-item>
<ion-toggle :checked="isDark" justify="space-between" @ion-change="toggleDark(); onToggleChange()">
暗色模式
</ion-toggle>
</ion-item>
</ion-list>
<div class="ledger-desktop-block">
<PageHead title="设置" subtitle="推荐规则、外观和数据都在这儿调" />
<h2 class="cook-group-header">
数据
</h2>
<ion-list :inset="true">
<ion-item>
<ion-toggle v-model:checked="app.settings.keepLocalData" justify="space-between" @ion-change="onToggleChange">
离开网页后保留选中数据
</ion-toggle>
</ion-item>
</ion-list>
<p class="cook-group-footer">
关闭后,选中的食材、厨具和调料只在这次打开时有效。
</p>
<div class="ledger-menu-grid">
<section class="ledger-section">
<div class="ledger-section-head">
<h2>推荐规则</h2>
<span class="hint">影响做菜页的结果</span>
</div>
<div class="ledger-card">
<div class="ledger-setting-row">
<div class="label">
<b>匹配范围</b>
<small>选完食材后,多宽松算匹配</small>
</div>
<select
class="ledger-select-pill"
aria-label="匹配范围"
:value="rStore.curMode"
@change="rStore.setMode(($event.target as HTMLSelectElement).value as 'loose' | 'strict' | 'survival')"
>
<option v-for="option in modeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<div class="ledger-setting-row">
<div class="label">
<b>不吃辣</b>
<small>排除带「辣」标签的菜谱</small>
</div>
<button
type="button"
class="ledger-switch"
:class="{ 'is-on': rStore.noSpicy }"
role="switch"
:aria-checked="rStore.noSpicy"
aria-label="不吃辣"
@click="rStore.noSpicy = !rStore.noSpicy"
/>
</div>
<div class="ledger-setting-row">
<div class="label">
<b>常备调料</b>
<small>推荐时算作家里已有</small>
</div>
<span class="value">{{ pantryText }}</span>
</div>
<div class="ledger-setting-row">
<div class="label">
<b>忌口</b>
<small>含忌口的菜谱不推荐</small>
</div>
<span class="value">{{ avoidText }}</span>
</div>
</div>
</section>
<h2 class="cook-group-header">
收藏和历史
</h2>
<ion-list :inset="true">
<ion-item :button="true" :detail="false" @click="exportToFile">
<CookListIcon slot="start" :icon="ioniconsDownloadOutline" color="#007aff" />
<ion-label>导出为文件</ion-label>
</ion-item>
<ion-item :button="true" :detail="false" @click="copyExport">
<CookListIcon slot="start" :icon="ioniconsCopyOutline" color="#5856d6" />
<ion-label>复制到剪贴板</ion-label>
</ion-item>
<ion-item :button="true" :detail="false" @click="importInput?.click()">
<CookListIcon slot="start" :icon="ioniconsFolderOpenOutline" color="#ff9500" />
<ion-label>从文件导入</ion-label>
</ion-item>
<ion-item :button="true" :detail="false" @click="importFromPaste">
<CookListIcon slot="start" :icon="ioniconsClipboardOutline" color="#34c759" />
<ion-label>粘贴导入</ion-label>
</ion-item>
</ion-list>
<p class="cook-group-footer">
导入会和本机已有数据合并,不会覆盖。登录云乐坊账号后,收藏和历史会自动同步。
</p>
<input
ref="importInput"
hidden
type="file"
accept="application/json,.json"
@change="importFromFile"
>
<section class="ledger-section">
<div class="ledger-section-head">
<h2>外观</h2>
</div>
<div class="ledger-card">
<div class="ledger-setting-row">
<div class="label">
<b>深色模式</b>
<small>跟随系统或手动切换</small>
</div>
<button
type="button"
class="ledger-switch"
:class="{ 'is-on': isDark }"
role="switch"
:aria-checked="isDark"
aria-label="深色模式"
@click="toggleDark(); onToggleChange()"
/>
</div>
<div class="ledger-setting-row">
<div class="label">
<b>保留选中数据</b>
<small>离开网页后仍记住食材和筛选</small>
</div>
<button
type="button"
class="ledger-switch"
:class="{ 'is-on': app.settings.keepLocalData }"
role="switch"
:aria-checked="app.settings.keepLocalData"
aria-label="保留选中数据"
@click="app.settings.keepLocalData = !app.settings.keepLocalData; onToggleChange()"
/>
</div>
</div>
</section>
<section class="ledger-section">
<div class="ledger-section-head">
<h2>数据</h2>
<span class="hint">都存在这台设备的浏览器里</span>
</div>
<div class="ledger-card">
<div class="ledger-setting-row">
<div class="label">
<b>导出为文件</b>
<small>把收藏和历史存成 JSON</small>
</div>
<button type="button" class="ledger-btn is-ghost is-sm" @click="exportToFile">
导出
</button>
</div>
<div class="ledger-setting-row">
<div class="label">
<b>复制到剪贴板</b>
<small>方便粘贴到别的设备</small>
</div>
<button type="button" class="ledger-btn is-ghost is-sm" @click="copyExport">
复制
</button>
</div>
<div class="ledger-setting-row">
<div class="label">
<b>从文件导入</b>
<small>和本机已有数据合并</small>
</div>
<button type="button" class="ledger-btn is-ghost is-sm" @click="importInput?.click()">
导入
</button>
</div>
<div class="ledger-setting-row">
<div class="label">
<b>粘贴导入</b>
<small>粘贴之前复制的数据</small>
</div>
<button type="button" class="ledger-btn is-ghost is-sm" @click="importFromPaste">
粘贴
</button>
</div>
</div>
</section>
</div>
<section class="ledger-section">
<div class="ledger-section-head">
<h2>数据来源</h2>
</div>
<div class="ledger-card ledger-card-pad">
<div class="ledger-prose">
<p>菜谱来自仓库里的 <strong>packages/data/recipe.json</strong>,共 <strong>{{ rStore.recipesLength }}</strong> 条记录;食材、厨具、别名和相克数据来自同目录下的 <strong>food.json</strong>、<strong>aliases.json</strong>、<strong>incompatible-foods.json</strong>。</p>
<p>推荐规则与正式应用的 <strong>packages/cook/src/utils/recommend.ts</strong> 保持一致:缺料少的排前面,大杂烩排同组末尾。</p>
</div>
</div>
</section>
</div>
<div class="ledger-mobile-only">
<h2 class="cook-group-header">
外观
</h2>
<ion-list :inset="true">
<ion-item>
<ion-toggle :checked="isDark" justify="space-between" @ion-change="toggleDark(); onToggleChange()">
暗色模式
</ion-toggle>
</ion-item>
</ion-list>
<h2 class="cook-group-header">
数据
</h2>
<ion-list :inset="true">
<ion-item>
<ion-toggle v-model:checked="app.settings.keepLocalData" justify="space-between" @ion-change="onToggleChange">
离开网页后保留选中数据
</ion-toggle>
</ion-item>
</ion-list>
<p class="cook-group-footer">
关闭后,选中的食材、厨具和调料只在这次打开时有效。
</p>
<h2 class="cook-group-header">
收藏和历史
</h2>
<ion-list :inset="true">
<ion-item :button="true" :detail="false" @click="exportToFile">
<CookListIcon slot="start" :icon="ioniconsDownloadOutline" color="#007aff" />
<ion-label>导出为文件</ion-label>
</ion-item>
<ion-item :button="true" :detail="false" @click="copyExport">
<CookListIcon slot="start" :icon="ioniconsCopyOutline" color="#5856d6" />
<ion-label>复制到剪贴板</ion-label>
</ion-item>
<ion-item :button="true" :detail="false" @click="importInput?.click()">
<CookListIcon slot="start" :icon="ioniconsFolderOpenOutline" color="#ff9500" />
<ion-label>从文件导入</ion-label>
</ion-item>
<ion-item :button="true" :detail="false" @click="importFromPaste">
<CookListIcon slot="start" :icon="ioniconsClipboardOutline" color="#34c759" />
<ion-label>粘贴导入</ion-label>
</ion-item>
</ion-list>
<p class="cook-group-footer">
导入会和本机已有数据合并,不会覆盖。登录云乐坊账号后,收藏和历史会自动同步。
</p>
<input
ref="importInput"
hidden
type="file"
accept="application/json,.json"
@change="importFromFile"
>
</div>
</ion-content>
</ion-page>
</template>