feat: 智能识别支持无数字精油名(默认1滴)、分号/空行分隔多配方
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Failing after 5s
Test / e2e-test (push) Has been skipped
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Failing after 5s
PR Preview / deploy-preview (pull_request) Has been skipped
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Failing after 5s
Test / e2e-test (push) Has been skipped
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Failing after 5s
PR Preview / deploy-preview (pull_request) Has been skipped
- 精油名后不写数字自动识别为1滴 - 分号分隔多配方(两边都有精油时) - 空行分隔多配方 - 混合格式支持(部分有数字部分无数字) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -144,12 +144,16 @@ export function greedyMatchOils(text, oilNames) {
|
|||||||
/**
|
/**
|
||||||
* Parse text chunk into [{oil, drops}] pairs.
|
* Parse text chunk into [{oil, drops}] pairs.
|
||||||
* Handles formats like "芳香调理8永久花10" or "薰衣草 3滴 茶树 2ml"
|
* Handles formats like "芳香调理8永久花10" or "薰衣草 3滴 茶树 2ml"
|
||||||
|
* Also handles oil names without numbers, defaulting to 1 drop.
|
||||||
*/
|
*/
|
||||||
export function parseOilChunk(text, oilNames) {
|
export function parseOilChunk(text, oilNames) {
|
||||||
const results = []
|
const results = []
|
||||||
|
// Match: name + optional number+unit
|
||||||
const regex = /([^\d]+?)(\d+\.?\d*)\s*(ml|毫升|ML|mL|滴)?/g
|
const regex = /([^\d]+?)(\d+\.?\d*)\s*(ml|毫升|ML|mL|滴)?/g
|
||||||
let match
|
let match
|
||||||
|
let lastIndex = 0
|
||||||
while ((match = regex.exec(text)) !== null) {
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
lastIndex = regex.lastIndex
|
||||||
const namePart = match[1].trim()
|
const namePart = match[1].trim()
|
||||||
let amount = parseFloat(match[2])
|
let amount = parseFloat(match[2])
|
||||||
const unit = match[3] || ''
|
const unit = match[3] || ''
|
||||||
@@ -164,7 +168,7 @@ export function parseOilChunk(text, oilNames) {
|
|||||||
if (matched.length > 0) {
|
if (matched.length > 0) {
|
||||||
// Last matched oil gets the drops
|
// Last matched oil gets the drops
|
||||||
for (let i = 0; i < matched.length - 1; i++) {
|
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 })
|
results.push({ oil: matched[matched.length - 1], drops: amount })
|
||||||
} else {
|
} 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
|
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.
|
* Split multi-recipe input by blank lines or semicolons.
|
||||||
* Detects recipe boundaries (non-oil text after seeing oils = new recipe).
|
* 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.
|
* appears after some oils have been found, it starts a new recipe.
|
||||||
*/
|
*/
|
||||||
export function parseMultiRecipes(raw, oilNames) {
|
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
|
// 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 = []
|
const parts = []
|
||||||
for (const rp of roughParts) {
|
for (const rp of roughParts) {
|
||||||
// If the part has spaces and contains mixed name+oil, split by spaces too
|
// If the part has spaces and contains mixed name+oil, split by spaces too
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
v-model="smartPasteText"
|
v-model="smartPasteText"
|
||||||
class="paste-input"
|
class="paste-input"
|
||||||
placeholder="直接粘贴配方文本,支持多条配方同时识别 例如: 舒缓放松,薰衣草3,茶树2 提神醒脑,柠檬5,椒样薄荷3"
|
placeholder="直接粘贴配方文本,支持多条配方同时识别 例如: 舒缓放松,薰衣草3,茶树2 提神醒脑,柠檬5,椒样薄荷3 不写数字默认1滴: 薰衣草,茶树,乳香"
|
||||||
rows="4"
|
rows="4"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button class="btn-primary btn-sm" @click="handleSmartPaste" :disabled="!smartPasteText.trim()">
|
<button class="btn-primary btn-sm" @click="handleSmartPaste" :disabled="!smartPasteText.trim()">
|
||||||
|
|||||||
Reference in New Issue
Block a user