feat: 落地 Warm Kitchen Ledger 原型,宽屏左导航 + 双栏布局
新增 ledger.css 设计令牌与桌面端布局,AppRail 左导航、PageHead 桌面页头、ResultsColumn 常驻结果栏;做菜/菜谱/发现/我的/设置/帮助/关于各页做桌面窄屏双形态适配,窄屏仍走原有底部标签栏与弹层交互。
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { CUISINE_GROUPS } from '@yunyoujun/cook/recommend'
|
||||
import { recipeHistories } from '~/composables/store/history'
|
||||
import { links } from '~/constants'
|
||||
import { tools } from '~/data/food'
|
||||
import { openRecipe } from '~/utils/recipe'
|
||||
|
||||
definePageMeta({
|
||||
alias: ['/apps'],
|
||||
@@ -8,6 +12,62 @@ definePageMeta({
|
||||
useHead({
|
||||
title: '发现',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const rStore = useRecipeStore()
|
||||
|
||||
const pool = computed(() => rStore.randomPool)
|
||||
const randomIndex = ref(0)
|
||||
const randomRecipe = computed(() => {
|
||||
const list = pool.value
|
||||
return list.length ? list[randomIndex.value % list.length] : undefined
|
||||
})
|
||||
|
||||
function shuffle() {
|
||||
const list = pool.value
|
||||
if (!list.length)
|
||||
return
|
||||
randomIndex.value = Math.floor(Math.random() * list.length)
|
||||
}
|
||||
|
||||
watch(pool, (list) => {
|
||||
if (list.length)
|
||||
randomIndex.value = Math.floor(Math.random() * list.length)
|
||||
}, { immediate: true })
|
||||
|
||||
function openRandom() {
|
||||
if (randomRecipe.value)
|
||||
openRecipe(randomRecipe.value)
|
||||
}
|
||||
|
||||
const cuisineStats = computed(() => CUISINE_GROUPS
|
||||
.map(group => ({
|
||||
label: group.label,
|
||||
count: pool.value.filter(recipe => group.tags.some(tag => recipe.tags?.includes(tag))).length,
|
||||
}))
|
||||
.filter(item => item.count))
|
||||
|
||||
const toolStats = computed(() => tools
|
||||
.map(tool => ({
|
||||
name: tool.name,
|
||||
label: tool.name === '一口大锅' ? '炒锅' : (tool.label || tool.name),
|
||||
count: pool.value.filter(recipe => recipe.tools?.includes(tool.name)).length,
|
||||
}))
|
||||
.filter(item => item.count))
|
||||
|
||||
const recentRecipes = computed(() => recipeHistories.value.slice(0, 8).map(item => item.recipe))
|
||||
|
||||
function pickCuisine(label: string) {
|
||||
if (!rStore.cuisines.includes(label))
|
||||
rStore.toggleCuisine(label)
|
||||
void router.push('/home')
|
||||
}
|
||||
|
||||
function pickTool(name: string) {
|
||||
if (rStore.curTool !== name)
|
||||
rStore.toggleTools(name)
|
||||
void router.push('/home')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,19 +86,113 @@ useHead({
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-list :inset="true">
|
||||
<ion-item router-link="/apps/random">
|
||||
<CookListIcon slot="start" :icon="ioniconsShuffleOutline" color="#ff9500" />
|
||||
<ion-label>今天吃什么</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<div class="ledger-page">
|
||||
<PageHead title="发现" subtitle="不知道做什么的时候,从这里翻翻">
|
||||
<button type="button" class="ledger-btn is-ghost is-sm" @click="shuffle">
|
||||
换一道
|
||||
</button>
|
||||
</PageHead>
|
||||
|
||||
<ion-list :inset="true">
|
||||
<ion-item :href="links.githubIssue" target="_blank" :detail-icon="ioniconsOpenOutline">
|
||||
<CookListIcon slot="start" :icon="ioniconsBulbOutline" color="#34c759" />
|
||||
<ion-label>新功能建议</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<!-- 桌面端 -->
|
||||
<div class="ledger-desktop-block">
|
||||
<section class="ledger-section">
|
||||
<div class="ledger-hero">
|
||||
<span class="kicker">随机推荐</span>
|
||||
<h2>{{ randomRecipe?.name ?? '正在抽一道菜…' }}</h2>
|
||||
<p>{{ randomRecipe ? `用${randomRecipe.stuff.join('、')}就能做,别纠结了,就它了` : '正在准备菜谱库…' }}</p>
|
||||
<div class="ledger-hero-actions">
|
||||
<button type="button" class="ledger-btn" @click="openRandom">
|
||||
<ion-icon :icon="ioniconsBookOutline" aria-hidden="true" />
|
||||
<span>看做法</span>
|
||||
</button>
|
||||
<button type="button" class="ledger-btn is-ghost is-hero" @click="shuffle">
|
||||
<ion-icon :icon="ioniconsRefreshOutline" aria-hidden="true" />
|
||||
<span>换一道</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ledger-section">
|
||||
<div class="ledger-section-head">
|
||||
<h2>按菜系逛</h2>
|
||||
<span class="hint">选一个菜系,回到做菜页按它找</span>
|
||||
</div>
|
||||
<div class="ledger-cuisine-grid">
|
||||
<button
|
||||
v-for="item in cuisineStats"
|
||||
:key="item.label"
|
||||
type="button"
|
||||
class="ledger-cuisine-card"
|
||||
@click="pickCuisine(item.label)"
|
||||
>
|
||||
<span class="dot" aria-hidden="true" />
|
||||
<b>{{ item.label }}</b>
|
||||
<small>{{ item.count }} 道</small>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ledger-section">
|
||||
<div class="ledger-section-head">
|
||||
<h2>按厨具找</h2>
|
||||
<span class="hint">家里有什么锅,就做什么菜</span>
|
||||
</div>
|
||||
<div class="ledger-tool-grid">
|
||||
<button
|
||||
v-for="item in toolStats"
|
||||
:key="item.name"
|
||||
type="button"
|
||||
class="ledger-tool-card"
|
||||
@click="pickTool(item.name)"
|
||||
>
|
||||
<b>{{ item.label }}</b>
|
||||
<small>{{ item.count }} 道</small>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="recentRecipes.length" class="ledger-section">
|
||||
<div class="ledger-section-head">
|
||||
<h2>最近看过的</h2>
|
||||
<span class="hint">点菜名去 B 站看做法</span>
|
||||
</div>
|
||||
<div class="ledger-list">
|
||||
<button
|
||||
v-for="recipe in recentRecipes"
|
||||
:key="recipe.name"
|
||||
type="button"
|
||||
class="ledger-dish-row"
|
||||
@click="openRecipe(recipe)"
|
||||
>
|
||||
<span class="ledger-dish-thumb" aria-hidden="true">{{ recipe.emojis?.[0] ?? '🍽️' }}</span>
|
||||
<span class="ledger-dish-main">
|
||||
<span class="ledger-dish-name">{{ recipe.name }}</span>
|
||||
<span class="ledger-dish-meta">{{ recipe.stuff.join(' · ') }}</span>
|
||||
</span>
|
||||
<span class="ledger-dish-tool">{{ recipe.tools?.[0] }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 移动端 -->
|
||||
<div class="ledger-mobile-only">
|
||||
<ion-list :inset="true">
|
||||
<ion-item router-link="/apps/random">
|
||||
<CookListIcon slot="start" :icon="ioniconsShuffleOutline" color="#ff9500" />
|
||||
<ion-label>今天吃什么</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list :inset="true">
|
||||
<ion-item :href="links.githubIssue" target="_blank" :detail-icon="ioniconsOpenOutline">
|
||||
<CookListIcon slot="start" :icon="ioniconsBulbOutline" color="#34c759" />
|
||||
<ion-label>新功能建议</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
@@ -78,7 +78,12 @@ watchDebounced(
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content ref="contentRef" :fullscreen="true" :style="{ '--padding-bottom': `${panelInset + 16}px` }">
|
||||
<ion-content
|
||||
ref="contentRef"
|
||||
class="cook-content"
|
||||
:fullscreen="true"
|
||||
:style="{ '--cook-panel-inset': `${panelInset}px` }"
|
||||
>
|
||||
<ion-header collapse="condense">
|
||||
<ion-toolbar>
|
||||
<ion-title size="large">
|
||||
@@ -87,10 +92,38 @@ watchDebounced(
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ChooseFood :flight-target="flightTarget" />
|
||||
<div class="ledger-page ledger-cook-page">
|
||||
<PageHead title="今天做点什么" subtitle="选好食材,看看能做什么">
|
||||
<button type="button" class="ledger-btn is-ghost is-sm" :disabled="!hasSelection" @click="reset">
|
||||
清空
|
||||
</button>
|
||||
</PageHead>
|
||||
|
||||
<div class="ledger-cook-split">
|
||||
<div class="ledger-cook-side">
|
||||
<ChooseFood :flight-target="flightTarget" />
|
||||
</div>
|
||||
|
||||
<div class="ledger-desktop-block">
|
||||
<ResultsColumn @detail="openDetail" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<ResultsPanel ref="panelRef" v-model:inset="panelInset" :active="active" @scroll-top="scrollTop" @detail="openDetail" />
|
||||
<RecipeDetailSheet v-model:open="detailOpen" :recipe="detail?.recipe" :match="detail?.match" />
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cook-content {
|
||||
--padding-bottom: calc(var(--cook-panel-inset, 0px) + 16px);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.cook-content {
|
||||
--padding-bottom: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,10 @@ import type { IonSearchbarCustomEvent, SearchbarInputEventDetail } from '@ionic/
|
||||
import type { DbRecipeItem } from '~/utils/db'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { useIndexedDB } from '~/composables/db'
|
||||
import { favoriteRecipeIds } from '~/composables/store/favorite'
|
||||
import { favoriteRecipeIds, toggleFavorite } from '~/composables/store/favorite'
|
||||
import { addRecipeHistory } from '~/composables/store/history'
|
||||
import { db } from '~/utils/db'
|
||||
import { openRecipe } from '~/utils/recipe'
|
||||
import { yunleHaptic } from '~/utils/yunleHost'
|
||||
|
||||
definePageMeta({
|
||||
@@ -78,9 +80,10 @@ function onIdle(cb: () => void) {
|
||||
: window.setTimeout(cb, 32)
|
||||
}
|
||||
function cancelIdle(handle: number) {
|
||||
return typeof cancelIdleCallback === 'function'
|
||||
? cancelIdleCallback(handle)
|
||||
: window.clearTimeout(handle)
|
||||
if (typeof cancelIdleCallback === 'function')
|
||||
cancelIdleCallback(handle)
|
||||
else
|
||||
window.clearTimeout(handle)
|
||||
}
|
||||
|
||||
let idleHandle = 0
|
||||
@@ -91,6 +94,25 @@ watch(displayed, (list) => {
|
||||
idleHandle = onIdle(() => (renderCount.value = list.length))
|
||||
})
|
||||
onBeforeUnmount(() => cancelIdle(idleHandle))
|
||||
|
||||
// ---------- 桌面端表格 ----------
|
||||
/** 一次只渲染一屏的量,往下滚再继续渲染 */
|
||||
const TABLE_CHUNK = 200
|
||||
const tableLimit = ref(TABLE_CHUNK)
|
||||
const tableRows = computed(() => displayed.value.slice(0, tableLimit.value))
|
||||
watch(displayed, () => {
|
||||
tableLimit.value = TABLE_CHUNK
|
||||
})
|
||||
|
||||
function openDish(dish: DbRecipeItem) {
|
||||
addRecipeHistory(dish)
|
||||
openRecipe(dish)
|
||||
}
|
||||
|
||||
function toggleStar(dish: DbRecipeItem) {
|
||||
toggleFavorite(dish)
|
||||
void yunleHaptic('success')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -113,14 +135,6 @@ onBeforeUnmount(() => cancelIdle(idleHandle))
|
||||
菜谱
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
<ion-toolbar>
|
||||
<ion-searchbar
|
||||
placeholder="搜索菜谱"
|
||||
:debounce="150"
|
||||
show-clear-button="focus"
|
||||
@ion-input="onInput"
|
||||
/>
|
||||
</ion-toolbar>
|
||||
<ion-toolbar class="library-segment">
|
||||
<ion-segment
|
||||
:value="view"
|
||||
@@ -136,35 +150,144 @@ onBeforeUnmount(() => cancelIdle(idleHandle))
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-list v-if="loading" aria-busy="true" aria-label="正在加载菜谱">
|
||||
<ion-item v-for="i in 12" :key="i">
|
||||
<ion-label>
|
||||
<ion-skeleton-text :animated="true" :style="{ width: `${40 + (i * 37) % 45}%` }" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<div class="ledger-page">
|
||||
<PageHead title="菜谱库" subtitle="按菜名、食材或厨具翻找所有菜谱">
|
||||
<button type="button" class="ledger-btn is-ghost is-sm" @click="navigateTo('/recipes/new')">
|
||||
新建
|
||||
</button>
|
||||
</PageHead>
|
||||
|
||||
<ion-list v-else-if="displayed.length">
|
||||
<div
|
||||
v-for="(chunk, i) in chunks"
|
||||
:key="i"
|
||||
class="library-chunk"
|
||||
:style="{ containIntrinsicSize: `auto ${chunk.length * 44}px` }"
|
||||
>
|
||||
<RecipeListRow
|
||||
v-for="dish in chunk"
|
||||
:key="dish.id ?? dish.name"
|
||||
class="library-row"
|
||||
:dish="dish"
|
||||
@toggled="onToggled"
|
||||
/>
|
||||
<!-- 桌面端:表格 -->
|
||||
<div class="ledger-desktop-block">
|
||||
<div class="ledger-toolbar">
|
||||
<div class="ledger-search">
|
||||
<ion-searchbar
|
||||
:value="keyword"
|
||||
placeholder="搜菜名,如 番茄炒蛋"
|
||||
aria-label="搜索菜谱"
|
||||
:debounce="150"
|
||||
show-clear-button="focus"
|
||||
@ion-input="onInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="ledger-segment" role="tablist" aria-label="菜谱范围">
|
||||
<button type="button" role="tab" :aria-selected="view === 'all'" :class="{ 'is-on': view === 'all' }" @click="view = 'all'">
|
||||
全部
|
||||
</button>
|
||||
<button type="button" role="tab" :aria-selected="view === 'fav'" :class="{ 'is-on': view === 'fav' }" @click="view = 'fav'">
|
||||
收藏
|
||||
</button>
|
||||
</div>
|
||||
<span class="ledger-lib-count">共 <b>{{ displayed.length }}</b> 道</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="ledger-table-wrap" aria-busy="true">
|
||||
<table class="ledger-table">
|
||||
<tbody>
|
||||
<tr v-for="i in 8" :key="i">
|
||||
<td colspan="6">
|
||||
<span class="ledger-skeleton-line" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tableRows.length" class="ledger-table-wrap">
|
||||
<table class="ledger-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30%;">
|
||||
菜名
|
||||
</th>
|
||||
<th>食材</th>
|
||||
<th>厨具</th>
|
||||
<th>做法</th>
|
||||
<th>难度</th>
|
||||
<th style="width: 48px;" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="dish in tableRows" :key="dish.id ?? dish.name">
|
||||
<td>
|
||||
<button type="button" class="ledger-table-dish ledger-table-dish-btn" @click="openDish(dish)">
|
||||
<span class="ledger-table-thumb" aria-hidden="true">{{ dish.emojis?.[0] ?? '🍽️' }}</span>
|
||||
<b>{{ dish.name }}</b>
|
||||
</button>
|
||||
</td>
|
||||
<td class="ledger-table-sub">
|
||||
{{ dish.stuff.join('、') }}
|
||||
</td>
|
||||
<td class="ledger-table-sub">
|
||||
{{ dish.tools?.join('、') }}
|
||||
</td>
|
||||
<td class="ledger-table-sub">
|
||||
{{ dish.methods?.join('、') }}
|
||||
</td>
|
||||
<td class="ledger-table-num">
|
||||
{{ dish.difficulty || '—' }}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="ledger-star"
|
||||
:class="{ 'is-on': isFav(dish) }"
|
||||
:aria-label="isFav(dish) ? '取消收藏' : '收藏'"
|
||||
:aria-pressed="isFav(dish)"
|
||||
@click="toggleStar(dish)"
|
||||
>
|
||||
<ion-icon :icon="isFav(dish) ? ioniconsStar : ioniconsStarOutline" aria-hidden="true" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="displayed.length > tableLimit" class="ledger-table-more">
|
||||
<button type="button" class="ledger-btn is-ghost is-sm" @click="tableLimit += TABLE_CHUNK">
|
||||
再看 {{ Math.min(TABLE_CHUNK, displayed.length - tableLimit) }} 道(还有 {{ displayed.length - tableLimit }} 道)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="ledger-empty">
|
||||
<h3>{{ keyword ? '没有找到相关菜谱' : '还没有收藏的菜谱' }}</h3>
|
||||
<p>{{ keyword ? '换个关键词试试' : '在列表里点星标即可收藏' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ion-list>
|
||||
|
||||
<div v-else class="cook-empty">
|
||||
<ion-icon :icon="view === 'fav' && !keyword ? ioniconsStarOutline : ioniconsSearchOutline" />
|
||||
<h3>{{ keyword ? '没有找到相关菜谱' : '还没有收藏的菜谱' }}</h3>
|
||||
<p>{{ keyword ? '换个关键词试试' : '左滑菜谱或点星标即可收藏' }}</p>
|
||||
<!-- 移动端:列表 -->
|
||||
<div class="ledger-mobile-only">
|
||||
<ion-list v-if="loading" aria-busy="true" aria-label="正在加载菜谱">
|
||||
<ion-item v-for="i in 12" :key="i">
|
||||
<ion-label>
|
||||
<ion-skeleton-text :animated="true" :style="{ width: `${40 + (i * 37) % 45}%` }" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list v-else-if="displayed.length">
|
||||
<div
|
||||
v-for="(chunk, i) in chunks"
|
||||
:key="i"
|
||||
class="library-chunk"
|
||||
:style="{ containIntrinsicSize: `auto ${chunk.length * 44}px` }"
|
||||
>
|
||||
<RecipeListRow
|
||||
v-for="dish in chunk"
|
||||
:key="dish.id ?? dish.name"
|
||||
class="library-row"
|
||||
:dish="dish"
|
||||
@toggled="onToggled"
|
||||
/>
|
||||
</div>
|
||||
</ion-list>
|
||||
|
||||
<div v-else class="cook-empty">
|
||||
<ion-icon :icon="view === 'fav' && !keyword ? ioniconsStarOutline : ioniconsSearchOutline" />
|
||||
<h3>{{ keyword ? '没有找到相关菜谱' : '还没有收藏的菜谱' }}</h3>
|
||||
<p>{{ keyword ? '换个关键词试试' : '左滑菜谱或点星标即可收藏' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
<ion-toast
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user