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 6s
PR Preview / deploy-preview (pull_request) Successful in 16s
Test / e2e-test (push) Successful in 49s
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 6s
PR Preview / deploy-preview (pull_request) Successful in 16s
Test / e2e-test (push) Successful in 49s
商业核算: - 加标语和示意项目(芳香调理),所有人可体验 - 管理员开放(不需认证) - 新增项目需认证,未认证提示 个人库存: - 搜索直接添加(回车或点击) - 4个套装快捷按钮(家庭医生/居家呵护3988/芳香调理/全精油) - 精油库默认折叠 - 配方匹配排除椰子油,降低门槛(至少1种匹配) 商业认证: - 简化为商户名+证明图片 - 通过后内容仍显示(和二维码页面一致) - 审核中内容不可修改 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,29 +1,29 @@
|
||||
<template>
|
||||
<div class="inventory-page">
|
||||
<!-- Search -->
|
||||
<!-- Search + direct add -->
|
||||
<div class="search-box">
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索精油..."
|
||||
placeholder="搜索精油名称,回车添加..."
|
||||
@keydown.enter="addFromSearch"
|
||||
/>
|
||||
<button v-if="searchQuery" class="search-clear-btn" @click="searchQuery = ''">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Oil Picker Grid -->
|
||||
<div class="section-label">点击添加到库存</div>
|
||||
<div class="oil-picker-grid">
|
||||
<div
|
||||
v-for="name in filteredOilNames"
|
||||
:key="name"
|
||||
class="oil-pick-chip"
|
||||
:class="{ owned: ownedSet.has(name) }"
|
||||
@click="toggleOil(name)"
|
||||
>
|
||||
<!-- Search results for direct add -->
|
||||
<div v-if="searchQuery && searchResults.length" class="search-results">
|
||||
<div v-for="name in searchResults" :key="name" class="search-result-item" @click="addOil(name)">
|
||||
<span class="pick-dot" :class="{ active: ownedSet.has(name) }">{{ ownedSet.has(name) ? '✓' : '+' }}</span>
|
||||
<span class="pick-name">{{ name }}</span>
|
||||
{{ name }}
|
||||
</div>
|
||||
<div v-if="filteredOilNames.length === 0" class="empty-hint">未找到精油</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick add kits -->
|
||||
<div class="kit-bar">
|
||||
<button class="kit-btn" @click="addKit('family')">家庭医生</button>
|
||||
<button class="kit-btn" @click="addKit('home3988')">居家呵护(3988)</button>
|
||||
<button class="kit-btn" @click="addKit('aroma')">芳香调理</button>
|
||||
<button class="kit-btn" @click="addKit('full')">全精油</button>
|
||||
</div>
|
||||
|
||||
<!-- Owned Oils Section -->
|
||||
@@ -36,7 +36,25 @@
|
||||
{{ name }} ✕
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-hint">暂未添加精油,点击上方精油添加到库存</div>
|
||||
<div v-else class="empty-hint">搜索添加精油,或点击上方套装快捷添加</div>
|
||||
|
||||
<!-- Oil Picker Grid (collapsed by default) -->
|
||||
<div class="section-header clickable" @click="showPicker = !showPicker">
|
||||
<span>📦 全部精油</span>
|
||||
<span class="toggle-icon">{{ showPicker ? '▾' : '▸' }}</span>
|
||||
</div>
|
||||
<div v-if="showPicker" class="oil-picker-grid">
|
||||
<div
|
||||
v-for="name in oils.oilNames"
|
||||
:key="name"
|
||||
class="oil-pick-chip"
|
||||
:class="{ owned: ownedSet.has(name) }"
|
||||
@click="toggleOil(name)"
|
||||
>
|
||||
<span class="pick-dot" :class="{ active: ownedSet.has(name) }">{{ ownedSet.has(name) ? '✓' : '+' }}</span>
|
||||
<span class="pick-name">{{ name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Matching Recipes Section -->
|
||||
<div class="section-header" style="margin-top:20px">
|
||||
@@ -85,32 +103,86 @@ const ui = useUiStore()
|
||||
const searchQuery = ref('')
|
||||
const ownedOils = ref([])
|
||||
const loading = ref(false)
|
||||
const showPicker = ref(false)
|
||||
|
||||
const ownedSet = computed(() => new Set(ownedOils.value))
|
||||
|
||||
const filteredOilNames = computed(() => {
|
||||
if (!searchQuery.value.trim()) return oils.oilNames
|
||||
const searchResults = computed(() => {
|
||||
if (!searchQuery.value.trim()) return []
|
||||
const q = searchQuery.value.trim().toLowerCase()
|
||||
return oils.oilNames.filter(n => n.toLowerCase().includes(q))
|
||||
return oils.oilNames.filter(n => n.toLowerCase().includes(q)).slice(0, 15)
|
||||
})
|
||||
|
||||
// Kit definitions
|
||||
const KITS = {
|
||||
family: ['乳香', '茶树', '薰衣草', '柠檬', '椒样薄荷', '保卫', '牛至', '乐活', '顺畅呼吸', '舒缓'],
|
||||
home3988: ['乳香', '野橘', '柠檬', '薰衣草', '椒样薄荷', '冬青', '茶树', '生姜', '柠檬草', '西班牙牛至',
|
||||
'西洋蓍草石榴籽', '乐活', '保卫', '新瑞活力', '舒缓', '安定情绪', '安宁神气', '顺畅呼吸', '柑橘清新', '芳香调理', '元气焕能'],
|
||||
aroma: ['芳香调理'],
|
||||
full: ['侧柏', '乳香', '雪松', '芫荽', '丝柏', '圆柚', '红橘', '冬青', '没药', '扁柏', '檀香', '姜黄', '玫瑰',
|
||||
'绿薄荷', '薰衣草', '永久花', '香蜂草', '迷迭香', '麦卢卡', '天竺葵', '蓝艾菊', '小茴香',
|
||||
'古巴香脂', '依兰依兰', '丁香花蕾', '柠檬尤加利', '藿香', '西班牙牛至尾草',
|
||||
'罗勒', '莱姆', '生姜', '柠檬', '茶树', '野橘', '香茅', '枫香',
|
||||
'芹菜籽', '岩兰草', '苦橙叶', '柠檬草', '山鸡椒', '黑云杉',
|
||||
'马郁兰', '佛手柑', '黑胡椒', '小豆蔻', '尤加利', '百里香',
|
||||
'椒样薄荷', '杜松浆果', '加州白鼠尾草',
|
||||
'快乐鼠尾草', '西伯利亚冷杉',
|
||||
'西班牙牛至', '斯里兰卡肉桂']
|
||||
}
|
||||
|
||||
function addKit(kitName) {
|
||||
const kit = KITS[kitName]
|
||||
if (!kit) return
|
||||
let added = 0
|
||||
for (const name of kit) {
|
||||
// Match existing oil names (fuzzy)
|
||||
const match = oils.oilNames.find(n => n === name) || oils.oilNames.find(n => n.includes(name) || name.includes(n))
|
||||
if (match && !ownedOils.value.includes(match)) {
|
||||
ownedOils.value.push(match)
|
||||
added++
|
||||
}
|
||||
}
|
||||
ownedOils.value.sort((a, b) => a.localeCompare(b, 'zh'))
|
||||
saveInventory()
|
||||
ui.showToast(`已添加 ${added} 种精油`)
|
||||
}
|
||||
|
||||
function addFromSearch() {
|
||||
if (searchResults.value.length > 0) {
|
||||
addOil(searchResults.value[0])
|
||||
}
|
||||
}
|
||||
|
||||
function addOil(name) {
|
||||
if (!ownedOils.value.includes(name)) {
|
||||
ownedOils.value.push(name)
|
||||
ownedOils.value.sort((a, b) => a.localeCompare(b, 'zh'))
|
||||
saveInventory()
|
||||
}
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
const matchingRecipes = computed(() => {
|
||||
if (ownedOils.value.length === 0) return []
|
||||
return recipeStore.recipes
|
||||
.filter(r => {
|
||||
const needed = r.ingredients.map(i => i.oil)
|
||||
// Exclude coconut oil from matching
|
||||
const needed = r.ingredients.filter(i => i.oil !== '椰子油').map(i => i.oil)
|
||||
if (needed.length === 0) return false
|
||||
const coverage = needed.filter(o => ownedSet.value.has(o)).length
|
||||
return coverage >= Math.ceil(needed.length * 0.5)
|
||||
// Show if at least 1 oil matches
|
||||
return coverage >= 1
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aCov = coverageRatio(a)
|
||||
const bCov = coverageRatio(b)
|
||||
return bCov - aCov
|
||||
if (bCov !== aCov) return bCov - aCov
|
||||
return a.name.localeCompare(b.name, 'zh')
|
||||
})
|
||||
})
|
||||
|
||||
function coverageRatio(recipe) {
|
||||
const needed = recipe.ingredients.map(i => i.oil)
|
||||
const needed = recipe.ingredients.filter(i => i.oil !== '椰子油').map(i => i.oil)
|
||||
if (needed.length === 0) return 0
|
||||
return needed.filter(o => ownedSet.value.has(o)).length / needed.length
|
||||
}
|
||||
@@ -205,6 +277,27 @@ onMounted(() => {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-bottom: 10px; max-height: 200px; overflow-y: auto;
|
||||
border: 1.5px solid #e5e4e7; border-radius: 10px; background: #fff;
|
||||
}
|
||||
.search-result-item {
|
||||
padding: 8px 12px; cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 6px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
.search-result-item:hover { background: #f0faf5; }
|
||||
.search-result-item:last-child { border-bottom: none; }
|
||||
|
||||
.kit-bar { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.kit-btn {
|
||||
padding: 5px 12px; border: 1.5px solid #e5e4e7; border-radius: 20px; background: #fff;
|
||||
font-size: 12px; cursor: pointer; font-family: inherit; color: #6b6375;
|
||||
}
|
||||
.kit-btn:hover { border-color: #7ec6a4; color: #4a9d7e; }
|
||||
|
||||
.clickable { cursor: pointer; }
|
||||
.toggle-icon { font-size: 12px; color: #999; margin-left: auto; }
|
||||
|
||||
.section-label {
|
||||
font-size: 12px;
|
||||
color: #b0aab5;
|
||||
|
||||
Reference in New Issue
Block a user