This commit is contained in:
309
utils/shippingAddress.js
Normal file
309
utils/shippingAddress.js
Normal file
@@ -0,0 +1,309 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user