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
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user