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