diff --git a/apps/web/app/app.vue b/apps/web/app/app.vue index 45f5875..8b6195a 100644 --- a/apps/web/app/app.vue +++ b/apps/web/app/app.vue @@ -43,6 +43,7 @@ onMounted(() => { --> + diff --git a/apps/web/app/components/ChooseFood.vue b/apps/web/app/components/ChooseFood.vue index 7e23ce9..087b5e5 100644 --- a/apps/web/app/components/ChooseFood.vue +++ b/apps/web/app/components/ChooseFood.vue @@ -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) + + diff --git a/apps/web/app/pages/tabs/library/index.vue b/apps/web/app/pages/tabs/library/index.vue index 37fdf6f..d76e694 100644 --- a/apps/web/app/pages/tabs/library/index.vue +++ b/apps/web/app/pages/tabs/library/index.vue @@ -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') +} diff --git a/apps/web/app/styles/index.ts b/apps/web/app/styles/index.ts index 4e6fd98..1c150f0 100644 --- a/apps/web/app/styles/index.ts +++ b/apps/web/app/styles/index.ts @@ -1,2 +1,4 @@ import '@ionic/vue/css/palettes/dark.class.css' import './ionic.css' +// Warm Kitchen Ledger 设计契约,放在 ionic.css 之后以覆盖 Ionic 变量 +import './ledger.css' diff --git a/apps/web/app/styles/ledger.css b/apps/web/app/styles/ledger.css new file mode 100644 index 0000000..3fa4177 --- /dev/null +++ b/apps/web/app/styles/ledger.css @@ -0,0 +1,1669 @@ +/* ========================================================================== + 食用手册 · Warm Kitchen Ledger 设计契约 + 与 prototype/cook-desktop 的令牌保持一致。 + 令牌全局生效,桌面(>=1024px)才启用左导航 + 双栏的新版布局。 + ========================================================================== */ + +:root { + /* 主色 */ + --ledger-primary: #1d7a45; + --ledger-primary-hover: #348758; + --ledger-primary-soft: #e8f4ec; + --ledger-primary-ink: #14522f; + + /* 强调色 */ + --ledger-accent: #c2410c; + --ledger-accent-soft: #fdf0e6; + + /* 中性 */ + --ledger-bg: #faf7f2; + --ledger-bg-alt: #f2ece1; + --ledger-rail: #f3ede2; + --ledger-surface: #ffffff; + --ledger-surface-sunken: #fdfbf7; + --ledger-text: #23201c; + --ledger-text-sub: #6b635a; + --ledger-text-mute: #9a9186; + --ledger-border: #e8e0d4; + --ledger-border-strong: #d8ccbb; + + /* 语义色 */ + --ledger-success: #3f7d3f; + --ledger-success-soft: #e9f3e9; + --ledger-warning: #b45309; + --ledger-warning-soft: #fef3c7; + --ledger-danger: #b91c1c; + + /* 字体 */ + --ledger-font-display: 'Noto Serif SC', 'Songti SC', 'STSong', 'SimSun', 'Source Han Serif SC', serif; + --ledger-font-body: + -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', 'Segoe UI', sans-serif; + --ledger-font-mono: 'SF Mono', ui-monospace, Consolas, monospace; + + /* 字号 */ + --ledger-t-11: 11px; + --ledger-t-12: 12px; + --ledger-t-13: 13px; + --ledger-t-14: 14px; + --ledger-t-15: 15px; + --ledger-t-16: 16px; + --ledger-t-18: 18px; + --ledger-t-22: 22px; + --ledger-t-28: 28px; + --ledger-t-34: 34px; + + /* 圆角 */ + --ledger-r-sm: 8px; + --ledger-r-md: 12px; + --ledger-r-lg: 18px; + --ledger-r-xl: 24px; + --ledger-r-pill: 999px; + + /* 阴影 */ + --ledger-sh-sm: 0 1px 2px rgba(35, 32, 28, 0.05); + --ledger-sh-md: 0 2px 8px rgba(35, 32, 28, 0.06), 0 1px 2px rgba(35, 32, 28, 0.04); + --ledger-sh-lg: 0 8px 28px rgba(35, 32, 28, 0.12); + + /* 布局 */ + --ledger-rail-w: 240px; + --ledger-content-max: 1180px; + --ledger-content-pad: 32px; +} + +html.dark, +html.ion-palette-dark, +html[data-theme='dark'] { + --ledger-primary: #56b478; + --ledger-primary-hover: #6ac48a; + --ledger-primary-soft: #1d3a28; + --ledger-primary-ink: #9ad9b0; + + --ledger-accent: #e8843c; + --ledger-accent-soft: #3a2214; + + --ledger-bg: #17140f; + --ledger-bg-alt: #211c15; + --ledger-rail: #1b1711; + --ledger-surface: #221d16; + --ledger-surface-sunken: #1a1610; + --ledger-text: #f2ede4; + --ledger-text-sub: #b3a898; + --ledger-text-mute: #8a7f6f; + --ledger-border: #373026; + --ledger-border-strong: #4a4234; + + --ledger-success: #6fbf6f; + --ledger-success-soft: #1e3320; + --ledger-warning: #e0a458; + --ledger-warning-soft: #3a2c15; + --ledger-danger: #e0736e; + + --ledger-sh-sm: 0 1px 2px rgba(0, 0, 0, 0.32); + --ledger-sh-md: 0 2px 8px rgba(0, 0, 0, 0.36), 0 1px 2px rgba(0, 0, 0, 0.28); + --ledger-sh-lg: 0 8px 28px rgba(0, 0, 0, 0.5); +} + +/* ========================================================================== + 基础:暖色纸纹底 + ========================================================================== */ + +html, +body { + background: var(--ledger-bg); + color: var(--ledger-text); + font-family: var(--ledger-font-body); +} + +#app { + background: transparent; +} + +ion-content { + --background: var(--ledger-bg); + --color: var(--ledger-text); +} + +/* 纸纹 + 暖光,只在桌面出现 */ +@media (min-width: 1024px) { + body::before { + position: fixed; + inset: 0; + z-index: 0; + background: + radial-gradient(120% 55% at 50% 0%, rgba(255, 250, 240, 0.85), transparent 60%), + radial-gradient(85% 45% at 100% 100%, rgba(194, 65, 12, 0.045), transparent 58%); + content: ''; + pointer-events: none; + } + + html.dark body::before, + html.ion-palette-dark body::before { + background: + radial-gradient(120% 55% at 50% 0%, rgba(86, 180, 120, 0.07), transparent 60%), + radial-gradient(85% 45% at 100% 100%, rgba(232, 132, 60, 0.05), transparent 58%); + } +} + +/* ========================================================================== + 通用页面骨架(桌面) + ========================================================================== */ + +/* 只在桌面出现的元素(窄屏由 Ionic 顶部栏/底部弹层承担) */ +.ledger-desktop-only { + display: none; +} + +.ledger-desktop-block { + display: none; +} + +/* 只在窄屏出现的元素 */ +.ledger-mobile-only { + display: block; +} + +.ledger-page { + max-width: var(--ledger-content-max); + margin: 0 auto; +} + +/* 注意:这里不要设 display,交给 .ledger-desktop-only 控制显隐, + 否则同级特异度下会覆盖掉窄屏的 display: none */ +.ledger-page-head { + align-items: flex-end; + gap: 16px; + margin-bottom: 24px; +} + +.ledger-page-titles { + flex: 1; + min-width: 0; +} + +.ledger-page-title { + margin: 0; + font: 600 var(--ledger-t-28) / 1.25 var(--ledger-font-display); + letter-spacing: 0.01em; +} + +.ledger-page-sub { + margin: 5px 0 0; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-13); +} + +.ledger-page-actions { + display: flex; + align-items: center; + gap: 8px; + flex: none; +} + +/* 按钮 */ +.ledger-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-height: 44px; + padding: 0 20px; + border: 1px solid transparent; + border-radius: var(--ledger-r-pill); + background: var(--ledger-primary); + color: #fff; + font: 500 var(--ledger-t-15) / 1 var(--ledger-font-body); + text-decoration: none; + white-space: nowrap; + cursor: pointer; + transition: + background-color 0.16s ease, + transform 0.16s ease; +} + +.ledger-btn:hover { + background: var(--ledger-primary-hover); +} + +.ledger-btn:active { + transform: scale(0.97); +} + +.ledger-btn:disabled { + opacity: 0.45; + cursor: not-allowed; + transform: none; +} + +.ledger-btn.is-ghost { + border-color: var(--ledger-border); + background: var(--ledger-surface); + color: var(--ledger-text-sub); +} + +.ledger-btn.is-ghost:hover { + border-color: var(--ledger-border-strong); + background: var(--ledger-bg); + color: var(--ledger-text); +} + +.ledger-btn.is-sm { + min-height: 34px; + padding: 0 14px; + font-size: var(--ledger-t-13); +} + +.ledger-btn ion-icon { + font-size: 17px; +} + +/* 卡片 */ +.ledger-card { + border: 1px solid var(--ledger-border); + border-radius: var(--ledger-r-lg); + background: var(--ledger-surface); + box-shadow: var(--ledger-sh-sm); +} + +.ledger-card-pad { + padding: 20px; +} + +/* 区块 */ +.ledger-section { + margin-bottom: 24px; +} + +.ledger-section-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin: 0 0 12px; +} + +.ledger-section-head h2 { + margin: 0; + font: 600 var(--ledger-t-18) / 1.3 var(--ledger-font-display); +} + +.ledger-section-head .hint { + color: var(--ledger-text-mute); + font-size: var(--ledger-t-13); +} + +/* 分段控件 */ +.ledger-segment { + display: inline-flex; + gap: 2px; + padding: 3px; + border-radius: var(--ledger-r-pill); + background: var(--ledger-bg-alt); +} + +.ledger-segment button { + min-height: 30px; + padding: 0 14px; + border: 0; + border-radius: var(--ledger-r-pill); + background: transparent; + color: var(--ledger-text-sub); + font: 500 var(--ledger-t-13) / 1 var(--ledger-font-body); + white-space: nowrap; + cursor: pointer; + transition: + background-color 0.18s ease, + color 0.18s ease, + box-shadow 0.18s ease; +} + +.ledger-segment button:hover { + color: var(--ledger-text); +} + +.ledger-segment button.is-on { + background: var(--ledger-surface); + color: var(--ledger-primary); + box-shadow: var(--ledger-sh-sm); +} + +.ledger-segment button .n { + margin-left: 4px; + opacity: 0.65; + font-family: var(--ledger-font-mono); + font-variant-numeric: tabular-nums; +} + +/* 搜索框 */ +.ledger-search { + position: relative; + display: flex; + align-items: center; +} + +.ledger-search > ion-icon { + position: absolute; + left: 14px; + z-index: 1; + color: var(--ledger-text-mute); + font-size: 17px; + pointer-events: none; +} + +.ledger-search ion-searchbar { + --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; + --padding-start: 14px; + --padding-end: 14px; + --height: 44px; + border-radius: var(--ledger-r-pill); + background: var(--ledger-surface); +} + +.ledger-search ion-searchbar::part(native-input) { + padding-left: 26px; +} + +/* 列表 */ +.ledger-list { + 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-list-row { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + min-height: 56px; + padding: 8px 16px; + border: 0; + border-bottom: 1px solid var(--ledger-border); + background: transparent; + color: inherit; + text-align: left; + text-decoration: none; + cursor: pointer; + transition: background-color 0.14s ease; +} + +.ledger-list-row:last-child { + border-bottom: 0; +} + +.ledger-list-row:hover { + background: var(--ledger-surface-sunken); +} + +/* 菜单(我的页 / 设置页) */ +.ledger-menu { + 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-menu > a, +.ledger-menu > button { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + min-height: 50px; + padding: 12px 16px; + border: 0; + border-bottom: 1px solid var(--ledger-border); + background: transparent; + color: inherit; + font: 400 var(--ledger-t-15) / 1.4 var(--ledger-font-body); + text-align: left; + text-decoration: none; + cursor: pointer; + transition: background-color 0.14s ease; +} + +.ledger-menu > a:last-child, +.ledger-menu > button:last-child { + border-bottom: 0; +} + +.ledger-menu > a:hover, +.ledger-menu > button:hover { + background: var(--ledger-surface-sunken); +} + +.ledger-menu .ico { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + flex: none; + border-radius: var(--ledger-r-sm); + background: var(--ledger-bg-alt); + color: var(--ledger-text-sub); +} + +.ledger-menu .ico ion-icon { + font-size: 17px; +} + +.ledger-menu .label { + flex: 1; +} + +.ledger-menu .value { + color: var(--ledger-text-mute); + font-size: var(--ledger-t-13); +} + +.ledger-menu .chev { + width: 16px; + height: 16px; + flex: none; + color: var(--ledger-border-strong); +} + +/* 账号卡 */ +.ledger-account { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + border: 1px solid var(--ledger-border); + border-radius: var(--ledger-r-lg); + background: var(--ledger-surface); + box-shadow: var(--ledger-sh-sm); +} + +.ledger-account .avatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: 52px; + height: 52px; + flex: none; + overflow: hidden; + border-radius: 50%; + background: var(--ledger-primary-soft); + color: var(--ledger-primary-ink); +} + +.ledger-account .avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.ledger-account .avatar ion-icon { + font-size: 26px; +} + +.ledger-account-info { + flex: 1; + min-width: 0; +} + +.ledger-account b { + display: block; + font: 600 var(--ledger-t-16) / 1.4 var(--ledger-font-body); +} + +.ledger-account small { + color: var(--ledger-text-mute); + font-size: var(--ledger-t-13); +} + +/* 菜单网格 */ +.ledger-menu-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; + align-items: start; +} + +/* 设置行 */ +.ledger-setting-row { + display: flex; + align-items: center; + gap: 16px; + min-height: 58px; + padding: 12px 20px; + border-bottom: 1px solid var(--ledger-border); +} + +.ledger-setting-row:last-child { + border-bottom: 0; +} + +.ledger-setting-row .label { + flex: 1; +} + +.ledger-setting-row .label b { + display: block; + font: 500 var(--ledger-t-15) / 1.4 var(--ledger-font-body); +} + +.ledger-setting-row .label small { + color: var(--ledger-text-mute); + font-size: var(--ledger-t-12); +} + +.ledger-setting-row .value { + color: var(--ledger-text-mute); + font-size: var(--ledger-t-13); + text-decoration: none; +} + +a.ledger-setting-row .value:hover { + color: var(--ledger-primary); +} + +/* 开关 */ +.ledger-switch { + position: relative; + width: 46px; + height: 28px; + flex: none; + border: 0; + border-radius: var(--ledger-r-pill); + background: var(--ledger-border-strong); + cursor: pointer; + transition: background-color 0.2s ease; +} + +.ledger-switch::after { + position: absolute; + top: 3px; + left: 3px; + width: 22px; + height: 22px; + border-radius: 50%; + background: #fff; + box-shadow: var(--ledger-sh-sm); + content: ''; + transition: transform 0.2s cubic-bezier(0.32, 0.72, 0, 1); +} + +.ledger-switch.is-on { + background: var(--ledger-primary); +} + +.ledger-switch.is-on::after { + transform: translateX(18px); +} + +/* 常见问题 */ +.ledger-faq-item { + border-bottom: 1px solid var(--ledger-border); +} + +.ledger-faq-item:last-child { + border-bottom: 0; +} + +.ledger-faq-q { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + min-height: 54px; + padding: 12px 20px; + border: 0; + background: transparent; + color: inherit; + font: 500 var(--ledger-t-15) / 1.5 var(--ledger-font-body); + text-align: left; + cursor: pointer; +} + +.ledger-faq-q .chev { + width: 16px; + height: 16px; + margin-left: auto; + flex: none; + color: var(--ledger-text-mute); + transition: transform 0.22s ease; +} + +.ledger-faq-item.is-open .ledger-faq-q .chev { + transform: rotate(180deg); +} + +.ledger-faq-a { + padding: 0 20px 16px; + color: var(--ledger-text-sub); + font-size: var(--ledger-t-14); + line-height: 1.8; +} + +/* 横向排列 */ +.ledger-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.ledger-stack > * + * { + margin-top: 16px; +} + +.ledger-step { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.ledger-step .ico { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + flex: none; + border-radius: 50%; + background: var(--ledger-primary-soft); + color: var(--ledger-primary-ink); + font: 600 var(--ledger-t-13) / 1 var(--ledger-font-mono); +} + +.ledger-step b { + display: block; + font: 500 var(--ledger-t-15) / 1.4 var(--ledger-font-body); +} + +.ledger-step span { + color: var(--ledger-text-sub); + font-size: var(--ledger-t-13); +} + +/* 正文 */ +.ledger-prose { + color: var(--ledger-text-sub); + font-size: var(--ledger-t-14); + line-height: 1.85; +} + +.ledger-prose p { + margin: 0 0 12px; +} + +.ledger-prose strong { + color: var(--ledger-text); + font-weight: 600; +} + +/* 英雄卡 */ +.ledger-hero { + position: relative; + overflow: hidden; + padding: 32px; + border-radius: var(--ledger-r-xl); + background: linear-gradient(135deg, #1d7a45, #14522f); + color: #fff; + box-shadow: var(--ledger-sh-md); +} + +.ledger-hero::after { + position: absolute; + top: -50px; + right: -30px; + width: 170px; + height: 170px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.08); + content: ''; +} + +.ledger-hero .kicker { + font-size: var(--ledger-t-13); + opacity: 0.82; +} + +.ledger-hero h2 { + position: relative; + margin: 8px 0 4px; + font: 600 var(--ledger-t-34) / 1.25 var(--ledger-font-display); +} + +.ledger-hero p { + position: relative; + margin: 0 0 20px; + font-size: var(--ledger-t-13); + opacity: 0.85; +} + +.ledger-hero-actions { + position: relative; + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +/* 英雄卡里的次要按钮:半透明白,叠在深色渐变上 */ +.ledger-btn.is-hero { + border-color: rgba(255, 255, 255, 0.28); + background: rgba(255, 255, 255, 0.16); + color: #fff; +} + +.ledger-btn.is-hero:hover { + border-color: rgba(255, 255, 255, 0.4); + background: rgba(255, 255, 255, 0.24); +} + +/* 菜系卡片 */ +.ledger-cuisine-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; +} + +.ledger-cuisine-card { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + border: 1px solid var(--ledger-border); + border-radius: var(--ledger-r-md); + background: var(--ledger-surface); + box-shadow: var(--ledger-sh-sm); + color: inherit; + text-align: left; + cursor: pointer; + transition: + transform 0.16s ease, + border-color 0.16s ease, + box-shadow 0.16s ease; +} + +.ledger-cuisine-card:hover { + border-color: var(--ledger-border-strong); + box-shadow: var(--ledger-sh-md); + transform: translateY(-1px); +} + +.ledger-cuisine-card .dot { + width: 8px; + height: 8px; + flex: none; + border-radius: 50%; + background: var(--ledger-primary); +} + +.ledger-cuisine-card b { + flex: 1; + font: 500 var(--ledger-t-15) / 1.3 var(--ledger-font-body); +} + +.ledger-cuisine-card small { + color: var(--ledger-text-mute); + font: var(--ledger-t-12) / 1.3 var(--ledger-font-mono); + font-variant-numeric: tabular-nums; +} + +/* 卡片网格 */ +.ledger-grid-2 { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 20px; + align-items: start; +} + +/* 统计块 */ +.ledger-stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 12px; +} + +.ledger-stat { + padding: 12px 16px; + border: 1px solid var(--ledger-border); + border-radius: var(--ledger-r-md); + background: var(--ledger-surface); +} + +.ledger-stat span { + display: block; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-11); + letter-spacing: 0.04em; +} + +.ledger-stat b { + display: block; + margin-top: 3px; + font: 500 var(--ledger-t-15) / 1.4 var(--ledger-font-body); +} + +/* ========================================================================== + 桌面壳:左侧固定导航栏 + ========================================================================== */ + +.ledger-rail { + position: fixed; + inset: 0 auto 0 0; + z-index: 60; + display: none; + width: var(--ledger-rail-w); + flex-direction: column; + padding: 24px 12px 16px; + border-right: 1px solid var(--ledger-border); + background: var(--ledger-rail); +} + +.ledger-rail-brand { + display: flex; + align-items: center; + gap: 12px; + padding: 0 12px 24px; +} + +.ledger-rail-mark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + flex: none; + border-radius: var(--ledger-r-md); + background: var(--ledger-primary); + color: #fff; + box-shadow: var(--ledger-sh-sm); +} + +.ledger-rail-mark ion-icon { + font-size: 21px; +} + +.ledger-rail-brand b { + display: block; + font: 600 var(--ledger-t-18) / 1.25 var(--ledger-font-display); + letter-spacing: 0.02em; +} + +.ledger-rail-brand small { + display: block; + margin-top: 2px; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-11); +} + +.ledger-rail-nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.ledger-rail-nav + .ledger-rail-nav { + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid var(--ledger-border); +} + +.ledger-rail-label { + padding: 0 12px 8px; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-11); + letter-spacing: 0.08em; +} + +.ledger-rail-link { + position: relative; + display: flex; + align-items: center; + gap: 12px; + min-height: 40px; + padding: 0 12px; + border: 0; + border-radius: 10px; + background: transparent; + color: var(--ledger-text-sub); + font: 400 var(--ledger-t-14) / 1 var(--ledger-font-body); + text-align: left; + text-decoration: none; + cursor: pointer; + transition: + background-color 0.16s ease, + color 0.16s ease; +} + +.ledger-rail-link:hover { + background: var(--ledger-bg-alt); + color: var(--ledger-text); +} + +.ledger-rail-link ion-icon { + width: 19px; + height: 19px; + flex: none; +} + +.ledger-rail-link.is-on { + background: var(--ledger-primary-soft); + color: var(--ledger-primary-ink); + font-weight: 600; +} + +.ledger-rail-link.is-on::before { + position: absolute; + left: 0; + width: 3px; + height: 18px; + border-radius: 0 3px 3px 0; + background: var(--ledger-primary); + content: ''; +} + +.ledger-rail-foot { + margin-top: auto; + padding-top: 16px; + border-top: 1px solid var(--ledger-border); +} + +.ledger-rail-theme { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + min-height: 38px; + padding: 0 12px; + border: 0; + border-radius: 10px; + background: transparent; + color: var(--ledger-text-sub); + font: 400 var(--ledger-t-13) / 1 var(--ledger-font-body); + text-align: left; + cursor: pointer; + transition: + background-color 0.16s ease, + color 0.16s ease; +} + +.ledger-rail-theme:hover { + background: var(--ledger-bg-alt); + color: var(--ledger-text); +} + +.ledger-rail-theme ion-icon { + font-size: 17px; + flex: none; +} + +.ledger-rail-stat { + padding: 12px 12px 0; + margin: 0; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-11); + line-height: 1.6; +} + +.ledger-rail-stat b { + color: var(--ledger-text-sub); + font-family: var(--ledger-font-mono); + font-variant-numeric: tabular-nums; +} + +/* ========================================================================== + 做菜页:左选食材 / 右结果常驻 + ========================================================================== */ + +.ledger-cook-split { + display: grid; + gap: 24px; + align-items: start; +} + +.ledger-cook-side { + min-width: 0; +} + +.ledger-cook-results { + position: sticky; + top: 32px; + display: flex; + flex-direction: column; + min-width: 0; + max-height: calc(100vh - 64px); + 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-results-head { + flex: none; + padding: 20px 20px 16px; + border-bottom: 1px solid var(--ledger-border); +} + +.ledger-results-title { + margin: 0; + font: 600 var(--ledger-t-22) / 1.3 var(--ledger-font-display); +} + +.ledger-results-sub { + margin: 4px 0 0; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-13); +} + +.ledger-results-toolbar { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-top: 12px; +} + +.ledger-results-count { + margin-left: auto; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-12); + font-family: var(--ledger-font-mono); + font-variant-numeric: tabular-nums; +} + +.ledger-select-pill { + min-height: 30px; + padding: 0 26px 0 11px; + border: 1px solid var(--ledger-border); + border-radius: var(--ledger-r-pill); + background-color: var(--ledger-surface); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239a9186' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 9px center; + color: var(--ledger-text); + font: 500 var(--ledger-t-13) / 1 var(--ledger-font-body); + appearance: none; + cursor: pointer; +} + +.ledger-select-pill:hover { + border-color: var(--ledger-border-strong); +} + +.ledger-results-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; +} + +.ledger-results-foot { + flex: none; + padding: 12px 20px; + margin: 0; + border-top: 1px solid var(--ledger-border); + background: var(--ledger-surface-sunken); + color: var(--ledger-text-mute); + font-size: var(--ledger-t-12); + line-height: 1.6; +} + +/* 「再补一样」 */ +.ledger-unlock { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 20px; + border-bottom: 1px solid var(--ledger-border); + background: var(--ledger-primary-soft); +} + +.ledger-unlock-label { + flex: none; + padding-top: 7px; + color: var(--ledger-primary-ink); + font-size: var(--ledger-t-12); + font-weight: 600; +} + +.ledger-unlock-row { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.ledger-unlock-chip { + display: inline-flex; + align-items: center; + gap: 3px; + height: 30px; + padding: 0 12px 0 8px; + border: 0; + border-radius: var(--ledger-r-pill); + background: var(--ledger-surface); + color: var(--ledger-primary-ink); + font: 500 var(--ledger-t-13) / 1 var(--ledger-font-body); + cursor: pointer; +} + +.ledger-unlock-chip ion-icon { + font-size: 15px; +} + +.ledger-unlock-chip span { + margin-left: 4px; + opacity: 0.65; + font-weight: 400; +} + +/* 结果行 */ +.ledger-dish-row { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + min-height: 60px; + padding: 8px 16px; + border: 0; + border-bottom: 1px solid var(--ledger-border); + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; + transition: background-color 0.14s ease; +} + +.ledger-dish-row:last-child { + border-bottom: 0; +} + +.ledger-dish-row:hover { + background: var(--ledger-surface-sunken); +} + +.ledger-dish-row:hover .ledger-dish-name { + color: var(--ledger-primary); +} + +.ledger-dish-thumb { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + flex: none; + border-radius: var(--ledger-r-md); + background: var(--ledger-bg-alt); + font-size: 22px; +} + +.ledger-dish-main { + flex: 1; + min-width: 0; +} + +.ledger-dish-name { + display: block; + font: 500 var(--ledger-t-15) / 1.35 var(--ledger-font-body); + transition: color 0.16s ease; +} + +.ledger-dish-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-top: 3px; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-12); +} + +.ledger-dish-missing { + color: var(--ledger-accent); + font-weight: 500; +} + +.ledger-dish-tool { + flex: none; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-12); +} + +/* 厨具网格 */ +.ledger-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.ledger-tool-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 8px; +} + +.ledger-tool-card { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + padding: 12px 14px; + border: 1px solid var(--ledger-border); + border-radius: var(--ledger-r-md); + background: var(--ledger-surface); + color: inherit; + text-align: left; + cursor: pointer; + transition: + transform 0.16s ease, + border-color 0.16s ease, + box-shadow 0.16s ease; +} + +.ledger-tool-card:hover { + border-color: var(--ledger-border-strong); + box-shadow: var(--ledger-sh-md); + transform: translateY(-1px); +} + +.ledger-tool-card b { + font: 500 var(--ledger-t-14) / 1.35 var(--ledger-font-body); +} + +.ledger-tool-card small { + color: var(--ledger-text-mute); + font: var(--ledger-t-12) / 1.3 var(--ledger-font-mono); + font-variant-numeric: tabular-nums; +} + +/* 筛选折叠 */ +.ledger-filters { + 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; + align-items: center; + gap: 8px; + width: 100%; + min-height: 48px; + padding: 0 16px; + border: 0; + background: transparent; + color: var(--ledger-text); + font: 500 var(--ledger-t-15) / 1 var(--ledger-font-body); + text-align: left; + cursor: pointer; +} + +.ledger-filter-bar .badge-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + border-radius: var(--ledger-r-pill); + background: var(--ledger-primary); + color: #fff; + font-size: var(--ledger-t-11); + font-weight: 600; + font-family: var(--ledger-font-mono); +} + +.ledger-filter-bar .caret { + margin-left: auto; + color: var(--ledger-text-mute); + font-size: 18px; + transition: transform 0.22s ease; +} + +.ledger-filters.is-open .ledger-filter-bar .caret { + transform: rotate(180deg); +} + +.ledger-filter-body { + display: none; + padding: 0 16px 16px; +} + +.ledger-filters.is-open .ledger-filter-body { + display: block; +} + +.ledger-filter-group + .ledger-filter-group { + margin-top: 16px; +} + +.ledger-filter-group h3 { + margin: 0 0 8px; + color: var(--ledger-text-sub); + font: 500 var(--ledger-t-13) / 1.3 var(--ledger-font-body); +} + +.ledger-active-filters { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed var(--ledger-border); +} + +.ledger-tag-chip { + display: inline-flex; + align-items: center; + gap: 4px; + height: 28px; + padding: 0 8px 0 11px; + border: 0; + border-radius: var(--ledger-r-pill); + background: var(--ledger-primary-soft); + color: var(--ledger-primary-ink); + font: 500 var(--ledger-t-13) / 1 var(--ledger-font-body); + cursor: pointer; +} + +.ledger-tag-chip.is-accent { + background: var(--ledger-accent-soft); + color: var(--ledger-accent); +} + +.ledger-tag-chip ion-icon { + font-size: 13px; + opacity: 0.65; +} + +/* 菜谱表格 */ +.ledger-table-wrap { + border: 1px solid var(--ledger-border); + border-radius: var(--ledger-r-lg); + background: var(--ledger-surface); + box-shadow: var(--ledger-sh-sm); + overflow: auto; +} + +.ledger-table { + width: 100%; + border-collapse: collapse; + font-size: var(--ledger-t-14); +} + +.ledger-table th { + position: sticky; + top: 0; + z-index: 1; + padding: 12px 16px; + background: var(--ledger-bg-alt); + color: var(--ledger-text-sub); + font: 600 var(--ledger-t-12) / 1.3 var(--ledger-font-body); + letter-spacing: 0.04em; + text-align: left; + white-space: nowrap; +} + +.ledger-table td { + padding: 10px 16px; + border-top: 1px solid var(--ledger-border); + vertical-align: middle; +} + +.ledger-table tbody tr { + transition: background-color 0.14s ease; +} + +.ledger-table tbody tr:hover { + background: var(--ledger-surface-sunken); +} + +.ledger-table-dish { + display: flex; + align-items: center; + gap: 12px; +} + +.ledger-table-thumb { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + flex: none; + border-radius: var(--ledger-r-sm); + background: var(--ledger-bg-alt); + font-size: 18px; +} + +.ledger-table-dish b { + font: 500 var(--ledger-t-14) / 1.4 var(--ledger-font-body); +} + +.ledger-table-sub { + color: var(--ledger-text-sub); + font-size: var(--ledger-t-13); +} + +.ledger-table-num { + color: var(--ledger-text-mute); + font-family: var(--ledger-font-mono); + font-variant-numeric: tabular-nums; +} + +/* 菜谱库工具栏 */ +.ledger-toolbar { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 16px; +} + +.ledger-toolbar .ledger-search { + flex: 1; + min-width: 240px; + max-width: 420px; +} + +.ledger-lib-count { + margin-left: auto; + color: var(--ledger-text-mute); + font-size: var(--ledger-t-13); +} + +.ledger-lib-count b { + color: var(--ledger-text); + font-family: var(--ledger-font-mono); + font-variant-numeric: tabular-nums; +} + +.ledger-table-dish-btn { + padding: 0; + border: 0; + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} + +.ledger-table-dish-btn:hover b { + color: var(--ledger-primary); +} + +.ledger-table-more { + display: flex; + justify-content: center; + padding: 16px; +} + +.ledger-star { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border: 0; + border-radius: var(--ledger-r-pill); + background: transparent; + color: var(--ledger-border-strong); + cursor: pointer; + transition: + color 0.16s ease, + background-color 0.16s ease; +} + +.ledger-star:hover { + background: var(--ledger-bg-alt); +} + +.ledger-star.is-on { + color: #d97706; +} + +.ledger-star ion-icon { + font-size: 19px; +} + +/* 空状态 */ +.ledger-empty { + padding: 48px 24px; + text-align: center; +} + +.ledger-empty h3 { + margin: 0 0 6px; + font: 600 var(--ledger-t-18) / 1.4 var(--ledger-font-display); + color: var(--ledger-text); +} + +.ledger-empty p { + margin: 0; + color: var(--ledger-text-sub); + font-size: var(--ledger-t-13); +} + +/* 骨架条 */ +.ledger-skeleton-line { + display: block; + height: 12px; + border-radius: var(--ledger-r-sm); + background: linear-gradient( + 90deg, + var(--ledger-bg-alt) 25%, + var(--ledger-surface-sunken) 37%, + var(--ledger-bg-alt) 63% + ); + background-size: 400% 100%; + animation: ledger-shimmer 1.3s ease infinite; +} + +.ledger-skeleton-line.is-short { + width: 60%; + height: 10px; + margin-top: 8px; +} + +@keyframes ledger-shimmer { + from { + background-position: 100% 0; + } + to { + background-position: 0 0; + } +} + +/* ========================================================================== + 桌面端启用 + ========================================================================== */ + +@media (min-width: 1024px) { + /* 左导航出现,内容整体右移让出位置 */ + .ledger-rail { + display: flex; + } + + .ledger-desktop-only { + display: flex; + } + + .ledger-desktop-block { + display: block; + } + + .ledger-mobile-only { + display: none; + } + + ion-app > ion-router-outlet { + left: var(--ledger-rail-w); + } + + /* 底部 tab 由左导航接手 */ + ion-tab-bar { + display: none !important; + } + + /* 桌面用页面自带的页头,不再需要 Ionic 顶部栏 + 注意:Ionic 把 渲染成
, + 所以这里要用 .ion-page 而不是 ion-page 选择器 */ + .ion-page > ion-header, + ion-content > ion-header { + display: none; + } + + ion-content { + --padding-top: 32px; + --padding-bottom: 48px; + --padding-start: var(--ledger-content-pad); + --padding-end: var(--ledger-content-pad); + } + + ion-content.ledger-content-flush { + --padding-top: 0; + --padding-bottom: 0; + --padding-start: 0; + --padding-end: 0; + } + + .ledger-page-title { + font-size: var(--ledger-t-28); + } + + /* 做菜页:左侧选食材,右侧结果常驻 */ + .ledger-cook-split { + grid-template-columns: minmax(340px, 400px) minmax(0, 1fr); + } + + .ledger-cook-side { + position: sticky; + top: 32px; + max-height: calc(100vh - 64px); + padding-right: 8px; + overflow-y: auto; + overscroll-behavior: contain; + } + + .ledger-cook-side::-webkit-scrollbar { + width: 6px; + } + + .ledger-cook-side::-webkit-scrollbar-thumb { + border-radius: var(--ledger-r-pill); + background: var(--ledger-border-strong); + } +} + +@media (min-width: 1600px) { + :root { + --ledger-content-max: 1280px; + } +}