diff --git a/frontend/cypress/e2e/price-display.cy.js b/frontend/cypress/e2e/price-display.cy.js index 6c2a21a..f9e9d94 100644 --- a/frontend/cypress/e2e/price-display.cy.js +++ b/frontend/cypress/e2e/price-display.cy.js @@ -4,8 +4,8 @@ describe('Price Display Regression', () => { cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) cy.wait(2000) // wait for oils store to load and re-render - // Check via .card-price elements which hold the formatted cost - cy.get('.card-price').first().invoke('text').then(text => { + // Check via .recipe-card-price elements which hold the formatted cost + cy.get('.recipe-card-price').first().invoke('text').then(text => { const match = text.match(/¥\s*(\d+\.?\d*)/) expect(match, 'Card price should contain ¥').to.not.be.null expect(parseFloat(match[1]), 'Price should be > 0').to.be.gt(0) diff --git a/frontend/cypress/e2e/recipe-detail.cy.js b/frontend/cypress/e2e/recipe-detail.cy.js index 1761508..bdda469 100644 --- a/frontend/cypress/e2e/recipe-detail.cy.js +++ b/frontend/cypress/e2e/recipe-detail.cy.js @@ -4,18 +4,16 @@ describe('Recipe Detail', () => { cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) }) - it('opens detail overlay when clicking a recipe card', () => { + it('opens detail panel when clicking a recipe card', () => { cy.get('.recipe-card').first().click() - cy.get('[class*="overlay"], [class*="detail"]').should('be.visible') + cy.get('[class*="detail"]').should('be.visible') }) it('shows recipe name in detail view', () => { - // Get recipe name from card, however it's structured cy.get('.recipe-card').first().invoke('text').then(cardText => { cy.get('.recipe-card').first().click() cy.wait(500) - // The detail view should show some text from the card - cy.get('[class*="overlay"], [class*="detail"]').should('be.visible') + cy.get('[class*="detail"]').should('be.visible') }) }) @@ -31,24 +29,21 @@ describe('Recipe Detail', () => { cy.contains('¥').should('exist') }) - it('closes detail overlay when clicking close button', () => { + it('closes detail panel when clicking close button', () => { cy.get('.recipe-card').first().click() - cy.get('[class*="overlay"], [class*="detail"]').should('be.visible') - cy.get('button').contains(/✕|关闭|←/).first().click() + cy.get('[class*="detail"]').should('be.visible') + cy.get('button').contains(/✕|关闭/).first().click() cy.get('.recipe-card').should('be.visible') }) it('shows action buttons in detail', () => { cy.get('.recipe-card').first().click() cy.wait(500) - // Should have at least one action button - cy.get('[class*="overlay"] button, [class*="detail"] button').should('have.length.gte', 1) + cy.get('[class*="detail"] button').should('have.length.gte', 1) }) - it('shows favorite star', () => { - cy.get('.recipe-card').first().click() - cy.wait(500) - cy.contains(/★|☆|收藏/).should('exist') + it('shows favorite star on recipe cards', () => { + cy.get('.fav-btn').first().should('exist') }) }) @@ -64,21 +59,21 @@ describe('Recipe Detail - Editor (Admin)', () => { cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) }) - it('shows edit button for admin', () => { + it('shows editable ingredients table directly', () => { cy.get('.recipe-card').first().click() cy.wait(500) - cy.contains(/编辑|✏/).should('exist') + cy.get('.oil-select, .drops-input').should('exist') }) - it('can switch to editor view', () => { + it('shows add ingredient button', () => { cy.get('.recipe-card').first().click() - cy.contains(/编辑|✏/).first().click() - cy.get('select, input[type="number"], .oil-select, .drops-input').should('exist') + cy.wait(500) + cy.contains('加精油').should('exist') }) - it('editor shows save button', () => { + it('shows export image button', () => { cy.get('.recipe-card').first().click() - cy.contains(/编辑|✏/).first().click() - cy.contains(/保存|💾/).should('exist') + cy.wait(500) + cy.contains('导出图片').should('exist') }) }) diff --git a/frontend/src/components/RecipeCard.vue b/frontend/src/components/RecipeCard.vue index 354a13b..c38e193 100644 --- a/frontend/src/components/RecipeCard.vue +++ b/frontend/src/components/RecipeCard.vue @@ -1,36 +1,19 @@ @@ -49,165 +32,88 @@ defineEmits(['click', 'toggle-fav']) const oilsStore = useOilsStore() const recipesStore = useRecipesStore() +const oilNames = computed(() => + props.recipe.ingredients.map(i => i.oil).join('、') +) const priceInfo = computed(() => oilsStore.fmtCostWithRetail(props.recipe.ingredients)) const isFav = computed(() => recipesStore.isFavorite(props.recipe)) diff --git a/frontend/src/components/RecipeDetailOverlay.vue b/frontend/src/components/RecipeDetailOverlay.vue index 20a4d26..9437b7c 100644 --- a/frontend/src/components/RecipeDetailOverlay.vue +++ b/frontend/src/components/RecipeDetailOverlay.vue @@ -1,155 +1,94 @@ @@ -157,10 +96,9 @@ diff --git a/frontend/src/views/RecipeSearch.vue b/frontend/src/views/RecipeSearch.vue index 8156170..d4ede12 100644 --- a/frontend/src/views/RecipeSearch.vue +++ b/frontend/src/views/RecipeSearch.vue @@ -1,121 +1,43 @@ @@ -231,248 +117,101 @@ function clearSearch() { padding: 0 12px 24px; } -.cat-wrap { - position: relative; - margin: 0 -12px 20px; - overflow: hidden; -} - -.cat-track { - display: flex; - transition: transform 0.4s ease; - will-change: transform; -} - -.cat-card { - flex: 0 0 100%; - min-height: 200px; - position: relative; - overflow: hidden; - cursor: pointer; - background-size: cover; - background-position: center; -} - -.cat-card::after { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(135deg, rgba(0,0,0,0.45), rgba(0,0,0,0.25)); -} - -.cat-inner { - position: relative; - z-index: 1; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 36px 24px; - color: white; - text-align: center; -} - -.cat-icon { - font-size: 48px; - margin-bottom: 10px; - filter: drop-shadow(0 2px 6px rgba(0,0,0,0.3)); -} - -.cat-name { - font-family: 'Noto Serif SC', serif; - font-size: 24px; - font-weight: 700; - letter-spacing: 3px; - text-shadow: 0 2px 8px rgba(0,0,0,0.5); -} - -.cat-sub { - font-size: 13px; - margin-top: 6px; - opacity: 0.9; - letter-spacing: 1px; -} - -.cat-arrow { - position: absolute; - top: 50%; - transform: translateY(-50%); - z-index: 2; - width: 36px; - height: 36px; - border-radius: 50%; - background: rgba(255,255,255,0.25); - border: none; - color: white; - font-size: 18px; - cursor: pointer; - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - transition: background 0.2s; -} -.cat-arrow:hover { background: rgba(255,255,255,0.45); } -.cat-arrow.left { left: 12px; } -.cat-arrow.right { right: 12px; } - -.cat-dots { - display: flex; - justify-content: center; - gap: 8px; - margin-bottom: 14px; -} -.cat-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--border, #e0d4c0); - cursor: pointer; - transition: all 0.25s; -} -.cat-dot.active { - background: var(--sage, #7a9e7e); - width: 22px; - border-radius: 4px; -} - -.cat-filter-bar { - display: flex; - align-items: center; - justify-content: space-between; - background: var(--sage-mist, #eef4ee); - border-radius: 10px; - padding: 10px 16px; - margin-bottom: 16px; - font-size: 14px; - font-weight: 600; - color: var(--sage-dark, #5a7d5e); -} - -.cat-label { - font-size: 12px; -} - -.cat-arrow { - width: 28px; - height: 28px; - border-radius: 50%; - border: 1.5px solid #d4cfc7; - background: #fff; - cursor: pointer; - font-size: 16px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - color: #6b6375; -} - -.cat-arrow:disabled { - opacity: 0.3; - cursor: default; -} - .search-box { + background: white; + border-radius: 16px; + padding: 20px 24px; + box-shadow: 0 4px 20px rgba(90, 60, 30, 0.08); + margin-bottom: 20px; +} + +.search-label { + font-size: 13px; + font-weight: 500; + color: #9a8570; + margin-bottom: 10px; +} + +.search-row { display: flex; + gap: 10px; align-items: center; - gap: 8px; - margin-bottom: 16px; - background: #f8f7f5; - border-radius: 12px; - padding: 4px 8px; - border: 1.5px solid #e5e4e7; + flex-wrap: wrap; } .search-input { flex: 1; - border: none; - background: transparent; - padding: 10px 8px; - font-size: 14px; - outline: none; + min-width: 200px; + padding: 11px 16px; + border: 1.5px solid #e0d4c0; + border-radius: 10px; + font-size: 15px; font-family: inherit; - color: #3e3a44; + outline: none; + color: #2c2416; + background: white; + transition: border-color 0.2s; +} + +.search-input:focus { + border-color: #7a9e7e; } .search-input::placeholder { - color: #b0aab5; -} - -.search-btn, -.search-clear-btn { - border: none; - background: transparent; - cursor: pointer; - font-size: 16px; - padding: 6px 8px; - border-radius: 8px; - color: #6b6375; -} - -.search-clear-btn:hover, -.search-btn:hover { - background: #eae8e5; -} - -.personal-section { - margin-bottom: 20px; -} - -.section-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 12px; - background: #f8f7f5; - border-radius: 10px; - cursor: pointer; - margin-bottom: 10px; - font-size: 14px; - font-weight: 600; - color: #3e3a44; -} - -.section-header:hover { - background: #f0eeeb; -} - -.toggle-icon { - font-size: 12px; - color: #999; -} - -.section-label { - font-size: 14px; - font-weight: 600; - color: #3e3a44; - padding: 8px 4px; - margin-bottom: 8px; -} - -.search-results-section { - margin-bottom: 20px; + color: #c4a882; } .recipe-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 12px; - margin-bottom: 16px; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; + margin-bottom: 24px; } .empty-hint { grid-column: 1 / -1; text-align: center; - color: #b0aab5; - font-size: 13px; - padding: 24px 0; + color: #9a8570; + font-size: 14px; + padding: 40px 0; +} + +/* Buttons */ +.btn { + padding: 11px 22px; + border-radius: 10px; + font-size: 14px; + font-family: inherit; + cursor: pointer; + border: none; + transition: all 0.2s; + font-weight: 500; + white-space: nowrap; +} + +.btn-outline { + background: transparent; + border: 1.5px solid #7a9e7e; + color: #5a7d5e; +} + +.btn-outline:hover { + background: #eef4ee; +} + +.btn-outline.active { + background: #eef4ee; + border-color: #5a7d5e; + font-weight: 600; } @media (max-width: 600px) { .recipe-grid { grid-template-columns: 1fr; } + + .search-box { + padding: 14px 16px; + } }