fix: 商业核算体验项目优化
Some checks failed
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 16s
Test / e2e-test (push) Has been cancelled

- 使用数据库中的真实芳香调理技术项目作为体验
- 去掉重复的项目,demo从项目列表隔离
- 非管理员:精油部分灰色只读,价格计算可修改,本地保存不影响其他人
- 管理员:可修改精油名称、滴数、添加精油(同步到数据库)
- 消耗分析:每种精油可做次数、最先消耗完提示

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 09:32:52 +00:00
parent c4005f229e
commit d041c8ed6f

View File

@@ -13,19 +13,19 @@
</div> </div>
<div v-if="!selectedProject" class="project-list"> <div v-if="!selectedProject" class="project-list">
<!-- Demo: use real 芳香调理技术 recipe --> <!-- Demo project (first one, or fallback) -->
<div class="project-card demo-card" @click="openDemo"> <div v-if="demoProject" class="project-card demo-card" @click="selectDemoProject">
<div class="proj-header"> <div class="proj-header">
<span class="proj-name">芳香调理技术</span> <span class="proj-name">{{ demoProject.name }}</span>
<span class="proj-badge">体验</span> <span class="proj-badge">体验</span>
</div> </div>
<div class="proj-summary"> <div class="proj-summary">
<span>点击体验成本利润分析</span> <span>点击体验成本利润分析</span>
</div> </div>
</div> </div>
<!-- Real projects --> <!-- Real projects (exclude demo) -->
<div <div
v-for="p in projects" v-for="p in userProjects"
:key="p._id || p.id" :key="p._id || p.id"
class="project-card" class="project-card"
@click="selectProject(p)" @click="selectProject(p)"
@@ -63,7 +63,7 @@
<div class="section-header-row"> <div class="section-header-row">
<h4>🧴 配方成分</h4> <h4>🧴 配方成分</h4>
<div class="section-actions"> <div class="section-actions">
<button class="btn-outline btn-sm" @click="addIngredient"> 添加精油</button> <button v-if="!isDemoMode || auth.isAdmin" class="btn-outline btn-sm" @click="addIngredient"> 添加精油</button>
</div> </div>
</div> </div>
<table class="ingredients-table"> <table class="ingredients-table">
@@ -77,19 +77,29 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(ing, i) in selectedProject.ingredients" :key="i"> <tr v-for="(ing, i) in selectedProject.ingredients" :key="i" :class="{ 'readonly-row': isDemoMode && !auth.isAdmin }">
<td> <td>
<select v-model="ing.oil" class="form-select" @change="saveProject"> <template v-if="isDemoMode && !auth.isAdmin">
<option value=""> 选择精油 </option> <span class="readonly-oil">{{ ing.oil || '—' }}</span>
<option v-for="name in oils.oilNames" :key="name" :value="name">{{ name }}</option> </template>
</select> <template v-else>
<select v-model="ing.oil" class="form-select" @change="saveProject">
<option value=""> 选择精油 </option>
<option v-for="name in oils.oilNames" :key="name" :value="name">{{ name }}</option>
</select>
</template>
</td> </td>
<td> <td>
<input v-model.number="ing.drops" type="number" min="0" step="0.5" class="drops-input" @change="saveProject" /> <template v-if="isDemoMode && !auth.isAdmin">
<span class="readonly-drops">{{ ing.drops }}</span>
</template>
<template v-else>
<input v-model.number="ing.drops" type="number" min="0" step="0.5" class="drops-input" @change="saveProject" />
</template>
</td> </td>
<td class="cell-ppd">{{ ing.oil ? oils.fmtPrice(oils.pricePerDrop(ing.oil)) : '—' }}</td> <td class="cell-ppd">{{ ing.oil ? oils.fmtPrice(oils.pricePerDrop(ing.oil)) : '—' }}</td>
<td class="cell-subtotal">{{ ing.oil && ing.drops ? oils.fmtPrice(oils.pricePerDrop(ing.oil) * ing.drops) : '—' }}</td> <td class="cell-subtotal">{{ ing.oil && ing.drops ? oils.fmtPrice(oils.pricePerDrop(ing.oil) * ing.drops) : '—' }}</td>
<td><button class="remove-btn" @click="removeIngredient(i)">×</button></td> <td><button v-if="!isDemoMode || auth.isAdmin" class="remove-btn" @click="removeIngredient(i)">×</button></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -281,6 +291,30 @@ async function loadProjects() {
} }
} }
// Demo = first project (芳香调理技术), managed by admin
const demoProject = computed(() => projects.value.find(p => p.name && p.name.includes('芳香调理')) || projects.value[0] || null)
const userProjects = computed(() => {
const demoId = demoProject.value?._id || demoProject.value?.id
return projects.value.filter(p => (p._id || p.id) !== demoId)
})
const isDemoMode = computed(() => selectedProject.value?._demo === true)
function selectDemoProject() {
const p = demoProject.value
if (!p) return
selectedProject.value = {
...p,
_demo: true,
ingredients: (p.ingredients || []).map(i => ({ ...i })),
packaging_cost: p.packaging_cost || 0,
labor_cost: p.labor_cost || 0,
other_cost: p.other_cost || 0,
selling_price: p.selling_price || 0,
quantity: p.quantity || 1,
notes: p.notes || '',
}
}
function handleCreateProject() { function handleCreateProject() {
if (!auth.isBusiness && !auth.isAdmin) { if (!auth.isBusiness && !auth.isAdmin) {
showCertPrompt() showCertPrompt()
@@ -289,23 +323,6 @@ function handleCreateProject() {
createProject() createProject()
} }
function openDemo() {
// Find the real 芳香调理技术 recipe
const recipe = recipeStore.recipes.find(r => r.name.includes('芳香调理技术') || r.name === '芳香调理')
const ings = recipe ? recipe.ingredients.map(i => ({ ...i })) : [{ oil: '芳香调理', drops: 12 }, { oil: '椰子油', drops: 186 }]
selectedProject.value = {
_demo: true,
name: recipe ? recipe.name : '芳香调理技术',
ingredients: ings,
packaging_cost: 5,
labor_cost: 30,
other_cost: 10,
selling_price: 198,
quantity: 1,
notes: '体验项目:修改数字不会影响其他用户',
}
}
async function createProject() { async function createProject() {
const name = await showPrompt('项目名称:', '新项目') const name = await showPrompt('项目名称:', '新项目')
if (!name) return if (!name) return
@@ -353,7 +370,10 @@ function selectProject(p) {
async function saveProject() { async function saveProject() {
if (!selectedProject.value) return if (!selectedProject.value) return
// Demo mode for non-admin: only save locally, don't hit API
if (isDemoMode.value && !auth.isAdmin) return
const id = selectedProject.value._id || selectedProject.value.id const id = selectedProject.value._id || selectedProject.value.id
if (!id) return
try { try {
await api(`/api/projects/${id}`, { await api(`/api/projects/${id}`, {
method: 'PUT', method: 'PUT',
@@ -476,6 +496,10 @@ function formatDate(d) {
.commercial-icon { font-size: 48px; margin-bottom: 8px; } .commercial-icon { font-size: 48px; margin-bottom: 8px; }
.commercial-desc { font-size: 14px; color: var(--text-light, #999); } .commercial-desc { font-size: 14px; color: var(--text-light, #999); }
.demo-card { border-style: dashed !important; opacity: 0.85; } .demo-card { border-style: dashed !important; opacity: 0.85; }
.readonly-row { background: #f8f7f5; }
.readonly-oil { font-size: 13px; color: #6b6375; }
.readonly-drops { font-size: 13px; color: #3e3a44; font-weight: 500; }
.consumption-section { margin-bottom: 20px; padding: 14px; background: #f8f7f5; border-radius: 12px; border: 1.5px solid #e5e4e7; } .consumption-section { margin-bottom: 20px; padding: 14px; background: #f8f7f5; border-radius: 12px; border: 1.5px solid #e5e4e7; }
.consumption-section h4 { margin: 0 0 12px; font-size: 14px; color: #3e3a44; } .consumption-section h4 { margin: 0 0 12px; font-size: 14px; color: #3e3a44; }
.consumption-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 10px; } .consumption-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 10px; }