Files
toor 8ff7b879b9 refactor: 个人自用,移除登录与云同步全链路
站点只给本人用,云乐坊 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
2026-10-22 15:52:54 +08:00

315 lines
12 KiB
Vue
Raw Permalink 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>