Refactor frontend to Vue 3 + Vite + Pinia + Cypress E2E
- Replace single-file 8441-line HTML with Vue 3 SPA - Pinia stores: auth, oils, recipes, diary, ui - Composables: useApi, useDialog, useSmartPaste, useOilTranslation - 6 shared components: RecipeCard, RecipeDetailOverlay, TagPicker, etc. - 9 page views: RecipeSearch, RecipeManager, Inventory, OilReference, etc. - 14 Cypress E2E test specs (113 tests), all passing - Multi-stage Dockerfile (Node build + Python runtime) - Demo video generation scripts (TTS + subtitles + screen recording) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
144
frontend/src/components/RecipeCard.vue
Normal file
144
frontend/src/components/RecipeCard.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="recipe-card" @click="$emit('click', index)">
|
||||
<div class="card-name">{{ recipe.name }}</div>
|
||||
|
||||
<div v-if="recipe.tags && recipe.tags.length" class="card-tags">
|
||||
<span v-for="tag in recipe.tags" :key="tag" class="card-tag">{{ tag }}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-oils">
|
||||
<span v-for="(ing, i) in recipe.ingredients" :key="i" class="card-oil">
|
||||
{{ ing.oil }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card-bottom">
|
||||
<span class="card-price">
|
||||
{{ priceInfo.cost }}
|
||||
<span v-if="priceInfo.hasRetail" class="card-retail">零售 {{ priceInfo.retail }}</span>
|
||||
</span>
|
||||
<button
|
||||
class="card-star"
|
||||
:class="{ favorited: isFav }"
|
||||
@click.stop="$emit('toggle-fav', recipe._id)"
|
||||
:title="isFav ? '取消收藏' : '收藏'"
|
||||
>
|
||||
{{ isFav ? '★' : '☆' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useOilsStore } from '../stores/oils'
|
||||
import { useRecipesStore } from '../stores/recipes'
|
||||
|
||||
const props = defineProps({
|
||||
recipe: { type: Object, required: true },
|
||||
index: { type: Number, required: true },
|
||||
})
|
||||
|
||||
defineEmits(['click', 'toggle-fav'])
|
||||
|
||||
const oilsStore = useOilsStore()
|
||||
const recipesStore = useRecipesStore()
|
||||
|
||||
const priceInfo = computed(() => oilsStore.fmtCostWithRetail(props.recipe.ingredients))
|
||||
const isFav = computed(() => recipesStore.isFavorite(props.recipe))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.recipe-card {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
padding: 18px 16px 14px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s, transform 0.15s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.recipe-card:hover {
|
||||
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #3e3a44;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
background: #f0ece4;
|
||||
color: #8a7e6b;
|
||||
}
|
||||
|
||||
.card-oils {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.card-oil {
|
||||
font-size: 12px;
|
||||
color: #6b6375;
|
||||
background: #f8f7f5;
|
||||
padding: 2px 7px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.card-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.card-price {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #4a9d7e;
|
||||
}
|
||||
|
||||
.card-retail {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
color: #999;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.card-star {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #ccc;
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.card-star.favorited {
|
||||
color: #f5a623;
|
||||
}
|
||||
|
||||
.card-star:hover {
|
||||
color: #f5a623;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user