站点只给本人用,云乐坊 SSO 授权、收藏/历史云同步、获客归因和账号切换记账都没有收益,却被登录状态检查和同步失败提示持续打扰。 - 删除 useYunlefunAuth / yunlefunAuth / yunlefunSsoConfig / yunlefunAuth 适配器、useUserDataSync / userDataSync / userData 适配器、三个 client 插件与 /auth/callback 回调页 - 抽出 adapters/cloudbase.ts,让「反馈一下」和匿名筛选信号不再依赖登录态;cook-feedback 云函数保留 - 删除 cook-user-data 云函数与 cook_user_data 权限规则,移除 @yunlefun/sso 依赖 - 「我的」页删掉登录按钮、账号卡、四个统计格和退出登录;「关于」页删掉项目作者、致谢名单和上游宣传链接 - 本地收藏/历史与导出导入保留,删除同步专用的 lastUid 记账 取舍见 docs/decisions/0005-drop-login-and-sync.md
315 lines
12 KiB
Vue
315 lines
12 KiB
Vue
<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>
|