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
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user