fix: 优化换货
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m48s

This commit is contained in:
sexygoat
2026-05-14 12:14:05 +08:00
parent 7253996f40
commit b611808316
11 changed files with 5653 additions and 86 deletions

309
utils/shippingAddress.js Normal file
View 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
};
}