diff --git a/frontend/src/composables/useSmartPaste.js b/frontend/src/composables/useSmartPaste.js index a609767..2b15267 100644 --- a/frontend/src/composables/useSmartPaste.js +++ b/frontend/src/composables/useSmartPaste.js @@ -144,12 +144,16 @@ export function greedyMatchOils(text, oilNames) { /** * Parse text chunk into [{oil, drops}] pairs. * Handles formats like "芳香调理8永久花10" or "薰衣草 3滴 茶树 2ml" + * Also handles oil names without numbers, defaulting to 1 drop. */ export function parseOilChunk(text, oilNames) { const results = [] + // Match: name + optional number+unit const regex = /([^\d]+?)(\d+\.?\d*)\s*(ml|毫升|ML|mL|滴)?/g let match + let lastIndex = 0 while ((match = regex.exec(text)) !== null) { + lastIndex = regex.lastIndex const namePart = match[1].trim() let amount = parseFloat(match[2]) const unit = match[3] || '' @@ -164,7 +168,7 @@ export function parseOilChunk(text, oilNames) { if (matched.length > 0) { // Last matched oil gets the drops for (let i = 0; i < matched.length - 1; i++) { - results.push({ oil: matched[i], drops: 0 }) + results.push({ oil: matched[i], drops: 1 }) } results.push({ oil: matched[matched.length - 1], drops: amount }) } else { @@ -177,9 +181,41 @@ export function parseOilChunk(text, oilNames) { } } } + + if (lastIndex === 0) { + // Regex matched nothing — try the whole text as oil names without numbers + _parseNamesOnly(text.trim(), oilNames, results) + } else { + // Handle trailing text after last number match + const trailing = text.substring(lastIndex).trim() + if (trailing) { + _parseNamesOnly(trailing, oilNames, results) + } + } + return results } +/** Parse text that contains only oil names (no numbers), default 1 drop each. */ +function _parseNamesOnly(text, oilNames, results) { + // Try greedy match first + const matched = greedyMatchOils(text, oilNames) + if (matched.length > 0) { + for (const oil of matched) { + results.push({ oil, drops: 1 }) + } + return + } + // Fallback: try splitting by common delimiters and fuzzy match + const parts = text.split(/[\s+、,,]+/).filter(s => s) + for (const part of parts) { + const found = findOil(part, oilNames) + if (found) { + results.push({ oil: found, drops: 1 }) + } + } +} + /** * Split multi-recipe input by blank lines or semicolons. * Detects recipe boundaries (non-oil text after seeing oils = new recipe). @@ -266,8 +302,23 @@ export function parseSingleBlock(raw, oilNames) { * appears after some oils have been found, it starts a new recipe. */ export function parseMultiRecipes(raw, oilNames) { + // Split by blank lines into major blocks + const blankLineSplit = raw.split(/\n\s*\n/).map(s => s.trim()).filter(s => s) + if (blankLineSplit.length > 1) { + return blankLineSplit.flatMap(block => parseMultiRecipes(block, oilNames)) + } + // Split by semicolons only if both sides contain oil names + const semiParts = raw.split(/[;;]/).map(s => s.trim()).filter(s => s) + if (semiParts.length > 1) { + const hasOilInPart = p => oilNames.some(oil => p.includes(oil)) || + Object.keys(OIL_HOMOPHONES).some(a => p.includes(a)) + if (semiParts.every(hasOilInPart)) { + return semiParts.flatMap(block => parseMultiRecipes(block, oilNames)) + } + } + // First split by lines/commas, then within each part also try space splitting - const roughParts = raw.split(/[,,、\n\r]+/).map(s => s.trim()).filter(s => s) + const roughParts = raw.split(/[,,、;;\n\r]+/).map(s => s.trim()).filter(s => s) const parts = [] for (const rp of roughParts) { // If the part has spaces and contains mixed name+oil, split by spaces too diff --git a/frontend/src/views/RecipeManager.vue b/frontend/src/views/RecipeManager.vue index 16e0da0..d55c7e6 100644 --- a/frontend/src/views/RecipeManager.vue +++ b/frontend/src/views/RecipeManager.vue @@ -193,7 +193,7 @@