Files
device-voice-h5/utils/shippingAddress.js
sexygoat fded2fe12d
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m0s
fix: 优化换货
2026-05-14 14:06:52 +08:00

505 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
};
}