feat: 方案待定制可编辑+通知去定制按钮+权限修复
All checks were successful
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 15s
Test / e2e-test (push) Successful in 50s

- 会员等待中可修改健康需求,修改后通知老师
- 通知里方案请求显示"去定制"按钮跳转用户管理
- 后端: 方案owner可更新health_desc,teacher可改title/status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 22:04:14 +00:00
parent fe74f45bca
commit a6c8e7a6e1
3 changed files with 53 additions and 6 deletions

View File

@@ -1659,12 +1659,21 @@ def update_oil_plan(plan_id: int, body: dict, user=Depends(get_current_user)):
if not plan: if not plan:
conn.close() conn.close()
raise HTTPException(404, "方案不存在") raise HTTPException(404, "方案不存在")
if plan["teacher_id"] != user["id"] and user["role"] != "admin": is_teacher = plan["teacher_id"] == user["id"] or user["role"] == "admin"
is_owner = plan["user_id"] == user["id"]
if not is_teacher and not is_owner:
conn.close() conn.close()
raise HTTPException(403, "无权操作") raise HTTPException(403, "无权操作")
if "title" in body: if "health_desc" in body and is_owner:
conn.execute("UPDATE oil_plans SET health_desc = ? WHERE id = ?", (body["health_desc"], plan_id))
who = user.get("display_name") or user["username"]
conn.execute(
"INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)",
("admin", "📋 方案需求已更新", f"{who} 更新了健康需求:{body['health_desc'][:50]}", plan["teacher_id"])
)
if "title" in body and is_teacher:
conn.execute("UPDATE oil_plans SET title = ? WHERE id = ?", (body["title"], plan_id)) conn.execute("UPDATE oil_plans SET title = ? WHERE id = ?", (body["title"], plan_id))
if "status" in body: if "status" in body and is_teacher:
old_status = plan["status"] old_status = plan["status"]
conn.execute("UPDATE oil_plans SET status = ? WHERE id = ?", (body["status"], plan_id)) conn.execute("UPDATE oil_plans SET status = ? WHERE id = ?", (body["status"], plan_id))
if body["status"] == "active" and old_status != "active": if body["status"] == "active" and old_status != "active":

View File

