feat: 商业核算+个人库存+认证优化 #23

Merged
hera merged 14 commits from feat/inventory-commercial into main 2026-04-11 10:47:08 +00:00
Showing only changes of commit d041c8ed6f - Show all commits

View File

@@ -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; }