revert: 删除购油方案功能,修复MyDiary模板错误
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 14s
Test / e2e-test (push) Successful in 53s

完全移除oil_plans相关代码:
- 后端: 7个API端点、2个数据库表
- 前端: plans store、Inventory方案UI、UserManagement方案编辑器
- UserMenu: 方案通知按钮
- 修复MyDiary.vue多余的section-card div导致的构建失败

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 11:51:52 +00:00
parent e8a2915962
commit 9e1ebb3c86
7 changed files with 4 additions and 740 deletions

View File

@@ -44,8 +44,6 @@
<!-- 用户名更新通知去修改按钮已改过则显示已读 -->
<button v-else-if="isUsernameNotice(n) && !auth.user.username_changed" class="notif-action-btn notif-btn-plan" @click="goRename(n)">去修改</button>
<button v-else-if="isUsernameNotice(n) && auth.user.username_changed" class="notif-mark-one" @click="markOneRead(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>
<!-- 默认已读按钮 -->
@@ -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; }

View File

@@ -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,
}
})

View File

@@ -2,86 +2,10 @@
<div class="inventory-page">
<!-- Login prompt -->
<div v-if="!auth.isLoggedIn" class="login-prompt">
<p>登录后可管理个人库存获取购油方案</p>
<p>登录后可管理个人库存</p>
<button class="btn-primary" @click="ui.openLogin()">登录 / 注册</button>
</div>
<template v-else>
<!-- My Oil Plan -->
<template v-if="auth.isLoggedIn">
<!-- Active plan: show shopping list -->
<div v-if="plansStore.activePlan" class="plan-section">
<div class="plan-header">
<span class="plan-title">📋 {{ plansStore.activePlan.title || '我的购油方案' }}</span>
<span class="plan-teacher">by {{ plansStore.activePlan.teacher_name || '老师' }}</span>
</div>
<div class="plan-recipes-summary">
<span v-for="r in plansStore.activePlan.recipes" :key="r.id" class="plan-recipe-chip">
{{ r.recipe_name }} × {{ r.times_per_month }}/
</span>
</div>
<div v-if="plansStore.shoppingList.length" class="shopping-list">
<div class="shopping-header">🛒 本月购油清单</div>
<div class="shopping-total">
预计月消费¥{{ shoppingTotal }}
</div>
<div v-for="item in plansStore.shoppingList" :key="item.oil_name"
class="shopping-item" :class="{ owned: item.in_inventory }">
<span class="shop-name">{{ item.oil_name }}</span>
<span class="shop-drops">{{ item.monthly_drops }}/</span>
<span class="shop-bottles">{{ item.bottles_needed }}</span>
<span class="shop-cost">¥{{ item.total_cost }}</span>
<button v-if="!item.in_inventory" class="shop-add-btn" @click="addOilFromPlan(item.oil_name)">
加入库存
</button>
<span v-else class="shop-owned-tag">已有</span>
</div>
</div>
</div>
<!-- Pending plan: editable -->
<div v-else-if="plansStore.pendingPlans.length" class="plan-section plan-pending">
<div class="plan-header">
<span class="plan-title"> 老师正在为你定制方案中</span>
</div>
<textarea v-model="pendingDesc" class="form-textarea" rows="3" @blur="updatePendingDesc"></textarea>
<div class="plan-pending-hint">修改后老师会收到通知</div>
</div>
<!-- No plan: request button -->
<div v-else class="plan-request-bar">
<button class="btn-primary" @click="showPlanRequest = true">📋 请求定制购油方案</button>
</div>
<!-- Plan request modal -->
<div v-if="showPlanRequest" class="overlay" @mousedown.self="showPlanRequest = false">
<div class="overlay-panel" style="max-width:400px">
<div class="overlay-header">
<h3>请求定制方案</h3>
<button class="btn-close" @click="showPlanRequest = false"></button>
</div>
<div style="padding:16px">
<label class="form-label">描述你的健康需求</label>
<textarea v-model="planHealthDesc" class="form-textarea" rows="4"
placeholder="例如:家里有老人膝盖痛,小孩经常感冒咳嗽,我自己想改善睡眠..."></textarea>
<label class="form-label" style="margin-top:12px">输入老师的名字</label>
<input v-model="planTeacherName" class="form-input" placeholder="请输入老师告诉你的名字"
@input="matchTeacher" />
<div v-if="matchedTeacher" class="teacher-matched">
已匹配{{ matchedTeacher.display_name }}
</div>
<div v-else-if="planTeacherName.trim() && !matchedTeacher" class="teacher-no-match">
未找到该老师请确认名字是否正确
</div>
<button class="btn-primary" style="width:100%;margin-top:16px"
:disabled="!planHealthDesc.trim() || !matchedTeacher"
@click="submitPlanRequest">
发送请求
</button>
</div>
</div>
</div>
</template>
<!-- Search + direct add -->
<div class="search-box">
<input
@@ -176,14 +100,12 @@ import { useAuthStore } from '../stores/auth'
import { useOilsStore } from '../stores/oils'
import { useRecipesStore } from '../stores/recipes'
import { useUiStore } from '../stores/ui'
import { usePlansStore } from '../stores/plans'
import { api } from '../composables/useApi'
const auth = useAuthStore()
const oils = useOilsStore()
const recipeStore = useRecipesStore()
const ui = useUiStore()
const plansStore = usePlansStore()
const searchQuery = ref('')
const ownedOils = ref([])
@@ -318,86 +240,14 @@ async function toggleOil(name) {
await saveInventory()
}
// Plan request
const showPlanRequest = ref(false)
const planHealthDesc = ref('')
const planTeacherName = ref('')
const matchedTeacher = ref(null)
function matchTeacher() {
const name = planTeacherName.value.trim()
if (!name) { matchedTeacher.value = null; return }
matchedTeacher.value = plansStore.teachers.find(t =>
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()
})
</script>
@@ -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; }
</style>

