feat: 区分我的配方(diary)和公共配方库(recipes)
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 14s
Test / e2e-test (push) Failing after 1m19s

配方查询页:
- 我的配方 → /api/diary (user_diary表),左绿色边框区分
- 收藏配方 → 收藏的公共配方
- 公共配方库 → /api/recipes (recipes表),所有公共配方
- 搜索同时过滤个人和公共配方

管理配方页:
- 我的配方 → diary store,支持搜索/标签过滤
- 公共配方库 → 所有公共配方,所有用户可见
- 管理员创建的公共配方不再误归为"我的配方"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 20:44:57 +00:00
parent 0c19153156
commit 2983036388
2 changed files with 145 additions and 49 deletions

View File

@@ -58,40 +58,33 @@
<button class="btn-sm btn-outline" @click="clearSelection">取消选择</button>
</div>
<!-- My Recipes Section -->
<!-- My Recipes Section (from diary) -->
<div class="recipe-section">
<h3 class="section-title">📖 我的配方 ({{ myRecipes.length }})</h3>
<div class="recipe-list">
<div
v-for="r in myFilteredRecipes"
:key="r._id"
class="recipe-row"
:class="{ selected: selectedIds.has(r._id) }"
v-for="d in myFilteredRecipes"
:key="'diary-' + d.id"
class="recipe-row diary-row"
>
<input
type="checkbox"
:checked="selectedIds.has(r._id)"
@change="toggleSelect(r._id)"
class="row-check"
/>
<div class="row-info" @click="editRecipe(r)">
<span class="row-name">{{ r.name }}</span>
<div class="row-info" @click="editDiaryRecipe(d)">
<span class="row-name">{{ d.name }}</span>
<span class="row-tags">
<span v-for="t in r.tags" :key="t" class="mini-tag">{{ t }}</span>
<span v-for="t in (d.tags || [])" :key="t" class="mini-tag">{{ t }}</span>
</span>
<span class="row-cost">{{ oils.fmtPrice(oils.calcCost(r.ingredients)) }}</span>
<span class="row-cost">{{ oils.fmtPrice(oils.calcCost(d.ingredients || [])) }}</span>
</div>
<div class="row-actions">
<button class="btn-icon" @click="editRecipe(r)" title="编辑"></button>
<button class="btn-icon" @click="removeRecipe(r)" title="删除">🗑</button>
<button class="btn-icon" @click="editDiaryRecipe(d)" title="编辑"></button>
<button class="btn-icon" @click="removeDiaryRecipe(d)" title="删除">🗑</button>
</div>
</div>
<div v-if="myFilteredRecipes.length === 0" class="empty-hint">暂无个人配方</div>
</div>
</div>
<!-- Public Recipes Section (admin/senior_editor only) -->
<div v-if="auth.canManage" class="recipe-section">
<!-- Public Recipes Section -->
<div class="recipe-section">
<h3 class="section-title">🌿 公共配方库 ({{ publicRecipes.length }})</h3>
<div class="recipe-list">
<div
@@ -203,10 +196,11 @@
</template>
<script setup>
import { ref, computed, reactive } from 'vue'
import { ref, computed, reactive, onMounted } from 'vue'
import { useAuthStore } from '../stores/auth'
import { useOilsStore } from '../stores/oils'
import { useRecipesStore } from '../stores/recipes'
import { useDiaryStore } from '../stores/diary'
import { useUiStore } from '../stores/ui'
import { api } from '../composables/useApi'
import { showConfirm, showPrompt } from '../composables/useDialog'
@@ -217,6 +211,7 @@ import TagPicker from '../components/TagPicker.vue'
const auth = useAuthStore()
const oils = useOilsStore()
const recipeStore = useRecipesStore()
const diaryStore = useDiaryStore()
const ui = useUiStore()
const manageSearch = ref('')
@@ -243,13 +238,11 @@ const tagPickerName = ref('')
const tagPickerTags = ref([])
// Computed lists
const myRecipes = computed(() =>
recipeStore.recipes.filter(r => r._owner_id === auth.user.id)
)
// "我的配方" = diary (user_diary table), personal recipes
const myRecipes = computed(() => diaryStore.userDiary)
const publicRecipes = computed(() =>
recipeStore.recipes.filter(r => r._owner_id !== auth.user.id)
)
// "公共配方库" = all recipes in public library (recipes table)
const publicRecipes = computed(() => recipeStore.recipes)
function filterBySearchAndTags(list) {
let result = list
@@ -257,7 +250,7 @@ function filterBySearchAndTags(list) {
if (q) {
result = result.filter(r =>
r.name.toLowerCase().includes(q) ||
r.ingredients.some(ing => ing.oil.toLowerCase().includes(q)) ||
(r.ingredients || []).some(ing => (ing.oil || '').toLowerCase().includes(q)) ||
(r.tags && r.tags.some(t => t.toLowerCase().includes(q)))
)
}
@@ -401,6 +394,30 @@ async function saveCurrentRecipe() {
}
}
// Load diary on mount
onMounted(async () => {
if (auth.isLoggedIn) {
await diaryStore.loadDiary()
}
})
function editDiaryRecipe(diary) {
// For now, navigate to MyDiary page to edit
// TODO: inline editing
ui.showToast('请到「我的」页面编辑个人配方')
}
async function removeDiaryRecipe(diary) {
const ok = await showConfirm(`确定删除个人配方 "${diary.name}"`)
if (!ok) return
try {
await diaryStore.deleteDiary(diary.id)
ui.showToast('已删除')
} catch {
ui.showToast('删除失败')
}
}
async function removeRecipe(recipe) {
const ok = await showConfirm(`确定删除配方 "${recipe.name}"`)
if (!ok) return