-
-
-
-
-
-
-
-
-
-
-
-
-
- 稀释 1:
-
-
-
-
{{ formDilutionHint }}
+
+
+ {{ recipeSummaryText }}
@@ -387,10 +392,48 @@ const formCustomVolume = ref(100)
const formCustomUnit = ref('drops')
const formDilution = ref(6)
+const formCocoRow = ref({ oil: '椰子油', drops: 10, _search: '椰子油', _open: false })
+
+// EO ingredients (everything except coconut)
+const formEoIngredients = computed(() =>
+ formIngredients.value.filter(i => i.oil !== '椰子油')
+)
+
+const eoTotalDrops = computed(() =>
+ formEoIngredients.value.filter(i => i.oil && i.drops > 0).reduce((s, i) => s + i.drops, 0)
+)
+
+const targetTotalDrops = computed(() => {
+ if (formVolume.value === 'single') return null
+ if (formVolume.value === 'custom') return Math.round((formCustomVolume.value || 0) * DROPS_PER_ML)
+ return Math.round(Number(formVolume.value) * DROPS_PER_ML)
+})
+
+const cocoActualDrops = computed(() => {
+ if (!formCocoRow.value) return 0
+ if (formVolume.value === 'single') return formCocoRow.value.drops || 0
+ if (!targetTotalDrops.value) return 0
+ return Math.max(0, targetTotalDrops.value - eoTotalDrops.value)
+})
+
+const cocoFillMl = computed(() => Math.round(cocoActualDrops.value / DROPS_PER_ML))
+
+const recipeSummaryText = computed(() => {
+ const eo = eoTotalDrops.value
+ const coco = cocoActualDrops.value
+ const ratio = eo > 0 ? Math.round(coco / eo) : 0
+ if (formVolume.value === 'single') {
+ return `该配方为单次用量,纯精油 ${eo} 滴,椰子油 ${coco} 滴,稀释比例 1:${ratio}`
+ }
+ const vol = formVolume.value === 'custom' ? (formCustomVolume.value || 0) : Number(formVolume.value)
+ return `该配方总容量 ${vol}ml,纯精油 ${eo} 滴,剩余用椰子油填满,稀释比例 1:${ratio}`
+})
+
const formTotalCost = computed(() => {
- const cost = formIngredients.value
- .filter(i => i.oil && i.drops > 0)
+ let cost = formIngredients.value
+ .filter(i => i.oil && i.oil !== '椰子油' && i.drops > 0)
.reduce((sum, i) => sum + oils.pricePerDrop(i.oil) * i.drops, 0)
+ cost += oils.pricePerDrop('椰子油') * cocoActualDrops.value
return oils.fmtPrice(cost)
})
@@ -457,10 +500,20 @@ function onBatchSelect() {
}
function toggleSelectAllDiary() {
- if (selectedDiaryIds.size === myFilteredRecipes.value.length) {
+ const allMySelected = myFilteredRecipes.value.length > 0 && selectedDiaryIds.size === myFilteredRecipes.value.length
+ const allPubSelected = auth.canEdit && publicFilteredRecipes.value.length > 0 && selectedIds.size === publicFilteredRecipes.value.length
+ if (allMySelected && (!auth.canEdit || allPubSelected)) {
+ // All selected → deselect all
selectedDiaryIds.clear()
+ selectedIds.clear()
} else {
+ // Select all
myFilteredRecipes.value.forEach(d => selectedDiaryIds.add(d.id))
+ if (auth.canEdit) {
+ publicFilteredRecipes.value.forEach(r => selectedIds.add(r._id))
+ }
+ showMyRecipes.value = true
+ if (auth.canEdit) showPublicRecipes.value = true
}
}
@@ -551,7 +604,10 @@ function calcDilutionFromIngs(ingredients) {
function editRecipe(recipe) {
editingRecipe.value = recipe
formName.value = recipe.name
- formIngredients.value = recipe.ingredients.map(i => ({ ...i, _search: i.oil, _open: false }))
+ const ings = recipe.ingredients || []
+ formIngredients.value = ings.filter(i => i.oil !== '椰子油').map(i => ({ ...i, _search: i.oil, _open: false }))
+ const coco = ings.find(i => i.oil === '椰子油')
+ formCocoRow.value = coco ? { ...coco, _search: '椰子油', _open: false } : { oil: '椰子油', drops: 10, _search: '椰子油', _open: false }
formNote.value = recipe.note || ''
formTags.value = [...(recipe.tags || [])]
calcDilutionFromIngs(recipe.ingredients)
@@ -567,6 +623,7 @@ function closeOverlay() {
function resetForm() {
formName.value = ''
formIngredients.value = [{ oil: '', drops: 1, _search: '', _open: false }]
+ formCocoRow.value = { oil: '椰子油', drops: 10, _search: '椰子油', _open: false }
formNote.value = ''
formTags.value = []
smartPasteText.value = ''
@@ -647,7 +704,7 @@ const formDilutionHint = computed(() => {
if (formVolume.value === 'single') {
const cocoDrops = Math.round(eoDrops * formDilution.value)
const totalDrops = eoDrops + cocoDrops
- return `单次用量:纯精油约 ${eoDrops} 滴 + 椰子油约 ${cocoDrops} 滴,共 ${totalDrops} 滴 (${(totalDrops / DROPS_PER_ML).toFixed(1)}ml),稀释 1:${formDilution.value}`
+ return `单次用量:纯精油约 ${eoDrops} 滴 + 椰子油约 ${cocoDrops} 滴,共 ${totalDrops} 滴 (${Math.round(totalDrops / DROPS_PER_ML)}ml),稀释 1:${formDilution.value}`
}
let totalDrops
if (formVolume.value === 'custom') {
@@ -657,7 +714,7 @@ const formDilutionHint = computed(() => {
}
const targetEo = Math.round(totalDrops / (1 + formDilution.value))
const cocoDrops = totalDrops - targetEo
- return `总容量 ${totalDrops} 滴 (${(totalDrops / DROPS_PER_ML).toFixed(1)}ml),纯精油约 ${targetEo} 滴 + 椰子油约 ${cocoDrops} 滴,稀释 1:${formDilution.value}`
+ return `总容量 ${totalDrops} 滴 (${Math.round(totalDrops / DROPS_PER_ML)}ml),纯精油约 ${targetEo} 滴 + 椰子油约 ${cocoDrops} 滴,稀释 1:${formDilution.value}`
})
function applyVolumeDilution() {
@@ -703,6 +760,18 @@ function setFormCoconut(drops) {
}
}
+function addOilRow() {
+ formIngredients.value.push({ oil: '', drops: 1, _search: '', _open: false })
+}
+
+function removeEoRow(index) {
+ // Find the actual index in formIngredients (skip coconut)
+ const eoIngs = formIngredients.value.filter(i => i.oil !== '椰子油')
+ const target = eoIngs[index]
+ const realIdx = formIngredients.value.indexOf(target)
+ if (realIdx >= 0) formIngredients.value.splice(realIdx, 1)
+}
+
function confirmAddIng() {
if (!newIngOil.value || !newIngDrops.value) return
formIngredients.value.push({ oil: newIngOil.value, drops: newIngDrops.value, _search: newIngOil.value, _open: false })
@@ -719,17 +788,21 @@ function toggleFormTag(tag) {
}
async function saveCurrentRecipe() {
- const validIngs = formIngredients.value.filter(i => i.oil && i.drops > 0)
+ const eoIngs = formIngredients.value.filter(i => i.oil && i.oil !== '椰子油' && i.drops > 0)
if (!formName.value.trim()) {
ui.showToast('请输入配方名称')
return
}
- if (validIngs.length === 0) {
- ui.showToast('请至少添加一个成分')
+ if (eoIngs.length === 0) {
+ ui.showToast('请至少添加一个精油')
return
}
- const cleanIngs = validIngs.map(i => ({ oil: i.oil, drops: i.drops }))
+ // Combine EO + coconut
+ const cleanIngs = eoIngs.map(i => ({ oil: i.oil, drops: i.drops }))
+ if (formCocoRow.value && cocoActualDrops.value > 0) {
+ cleanIngs.push({ oil: '椰子油', drops: cocoActualDrops.value })
+ }
const diaryPayload = {
name: formName.value.trim(),
ingredients: cleanIngs,
@@ -864,7 +937,10 @@ onMounted(async () => {
function editDiaryRecipe(diary) {
editingRecipe.value = { _diary_id: diary.id, name: diary.name }
formName.value = diary.name
- formIngredients.value = (diary.ingredients || []).map(i => ({ ...i, _search: i.oil, _open: false }))
+ const ings = diary.ingredients || []
+ formIngredients.value = ings.filter(i => i.oil !== '椰子油').map(i => ({ ...i, _search: i.oil, _open: false }))
+ const coco = ings.find(i => i.oil === '椰子油')
+ formCocoRow.value = coco ? { ...coco, _search: '椰子油', _open: false } : { oil: '椰子油', drops: 10, _search: '椰子油', _open: false }
formNote.value = diary.note || ''
formTags.value = [...(diary.tags || [])]
calcDilutionFromIngs(diary.ingredients)
@@ -1419,12 +1495,23 @@ watch(() => recipeStore.recipes, () => {
.volume-btn.active { background: #e8f5e9; border-color: #7ec6a4; color: #2e7d5a; font-weight: 600; }
.volume-btn:hover { border-color: #7ec6a4; }
.custom-volume-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
-.dilution-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
-.dilution-label { font-size: 12px; color: #3e3a44; white-space: nowrap; }
+.coco-row { background: #f8faf8; }
+.coco-label { font-weight: 600; color: #4a9d7e; font-size: 13px; }
+.coco-fill { font-size: 12px; color: #4a9d7e; font-weight: 500; }
+.recipe-summary {
+ padding: 10px 14px; background: #f0faf5; border-radius: 10px; border-left: 3px solid #7ec6a4;
+ font-size: 13px; color: #2e7d5a; margin-bottom: 12px; line-height: 1.6;
+}
.drops-sm { width: 50px; padding: 4px 6px; border: 1.5px solid #d4cfc7; border-radius: 6px; font-size: 12px; text-align: center; outline: none; font-family: inherit; }
.drops-sm:focus { border-color: #7ec6a4; }
.select-sm { padding: 4px 6px; border: 1.5px solid #d4cfc7; border-radius: 6px; font-size: 12px; font-family: inherit; background: #fff; width: auto; }
.btn-select-active { background: #e8f5e9; color: #2e7d5a; border: 1.5px solid #7ec6a4; border-radius: 10px; padding: 7px 14px; font-size: 13px; cursor: pointer; font-family: inherit; font-weight: 600; }
+.export-btn {
+ margin-left: auto;
+ border: none; background: none; cursor: pointer; font-size: 16px; padding: 4px 6px;
+ opacity: 0.6;
+}
+.export-btn:hover { opacity: 1; }
.batch-count { font-size: 12px; color: #4a9d7e; font-weight: 600; white-space: nowrap; }
.batch-select { padding: 5px 8px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 12px; font-family: inherit; background: #fff; }