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

315 lines
11 KiB
Vue
Raw 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 dayjs from 'dayjs'
import { exportLocalUserData, importLocalUserData } from '~/composables/store/userData'
import { promptText, showAlert } from '~/utils/dialog'
import { yunleHaptic } from '~/utils/yunleHost'
const { isDark, toggleDark } = useDarkMode()
const app = useAppStore()
const rStore = useRecipeStore()
definePageMeta({
layout: 'child',
})
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())
}
function exportToFile(): void {
const blob = new Blob([exportText()], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `cook-data-${dayjs().format('YYYYMMDD-HHmm')}.json`
link.click()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
async function copyExport(): Promise<void> {
try {
await navigator.clipboard.writeText(exportText())
void yunleHaptic('success')
await showAlert('已复制', '数据已复制到剪贴板,可以在新设备上用「粘贴导入」恢复。')
}
catch {
await showAlert('复制失败', '当前环境不支持写入剪贴板,请改用「导出为文件」。')
}
}
async function applyImport(text: string): Promise<void> {
const result = importLocalUserData(text)
void yunleHaptic(result ? 'success' : 'heavy')
if (result)
await showAlert('导入完成', `已合并到本机:现有 ${result.favorites} 个收藏、${result.histories} 条历史记录。`)
else
await showAlert('导入失败', '无法识别这份数据,请确认它来自食用手册的导出。')
}
async function importFromFile(event: Event): Promise<void> {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (file)
await applyImport(await file.text())
}
function onToggleChange() {
void yunleHaptic('light')
}
async function importFromPaste(): Promise<void> {
const value = await promptText({ header: '粘贴导入', message: '粘贴之前复制的数据', okText: '导入' })
if (value?.trim())
await applyImport(value)
}
</script>
<template>
<ion-page>
<ion-header :translucent="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button default-href="/my" />
</ion-buttons>
<ion-title>设置</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<div class="ledger-desktop-block">
<PageHead title="设置" subtitle="推荐规则、外观和数据都在这儿调" />
<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>
<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>
<style>
/* (Optional) This is added to prevent the flashing that happens when toggling between palettes */
ion-item {
--transition: none;
}
</style>