310 lines
8.9 KiB
JavaScript
310 lines
8.9 KiB
JavaScript
import { regionData } from 'element-china-area-data';
|
||
|
||
const GENERIC_CITY_LABELS = new Set([
|
||
'市辖区',
|
||
'县',
|
||
'自治区直辖县级行政区划',
|
||
'省直辖县级行政区划'
|
||
]);
|
||
|
||
const NAME_LABEL_REG = /(收货人|收件人|联系人|姓名)/g;
|
||
const PHONE_LABEL_REG = /(手机号码|手机号|联系电话|联系方式|电话|手机)/g;
|
||
const ADDRESS_LABEL_REG = /(收货地址|详细地址|地址|所在地区|地区)/g;
|
||
const ADDRESS_SIGNAL_REG = /(省|市|区|县|旗|乡|镇|街道|大道|路|街|巷|弄|村|社区|小区|号|栋|单元|室|楼)/;
|
||
const PHONE_REG = /(?<!\d)(?:\+?86[-\s]?)?(1[3-9](?:[\s-]?\d){9})(?!\d)/;
|
||
|
||
const provinceOptions = regionData.map(({ label, value }) => ({ label, value }));
|
||
|
||
const districtRecords = regionData.flatMap((province) =>
|
||
(province.children || []).flatMap((city) =>
|
||
(city.children || []).map((district) => ({
|
||
province,
|
||
city,
|
||
district,
|
||
provinceSearchLabel: normalizeForSearch(province.label),
|
||
citySearchLabel: normalizeForSearch(getDisplayCityLabel(city.label)),
|
||
districtSearchLabel: normalizeForSearch(district.label),
|
||
fullSearchLabel: normalizeForSearch(
|
||
`${province.label}${getDisplayCityLabel(city.label)}${district.label}`
|
||
)
|
||
}))
|
||
)
|
||
);
|
||
|
||
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 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 cleanupAddressText(value = '') {
|
||
return normalizeText(value)
|
||
.replace(NAME_LABEL_REG, ' ')
|
||
.replace(PHONE_LABEL_REG, ' ')
|
||
.replace(ADDRESS_LABEL_REG, ' ')
|
||
.replace(/[::]/g, ' ')
|
||
.replace(/[,,;;]/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
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.fullSearchLabel.length - left.fullSearchLabel.length ||
|
||
right.districtSearchLabel.length - left.districtSearchLabel.length
|
||
);
|
||
})[0] || null;
|
||
}
|
||
|
||
function findRegionMatch(rawText = '') {
|
||
const searchText = normalizeForSearch(rawText);
|
||
if (!searchText) {
|
||
return null;
|
||
}
|
||
|
||
let match = pickBestMatch(districtRecords, (record) => {
|
||
return record.fullSearchLabel && searchText.includes(record.fullSearchLabel);
|
||
});
|
||
|
||
if (!match) {
|
||
match = pickBestMatch(districtRecords, (record) => {
|
||
return (
|
||
record.provinceSearchLabel &&
|
||
record.districtSearchLabel &&
|
||
searchText.includes(record.provinceSearchLabel) &&
|
||
searchText.includes(record.districtSearchLabel)
|
||
);
|
||
});
|
||
}
|
||
|
||
return match;
|
||
}
|
||
|
||
function getRegionStartIndex(text, match) {
|
||
const cityLabel = getDisplayCityLabel(match.city.label);
|
||
const candidates = [match.province.label, cityLabel, match.district.label]
|
||
.filter(Boolean)
|
||
.map((label) => text.indexOf(label))
|
||
.filter((index) => index >= 0);
|
||
|
||
return candidates.length ? Math.min(...candidates) : -1;
|
||
}
|
||
|
||
function stripMatchedRegion(addressText, match) {
|
||
const cityLabel = getDisplayCityLabel(match.city.label);
|
||
let detailAddress = normalizeText(addressText);
|
||
|
||
[match.province.label, cityLabel, match.district.label]
|
||
.filter(Boolean)
|
||
.forEach((label) => {
|
||
detailAddress = detailAddress.replace(label, '');
|
||
});
|
||
|
||
return detailAddress.replace(/^[,,;;::\s]+/, '').trim();
|
||
}
|
||
|
||
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})/
|
||
);
|
||
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}$/.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];
|
||
}
|
||
|
||
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 { 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
|
||
};
|
||
}
|