feat: 商业核算+个人库存+认证优化 #23
@@ -13,19 +13,19 @@
|
||||
</div>
|
||||
|
||||
<div v-if="!selectedProject" class="project-list">
|
||||
<!-- Demo: use real 芳香调理技术 recipe -->
|
||||
<div class="project-card demo-card" @click="openDemo">
|
||||
<!-- Demo project (first one, or fallback) -->
|
||||
<div v-if="demoProject" class="project-card demo-card" @click="selectDemoProject">
|
||||
<div class="proj-header">
|
||||
<span class="proj-name">芳香调理技术</span>
|
||||
<span class="proj-name">{{ demoProject.name }}</span>
|
||||
<span class="proj-badge">体验</span>
|
||||
</div>
|
||||
<div class="proj-summary">
|
||||
<span>点击体验成本利润分析</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Real projects -->
|
||||
<!-- Real projects (exclude demo) -->
|
||||
<div
|
||||
v-for="p in projects"
|
||||
v-for="p in userProjects"
|
||||
:key="p._id || p.id"
|
||||
class="project-card"
|
||||
@click="selectProject(p)"
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="section-header-row">
|
||||
<h4>🧴 配方成分</h4>
|
||||
<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>
|
||||
<table class="ingredients-table">
|
||||
@@ -77,19 +77,29 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<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 v-if="isDemoMode && !auth.isAdmin">
|
||||
<span class="readonly-oil">{{ ing.oil || '—' }}</span>
|
||||
</template>
|
||||
<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>
|
||||
<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 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><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>
|
||||
</tbody>
|
||||
</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() {
|
||||
if (!auth.isBusiness && !auth.isAdmin) {
|
||||
showCertPrompt()
|
||||
@@ -289,23 +323,6 @@ function handleCreateProject() {
|
||||
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() {
|
||||
const name = await showPrompt('项目名称:', '新项目')
|
||||
if (!name) return
|
||||
@@ -353,7 +370,10 @@ function selectProject(p) {
|
||||
|
||||
async function saveProject() {
|
||||
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
|
||||
if (!id) return
|
||||
try {
|
||||
await api(`/api/projects/${id}`, {
|
||||
method: 'PUT',
|
||||
@@ -476,6 +496,10 @@ function formatDate(d) {
|
||||
.commercial-icon { font-size: 48px; margin-bottom: 8px; }
|
||||
.commercial-desc { font-size: 14px; color: var(--text-light, #999); }
|
||||
.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 h4 { margin: 0 0 12px; font-size: 14px; color: #3e3a44; }
|
||||
.consumption-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 10px; }
|
||||
|
||||
Reference in New Issue
Block a user