505 lines
14 KiB
JavaScript
505 lines
14 KiB
JavaScript
import { regionData } from 'element-china-area-data';
|
||
|
||
const GENERIC_CITY_LABELS = new Set([
|
||
'市辖区',
|
||
'县',
|
||
'自治区直辖县级行政区划',
|
||
'省直辖县级行政区划'
|
||
]);
|
||
|
||
const NAME_LABELS = ['收货人', '收件人', '联系人', '姓名'];
|
||
const PHONE_LABELS = ['手机号码', '手机号', '联系电话', '联系方式', '电话', '手机'];
|
||
const REGION_LABELS = ['所在地区', '地区'];
|
||
const DETAIL_LABELS = ['详细地址', '收货地址', '地址'];
|
||
const ALL_FIELD_LABELS = [
|
||
...new Set([...NAME_LABELS, ...PHONE_LABELS, ...REGION_LABELS, ...DETAIL_LABELS])
|
||
];
|
||
|
||
const PHONE_REG = /(?<!\d)(?:\+?86[-\s]?)?(1[3-9](?:[\s-]?\d){9})(?!\d)/;
|
||
const ADDRESS_SIGNAL_REG = /(省|市|区|县|旗|乡|镇|街道|大道|路|街|巷|弄|村|社区|小区|号|栋|单元|室|楼)/;
|
||
const FIELD_LABEL_PATTERN = createLabelPattern(ALL_FIELD_LABELS);
|
||
const STRIP_FIELD_LABEL_REG = new RegExp(
|
||
`(^|[\\s,,;;\\n])(?:${FIELD_LABEL_PATTERN})\\s*[::]`,
|
||
'g'
|
||
);
|
||
|
||
const provinceOptions = regionData.map(({ label, value }) => ({ label, value }));
|
||
|
||
const districtRecords = regionData.flatMap((province) =>
|
||
(province.children || []).flatMap((city) =>
|
||
(city.children || []).map((district) => {
|
||
const provinceAliases = createProvinceAliases(province.label);
|
||
const cityAliases = createCityAliases(getDisplayCityLabel(city.label));
|
||
const districtAliases = createDistrictAliases(district.label);
|
||
|
||
return {
|
||
province,
|
||
city,
|
||
district,
|
||
provinceAliases,
|
||
cityAliases,
|
||
districtAliases,
|
||
provinceSearchAliases: provinceAliases.map(normalizeForSearch).filter(Boolean),
|
||
citySearchAliases: cityAliases.map(normalizeForSearch).filter(Boolean),
|
||
districtSearchAliases: districtAliases.map(normalizeForSearch).filter(Boolean),
|
||
fullSearchLabels: buildFullSearchLabels(provinceAliases, cityAliases, districtAliases)
|
||
};
|
||
})
|
||
)
|
||
);
|
||
|
||
function escapeRegExp(value = '') {
|
||
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
}
|
||
|
||
function createLabelPattern(labels = []) {
|
||
return labels
|
||
.slice()
|
||
.sort((left, right) => right.length - left.length)
|
||
.map((label) => escapeRegExp(label))
|
||
.join('|');
|
||
}
|
||
|
||
function normalizeText(value = '') {
|
||
return String(value)
|
||
.replace(/\r/g, '\n')
|
||
.replace(/\u00a0/g, ' ')
|
||
.replace(/\t/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function normalizeForSearch(value = '') {
|
||
return normalizeText(value).replace(/[\s,,.。;;::、\-_/]/g, '');
|
||
}
|
||
|
||
function stripProvinceSuffix(label = '') {
|
||
return normalizeText(label).replace(
|
||
/(特别行政区|维吾尔自治区|壮族自治区|回族自治区|自治区|省|市)$/u,
|
||
''
|
||
);
|
||
}
|
||
|
||
function stripCitySuffix(label = '') {
|
||
return normalizeText(label).replace(/(自治州|地区|盟|市)$/u, '');
|
||
}
|
||
|
||
function stripDistrictSuffix(label = '') {
|
||
return normalizeText(label).replace(/(自治县|自治旗|区|县|旗|市)$/u, '');
|
||
}
|
||
|
||
function createAliases(label = '', stripFn = null) {
|
||
const rawLabel = normalizeText(label);
|
||
const aliases = new Set([rawLabel]);
|
||
const simplifiedLabel = stripFn ? normalizeText(stripFn(rawLabel)) : '';
|
||
|
||
if (simplifiedLabel && simplifiedLabel !== rawLabel) {
|
||
aliases.add(simplifiedLabel);
|
||
}
|
||
|
||
return [...aliases].filter(Boolean);
|
||
}
|
||
|
||
function createProvinceAliases(label = '') {
|
||
return createAliases(label, stripProvinceSuffix);
|
||
}
|
||
|
||
function createCityAliases(label = '') {
|
||
if (!label) return [''];
|
||
return createAliases(label, stripCitySuffix);
|
||
}
|
||
|
||
function createDistrictAliases(label = '') {
|
||
return createAliases(label, stripDistrictSuffix);
|
||
}
|
||
|
||
function buildFullSearchLabels(provinceAliases = [], cityAliases = [], districtAliases = []) {
|
||
const fullLabels = new Set();
|
||
const normalizedCityAliases = cityAliases.length ? cityAliases : [''];
|
||
|
||
provinceAliases.forEach((provinceAlias) => {
|
||
normalizedCityAliases.forEach((cityAlias) => {
|
||
districtAliases.forEach((districtAlias) => {
|
||
const combinedLabel = normalizeForSearch(`${provinceAlias}${cityAlias}${districtAlias}`);
|
||
if (combinedLabel) {
|
||
fullLabels.add(combinedLabel);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
return [...fullLabels];
|
||
}
|
||
|
||
function getDisplayCityLabel(label = '') {
|
||
return GENERIC_CITY_LABELS.has(label) ? '' : label;
|
||
}
|
||
|
||
function getProvinceByCode(provinceCode) {
|
||
return regionData.find((item) => item.value === provinceCode) || null;
|
||
}
|
||
|
||
function getCityByCode(provinceCode, cityCode) {
|
||
const province = getProvinceByCode(provinceCode);
|
||
return province?.children?.find((item) => item.value === cityCode) || null;
|
||
}
|
||
|
||
function getDistrictByCode(provinceCode, cityCode, districtCode) {
|
||
const city = getCityByCode(provinceCode, cityCode);
|
||
return city?.children?.find((item) => item.value === districtCode) || null;
|
||
}
|
||
|
||
function getCityOptions(provinceCode) {
|
||
const province = getProvinceByCode(provinceCode) || regionData[0];
|
||
return (province?.children || []).map(({ label, value }) => ({ label, value }));
|
||
}
|
||
|
||
function getDistrictOptions(provinceCode, cityCode) {
|
||
const province = getProvinceByCode(provinceCode) || regionData[0];
|
||
const city =
|
||
province?.children?.find((item) => item.value === cityCode) ||
|
||
province?.children?.[0] ||
|
||
null;
|
||
|
||
return (city?.children || []).map(({ label, value }) => ({ label, value }));
|
||
}
|
||
|
||
function getLabelParts(regionCodes = []) {
|
||
const [provinceCode, cityCode, districtCode] = regionCodes;
|
||
const province = getProvinceByCode(provinceCode);
|
||
const city = getCityByCode(provinceCode, cityCode);
|
||
const district = getDistrictByCode(provinceCode, cityCode, districtCode);
|
||
|
||
if (!province || !city || !district) {
|
||
return [];
|
||
}
|
||
|
||
const displayCityLabel = getDisplayCityLabel(city.label);
|
||
return [province.label, displayCityLabel, district.label].filter(Boolean);
|
||
}
|
||
|
||
function stripFieldLabels(value = '') {
|
||
return normalizeText(value).replace(STRIP_FIELD_LABEL_REG, '$1');
|
||
}
|
||
|
||
function cleanupAddressText(value = '') {
|
||
return stripFieldLabels(value)
|
||
.replace(/[::]/g, ' ')
|
||
.replace(/[,,;;]/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function cleanupName(value = '') {
|
||
return normalizeText(value)
|
||
.replace(/^[\s,,;;::]+/, '')
|
||
.replace(/[\s,,;;::]+$/, '')
|
||
.trim();
|
||
}
|
||
|
||
function findBestAliasInText(text = '', aliases = []) {
|
||
return aliases
|
||
.slice()
|
||
.sort((left, right) => right.length - left.length)
|
||
.find((alias) => alias && text.includes(alias)) || '';
|
||
}
|
||
|
||
function extractPhone(rawText = '') {
|
||
const text = normalizeText(rawText);
|
||
const match = text.match(PHONE_REG);
|
||
if (!match) {
|
||
return {
|
||
phone: '',
|
||
textWithoutPhone: text
|
||
};
|
||
}
|
||
|
||
const digits = match[1].replace(/\D/g, '');
|
||
const phone = digits.length > 11 ? digits.slice(-11) : digits;
|
||
|
||
return {
|
||
phone,
|
||
textWithoutPhone: text.replace(match[0], ' ').replace(/\s+/g, ' ').trim()
|
||
};
|
||
}
|
||
|
||
function pickBestMatch(records, matcher) {
|
||
return records
|
||
.filter(matcher)
|
||
.sort((left, right) => {
|
||
return (
|
||
right.fullSearchLabels[0]?.length - left.fullSearchLabels[0]?.length ||
|
||
right.district.label.length - left.district.label.length
|
||
);
|
||
})[0] || null;
|
||
}
|
||
|
||
function matchAnyAlias(searchText = '', aliases = []) {
|
||
return aliases.some((alias) => alias && searchText.includes(alias));
|
||
}
|
||
|
||
function findRegionMatch(rawText = '') {
|
||
const searchText = normalizeForSearch(rawText);
|
||
if (!searchText) {
|
||
return null;
|
||
}
|
||
|
||
let match = pickBestMatch(districtRecords, (record) => {
|
||
return record.fullSearchLabels.some((label) => searchText.includes(label));
|
||
});
|
||
|
||
if (!match) {
|
||
match = pickBestMatch(districtRecords, (record) => {
|
||
return (
|
||
matchAnyAlias(searchText, record.provinceSearchAliases) &&
|
||
matchAnyAlias(searchText, record.districtSearchAliases) &&
|
||
(!record.citySearchAliases.length || matchAnyAlias(searchText, record.citySearchAliases))
|
||
);
|
||
});
|
||
}
|
||
|
||
if (!match) {
|
||
match = pickBestMatch(districtRecords, (record) => {
|
||
return (
|
||
matchAnyAlias(searchText, record.citySearchAliases) &&
|
||
matchAnyAlias(searchText, record.districtSearchAliases)
|
||
);
|
||
});
|
||
}
|
||
|
||
return match;
|
||
}
|
||
|
||
function getRegionStartIndex(text, match) {
|
||
const candidates = [
|
||
findBestAliasInText(text, match.provinceAliases),
|
||
findBestAliasInText(text, match.cityAliases),
|
||
findBestAliasInText(text, match.districtAliases)
|
||
]
|
||
.filter(Boolean)
|
||
.map((label) => text.indexOf(label))
|
||
.filter((index) => index >= 0);
|
||
|
||
return candidates.length ? Math.min(...candidates) : -1;
|
||
}
|
||
|
||
function stripMatchedRegion(addressText, match) {
|
||
let detailAddress = normalizeText(addressText);
|
||
const matchedProvince = findBestAliasInText(detailAddress, match.provinceAliases);
|
||
const matchedCity = findBestAliasInText(detailAddress, match.cityAliases);
|
||
const matchedDistrict = findBestAliasInText(detailAddress, match.districtAliases);
|
||
|
||
[matchedProvince, matchedCity, matchedDistrict]
|
||
.filter(Boolean)
|
||
.forEach((label) => {
|
||
detailAddress = detailAddress.replace(label, '');
|
||
});
|
||
|
||
return cleanupAddressText(detailAddress);
|
||
}
|
||
|
||
function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartIndex = -1) {
|
||
const sourceText =
|
||
regionStartIndex > 0 ? textWithoutPhone.slice(0, regionStartIndex) : textWithoutPhone || rawText;
|
||
|
||
const directNameMatch = normalizeText(rawText).match(
|
||
/(?:收货人|收件人|联系人|姓名)\s*[::]?\s*([A-Za-z\u4e00-\u9fa5·]{2,20})/u
|
||
);
|
||
if (directNameMatch?.[1]) {
|
||
return directNameMatch[1].trim();
|
||
}
|
||
|
||
const cleaned = cleanupAddressText(sourceText)
|
||
.replace(PHONE_REG, ' ')
|
||
.replace(/\d+/g, ' ')
|
||
.trim();
|
||
const tokens = cleaned.split(/\s+/).filter(Boolean);
|
||
const nameToken = tokens.find((token) => {
|
||
return !ADDRESS_SIGNAL_REG.test(token) && /^[A-Za-z\u4e00-\u9fa5·]{2,20}$/u.test(token);
|
||
});
|
||
|
||
return nameToken || '';
|
||
}
|
||
|
||
function extractAddressSource(textWithoutPhone = '', name = '', match = null) {
|
||
let addressSource = normalizeText(textWithoutPhone);
|
||
|
||
if (name) {
|
||
addressSource = addressSource.replace(name, ' ').replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
if (match) {
|
||
const regionStartIndex = getRegionStartIndex(addressSource, match);
|
||
if (regionStartIndex >= 0) {
|
||
return addressSource.slice(regionStartIndex).trim();
|
||
}
|
||
}
|
||
|
||
return cleanupAddressText(addressSource);
|
||
}
|
||
|
||
function formatRegionCodes(match) {
|
||
return [match.province.value, match.city.value, match.district.value];
|
||
}
|
||
|
||
function combineDetailParts(parts = []) {
|
||
return parts
|
||
.map((item) => cleanupAddressText(item))
|
||
.filter(Boolean)
|
||
.join('');
|
||
}
|
||
|
||
function extractFieldValue(text = '', labels = []) {
|
||
if (!text || !labels.length) {
|
||
return '';
|
||
}
|
||
|
||
const labelPattern = createLabelPattern(labels);
|
||
const fieldReg = new RegExp(
|
||
`(?:${labelPattern})\\s*[::]\\s*([\\s\\S]*?)(?=(?:[\\n,,]\\s*)?(?:${FIELD_LABEL_PATTERN})\\s*[::]|$)`,
|
||
'u'
|
||
);
|
||
const match = normalizeText(text).match(fieldReg);
|
||
|
||
if (!match?.[1]) {
|
||
return '';
|
||
}
|
||
|
||
return normalizeText(match[1]).replace(/^[,,\s]+|[,,\s]+$/g, '');
|
||
}
|
||
|
||
function extractLabeledFields(text = '') {
|
||
return {
|
||
name: extractFieldValue(text, NAME_LABELS),
|
||
phone: extractFieldValue(text, PHONE_LABELS),
|
||
region: extractFieldValue(text, REGION_LABELS),
|
||
detail: extractFieldValue(text, DETAIL_LABELS)
|
||
};
|
||
}
|
||
|
||
function parseFieldBasedShippingText(text = '') {
|
||
const fields = extractLabeledFields(text);
|
||
const hasAnyField = Object.values(fields).some(Boolean);
|
||
|
||
if (!hasAnyField) {
|
||
return null;
|
||
}
|
||
|
||
const recipientName = cleanupName(fields.name);
|
||
const recipientPhone = extractPhone(fields.phone || text).phone;
|
||
const regionSource = normalizeText(fields.region);
|
||
const detailSource = normalizeText(fields.detail);
|
||
let regionMatch = findRegionMatch(regionSource || `${detailSource}${text}`);
|
||
|
||
if (!regionMatch) {
|
||
regionMatch = findRegionMatch(text);
|
||
}
|
||
|
||
const detailParts = [];
|
||
if (regionSource) {
|
||
const regionRemainder = regionMatch ? stripMatchedRegion(regionSource, regionMatch) : regionSource;
|
||
if (regionRemainder) {
|
||
detailParts.push(regionRemainder);
|
||
}
|
||
}
|
||
if (detailSource) {
|
||
detailParts.push(detailSource);
|
||
}
|
||
|
||
return {
|
||
recipient_name: recipientName,
|
||
recipient_phone: recipientPhone,
|
||
regionCodes: regionMatch ? formatRegionCodes(regionMatch) : [],
|
||
detailAddress: combineDetailParts(detailParts)
|
||
};
|
||
}
|
||
|
||
export function buildRegionColumns(regionCodes = []) {
|
||
const provinceCode =
|
||
getProvinceByCode(regionCodes[0])?.value || provinceOptions[0]?.value || '';
|
||
const cityOptions = getCityOptions(provinceCode);
|
||
const cityCode =
|
||
cityOptions.find((item) => item.value === regionCodes[1])?.value ||
|
||
cityOptions[0]?.value ||
|
||
'';
|
||
const districtOptions = getDistrictOptions(provinceCode, cityCode);
|
||
const districtCode =
|
||
districtOptions.find((item) => item.value === regionCodes[2])?.value ||
|
||
districtOptions[0]?.value ||
|
||
'';
|
||
|
||
return {
|
||
columns: [provinceOptions, cityOptions, districtOptions],
|
||
defaultIndex: [
|
||
provinceOptions.findIndex((item) => item.value === provinceCode),
|
||
cityOptions.findIndex((item) => item.value === cityCode),
|
||
districtOptions.findIndex((item) => item.value === districtCode)
|
||
]
|
||
};
|
||
}
|
||
|
||
export function getRegionDisplayText(regionCodes = []) {
|
||
return getLabelParts(regionCodes).join(' / ');
|
||
}
|
||
|
||
export function composeRecipientAddress(regionCodes = [], detailAddress = '') {
|
||
const regionText = getLabelParts(regionCodes).join('');
|
||
return `${regionText}${normalizeText(detailAddress)}`.trim();
|
||
}
|
||
|
||
export function parseSavedRecipientAddress(rawAddress = '') {
|
||
const addressText = normalizeText(rawAddress);
|
||
if (!addressText) {
|
||
return {
|
||
regionCodes: [],
|
||
detailAddress: ''
|
||
};
|
||
}
|
||
|
||
const match = findRegionMatch(addressText);
|
||
if (!match) {
|
||
return {
|
||
regionCodes: [],
|
||
detailAddress: addressText
|
||
};
|
||
}
|
||
|
||
const detailAddress = stripMatchedRegion(addressText, match);
|
||
return {
|
||
regionCodes: formatRegionCodes(match),
|
||
detailAddress: detailAddress || addressText
|
||
};
|
||
}
|
||
|
||
export function parseShippingText(rawText = '') {
|
||
const text = normalizeText(rawText);
|
||
if (!text) {
|
||
return {
|
||
recipient_name: '',
|
||
recipient_phone: '',
|
||
regionCodes: [],
|
||
detailAddress: ''
|
||
};
|
||
}
|
||
|
||
const fieldBasedResult = parseFieldBasedShippingText(text);
|
||
if (fieldBasedResult) {
|
||
return fieldBasedResult;
|
||
}
|
||
|
||
const { phone, textWithoutPhone } = extractPhone(text);
|
||
const regionMatch = findRegionMatch(textWithoutPhone);
|
||
const regionStartIndex = regionMatch ? getRegionStartIndex(textWithoutPhone, regionMatch) : -1;
|
||
const recipientName = extractNameFromText(text, textWithoutPhone, regionStartIndex);
|
||
const addressSource = extractAddressSource(textWithoutPhone, recipientName, regionMatch);
|
||
const detailAddress = regionMatch
|
||
? stripMatchedRegion(addressSource, regionMatch)
|
||
: cleanupAddressText(addressSource);
|
||
|
||
return {
|
||
recipient_name: recipientName,
|
||
recipient_phone: phone,
|
||
regionCodes: regionMatch ? formatRegionCodes(regionMatch) : [],
|
||
detailAddress
|
||
};
|
||
}
|