diff --git a/backend/database.py b/backend/database.py
index 88c2045..137ec68 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -247,24 +247,6 @@ def init_db():
if "en_name" not in cols:
c.execute("ALTER TABLE recipes ADD COLUMN en_name TEXT DEFAULT ''")
- # Oil plans
- c.execute("""CREATE TABLE IF NOT EXISTS oil_plans (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
- teacher_id INTEGER,
- title TEXT DEFAULT '',
- health_desc TEXT DEFAULT '',
- status TEXT NOT NULL DEFAULT 'pending',
- created_at TEXT DEFAULT (datetime('now'))
- )""")
- c.execute("""CREATE TABLE IF NOT EXISTS oil_plan_recipes (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- plan_id INTEGER NOT NULL REFERENCES oil_plans(id) ON DELETE CASCADE,
- recipe_name TEXT NOT NULL,
- ingredients TEXT NOT NULL DEFAULT '[]',
- times_per_month INTEGER NOT NULL DEFAULT 1
- )""")
-
# Seed admin user if no users exist
count = c.execute("SELECT COUNT(*) FROM users").fetchone()[0]
if count == 0:
diff --git a/backend/main.py b/backend/main.py
index 9149e74..55a5729 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1611,200 +1611,8 @@ def recipes_by_inventory(user=Depends(get_current_user)):
return result
-# ── Oil Plans (purchase plans) ─────────────────────────
-@app.get("/api/teachers")
-def list_teachers(user=Depends(get_current_user)):
- conn = get_db()
- rows = conn.execute(
- "SELECT id, display_name, username FROM users WHERE role IN ('senior_editor', 'admin') ORDER BY id"
- ).fetchall()
- conn.close()
- return [{"id": r["id"], "display_name": r["display_name"] or r["username"], "username": r["username"]} for r in rows]
-
-
-@app.get("/api/oil-plans")
-def list_oil_plans(user=Depends(get_current_user)):
- if not user.get("id"):
- return []
- conn = get_db()
- if user["role"] in ("senior_editor", "admin"):
- plans = conn.execute(
- "SELECT p.*, u.display_name as user_name, u.username "
- "FROM oil_plans p LEFT JOIN users u ON p.user_id = u.id "
- "WHERE p.teacher_id = ? OR p.user_id = ? ORDER BY p.created_at DESC",
- (user["id"], user["id"])
- ).fetchall()
- else:
- plans = conn.execute(
- "SELECT p.*, u.display_name as teacher_name "
- "FROM oil_plans p LEFT JOIN users u ON p.teacher_id = u.id "
- "WHERE p.user_id = ? ORDER BY p.created_at DESC", (user["id"],)
- ).fetchall()
- result = []
- for p in plans:
- d = dict(p)
- recipes = conn.execute(
- "SELECT id, recipe_name, ingredients, times_per_month FROM oil_plan_recipes WHERE plan_id = ?",
- (p["id"],)
- ).fetchall()
- d["recipes"] = [{"id": r["id"], "recipe_name": r["recipe_name"],
- "ingredients": json.loads(r["ingredients"]), "times_per_month": r["times_per_month"]} for r in recipes]
- result.append(d)
- conn.close()
- return result
-
-
-@app.post("/api/oil-plans", status_code=201)
-def create_oil_plan(body: dict, user=Depends(get_current_user)):
- if not user.get("id"):
- raise HTTPException(403, "请先登录")
- health_desc = body.get("health_desc", "").strip()
- teacher_id = body.get("teacher_id")
- # Teacher/admin can create plan for a user directly
- target_user_id = body.get("user_id") if user["role"] in ("senior_editor", "admin") else None
- if not teacher_id:
- raise HTTPException(400, "请选择老师")
- conn = get_db()
- c = conn.cursor()
- plan_user_id = target_user_id or user["id"]
- c.execute("INSERT INTO oil_plans (user_id, teacher_id, health_desc) VALUES (?, ?, ?)",
- (plan_user_id, teacher_id, health_desc))
- plan_id = c.lastrowid
- who = user.get("display_name") or user["username"]
- conn.execute(
- "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)",
- ("admin", "📋 新方案请求", f"{who}:{health_desc[:50]}", teacher_id)
- )
- conn.commit()
- conn.close()
- return {"id": plan_id}
-
-
-@app.put("/api/oil-plans/{plan_id}")
-def update_oil_plan(plan_id: int, body: dict, user=Depends(get_current_user)):
- conn = get_db()
- plan = conn.execute("SELECT * FROM oil_plans WHERE id = ?", (plan_id,)).fetchone()
- if not plan:
- conn.close()
- raise HTTPException(404, "方案不存在")
- 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()
- raise HTTPException(403, "无权操作")
- 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))
- if "status" in body and is_teacher:
- old_status = plan["status"]
- conn.execute("UPDATE oil_plans SET status = ? WHERE id = ?", (body["status"], plan_id))
- if body["status"] == "active" and old_status != "active":
- teacher_name = user.get("display_name") or user["username"]
- conn.execute(
- "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)",
- ("viewer", "🎉 你的购油方案已就绪", f"{teacher_name}老师已为你定制好方案,去库存页查看!", plan["user_id"])
- )
- conn.commit()
- conn.close()
- return {"ok": True}
-
-
-@app.post("/api/oil-plans/{plan_id}/recipes", status_code=201)
-def add_plan_recipe(plan_id: int, body: dict, user=Depends(get_current_user)):
- conn = get_db()
- plan = conn.execute("SELECT * FROM oil_plans WHERE id = ?", (plan_id,)).fetchone()
- if not plan:
- conn.close()
- raise HTTPException(404, "方案不存在")
- if plan["teacher_id"] != user["id"] and user["role"] != "admin":
- conn.close()
- raise HTTPException(403, "无权操作")
- recipe_name = body.get("recipe_name", "").strip()
- ingredients = body.get("ingredients", [])
- times = body.get("times_per_month", 1)
- if not recipe_name or not ingredients:
- conn.close()
- raise HTTPException(400, "配方名和成分不能为空")
- c = conn.cursor()
- c.execute("INSERT INTO oil_plan_recipes (plan_id, recipe_name, ingredients, times_per_month) VALUES (?, ?, ?, ?)",
- (plan_id, recipe_name, json.dumps(ingredients, ensure_ascii=False), times))
- conn.commit()
- rid = c.lastrowid
- conn.close()
- return {"id": rid}
-
-
-@app.delete("/api/oil-plans/{plan_id}/recipes/{recipe_id}")
-def remove_plan_recipe(plan_id: int, recipe_id: int, user=Depends(get_current_user)):
- conn = get_db()
- plan = conn.execute("SELECT * FROM oil_plans WHERE id = ?", (plan_id,)).fetchone()
- if not plan:
- conn.close()
- raise HTTPException(404)
- if plan["teacher_id"] != user["id"] and user["role"] != "admin":
- conn.close()
- raise HTTPException(403)
- conn.execute("DELETE FROM oil_plan_recipes WHERE id = ? AND plan_id = ?", (recipe_id, plan_id))
- conn.commit()
- conn.close()
- return {"ok": True}
-
-
-@app.get("/api/oil-plans/{plan_id}/shopping-list")
-def plan_shopping_list(plan_id: int, user=Depends(get_current_user)):
- if not user.get("id"):
- raise HTTPException(403, "请先登录")
- conn = get_db()
- plan = conn.execute("SELECT * FROM oil_plans WHERE id = ?", (plan_id,)).fetchone()
- if not plan:
- conn.close()
- raise HTTPException(404)
- if plan["user_id"] != user["id"] and plan["teacher_id"] != user["id"] and user["role"] != "admin":
- conn.close()
- raise HTTPException(403)
- recipes = conn.execute("SELECT * FROM oil_plan_recipes WHERE plan_id = ?", (plan_id,)).fetchall()
- # Calculate monthly drops per oil
- oil_drops = {}
- for r in recipes:
- ings = json.loads(r["ingredients"])
- for ing in ings:
- name = ing.get("oil_name") or ing.get("oil", "")
- drops = ing.get("drops", 0)
- if name and name != "椰子油":
- oil_drops[name] = oil_drops.get(name, 0) + drops * r["times_per_month"]
- # Get oil metadata
- oils_meta = {}
- for row in conn.execute("SELECT name, bottle_price, drop_count FROM oils").fetchall():
- oils_meta[row["name"]] = {"bottle_price": row["bottle_price"], "drop_count": row["drop_count"]}
- # Get user inventory
- inv = set(r["oil_name"] for r in conn.execute(
- "SELECT oil_name FROM user_inventory WHERE user_id = ?", (plan["user_id"],)).fetchall())
- conn.close()
- import math
- result = []
- for oil, drops in sorted(oil_drops.items()):
- meta = oils_meta.get(oil, {})
- drop_count = meta.get("drop_count", 250)
- bottle_price = meta.get("bottle_price", 0)
- bottles = math.ceil(drops / drop_count) if drop_count else 1
- result.append({
- "oil_name": oil,
- "monthly_drops": round(drops, 1),
- "bottles_needed": bottles,
- "bottle_price": bottle_price,
- "total_cost": round(bottles * bottle_price, 2),
- "in_inventory": oil in inv,
- })
- result.sort(key=lambda x: (x["in_inventory"], x["oil_name"]))
- return result
-
+# ── Search Logging ─────────────────────────────────────
# ── Search Logging ─────────────────────────────────────
@app.post("/api/search-log")
def log_search(body: dict, user=Depends(get_current_user)):
diff --git a/frontend/src/components/UserMenu.vue b/frontend/src/components/UserMenu.vue
index f55268b..dc39e9b 100644
--- a/frontend/src/components/UserMenu.vue
+++ b/frontend/src/components/UserMenu.vue
@@ -44,8 +44,6 @@
-
-
@@ -167,16 +165,6 @@ function goRename(n) {
changeUsername()
}
-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) {
if (!n.title) return false
// Admin: review recipe/business/applications
@@ -319,8 +307,6 @@ onMounted(loadNotifications)
}
.notif-btn-added { color: #4a9d7e; border-color: #7ec6a4; }
.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:hover { background: #fff3e0; }
.notif-body { color: #888; font-size: 12px; margin-top: 2px; white-space: pre-line; }
diff --git a/frontend/src/stores/plans.js b/frontend/src/stores/plans.js
deleted file mode 100644
index 8a7b9fa..0000000
--- a/frontend/src/stores/plans.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import { defineStore } from 'pinia'
-import { ref, computed } from 'vue'
-import { api } from '../composables/useApi'
-
-export const usePlansStore = defineStore('plans', () => {
- const plans = ref([])
- const teachers = ref([])
- const shoppingList = ref([])
-
- const activePlan = computed(() => plans.value.find(p => p.status === 'active'))
- const pendingPlans = computed(() => plans.value.filter(p => p.status === 'pending'))
-
- async function loadPlans() {
- const res = await api('/api/oil-plans')
- if (res.ok) plans.value = await res.json()
- }
-
- async function loadTeachers() {
- const res = await api('/api/teachers')
- if (res.ok) teachers.value = await res.json()
- }
-
- async function createPlan(healthDesc, teacherId) {
- const res = await api('/api/oil-plans', {
- method: 'POST',
- body: JSON.stringify({ health_desc: healthDesc, teacher_id: teacherId }),
- })
- if (!res.ok) throw new Error('创建失败')
- await loadPlans()
- return res.json()
- }
-
- async function updatePlan(planId, data) {
- await api(`/api/oil-plans/${planId}`, {
- method: 'PUT',
- body: JSON.stringify(data),
- })
- await loadPlans()
- }
-
- async function addRecipe(planId, recipeName, ingredients, timesPerMonth) {
- await api(`/api/oil-plans/${planId}/recipes`, {
- method: 'POST',
- body: JSON.stringify({ recipe_name: recipeName, ingredients, times_per_month: timesPerMonth }),
- })
- await loadPlans()
- }
-
- async function removeRecipe(planId, recipeId) {
- await api(`/api/oil-plans/${planId}/recipes/${recipeId}`, { method: 'DELETE' })
- await loadPlans()
- }
-
- async function loadShoppingList(planId) {
- const res = await api(`/api/oil-plans/${planId}/shopping-list`)
- if (res.ok) shoppingList.value = await res.json()
- }
-
- return {
- plans, teachers, shoppingList,
- activePlan, pendingPlans,
- loadPlans, loadTeachers, createPlan, updatePlan,
- addRecipe, removeRecipe, loadShoppingList,
- }
-})
diff --git a/frontend/src/views/Inventory.vue b/frontend/src/views/Inventory.vue
index 1e481cc..6cdecc3 100644
--- a/frontend/src/views/Inventory.vue
+++ b/frontend/src/views/Inventory.vue
@@ -2,86 +2,10 @@
-
登录后可管理个人库存、获取购油方案
+
登录后可管理个人库存
-
-
-
-
-
-
-
- {{ r.recipe_name }} × {{ r.times_per_month }}/月
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 已匹配:{{ matchedTeacher.display_name }}
-
-
- 未找到该老师,请确认名字是否正确
-
-
-
-
-
-
-
- t.display_name === name || t.username === name
- ) || 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(() =>
- plansStore.shoppingList.filter(i => !i.in_inventory).reduce((s, i) => s + i.total_cost, 0).toFixed(2)
-)
-
-async function submitPlanRequest() {
- try {
- await plansStore.createPlan(planHealthDesc.value, matchedTeacher.value.id)
- showPlanRequest.value = false
- planHealthDesc.value = ''
- planTeacherName.value = ''
- matchedTeacher.value = null
- ui.showToast('已发送方案请求')
- } catch {
- ui.showToast('发送失败')
- }
-}
-
-async function addOilFromPlan(name) {
- if (!ownedOils.value.includes(name)) {
- ownedOils.value.push(name)
- ownedOils.value.sort((a, b) => a.localeCompare(b, 'zh'))
- await saveInventory()
- }
- // Refresh shopping list
- if (plansStore.activePlan) {
- await plansStore.loadShoppingList(plansStore.activePlan.id)
- }
- ui.showToast(`${name} 已加入库存`)
-}
-
async function clearAll() {
ownedOils.value = []
await saveInventory()
ui.showToast('已清空库存')
}
-onMounted(async () => {
- await loadInventory()
- if (auth.isLoggedIn) {
- await Promise.all([plansStore.loadPlans(), plansStore.loadTeachers()])
- if (plansStore.activePlan) {
- await plansStore.loadShoppingList(plansStore.activePlan.id)
- }
- }
+onMounted(() => {
+ loadInventory()
})
@@ -631,49 +481,6 @@ onMounted(async () => {
padding: 24px 0;
}
-/* Plan section */
-.plan-section {
- background: #f8faf8; border: 1.5px solid #d4e8d4; border-radius: 12px;
- padding: 14px; margin-bottom: 16px;
-}
-.plan-pending { border-color: #e5e4e7; background: #fafafa; }
-.plan-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
-.plan-title { font-weight: 600; font-size: 15px; color: #2e7d5a; }
-.plan-teacher { font-size: 12px; color: #b0aab5; }
-.plan-desc { font-size: 13px; color: #6b6375; margin: 0; }
-.plan-recipes-summary { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
-.plan-recipe-chip {
- font-size: 11px; padding: 3px 10px; border-radius: 8px;
- background: #e8f5e9; color: #2e7d5a; white-space: nowrap;
-}
-.shopping-list { margin-top: 8px; }
-.shopping-header { font-weight: 600; font-size: 14px; color: #3e3a44; margin-bottom: 4px; }
-.shopping-total { font-size: 12px; color: #5a7d5e; margin-bottom: 8px; font-weight: 500; }
-.shopping-item {
- display: flex; align-items: center; gap: 8px; padding: 6px 0;
- border-bottom: 1px solid #eee; font-size: 13px;
-}
-.shopping-item.owned { opacity: 0.5; }
-.shop-name { flex: 1; font-weight: 500; color: #3e3a44; }
-.shop-drops { color: #6b6375; font-size: 12px; white-space: nowrap; }
-.shop-bottles { color: #5a7d5e; font-size: 12px; white-space: nowrap; }
-.shop-cost { color: #5a7d5e; font-weight: 600; font-size: 12px; white-space: nowrap; }
-.shop-add-btn {
- padding: 2px 10px; border-radius: 6px; border: 1px solid #7ec6a4;
- background: #fff; color: #2e7d5a; font-size: 11px; cursor: pointer; font-family: inherit;
-}
-.shop-add-btn:hover { background: #e8f5e9; }
-.shop-owned-tag { font-size: 11px; color: #b0aab5; }
-.plan-request-bar { text-align: center; margin-bottom: 16px; }
-.overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); z-index: 100; display: flex; align-items: center; justify-content: center; }
-.overlay-panel { background: #fff; border-radius: 14px; width: 90%; max-width: 420px; max-height: 80vh; overflow-y: auto; }
-.overlay-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid #eee; }
-.overlay-header h3 { margin: 0; font-size: 16px; }
-.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; }
.login-prompt { text-align: center; padding: 60px 20px; color: #6b6375; }
.login-prompt p { margin-bottom: 16px; font-size: 15px; }
-.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-no-match { color: #c62828; font-size: 12px; margin-top: 4px; }
diff --git a/frontend/src/views/MyDiary.vue b/frontend/src/views/MyDiary.vue
index a92d73f..ccc47fa 100644
--- a/frontend/src/views/MyDiary.vue
+++ b/frontend/src/views/MyDiary.vue
@@ -208,7 +208,6 @@
-
🔑 修改密码
diff --git a/frontend/src/views/UserManagement.vue b/frontend/src/views/UserManagement.vue
index 25cd3e3..520c7ab 100644
--- a/frontend/src/views/UserManagement.vue
+++ b/frontend/src/views/UserManagement.vue
@@ -102,7 +102,6 @@
-
@@ -111,87 +110,6 @@
共 {{ users.length }} 个用户
-
-
-
📋 方案请求 ({{ pendingPlanRequests.length }})
-
-
-
-
{{ p.health_desc }}
-
-
-
-
-
-
-
📋 定制记录
-
-
-
-
{{ p.title }}
-
- {{ r.recipe_name }} ×{{ r.times_per_month }}/月
-
-
-
-
-
-
-
-
-
-
-
- 健康需求:{{ planEditing.health_desc }}
-
-
-
-
-
-
-
在下方搜索框搜索配方添加
-
- {{ pr.recipe_name }}
-
- 次/月
-
-
-
-
-
-
-
-
-
- {{ r.name }}
- {{ r.ingredients.map(i => i.oil).join('、') }}
-
-
-
- 未找到匹配的配方
-
-
-
-
-
-
-
-
-
-
-
![]()
@@ -203,15 +121,11 @@
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '../stores/auth'
import { useUiStore } from '../stores/ui'
-import { useRecipesStore } from '../stores/recipes'
-import { usePlansStore } from '../stores/plans'
import { api } from '../composables/useApi'
import { showConfirm, showPrompt } from '../composables/useDialog'
const auth = useAuthStore()
const ui = useUiStore()
-const recipeStore = useRecipesStore()
-const plansStore = usePlansStore()
const users = ref([])
const searchQuery = ref('')
@@ -426,142 +340,10 @@ async function rejectBusiness(app) {
}
}
-// Plan management
-const planEditing = ref(null)
-const planTitle = ref('')
-const planRecipes = ref([])
-const planRecipeSearch = ref('')
-
-const pendingPlanRequests = computed(() =>
- plansStore.plans.filter(p => p.status === 'pending')
-)
-const allTeacherPlans = computed(() =>
- plansStore.plans.filter(p => p.status !== 'pending')
-)
-
-const planSearchResults = computed(() => {
- if (!planRecipeSearch.value.trim()) return []
- const q = planRecipeSearch.value.trim().toLowerCase()
- const existing = new Set(planRecipes.value.map(r => r.recipe_name))
- return recipeStore.recipes
- .filter(r => r.name.toLowerCase().includes(q) && !existing.has(r.name))
- .slice(0, 10)
-})
-
-function openPlanEditor(user) {
- const existing = plansStore.plans.find(p => p.user_id === (user._id || user.id) && p.status !== 'archived')
- if (existing) {
- planEditing.value = { ...existing, user_name: user.display_name || user.username }
- planTitle.value = existing.title || ''
- planRecipes.value = (existing.recipes || []).map(r => ({ ...r }))
- } else {
- planEditing.value = { user_id: user._id || user.id, user_name: user.display_name || user.username, health_desc: '' }
- planTitle.value = ''
- planRecipes.value = []
- }
- planRecipeSearch.value = ''
-}
-
-function openPlanEditorForRequest(plan) {
- planEditing.value = { ...plan }
- planTitle.value = plan.title || ''
- planRecipes.value = (plan.recipes || []).map(r => ({ ...r }))
- planRecipeSearch.value = ''
-}
-
-function addRecipeToPlan(recipe) {
- planRecipes.value.push({
- recipe_name: recipe.name,
- ingredients: recipe.ingredients.map(i => ({ oil_name: i.oil, drops: i.drops })),
- times_per_month: 30,
- })
- planRecipeSearch.value = ''
-}
-
-async function saveDraft() {
- if (!planEditing.value) return
- try {
- let planId = planEditing.value.id
- if (planId) {
- await api(`/api/oil-plans/${planId}`, {
- method: 'PUT',
- body: JSON.stringify({ title: planTitle.value }),
- })
- // Sync recipes
- const existing = planEditing.value.recipes || []
- for (const r of existing) {
- await api(`/api/oil-plans/${planId}/recipes/${r.id}`, { method: 'DELETE' })
- }
- for (const r of planRecipes.value) {
- await api(`/api/oil-plans/${planId}/recipes`, {
- method: 'POST',
- body: JSON.stringify({ recipe_name: r.recipe_name, ingredients: r.ingredients, times_per_month: r.times_per_month }),
- })
- }
- await plansStore.loadPlans()
- ui.showToast('草稿已保存')
- }
- } catch {
- ui.showToast('保存失败')
- }
- planEditing.value = null
-}
-
-async function savePlan() {
- try {
- let planId = planEditing.value.id
- if (!planId) {
- // Create new plan
- const res = await api('/api/oil-plans', {
- method: 'POST',
- body: JSON.stringify({
- health_desc: planEditing.value.health_desc || '',
- teacher_id: auth.user.id,
- user_id: planEditing.value.user_id,
- }),
- })
- const data = await res.json()
- planId = data.id
- }
- // Update title
- await api(`/api/oil-plans/${planId}`, {
- method: 'PUT',
- body: JSON.stringify({ title: planTitle.value }),
- })
- // Sync recipes: remove old, add new
- const existing = planEditing.value.recipes || []
- for (const r of existing) {
- await api(`/api/oil-plans/${planId}/recipes/${r.id}`, { method: 'DELETE' })
- }
- for (const r of planRecipes.value) {
- await api(`/api/oil-plans/${planId}/recipes`, {
- method: 'POST',
- body: JSON.stringify({
- recipe_name: r.recipe_name,
- ingredients: r.ingredients,
- times_per_month: r.times_per_month,
- }),
- })
- }
- // Activate
- await api(`/api/oil-plans/${planId}`, {
- method: 'PUT',
- body: JSON.stringify({ status: 'active' }),
- })
- planEditing.value = null
- await plansStore.loadPlans()
- ui.showToast('方案已激活')
- } catch {
- ui.showToast('保存失败')
- }
-}
-
onMounted(() => {
loadUsers()
loadTranslations()
loadBusinessApps()
- plansStore.loadPlans()
- recipeStore.loadRecipes()
})
@@ -973,39 +755,4 @@ onMounted(() => {
}
}
-/* Plan editor */
-.overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); z-index: 100; display: flex; align-items: center; justify-content: center; }
-.overlay-panel { background: #fff; border-radius: 14px; width: 90%; max-height: 85vh; overflow-y: auto; }
-.overlay-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid #eee; }
-.overlay-header h3 { margin: 0; font-size: 16px; }
-.plan-health-desc { background: #f8f7f5; border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; font-size: 13px; color: #6b6375; }
-.form-label { display: block; font-size: 13px; color: #6b6375; margin-bottom: 4px; font-weight: 500; }
-.form-input { width: 100%; border: 1.5px solid #e5e4e7; border-radius: 8px; padding: 8px; font-size: 13px; font-family: inherit; box-sizing: border-box; }
-.plan-recipe-row {
- display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #f0f0f0;
-}
-.plan-recipe-name { flex: 1; font-size: 13px; font-weight: 500; }
-.plan-freq-input { width: 50px; padding: 4px 6px; border: 1px solid #e5e4e7; border-radius: 6px; font-size: 12px; text-align: center; }
-.plan-freq-label { font-size: 11px; color: #b0aab5; }
-.plan-add-recipe { position: relative; margin-top: 8px; }
-.plan-search-results {
- position: absolute; top: 100%; left: 0; right: 0; background: #fff;
- border: 1px solid #e5e4e7; border-radius: 8px; max-height: 200px; overflow-y: auto; z-index: 10;
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
-}
-.plan-search-item { padding: 8px 12px; cursor: pointer; font-size: 13px; border-bottom: 1px solid #f0f0f0; }
-.plan-search-item:hover { background: #f8faf8; }
-.plan-search-ings { display: block; font-size: 11px; color: #b0aab5; margin-top: 2px; }
-.plan-no-results { font-size: 12px; color: #b0aab5; padding: 8px 0; }
-.plan-empty-hint { font-size: 12px; color: #b0aab5; padding: 8px 0; }
-.plan-request-card { background: #fff; border: 1.5px solid #e5e4e7; border-radius: 10px; padding: 12px; margin-bottom: 8px; }
-.plan-request-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
-.plan-request-user { font-weight: 600; font-size: 14px; color: #3e3a44; flex: 1; }
-.plan-request-desc { font-size: 13px; color: #6b6375; line-height: 1.5; }
-.plan-status-tag { font-size: 11px; padding: 2px 8px; border-radius: 8px; }
-.status-active { background: #e8f5e9; color: #2e7d5a; }
-.status-archived { background: #f0eeeb; color: #6b6375; }
-.status-pending { background: #fff3e0; color: #e65100; }
-.plan-recipe-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
-.plan-chip { font-size: 11px; padding: 2px 8px; border-radius: 6px; background: #e8f5e9; color: #2e7d5a; }