From 54003bc4669057867cc6e4c226e518c59c4d12f6 Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Thu, 9 Apr 2026 17:54:18 +0000 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=E6=90=9C=E7=B4=A2=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=E6=94=B6=E8=97=8F=E3=80=81=E6=8B=BC=E9=9F=B3=E9=A6=96=E5=AD=97?= =?UTF-8?q?=E6=AF=8D=E5=8C=B9=E9=85=8D=E3=80=81=E6=B8=85=E9=99=A4=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E3=80=81=E6=BB=91=E5=8A=A8=E5=88=87=E6=8D=A2=E3=80=81?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E5=B7=B2=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 搜索时收藏配方也按关键词过滤,不匹配的隐藏 2. 编辑配方添加精油时支持拼音首字母匹配(如xyc→薰衣草) 3. 品牌设置页清除图片立即保存到后端,不需点保存按钮 4. 左右滑动切换tab,轮播区域内滑动切换图片不触发tab切换 5. 通知列表每条未读通知加"已读"按钮,调用POST /api/notifications/{id}/read Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.vue | 46 +++++++++++- .../src/components/RecipeDetailOverlay.vue | 3 +- frontend/src/components/UserMenu.vue | 21 +++++- frontend/src/composables/usePinyinMatch.js | 73 +++++++++++++++++++ frontend/src/views/MyDiary.vue | 20 +++++ frontend/src/views/RecipeSearch.vue | 28 ++++++- 6 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 frontend/src/composables/usePinyinMatch.js diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 987e56d..593f672 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -39,7 +39,7 @@ -
+
@@ -54,7 +54,7 @@ diff --git a/frontend/src/components/RecipeDetailOverlay.vue b/frontend/src/components/RecipeDetailOverlay.vue index 68ecc11..9bad572 100644 --- a/frontend/src/components/RecipeDetailOverlay.vue +++ b/frontend/src/components/RecipeDetailOverlay.vue @@ -9,7 +9,7 @@ -
@@ -359,7 +359,9 @@ import { matchesPinyinInitials } from '../composables/usePinyinMatch' // TagPicker replaced with inline tag editing const props = defineProps({ - recipeIndex: { type: Number, required: true }, + recipeIndex: { type: Number, default: null }, + recipeData: { type: Object, default: null }, + isDiary: { type: Boolean, default: false }, }) const emit = defineEmits(['close']) @@ -386,9 +388,10 @@ const generatingImage = ref(false) const previewOverride = ref(null) // ---- Source recipe ---- -const recipe = computed(() => - recipesStore.recipes[props.recipeIndex] || { name: '', ingredients: [], tags: [], note: '' } -) +const recipe = computed(() => { + if (props.recipeData) return props.recipeData + return recipesStore.recipes[props.recipeIndex] || { name: '', ingredients: [], tags: [], note: '' } +}) // ---- Display recipe: previewOverride when in preview mode, otherwise saved recipe ---- const displayRecipe = computed(() => { @@ -710,22 +713,31 @@ async function saveToDiary() { return } const name = await showPrompt('保存为我的配方,名称:', recipe.value.name) - // null = user cancelled (clicked 取消) if (name === null) return - // empty string = user cleared the name field if (!name.trim()) { ui.showToast('请输入配方名称') return } + const trimmed = name.trim() + const dupDiary = diaryStore.userDiary.some(d => d.name === trimmed) + const dupPublic = recipesStore.recipes.some(r => r.name === trimmed) + if (dupDiary) { + ui.showToast('我的配方中已有同名配方「' + trimmed + '」') + return + } + if (dupPublic) { + ui.showToast('公共配方库中已有同名配方「' + trimmed + '」') + return + } try { const payload = { name: name.trim(), note: recipe.value.note || '', - ingredients: recipe.value.ingredients.map(i => ({ oil_name: i.oil, drops: i.drops })), + ingredients: recipe.value.ingredients.map(i => ({ oil: i.oil, drops: i.drops })), tags: recipe.value.tags || [], + source_recipe_id: recipe.value._id || null, } - console.log('[saveToDiary] saving recipe:', payload) - await recipesStore.saveRecipe(payload) + await diaryStore.createDiary(payload) ui.showToast('已保存!可在「配方查询 → 我的配方」查看') } catch (e) { console.error('[saveToDiary] failed:', e) diff --git a/frontend/src/components/UserMenu.vue b/frontend/src/components/UserMenu.vue index b44b453..a20abdd 100644 --- a/frontend/src/components/UserMenu.vue +++ b/frontend/src/components/UserMenu.vue @@ -30,7 +30,14 @@ class="notif-item" :class="{ unread: !n.is_read }">
{{ n.title }}
- +
+ + + + + + +
{{ n.body }}
{{ formatTime(n.created_at) }}
@@ -108,6 +115,29 @@ async function submitBug() { } } +function isSearchMissing(n) { + return n.title && n.title.includes('用户需求') +} + +function isReviewable(n) { + if (!n.title) return false + return n.title.includes('待审核') || n.title.includes('商业认证') || n.title.includes('申请') +} + +async function markAdded(n) { + await markOneRead(n) +} + +function goReview(n) { + markOneRead(n) + emit('close') + if (n.title.includes('配方')) { + router.push('/manage') + } else if (n.title.includes('商业认证') || n.title.includes('申请')) { + router.push('/users') + } +} + async function markOneRead(n) { try { await api(`/api/notifications/${n.id}/read`, { method: 'POST', body: '{}' }) @@ -209,6 +239,16 @@ onMounted(loadNotifications) font-family: inherit; white-space: nowrap; flex-shrink: 0; } .notif-mark-one:hover { background: #f0faf5; border-color: #7a9e7e; } +.notif-actions { display: flex; gap: 4px; flex-shrink: 0; } +.notif-action-btn { + background: none; border: 1px solid #ccc; border-radius: 6px; + font-size: 11px; cursor: pointer; padding: 2px 8px; + font-family: inherit; white-space: nowrap; +} +.notif-btn-added { color: #4a9d7e; border-color: #7ec6a4; } +.notif-btn-added:hover { background: #e8f5e9; } +.notif-btn-review { color: #e65100; border-color: #ffb74d; } +.notif-btn-review:hover { background: #fff3e0; } .notif-body { color: #888; font-size: 12px; margin-top: 2px; white-space: pre-line; } .notif-time { color: #bbb; font-size: 11px; margin-top: 2px; } .notif-empty { text-align: center; color: #ccc; padding: 16px; font-size: 13px; } diff --git a/frontend/src/views/OilReference.vue b/frontend/src/views/OilReference.vue index e29a476..bd6a961 100644 --- a/frontend/src/views/OilReference.vue +++ b/frontend/src/views/OilReference.vue @@ -260,11 +260,14 @@ -