- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面 - 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面 - 预约看样流程新增房型/楼栋/楼层选择器组件 - 新增工具模块: booking-entry、privacy、reservation-flow - api/index.js 补充楼层列表、看样提交等接口 - locales/messages.js 补充多语言文案 - manifest.json 开启微信小程序隐私检查 - 完善登录、注册、申请详情、账单等相关页面交互
412 lines
12 KiB
JavaScript
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
|
|
}
|
|
}
|