feat: 购油方案功能 #28

Merged
hera merged 13 commits from fix/next-batch-2 into main 2026-04-13 13:07:09 +00:00
Showing only changes of commit e8a2915962 - Show all commits

View File

@@ -113,35 +113,53 @@
<!-- Pending Plan Requests -->
<div v-if="pendingPlanRequests.length" class="review-section">
<h4 class="section-title">📋 方案请求</h4>
<h4 class="section-title">📋 方案请求 ({{ pendingPlanRequests.length }})</h4>
<div class="review-list">
<div v-for="p in pendingPlanRequests" :key="p.id" class="review-item">
<div class="review-info">
<span class="review-name">{{ p.user_name || p.username || '用户' }}</span>
<span class="review-reason">{{ p.health_desc }}</span>
<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="review-actions">
<button class="btn-sm btn-approve" @click="openPlanEditorForRequest(p)">制定方案</button>
<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="planEditing = null">
<div class="overlay-panel" style="max-width:500px">
<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="planEditing = null"></button>
<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">配方列表</label>
<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" />
@@ -150,18 +168,24 @@
</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 && planSearchResults.length" class="plan-search-results">
<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">
{{ planEditing.id ? '保存并激活' : '创建并激活' }}
发送给会员
</button>
</div>
</div>
@@ -411,6 +435,9 @@ 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 []
@@ -451,6 +478,35 @@ function addRecipeToPlan(recipe) {
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
@@ -505,6 +561,7 @@ onMounted(() => {
loadTranslations()
loadBusinessApps()
plansStore.loadPlans()
recipeStore.loadRecipes()
})
</script>
@@ -936,6 +993,19 @@ onMounted(() => {
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; }
.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>