From 9f0c66e58347372ed984f3a8796feb4296a2f129 Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Fri, 10 Apr 2026 14:32:56 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A0=87=E7=AD=BE=E4=BF=9D=E5=AD=98+?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 create_diary 不保存 tags 的问题 - 新建标签后加入全局标签列表,移除后显示在候选区 - 标签筛选区:编辑者可新增标签,管理员可删除标签 - 标签筛选区每个标签旁加×删除按钮(管理员) Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/main.py | 5 +-- frontend/src/views/RecipeManager.vue | 54 ++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/backend/main.py b/backend/main.py index 84b14f9..54ce768 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1238,14 +1238,15 @@ def create_diary(body: dict, user=Depends(get_current_user)): name = body.get("name", "").strip() ingredients = body.get("ingredients", []) note = body.get("note", "") + tags = body.get("tags", []) source_id = body.get("source_recipe_id") if not name: raise HTTPException(400, "请输入配方名称") conn = get_db() c = conn.cursor() c.execute( - "INSERT INTO user_diary (user_id, source_recipe_id, name, ingredients, note) VALUES (?, ?, ?, ?, ?)", - (user["id"], source_id, name, json.dumps(ingredients, ensure_ascii=False), note) + "INSERT INTO user_diary (user_id, source_recipe_id, name, ingredients, note, tags) VALUES (?, ?, ?, ?, ?, ?)", + (user["id"], source_id, name, json.dumps(ingredients, ensure_ascii=False), note, json.dumps(tags, ensure_ascii=False)) ) conn.commit() did = c.lastrowid diff --git a/frontend/src/views/RecipeManager.vue b/frontend/src/views/RecipeManager.vue index 231f0d3..d0e7fcf 100644 --- a/frontend/src/views/RecipeManager.vue +++ b/frontend/src/views/RecipeManager.vue @@ -54,7 +54,11 @@ class="tag-chip" :class="{ active: selectedTags.includes(tag) }" @click="toggleTag(tag)" - >{{ tag }} + >{{ tag }}× +
+ + +
@@ -486,6 +490,38 @@ function filterBySearchAndTags(list) { const myFilteredRecipes = computed(() => filterBySearchAndTags(myRecipes.value)) const publicFilteredRecipes = computed(() => filterBySearchAndTags(publicRecipes.value)) +const globalNewTag = ref('') + +async function addGlobalTag() { + const tag = globalNewTag.value.trim() + if (!tag) return + try { + await api('/api/tags', { method: 'POST', body: JSON.stringify({ name: tag }) }) + if (!recipeStore.allTags.includes(tag)) { + recipeStore.allTags.push(tag) + recipeStore.allTags.sort((a, b) => a.localeCompare(b, 'zh')) + } + globalNewTag.value = '' + ui.showToast('标签已添加') + } catch { + ui.showToast('添加失败') + } +} + +async function deleteGlobalTag(tag) { + const { showConfirm } = await import('../composables/useDialog') + const ok = await showConfirm(`确定删除标签「${tag}」?`) + if (!ok) return + try { + await api(`/api/tags/${encodeURIComponent(tag)}`, { method: 'DELETE' }) + const idx = recipeStore.allTags.indexOf(tag) + if (idx >= 0) recipeStore.allTags.splice(idx, 1) + ui.showToast('标签已删除') + } catch { + ui.showToast('删除失败') + } +} + function toggleTag(tag) { const idx = selectedTags.value.indexOf(tag) if (idx >= 0) selectedTags.value.splice(idx, 1) @@ -712,9 +748,15 @@ const formCandidateTags = computed(() => function addNewFormTag() { const tag = newTagInput.value.trim() - if (tag && !formTags.value.includes(tag)) { + if (!tag) return + if (!formTags.value.includes(tag)) { formTags.value.push(tag) } + // Also add to global tags so it appears in candidates if removed + if (!recipeStore.allTags.includes(tag)) { + recipeStore.allTags.push(tag) + recipeStore.allTags.sort((a, b) => a.localeCompare(b, 'zh')) + } newTagInput.value = '' } @@ -1546,6 +1588,14 @@ watch(() => recipeStore.recipes, () => { .drops-sm:focus { border-color: #7ec6a4; } .select-sm { padding: 4px 6px; border: 1.5px solid #d4cfc7; border-radius: 6px; font-size: 12px; font-family: inherit; background: #fff; width: auto; } .btn-select-active { background: #e8f5e9; color: #2e7d5a; border: 1.5px solid #7ec6a4; border-radius: 10px; padding: 7px 14px; font-size: 13px; cursor: pointer; font-family: inherit; font-weight: 600; } +.tag-delete { margin-left: 4px; cursor: pointer; font-size: 12px; color: #ccc; } +.tag-delete:hover { color: #c0392b; } +.tag-add-row { display: flex; gap: 4px; align-items: center; width: 100%; margin-top: 4px; } +.tag-add-input { flex: 1; padding: 4px 8px; border: 1px solid #e5e4e7; border-radius: 8px; font-size: 12px; outline: none; font-family: inherit; } +.tag-add-input:focus { border-color: #7ec6a4; } +.tag-add-btn { border: none; background: #e8f5e9; color: #4a9d7e; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; font-family: inherit; } +.tag-add-btn:disabled { opacity: 0.4; } + .export-btn { margin-left: auto; border: none; background: none; cursor: pointer; font-size: 16px; padding: 4px 6px;