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;