From 1d9631f5df13854bd4ef3002ab7a4b81780d984a Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Fri, 10 Apr 2026 15:54:16 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=A1=E6=A0=B8=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E5=8F=AA=E9=80=9A=E7=9F=A5=E7=AE=A1=E7=90=86=E5=91=98=20+=20?= =?UTF-8?q?=E6=8C=87=E6=B4=BE=E9=AB=98=E7=BA=A7=E7=BC=96=E8=BE=91=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 去审核按钮仅管理员可见,其他用户显示已读 - 共享配方通知只发管理员 - 管理员待审核栏加"指派"按钮,选择高级编辑者审核 - 指派后发送通知给被指派人 - 新增 /api/recipes/{id}/assign-review 端点 Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/main.py | 40 ++++++++++++++++++++++------ frontend/src/components/UserMenu.vue | 2 +- frontend/src/views/RecipeManager.vue | 37 +++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/backend/main.py b/backend/main.py index 54ce768..6dd3136 100644 --- a/backend/main.py +++ b/backend/main.py @@ -781,15 +781,14 @@ def create_recipe(recipe: RecipeIn, user=Depends(get_current_user)): c.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag,)) c.execute("INSERT OR IGNORE INTO recipe_tags (recipe_id, tag_name) VALUES (?, ?)", (rid, tag)) log_audit(conn, user["id"], "create_recipe", "recipe", rid, recipe.name) - # Notify admin and senior editors when non-admin creates a recipe - if user["role"] not in ("admin", "senior_editor"): + # Notify admin only when non-admin creates a recipe + if user["role"] != "admin": who = user.get("display_name") or user["username"] - for role in ("admin", "senior_editor"): - conn.execute( - "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", - (role, "📝 新配方待审核", - f"{who} 共享了配方「{recipe.name}」,请到管理配方查看。\n[recipe_id:{rid}]") - ) + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + ("admin", "📝 新配方待审核", + f"{who} 共享了配方「{recipe.name}」,请到管理配方查看。\n[recipe_id:{rid}]") + ) conn.commit() conn.close() return {"id": rid} @@ -925,6 +924,31 @@ def reject_recipe(recipe_id: int, body: dict = None, user=Depends(require_role(" return {"ok": True} +@app.post("/api/recipes/{recipe_id}/assign-review") +def assign_recipe_review(recipe_id: int, body: dict, user=Depends(require_role("admin"))): + reviewer_id = body.get("user_id") + if not reviewer_id: + raise HTTPException(400, "请选择审核人") + conn = get_db() + recipe = conn.execute("SELECT name FROM recipes WHERE id = ?", (recipe_id,)).fetchone() + if not recipe: + conn.close() + raise HTTPException(404, "配方不存在") + reviewer = conn.execute("SELECT role, display_name, username FROM users WHERE id = ?", (reviewer_id,)).fetchone() + if not reviewer: + conn.close() + raise HTTPException(404, "用户不存在") + conn.execute( + "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)", + (reviewer["role"], "📋 请审核配方", + f"管理员指派你审核配方「{recipe['name']}」,请到管理配方页面查看并反馈意见。\n[recipe_id:{recipe_id}]", + reviewer_id) + ) + conn.commit() + conn.close() + return {"ok": True} + + @app.post("/api/recipes/adopt-batch") def adopt_batch(body: dict, user=Depends(require_role("admin"))): ids = body.get("ids", []) diff --git a/frontend/src/components/UserMenu.vue b/frontend/src/components/UserMenu.vue index d72ff52..116f4a3 100644 --- a/frontend/src/components/UserMenu.vue +++ b/frontend/src/components/UserMenu.vue @@ -130,7 +130,7 @@ function isSearchMissing(n) { } function isReviewable(n) { - if (!n.title) return false + if (!auth.isAdmin || !n.title) return false return n.title.includes('待审核') || n.title.includes('商业认证') || n.title.includes('申请') } diff --git a/frontend/src/views/RecipeManager.vue b/frontend/src/views/RecipeManager.vue index ad6fce4..c3b19ab 100644 --- a/frontend/src/views/RecipeManager.vue +++ b/frontend/src/views/RecipeManager.vue @@ -11,6 +11,14 @@ {{ r._owner_name }} + +
+ + +
@@ -416,6 +424,7 @@ const editingRecipe = ref(null) const showPending = ref(false) const pendingRecipes = ref([]) const pendingCount = ref(0) +const seniorEditors = ref([]) // Form state const formName = ref('') @@ -1069,6 +1078,13 @@ onMounted(async () => { const res = await api('/api/recipe-reviews') if (res.ok) reviewHistory.value = await res.json() } catch {} + try { + const res = await api('/api/users') + if (res.ok) { + const users = await res.json() + seniorEditors.value = users.filter(u => u.role === 'senior_editor') + } + } catch {} } // Open recipe editor if redirected from card view const editId = localStorage.getItem('oil_edit_recipe_id') @@ -1092,6 +1108,24 @@ function editDiaryRecipe(diary) { showAddOverlay.value = true } +async function assignReview(recipe) { + const userId = recipe._assignTo + if (!userId) return + try { + const res = await api('/api/recipes/' + recipe._id + '/assign-review', { + method: 'POST', + body: JSON.stringify({ user_id: userId }), + }) + if (res.ok) { + recipe._showAssign = false + recipe._assignTo = '' + ui.showToast('已指派审核') + } + } catch { + ui.showToast('指派失败') + } +} + function openRecipeDetail(recipe) { const idx = recipeStore.recipes.findIndex(r => r._id === recipe._id) if (idx >= 0) previewRecipeIndex.value = idx @@ -1700,6 +1734,9 @@ watch(() => recipeStore.recipes, () => { padding: 0; margin-right: 4px; line-height: 1; } .mini-select.active { background: #4a9d7e; border-color: #4a9d7e; color: #fff; } +.assign-row { display: flex; gap: 6px; align-items: center; margin-top: 4px; } +.assign-select { padding: 4px 8px; border: 1px solid #d4cfc7; border-radius: 6px; font-size: 12px; font-family: inherit; } +.btn-primary { background: linear-gradient(135deg, #7ec6a4, #4a9d7e); color: #fff; border: none; border-radius: 8px; cursor: pointer; font-family: inherit; } .tag-list-bar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; padding: 8px 0; }