feat: 商业认证+核算页面重写,管理入口移到用户菜单
All checks were successful
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 4s
PR Preview / deploy-preview (pull_request) Successful in 15s
Test / e2e-test (push) Successful in 55s

商业认证:
- 重写申请表单:认证类型、企业名称、联系电话、业务描述
- 状态栏样式:左侧彩色条(绿/橙/红)
- 用户管理页:同一用户只显示一条,可展开历史查看拒绝原因
- 后端 API 补充 reject_reason 字段

商业核算:
- 成分表改为标准表格(精油/用量/单价/小计)
- 总成本显示栏(绿色背景大字)
- 定价字段放在成本下方

管理入口:
- 操作日志/Bug/用户管理从主 tab 栏移到管理员用户菜单
- 添加配方按钮对所有用户可见

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 10:01:36 +00:00
parent ad95ba7d1f
commit 418986e46c
7 changed files with 223 additions and 102 deletions

View File

@@ -42,26 +42,53 @@
<button class="btn-outline btn-sm" @click="importFromRecipe">📋 从配方导入</button>
</div>
<!-- Ingredients Editor -->
<!-- Ingredients Table -->
<div class="ingredients-section">
<h4>🧴 配方成分</h4>
<div v-for="(ing, i) in selectedProject.ingredients" :key="i" class="ing-row">
<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>
<input
v-model.number="ing.drops"
type="number"
min="0"
class="form-input-sm"
placeholder="滴数"
@change="saveProject"
/>
<span class="ing-cost">{{ ing.oil ? oils.fmtPrice(oils.pricePerDrop(ing.oil) * (ing.drops || 0)) : '--' }}</span>
<button class="btn-icon-sm" @click="removeIngredient(i)"></button>
<div class="section-header-row">
<h4>🧴 配方成分</h4>
<div class="section-actions">
<button class="btn-outline btn-sm" @click="addIngredient"> 添加精油</button>
</div>
</div>
<table class="ingredients-table">
<thead>
<tr>
<th>精油</th>
<th>单次用量()</th>
<th>单价/</th>
<th>小计</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(ing, i) in selectedProject.ingredients" :key="i">
<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>
</td>
<td>
<input v-model.number="ing.drops" type="number" min="0" step="0.5" class="drops-input" @change="saveProject" />
</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>
</tr>
</tbody>
</table>
<div class="total-row">
<span class="total-label">配方总成本</span>
<span class="total-price">{{ oils.fmtPrice(materialCost) }}</span>
</div>
<!-- Pricing -->
<div class="pricing-inline">
<div class="price-field">
<label>定价 ¥</label>
<input v-model.number="selectedProject.selling_price" type="number" class="price-input" placeholder="/次" @change="saveProject" />
</div>
</div>
<button class="btn-outline btn-sm" @click="addIngredient">+ 添加成分</button>
</div>
<!-- Pricing Section -->
@@ -492,12 +519,38 @@ function formatDate(d) {
color: #3e3a44;
}
.ing-row {
display: flex;
gap: 6px;
align-items: center;
margin-bottom: 6px;
.section-header-row {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;
}
.section-header-row h4 { margin: 0; }
.section-actions { display: flex; gap: 6px; }
.ingredients-table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
.ingredients-table th {
text-align: center; padding: 10px 8px; font-size: 12px; font-weight: 600;
color: var(--text-light, #999); border-bottom: 2px solid #e5e4e7;
}
.ingredients-table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; text-align: center; }
.ingredients-table .form-select { width: 100%; padding: 6px 8px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 13px; font-family: inherit; background: #fff; }
.drops-input { width: 65px; padding: 6px 8px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 13px; text-align: center; outline: none; font-family: inherit; }
.drops-input:focus { border-color: #7ec6a4; }
.cell-ppd { color: #999; font-size: 12px; }
.cell-subtotal { color: #4a9d7e; font-weight: 600; }
.remove-btn { border: none; background: none; color: #ccc; cursor: pointer; font-size: 18px; }
.remove-btn:hover { color: #c0392b; }
.total-row {
background: #e8f5e9; border-radius: 12px; padding: 14px 18px;
display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;
}
.total-label { font-size: 14px; color: #3e3a44; font-weight: 500; }
.total-price { font-size: 20px; font-weight: 700; color: #2e7d5a; }
.pricing-inline { margin-top: 12px; }
.price-field { display: flex; align-items: center; gap: 8px; }
.price-field label { font-size: 13px; font-weight: 600; color: #3e3a44; white-space: nowrap; }
.price-input { width: 100px; padding: 8px 10px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 14px; font-family: inherit; outline: none; }
.price-input:focus { border-color: #7ec6a4; }
.form-select {
flex: 1;
@@ -520,14 +573,6 @@ function formatDate(d) {
text-align: center;
}
.ing-cost {
font-size: 13px;
color: #4a9d7e;
font-weight: 500;
min-width: 60px;
text-align: right;
}
.price-row {
display: flex;
justify-content: space-between;