Files
QSY-lvjuxcx/utils/select-room-normalizers.js
liangjian 445bd850f4 feat: 新增工单中心、房间巡查、失物招领等业务模块
- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面
- 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面
- 预约看样流程新增房型/楼栋/楼层选择器组件
- 新增工具模块: booking-entry、privacy、reservation-flow
- api/index.js 补充楼层列表、看样提交等接口
- locales/messages.js 补充多语言文案
- manifest.json 开启微信小程序隐私检查
- 完善登录、注册、申请详情、账单等相关页面交互
2026-07-24 09:35:46 +08:00

412 lines
12 KiB
JavaScript

import { addFilePrefix } from './common'
import { getCurrentLanguage } from './i18n'
const BANNER_TEXT_FALLBACK_MAP = {
title: {
'绿居·和山国际公寓': 'Luju Heshan Intl. Apartments',
'和山国际公寓': 'Heshan Intl. Apartments',
'乾晖里': 'Qianhui Residence'
},
subtitle: {
'国际轻奢公寓': 'International Light Luxury Apartment',
'城芯静境': 'Urban Quiet Retreat'
},
tab: {
'国际轻奢公寓': 'Light Luxury Living',
'城芯静境': 'Urban Quiet Retreat'
},
desc: {
'山境雅境 | 涉外住区 | 精致旅居': 'Mountain Views | Global Living | Boutique Stay'
},
areaLabel: {}
}
const VALUE_TEXT_FALLBACK_MAP = {
tags: {
'押一付一': 'Monthly Deposit',
'大型超市': 'Large Supermarket',
'地铁口': 'Near Metro',
'生活便利': 'Convenient Living',
'独立厨卫': 'Private Kitchen & Bath',
'商圈环绕': 'City Center',
'民水民电': 'Residential Utilities',
'拎包入住': 'Move-in Ready',
'近地铁': 'Near Metro',
'智能家居': 'Smart Home'
},
payMethod: {
'押一付一': 'Monthly Deposit',
'押一付三': 'Deposit 1, Pay 3',
'押一付六': 'Deposit 1, Pay 6',
'押一付年': 'Deposit 1, Annual Pay',
'月付': 'Monthly Pay',
'季付': 'Quarterly Pay',
'半年付': 'Semiannual Pay',
'年付': 'Annual Pay'
},
facility: {
'大型超市': 'Large Supermarket',
'地铁口': 'Near Metro',
'快递柜': 'Parcel Locker',
'健身房': 'Gym',
'自习室': 'Study Room',
'洗衣房': 'Laundry Room',
'停车场': 'Parking',
'便利店': 'Convenience Store',
'公共厨房': 'Shared Kitchen',
'休闲区': 'Lounge'
}
}
function firstNonEmptyText(...values) {
const matched = values.find((value) => String(value || '').trim())
return matched ? String(matched).trim() : ''
}
function normalizePayCount(value) {
if (value === '' || value === null || typeof value === 'undefined') {
return null
}
const parsed = Number(value)
if (Number.isFinite(parsed) && parsed >= 0) {
return parsed
}
return null
}
function formatPayCount(value) {
return String(value)
}
function buildPayMethodFromPricePlan(pricePlanDetail = {}, fallbackText = '') {
const deposit = normalizePayCount(pricePlanDetail.deposit)
const prepay = normalizePayCount(pricePlanDetail.prepay)
if (deposit !== null && prepay !== null) {
return `押${formatPayCount(deposit)}付${formatPayCount(prepay)}`
}
return String(fallbackText || '').trim()
}
export function compactEnglishText(text = '', maxLength = 24) {
const rawText = String(text || '').trim()
if (!rawText) {
return ''
}
const compacted = rawText
.replace(/\bInternational\b/gi, 'Intl.')
.replace(/\bApartment(s)?\b/gi, 'Apt$1')
.replace(/\bResidence\b/gi, 'Resid.')
.replace(/\bBuilding\b/gi, 'Bldg.')
.replace(/\bCommunity\b/gi, 'Comm.')
.replace(/\bBoulevard\b/gi, 'Blvd.')
.replace(/\bRoad\b/gi, 'Rd.')
.replace(/\bStreet\b/gi, 'St.')
.replace(/\bAvenue\b/gi, 'Ave.')
.replace(/\s{2,}/g, ' ')
.trim()
if (compacted.length <= maxLength) {
return compacted
}
return `${compacted.slice(0, Math.max(maxLength - 3, 1)).trim()}...`
}
function getSelectRoomText(key = '', params = {}) {
const isEn = getCurrentLanguage() === 'en'
const map = {
featuredContent: isEn ? 'Featured Picks' : '精选内容',
recommendedTab: isEn ? `Featured ${params.index}` : `推荐${params.index}`
}
return map[key] || ''
}
function localizeKnownValue(field, text = '', language = getCurrentLanguage()) {
const normalizedText = String(text || '').trim()
if (!normalizedText || language !== 'en') {
return normalizedText
}
if (field === 'payMethod') {
const payMethodMatch = normalizedText.match(/^押(\d+(?:\.\d+)?)付(\d+(?:\.\d+)?)$/)
if (payMethodMatch) {
return `Deposit ${payMethodMatch[1]}, Pay ${payMethodMatch[2]}`
}
}
const fieldMap = VALUE_TEXT_FALLBACK_MAP[field] || {}
return fieldMap[normalizedText] || normalizedText
}
export function localizeHousingTag(tag = '', language = getCurrentLanguage()) {
return localizeKnownValue('tags', tag, language)
}
export function localizePayMethodText(payMethod = '', language = getCurrentLanguage()) {
return localizeKnownValue('payMethod', payMethod, language)
}
export function localizeFacilityName(name = '', language = getCurrentLanguage()) {
return localizeKnownValue('facility', name, language)
}
function resolveBannerFallbackText(field, text) {
const normalizedText = String(text || '').trim()
if (!normalizedText) {
return ''
}
return BANNER_TEXT_FALLBACK_MAP[field] && BANNER_TEXT_FALLBACK_MAP[field][normalizedText]
? BANNER_TEXT_FALLBACK_MAP[field][normalizedText]
: ''
}
function extractLabelValues(...sources) {
const values = sources.flatMap((source) => {
if (!Array.isArray(source)) {
return []
}
return source.map((item) => {
if (!item) {
return ''
}
if (typeof item === 'string') {
return item
}
if (typeof item === 'object') {
return item.value || item.label || item.name || item.key || ''
}
return ''
})
})
return [...new Set(values.filter(Boolean))]
}
export function parseBannerExtra(extra) {
if (!extra) {
return {}
}
if (typeof extra === 'object') {
return extra
}
try {
return JSON.parse(extra)
} catch (error) {
return {}
}
}
export function normalizeBannerImage(imageUrl) {
if (!imageUrl) {
return ''
}
if (/^https?:\/\//i.test(imageUrl)) {
return imageUrl
}
return addFilePrefix(imageUrl)
}
function isImageLikeLogo(value) {
if (!value) {
return false
}
return /^(https?:)?\/\//i.test(value) || value.startsWith('/') || value.startsWith('data:image')
}
function normalizeBannerLogo(extra = {}) {
const rawType = String(extra.logoType || '').trim().toLowerCase()
const rawLogo = String(extra.logo || '').trim()
const rawLogoIcon = String(extra.logoIcon || '').trim()
const rawLogoImage = String(extra.logoImage || '').trim()
const isImage = rawType === 'image' || (!rawType && !rawLogoImage && isImageLikeLogo(rawLogo))
const logoImageSource = rawLogoImage || (isImage ? rawLogo : '')
const logoIconSource = rawLogoIcon || (!isImage ? rawLogo : '')
return {
logoType: isImage ? 'image' : 'icon',
logoImage: normalizeBannerImage(logoImageSource),
logoIcon: logoIconSource,
}
}
function normalizeBannerLogoIconClass(icon) {
const normalizedIcon = String(icon || '').trim().toLowerCase()
if (!normalizedIcon) {
return ''
}
return normalizedIcon.startsWith('ri-') ? normalizedIcon : `ri-${normalizedIcon}`
}
export function normalizeBannerItem(item = {}, index = 0) {
const extra = parseBannerExtra(item.extra)
const title = firstNonEmptyText(item.title, item.name, extra.title, extra.mainTitle, extra.titleZh, getSelectRoomText('featuredContent'))
const titleEn = firstNonEmptyText(
item.titleEn,
item.title_en,
item.nameEn,
item.name_en,
extra.titleEn,
extra.title_en,
extra.enTitle,
extra.mainTitleEn,
extra.titleEnUs,
resolveBannerFallbackText('title', title),
'Featured Picks'
)
const subtitle = firstNonEmptyText(extra.subTitle, extra.subtitle, extra.remark, extra.note, item.remark)
const subtitleEn = firstNonEmptyText(
extra.subTitleEn,
extra.subtitleEn,
extra.sub_title_en,
extra.enSubtitle,
extra.remarkEn,
extra.remark_en,
extra.noteEn,
item.remarkEn,
item.remark_en,
resolveBannerFallbackText('subtitle', subtitle)
)
const desc = firstNonEmptyText(extra.desc, extra.description, extra.mainDesc, extra.slogan)
const descEn = firstNonEmptyText(
extra.descEn,
extra.descriptionEn,
extra.desc_en,
extra.description_en,
extra.enDesc,
extra.mainDescEn,
extra.sloganEn,
resolveBannerFallbackText('desc', desc)
)
const areaLabel = firstNonEmptyText(extra.areaName, extra.areaLabel, extra.area)
const areaLabelEn = firstNonEmptyText(extra.areaNameEn, extra.areaLabelEn, extra.area_name_en, extra.area_label_en, extra.enAreaName, resolveBannerFallbackText('areaLabel', areaLabel))
const tab = firstNonEmptyText(extra.tab, extra.topTab, title, getSelectRoomText('recommendedTab', { index: index + 1 }))
const tabEn = firstNonEmptyText(extra.tabEn, extra.tab_en, extra.enTab, extra.topTabEn, resolveBannerFallbackText('tab', tab), titleEn, `Featured ${index + 1}`)
const roomTypes = Array.isArray(extra.roomTypes) ? extra.roomTypes : []
const tags = Array.isArray(extra.tags) && extra.tags.length
? extra.tags.filter(Boolean)
: []
const logoInfo = normalizeBannerLogo(extra)
return {
id: item.no || item.code || item.id || `banner-${index}`,
tab,
tabEn,
tabEnDisplay: compactEnglishText(tabEn, 18),
title,
titleEn,
titleEnDisplay: compactEnglishText(titleEn, 24),
subtitle,
subtitleEn,
subtitleEnDisplay: compactEnglishText(subtitleEn, 28),
desc,
descEn,
descEnDisplay: compactEnglishText(descEn, 36),
areaCode: extra.areaCode || extra.area || '',
areaLabel,
areaLabelEn,
areaLabelEnDisplay: compactEnglishText(areaLabelEn, 22),
roomTypes,
recommendLabel: extra.recommendLabel || extra.recommend || '',
image: normalizeBannerImage(item.imageUrl || extra.imageUrl),
tags,
logoType: logoInfo.logoType,
logoImage: logoInfo.logoImage,
logoIcon: logoInfo.logoIcon,
logoIconClass: normalizeBannerLogoIconClass(logoInfo.logoIcon),
}
}
export function normalizeImageList(item = {}) {
const candidates = []
const listFields = [item.images, item.imageUrls, item.pictures, item.pictureList]
listFields.forEach((field) => {
if (Array.isArray(field)) {
field.forEach((image) => {
const normalized = normalizeBannerImage(image)
if (normalized) {
candidates.push(normalized)
}
})
}
})
;[item.imageUrl, item.image, item.cover, item.picture].forEach((image) => {
const normalized = normalizeBannerImage(image)
if (normalized) {
candidates.push(normalized)
}
})
return [...new Set(candidates)]
}
export function normalizeAreaRoomType(item = {}) {
const price = item.pricePlanDetail && typeof item.pricePlanDetail.price !== 'undefined'
? String(item.pricePlanDetail.price)
: ''
const priceValue = item.pricePlanDetail && typeof item.pricePlanDetail.price !== 'undefined'
? Number(item.pricePlanDetail.price)
: Number.MAX_SAFE_INTEGER
const tags = extractLabelValues(item.labelList, item.tags, item.labels, item.featureTags)
return {
code: item.code || '',
areaCode: item.areaCode || '',
buildingCode: item.buildingCode || item.buildingNo || '',
buildingName: item.buildingName || '',
buildingSectionCode: item.buildingSectionCode || item.unitCode || '',
buildingSectionName: item.buildingSectionName || item.unitName || '',
name: item.name || '',
nameEn: item.nameEn || '',
nameEnDisplay: compactEnglishText(item.nameEn || '', 22),
description: item.description || '',
descriptionEn: item.descriptionEn || '',
descriptionEnDisplay: compactEnglishText(item.descriptionEn || '', 34),
images: normalizeImageList(item),
price,
priceValue,
payMethod: buildPayMethodFromPricePlan(item.pricePlanDetail, item.pricePlanDetail && item.pricePlanDetail.payMethod),
tags
}
}
export function normalizeAreaItem(item = {}) {
const roomTypes = Array.isArray(item.roomTypes)
? item.roomTypes
.map((roomType) => normalizeAreaRoomType(roomType))
.filter((roomType) => roomType.code || roomType.name)
: []
const tags = extractLabelValues(item.labelList, item.tags, item.labels, item.featureTags)
return {
code: item.code || '',
name: item.name || '',
nameEn: item.nameEn || '',
nameEnDisplay: compactEnglishText(item.nameEn || '', 24),
description: item.description || '',
descriptionEn: item.descriptionEn || '',
descriptionEnDisplay: compactEnglishText(item.descriptionEn || '', 36),
address: item.address || '',
addressEn: item.addressEn || '',
addressEnDisplay: compactEnglishText(item.addressEn || '', 32),
latitude: item.latitude || item.lat || '',
longitude: item.longitude || item.lng || '',
images: normalizeImageList(item),
tags,
roomTypes
}
}