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
+270 -110
View File
@@ -1,6 +1,8 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { customRecipes } from '~/composables/store/customRecipe'
import { favoriteEntries } from '~/composables/store/favorite'
import { recipeHistories } from '~/composables/store/history'
import { links } from '~/constants'
definePageMeta({
@@ -39,6 +41,16 @@ const syncText = computed(() => {
}
})
const rStore = useRecipeStore()
const favCount = computed(() => favoriteEntries.value.length)
const historyCount = computed(() => recipeHistories.value.length)
const mineCount = computed(() => customRecipes.value.length)
const syncedText = computed(() => lastSyncedAt.value ? dayjs(lastSyncedAt.value).format('MM-DD HH:mm') : '—')
const pantryText = computed(() => rStore.selectedPantry.join('、') || '无')
const avoidText = computed(() => rStore.selectedAvoid.join('、') || '无')
const spicyText = computed(() => rStore.noSpicy ? '开' : '关')
function handleSyncClick(): void {
if (syncStatus.value === 'error')
syncNow()
@@ -70,122 +82,270 @@ async function handleSignOut(): Promise<void> {
</ion-toolbar>
</ion-header>
<ion-list :inset="true">
<ion-item v-if="isAuthenticated && account" class="my-account" lines="full">
<ion-avatar v-if="account.avatarUrl" slot="start">
<img :alt="account.displayName" :src="account.avatarUrl">
</ion-avatar>
<ion-icon v-else slot="start" class="my-account-placeholder" :icon="ioniconsPersonCircle" />
<ion-label>
<h2>{{ account.displayName }}</h2>
<p>云乐坊账号</p>
</ion-label>
</ion-item>
<ion-item
v-if="isAuthenticated"
:button="syncStatus === 'error'"
:detail="false"
@click="handleSyncClick"
>
<span v-if="syncStatus === 'syncing'" slot="start" class="my-sync-spinner">
<ion-spinner name="crescent" />
</span>
<CookListIcon
v-else
slot="start"
:icon="syncStatus === 'error' ? ioniconsCloudOfflineOutline : ioniconsCloudDoneOutline"
:color="syncStatus === 'error' ? '#ff3b30' : '#34c759'"
/>
<ion-label>
<h2>收藏和历史云同步</h2>
<p>{{ syncText }}</p>
</ion-label>
</ion-item>
<ion-item
v-if="!isAuthenticated"
:button="true"
:disabled="authBusy || Boolean(configurationMessage)"
@click="handleSignIn"
>
<span v-if="authBusy" slot="start" class="my-sync-spinner">
<ion-spinner name="crescent" />
</span>
<CookListIcon v-else slot="start" :icon="ioniconsLogInOutline" color="#007aff" />
<ion-label>
<h2>{{ authBusy ? '正在检查登录状态' : '使用云乐坊账号登录' }}</h2>
<p>安全授权,无需在 Cook 输入密码</p>
</ion-label>
</ion-item>
<ion-item v-if="configurationMessage || errorMessage" lines="none">
<ion-icon slot="start" color="danger" :icon="ioniconsAlertCircleOutline" />
<ion-label class="ion-text-wrap" color="danger">
{{ configurationMessage || errorMessage }}
</ion-label>
</ion-item>
</ion-list>
<div class="ledger-desktop-block">
<PageHead title="我的" subtitle="收藏、历史和自己记的菜谱都在这儿">
<button
v-if="!isAuthenticated"
type="button"
class="ledger-btn is-ghost is-sm"
:disabled="authBusy || Boolean(configurationMessage)"
@click="handleSignIn"
>
<ion-icon :icon="ioniconsLogInOutline" aria-hidden="true" />
<span>{{ authBusy ? '检查中…' : '登录' }}</span>
</button>
</PageHead>
<ion-list :inset="true">
<ion-item router-link="/recipes/history">
<CookListIcon slot="start" :icon="ioniconsTimeOutline" color="#5856d6" />
<ion-label>历史记录</ion-label>
</ion-item>
<ion-item router-link="/recipes/favorites">
<CookListIcon slot="start" :icon="ioniconsStarOutline" color="#ff9500" />
<ion-label>我的收藏</ion-label>
</ion-item>
<ion-item router-link="/recipes/mine">
<CookListIcon slot="start" :icon="ioniconsBookOutline" color="#a2845e" />
<ion-label>我的菜谱</ion-label>
<ion-note v-if="customRecipes.length" slot="end">
{{ customRecipes.length }}
</ion-note>
</ion-item>
</ion-list>
<section class="ledger-section">
<div class="ledger-account">
<span class="avatar">
<img v-if="account?.avatarUrl" :alt="account.displayName" :src="account.avatarUrl">
<ion-icon v-else :icon="ioniconsPersonCircle" aria-hidden="true" />
</span>
<div class="ledger-account-info">
<b>{{ account?.displayName ?? '未登录' }}</b>
<small>{{ isAuthenticated ? '云乐坊账号' : '登录后可以同步收藏和历史' }}</small>
</div>
<button
type="button"
class="ledger-btn is-ghost is-sm"
:disabled="!isAuthenticated || syncStatus === 'syncing'"
@click="handleSyncClick"
>
<ion-icon :icon="ioniconsCloudDoneOutline" aria-hidden="true" />
<span>{{ syncStatus === 'syncing' ? '同步中' : '同步' }}</span>
</button>
</div>
</section>
<ion-list :inset="true">
<ion-item :href="links.githubIssue" target="_blank" :detail-icon="ioniconsOpenOutline">
<CookListIcon slot="start" :icon="ioniconsChatbubbleEllipsesOutline" color="#34c759" />
<ion-label>问题反馈</ion-label>
</ion-item>
<ion-item :href="links.githubDiscussions" target="_blank" :detail-icon="ioniconsOpenOutline">
<CookListIcon slot="start" :icon="ioniconsChatbubblesOutline" color="#30b0c7" />
<ion-label>参与讨论</ion-label>
</ion-item>
<ion-item :href="links.contribute" target="_blank" :detail-icon="ioniconsOpenOutline">
<CookListIcon slot="start" :icon="ioniconsMailOutline" color="#ff2d55" />
<ion-label>菜谱投稿</ion-label>
</ion-item>
</ion-list>
<section class="ledger-section">
<div class="ledger-stat-grid">
<div class="ledger-stat">
<span>收藏</span>
<b>{{ favCount }}</b>
</div>
<div class="ledger-stat">
<span>浏览历史</span>
<b>{{ historyCount }}</b>
</div>
<div class="ledger-stat">
<span>我的菜谱</span>
<b>{{ mineCount }}</b>
</div>
<div class="ledger-stat">
<span>上次同步</span>
<b>{{ syncedText }}</b>
</div>
</div>
</section>
<ion-list :inset="true">
<ion-item router-link="/settings">
<CookListIcon slot="start" :icon="ioniconsSettingsOutline" color="#8e8e93" />
<ion-label>设置</ion-label>
</ion-item>
</ion-list>
<div class="ledger-menu-grid">
<section class="ledger-section">
<div class="ledger-section-head">
<h2>我的菜谱</h2>
</div>
<div class="ledger-menu">
<NuxtLink to="/recipes/favorites">
<span class="ico"><ion-icon :icon="ioniconsStarOutline" aria-hidden="true" /></span>
<span class="label">收藏的菜谱</span>
<span class="value">{{ favCount }}</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</NuxtLink>
<NuxtLink to="/recipes/history">
<span class="ico"><ion-icon :icon="ioniconsTimeOutline" aria-hidden="true" /></span>
<span class="label">浏览历史</span>
<span class="value">{{ historyCount }}</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</NuxtLink>
<NuxtLink to="/recipes/mine">
<span class="ico"><ion-icon :icon="ioniconsBookOutline" aria-hidden="true" /></span>
<span class="label">自己记的菜谱</span>
<span class="value">{{ mineCount }}</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</NuxtLink>
</div>
</section>
<ion-list :inset="true">
<ion-item router-link="/changelog">
<CookListIcon slot="start" :icon="ioniconsDocumentTextOutline" color="#af52de" />
<ion-label>更新日志</ion-label>
</ion-item>
<ion-item router-link="/help">
<CookListIcon slot="start" :icon="ioniconsHelpCircleOutline" color="#007aff" />
<ion-label>帮助</ion-label>
</ion-item>
<ion-item router-link="/about">
<CookListIcon slot="start" :icon="ioniconsInformationCircleOutline" color="#5ac8fa" />
<ion-label>关于</ion-label>
</ion-item>
</ion-list>
<section class="ledger-section">
<div class="ledger-section-head">
<h2>偏好</h2>
</div>
<div class="ledger-menu">
<NuxtLink to="/home">
<span class="ico"><ion-icon :icon="ioniconsRestaurantOutline" aria-hidden="true" /></span>
<span class="label">常备调料</span>
<span class="value">{{ pantryText }}</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</NuxtLink>
<NuxtLink to="/home">
<span class="ico"><ion-icon :icon="ioniconsAlertCircleOutline" aria-hidden="true" /></span>
<span class="label">忌口</span>
<span class="value">{{ avoidText }}</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</NuxtLink>
<NuxtLink to="/home">
<span class="ico"><ion-icon :icon="ioniconsFlameOutline" aria-hidden="true" /></span>
<span class="label">不吃辣</span>
<span class="value">{{ spicyText }}</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</NuxtLink>
</div>
</section>
<ion-list v-if="isAuthenticated" :inset="true">
<ion-item :button="true" :detail="false" :disabled="authBusy" @click="handleSignOut">
<ion-label class="ion-text-center" color="danger">
<section class="ledger-section">
<div class="ledger-section-head">
<h2>应用</h2>
</div>
<div class="ledger-menu">
<NuxtLink to="/settings">
<span class="ico"><ion-icon :icon="ioniconsSettingsOutline" aria-hidden="true" /></span>
<span class="label">设置</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</NuxtLink>
<NuxtLink to="/help">
<span class="ico"><ion-icon :icon="ioniconsHelpCircleOutline" aria-hidden="true" /></span>
<span class="label">帮助</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</NuxtLink>
<NuxtLink to="/about">
<span class="ico"><ion-icon :icon="ioniconsInformationCircleOutline" aria-hidden="true" /></span>
<span class="label">关于</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</NuxtLink>
<a :href="links.githubIssue" target="_blank" rel="noopener">
<span class="ico"><ion-icon :icon="ioniconsChatbubbleEllipsesOutline" aria-hidden="true" /></span>
<span class="label">反馈问题</span>
<span class="chev"><ion-icon :icon="ioniconsChevronForwardOutline" aria-hidden="true" /></span>
</a>
</div>
</section>
</div>
<section v-if="isAuthenticated" class="ledger-section">
<button type="button" class="ledger-btn is-ghost" @click="handleSignOut">
退出登录
</ion-label>
</ion-item>
</ion-list>
</button>
</section>
</div>
<div class="ledger-mobile-only">
<ion-list :inset="true">
<ion-item v-if="isAuthenticated && account" class="my-account" lines="full">
<ion-avatar v-if="account.avatarUrl" slot="start">
<img :alt="account.displayName" :src="account.avatarUrl">
</ion-avatar>
<ion-icon v-else slot="start" class="my-account-placeholder" :icon="ioniconsPersonCircle" />
<ion-label>
<h2>{{ account.displayName }}</h2>
<p>云乐坊账号</p>
</ion-label>
</ion-item>
<ion-item
v-if="isAuthenticated"
:button="syncStatus === 'error'"
:detail="false"
@click="handleSyncClick"
>
<span v-if="syncStatus === 'syncing'" slot="start" class="my-sync-spinner">
<ion-spinner name="crescent" />
</span>
<CookListIcon
v-else
slot="start"
:icon="syncStatus === 'error' ? ioniconsCloudOfflineOutline : ioniconsCloudDoneOutline"
:color="syncStatus === 'error' ? '#ff3b30' : '#34c759'"
/>
<ion-label>
<h2>收藏和历史云同步</h2>
<p>{{ syncText }}</p>
</ion-label>
</ion-item>
<ion-item
v-if="!isAuthenticated"
:button="true"
:disabled="authBusy || Boolean(configurationMessage)"
@click="handleSignIn"
>
<span v-if="authBusy" slot="start" class="my-sync-spinner">
<ion-spinner name="crescent" />
</span>
<CookListIcon v-else slot="start" :icon="ioniconsLogInOutline" color="#007aff" />
<ion-label>
<h2>{{ authBusy ? '正在检查登录状态' : '使用云乐坊账号登录' }}</h2>
<p>安全授权,无需在 Cook 输入密码</p>
</ion-label>
</ion-item>
<ion-item v-if="configurationMessage || errorMessage" lines="none">
<ion-icon slot="start" color="danger" :icon="ioniconsAlertCircleOutline" />
<ion-label class="ion-text-wrap" color="danger">
{{ configurationMessage || errorMessage }}
</ion-label>
</ion-item>
</ion-list>
<ion-list :inset="true">
<ion-item router-link="/recipes/history">
<CookListIcon slot="start" :icon="ioniconsTimeOutline" color="#5856d6" />
<ion-label>历史记录</ion-label>
</ion-item>
<ion-item router-link="/recipes/favorites">
<CookListIcon slot="start" :icon="ioniconsStarOutline" color="#ff9500" />
<ion-label>我的收藏</ion-label>
</ion-item>
<ion-item router-link="/recipes/mine">
<CookListIcon slot="start" :icon="ioniconsBookOutline" color="#a2845e" />
<ion-label>我的菜谱</ion-label>
<ion-note v-if="customRecipes.length" slot="end">
{{ customRecipes.length }}
</ion-note>
</ion-item>
</ion-list>
<ion-list :inset="true">
<ion-item :href="links.githubIssue" target="_blank" :detail-icon="ioniconsOpenOutline">
<CookListIcon slot="start" :icon="ioniconsChatbubbleEllipsesOutline" color="#34c759" />
<ion-label>问题反馈</ion-label>
</ion-item>
<ion-item :href="links.githubDiscussions" target="_blank" :detail-icon="ioniconsOpenOutline">
<CookListIcon slot="start" :icon="ioniconsChatbubblesOutline" color="#30b0c7" />
<ion-label>参与讨论</ion-label>
</ion-item>
<ion-item :href="links.contribute" target="_blank" :detail-icon="ioniconsOpenOutline">
<CookListIcon slot="start" :icon="ioniconsMailOutline" color="#ff2d55" />
<ion-label>菜谱投稿</ion-label>
</ion-item>
</ion-list>
<ion-list :inset="true">
<ion-item router-link="/settings">
<CookListIcon slot="start" :icon="ioniconsSettingsOutline" color="#8e8e93" />
<ion-label>设置</ion-label>
</ion-item>
</ion-list>
<ion-list :inset="true">
<ion-item router-link="/changelog">
<CookListIcon slot="start" :icon="ioniconsDocumentTextOutline" color="#af52de" />
<ion-label>更新日志</ion-label>
</ion-item>
<ion-item router-link="/help">
<CookListIcon slot="start" :icon="ioniconsHelpCircleOutline" color="#007aff" />
<ion-label>帮助</ion-label>
</ion-item>
<ion-item router-link="/about">
<CookListIcon slot="start" :icon="ioniconsInformationCircleOutline" color="#5ac8fa" />
<ion-label>关于</ion-label>
</ion-item>
</ion-list>
<ion-list v-if="isAuthenticated" :inset="true">
<ion-item :button="true" :detail="false" :disabled="authBusy" @click="handleSignOut">
<ion-label class="ion-text-center" color="danger">
退出登录
</ion-label>
</ion-item>
</ion-list>
</div>
</ion-content>
</ion-page>
</template>