chore: 导入清理前的完整工程状态(含 Capacitor 原生壳,作为回滚备份点)

This commit is contained in:
toor
2026-10-21 11:50:10 +08:00
committed by trae-solo-agent
commit 8ded87a3a8
389 changed files with 52242 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
<script lang="ts" setup>
import type { CustomRecipe } from '~/composables/store/customRecipe'
import type { StuffItem } from '~/types'
import { onIonViewWillEnter } from '@ionic/vue'
import { uniqueStuff } from '@yunyoujun/cook/recommend'
import { getCustomRecipe, removeCustomRecipe, saveCustomRecipe } from '~/composables/store/customRecipe'
import { meat, staple, tools, vegetable } from '~/data/food'
import { buildCustomRecipe, CUSTOM_RECIPE_NAME_MAX } from '~/utils/customRecipe'
import { confirmDestructive } from '~/utils/dialog'
import { getRecipeVideoLink } from '~/utils/recipe'
import { yunleHaptic } from '~/utils/yunleHost'
definePageMeta({
layout: 'child',
title: '添加菜谱',
})
/**
* 添加 / 编辑自己的菜谱:带 ?id= 时编辑已有的那道
*/
const router = useRouter()
const ionRouter = useIonRouter()
const existing = shallowRef<CustomRecipe>()
const name = ref('')
const stuff = ref<string[]>([])
const selectedTools = ref<string[]>([])
const source = ref('')
const note = ref('')
const error = ref('')
// Ionic 会复用路由栈里的页面,所以每次进入都按当前地址重新填表;
// 在 ion-router-outlet 里 useRoute 读不到这一页的 ?id=,直接读 router
function fillForm() {
const id = router.currentRoute.value.query.id
const recipe = typeof id === 'string' ? getCustomRecipe(id) : undefined
existing.value = recipe
name.value = recipe?.name ?? ''
stuff.value = recipe ? [...recipe.stuff] : []
selectedTools.value = recipe ? [...recipe.tools] : []
source.value = recipe ? getRecipeVideoLink(recipe) : ''
note.value = recipe?.note ?? ''
error.value = ''
}
fillForm()
onIonViewWillEnter(fillForm)
const sections = [
{ title: '蔬菜', items: vegetable },
{ title: '肉和蛋', items: meat },
{ title: '主食', items: staple },
]
function selectedNote(items: StuffItem[]) {
const count = items.filter(item => stuff.value.includes(item.name)).length
return count ? `已选 ${count} 种` : ''
}
function addStuff(text: string) {
stuff.value = uniqueStuff([...stuff.value, text])
error.value = ''
}
function toggleStuff(item: string) {
if (stuff.value.includes(item))
stuff.value = stuff.value.filter(name => name !== item)
else
addStuff(item)
void yunleHaptic('light')
}
function toggleTool(item: string) {
selectedTools.value = selectedTools.value.includes(item)
? selectedTools.value.filter(name => name !== item)
: [...selectedTools.value, item]
void yunleHaptic('light')
}
function save() {
const result = buildCustomRecipe({
name: name.value,
stuff: stuff.value,
tools: selectedTools.value,
source: source.value,
note: note.value,
}, existing.value)
if ('error' in result) {
error.value = result.error
void yunleHaptic('heavy')
return
}
saveCustomRecipe(result.recipe)
void yunleHaptic('success')
ionRouter.navigate('/recipes/mine', 'back', 'replace')
}
async function remove() {
const recipe = existing.value
if (!recipe)
return
if (await confirmDestructive({ header: `删除「${recipe.name}」后不可恢复`, confirmText: '删除菜谱' })) {
removeCustomRecipe(recipe.customId)
void yunleHaptic('success')
ionRouter.navigate('/recipes/mine', 'back', 'replace')
}
}
</script>
<template>
<ion-page>
<ion-header :translucent="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button default-href="/recipes/mine" />
</ion-buttons>
<ion-title>{{ existing ? '编辑菜谱' : '添加菜谱' }}</ion-title>
<ion-buttons slot="end">
<ion-button :strong="true" @click="save">
保存
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true" class="custom-recipe-form">
<ion-list :inset="true">
<ion-item>
<ion-input
v-model="name"
label="菜名"
placeholder="例如:外婆的红烧肉"
:maxlength="CUSTOM_RECIPE_NAME_MAX"
:clear-input="true"
@ion-input="error = ''"
/>
</ion-item>
</ion-list>
<h2 class="cook-group-header">
食材 · {{ stuff.length }} 种
</h2>
<IngredientInput :selected="stuff" @add="addStuff" @remove="toggleStuff" />
<IngredientSection v-for="section in sections" :key="section.title" :title="section.title" :note="selectedNote(section.items)">
<IngredientChip
v-for="item in section.items"
:key="item.name"
:label="item.name"
:emoji="item.emoji"
:selected="stuff.includes(item.name)"
@click="toggleStuff(item.name)"
/>
</IngredientSection>
<IngredientSection title="厨具(可选)">
<IngredientChip
v-for="item in tools"
:key="item.name"
:label="item.name === '一口大锅' ? '炒锅' : (item.label || item.name)"
:icon="item.icon"
:selected="selectedTools.includes(item.name)"
@click="toggleTool(item.name)"
/>
</IngredientSection>
<h2 class="cook-group-header">
做法(可选)
</h2>
<ion-list :inset="true" class="custom-recipe-fields">
<ion-item>
<ion-input
v-model="source"
label="链接"
placeholder="B 站链接、BV 号或网址"
inputmode="url"
:clear-input="true"
@ion-input="error = ''"
/>
</ion-item>
<ion-item>
<ion-textarea
v-model="note"
label="备注"
label-placement="stacked"
placeholder="没有视频的话,简单记一下步骤和用量"
:auto-grow="true"
:rows="3"
/>
</ion-item>
</ion-list>
<p v-if="error" class="cook-group-footer custom-recipe-error" role="alert">
{{ error }}
</p>
<p class="cook-group-footer">
自己添加的菜谱只保存在这台设备上,会和收录的菜谱一起参与搭配推荐和「吃什么」。
</p>
<ion-list v-if="existing" :inset="true" class="custom-recipe-delete">
<ion-item :button="true" :detail="false" @click="remove">
<ion-label class="ion-text-center" color="danger">
删除菜谱
</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<style scoped>
.custom-recipe-form .cook-group-header {
margin-top: 20px;
}
.custom-recipe-fields {
margin-top: 0;
}
.custom-recipe-error {
color: var(--ion-color-danger);
}
.custom-recipe-delete {
margin-bottom: 40px;
}
</style>