@@ -160,30 +185,42 @@
@@ -232,7 +269,7 @@ import { useDiaryStore } from '../stores/diary'
import { useUiStore } from '../stores/ui'
import { api } from '../composables/useApi'
import { showConfirm, showPrompt } from '../composables/useDialog'
-import { parseSingleBlock } from '../composables/useSmartPaste'
+import { parseSingleBlock, parseMultiRecipes } from '../composables/useSmartPaste'
import { matchesPinyinInitials } from '../composables/usePinyinMatch'
import RecipeCard from '../components/RecipeCard.vue'
import TagPicker from '../components/TagPicker.vue'
@@ -256,10 +293,18 @@ const pendingCount = ref(0)
// Form state
const formName = ref('')
-const formIngredients = ref([{ oil: '', drops: 1 }])
+const formIngredients = ref([{ oil: '', drops: 1, _search: '', _open: false }])
const formNote = ref('')
const formTags = ref([])
const smartPasteText = ref('')
+const parsedRecipes = ref([])
+
+const formTotalCost = computed(() => {
+ const cost = formIngredients.value
+ .filter(i => i.oil && i.drops > 0)
+ .reduce((sum, i) => sum + oils.pricePerDrop(i.oil) * i.drops, 0)
+ return oils.fmtPrice(cost)
+})
// Tag picker state
const showTagPicker = ref(false)
@@ -412,13 +457,26 @@ function resetForm() {
}
function handleSmartPaste() {
- const result = parseSingleBlock(smartPasteText.value, oils.oilNames)
- formName.value = result.name
- formIngredients.value = result.ingredients.length > 0
- ? result.ingredients
- : [{ oil: '', drops: 1 }]
- if (result.notFound.length > 0) {
- ui.showToast(`未识别: ${result.notFound.join(', ')}`)
+ const results = parseMultiRecipes(smartPasteText.value, oils.oilNames)
+ if (results.length === 0) {
+ ui.showToast('未能识别出任何配方')
+ return
+ }
+ if (results.length === 1) {
+ // Single recipe: populate form directly
+ const r = results[0]
+ formName.value = r.name
+ formIngredients.value = r.ingredients.length > 0
+ ? r.ingredients.map(i => ({ ...i, _search: i.oil, _open: false }))
+ : [{ oil: '', drops: 1, _search: '', _open: false }]
+ if (r.notFound.length > 0) {
+ ui.showToast(`未识别: ${r.notFound.join('、')}`)
+ }
+ parsedRecipes.value = []
+ } else {
+ // Multiple recipes: show preview cards
+ parsedRecipes.value = results
+ ui.showToast(`识别出 ${results.length} 条配方`)
}
}
@@ -461,17 +519,17 @@ async function saveCurrentRecipe() {
return
}
- const payload = {
- name: formName.value.trim(),
- ingredients: validIngs,
- note: formNote.value,
- tags: formTags.value,
- }
+ const cleanIngs = validIngs.map(i => ({ oil: i.oil, drops: i.drops }))
if (editingRecipe.value && editingRecipe.value._diary_id) {
// Editing a diary (personal) recipe
try {
- await diaryStore.updateDiary(editingRecipe.value._diary_id, payload)
+ await diaryStore.updateDiary(editingRecipe.value._diary_id, {
+ name: formName.value.trim(),
+ ingredients: cleanIngs,
+ note: formNote.value,
+ tags: formTags.value,
+ })
ui.showToast('个人配方已更新')
closeOverlay()
} catch (e) {
@@ -480,6 +538,14 @@ async function saveCurrentRecipe() {
return
}
+ // Public recipe: API expects oil_name
+ const payload = {
+ name: formName.value.trim(),
+ ingredients: cleanIngs.map(i => ({ oil_name: i.oil, drops: i.drops })),
+ note: formNote.value,
+ tags: formTags.value,
+ }
+
if (editingRecipe.value) {
payload._id = editingRecipe.value._id
payload._version = editingRecipe.value._version
@@ -494,6 +560,46 @@ async function saveCurrentRecipe() {
}
}
+async function saveParsedRecipe(index) {
+ const r = parsedRecipes.value[index]
+ if (!r.name.trim() || r.ingredients.length === 0) {
+ ui.showToast('配方名称和成分不能为空')
+ return
+ }
+ try {
+ await diaryStore.createDiary({
+ name: r.name.trim(),
+ ingredients: r.ingredients.map(i => ({ oil: i.oil, drops: i.drops })),
+ note: '',
+ tags: [],
+ })
+ parsedRecipes.value.splice(index, 1)
+ ui.showToast(`「${r.name}」已保存到我的配方`)
+ } catch (e) {
+ ui.showToast('保存失败: ' + (e?.message || '未知错误'))
+ }
+}
+
+async function saveAllParsed() {
+ let saved = 0
+ for (let i = parsedRecipes.value.length - 1; i >= 0; i--) {
+ const r = parsedRecipes.value[i]
+ if (!r.name.trim() || r.ingredients.length === 0) continue
+ try {
+ await diaryStore.createDiary({
+ name: r.name.trim(),
+ ingredients: r.ingredients.map(i => ({ oil: i.oil, drops: i.drops })),
+ note: '',
+ tags: [],
+ })
+ saved++
+ } catch {}
+ }
+ parsedRecipes.value = []
+ ui.showToast(`已保存 ${saved} 条配方到我的配方`)
+ closeOverlay()
+}
+
// Load diary on mount
onMounted(async () => {
if (auth.isLoggedIn) {
@@ -912,6 +1018,33 @@ watch(() => recipeStore.recipes, () => {
border-color: #7ec6a4;
}
+.parsed-results { margin: 12px 0; }
+.parsed-recipe-card {
+ background: #f8faf8;
+ border: 1.5px solid #d4e8d4;
+ border-radius: 10px;
+ padding: 12px;
+ margin-bottom: 10px;
+}
+.parsed-header { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
+.parsed-name { flex: 1; font-weight: 600; }
+.parsed-ings { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
+.parsed-ing {
+ display: flex; align-items: center; gap: 4px;
+ background: #fff; border: 1px solid #e5e4e7; border-radius: 8px; padding: 4px 8px; font-size: 13px;
+}
+.parsed-oil { color: #3e3a44; font-weight: 500; }
+.parsed-ing .form-input-sm { width: 50px; padding: 4px 6px; font-size: 12px; }
+.parsed-warn { color: #e65100; font-size: 12px; margin-bottom: 6px; }
+.parsed-actions { display: flex; gap: 8px; justify-content: center; margin-top: 8px; }
+
+.editor-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 8px; }
+.editor-table th { text-align: left; padding: 6px 4px; color: #999; font-weight: 500; font-size: 12px; border-bottom: 1px solid #eee; }
+.editor-table td { padding: 6px 4px; border-bottom: 1px solid #f5f5f5; }
+.cell-muted { color: #b0aab5; font-size: 12px; }
+.cell-cost { color: #4a9d7e; font-weight: 500; font-size: 13px; }
+.form-total { text-align: right; font-size: 14px; font-weight: 600; color: #4a9d7e; margin-top: 8px; }
+
.divider-text {
text-align: center;
color: #b0aab5;