@@ -38,6 +38,8 @@
<div v-if="!n.is_read" class="notif-actions"> <div v-if="!n.is_read" class="notif-actions">
<!-- 搜索未收录通知已添加按钮 --> <!-- 搜索未收录通知已添加按钮 -->
<button v-if="isSearchMissing(n)" class="notif-action-btn notif-btn-added" @click="markAdded(n)">已添加</button> <button v-if="isSearchMissing(n)" class="notif-action-btn notif-btn-added" @click="markAdded(n)">已添加</button>
<!-- 方案请求通知去定制按钮 -->
<button v-else-if="isPlanRequest(n)" class="notif-action-btn notif-btn-plan" @click="goPlanDesign(n)">去定制</button>
<!-- 审核类通知去审核按钮 --> <!-- 审核类通知去审核按钮 -->
<button v-else-if="isReviewable(n)" class="notif-action-btn notif-btn-review" @click="goReview(n)">去审核</button> <button v-else-if="isReviewable(n)" class="notif-action-btn notif-btn-review" @click="goReview(n)">去审核</button>
<!-- 默认已读按钮 --> <!-- 默认已读按钮 -->
@@ -129,6 +131,16 @@ function isSearchMissing(n) {
return n.title && n.title.includes('用户需求') return n.title && n.title.includes('用户需求')
} }
function isPlanRequest(n) {
return n.title && (n.title.includes('方案请求') || n.title.includes('方案需求'))
}
function goPlanDesign(n) {
markOneRead(n)
emit('close')
window.location.hash = '#/users'
}
function isReviewable(n) { function isReviewable(n) {
if (!n.title) return false if (!n.title) return false
// Admin: review recipe/business/applications // Admin: review recipe/business/applications
@@ -269,6 +281,8 @@ onMounted(loadNotifications)
} }
.notif-btn-added { color: #4a9d7e; border-color: #7ec6a4; } .notif-btn-added { color: #4a9d7e; border-color: #7ec6a4; }
.notif-btn-added:hover { background: #e8f5e9; } .notif-btn-added:hover { background: #e8f5e9; }
.notif-btn-plan { color: #1565c0; border-color: #90caf9; }
.notif-btn-plan:hover { background: #e3f2fd; }
.notif-btn-review { color: #e65100; border-color: #ffb74d; } .notif-btn-review { color: #e65100; border-color: #ffb74d; }
.notif-btn-review:hover { background: #fff3e0; } .notif-btn-review:hover { background: #fff3e0; }
.notif-body { color: #888; font-size: 12px; margin-top: 2px; white-space: pre-line; } .notif-body { color: #888; font-size: 12px; margin-top: 2px; white-space: pre-line; }

View File

@@ -32,12 +32,13 @@
</div> </div>
</div> </div>
<!-- Pending plan --> <!-- Pending plan: editable -->
<div v-else-if="plansStore.pendingPlans.length" class="plan-section plan-pending"> <div v-else-if="plansStore.pendingPlans.length" class="plan-section plan-pending">
<div class="plan-header"> <div class="plan-header">
<span class="plan-title"> 方案审核</span> <span class="plan-title"> 老师正在为你定制方案中</span>
</div> </div>
<p class="plan-desc">{{ plansStore.pendingPlans[0].health_desc }}</p> <textarea v-model="pendingDesc" class="form-textarea" rows="3" @blur="updatePendingDesc"></textarea>
<div class="plan-pending-hint">修改后老师会收到通知</div>
</div> </div>
<!-- No plan: request button --> <!-- No plan: request button -->
@@ -324,6 +325,28 @@ function matchTeacher() {
) || null ) || null
} }
const pendingDesc = ref('')
// Sync pendingDesc when plans load
watch(() => plansStore.pendingPlans, (pp) => {
if (pp.length) pendingDesc.value = pp[0].health_desc || ''
}, { immediate: true })
async function updatePendingDesc() {
const plan = plansStore.pendingPlans[0]
if (!plan || pendingDesc.value === plan.health_desc) return
try {
await api(`/api/oil-plans/${plan.id}`, {
method: 'PUT',
body: JSON.stringify({ health_desc: pendingDesc.value }),
})
plan.health_desc = pendingDesc.value
ui.showToast('已更新,老师会收到通知')
} catch {
ui.showToast('更新失败')
}
}
const shoppingTotal = computed(() => const shoppingTotal = computed(() =>
plansStore.shoppingList.filter(i => !i.in_inventory).reduce((s, i) => s + i.total_cost, 0).toFixed(2) plansStore.shoppingList.filter(i => !i.in_inventory).reduce((s, i) => s + i.total_cost, 0).toFixed(2)
) )
@@ -641,6 +664,7 @@ onMounted(async () => {
.overlay-header h3 { margin: 0; font-size: 16px; } .overlay-header h3 { margin: 0; font-size: 16px; }
.form-label { display: block; font-size: 13px; color: #6b6375; margin-bottom: 4px; font-weight: 500; } .form-label { display: block; font-size: 13px; color: #6b6375; margin-bottom: 4px; font-weight: 500; }
.form-textarea { width: 100%; border: 1.5px solid #e5e4e7; border-radius: 8px; padding: 8px; font-size: 13px; font-family: inherit; resize: vertical; box-sizing: border-box; } .form-textarea { width: 100%; border: 1.5px solid #e5e4e7; border-radius: 8px; padding: 8px; font-size: 13px; font-family: inherit; resize: vertical; box-sizing: border-box; }
.plan-pending-hint { font-size: 11px; color: #b0aab5; margin-top: 4px; }
.teacher-matched { color: #2e7d5a; font-size: 13px; margin-top: 4px; font-weight: 500; } .teacher-matched { color: #2e7d5a; font-size: 13px; margin-top: 4px; font-weight: 500; }
.teacher-no-match { color: #c62828; font-size: 12px; margin-top: 4px; } .teacher-no-match { color: #c62828; font-size: 12px; margin-top: 4px; }
</style> </style>