View File

@@ -208,7 +208,6 @@
<!-- Account Tab -->
<div v-if="activeTab === 'account'" class="tab-content">
<div class="section-card">
<div class="section-card">
<h4>🔑 修改密码</h4>
<div class="form-group">

View File

@@ -102,7 +102,6 @@
</select>
<button v-if="!u.business_verified" class="btn-sm btn-outline" @click="grantBusiness(u)" title="开通商业认证">💼</button>
<button v-else class="btn-sm btn-outline" @click="revokeBusiness(u)" title="撤销商业认证" style="opacity:0.5">💼</button>
<button class="btn-sm btn-outline" @click="openPlanEditor(u)" title="定制方案">📋</button>
<button class="btn-sm btn-delete" @click="removeUser(u)" title="删除用户">🗑</button>
</div>
</div>
@@ -111,87 +110,6 @@
<div class="user-count"> {{ users.length }} 个用户</div>
<!-- Pending Plan Requests -->
<div v-if="pendingPlanRequests.length" class="review-section">
<h4 class="section-title">📋 方案请求 ({{ pendingPlanRequests.length }})</h4>
<div class="review-list">
<div v-for="p in pendingPlanRequests" :key="p.id" class="plan-request-card">
<div class="plan-request-header">
<span class="plan-request-user">{{ p.user_name || p.username || '用户' }} 的需求</span>
<button class="btn-sm btn-approve" @click="openPlanEditorForRequest(p)">去定制</button>
</div>
<div class="plan-request-desc">{{ p.health_desc }}</div>
</div>
</div>
</div>
<!-- All Plans (active/archived) -->
<div v-if="allTeacherPlans.length" class="review-section">
<h4 class="section-title">📋 定制记录</h4>
<div class="review-list">
<div v-for="p in allTeacherPlans" :key="p.id" class="plan-request-card">
<div class="plan-request-header">
<span class="plan-request-user">{{ p.user_name || p.username || '用户' }}</span>
<span class="plan-status-tag" :class="'status-' + p.status">{{ {active:'进行中',archived:'已归档',pending:'待定制'}[p.status] }}</span>
<button class="btn-sm btn-outline" @click="openPlanEditorForRequest(p)">编辑</button>
</div>
<div v-if="p.title" class="plan-request-desc">{{ p.title }}</div>
<div class="plan-recipe-chips">
<span v-for="r in (p.recipes || [])" :key="r.id" class="plan-chip">{{ r.recipe_name }} ×{{ r.times_per_month }}/</span>
</div>
</div>
</div>
</div>
<!-- Plan Editor Modal -->
<div v-if="planEditing" class="overlay" @mousedown.self="saveDraft">
<div class="overlay-panel" style="max-width:520px">
<div class="overlay-header">
<h3> {{ planEditing.user_name || '用户' }} 定制方案</h3>
<button class="btn-close" @click="saveDraft"></button>
</div>
<div style="padding:16px">
<div v-if="planEditing.health_desc" class="plan-health-desc">
<strong>健康需求</strong>{{ planEditing.health_desc }}
</div>
<label class="form-label">方案标题</label>
<input v-model="planTitle" class="form-input" placeholder="例如:家庭日常保健方案" />
<label class="form-label" style="margin-top:12px">已添加配方 ({{ planRecipes.length }})</label>
<div v-if="planRecipes.length === 0" class="plan-empty-hint">在下方搜索框搜索配方添加</div>
<div v-for="(pr, i) in planRecipes" :key="i" class="plan-recipe-row">
<span class="plan-recipe-name">{{ pr.recipe_name }}</span>
<input v-model.number="pr.times_per_month" type="number" min="1" class="plan-freq-input" />
<span class="plan-freq-label">/</span>
<button class="btn-icon-sm" @click="planRecipes.splice(i, 1)"></button>
</div>
<!-- Add recipe from public library -->
<label class="form-label" style="margin-top:12px">搜索配方添加</label>
<div class="plan-add-recipe">
<input v-model="planRecipeSearch" class="form-input" placeholder="输入配方名称搜索..." />
<div v-if="planRecipeSearch.trim() && planSearchResults.length" class="plan-search-results">
<div v-for="r in planSearchResults" :key="r._id" class="plan-search-item" @click="addRecipeToPlan(r)">
{{ r.name }}
<span class="plan-search-ings">{{ r.ingredients.map(i => i.oil).join('、') }}</span>
</div>
</div>
<div v-if="planRecipeSearch.trim() && !planSearchResults.length" class="plan-no-results">
未找到匹配的配方
</div>
</div>
<div style="display:flex;gap:8px;margin-top:16px">
<button class="btn-outline" style="flex:1" @click="saveDraft">保存草稿</button>
<button class="btn-primary" style="flex:1" @click="savePlan" :disabled="planRecipes.length === 0">
发送给会员
</button>
</div>
</div>
</div>
</div>
<!-- Full-size document preview -->
<div v-if="showDocFull" class="doc-overlay" @click="showDocFull = null">
<img :src="showDocFull" class="doc-full-img" />
@@ -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()
})
</script>
@@ -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; }
</style>