From f34dd49dcbfd30402c9610482c02fb5ac7f6363f Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Wed, 15 Apr 2026 20:47:59 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=85=8D=E6=96=B9=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=8C=89=E7=B2=BE=E6=B2=B9=E5=90=8D=E6=90=9C?= =?UTF-8?q?=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 输入精油中文名/英文名会返回含该精油的所有配方。 中文查询 ≥2 字才匹配精油,避免「草」这样的单字噪音。 Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/cypress/e2e/recipe-search.cy.js | 8 +++ .../src/__tests__/recipeSearchByOil.test.js | 57 +++++++++++++++++++ frontend/src/views/RecipeSearch.vue | 5 +- 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 frontend/src/__tests__/recipeSearchByOil.test.js diff --git a/frontend/cypress/e2e/recipe-search.cy.js b/frontend/cypress/e2e/recipe-search.cy.js index 7229df8..9cbb7e0 100644 --- a/frontend/cypress/e2e/recipe-search.cy.js +++ b/frontend/cypress/e2e/recipe-search.cy.js @@ -26,6 +26,14 @@ describe('Recipe Search', () => { }) }) + it('searching by oil name returns recipes containing that oil', () => { + cy.get('input[placeholder*="搜索"]').type('薰衣草') + cy.wait(500) + cy.get('.search-results-section, .recipe-card', { timeout: 5000 }).should('exist') + // At least one result card should exist (any recipe using 薰衣草) + cy.get('.recipe-card').should('have.length.gte', 1) + }) + it('clears search and restores all recipes', () => { cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) cy.get('input[placeholder*="搜索"]').type('薰衣草') diff --git a/frontend/src/__tests__/recipeSearchByOil.test.js b/frontend/src/__tests__/recipeSearchByOil.test.js new file mode 100644 index 0000000..886a3ab --- /dev/null +++ b/frontend/src/__tests__/recipeSearchByOil.test.js @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest' + +// Mirrors the exactResults matching rule in RecipeSearch.vue +function matches(recipes, q, oilEn = (s) => '') { + const query = (q || '').trim().toLowerCase() + if (!query) return [] + const isEn = /^[a-zA-Z\s]+$/.test(query) + return recipes.filter(r => { + if (r.tags && r.tags.includes('已下架')) return false + const nameMatch = r.name.toLowerCase().includes(query) + const enNameMatch = isEn && (r.en_name || '').toLowerCase().includes(query) + const oilEnMatch = isEn && (r.ingredients || []).some(ing => (oilEn(ing.oil) || '').toLowerCase().includes(query)) + const oilZhMatch = query.length >= 2 && (r.ingredients || []).some(ing => ing.oil.toLowerCase().includes(query)) + const tagMatch = (r.tags || []).some(t => t.toLowerCase().includes(query)) + return nameMatch || enNameMatch || oilEnMatch || oilZhMatch || tagMatch + }) +} + +const recipes = [ + { name: '助眠晚安', tags: [], ingredients: [{ oil: '薰衣草', drops: 3 }, { oil: '乳香', drops: 2 }] }, + { name: '提神醒脑', tags: [], ingredients: [{ oil: '椒样薄荷', drops: 2 }, { oil: '柠檬', drops: 3 }] }, + { name: '肩颈舒缓', tags: ['舒缓'], ingredients: [{ oil: '西班牙牛至', drops: 1 }, { oil: '椰子油', drops: 10 }] }, + { name: '感冒护理', tags: [], ingredients: [{ oil: '牛至呵护', drops: 2 }] }, + { name: '下架配方', tags: ['已下架'], ingredients: [{ oil: '薰衣草', drops: 1 }] }, +] + +describe('Recipe search by oil name', () => { + it('finds recipes containing the oil (Chinese exact)', () => { + const r = matches(recipes, '薰衣草') + expect(r.map(x => x.name)).toEqual(['助眠晚安']) + }) + + it('finds multiple recipes for a common oil', () => { + expect(matches(recipes, '牛至').map(x => x.name).sort()).toEqual(['感冒护理', '肩颈舒缓']) + }) + + it('excludes 已下架 recipes', () => { + const r = matches(recipes, '薰衣草') + expect(r.some(x => x.name === '下架配方')).toBe(false) + }) + + it('single-char query does not match oil names (avoids noise)', () => { + const r = matches(recipes, '草') + expect(r).toEqual([]) + }) + + it('still matches recipe name for short queries', () => { + const r = matches(recipes, '晚') + expect(r.map(x => x.name)).toEqual(['助眠晚安']) + }) + + it('matches english oil name when query is english', () => { + const oilEn = (o) => ({ '薰衣草': 'Lavender', '乳香': 'Frankincense' }[o] || '') + const r = matches(recipes, 'Lavender', oilEn) + expect(r.map(x => x.name)).toEqual(['助眠晚安']) + }) +}) diff --git a/frontend/src/views/RecipeSearch.vue b/frontend/src/views/RecipeSearch.vue index d5df405..3944770 100644 --- a/frontend/src/views/RecipeSearch.vue +++ b/frontend/src/views/RecipeSearch.vue @@ -312,7 +312,7 @@ function expandQuery(q) { return terms } -// Search results: exact matches (query in recipe name or tags, NOT oil names to avoid noise like 西班牙牛至) +// Search results: matches in recipe name, tags, oil names (zh + en) const exactResults = computed(() => { if (!searchQuery.value.trim()) return [] const q = searchQuery.value.trim().toLowerCase() @@ -322,9 +322,10 @@ const exactResults = computed(() => { const nameMatch = r.name.toLowerCase().includes(q) const enNameMatch = isEn && (r.en_name || '').toLowerCase().includes(q) const oilEnMatch = isEn && r.ingredients.some(ing => (oilEn(ing.oil) || '').toLowerCase().includes(q)) + const oilZhMatch = q.length >= 2 && r.ingredients.some(ing => ing.oil.toLowerCase().includes(q)) const visibleTags = auth.canEdit ? (r.tags || []) : (r.tags || []).filter(t => !EDITOR_ONLY_TAGS.includes(t)) const tagMatch = visibleTags.some(t => t.toLowerCase().includes(q)) - return nameMatch || enNameMatch || oilEnMatch || tagMatch + return nameMatch || enNameMatch || oilEnMatch || oilZhMatch || tagMatch }).sort((a, b) => a.name.localeCompare(b.name, 'zh')) }) -- 2.49.1