feat: 优化首页服务入口与租住业务流程
补齐首页待办跳转、押金支付引导和多类租住服务页面能力,统一租客端关键业务流程体验。
This commit is contained in:
@@ -77,8 +77,8 @@ function buildRoomDataFromOrder(order = {}) {
|
||||
order.roomTypeName || order.roomType || ''
|
||||
].filter(Boolean).join(' · ')
|
||||
|
||||
const start = String(order.startDate || order.start || order.checkinDate || '').slice(0, 10)
|
||||
const end = String(order.endDate || order.end || order.checkoutDate || '').slice(0, 10)
|
||||
const start = String(order.contractStartTime || order.startDate || order.start || order.checkinDate || '').slice(0, 10)
|
||||
const end = String(order.contractEndTime || order.endDate || order.end || order.checkoutDate || '').slice(0, 10)
|
||||
|
||||
return {
|
||||
...order,
|
||||
|
||||
@@ -11,12 +11,8 @@ export function resolveExistingBookingNavigationTarget(stayState = {}) {
|
||||
const currentBooking = source.currentBooking || null
|
||||
const hasCheckedIn = source.hasCheckedIn === true
|
||||
|
||||
if (currentBooking && currentBooking.orderNo) {
|
||||
return `/pkgBooking/booking_detail/booking_detail?orderNo=${encodeURIComponent(currentBooking.orderNo)}`
|
||||
}
|
||||
|
||||
if (currentBooking) {
|
||||
return '/pkgBooking/booking_detail/booking_detail'
|
||||
return '/pages/index/index?pageId=stay-service'
|
||||
}
|
||||
|
||||
if (hasCheckedIn) {
|
||||
|
||||
@@ -13,11 +13,13 @@ function getGuardText(language = 'zh') {
|
||||
return {
|
||||
loginTitle: zh ? '提示' : 'Notice',
|
||||
loginContent: zh ? '您还未登录,请先登录或注册账号。' : 'Please sign in or register an account first.',
|
||||
loginCancelText: zh ? '取消' : 'Cancel',
|
||||
loginConfirmText: zh ? '去登录' : 'Sign In',
|
||||
existingBookingTitle: zh ? '已有进行中的预订' : 'Existing Booking',
|
||||
existingBookingContent: zh
|
||||
? '您当前状态为“{status}”,请先查看预订详情。'
|
||||
: 'Your current booking status is "{status}". Please review the booking detail first.',
|
||||
existingBookingCancelText: zh ? '取消' : 'Cancel',
|
||||
viewBooking: zh ? '查看预订' : 'View Booking'
|
||||
}
|
||||
}
|
||||
@@ -69,6 +71,7 @@ export function ensureBookingAccess(options = {}) {
|
||||
uni.showModal({
|
||||
title: options.loginTitle || guardText.loginTitle,
|
||||
content: options.loginContent || guardText.loginContent,
|
||||
cancelText: options.loginCancelText || guardText.loginCancelText,
|
||||
confirmText: options.loginConfirmText || guardText.loginConfirmText,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
@@ -89,6 +92,7 @@ export function ensureBookingAccess(options = {}) {
|
||||
uni.showModal({
|
||||
title: guardText.existingBookingTitle,
|
||||
content: guardText.existingBookingContent.replace('{status}', statusText),
|
||||
cancelText: guardText.existingBookingCancelText,
|
||||
confirmText: guardText.viewBooking,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
|
||||
@@ -348,8 +348,8 @@ export function normalizeBookingOrder(order = {}, language = getCurrentLanguage(
|
||||
const statusKey = normalizeStatusKey(safeOrder)
|
||||
const statusCode = getOrderStatusCode(safeOrder)
|
||||
const checkInPeriod = safeOrder.checkInPeriod || {}
|
||||
const startDate = safeOrder.startDate || safeOrder.start || safeOrder.checkinDate || checkInPeriod.start
|
||||
const endDate = safeOrder.endDate || safeOrder.end || safeOrder.checkoutDate || checkInPeriod.end
|
||||
const startDate = safeOrder.contractStartTime || safeOrder.startDate || safeOrder.start || safeOrder.checkinDate || checkInPeriod.start
|
||||
const endDate = safeOrder.contractEndTime || safeOrder.endDate || safeOrder.end || safeOrder.checkoutDate || checkInPeriod.end
|
||||
return {
|
||||
...safeOrder,
|
||||
orderNo: getOrderNo(safeOrder),
|
||||
|
||||
112
utils/contract-display.js
Normal file
112
utils/contract-display.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { contractDetail } from '@/api/index.js'
|
||||
|
||||
const contractNameCache = Object.create(null)
|
||||
const contractDetailCache = Object.create(null)
|
||||
|
||||
function pickFirstText(...values) {
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const value = values[index]
|
||||
if (value === undefined || value === null) continue
|
||||
const text = String(value).trim()
|
||||
if (text) return text
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function parseContractDetailResponse(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||||
return payload.data
|
||||
}
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function resolveContractNameFromSource(source = {}) {
|
||||
return pickFirstText(
|
||||
source.contractDisplayName,
|
||||
source.contractName,
|
||||
source.contractTemplateName,
|
||||
source.contractTypeName,
|
||||
source.contractType,
|
||||
source.type,
|
||||
source.typeName,
|
||||
source.templateName,
|
||||
source.title
|
||||
)
|
||||
}
|
||||
|
||||
function resolveContractNo(source = {}) {
|
||||
return pickFirstText(source.no, source.contractNo)
|
||||
}
|
||||
|
||||
export function getContractDisplayName(source = {}, fallbackText = '') {
|
||||
const directName = resolveContractNameFromSource(source)
|
||||
if (directName) return directName
|
||||
|
||||
const contractNo = resolveContractNo(source)
|
||||
if (contractNo && contractNameCache[contractNo]) return contractNameCache[contractNo]
|
||||
|
||||
return fallbackText || ''
|
||||
}
|
||||
|
||||
export async function fetchContractDisplayInfo(contractNo = '', language) {
|
||||
const normalizedContractNo = String(contractNo || '').trim()
|
||||
if (!normalizedContractNo) {
|
||||
return {
|
||||
contractDisplayName: '',
|
||||
detail: {}
|
||||
}
|
||||
}
|
||||
|
||||
if (contractDetailCache[normalizedContractNo]) {
|
||||
const cachedDetail = contractDetailCache[normalizedContractNo]
|
||||
return {
|
||||
contractDisplayName: resolveContractNameFromSource(cachedDetail) || contractNameCache[normalizedContractNo] || '',
|
||||
detail: cachedDetail
|
||||
}
|
||||
}
|
||||
|
||||
const response = await contractDetail({ contractNo: normalizedContractNo }, language)
|
||||
const detail = parseContractDetailResponse(response)
|
||||
const contractDisplayName = resolveContractNameFromSource(detail)
|
||||
|
||||
contractDetailCache[normalizedContractNo] = detail
|
||||
contractNameCache[normalizedContractNo] = contractDisplayName
|
||||
|
||||
return {
|
||||
contractDisplayName,
|
||||
detail
|
||||
}
|
||||
}
|
||||
|
||||
export async function enrichContractDisplayNames(list = [], language) {
|
||||
const sourceList = Array.isArray(list) ? list : []
|
||||
const pendingNos = [...new Set(
|
||||
sourceList
|
||||
.filter(item => !resolveContractNameFromSource(item))
|
||||
.map(item => resolveContractNo(item))
|
||||
.filter(Boolean)
|
||||
)]
|
||||
|
||||
await Promise.all(
|
||||
pendingNos.map((contractNo) =>
|
||||
fetchContractDisplayInfo(contractNo, language).catch(() => ({
|
||||
contractDisplayName: '',
|
||||
detail: {}
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
return sourceList.map((item) => {
|
||||
const contractNo = resolveContractNo(item)
|
||||
return {
|
||||
...item,
|
||||
contractDisplayName: getContractDisplayName(item, '')
|
||||
|| (contractNo ? contractNameCache[contractNo] : '')
|
||||
|| ''
|
||||
}
|
||||
})
|
||||
}
|
||||
90
utils/deposit-other-payment.js
Normal file
90
utils/deposit-other-payment.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { sysdictPage } from '@/api/index.js'
|
||||
|
||||
export const MOBILE_DEPOSIT_OTHER_PAYMENT_CONFIG_TYPE = 'MobileDepositOtherPaymentConfig'
|
||||
export const MOBILE_PROVIDER_TYPE = 'WechatMiniProgram'
|
||||
export const DEPOSIT_OTHER_PAYMENT_CONFIG_KEY = 'depositOtherPayment'
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function parseJsonStringMaybe(value) {
|
||||
const text = normalizeText(value)
|
||||
if (!text) {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
} catch (error) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRows(response) {
|
||||
const payload = response && response.data !== undefined ? response.data : response
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
if (payload && Array.isArray(payload.data)) {
|
||||
return payload.data
|
||||
}
|
||||
if (payload && payload.data && Array.isArray(payload.data.data)) {
|
||||
return payload.data.data
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function getProviderType(row = {}) {
|
||||
return normalizeText(row.providerType || row.ProviderType)
|
||||
}
|
||||
|
||||
export function createDefaultDepositOtherPaymentConfig() {
|
||||
return {
|
||||
code: '',
|
||||
key: DEPOSIT_OTHER_PAYMENT_CONFIG_KEY,
|
||||
phone: '',
|
||||
qrCode: '',
|
||||
contentZh: '',
|
||||
contentEn: ''
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeDepositOtherPaymentConfig(row = {}) {
|
||||
const defaults = createDefaultDepositOtherPaymentConfig()
|
||||
const extra = parseJsonStringMaybe(row.extraInfo || row.extraConfig || row.ExtraConfig)
|
||||
|
||||
return {
|
||||
code: normalizeText(row.code),
|
||||
key: normalizeText(row.key) || defaults.key,
|
||||
phone: normalizeText(extra.phone || row.description),
|
||||
qrCode: normalizeText(extra.qrCode || extra.qrCodeImage || extra.qrImage),
|
||||
contentZh: normalizeText(extra.contentZh || extra.content || defaults.contentZh),
|
||||
contentEn: normalizeText(extra.contentEn || defaults.contentEn)
|
||||
}
|
||||
}
|
||||
|
||||
function findMatchedRow(rows = []) {
|
||||
const providerRows = rows.filter(item => getProviderType(item) === MOBILE_PROVIDER_TYPE)
|
||||
const legacyRows = rows.filter(item => !getProviderType(item))
|
||||
return (
|
||||
providerRows.find(item => normalizeText(item && item.key) === DEPOSIT_OTHER_PAYMENT_CONFIG_KEY)
|
||||
|| providerRows[0]
|
||||
|| legacyRows.find(item => normalizeText(item && item.key) === DEPOSIT_OTHER_PAYMENT_CONFIG_KEY)
|
||||
|| legacyRows[0]
|
||||
|| null
|
||||
)
|
||||
}
|
||||
|
||||
export function loadDepositOtherPaymentConfig(language) {
|
||||
return sysdictPage({
|
||||
Type: MOBILE_DEPOSIT_OTHER_PAYMENT_CONFIG_TYPE,
|
||||
ProviderType: MOBILE_PROVIDER_TYPE,
|
||||
PageIndex: 1,
|
||||
PageSize: 100
|
||||
}, language).then((response) => {
|
||||
const rows = normalizeRows(response)
|
||||
const matchedRow = findMatchedRow(rows)
|
||||
return normalizeDepositOtherPaymentConfig(matchedRow || {})
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
const RESERVATION_FLOW_STEP = {
|
||||
APPLICATION: 1,
|
||||
INSPECTION: 2,
|
||||
BILL: 3,
|
||||
CONTRACT: 4,
|
||||
CONTRACT: 3,
|
||||
BILL: 4,
|
||||
CHECKIN: 5
|
||||
}
|
||||
|
||||
@@ -90,7 +90,70 @@ export function getReservationCurrentNode(flow = {}) {
|
||||
return null
|
||||
}
|
||||
|
||||
return nodes.find(item => item.isCurrent) || nodes[nodes.length - 1]
|
||||
const currentNodes = nodes.filter(item => item.isCurrent)
|
||||
if (currentNodes.length) {
|
||||
const currentCheckinNode = currentNodes.find(item => (
|
||||
Number(item.step || 0) === RESERVATION_FLOW_STEP.CHECKIN
|
||||
&& String(item.refNo || '').trim()
|
||||
))
|
||||
if (currentCheckinNode) {
|
||||
return currentCheckinNode
|
||||
}
|
||||
|
||||
// 后端偶发会把前序节点也标记成 isCurrent,优先取排序最靠后的当前节点
|
||||
return currentNodes[currentNodes.length - 1]
|
||||
}
|
||||
|
||||
const checkinNode = nodes.find(item => (
|
||||
Number(item.step || 0) === RESERVATION_FLOW_STEP.CHECKIN
|
||||
&& String(item.refNo || '').trim()
|
||||
))
|
||||
if (checkinNode) {
|
||||
return checkinNode
|
||||
}
|
||||
|
||||
return nodes[nodes.length - 1]
|
||||
}
|
||||
|
||||
export function getReservationCurrentFlow(flowList = []) {
|
||||
const flows = normalizeArray(flowList)
|
||||
.filter(item => item && typeof item === 'object')
|
||||
.map(item => ({
|
||||
flow: item,
|
||||
currentNode: getReservationCurrentNode(item)
|
||||
}))
|
||||
.filter(item => item.currentNode)
|
||||
|
||||
if (!flows.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const compareNode = (target) => {
|
||||
const node = target.currentNode || {}
|
||||
const step = Number(node.step || 0)
|
||||
const sort = Number(node.sort || 0)
|
||||
const refNo = String(node.refNo || '').trim()
|
||||
const isCurrent = !!node.isCurrent
|
||||
const isCheckinWithRefNo = step === RESERVATION_FLOW_STEP.CHECKIN && !!refNo
|
||||
return [
|
||||
isCheckinWithRefNo ? 1 : 0,
|
||||
isCurrent ? 1 : 0,
|
||||
step,
|
||||
sort
|
||||
]
|
||||
}
|
||||
|
||||
return flows
|
||||
.sort((a, b) => {
|
||||
const aScore = compareNode(a)
|
||||
const bScore = compareNode(b)
|
||||
for (let index = 0; index < aScore.length; index += 1) {
|
||||
if (aScore[index] !== bScore[index]) {
|
||||
return bScore[index] - aScore[index]
|
||||
}
|
||||
}
|
||||
return 0
|
||||
})[0].flow
|
||||
}
|
||||
|
||||
export function hasEffectiveReservationFlow(flow = {}) {
|
||||
@@ -149,7 +212,7 @@ export function getReservationStepMeta(step, language = 'zh') {
|
||||
},
|
||||
[RESERVATION_FLOW_STEP.BILL]: {
|
||||
label: currentLanguage === 'en' ? 'Bill' : '账单',
|
||||
actionText: currentLanguage === 'en' ? 'Go to Bill' : '去缴费'
|
||||
actionText: currentLanguage === 'en' ? 'Go to Payment' : '去支付'
|
||||
},
|
||||
[RESERVATION_FLOW_STEP.CONTRACT]: {
|
||||
label: currentLanguage === 'en' ? 'Contract' : '合同',
|
||||
|
||||
@@ -22,6 +22,11 @@ const BANNER_TEXT_FALLBACK_MAP = {
|
||||
}
|
||||
|
||||
const VALUE_TEXT_FALLBACK_MAP = {
|
||||
apartment: {
|
||||
'绿居·和山国际公寓': 'Luju Heshan Intl. Apartments',
|
||||
'和山国际公寓': 'Heshan Intl. Apartments',
|
||||
'乾晖里': 'Qianhui Residence'
|
||||
},
|
||||
tags: {
|
||||
'押一付一': 'Monthly Deposit',
|
||||
'大型超市': 'Large Supermarket',
|
||||
@@ -148,6 +153,10 @@ export function localizeHousingTag(tag = '', language = getCurrentLanguage()) {
|
||||
return localizeKnownValue('tags', tag, language)
|
||||
}
|
||||
|
||||
export function localizeApartmentName(name = '', language = getCurrentLanguage()) {
|
||||
return localizeKnownValue('apartment', name, language)
|
||||
}
|
||||
|
||||
export function localizePayMethodText(payMethod = '', language = getCurrentLanguage()) {
|
||||
return localizeKnownValue('payMethod', payMethod, language)
|
||||
}
|
||||
@@ -330,7 +339,19 @@ export function normalizeBannerItem(item = {}, index = 0) {
|
||||
|
||||
export function normalizeImageList(item = {}) {
|
||||
const candidates = []
|
||||
const listFields = [item.images, item.imageUrls, item.pictures, item.pictureList]
|
||||
const listFields = [
|
||||
item.images,
|
||||
item.imageUrls,
|
||||
item.pictures,
|
||||
item.pictureList,
|
||||
item.bannerImages,
|
||||
item.carouselImages,
|
||||
item.imageList,
|
||||
item.photos,
|
||||
item.photoList,
|
||||
item.album,
|
||||
item.albums
|
||||
]
|
||||
|
||||
listFields.forEach((field) => {
|
||||
if (Array.isArray(field)) {
|
||||
@@ -343,7 +364,19 @@ export function normalizeImageList(item = {}) {
|
||||
}
|
||||
})
|
||||
|
||||
;[item.imageUrl, item.image, item.cover, item.picture].forEach((image) => {
|
||||
;[
|
||||
item.imageUrl,
|
||||
item.image,
|
||||
item.cover,
|
||||
item.coverUrl,
|
||||
item.picture,
|
||||
item.poster,
|
||||
item.posterUrl,
|
||||
item.banner,
|
||||
item.bannerUrl,
|
||||
item.areaImage,
|
||||
item.areaImageUrl
|
||||
].forEach((image) => {
|
||||
const normalized = normalizeBannerImage(image)
|
||||
if (normalized) {
|
||||
candidates.push(normalized)
|
||||
|
||||
@@ -131,6 +131,7 @@ function buildEffectiveContractQuery(order = {}) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
ProviderType: 'WechatMiniProgram',
|
||||
OrderNo: orderNo
|
||||
}
|
||||
}
|
||||
|
||||
64
utils/todo-route.js
Normal file
64
utils/todo-route.js
Normal file
@@ -0,0 +1,64 @@
|
||||
function ensureArray(value) {
|
||||
return Array.isArray(value) ? value : []
|
||||
}
|
||||
|
||||
function pickFirstText() {
|
||||
for (let index = 0; index < arguments.length; index += 1) {
|
||||
const value = arguments[index]
|
||||
if (value === null || value === undefined) continue
|
||||
const text = String(value).trim()
|
||||
if (text) return text
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizeNumber(value, fallback = 0) {
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num : fallback
|
||||
}
|
||||
|
||||
function normalizeTodoEntry(raw) {
|
||||
if (raw && typeof raw === 'object') {
|
||||
return {
|
||||
...raw,
|
||||
_todoCode: pickFirstText(raw.code, raw.no, raw.contractNo, raw.billNo, raw.orderNo, raw.referenceTypeNo)
|
||||
}
|
||||
}
|
||||
const text = pickFirstText(raw)
|
||||
return text ? { code: text, _todoCode: text } : null
|
||||
}
|
||||
|
||||
function extractTodoEntries(item) {
|
||||
const sourceList = [item && item.items, item && item.codes, item && item.list, item && item.data]
|
||||
for (let index = 0; index < sourceList.length; index += 1) {
|
||||
const current = sourceList[index]
|
||||
if (!Array.isArray(current)) continue
|
||||
return current.map(entry => normalizeTodoEntry(entry)).filter(Boolean)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function extractTodoRows(response) {
|
||||
const payload = response && response.data !== undefined ? response.data : response
|
||||
if (Array.isArray(payload)) return payload
|
||||
if (!payload || typeof payload !== 'object') return []
|
||||
if (Array.isArray(payload.data)) return payload.data
|
||||
if (payload.data && Array.isArray(payload.data.data)) return payload.data.data
|
||||
return []
|
||||
}
|
||||
|
||||
export function normalizeTodoList(rows = []) {
|
||||
return ensureArray(rows).map((item) => {
|
||||
const entries = extractTodoEntries(item)
|
||||
const count = Math.max(normalizeNumber(item && item.count, entries.length), entries.length)
|
||||
return {
|
||||
todoType: String(item && item.todoType !== undefined ? item.todoType : ''),
|
||||
todoTypeDesc: pickFirstText(item && item.todoTypeDesc),
|
||||
count,
|
||||
entries,
|
||||
codes: entries
|
||||
.map(entry => ({ code: pickFirstText(entry && entry._todoCode) }))
|
||||
.filter(entry => entry.code)
|
||||
}
|
||||
}).filter(item => item.count > 0 || item.entries.length > 0 || item.todoTypeDesc)
|
||||
}
|
||||
Reference in New Issue
Block a user