chore: initial import
This commit is contained in:
20
utils/account-api.js
Normal file
20
utils/account-api.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export {
|
||||
applicationFormMePage,
|
||||
checkRecordByChecked,
|
||||
contractPreview,
|
||||
contractSign,
|
||||
getCountryList,
|
||||
getEnums,
|
||||
getMyEffectiveContracts,
|
||||
getMyPendingSignContracts,
|
||||
notificationPage,
|
||||
notificationRead,
|
||||
orderSearchedMePage,
|
||||
registrationApplicationMePage,
|
||||
submitApplicationForm,
|
||||
submitRegistrationApplication,
|
||||
sysdictPage,
|
||||
userMe,
|
||||
uploadCommonFile,
|
||||
usersLogin
|
||||
} from '../api/index'
|
||||
431
utils/app-state.js
Normal file
431
utils/app-state.js
Normal file
@@ -0,0 +1,431 @@
|
||||
import store from '@/store'
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
REGISTERED_USER: 'registeredUser',
|
||||
AUTH_TOKEN: 'authToken',
|
||||
AUTH_TOKEN_EXPIRES: 'authTokenExpires',
|
||||
IS_LOGIN: 'isLogin',
|
||||
USER_INFO: 'userInfo',
|
||||
HAS_CHECKED_IN: 'hasCheckedIn',
|
||||
MY_ROOM_DATA: 'myRoomData',
|
||||
CURRENT_BOOKING: 'currentBooking',
|
||||
BOOKING_LIST: 'bookingList',
|
||||
PENDING_CONTRACTS: 'pendingContracts',
|
||||
PENDING_CONTRACT_COUNT: 'pendingContractCount',
|
||||
EFFECTIVE_CONTRACTS: 'effectiveContracts',
|
||||
MY_APPLICATION: 'myApplication',
|
||||
ACTIVE_APPLICATION_CODE: 'activeApplicationCode',
|
||||
FAILED_REGISTRATION_DRAFT: 'failedRegistrationDraft',
|
||||
HOME_PAGE_TAB_ID: 'homePageTabId',
|
||||
MESSAGE_LIST: 'messageList',
|
||||
SERVICE_ORDER_LIST: 'serviceOrderList',
|
||||
UTILITY_RECHARGE_LIST: 'utilityRechargeList',
|
||||
LEGACY_DEMO_CACHE_CLEARED: 'legacyDemoCacheCleared'
|
||||
}
|
||||
|
||||
function getStorage(key, fallback = null) {
|
||||
const value = uni.getStorageSync(key)
|
||||
return value === '' || typeof value === 'undefined' ? fallback : value
|
||||
}
|
||||
|
||||
function getStoreState() {
|
||||
return (store && store.state) || {}
|
||||
}
|
||||
|
||||
function getStoreUser() {
|
||||
const user = getStoreState().vuex_user
|
||||
return user && typeof user === 'object' ? user : {}
|
||||
}
|
||||
|
||||
function getStorePendingContracts() {
|
||||
const list = getStoreState().vuex_pending_contracts
|
||||
return Array.isArray(list) ? list : []
|
||||
}
|
||||
|
||||
function getStoreEffectiveContracts() {
|
||||
const list = getStoreState().vuex_effective_contracts
|
||||
return Array.isArray(list) ? list : []
|
||||
}
|
||||
|
||||
function getStoreValidOrders() {
|
||||
const list = getStoreState().vuex_valid_orders
|
||||
return Array.isArray(list) ? list : []
|
||||
}
|
||||
|
||||
function getStoreCurrentOrder() {
|
||||
return getStoreState().vuex_current_order || null
|
||||
}
|
||||
|
||||
function buildRoomDataFromOrder(order = {}) {
|
||||
if (!order || typeof order !== 'object' || !Object.keys(order).length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const imageUrls = Array.isArray(order.imageUrls)
|
||||
? order.imageUrls
|
||||
: typeof order.imageUrls === 'string'
|
||||
? order.imageUrls.split(',').map(item => item.trim()).filter(Boolean)
|
||||
: []
|
||||
|
||||
const roomLabel = [
|
||||
order.buildingName || order.buildingSectionName || order.unitName || '',
|
||||
order.roomNo || order.roomCode || order.roomName || ''
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
const title = [
|
||||
order.areaName || order.campusName || '',
|
||||
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)
|
||||
|
||||
return {
|
||||
...order,
|
||||
id: roomLabel || order.roomNo || order.roomCode || order.orderNo || '',
|
||||
roomLabel: roomLabel || order.roomNo || order.roomCode || '',
|
||||
titleZh: title || order.roomTypeName || order.roomName || '当前入住房间',
|
||||
titleEn: order.roomTypeNameEn || title || order.roomTypeName || 'Current Stay',
|
||||
leaseText: [start, end].filter(Boolean).join(' - '),
|
||||
image: order.imageUrl || imageUrls[0] || ''
|
||||
}
|
||||
}
|
||||
|
||||
function clearStoreSessionState() {
|
||||
if (!store || !store.commit) {
|
||||
return
|
||||
}
|
||||
|
||||
store.commit('$tStore', { name: 'vuex_user', value: {} })
|
||||
store.commit('$tStore', { name: 'vuex_checkin_status', value: false })
|
||||
store.commit('$tStore', { name: 'vuex_current_order', value: null })
|
||||
store.commit('$tStore', { name: 'vuex_pending_contracts', value: [] })
|
||||
store.commit('$tStore', { name: 'vuex_pending_contract_count', value: 0 })
|
||||
store.commit('$tStore', { name: 'vuex_effective_contracts', value: [] })
|
||||
store.commit('$tStore', { name: 'vuex_valid_orders', value: [] })
|
||||
}
|
||||
|
||||
export { STORAGE_KEYS }
|
||||
|
||||
export function getRegisteredUser() {
|
||||
return getStorage(STORAGE_KEYS.REGISTERED_USER, null)
|
||||
}
|
||||
|
||||
export function saveRegisteredUser(user) {
|
||||
uni.setStorageSync(STORAGE_KEYS.REGISTERED_USER, user)
|
||||
return user
|
||||
}
|
||||
|
||||
export function getCurrentUser() {
|
||||
return getStorage(STORAGE_KEYS.USER_INFO, null)
|
||||
}
|
||||
|
||||
export function saveCurrentUser(user) {
|
||||
uni.setStorageSync(STORAGE_KEYS.USER_INFO, user)
|
||||
return user
|
||||
}
|
||||
|
||||
export function getLoginStatus() {
|
||||
return !!getStorage(STORAGE_KEYS.IS_LOGIN, false)
|
||||
}
|
||||
|
||||
export function setLoginStatus(isLogin) {
|
||||
uni.setStorageSync(STORAGE_KEYS.IS_LOGIN, !!isLogin)
|
||||
return !!isLogin
|
||||
}
|
||||
|
||||
export function getAuthToken() {
|
||||
return getStorage(STORAGE_KEYS.AUTH_TOKEN, '')
|
||||
}
|
||||
|
||||
export function saveAuthToken(token) {
|
||||
uni.setStorageSync(STORAGE_KEYS.AUTH_TOKEN, token || '')
|
||||
return token || ''
|
||||
}
|
||||
|
||||
export function clearAuthToken() {
|
||||
uni.removeStorageSync(STORAGE_KEYS.AUTH_TOKEN)
|
||||
}
|
||||
|
||||
export function getAuthTokenExpires() {
|
||||
return getStorage(STORAGE_KEYS.AUTH_TOKEN_EXPIRES, '')
|
||||
}
|
||||
|
||||
export function saveAuthTokenExpires(expires) {
|
||||
uni.setStorageSync(STORAGE_KEYS.AUTH_TOKEN_EXPIRES, expires || '')
|
||||
return expires || ''
|
||||
}
|
||||
|
||||
export function clearAuthTokenExpires() {
|
||||
uni.removeStorageSync(STORAGE_KEYS.AUTH_TOKEN_EXPIRES)
|
||||
}
|
||||
|
||||
export function startLoginSession(user) {
|
||||
setLoginStatus(true)
|
||||
saveCurrentUser(user)
|
||||
if (store && store.commit) {
|
||||
store.commit('$tStore', {
|
||||
name: 'vuex_user',
|
||||
value: user && typeof user === 'object' ? user : {}
|
||||
})
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
export function startAuthorizedLoginSession(user, token, expires) {
|
||||
if (token) saveAuthToken(token)
|
||||
if (expires) saveAuthTokenExpires(expires)
|
||||
return startLoginSession(user)
|
||||
}
|
||||
|
||||
export function registerAndLogin(user) {
|
||||
saveRegisteredUser(user)
|
||||
startLoginSession(user)
|
||||
return user
|
||||
}
|
||||
|
||||
export function clearLoginSession() {
|
||||
clearAuthToken()
|
||||
clearAuthTokenExpires()
|
||||
uni.removeStorageSync('lifeData')
|
||||
clearStoreSessionState()
|
||||
clearBookingState()
|
||||
clearContractState()
|
||||
clearMyRoomData()
|
||||
uni.removeStorageSync(STORAGE_KEYS.HAS_CHECKED_IN)
|
||||
uni.removeStorageSync(STORAGE_KEYS.IS_LOGIN)
|
||||
uni.removeStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
uni.removeStorageSync(STORAGE_KEYS.MY_APPLICATION)
|
||||
uni.removeStorageSync(STORAGE_KEYS.ACTIVE_APPLICATION_CODE)
|
||||
}
|
||||
|
||||
export function getCheckedInStatus() {
|
||||
return !!getStorage(STORAGE_KEYS.HAS_CHECKED_IN, false)
|
||||
}
|
||||
|
||||
export function setCheckedInStatus(hasCheckedIn) {
|
||||
uni.setStorageSync(STORAGE_KEYS.HAS_CHECKED_IN, !!hasCheckedIn)
|
||||
return !!hasCheckedIn
|
||||
}
|
||||
|
||||
export function getMyRoomData() {
|
||||
return getStorage(STORAGE_KEYS.MY_ROOM_DATA, null)
|
||||
}
|
||||
|
||||
export function saveMyRoomData(roomData) {
|
||||
uni.setStorageSync(STORAGE_KEYS.MY_ROOM_DATA, roomData)
|
||||
return roomData
|
||||
}
|
||||
|
||||
export function clearMyRoomData() {
|
||||
uni.removeStorageSync(STORAGE_KEYS.MY_ROOM_DATA)
|
||||
}
|
||||
|
||||
export function getCurrentBooking() {
|
||||
return getStorage(STORAGE_KEYS.CURRENT_BOOKING, null)
|
||||
}
|
||||
|
||||
export function saveCurrentBooking(booking) {
|
||||
uni.setStorageSync(STORAGE_KEYS.CURRENT_BOOKING, booking)
|
||||
return booking
|
||||
}
|
||||
|
||||
export function getBookingList() {
|
||||
return getStorage(STORAGE_KEYS.BOOKING_LIST, [])
|
||||
}
|
||||
|
||||
export function saveBookingList(list) {
|
||||
uni.setStorageSync(STORAGE_KEYS.BOOKING_LIST, list)
|
||||
return list
|
||||
}
|
||||
|
||||
export function clearBookingState() {
|
||||
uni.removeStorageSync(STORAGE_KEYS.CURRENT_BOOKING)
|
||||
uni.removeStorageSync(STORAGE_KEYS.BOOKING_LIST)
|
||||
}
|
||||
|
||||
export function getPendingContracts() {
|
||||
return getStorage(STORAGE_KEYS.PENDING_CONTRACTS, [])
|
||||
}
|
||||
|
||||
export function savePendingContracts(list) {
|
||||
uni.setStorageSync(STORAGE_KEYS.PENDING_CONTRACTS, list || [])
|
||||
return list || []
|
||||
}
|
||||
|
||||
export function getPendingContractCount() {
|
||||
return Number(getStorage(STORAGE_KEYS.PENDING_CONTRACT_COUNT, 0) || 0)
|
||||
}
|
||||
|
||||
export function savePendingContractCount(count) {
|
||||
const nextCount = Number(count || 0)
|
||||
uni.setStorageSync(STORAGE_KEYS.PENDING_CONTRACT_COUNT, nextCount)
|
||||
return nextCount
|
||||
}
|
||||
|
||||
export function getEffectiveContracts() {
|
||||
return getStorage(STORAGE_KEYS.EFFECTIVE_CONTRACTS, [])
|
||||
}
|
||||
|
||||
export function saveEffectiveContracts(list) {
|
||||
uni.setStorageSync(STORAGE_KEYS.EFFECTIVE_CONTRACTS, list || [])
|
||||
return list || []
|
||||
}
|
||||
|
||||
function patchContractListSign(list = [], contractNo = '', signImageUrl = '') {
|
||||
const targetNo = String(contractNo || '').trim()
|
||||
const targetSignImageUrl = String(signImageUrl || '').trim()
|
||||
|
||||
if (!targetNo || !Array.isArray(list) || !list.length) {
|
||||
return Array.isArray(list) ? list : []
|
||||
}
|
||||
|
||||
return list.map(item => {
|
||||
const currentNo = String((item && (item.no || item.contractNo)) || '').trim()
|
||||
if (currentNo !== targetNo) {
|
||||
return item
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
signImageUrl: targetSignImageUrl || item.signImageUrl || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function patchStoredContractSign(contractNo, signImageUrl) {
|
||||
const nextPendingContracts = patchContractListSign(getPendingContracts(), contractNo, signImageUrl)
|
||||
const nextEffectiveContracts = patchContractListSign(getEffectiveContracts(), contractNo, signImageUrl)
|
||||
|
||||
savePendingContracts(nextPendingContracts)
|
||||
saveEffectiveContracts(nextEffectiveContracts)
|
||||
|
||||
return {
|
||||
pendingContracts: nextPendingContracts,
|
||||
effectiveContracts: nextEffectiveContracts
|
||||
}
|
||||
}
|
||||
|
||||
export function clearContractState() {
|
||||
uni.removeStorageSync(STORAGE_KEYS.PENDING_CONTRACTS)
|
||||
uni.removeStorageSync(STORAGE_KEYS.PENDING_CONTRACT_COUNT)
|
||||
uni.removeStorageSync(STORAGE_KEYS.EFFECTIVE_CONTRACTS)
|
||||
}
|
||||
|
||||
export function getListStorage(key) {
|
||||
return getStorage(key, [])
|
||||
}
|
||||
|
||||
export function saveListStorage(key, list) {
|
||||
uni.setStorageSync(key, list)
|
||||
return list
|
||||
}
|
||||
|
||||
export function prependListStorage(key, item) {
|
||||
const currentList = getListStorage(key)
|
||||
const nextList = [item, ...currentList]
|
||||
saveListStorage(key, nextList)
|
||||
return nextList
|
||||
}
|
||||
|
||||
export function getMyApplicationList() {
|
||||
return getListStorage(STORAGE_KEYS.MY_APPLICATION)
|
||||
}
|
||||
|
||||
export function saveMyApplicationList(list) {
|
||||
return saveListStorage(STORAGE_KEYS.MY_APPLICATION, list || [])
|
||||
}
|
||||
|
||||
export function getActiveApplicationCode() {
|
||||
return getStorage(STORAGE_KEYS.ACTIVE_APPLICATION_CODE, '')
|
||||
}
|
||||
|
||||
export function saveActiveApplicationCode(code) {
|
||||
const nextCode = code || ''
|
||||
uni.setStorageSync(STORAGE_KEYS.ACTIVE_APPLICATION_CODE, nextCode)
|
||||
return nextCode
|
||||
}
|
||||
|
||||
export function getFailedRegistrationDraft() {
|
||||
return getStorage(STORAGE_KEYS.FAILED_REGISTRATION_DRAFT, null)
|
||||
}
|
||||
|
||||
export function saveFailedRegistrationDraft(draft) {
|
||||
const nextDraft = draft && typeof draft === 'object' ? draft : null
|
||||
if (!nextDraft) {
|
||||
uni.removeStorageSync(STORAGE_KEYS.FAILED_REGISTRATION_DRAFT)
|
||||
return null
|
||||
}
|
||||
uni.setStorageSync(STORAGE_KEYS.FAILED_REGISTRATION_DRAFT, nextDraft)
|
||||
return nextDraft
|
||||
}
|
||||
|
||||
export function clearFailedRegistrationDraft() {
|
||||
uni.removeStorageSync(STORAGE_KEYS.FAILED_REGISTRATION_DRAFT)
|
||||
}
|
||||
|
||||
export function getHomePageTabId() {
|
||||
return getStorage(STORAGE_KEYS.HOME_PAGE_TAB_ID, '')
|
||||
}
|
||||
|
||||
export function saveHomePageTabId(tabId) {
|
||||
const nextTabId = tabId || ''
|
||||
uni.setStorageSync(STORAGE_KEYS.HOME_PAGE_TAB_ID, nextTabId)
|
||||
return nextTabId
|
||||
}
|
||||
|
||||
export function buildHomePageUrl(tabId) {
|
||||
const nextTabId = tabId || getHomePageTabId()
|
||||
return nextTabId
|
||||
? `/pages/index/index?pageId=${encodeURIComponent(nextTabId)}`
|
||||
: '/pages/index/index'
|
||||
}
|
||||
|
||||
export function getSessionSnapshot() {
|
||||
const storeUser = getStoreUser()
|
||||
const storageCurrentUser = getCurrentUser() || {}
|
||||
const registeredUser = getRegisteredUser() || {}
|
||||
const currentUser = Object.keys(storeUser).length ? storeUser : storageCurrentUser
|
||||
const resolvedUser = Object.keys(currentUser).length ? currentUser : registeredUser
|
||||
const storeCurrentOrder = getStoreCurrentOrder()
|
||||
const storeBookingList = getStoreValidOrders()
|
||||
const storePendingContracts = getStorePendingContracts()
|
||||
const storeEffectiveContracts = getStoreEffectiveContracts()
|
||||
const currentBooking = resolvedUser.currentBooking || getCurrentBooking()
|
||||
const pendingContractCount = storePendingContracts.length || getPendingContractCount()
|
||||
const hasCheckedIn = !!(resolvedUser.hasCheckedIn || getStoreState().vuex_checkin_status || getCheckedInStatus())
|
||||
const myRoomData = hasCheckedIn
|
||||
? (getMyRoomData() || buildRoomDataFromOrder(storeCurrentOrder))
|
||||
: null
|
||||
const isLogin = getLoginStatus() || !!Object.keys(currentUser).length
|
||||
|
||||
return {
|
||||
isLogin,
|
||||
currentUser,
|
||||
registeredUser,
|
||||
resolvedUser,
|
||||
hasCheckedIn: isLogin && hasCheckedIn,
|
||||
currentBooking,
|
||||
bookingList: storeBookingList.length ? storeBookingList : getBookingList(),
|
||||
myRoomData,
|
||||
pendingContracts: storePendingContracts.length ? storePendingContracts : getPendingContracts(),
|
||||
pendingContractCount,
|
||||
effectiveContracts: storeEffectiveContracts.length ? storeEffectiveContracts : getEffectiveContracts()
|
||||
}
|
||||
}
|
||||
|
||||
export function clearLegacyDemoCache() {
|
||||
if (uni.getStorageSync(STORAGE_KEYS.LEGACY_DEMO_CACHE_CLEARED)) {
|
||||
return
|
||||
}
|
||||
;[
|
||||
STORAGE_KEYS.CURRENT_BOOKING,
|
||||
STORAGE_KEYS.BOOKING_LIST,
|
||||
STORAGE_KEYS.MY_APPLICATION,
|
||||
STORAGE_KEYS.MESSAGE_LIST,
|
||||
STORAGE_KEYS.SERVICE_ORDER_LIST,
|
||||
STORAGE_KEYS.UTILITY_RECHARGE_LIST
|
||||
].forEach(key => {
|
||||
uni.removeStorageSync(key)
|
||||
})
|
||||
uni.setStorageSync(STORAGE_KEYS.LEGACY_DEMO_CACHE_CLEARED, true)
|
||||
}
|
||||
1
utils/banner-api.js
Normal file
1
utils/banner-api.js
Normal file
@@ -0,0 +1 @@
|
||||
export { bannerAll } from '../api/index'
|
||||
112
utils/booking-guard.js
Normal file
112
utils/booking-guard.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { getSessionSnapshot } from './app-state'
|
||||
import { getBookingStatusText, getStoredUser } from './flow'
|
||||
import { getCurrentLanguage } from './i18n'
|
||||
|
||||
const ACTIVE_BOOKING_STATUSES = ['pending_review', 'approved_wait_assign', 'assigned_wait_room']
|
||||
|
||||
function isZh(language) {
|
||||
return language !== 'en'
|
||||
}
|
||||
|
||||
function getGuardText(language = 'zh') {
|
||||
const zh = isZh(language)
|
||||
return {
|
||||
loginTitle: zh ? '提示' : 'Notice',
|
||||
loginContent: zh ? '您还未登录,请先登录或注册账号。' : 'Please sign in or register an account first.',
|
||||
loginConfirmText: zh ? '去登录' : 'Sign In',
|
||||
existingBookingTitle: zh ? '已有进行中的预订' : 'Existing Booking',
|
||||
existingBookingContent: zh
|
||||
? '您当前状态为“{status}”,请先查看预订详情。'
|
||||
: 'Your current booking status is "{status}". Please review the booking detail first.',
|
||||
viewBooking: zh ? '查看预订' : 'View Booking'
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueryString(query = {}) {
|
||||
return Object.keys(query).reduce((result, key) => {
|
||||
const value = query[key]
|
||||
if (value === '' || value === null || typeof value === 'undefined') {
|
||||
return result
|
||||
}
|
||||
result.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
return result
|
||||
}, []).join('&')
|
||||
}
|
||||
|
||||
function getCurrentPageUrl() {
|
||||
const pages = getCurrentPages()
|
||||
if (!Array.isArray(pages) || !pages.length) {
|
||||
return ''
|
||||
}
|
||||
const currentPage = pages[pages.length - 1] || {}
|
||||
const route = currentPage.route || ''
|
||||
const options = currentPage.options || (currentPage.$page && currentPage.$page.options) || {}
|
||||
if (!route) {
|
||||
return ''
|
||||
}
|
||||
const queryString = buildQueryString(options)
|
||||
return `/${route}${queryString ? `?${queryString}` : ''}`
|
||||
}
|
||||
|
||||
export function ensureBookingAccess(options = {}) {
|
||||
const language = options.language || getCurrentLanguage()
|
||||
const guardText = getGuardText(language)
|
||||
const session = getSessionSnapshot()
|
||||
const sessionUser = session.resolvedUser && Object.keys(session.resolvedUser).length
|
||||
? session.resolvedUser
|
||||
: {}
|
||||
const optionUser = options.userInfo && Object.keys(options.userInfo).length
|
||||
? options.userInfo
|
||||
: null
|
||||
const userInfo = optionUser
|
||||
? {
|
||||
...sessionUser,
|
||||
...optionUser
|
||||
}
|
||||
: (Object.keys(sessionUser).length ? sessionUser : getStoredUser())
|
||||
|
||||
if (!session.isLogin) {
|
||||
uni.showModal({
|
||||
title: options.loginTitle || guardText.loginTitle,
|
||||
content: options.loginContent || guardText.loginContent,
|
||||
confirmText: options.loginConfirmText || guardText.loginConfirmText,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const redirectUrl = options.loginRedirectUrl || getCurrentPageUrl()
|
||||
const query = [`bypass=1`]
|
||||
if (redirectUrl) {
|
||||
query.push(`redirect=${encodeURIComponent(redirectUrl)}`)
|
||||
}
|
||||
uni.navigateTo({ url: `/pkgAccount/login/login?${query.join('&')}` })
|
||||
}
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (ACTIVE_BOOKING_STATUSES.includes(userInfo.bookingStatus)) {
|
||||
const statusText = getBookingStatusText(userInfo.bookingStatus, language)
|
||||
uni.showModal({
|
||||
title: guardText.existingBookingTitle,
|
||||
content: guardText.existingBookingContent.replace('{status}', statusText),
|
||||
confirmText: guardText.viewBooking,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const orderNo = encodeURIComponent(
|
||||
userInfo.currentBooking && userInfo.currentBooking.orderNo
|
||||
? userInfo.currentBooking.orderNo
|
||||
: ''
|
||||
)
|
||||
uni.navigateTo({
|
||||
url: orderNo
|
||||
? `/pkgBooking/booking_detail/booking_detail?orderNo=${orderNo}`
|
||||
: '/pkgBooking/booking_detail/booking_detail'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
386
utils/booking-helper.js
Normal file
386
utils/booking-helper.js
Normal file
@@ -0,0 +1,386 @@
|
||||
import { getCurrentLanguage } from './i18n'
|
||||
|
||||
function getBookingText(key = '', language = getCurrentLanguage()) {
|
||||
const isEn = language === 'en'
|
||||
const map = {
|
||||
pendingReview: isEn ? 'Pending Review' : '审核中',
|
||||
approved: isEn ? 'Approved' : '已通过',
|
||||
waitingCheckin: isEn ? 'Waiting Check-in' : '待入住',
|
||||
rejected: isEn ? 'Rejected' : '已驳回',
|
||||
processing: isEn ? 'Processing' : '处理中',
|
||||
contractSigned: isEn ? 'Signed' : '已签署'
|
||||
}
|
||||
return map[key] || ''
|
||||
}
|
||||
|
||||
function parseJsonStringMaybe(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
return value
|
||||
}
|
||||
|
||||
const looksLikeJson = (
|
||||
(trimmed.startsWith('{') && trimmed.endsWith('}'))
|
||||
|| (trimmed.startsWith('[') && trimmed.endsWith(']'))
|
||||
)
|
||||
|
||||
if (!looksLikeJson) {
|
||||
return value
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed)
|
||||
} catch (error) {
|
||||
console.warn('[orderSearchedMePage] JSON parse failed:', error)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function parseResponsePayload(response) {
|
||||
const basePayload = response && response.data ? response.data : response
|
||||
return parseJsonStringMaybe(basePayload)
|
||||
}
|
||||
|
||||
function collectObjectArray(value, depth = 0) {
|
||||
if (!value || depth > 6) {
|
||||
return []
|
||||
}
|
||||
|
||||
const normalizedValue = parseJsonStringMaybe(value)
|
||||
|
||||
if (Array.isArray(normalizedValue)) {
|
||||
if (normalizedValue.some(item => item && typeof item === 'object' && !Array.isArray(item))) {
|
||||
return normalizedValue
|
||||
}
|
||||
|
||||
for (let index = 0; index < normalizedValue.length; index += 1) {
|
||||
const nestedList = collectObjectArray(normalizedValue[index], depth + 1)
|
||||
if (nestedList.length) {
|
||||
return nestedList
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
if (typeof normalizedValue !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
const candidateKeys = [
|
||||
'data',
|
||||
'list',
|
||||
'rows',
|
||||
'items',
|
||||
'records',
|
||||
'result',
|
||||
'payload',
|
||||
'$values'
|
||||
]
|
||||
|
||||
for (let index = 0; index < candidateKeys.length; index += 1) {
|
||||
const key = candidateKeys[index]
|
||||
if (!Object.prototype.hasOwnProperty.call(normalizedValue, key)) {
|
||||
continue
|
||||
}
|
||||
const nestedList = collectObjectArray(normalizedValue[key], depth + 1)
|
||||
if (nestedList.length) {
|
||||
return nestedList
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export function parseOrderListResponse(response) {
|
||||
const payload = parseResponsePayload(response)
|
||||
const orderList = collectObjectArray(payload)
|
||||
|
||||
if (!orderList.length) {
|
||||
console.warn('[orderSearchedMePage] unsupported response structure:', payload)
|
||||
} else {
|
||||
console.log('[orderSearchedMePage] parsed order count:', orderList.length)
|
||||
}
|
||||
|
||||
return orderList
|
||||
}
|
||||
|
||||
export function parseOrderDetailResponse(response) {
|
||||
const payload = parseResponsePayload(response)
|
||||
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
|
||||
const orderList = collectObjectArray(payload)
|
||||
return Array.isArray(orderList) && orderList.length ? orderList[0] : {}
|
||||
}
|
||||
|
||||
export function formatOrderDateTime(value) {
|
||||
if (!value) {
|
||||
return '--'
|
||||
}
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value).replace('T', ' ').slice(0, 16) || '--'
|
||||
}
|
||||
const pad = item => String(item).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
|
||||
export function formatOrderDate(value) {
|
||||
if (!value) {
|
||||
return '--'
|
||||
}
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value).slice(0, 10) || '--'
|
||||
}
|
||||
const pad = item => String(item).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
}
|
||||
|
||||
function normalizeStatusKey(order = {}) {
|
||||
const rawStatus = String(order.status || order.orderStatus || '').trim()
|
||||
if (['pending_review', 'approved_wait_assign', 'assigned_wait_room', 'rejected', 'contract_signed'].includes(rawStatus)) {
|
||||
return rawStatus
|
||||
}
|
||||
|
||||
const contractStatus = Number(order.contractStatus)
|
||||
if (contractStatus === 1) {
|
||||
return 'contract_signed'
|
||||
}
|
||||
|
||||
const approvalStatus = Number(
|
||||
order.approvalStatus !== undefined
|
||||
? order.approvalStatus
|
||||
: order.auditStatus !== undefined
|
||||
? order.auditStatus
|
||||
: order.statusCode
|
||||
)
|
||||
|
||||
if (approvalStatus === -1 || /reject|refuse|驳回/i.test(rawStatus)) {
|
||||
return 'rejected'
|
||||
}
|
||||
if (approvalStatus === 0 || /pending|review|审核中|待审批/i.test(rawStatus)) {
|
||||
return 'pending_review'
|
||||
}
|
||||
if (
|
||||
order.roomNo ||
|
||||
order.roomCode ||
|
||||
order.roomName ||
|
||||
order.checkinDate ||
|
||||
order.startDate ||
|
||||
order.start
|
||||
) {
|
||||
return 'assigned_wait_room'
|
||||
}
|
||||
return 'approved_wait_assign'
|
||||
}
|
||||
|
||||
function resolveStatusText(order = {}, statusKey, language = getCurrentLanguage()) {
|
||||
|
||||
const contractStatus = Number(order.contractStatus)
|
||||
if (contractStatus === 1 && order.contractStatusDesc && language !== 'en') {
|
||||
return order.contractStatusDesc
|
||||
}
|
||||
|
||||
if (language !== 'en' && order.approvalStatusDesc) {
|
||||
return order.approvalStatusDesc
|
||||
}
|
||||
if (language !== 'en' && order.statusDesc) {
|
||||
return order.statusDesc
|
||||
}
|
||||
const map = {
|
||||
pending_review: getBookingText('pendingReview', language),
|
||||
approved_wait_assign: getBookingText('approved', language),
|
||||
assigned_wait_room: getBookingText('waitingCheckin', language),
|
||||
rejected: getBookingText('rejected', language),
|
||||
contract_signed: getBookingText('contractSigned', language)
|
||||
}
|
||||
return map[statusKey] || getBookingText('processing', language)
|
||||
}
|
||||
|
||||
function resolveTagType(statusKey) {
|
||||
if (statusKey === 'rejected') {
|
||||
return 'rejected'
|
||||
}
|
||||
if (statusKey === 'pending_review') {
|
||||
return 'pending'
|
||||
}
|
||||
if (statusKey === 'contract_signed') {
|
||||
return 'approved'
|
||||
}
|
||||
return 'approved'
|
||||
}
|
||||
|
||||
function getBuildingFloorText(order = {}, language = getCurrentLanguage()) {
|
||||
const isEn = language === 'en'
|
||||
const building = [
|
||||
isEn
|
||||
? (order.buildingNameEn || order.buildingEn || order.buildingSectionNameEn || order.buildingName || order.building || order.buildingSectionName || '')
|
||||
: (order.buildingName || order.building || order.buildingSectionName || ''),
|
||||
isEn
|
||||
? (order.floorNameEn || order.floorEn || order.floorName || order.floor || '')
|
||||
: (order.floorName || order.floor || '')
|
||||
].filter(Boolean).join(' ')
|
||||
return building || '--'
|
||||
}
|
||||
|
||||
function getRoomTypeText(order = {}, language = getCurrentLanguage()) {
|
||||
const isEn = language === 'en'
|
||||
if (isEn) {
|
||||
return order.roomTypeNameEn || order.styleNameEn || order.roomNameEn || order.roomTypeName || order.styleNameZh || order.roomType || order.roomName || '--'
|
||||
}
|
||||
return order.roomTypeName || order.styleNameZh || order.roomType || order.roomName || '--'
|
||||
}
|
||||
|
||||
function getOrderNo(order = {}) {
|
||||
return order.orderNo || order.no || order.code || '--'
|
||||
}
|
||||
|
||||
export function getOrderStatusCode(order = {}) {
|
||||
const candidates = [
|
||||
order.orderStatus,
|
||||
order.statusCode,
|
||||
order.status
|
||||
]
|
||||
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
const value = candidates[index]
|
||||
if (value === '' || value === null || value === undefined) {
|
||||
continue
|
||||
}
|
||||
const numericValue = Number(value)
|
||||
if (Number.isFinite(numericValue)) {
|
||||
return numericValue
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function getOrderTime(order = {}) {
|
||||
const checkInPeriod = order.checkInPeriod || {}
|
||||
const candidates = [
|
||||
order.updateTime,
|
||||
order.createTime,
|
||||
order.createdAt,
|
||||
order.startDate,
|
||||
order.start,
|
||||
order.checkinDate,
|
||||
checkInPeriod.start
|
||||
]
|
||||
|
||||
const timestamp = candidates
|
||||
.map(value => new Date(value || 0).getTime())
|
||||
.find(value => Number.isFinite(value) && value > 0)
|
||||
|
||||
return timestamp || 0
|
||||
}
|
||||
|
||||
export function sortOrdersByLatest(orderList = []) {
|
||||
return [...orderList].sort((prev, next) => getOrderTime(next) - getOrderTime(prev))
|
||||
}
|
||||
|
||||
function hasStayFields(order = {}) {
|
||||
return !!(
|
||||
order.roomNo
|
||||
|| order.roomCode
|
||||
|| order.slotCode
|
||||
|| order.contractNo
|
||||
|| order.actualCheckInTime
|
||||
|| order.checkinTime
|
||||
)
|
||||
}
|
||||
|
||||
export function isRejectedBookingOrder(order = {}) {
|
||||
const statusKey = normalizeStatusKey(order)
|
||||
const statusCode = getOrderStatusCode(order)
|
||||
return statusKey === 'rejected' || statusCode === -1
|
||||
}
|
||||
|
||||
export function isCheckedInOrder(order = {}) {
|
||||
const statusCode = getOrderStatusCode(order)
|
||||
if (statusCode !== null) {
|
||||
return statusCode > 0
|
||||
}
|
||||
|
||||
const contractStatus = Number(order.contractStatus)
|
||||
if (contractStatus === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isRejectedBookingOrder(order)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasStayFields(order)
|
||||
}
|
||||
|
||||
export function isCurrentBookingOrder(order = {}) {
|
||||
if (isRejectedBookingOrder(order) || isCheckedInOrder(order)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const statusCode = getOrderStatusCode(order)
|
||||
if (statusCode === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const statusKey = normalizeStatusKey(order)
|
||||
return ['pending_review', 'approved_wait_assign', 'assigned_wait_room', 'contract_signed'].includes(statusKey)
|
||||
}
|
||||
|
||||
export function normalizeBookingOrder(order = {}, language = getCurrentLanguage()) {
|
||||
const statusKey = normalizeStatusKey(order)
|
||||
const statusCode = getOrderStatusCode(order)
|
||||
const checkInPeriod = order.checkInPeriod || {}
|
||||
const startDate = order.startDate || order.start || order.checkinDate || checkInPeriod.start
|
||||
const endDate = order.endDate || order.end || order.checkoutDate || checkInPeriod.end
|
||||
return {
|
||||
...order,
|
||||
orderNo: getOrderNo(order),
|
||||
roomTypeText: getRoomTypeText(order, language),
|
||||
buildingFloorText: getBuildingFloorText(order, language),
|
||||
startDateText: formatOrderDate(startDate),
|
||||
endDateText: formatOrderDate(endDate),
|
||||
createdAtText: formatOrderDateTime(order.createTime || order.updateTime || order.createdAt),
|
||||
roomNoText: language === 'en'
|
||||
? (order.roomNoEn || order.roomCodeEn || order.roomNameEn || order.roomNo || order.roomCode || order.roomName || '--')
|
||||
: (order.roomNo || order.roomCode || order.roomName || order.roomNoEn || order.roomCodeEn || order.roomNameEn || '--'),
|
||||
areaText: language === 'en'
|
||||
? (order.areaNameEn || order.campusNameEn || order.areaName || order.campusName || '--')
|
||||
: (order.areaName || order.campusName || '--'),
|
||||
rejectReasonText: order.rejectReason || order.approveReason || order.remark || '',
|
||||
startDate,
|
||||
endDate,
|
||||
status: statusKey,
|
||||
statusCode,
|
||||
statusKey,
|
||||
statusText: resolveStatusText(order, statusKey, language),
|
||||
tagType: resolveTagType(statusKey)
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeBookingList(orderList = [], language = getCurrentLanguage()) {
|
||||
return sortOrdersByLatest(orderList).map(item => normalizeBookingOrder(item, language))
|
||||
}
|
||||
|
||||
export function normalizeBookingDetail(order = {}, language = getCurrentLanguage()) {
|
||||
if (!order || typeof order !== 'object' || Array.isArray(order)) {
|
||||
return null
|
||||
}
|
||||
return normalizeBookingOrder(order, language)
|
||||
}
|
||||
|
||||
export function pickCurrentStayOrder(orderList = []) {
|
||||
return sortOrdersByLatest(orderList).find(item => isCheckedInOrder(item)) || null
|
||||
}
|
||||
|
||||
export function pickCurrentBooking(orderList = []) {
|
||||
return sortOrdersByLatest(orderList).find(item => isCurrentBookingOrder(item)) || null
|
||||
}
|
||||
10
utils/common.js
Normal file
10
utils/common.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { config } from '../config.js'
|
||||
|
||||
export function addFilePrefix(url = '') {
|
||||
const value = String(url || '').trim()
|
||||
if (!value) return ''
|
||||
if (/^(https?:\/\/|data:image|blob:)/i.test(value)) {
|
||||
return value
|
||||
}
|
||||
return `${config.filePrefix}${value.startsWith('/') ? '' : '/'}${value}`
|
||||
}
|
||||
167
utils/enum-store.js
Normal file
167
utils/enum-store.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { getEnums } from '@/api/index.js'
|
||||
import { getCurrentLanguage } from '@/utils/i18n'
|
||||
|
||||
const ENUMS_CACHE_KEY = 'mini_enums_cache_v1'
|
||||
|
||||
let enumsMap = {}
|
||||
let enumsLanguage = ''
|
||||
let fetchPromise = null
|
||||
|
||||
function normalizeEnumMap(data) {
|
||||
const map = {}
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((item) => {
|
||||
if (item && item.code) {
|
||||
map[item.code] = Array.isArray(item.data) ? item.data : []
|
||||
}
|
||||
})
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
function getNormalizedLanguage(language = '') {
|
||||
return String(language || getCurrentLanguage() || 'zh').trim().toLowerCase() === 'en' ? 'en' : 'zh'
|
||||
}
|
||||
|
||||
function readEnumsCache(language) {
|
||||
const cacheLanguage = getNormalizedLanguage(language)
|
||||
try {
|
||||
const cached = uni.getStorageSync(ENUMS_CACHE_KEY)
|
||||
if (!cached) return {}
|
||||
const resolveLanguageBucket = (payload) => {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return {}
|
||||
}
|
||||
if (payload.en || payload.zh) {
|
||||
const bucket = payload[cacheLanguage]
|
||||
return bucket && typeof bucket === 'object' ? bucket : {}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
if (typeof cached === 'string') {
|
||||
const parsed = JSON.parse(cached)
|
||||
return resolveLanguageBucket(parsed)
|
||||
}
|
||||
return resolveLanguageBucket(cached)
|
||||
} catch (error) {
|
||||
console.error('Failed to read enums cache', error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function writeEnumsCache(map, language) {
|
||||
const cacheLanguage = getNormalizedLanguage(language)
|
||||
try {
|
||||
const cached = uni.getStorageSync(ENUMS_CACHE_KEY)
|
||||
let nextCache = {}
|
||||
if (cached && typeof cached === 'object' && !Array.isArray(cached)) {
|
||||
nextCache = { ...cached }
|
||||
} else if (typeof cached === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(cached)
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
nextCache = { ...parsed }
|
||||
}
|
||||
} catch (error) {
|
||||
nextCache = {}
|
||||
}
|
||||
}
|
||||
nextCache[cacheLanguage] = map
|
||||
uni.setStorageSync(ENUMS_CACHE_KEY, nextCache)
|
||||
} catch (error) {
|
||||
console.error('Failed to write enums cache', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function initEnumsFromCache(language) {
|
||||
const currentLanguage = getNormalizedLanguage(language)
|
||||
if (Object.keys(enumsMap).length && enumsLanguage === currentLanguage) return enumsMap
|
||||
enumsMap = readEnumsCache(currentLanguage)
|
||||
enumsLanguage = currentLanguage
|
||||
return enumsMap
|
||||
}
|
||||
|
||||
export async function fetchEnums(force = false, language) {
|
||||
const currentLanguage = getNormalizedLanguage(language)
|
||||
if (!force && Object.keys(enumsMap).length && enumsLanguage === currentLanguage) return enumsMap
|
||||
if (fetchPromise) return fetchPromise
|
||||
|
||||
fetchPromise = (async () => {
|
||||
if (!force && (!Object.keys(enumsMap).length || enumsLanguage !== currentLanguage)) {
|
||||
initEnumsFromCache(currentLanguage)
|
||||
if (Object.keys(enumsMap).length) return enumsMap
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getEnums({}, currentLanguage)
|
||||
const data = res && res.data ? res.data : res
|
||||
const map = normalizeEnumMap(data)
|
||||
if (Object.keys(map).length) {
|
||||
enumsMap = map
|
||||
enumsLanguage = currentLanguage
|
||||
writeEnumsCache(map, currentLanguage)
|
||||
}
|
||||
return enumsMap
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch enums', error)
|
||||
return enumsMap
|
||||
} finally {
|
||||
fetchPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return fetchPromise
|
||||
}
|
||||
|
||||
export function getEnumOptions(code, language) {
|
||||
initEnumsFromCache(language)
|
||||
return enumsMap[code] || []
|
||||
}
|
||||
|
||||
export function getEnumLabel(code, key, fallback = '', language) {
|
||||
const options = getEnumOptions(code, language)
|
||||
const matched = options.find(item => String(item.key) === String(key))
|
||||
if (matched) return matched.value
|
||||
return fallback || key
|
||||
}
|
||||
|
||||
export function getApprovalStatusLabel(status, fallback = '', language) {
|
||||
return getEnumLabel('ApprovalStatusEnum', status, fallback, language)
|
||||
}
|
||||
|
||||
export function getGenderLabel(gender, fallback = '', language) {
|
||||
return getEnumLabel('GenderEnum', gender, fallback, language)
|
||||
}
|
||||
|
||||
export const ACCOUNT_BILL_STATUS = {
|
||||
ACTIVE: 10,
|
||||
FINISHED: 99
|
||||
}
|
||||
|
||||
export const ACCOUNT_BILL_PAYMENT_STATUS = {
|
||||
UNPAID: 0,
|
||||
PARTIAL: 1,
|
||||
PAID: 2
|
||||
}
|
||||
|
||||
export const ACCOUNT_BILL_REFUND_STATUS = {
|
||||
NONE: 0,
|
||||
PARTIAL: 10,
|
||||
REFUNDED: 99
|
||||
}
|
||||
|
||||
export function getAccountBillStatusLabel(status, fallback = '', language) {
|
||||
return getEnumLabel('AccountBillStatusEnum', status, fallback, language)
|
||||
}
|
||||
|
||||
export function getAccountBillPaymentStatusLabel(status, fallback = '', language) {
|
||||
return getEnumLabel('AccountBillPaymentStatusEnum', status, fallback, language)
|
||||
}
|
||||
|
||||
export function getAccountBillRefundStatusLabel(status, fallback = '', language) {
|
||||
return getEnumLabel('AccountBillRefundStatusEnum', status, fallback, language)
|
||||
}
|
||||
|
||||
export function getAccountReferenceTypeLabel(type, fallback = '', language) {
|
||||
return getEnumLabel('AccountReferenceTypeEnum', type, fallback, language)
|
||||
}
|
||||
294
utils/flow.js
Normal file
294
utils/flow.js
Normal file
@@ -0,0 +1,294 @@
|
||||
import {
|
||||
getBookingList,
|
||||
getCurrentUser,
|
||||
getRegisteredUser,
|
||||
saveBookingList,
|
||||
saveCurrentBooking,
|
||||
saveCurrentUser,
|
||||
saveRegisteredUser
|
||||
} from './app-state'
|
||||
import { getApprovalStatusLabel, getGenderLabel } from './enum-store'
|
||||
import { getCurrentLanguage } from './i18n'
|
||||
import store from '@/store'
|
||||
|
||||
const AUTH_STATUS_TEXT = {
|
||||
zh: {
|
||||
unsubmitted: '未实名',
|
||||
pending: '审核中',
|
||||
approved: '已通过',
|
||||
rejected: '已驳回'
|
||||
},
|
||||
en: {
|
||||
unsubmitted: 'Not Verified',
|
||||
pending: 'Under Review',
|
||||
approved: 'Approved',
|
||||
rejected: 'Rejected'
|
||||
}
|
||||
}
|
||||
|
||||
const BOOKING_STATUS_TEXT = {
|
||||
zh: {
|
||||
none: '暂无预订',
|
||||
pending_review: '预订审核中',
|
||||
approved_wait_assign: '审核通过',
|
||||
assigned_wait_room: '已分配管家,待分房号',
|
||||
rejected: '预订已驳回'
|
||||
},
|
||||
en: {
|
||||
none: 'No Booking',
|
||||
pending_review: 'Booking Under Review',
|
||||
approved_wait_assign: 'Approved',
|
||||
assigned_wait_room: 'Butler Assigned, Awaiting Room',
|
||||
rejected: 'Booking Rejected'
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLanguageMap(map, language) {
|
||||
const currentLanguage = language || getCurrentLanguage()
|
||||
return currentLanguage === 'en' ? map.en : map.zh
|
||||
}
|
||||
|
||||
function getFlowGenderFallback(gender, language) {
|
||||
const currentLanguage = language || getCurrentLanguage()
|
||||
if (gender === 1) {
|
||||
return currentLanguage === 'en' ? 'Male' : '男'
|
||||
}
|
||||
if (gender === 0) {
|
||||
return currentLanguage === 'en' ? 'Female' : '女'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function mapRegistrationApprovalToAuthStatus(status) {
|
||||
if (status === 1) return 'approved'
|
||||
if (status === -1) return 'rejected'
|
||||
if (status === 0) return 'pending'
|
||||
return 'unsubmitted'
|
||||
}
|
||||
|
||||
function normalizeLookupValue(value) {
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
return String(value).trim()
|
||||
}
|
||||
|
||||
function normalizeRegistrationCodeCandidate(value) {
|
||||
const normalized = normalizeLookupValue(value)
|
||||
if (!normalized) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 过滤业务状态码,避免把 200/500 这类响应码误存成申请单编号
|
||||
if (/^\d{1,6}$/.test(normalized)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function resolveRegistrationApplicationCode(source = {}) {
|
||||
const raw = source && source.data ? source.data : source
|
||||
const candidates = [
|
||||
source && source.registrationCode,
|
||||
source && source.data,
|
||||
raw && raw.code,
|
||||
raw && raw.registrationCode,
|
||||
raw,
|
||||
raw && raw.data && raw.data.code,
|
||||
raw && raw.data && raw.data.registrationCode,
|
||||
raw && raw.data,
|
||||
source && source.data && source.data.data && source.data.data.code,
|
||||
source && source.data && source.data.data && source.data.data.registrationCode,
|
||||
source && source.code
|
||||
]
|
||||
|
||||
return candidates.map(normalizeRegistrationCodeCandidate).find(Boolean) || ''
|
||||
}
|
||||
|
||||
export function normalizeUser(raw = {}) {
|
||||
const user = raw || {}
|
||||
return {
|
||||
...user,
|
||||
hasAuth: user.hasAuth === true,
|
||||
authStatus: user.authStatus || 'unsubmitted',
|
||||
authRejectReason: user.authRejectReason || '',
|
||||
authProfile: user.authProfile || null,
|
||||
bookingStatus: user.bookingStatus || 'none',
|
||||
bookingRejectReason: user.bookingRejectReason || '',
|
||||
currentBooking: user.currentBooking || null,
|
||||
hasCheckedIn: user.hasCheckedIn === true
|
||||
}
|
||||
}
|
||||
|
||||
export function buildUserFromRegistrationApplication(application = {}, options = {}) {
|
||||
const source = application || {}
|
||||
const normalizedOptions = options || {}
|
||||
const language = normalizedOptions.language || getCurrentLanguage()
|
||||
const profile = {
|
||||
identityType: source.passportNumber ? 'foreign' : 'domestic',
|
||||
account: source.account || normalizedOptions.account || '',
|
||||
email: source.email || normalizedOptions.email || '',
|
||||
roleNo: source.roleNo || normalizedOptions.roleNo || '',
|
||||
name: source.name || normalizedOptions.name || '',
|
||||
gender: typeof source.gender === 'number' ? source.gender : null,
|
||||
genderText: source.genderDesc || getGenderLabel(source.gender, getFlowGenderFallback(source.gender, language), language),
|
||||
phone: source.phone || normalizedOptions.phone || '',
|
||||
country: source.country || normalizedOptions.country || '',
|
||||
idCard: source.idCard || '',
|
||||
workUnit: source.workUnit || '',
|
||||
hasHangzhouProperty: typeof source.hasHangzhouProperty === 'boolean' ? source.hasHangzhouProperty : null,
|
||||
dietaryHabitTags: source.dietaryHabitTags || [],
|
||||
hobbyTags: source.hobbyTags || [],
|
||||
passportNumber: source.passportNumber || '',
|
||||
passportExpiry: source.passportExpiry || '',
|
||||
school: source.school || '',
|
||||
religionTags: source.religionTags || [],
|
||||
passportAttachment: source.passportAttachment || '',
|
||||
idCardFrontAttachment: source.idCardFrontAttachment || '',
|
||||
idCardBackAttachment: source.idCardBackAttachment || ''
|
||||
}
|
||||
|
||||
return normalizeUser({
|
||||
id: source.convertedUserCode || source.code || normalizedOptions.id || '',
|
||||
code: source.code || source.convertedUserCode || normalizedOptions.code || '',
|
||||
userCode: source.convertedUserCode || source.userCode || source.code || normalizedOptions.userCode || '',
|
||||
firstChar: (source.name || normalizedOptions.name || 'U').charAt(0).toUpperCase(),
|
||||
name: source.name || normalizedOptions.name || '',
|
||||
account: source.account || normalizedOptions.account || '',
|
||||
username: source.account || normalizedOptions.account || '',
|
||||
password: normalizedOptions.password || '',
|
||||
email: source.email || normalizedOptions.email || '',
|
||||
phone: source.phone || normalizedOptions.phone || '',
|
||||
roleNo: source.roleNo || normalizedOptions.roleNo || '',
|
||||
hasAuth: source.approvalStatus === 1,
|
||||
authStatus: mapRegistrationApprovalToAuthStatus(source.approvalStatus),
|
||||
authRejectReason: source.approveReason || '',
|
||||
authDate: source.createTime ? new Date(source.createTime).toLocaleDateString() : '',
|
||||
authProfile: profile
|
||||
})
|
||||
}
|
||||
|
||||
export function mergeBackendUserInfo(baseUser = {}, backendUserInfo = {}) {
|
||||
const user = baseUser || {}
|
||||
const response = backendUserInfo || {}
|
||||
const raw = response && response.data ? response.data : response
|
||||
const info = raw || {}
|
||||
const mergedUser = {
|
||||
...user,
|
||||
id: info.code || info.userCode || user.id || '',
|
||||
code: info.code || info.userCode || user.code || '',
|
||||
userCode: info.userCode || info.code || user.userCode || '',
|
||||
backendUserInfo: info,
|
||||
name: info.name || user.name || '',
|
||||
account: info.account || user.account || '',
|
||||
username: info.account || user.username || user.account || '',
|
||||
avatar: info.avatar || info.base64Face || user.avatar || '',
|
||||
base64Face: info.base64Face || user.base64Face || '',
|
||||
phone: info.phone || user.phone || '',
|
||||
email: info.email || user.email || '',
|
||||
roleNo: info.roleNo || user.roleNo || '',
|
||||
roles: Array.isArray(info.roles) ? info.roles : (user.roles || []),
|
||||
permissions: Array.isArray(info.permissions) ? info.permissions : (user.permissions || [])
|
||||
}
|
||||
|
||||
if (mergedUser.authProfile) {
|
||||
mergedUser.authProfile = {
|
||||
...mergedUser.authProfile,
|
||||
name: mergedUser.name || mergedUser.authProfile.name || '',
|
||||
account: mergedUser.account || mergedUser.authProfile.account || '',
|
||||
email: mergedUser.email || mergedUser.authProfile.email || '',
|
||||
phone: mergedUser.phone || mergedUser.authProfile.phone || '',
|
||||
roleNo: mergedUser.roleNo || mergedUser.authProfile.roleNo || ''
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeUser(mergedUser)
|
||||
}
|
||||
|
||||
export function getStoredUser() {
|
||||
const storeUser = store && store.state ? store.state.vuex_user : {}
|
||||
if (storeUser && Object.keys(storeUser).length) {
|
||||
return normalizeUser(storeUser)
|
||||
}
|
||||
const currentUser = getCurrentUser()
|
||||
if (currentUser && Object.keys(currentUser).length) {
|
||||
return normalizeUser(currentUser)
|
||||
}
|
||||
const registeredUser = getRegisteredUser()
|
||||
return normalizeUser(registeredUser || {})
|
||||
}
|
||||
|
||||
export function saveUser(user) {
|
||||
const normalizedUser = normalizeUser(user)
|
||||
saveCurrentUser(normalizedUser)
|
||||
saveRegisteredUser(normalizedUser)
|
||||
if (store && store.commit) {
|
||||
store.commit('$tStore', {
|
||||
name: 'vuex_user',
|
||||
value: normalizedUser
|
||||
})
|
||||
}
|
||||
return normalizedUser
|
||||
}
|
||||
|
||||
export function getAuthStatusText(status, language) {
|
||||
const languageMap = resolveLanguageMap(AUTH_STATUS_TEXT, language)
|
||||
const numericMap = {
|
||||
pending: 0,
|
||||
approved: 1,
|
||||
rejected: -1
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(numericMap, status)) {
|
||||
return getApprovalStatusLabel(numericMap[status], languageMap[status] || languageMap.unsubmitted, language)
|
||||
}
|
||||
return languageMap[status] || languageMap.unsubmitted
|
||||
}
|
||||
|
||||
export function getBookingStatusText(status, language) {
|
||||
const languageMap = resolveLanguageMap(BOOKING_STATUS_TEXT, language)
|
||||
return languageMap[status] || languageMap.none
|
||||
}
|
||||
|
||||
export function getBookingTagType(status) {
|
||||
if (status === 'rejected') {
|
||||
return 'rejected'
|
||||
}
|
||||
if (status === 'pending_review') {
|
||||
return 'pending'
|
||||
}
|
||||
return 'approved'
|
||||
}
|
||||
|
||||
export function createBooking(payload = {}) {
|
||||
const source = payload || {}
|
||||
const now = Date.now()
|
||||
const status = source.status || 'pending_review'
|
||||
return {
|
||||
orderNo: `LJYD${now}`,
|
||||
status,
|
||||
statusText: getBookingStatusText(status),
|
||||
statusTextZh: getBookingStatusText(status, 'zh'),
|
||||
statusTextEn: getBookingStatusText(status, 'en'),
|
||||
createdAt: new Date(now).toLocaleString(),
|
||||
butlerName: '',
|
||||
roomAssignStatus: 'pending',
|
||||
rejectReason: '',
|
||||
...source
|
||||
}
|
||||
}
|
||||
|
||||
export function saveBooking(booking) {
|
||||
const normalizedBooking = booking && typeof booking === 'object'
|
||||
? {
|
||||
...booking,
|
||||
statusTextZh: booking.statusTextZh || getBookingStatusText(booking.status || 'pending_review', 'zh'),
|
||||
statusTextEn: booking.statusTextEn || getBookingStatusText(booking.status || 'pending_review', 'en')
|
||||
}
|
||||
: booking
|
||||
const bookingList = getBookingList()
|
||||
const nextList = [normalizedBooking, ...bookingList.filter(item => item.orderNo !== normalizedBooking.orderNo)]
|
||||
saveCurrentBooking(normalizedBooking)
|
||||
saveBookingList(nextList)
|
||||
return nextList
|
||||
}
|
||||
130
utils/i18n.js
Normal file
130
utils/i18n.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import store from '@/store'
|
||||
import globalMessages from '@/locales/messages'
|
||||
|
||||
export const ZH_LOCALE = 'zh-Hans'
|
||||
export const EN_LOCALE = 'en'
|
||||
export const DEFAULT_LOCALE = ZH_LOCALE
|
||||
|
||||
function safeReadStorageLocale() {
|
||||
try {
|
||||
const lifeData = uni.getStorageSync('lifeData') || {}
|
||||
return lifeData.vuex_locale || ''
|
||||
} catch (error) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeLocale(locale) {
|
||||
const normalized = String(locale || '').trim().toLowerCase()
|
||||
if (!normalized) {
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
if (normalized === 'en' || normalized === 'en-us' || normalized === 'en_us') {
|
||||
return EN_LOCALE
|
||||
}
|
||||
if (normalized === 'zh' || normalized === 'zh-cn' || normalized === 'zh_cn' || normalized === 'zh-hans') {
|
||||
return ZH_LOCALE
|
||||
}
|
||||
return normalized.startsWith('en') ? EN_LOCALE : ZH_LOCALE
|
||||
}
|
||||
|
||||
export function getLocaleShort(locale) {
|
||||
return normalizeLocale(locale) === EN_LOCALE ? 'en' : 'zh'
|
||||
}
|
||||
|
||||
export function getCurrentLocale() {
|
||||
const stateLocale = store && store.state ? store.state.vuex_locale : ''
|
||||
return normalizeLocale(stateLocale || safeReadStorageLocale())
|
||||
}
|
||||
|
||||
export function getCurrentLanguage() {
|
||||
return getLocaleShort(getCurrentLocale())
|
||||
}
|
||||
|
||||
export function getNextLocale(locale) {
|
||||
return normalizeLocale(locale) === EN_LOCALE ? ZH_LOCALE : EN_LOCALE
|
||||
}
|
||||
|
||||
export function resolveLanguageHeader(locale) {
|
||||
return getLocaleShort(locale) === 'en' ? 'en-US' : 'zh-CN'
|
||||
}
|
||||
|
||||
export function formatI18nText(template, params = {}) {
|
||||
return String(template || '').replace(/\{(\w+)\}/g, (match, key) => {
|
||||
return Object.prototype.hasOwnProperty.call(params, key) ? params[key] : match
|
||||
})
|
||||
}
|
||||
|
||||
function getMessageText(messages = {}, locale, key) {
|
||||
const shortLocale = getLocaleShort(locale)
|
||||
const fallbackMessages = messages.zh || messages[ZH_LOCALE] || {}
|
||||
const scopedMessages = messages[shortLocale] || messages[normalizeLocale(locale)] || fallbackMessages
|
||||
if (scopedMessages[key] !== undefined) {
|
||||
return scopedMessages[key]
|
||||
}
|
||||
return fallbackMessages[key]
|
||||
}
|
||||
|
||||
export function translateMessages(messages = {}, locale, key, params = {}) {
|
||||
const text = getMessageText(messages, locale, key)
|
||||
return formatI18nText(text !== undefined ? text : key, params)
|
||||
}
|
||||
|
||||
export function getMessageScope(messages = {}, namespace = '') {
|
||||
if (!namespace) {
|
||||
return messages
|
||||
}
|
||||
return String(namespace)
|
||||
.split('.')
|
||||
.filter(Boolean)
|
||||
.reduce((current, segment) => {
|
||||
if (!current || typeof current !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
return current[segment]
|
||||
}, messages)
|
||||
}
|
||||
|
||||
export function getGlobalMessages(namespace = '') {
|
||||
if (!namespace) {
|
||||
return globalMessages
|
||||
}
|
||||
const zhScope = getMessageScope(globalMessages.zh || globalMessages[ZH_LOCALE] || {}, namespace)
|
||||
const enScope = getMessageScope(globalMessages.en || globalMessages[EN_LOCALE] || {}, namespace)
|
||||
|
||||
if (!zhScope && !enScope) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
zh: zhScope || {},
|
||||
en: enScope || {}
|
||||
}
|
||||
}
|
||||
|
||||
export function translateGlobalMessages(namespace, locale, key, params = {}) {
|
||||
const scopedMessages = getGlobalMessages(namespace)
|
||||
if (!scopedMessages || typeof scopedMessages !== 'object') {
|
||||
return key
|
||||
}
|
||||
return translateMessages(scopedMessages, locale, key, params)
|
||||
}
|
||||
|
||||
export function getGlobalMessage(namespace, locale, key, params = {}) {
|
||||
const scopedMessages = getGlobalMessages(namespace)
|
||||
if (!scopedMessages || typeof scopedMessages !== 'object') {
|
||||
return ''
|
||||
}
|
||||
const text = getMessageText(scopedMessages, locale, key)
|
||||
return text === undefined ? '' : formatI18nText(text, params)
|
||||
}
|
||||
|
||||
export function pickLocalizedText(options = {}) {
|
||||
const locale = getLocaleShort(options.locale)
|
||||
const zhText = options.zh !== undefined ? options.zh : ''
|
||||
const enText = options.en !== undefined ? options.en : zhText
|
||||
if (locale === 'en') {
|
||||
return enText || options.fallback || zhText || ''
|
||||
}
|
||||
return zhText || options.fallback || enText || ''
|
||||
}
|
||||
170
utils/login-helper.js
Normal file
170
utils/login-helper.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
buildUserFromRegistrationApplication,
|
||||
normalizeUser
|
||||
} from './flow'
|
||||
|
||||
function getText(language, zhText, enText) {
|
||||
return String(language || 'zh').trim().toLowerCase() === 'en' ? enText : zhText
|
||||
}
|
||||
|
||||
function getResponseData(response = {}) {
|
||||
if (response && response.data && typeof response.data === 'object') {
|
||||
return response.data
|
||||
}
|
||||
return response || {}
|
||||
}
|
||||
|
||||
export function matchRegisteredUser(user = {}, account = '') {
|
||||
const source = user || {}
|
||||
const currentAccount = String(account || '').trim()
|
||||
const candidateAccounts = [
|
||||
source.account,
|
||||
source.username,
|
||||
source.email
|
||||
].map(value => String(value || '').trim()).filter(Boolean)
|
||||
|
||||
if (!currentAccount || !candidateAccounts.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!candidateAccounts.includes(currentAccount)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
export function pickLatestRegistration(response = {}, options = {}) {
|
||||
const source = options || {}
|
||||
const account = String(source.account || '').trim()
|
||||
const responseData = getResponseData(response)
|
||||
const list = Array.isArray(responseData.data)
|
||||
? responseData.data
|
||||
: (Array.isArray(responseData) ? responseData : [])
|
||||
|
||||
const matchedList = list.filter(item => {
|
||||
if (!item) {
|
||||
return false
|
||||
}
|
||||
return item.account === account || item.email === account
|
||||
})
|
||||
const targetList = matchedList.length ? matchedList : list
|
||||
|
||||
return targetList[0] || null
|
||||
}
|
||||
|
||||
export function extractLoginSession(response = {}, language = 'zh') {
|
||||
const responseData = getResponseData(response)
|
||||
const nestedData = responseData && responseData.data && typeof responseData.data === 'object'
|
||||
? responseData.data
|
||||
: {}
|
||||
const token = responseData.accessToken
|
||||
|| responseData.token
|
||||
|| nestedData.accessToken
|
||||
|| nestedData.token
|
||||
|| response.accessToken
|
||||
|| response.token
|
||||
|| ''
|
||||
const expires = responseData.expires
|
||||
|| responseData.expireTime
|
||||
|| nestedData.expires
|
||||
|| nestedData.expireTime
|
||||
|| response.expires
|
||||
|| response.expireTime
|
||||
|| ''
|
||||
|
||||
if (!token) {
|
||||
throw new Error(getText(language, '登录成功但未返回 accessToken', 'Login succeeded but accessToken is missing'))
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
expires
|
||||
}
|
||||
}
|
||||
|
||||
export function buildHandledLoginError(language = 'zh', options = {}) {
|
||||
const source = options || {}
|
||||
return {
|
||||
title: source.title || '',
|
||||
navigateToAuthResult: source.navigateToAuthResult === true
|
||||
}
|
||||
}
|
||||
|
||||
export function buildLoginSessionUser(options = {}) {
|
||||
const source = options || {}
|
||||
const latestRegistration = source.latestRegistration || null
|
||||
const matchedLocalUser = source.matchedLocalUser || null
|
||||
const account = source.account || ''
|
||||
const password = source.password || ''
|
||||
const language = source.language || 'zh'
|
||||
|
||||
if (latestRegistration) {
|
||||
return buildUserFromRegistrationApplication(latestRegistration, {
|
||||
password,
|
||||
language
|
||||
})
|
||||
}
|
||||
|
||||
if (matchedLocalUser) {
|
||||
return matchedLocalUser
|
||||
}
|
||||
|
||||
return normalizeUser({
|
||||
account,
|
||||
username: account,
|
||||
password
|
||||
})
|
||||
}
|
||||
|
||||
export function resolveLoginPreparation(options = {}) {
|
||||
const source = options || {}
|
||||
const language = source.language || 'zh'
|
||||
const matchedLocalUser = source.matchedLocalUser || null
|
||||
const latestRegistration = source.latestRegistration || null
|
||||
const account = source.account || ''
|
||||
const password = source.password || ''
|
||||
|
||||
if (!latestRegistration) {
|
||||
return {
|
||||
ok: true,
|
||||
sessionUser: buildLoginSessionUser({
|
||||
matchedLocalUser,
|
||||
account,
|
||||
password,
|
||||
language
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (latestRegistration.approvalStatus === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
handledError: buildHandledLoginError(language, {
|
||||
title: getText(language, '注册申请审核中,暂不能登录', 'Registration is under review'),
|
||||
navigateToAuthResult: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (latestRegistration.approvalStatus === -1) {
|
||||
return {
|
||||
ok: false,
|
||||
handledError: buildHandledLoginError(language, {
|
||||
title: latestRegistration.approveReason || getText(language, '注册申请已驳回', 'Rejected'),
|
||||
navigateToAuthResult: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
sessionUser: buildLoginSessionUser({
|
||||
latestRegistration,
|
||||
matchedLocalUser,
|
||||
account,
|
||||
password,
|
||||
language
|
||||
})
|
||||
}
|
||||
}
|
||||
49
utils/message-helper.js
Normal file
49
utils/message-helper.js
Normal file
@@ -0,0 +1,49 @@
|
||||
export function resolveNotificationList(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
if (payload && Array.isArray(payload.data)) {
|
||||
return payload.data
|
||||
}
|
||||
if (payload && payload.data && Array.isArray(payload.data.data)) {
|
||||
return payload.data.data
|
||||
}
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function resolveNotificationUnreadTotal(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
return Number((payload && payload.total) || (payload && payload.data && payload.data.total) || 0)
|
||||
}
|
||||
|
||||
export function extractInspectionList(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
if (payload && Array.isArray(payload.data)) {
|
||||
return payload.data
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function isPendingInspectionRecord(record) {
|
||||
return !!record && record.checkedUserAgree !== true && record.checkedUserAgree !== false
|
||||
}
|
||||
|
||||
export function countPendingInspectionRecords(list = []) {
|
||||
return (Array.isArray(list) ? list : []).filter(item => isPendingInspectionRecord(item)).length
|
||||
}
|
||||
|
||||
export function formatMessageDateTime(value) {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value).replace('T', ' ').slice(0, 16)
|
||||
}
|
||||
const pad = item => String(item).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
47
utils/messages.js
Normal file
47
utils/messages.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const MESSAGE_DETAIL_KEY = 'messageDetailPayload'
|
||||
const MESSAGE_LIST_KEY = 'messageListSnapshot'
|
||||
|
||||
function normalizeLookupValue(value = '') {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function getMessageStorageKey(baseKey, language = '') {
|
||||
const normalizedLanguage = String(language || '').trim().toLowerCase() === 'en' ? 'en' : 'zh'
|
||||
return `${baseKey}_${normalizedLanguage}`
|
||||
}
|
||||
|
||||
export function saveMessageDetailPayload(payload = {}, language = '') {
|
||||
uni.setStorageSync(getMessageStorageKey(MESSAGE_DETAIL_KEY, language), payload || {})
|
||||
return payload || {}
|
||||
}
|
||||
|
||||
export function getMessageDetailPayload(language = '') {
|
||||
return uni.getStorageSync(getMessageStorageKey(MESSAGE_DETAIL_KEY, language)) || {}
|
||||
}
|
||||
|
||||
export function saveMessageListSnapshot(list = [], language = '') {
|
||||
const nextList = Array.isArray(list) ? list : []
|
||||
uni.setStorageSync(getMessageStorageKey(MESSAGE_LIST_KEY, language), nextList)
|
||||
return nextList
|
||||
}
|
||||
|
||||
export function getMessageListSnapshot(language = '') {
|
||||
const list = uni.getStorageSync(getMessageStorageKey(MESSAGE_LIST_KEY, language))
|
||||
return Array.isArray(list) ? list : []
|
||||
}
|
||||
|
||||
export function findMessageDetailPayload(id = '', code = '', language = '') {
|
||||
const targetId = normalizeLookupValue(id)
|
||||
const targetCode = normalizeLookupValue(code)
|
||||
const list = getMessageListSnapshot(language)
|
||||
|
||||
return list.find(item => {
|
||||
const currentId = normalizeLookupValue(item && item.id)
|
||||
const currentCode = normalizeLookupValue(item && item.code)
|
||||
return (targetId && currentId === targetId) || (targetCode && currentCode === targetCode)
|
||||
}) || {}
|
||||
}
|
||||
|
||||
export function clearMessageDetailPayload(language = '') {
|
||||
uni.removeStorageSync(getMessageStorageKey(MESSAGE_DETAIL_KEY, language))
|
||||
}
|
||||
16
utils/navigation-title.js
Normal file
16
utils/navigation-title.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getGlobalMessage, normalizeLocale } from './i18n'
|
||||
|
||||
export function getNavigationTitle(route = '', locale) {
|
||||
return getGlobalMessage('navigation.routes', normalizeLocale(locale), String(route || '').trim())
|
||||
}
|
||||
|
||||
export function syncNavigationBarTitle(route = '', locale) {
|
||||
const title = getNavigationTitle(route, locale)
|
||||
if (!title) {
|
||||
return ''
|
||||
}
|
||||
uni.setNavigationBarTitle({
|
||||
title
|
||||
})
|
||||
return title
|
||||
}
|
||||
210
utils/request.js
Normal file
210
utils/request.js
Normal file
@@ -0,0 +1,210 @@
|
||||
import { config } from '../config.js'
|
||||
import { clearLoginSession, getAuthToken } from './app-state'
|
||||
import { getCurrentLanguage } from './i18n'
|
||||
|
||||
const API_BASE_URL = config.baseURL
|
||||
const FILE_BASE_URL = config.filePrefix
|
||||
const FIXED_PROVIDER_TYPE = 'WechatMiniProgram'
|
||||
|
||||
function getRequestText(key = '') {
|
||||
const language = getCurrentLanguage()
|
||||
const isEn = language === 'en'
|
||||
const map = {
|
||||
requestFailed: isEn ? 'Request failed' : '请求失败',
|
||||
loginExpired: isEn ? 'Login expired. Please sign in again' : '登录已过期,请重新登录',
|
||||
tooManyRequests: isEn ? 'Too many requests. Please try again later' : '操作太频繁了,请稍后再试',
|
||||
networkFailed: isEn ? 'Network request failed' : '网络请求失败'
|
||||
}
|
||||
return map[key] || ''
|
||||
}
|
||||
|
||||
function normalizeUrl(url = '') {
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return url
|
||||
}
|
||||
if (!url) {
|
||||
return API_BASE_URL
|
||||
}
|
||||
if (url.startsWith('/')) {
|
||||
return `${API_BASE_URL}${url}`
|
||||
}
|
||||
return `${API_BASE_URL}/${url}`
|
||||
}
|
||||
|
||||
function isPlainRequestObject(value) {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function appendFixedProviderType(payload) {
|
||||
if (isPlainRequestObject(payload)) {
|
||||
return {
|
||||
...payload,
|
||||
ProviderType: FIXED_PROVIDER_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ProviderType: FIXED_PROVIDER_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
function hasBusinessCode(payload) {
|
||||
return !!payload && typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'code')
|
||||
}
|
||||
|
||||
function isBusinessSuccess(payload) {
|
||||
if (!hasBusinessCode(payload)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const code = String(payload.code)
|
||||
return code === '200' || code === '0'
|
||||
}
|
||||
|
||||
function normalizeBusinessError(payload, fallback = {}) {
|
||||
const message = (payload && (payload.msg || payload.message)) || fallback.errMsg || getRequestText('requestFailed')
|
||||
return {
|
||||
...fallback,
|
||||
...(payload && typeof payload === 'object' ? payload : {}),
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentRoute() {
|
||||
const pages = getCurrentPages()
|
||||
if (!Array.isArray(pages) || !pages.length) {
|
||||
return ''
|
||||
}
|
||||
const currentPage = pages[pages.length - 1] || {}
|
||||
return currentPage.route || ''
|
||||
}
|
||||
|
||||
function shouldSkipUnauthorizedRedirect() {
|
||||
const route = getCurrentRoute()
|
||||
return route === 'pkgAccount/login/login' || route === 'pages/login/index' || route === 'pages/login/login'
|
||||
}
|
||||
|
||||
function handleUnauthorized(responseData) {
|
||||
clearLoginSession()
|
||||
|
||||
if (shouldSkipUnauthorizedRedirect()) {
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: getRequestText('loginExpired'),
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pkgAccount/login/login'
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
export function request(options = {}) {
|
||||
const {
|
||||
url = '',
|
||||
method = 'GET',
|
||||
data,
|
||||
params,
|
||||
header = {},
|
||||
hideErrorToast = false,
|
||||
withAuth = true
|
||||
} = options
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = withAuth ? getAuthToken() : ''
|
||||
const uppercaseMethod = String(method || 'GET').toUpperCase()
|
||||
const normalizedParams = appendFixedProviderType(params)
|
||||
const normalizedData = appendFixedProviderType(data)
|
||||
uni.request({
|
||||
url: normalizeUrl(url),
|
||||
method: uppercaseMethod,
|
||||
data: uppercaseMethod === 'GET'
|
||||
? (params !== undefined ? normalizedParams : normalizedData)
|
||||
: normalizedData,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-Language': getCurrentLanguage(),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...header
|
||||
},
|
||||
success: (res) => {
|
||||
const { statusCode, data: responseData } = res
|
||||
if ((statusCode === 401) || (responseData && Number(responseData.code) === 401)) {
|
||||
handleUnauthorized(responseData)
|
||||
reject(normalizeBusinessError(responseData, res))
|
||||
return
|
||||
}
|
||||
|
||||
if (statusCode === 429) {
|
||||
if (!hideErrorToast) {
|
||||
uni.showToast({
|
||||
title: getRequestText('tooManyRequests'),
|
||||
icon: 'none',
|
||||
duration: 2500
|
||||
})
|
||||
}
|
||||
reject({
|
||||
...normalizeBusinessError(responseData, res),
|
||||
message: getRequestText('tooManyRequests'),
|
||||
__toastShown: !hideErrorToast
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
if (isBusinessSuccess(responseData)) {
|
||||
resolve(responseData)
|
||||
return
|
||||
}
|
||||
|
||||
const businessError = normalizeBusinessError(responseData, res)
|
||||
if (!hideErrorToast) {
|
||||
uni.showToast({
|
||||
title: businessError.message || getRequestText('requestFailed'),
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
reject({
|
||||
...businessError,
|
||||
__toastShown: !hideErrorToast
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const requestError = normalizeBusinessError(responseData, res)
|
||||
if (!hideErrorToast) {
|
||||
uni.showToast({
|
||||
title: requestError.message || getRequestText('requestFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
reject({
|
||||
...requestError,
|
||||
__toastShown: !hideErrorToast
|
||||
})
|
||||
},
|
||||
fail: (error) => {
|
||||
const requestError = normalizeBusinessError(null, error)
|
||||
if (!hideErrorToast) {
|
||||
uni.showToast({
|
||||
title: requestError.message || getRequestText('networkFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
reject({
|
||||
...requestError,
|
||||
__toastShown: !hideErrorToast
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export { API_BASE_URL, FILE_BASE_URL }
|
||||
export default request
|
||||
20
utils/rich-text.js
Normal file
20
utils/rich-text.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { addFilePrefix } from './common'
|
||||
|
||||
export function normalizeRichTextImages(content = '', imageStyle = '') {
|
||||
const normalizedStyle = String(imageStyle || '').trim()
|
||||
|
||||
return String(content || '').replace(/<img\b([^>]*)>/gi, (match, rawAttrs = '') => {
|
||||
const attrs = String(rawAttrs || '')
|
||||
.replace(/\swidth\s*=\s*(['"]).*?\1/gi, '')
|
||||
.replace(/\sheight\s*=\s*(['"]).*?\1/gi, '')
|
||||
.replace(/\sstyle\s*=\s*(['"]).*?\1/gi, '')
|
||||
const normalizedAttrs = attrs.replace(/\ssrc\s*=\s*(['"])(.*?)\1/gi, (srcMatch, quote, srcValue = '') => {
|
||||
const normalizedSrc = addFilePrefix(srcValue)
|
||||
return normalizedSrc ? ` src=${quote}${normalizedSrc}${quote}` : srcMatch
|
||||
})
|
||||
|
||||
return normalizedStyle
|
||||
? `<img${normalizedAttrs} style="${normalizedStyle}" />`
|
||||
: `<img${normalizedAttrs} />`
|
||||
})
|
||||
}
|
||||
7
utils/room-api.js
Normal file
7
utils/room-api.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
areaPagination,
|
||||
roomDetail,
|
||||
roomTypeDetail,
|
||||
roomPagination,
|
||||
roomtypeList
|
||||
} from '../api/index'
|
||||
406
utils/select-room-normalizers.js
Normal file
406
utils/select-room-normalizers.js
Normal file
@@ -0,0 +1,406 @@
|
||||
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 || '',
|
||||
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
|
||||
}
|
||||
}
|
||||
346
utils/session-sync.js
Normal file
346
utils/session-sync.js
Normal file
@@ -0,0 +1,346 @@
|
||||
import {
|
||||
clearBookingState,
|
||||
clearMyRoomData,
|
||||
getCurrentUser,
|
||||
getLoginStatus,
|
||||
saveBookingList,
|
||||
saveCurrentBooking,
|
||||
saveEffectiveContracts,
|
||||
saveCurrentUser,
|
||||
saveMyRoomData,
|
||||
savePendingContractCount,
|
||||
savePendingContracts,
|
||||
saveRegisteredUser,
|
||||
setCheckedInStatus
|
||||
} from './app-state'
|
||||
import {
|
||||
getMyEffectiveContracts,
|
||||
getMyPendingSignContracts,
|
||||
orderSearchedMePage
|
||||
} from '@/api/index.js'
|
||||
import store from '@/store'
|
||||
import {
|
||||
normalizeBookingList,
|
||||
parseOrderListResponse,
|
||||
pickCurrentBooking,
|
||||
pickCurrentStayOrder
|
||||
} from './booking-helper'
|
||||
import { getCurrentLanguage } from './i18n'
|
||||
|
||||
const SYNC_TTL = 3000
|
||||
|
||||
let syncStayStatePromise = null
|
||||
let lastSyncStayStateAt = 0
|
||||
|
||||
function getSessionSyncText(key = '') {
|
||||
const isEn = getCurrentLanguage() === 'en'
|
||||
const map = {
|
||||
pendingContract: isEn ? 'Pending Contract Signature' : '待签合同'
|
||||
}
|
||||
|
||||
return map[key] || ''
|
||||
}
|
||||
|
||||
function parseContractList(response) {
|
||||
const payload = response && response.data ? 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 buildLeaseText(order = {}) {
|
||||
const start = String(order.startDate || order.start || order.checkinDate || '').slice(0, 10)
|
||||
const end = String(order.endDate || order.end || order.checkoutDate || '').slice(0, 10)
|
||||
|
||||
if (start && end) {
|
||||
return `${start} - ${end}`
|
||||
}
|
||||
|
||||
return start || end || ''
|
||||
}
|
||||
|
||||
function mapOrderToRoomData(order = {}) {
|
||||
const imageUrls = Array.isArray(order.imageUrls)
|
||||
? order.imageUrls
|
||||
: typeof order.imageUrls === 'string'
|
||||
? order.imageUrls.split(',').map(item => item.trim()).filter(Boolean)
|
||||
: []
|
||||
|
||||
const roomLabel = [
|
||||
order.buildingName || order.buildingSectionName || order.unitName || '',
|
||||
order.roomNo || order.roomCode || order.roomName || ''
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
const title = [
|
||||
order.areaName || order.campusName || '',
|
||||
order.roomTypeName || order.roomType || ''
|
||||
].filter(Boolean).join(' · ')
|
||||
|
||||
return {
|
||||
...order,
|
||||
id: roomLabel || order.roomNo || order.roomCode || order.orderNo || '',
|
||||
roomLabel: roomLabel || order.roomNo || order.roomCode || '',
|
||||
titleZh: title || order.roomTypeName || order.roomName || '当前入住房间',
|
||||
titleEn: order.roomTypeNameEn || title || order.roomTypeName || 'Current Stay',
|
||||
leaseText: buildLeaseText(order),
|
||||
image: order.imageUrl || imageUrls[0] || ''
|
||||
}
|
||||
}
|
||||
|
||||
function pickFirstValue(...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 buildPendingContractQuery() {
|
||||
const user = getCurrentUser() || {}
|
||||
const userCode = pickFirstValue(user.userCode, user.code, user.UserCode)
|
||||
const query = {
|
||||
ProviderType: 'WechatMiniProgram'
|
||||
}
|
||||
|
||||
if (userCode) {
|
||||
query.UserCode = userCode
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
function enrichBookingLanguageFields(booking = {}) {
|
||||
if (!booking || typeof booking !== 'object') {
|
||||
return booking
|
||||
}
|
||||
return {
|
||||
...booking,
|
||||
statusTextZh: booking.statusTextZh || '待签合同',
|
||||
statusTextEn: booking.statusTextEn || 'Pending Contract Signature'
|
||||
}
|
||||
}
|
||||
|
||||
function buildContractStepBooking(pendingContracts = []) {
|
||||
const contract = Array.isArray(pendingContracts) ? pendingContracts[0] : null
|
||||
if (!contract || typeof contract !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...contract,
|
||||
orderNo: contract.orderNo || contract.OrderNo || '',
|
||||
roomName: contract.roomName || contract.roomTypeName || contract.houseTypeName || '',
|
||||
roomTypeName: contract.roomTypeName || contract.houseTypeName || contract.roomName || '',
|
||||
startDate: contract.start || contract.startDate || '',
|
||||
endDate: contract.end || contract.endDate || '',
|
||||
status: 'contract_pending_sign',
|
||||
statusKey: 'contract_pending_sign',
|
||||
statusCode: 10,
|
||||
statusText: getSessionSyncText('pendingContract'),
|
||||
statusTextZh: '待签合同',
|
||||
statusTextEn: 'Pending Contract Signature'
|
||||
}
|
||||
}
|
||||
|
||||
function syncStoredUser(hasCheckedIn, currentOrder, currentBooking) {
|
||||
const user = getCurrentUser()
|
||||
|
||||
if (!user || typeof user !== 'object' || !Object.keys(user).length) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextUser = {
|
||||
...user,
|
||||
hasCheckedIn,
|
||||
currentOrder: currentOrder || null,
|
||||
currentBooking: currentBooking || null,
|
||||
bookingStatus: currentBooking ? (currentBooking.status || currentBooking.statusKey || 'none') : 'none',
|
||||
bookingRejectReason: currentBooking ? (currentBooking.rejectReasonText || currentBooking.rejectReason || '') : ''
|
||||
}
|
||||
|
||||
saveCurrentUser(nextUser)
|
||||
saveRegisteredUser(nextUser)
|
||||
|
||||
if (store && store.commit) {
|
||||
store.commit('$tStore', {
|
||||
name: 'vuex_user',
|
||||
value: {
|
||||
...(store.state && store.state.vuex_user ? store.state.vuex_user : {}),
|
||||
...nextUser
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function syncStoreState({
|
||||
hasCheckedIn = false,
|
||||
currentOrder = null,
|
||||
pendingContracts = [],
|
||||
effectiveContracts = [],
|
||||
orderList = []
|
||||
} = {}) {
|
||||
if (!store || !store.commit) {
|
||||
return
|
||||
}
|
||||
|
||||
store.commit('$tStore', {
|
||||
name: 'vuex_checkin_status',
|
||||
value: !!hasCheckedIn
|
||||
})
|
||||
store.commit('$tStore', {
|
||||
name: 'vuex_current_order',
|
||||
value: currentOrder || null
|
||||
})
|
||||
store.commit('$tStore', {
|
||||
name: 'vuex_pending_contracts',
|
||||
value: Array.isArray(pendingContracts) ? pendingContracts : []
|
||||
})
|
||||
store.commit('$tStore', {
|
||||
name: 'vuex_pending_contract_count',
|
||||
value: Array.isArray(pendingContracts) ? pendingContracts.length : 0
|
||||
})
|
||||
store.commit('$tStore', {
|
||||
name: 'vuex_effective_contracts',
|
||||
value: Array.isArray(effectiveContracts) ? effectiveContracts : []
|
||||
})
|
||||
store.commit('$tStore', {
|
||||
name: 'vuex_valid_orders',
|
||||
value: Array.isArray(orderList) ? orderList : []
|
||||
})
|
||||
}
|
||||
|
||||
function requestStayState(language = 'zh') {
|
||||
return orderSearchedMePage({
|
||||
PageIndex: 1,
|
||||
PageSize: 100,
|
||||
IsExport: false
|
||||
}, language)
|
||||
.then((orderResponse) => {
|
||||
return Promise.all([
|
||||
Promise.resolve(orderResponse),
|
||||
getMyPendingSignContracts(buildPendingContractQuery(), language).catch(() => []),
|
||||
getMyEffectiveContracts({}, language).catch(() => [])
|
||||
])
|
||||
})
|
||||
.then(([orderResponse, pendingContractsResponse, effectiveContractsResponse]) => {
|
||||
const orderList = normalizeBookingList(parseOrderListResponse(orderResponse), language)
|
||||
const currentOrder = pickCurrentStayOrder(orderList)
|
||||
const pendingContracts = parseContractList(pendingContractsResponse)
|
||||
const effectiveContracts = parseContractList(effectiveContractsResponse)
|
||||
const currentBooking = enrichBookingLanguageFields(pickCurrentBooking(orderList) || buildContractStepBooking(pendingContracts))
|
||||
|
||||
return {
|
||||
hasCheckedIn: !!currentOrder,
|
||||
currentOrder,
|
||||
currentBooking,
|
||||
orderList,
|
||||
pendingContracts,
|
||||
effectiveContracts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function syncLoginStayState(options = {}) {
|
||||
const { language = 'zh', force = false } = options
|
||||
|
||||
if (!getLoginStatus()) {
|
||||
return Promise.resolve({
|
||||
hasCheckedIn: false,
|
||||
currentOrder: null,
|
||||
currentBooking: null,
|
||||
orderList: []
|
||||
})
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
if (!force && syncStayStatePromise) {
|
||||
return syncStayStatePromise
|
||||
}
|
||||
|
||||
if (!force && lastSyncStayStateAt && now - lastSyncStayStateAt < SYNC_TTL) {
|
||||
return Promise.resolve({
|
||||
skipped: true
|
||||
})
|
||||
}
|
||||
|
||||
const syncSource = store && store.dispatch
|
||||
? store.dispatch('fetchUserGlobalStatus', {
|
||||
force,
|
||||
language
|
||||
})
|
||||
: requestStayState(language)
|
||||
|
||||
syncStayStatePromise = syncSource
|
||||
.then((result = {}) => {
|
||||
const orderList = Array.isArray(result.orderList)
|
||||
? result.orderList
|
||||
: (Array.isArray(result.validOrders) ? result.validOrders : [])
|
||||
const pendingContracts = Array.isArray(result.pendingContracts) ? result.pendingContracts : []
|
||||
const effectiveContracts = Array.isArray(result.effectiveContracts) ? result.effectiveContracts : []
|
||||
const currentOrder = result.currentOrder || pickCurrentStayOrder(orderList)
|
||||
const currentBooking = enrichBookingLanguageFields(result.currentBooking || pickCurrentBooking(orderList) || buildContractStepBooking(pendingContracts))
|
||||
const hasCheckedIn = typeof result.hasCheckedIn === 'boolean'
|
||||
? result.hasCheckedIn
|
||||
: !!currentOrder
|
||||
|
||||
saveBookingList(orderList)
|
||||
if (currentBooking) {
|
||||
saveCurrentBooking(currentBooking)
|
||||
} else {
|
||||
clearBookingState()
|
||||
saveBookingList(orderList)
|
||||
}
|
||||
setCheckedInStatus(hasCheckedIn)
|
||||
savePendingContracts(pendingContracts)
|
||||
savePendingContractCount(pendingContracts.length)
|
||||
saveEffectiveContracts(effectiveContracts)
|
||||
|
||||
if (hasCheckedIn) {
|
||||
saveMyRoomData(mapOrderToRoomData(currentOrder))
|
||||
} else {
|
||||
clearMyRoomData()
|
||||
}
|
||||
|
||||
syncStoreState({
|
||||
hasCheckedIn,
|
||||
currentOrder,
|
||||
pendingContracts,
|
||||
effectiveContracts,
|
||||
orderList
|
||||
})
|
||||
syncStoredUser(hasCheckedIn, currentOrder, currentBooking)
|
||||
lastSyncStayStateAt = Date.now()
|
||||
|
||||
return {
|
||||
hasCheckedIn,
|
||||
currentOrder,
|
||||
currentBooking,
|
||||
orderList,
|
||||
pendingContracts,
|
||||
effectiveContracts
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
syncStayStatePromise = null
|
||||
})
|
||||
|
||||
return syncStayStatePromise
|
||||
}
|
||||
809
utils/smart-device.js
Normal file
809
utils/smart-device.js
Normal file
@@ -0,0 +1,809 @@
|
||||
import { getCurrentLanguage } from './i18n'
|
||||
|
||||
function safeJsonParse(value, fallback = null) {
|
||||
if (typeof value !== 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
const text = value.trim()
|
||||
if (!text) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const looksLikeJson = (
|
||||
(text.startsWith('{') && text.endsWith('}'))
|
||||
|| (text.startsWith('[') && text.endsWith(']'))
|
||||
)
|
||||
|
||||
if (!looksLikeJson) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (error) {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
function pickFirstFilledValue(...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 getSmartDeviceText(key = '', params = {}) {
|
||||
const isEn = getCurrentLanguage() === 'en'
|
||||
const map = {
|
||||
commonSwitch: isEn ? 'General Switch' : '通用开关',
|
||||
slideControl: isEn ? 'Slider Control' : '滑动控制',
|
||||
curtainButton: isEn ? 'Curtain Button' : '窗帘按钮',
|
||||
unknownFunction: isEn ? 'Unknown Function' : '未知功能',
|
||||
fullyOpen: isEn ? 'Fully Open' : '已全开',
|
||||
fullyClosed: isEn ? 'Fully Closed' : '已全关',
|
||||
paused: isEn ? 'Paused' : '已暂停',
|
||||
openness: isEn ? `Open Level ${params.value}%` : `开合度 ${params.value}%`,
|
||||
controllable: isEn ? 'Ready to Control' : '可控制',
|
||||
currentValue: isEn ? `Current Value ${params.value}%` : `当前值 ${params.value}%`,
|
||||
adjustable: isEn ? 'Adjustable' : '可调节',
|
||||
enabled: isEn ? 'Enabled' : '已开启',
|
||||
disabled: isEn ? 'Disabled' : '已关闭',
|
||||
unnamedDevice: isEn ? 'Unnamed Device' : '未命名设备'
|
||||
}
|
||||
|
||||
return map[key] || ''
|
||||
}
|
||||
|
||||
function collectObjectArray(value, depth = 0) {
|
||||
if (!value || depth > 6) {
|
||||
return []
|
||||
}
|
||||
|
||||
const normalizedValue = typeof value === 'string'
|
||||
? (safeJsonParse(value, value))
|
||||
: value
|
||||
|
||||
if (Array.isArray(normalizedValue)) {
|
||||
if (normalizedValue.some(item => item && typeof item === 'object' && !Array.isArray(item))) {
|
||||
return normalizedValue
|
||||
}
|
||||
|
||||
for (let index = 0; index < normalizedValue.length; index += 1) {
|
||||
const nestedList = collectObjectArray(normalizedValue[index], depth + 1)
|
||||
if (nestedList.length) {
|
||||
return nestedList
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
if (!normalizedValue || typeof normalizedValue !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
const candidateKeys = ['data', 'list', 'rows', 'items', 'records', 'result', 'payload', '$values']
|
||||
for (let index = 0; index < candidateKeys.length; index += 1) {
|
||||
const key = candidateKeys[index]
|
||||
if (!Object.prototype.hasOwnProperty.call(normalizedValue, key)) {
|
||||
continue
|
||||
}
|
||||
const nestedList = collectObjectArray(normalizedValue[key], depth + 1)
|
||||
if (nestedList.length) {
|
||||
return nestedList
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function isSmartDeviceObject(value = {}) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (Array.isArray(value.functions)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const deviceKeys = [
|
||||
'deviceId',
|
||||
'deviceNo',
|
||||
'deviceName',
|
||||
'platformDeviceId',
|
||||
'deviceType',
|
||||
'facilityCode',
|
||||
'facilityType',
|
||||
'currentStatus'
|
||||
]
|
||||
|
||||
return deviceKeys.some(key => Object.prototype.hasOwnProperty.call(value, key))
|
||||
}
|
||||
|
||||
function unwrapSmartDevicePayload(value, depth = 0) {
|
||||
if (!value || depth > 6) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const normalizedValue = typeof value === 'string'
|
||||
? (safeJsonParse(value, value))
|
||||
: value
|
||||
|
||||
if (!normalizedValue || typeof normalizedValue !== 'object') {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (Array.isArray(normalizedValue)) {
|
||||
for (let index = 0; index < normalizedValue.length; index += 1) {
|
||||
const nestedDevice = unwrapSmartDevicePayload(normalizedValue[index], depth + 1)
|
||||
if (Object.keys(nestedDevice).length) {
|
||||
return nestedDevice
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
if (isSmartDeviceObject(normalizedValue)) {
|
||||
return normalizedValue
|
||||
}
|
||||
|
||||
const candidateKeys = ['data', 'result', 'payload', 'item', 'device', 'record']
|
||||
for (let index = 0; index < candidateKeys.length; index += 1) {
|
||||
const key = candidateKeys[index]
|
||||
if (!Object.prototype.hasOwnProperty.call(normalizedValue, key)) {
|
||||
continue
|
||||
}
|
||||
const nestedDevice = unwrapSmartDevicePayload(normalizedValue[key], depth + 1)
|
||||
if (Object.keys(nestedDevice).length) {
|
||||
return nestedDevice
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
function isCurtainDeviceLike(device = {}) {
|
||||
const numericDeviceType = Number(device.deviceType)
|
||||
if (numericDeviceType === 2) {
|
||||
return true
|
||||
}
|
||||
|
||||
const name = pickFirstFilledValue(device.deviceName, device.deviceNo).toLowerCase()
|
||||
return name.includes('窗帘') || name.includes('curtain')
|
||||
}
|
||||
|
||||
function normalizeFunctionCode(functionCode = '', device = {}) {
|
||||
const normalizedCode = String(functionCode || '').trim().toLowerCase()
|
||||
if (!normalizedCode) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (['light', 'switch', 'common:switch'].includes(normalizedCode)) {
|
||||
return 'common:switch'
|
||||
}
|
||||
if (['slide', 'common:slide'].includes(normalizedCode)) {
|
||||
return 'common:slide'
|
||||
}
|
||||
if (['curtain', 'curtain:button'].includes(normalizedCode)) {
|
||||
return 'curtain:button'
|
||||
}
|
||||
if (normalizedCode === 'button') {
|
||||
return isCurtainDeviceLike(device) ? 'curtain:button' : 'button'
|
||||
}
|
||||
|
||||
return normalizedCode
|
||||
}
|
||||
|
||||
function normalizeFunctionItem(item = {}, device = {}) {
|
||||
const schema = item.commandSchema === undefined
|
||||
? null
|
||||
: (typeof item.commandSchema === 'string'
|
||||
? (safeJsonParse(item.commandSchema, item.commandSchema))
|
||||
: item.commandSchema)
|
||||
const rawFunctionCode = pickFirstFilledValue(item.functionCode, item.controlType)
|
||||
const normalizedCode = normalizeFunctionCode(rawFunctionCode, device)
|
||||
|
||||
return {
|
||||
...item,
|
||||
functionCode: pickFirstFilledValue(item.functionCode, item.controlType),
|
||||
functionDes: pickFirstFilledValue(item.functionDes, item.description),
|
||||
controlType: normalizedCode,
|
||||
commandSchema: schema
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSchemaOption(option, index = 0) {
|
||||
if (option && typeof option === 'object' && !Array.isArray(option)) {
|
||||
const value = Object.prototype.hasOwnProperty.call(option, 'value')
|
||||
? option.value
|
||||
: (
|
||||
Object.prototype.hasOwnProperty.call(option, 'code')
|
||||
? option.code
|
||||
: (Object.prototype.hasOwnProperty.call(option, 'key') ? option.key : index)
|
||||
)
|
||||
return {
|
||||
label: pickFirstFilledValue(option.label, option.text, option.name, option.title, value),
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: String(option),
|
||||
value: option
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSchemaOptions(rawOptions) {
|
||||
if (!Array.isArray(rawOptions)) {
|
||||
return []
|
||||
}
|
||||
return rawOptions.map((item, index) => normalizeSchemaOption(item, index))
|
||||
}
|
||||
|
||||
function isSchemaMetaKey(key = '') {
|
||||
return [
|
||||
'type',
|
||||
'title',
|
||||
'description',
|
||||
'required',
|
||||
'$schema',
|
||||
'properties',
|
||||
'fields',
|
||||
'params',
|
||||
'definitions'
|
||||
].includes(String(key || '').trim())
|
||||
}
|
||||
|
||||
function inferSchemaFieldType(fieldName = '', rawField) {
|
||||
if (Array.isArray(rawField)) {
|
||||
return 'enum'
|
||||
}
|
||||
|
||||
if (typeof rawField === 'string') {
|
||||
const normalizedType = rawField.trim().toLowerCase()
|
||||
if (['boolean', 'bool'].includes(normalizedType)) {
|
||||
return 'boolean'
|
||||
}
|
||||
if (['number', 'integer', 'int', 'float', 'double'].includes(normalizedType)) {
|
||||
return 'number'
|
||||
}
|
||||
}
|
||||
|
||||
if (!rawField || typeof rawField !== 'object') {
|
||||
return typeof rawField === 'boolean' ? 'boolean' : (typeof rawField === 'number' ? 'number' : 'string')
|
||||
}
|
||||
|
||||
const explicitType = String(
|
||||
rawField.type
|
||||
|| rawField.valueType
|
||||
|| rawField.inputType
|
||||
|| rawField.dataType
|
||||
|| ''
|
||||
).trim().toLowerCase()
|
||||
|
||||
if (rawField.enum || rawField.options || rawField.oneOf || rawField.values) {
|
||||
return 'enum'
|
||||
}
|
||||
if (['boolean', 'bool'].includes(explicitType)) {
|
||||
return 'boolean'
|
||||
}
|
||||
if (['number', 'integer', 'int', 'float', 'double'].includes(explicitType)) {
|
||||
return 'number'
|
||||
}
|
||||
if (['string', 'text'].includes(explicitType)) {
|
||||
return 'string'
|
||||
}
|
||||
if (typeof rawField.default === 'boolean') {
|
||||
return 'boolean'
|
||||
}
|
||||
if (typeof rawField.default === 'number') {
|
||||
return 'number'
|
||||
}
|
||||
if (fieldName === 'value' && typeof rawField === 'object' && !Array.isArray(rawField)) {
|
||||
return 'number'
|
||||
}
|
||||
return 'string'
|
||||
}
|
||||
|
||||
function normalizeSchemaField(fieldName = '', rawField = {}) {
|
||||
const normalizedType = inferSchemaFieldType(fieldName, rawField)
|
||||
const fieldObject = rawField && typeof rawField === 'object' && !Array.isArray(rawField)
|
||||
? rawField
|
||||
: {}
|
||||
const optionCandidates = fieldObject.enum || fieldObject.options || fieldObject.oneOf || fieldObject.values || rawField
|
||||
|
||||
return {
|
||||
name: String(fieldName || '').trim(),
|
||||
label: pickFirstFilledValue(
|
||||
fieldObject.label,
|
||||
fieldObject.title,
|
||||
fieldObject.name,
|
||||
fieldObject.description,
|
||||
fieldName
|
||||
),
|
||||
type: normalizedType,
|
||||
required: !!fieldObject.required,
|
||||
min: Number.isFinite(Number(fieldObject.min))
|
||||
? Number(fieldObject.min)
|
||||
: (Number.isFinite(Number(fieldObject.minimum)) ? Number(fieldObject.minimum) : undefined),
|
||||
max: Number.isFinite(Number(fieldObject.max))
|
||||
? Number(fieldObject.max)
|
||||
: (Number.isFinite(Number(fieldObject.maximum)) ? Number(fieldObject.maximum) : undefined),
|
||||
step: Number.isFinite(Number(fieldObject.step))
|
||||
? Number(fieldObject.step)
|
||||
: (Number.isFinite(Number(fieldObject.multipleOf)) ? Number(fieldObject.multipleOf) : 1),
|
||||
placeholder: pickFirstFilledValue(fieldObject.placeholder, fieldObject.hint, fieldObject.description),
|
||||
defaultValue: Object.prototype.hasOwnProperty.call(fieldObject, 'default') ? fieldObject.default : undefined,
|
||||
options: normalizeSchemaOptions(optionCandidates)
|
||||
}
|
||||
}
|
||||
|
||||
function parseCurrentStatus(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return value
|
||||
}
|
||||
|
||||
const parsedJson = safeJsonParse(value, null)
|
||||
if (parsedJson !== null) {
|
||||
return parsedJson
|
||||
}
|
||||
|
||||
const text = String(value).trim()
|
||||
if (!text) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (text === 'true' || text === 'false') {
|
||||
return {
|
||||
value: text === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
const numericValue = Number(text)
|
||||
if (Number.isFinite(numericValue) && String(numericValue) === text) {
|
||||
return {
|
||||
value: numericValue
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFunctionCurrentStatus(device = {}, preferredControlType = '') {
|
||||
const functionList = Array.isArray(device.functions) ? device.functions : []
|
||||
if (!functionList.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const preferredCategory = getControlTypeCategory(preferredControlType)
|
||||
const preferredFunction = functionList.find((item) => {
|
||||
const itemControlType = pickFirstFilledValue(item && item.controlType, item && item.functionCode)
|
||||
return getControlTypeCategory(itemControlType) === preferredCategory && parseCurrentStatus(item && item.currentStatus)
|
||||
})
|
||||
if (preferredFunction) {
|
||||
return preferredFunction.currentStatus
|
||||
}
|
||||
|
||||
const fallbackFunction = functionList.find(item => parseCurrentStatus(item && item.currentStatus))
|
||||
return fallbackFunction ? fallbackFunction.currentStatus : null
|
||||
}
|
||||
|
||||
function resolveDeviceCurrentStatus(device = {}, preferredControlType = '') {
|
||||
const deviceStatus = parseCurrentStatus(device.currentStatus)
|
||||
if (deviceStatus) {
|
||||
return device.currentStatus
|
||||
}
|
||||
return resolveFunctionCurrentStatus(device, preferredControlType)
|
||||
}
|
||||
|
||||
const CONTROL_TYPE_CATEGORY_MAP = {
|
||||
light: 'switch',
|
||||
switch: 'switch',
|
||||
'common:switch': 'switch',
|
||||
slide: 'slide',
|
||||
'common:slide': 'slide',
|
||||
curtain: 'curtain',
|
||||
'curtain:button': 'curtain'
|
||||
}
|
||||
|
||||
export const SUPPORTED_DEVICE_CONTROL_TYPES = ['common:switch', 'common:slide', 'curtain:button', 'light', 'switch', 'curtain']
|
||||
|
||||
export function getControlTypeCategory(controlType = '') {
|
||||
const normalizedType = String(controlType || '').trim().toLowerCase()
|
||||
return CONTROL_TYPE_CATEGORY_MAP[normalizedType] || normalizedType
|
||||
}
|
||||
|
||||
export function isSupportedDeviceControlType(controlType = '') {
|
||||
const normalizedType = String(controlType || '').trim().toLowerCase()
|
||||
return SUPPORTED_DEVICE_CONTROL_TYPES.includes(normalizedType) || ['switch', 'slide', 'curtain'].includes(getControlTypeCategory(normalizedType))
|
||||
}
|
||||
|
||||
export function getDeviceControlLabel(controlType = '') {
|
||||
const category = getControlTypeCategory(controlType)
|
||||
if (category === 'switch') {
|
||||
return getSmartDeviceText('commonSwitch')
|
||||
}
|
||||
if (category === 'slide') {
|
||||
return getSmartDeviceText('slideControl')
|
||||
}
|
||||
if (category === 'curtain') {
|
||||
return getSmartDeviceText('curtainButton')
|
||||
}
|
||||
return String(controlType || '').trim() || getSmartDeviceText('unknownFunction')
|
||||
}
|
||||
|
||||
export function inferDeviceControlType(device = {}) {
|
||||
const numericDeviceType = Number(device.deviceType)
|
||||
if (numericDeviceType === 2) {
|
||||
return 'curtain:button'
|
||||
}
|
||||
|
||||
const functionList = Array.isArray(device.functions) ? device.functions : []
|
||||
const normalizedFunctionTypes = functionList
|
||||
.map(item => normalizeFunctionCode(
|
||||
pickFirstFilledValue(item && item.functionCode, item && item.controlType),
|
||||
device
|
||||
))
|
||||
.filter(Boolean)
|
||||
|
||||
if (normalizedFunctionTypes.some(item => getControlTypeCategory(item) === 'switch')) {
|
||||
return 'common:switch'
|
||||
}
|
||||
if (normalizedFunctionTypes.some(item => getControlTypeCategory(item) === 'slide')) {
|
||||
return 'common:slide'
|
||||
}
|
||||
if (normalizedFunctionTypes.some(item => getControlTypeCategory(item) === 'curtain')) {
|
||||
return 'curtain:button'
|
||||
}
|
||||
|
||||
if (numericDeviceType === 1 || numericDeviceType === 0) {
|
||||
return 'common:switch'
|
||||
}
|
||||
|
||||
const name = pickFirstFilledValue(device.deviceName, device.deviceNo).toLowerCase()
|
||||
if (name.includes('窗帘') || name.includes('curtain')) {
|
||||
return 'curtain:button'
|
||||
}
|
||||
if (name.includes('开关') || name.includes('switch')) {
|
||||
return 'common:switch'
|
||||
}
|
||||
if (name.includes('亮度') || name.includes('滑块') || name.includes('slider') || name.includes('slide')) {
|
||||
return 'common:slide'
|
||||
}
|
||||
|
||||
return 'common:switch'
|
||||
}
|
||||
|
||||
export function getDeviceIconClass(controlType = '') {
|
||||
const category = getControlTypeCategory(controlType)
|
||||
if (category === 'curtain') {
|
||||
return 'tn-icon-hardware-fill'
|
||||
}
|
||||
if (category === 'slide') {
|
||||
return 'tn-icon-more-horizontal'
|
||||
}
|
||||
if (category === 'switch') {
|
||||
return 'tn-icon-power'
|
||||
}
|
||||
return 'tn-icon-light-fill'
|
||||
}
|
||||
|
||||
export function buildDeviceStateText(device = {}) {
|
||||
const controlType = inferDeviceControlType(device)
|
||||
const category = getControlTypeCategory(controlType)
|
||||
const status = parseCurrentStatus(resolveDeviceCurrentStatus(device, controlType))
|
||||
|
||||
if (category === 'curtain' || Number(device.deviceType) === 2) {
|
||||
const action = pickFirstFilledValue(
|
||||
status && status.command,
|
||||
status && status.action,
|
||||
status && status.state,
|
||||
status && status.text
|
||||
).toLowerCase()
|
||||
const percentValue = Number(
|
||||
status && (
|
||||
status.value !== undefined
|
||||
? status.value
|
||||
: (status.percent !== undefined ? status.percent : status.position)
|
||||
)
|
||||
)
|
||||
|
||||
if (action === 'open') {
|
||||
return getSmartDeviceText('fullyOpen')
|
||||
}
|
||||
if (action === 'close') {
|
||||
return getSmartDeviceText('fullyClosed')
|
||||
}
|
||||
if (action === 'stop') {
|
||||
return getSmartDeviceText('paused')
|
||||
}
|
||||
if (Number.isFinite(percentValue)) {
|
||||
return getSmartDeviceText('openness', { value: percentValue })
|
||||
}
|
||||
return getSmartDeviceText('controllable')
|
||||
}
|
||||
|
||||
if (category === 'slide') {
|
||||
const slideValue = Number(
|
||||
status && (
|
||||
status.value !== undefined
|
||||
? status.value
|
||||
: (status.percent !== undefined ? status.percent : status.position)
|
||||
)
|
||||
)
|
||||
if (Number.isFinite(slideValue)) {
|
||||
return getSmartDeviceText('currentValue', { value: slideValue })
|
||||
}
|
||||
return getSmartDeviceText('adjustable')
|
||||
}
|
||||
|
||||
const switchValue = status && status.value
|
||||
if (typeof switchValue === 'boolean') {
|
||||
return switchValue ? getSmartDeviceText('enabled') : getSmartDeviceText('disabled')
|
||||
}
|
||||
|
||||
const switchCommand = String(
|
||||
(status && (
|
||||
status.command !== undefined
|
||||
? status.command
|
||||
: (status.action !== undefined ? status.action : status.text)
|
||||
)) || ''
|
||||
).trim().toLowerCase()
|
||||
if (switchCommand === 'on') {
|
||||
return getSmartDeviceText('enabled')
|
||||
}
|
||||
if (switchCommand === 'off') {
|
||||
return getSmartDeviceText('disabled')
|
||||
}
|
||||
|
||||
if (status && status.text) {
|
||||
return String(status.text)
|
||||
}
|
||||
|
||||
return getSmartDeviceText('controllable')
|
||||
}
|
||||
|
||||
export function isDeviceActive(device = {}) {
|
||||
const controlType = inferDeviceControlType(device)
|
||||
const category = getControlTypeCategory(controlType)
|
||||
const status = parseCurrentStatus(resolveDeviceCurrentStatus(device, controlType))
|
||||
|
||||
if (!status) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (category === 'curtain' || Number(device.deviceType) === 2) {
|
||||
const action = pickFirstFilledValue(
|
||||
status.command,
|
||||
status.action,
|
||||
status.state,
|
||||
status.text
|
||||
).toLowerCase()
|
||||
const value = Number(
|
||||
status.value !== undefined
|
||||
? status.value
|
||||
: (status.percent !== undefined ? status.percent : status.position)
|
||||
)
|
||||
if (action === 'open') {
|
||||
return true
|
||||
}
|
||||
if (action === 'close') {
|
||||
return false
|
||||
}
|
||||
if (Number.isFinite(value)) {
|
||||
return value > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (category === 'slide') {
|
||||
const value = Number(
|
||||
status.value !== undefined
|
||||
? status.value
|
||||
: (status.percent !== undefined ? status.percent : status.position)
|
||||
)
|
||||
return Number.isFinite(value) ? value > 0 : false
|
||||
}
|
||||
|
||||
const switchCommand = String(
|
||||
(
|
||||
status.command !== undefined
|
||||
? status.command
|
||||
: (status.action !== undefined ? status.action : status.text)
|
||||
) || ''
|
||||
).trim().toLowerCase()
|
||||
if (switchCommand === 'on') {
|
||||
return true
|
||||
}
|
||||
if (switchCommand === 'off') {
|
||||
return false
|
||||
}
|
||||
|
||||
return !!status.value
|
||||
}
|
||||
|
||||
export function normalizeSmartDevice(item = {}) {
|
||||
const sourceDevice = unwrapSmartDevicePayload(item)
|
||||
const functionList = Array.isArray(sourceDevice.functions)
|
||||
? sourceDevice.functions.filter(entry => entry && typeof entry === 'object').map(entry => normalizeFunctionItem(entry, sourceDevice))
|
||||
: []
|
||||
const normalizedDevice = {
|
||||
...sourceDevice,
|
||||
functions: functionList
|
||||
}
|
||||
const controlType = inferDeviceControlType(normalizedDevice)
|
||||
const currentStatus = resolveDeviceCurrentStatus(normalizedDevice, controlType)
|
||||
|
||||
return {
|
||||
...normalizedDevice,
|
||||
currentStatus,
|
||||
controlType,
|
||||
iconClass: getDeviceIconClass(controlType),
|
||||
active: isDeviceActive(normalizedDevice),
|
||||
stateText: buildDeviceStateText(normalizedDevice),
|
||||
roomName: pickFirstFilledValue(sourceDevice.roomName),
|
||||
deviceName: pickFirstFilledValue(sourceDevice.deviceName, sourceDevice.deviceNo, sourceDevice.platformDeviceId, getSmartDeviceText('unnamedDevice'))
|
||||
}
|
||||
}
|
||||
|
||||
export function getDeviceFunctions(device = {}, includeFallback = true) {
|
||||
const functionList = Array.isArray(device.functions)
|
||||
? device.functions.filter(item => item && typeof item === 'object').map(item => normalizeFunctionItem(item, device))
|
||||
: []
|
||||
|
||||
if (functionList.length) {
|
||||
return functionList
|
||||
}
|
||||
|
||||
if (!includeFallback) {
|
||||
return []
|
||||
}
|
||||
|
||||
const fallbackType = inferDeviceControlType(device)
|
||||
return fallbackType
|
||||
? [{
|
||||
functionCode: fallbackType,
|
||||
functionDes: getDeviceControlLabel(fallbackType),
|
||||
controlType: fallbackType,
|
||||
commandSchema: null,
|
||||
isFallback: true
|
||||
}]
|
||||
: []
|
||||
}
|
||||
|
||||
export function buildCommandSchemaFields(commandSchema = null) {
|
||||
if (!commandSchema) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (Array.isArray(commandSchema)) {
|
||||
return commandSchema
|
||||
.map((item, index) => normalizeSchemaField(
|
||||
pickFirstFilledValue(item && item.name, item && item.key, `field${index + 1}`),
|
||||
item
|
||||
))
|
||||
.filter(item => item.name)
|
||||
}
|
||||
|
||||
if (typeof commandSchema !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
let schemaSource = null
|
||||
if (commandSchema.properties && typeof commandSchema.properties === 'object') {
|
||||
schemaSource = commandSchema.properties
|
||||
} else if (commandSchema.fields && typeof commandSchema.fields === 'object') {
|
||||
schemaSource = commandSchema.fields
|
||||
} else if (commandSchema.params && typeof commandSchema.params === 'object') {
|
||||
schemaSource = commandSchema.params
|
||||
} else {
|
||||
const candidateEntries = Object.keys(commandSchema)
|
||||
.filter(key => !isSchemaMetaKey(key))
|
||||
.filter(key => {
|
||||
const value = commandSchema[key]
|
||||
return value !== undefined && value !== null && typeof value !== 'function'
|
||||
})
|
||||
|
||||
if (candidateEntries.length) {
|
||||
schemaSource = candidateEntries.reduce((result, key) => {
|
||||
result[key] = commandSchema[key]
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
if (!schemaSource || typeof schemaSource !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.keys(schemaSource)
|
||||
.map(key => normalizeSchemaField(key, schemaSource[key]))
|
||||
.filter(item => item.name)
|
||||
}
|
||||
|
||||
export function normalizeSmartDeviceList(response) {
|
||||
const payload = response && response.data !== undefined ? response.data : response
|
||||
const deviceList = collectObjectArray(payload)
|
||||
if (deviceList.length) {
|
||||
return deviceList
|
||||
.filter(item => item && typeof item === 'object' && !Array.isArray(item))
|
||||
.map(item => normalizeSmartDevice(item))
|
||||
}
|
||||
|
||||
const singleDevice = unwrapSmartDevicePayload(payload)
|
||||
return Object.keys(singleDevice).length ? [normalizeSmartDevice(singleDevice)] : []
|
||||
}
|
||||
|
||||
export function patchDeviceStatus(device = {}, command = {}, forcedControlType = '') {
|
||||
const controlType = String(forcedControlType || '').trim().toLowerCase() || inferDeviceControlType(device)
|
||||
const category = getControlTypeCategory(controlType)
|
||||
|
||||
if (category === 'curtain') {
|
||||
const action = String(command.command || command.action || command.value || '').trim().toLowerCase()
|
||||
if (!action) {
|
||||
return normalizeSmartDevice(device)
|
||||
}
|
||||
|
||||
return normalizeSmartDevice({
|
||||
...device,
|
||||
currentStatus: {
|
||||
command: action
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (category === 'slide') {
|
||||
const value = Number(
|
||||
command.value !== undefined
|
||||
? command.value
|
||||
: (command.percent !== undefined ? command.percent : command.position)
|
||||
)
|
||||
return normalizeSmartDevice({
|
||||
...device,
|
||||
currentStatus: {
|
||||
value: Number.isFinite(value) ? value : 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const switchCommand = String(
|
||||
(
|
||||
command.command !== undefined
|
||||
? command.command
|
||||
: (
|
||||
command.action !== undefined
|
||||
? command.action
|
||||
: (
|
||||
command.value === true
|
||||
? 'on'
|
||||
: (command.value === false ? 'off' : command.value)
|
||||
)
|
||||
)
|
||||
) || ''
|
||||
).trim().toLowerCase()
|
||||
|
||||
if (switchCommand) {
|
||||
return normalizeSmartDevice({
|
||||
...device,
|
||||
currentStatus: {
|
||||
command: switchCommand
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return normalizeSmartDevice(device)
|
||||
}
|
||||
170
utils/style-data.js
Normal file
170
utils/style-data.js
Normal file
@@ -0,0 +1,170 @@
|
||||
export const STYLE_LIST = [
|
||||
{
|
||||
id: 'moroccan',
|
||||
nameZh: '摩洛哥风',
|
||||
nameEn: 'Moroccan Style',
|
||||
descZh: '异域度假 · 热情松弛 · 拱门结构',
|
||||
descEn: 'Exotic Vibe · Relaxing · Arch Design',
|
||||
longDescZh: '摩洛哥风格以其大胆的色彩碰撞、繁复的几何图案和独特的拱门元素著称。在这里,你可以感受到浓郁的北非风情与地中海的慵懒气息。',
|
||||
longDescEn: 'Moroccan style is known for its bold colors, intricate geometric patterns, and unique arch elements. Experience the rich North African flair and Mediterranean laziness here.',
|
||||
tagsZh: ['拱门设计', '异域风情', '色彩碰撞'],
|
||||
tagsEn: ['Arch Design', 'Exotic', 'Color Clash'],
|
||||
cover: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_6.png',
|
||||
plan: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_5.png',
|
||||
planNameZh: '65方户型平面图',
|
||||
planNameEn: '65m² Floor Plan',
|
||||
buildings: ['B1', 'B2', 'B3'],
|
||||
floors: ['1F', '2F', '3F', '4F', '5F'],
|
||||
roomTypes: [
|
||||
{
|
||||
id: 'moroccan-onebed',
|
||||
nameZh: '拱门一居套房',
|
||||
nameEn: 'Arch One Bedroom',
|
||||
area: '65㎡',
|
||||
monthlyRent: '4200',
|
||||
images: [
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_6.png',
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_8.png'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'moroccan-twobed',
|
||||
nameZh: '庭院双卧套房',
|
||||
nameEn: 'Courtyard Two Bedroom',
|
||||
area: '92㎡',
|
||||
monthlyRent: '5600',
|
||||
images: [
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_14.png',
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_20.png'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'southeast',
|
||||
nameZh: '东南亚风',
|
||||
nameEn: 'Southeast Asian',
|
||||
descZh: '自然禅意 · 开放空间 · 原木藤编',
|
||||
descEn: 'Zen Nature · Open Space · Rattan & Wood',
|
||||
longDescZh: '大量使用原木、藤编和棉麻等天然材质,结合宽敞的开放式空间设计,营造出一种回归自然、宁静致远的居住体验。',
|
||||
longDescEn: 'Extensive use of natural materials like wood, rattan, and linen, combined with spacious open design, creates a living experience that returns to nature and tranquility.',
|
||||
tagsZh: ['原木材质', '自然禅意', '通透采光'],
|
||||
tagsEn: ['Wood Material', 'Zen', 'Natural Light'],
|
||||
cover: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_13.png',
|
||||
plan: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_12.png',
|
||||
planNameZh: '123方户型平面图',
|
||||
planNameEn: '123m² Floor Plan',
|
||||
buildings: ['S1', 'S2'],
|
||||
floors: ['1F', '2F', '3F'],
|
||||
roomTypes: [
|
||||
{
|
||||
id: 'southeast-garden',
|
||||
nameZh: '花园阳台房',
|
||||
nameEn: 'Garden Balcony Suite',
|
||||
area: '88㎡',
|
||||
monthlyRent: '4600',
|
||||
images: [
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_13.png',
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_14.png'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'southeast-family',
|
||||
nameZh: '藤编家庭套房',
|
||||
nameEn: 'Rattan Family Suite',
|
||||
area: '123㎡',
|
||||
monthlyRent: '6200',
|
||||
images: [
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_20.png',
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_12.png'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'european',
|
||||
nameZh: '简欧风格',
|
||||
nameEn: 'Modern European',
|
||||
descZh: '俄式简约 · 典雅通透 · 奶油底色',
|
||||
descEn: 'Russian Minimalist · Elegant · Cream Tone',
|
||||
longDescZh: '摒弃了传统欧式的繁琐,保留了经典的线条与对称美学。以低饱和度的奶油色为基调,展现出一种低调而不失优雅的生活态度。',
|
||||
longDescEn: 'Discarding the cumbersomeness of traditional European style, it retains classic lines and symmetrical aesthetics. Based on low-saturation cream colors, it shows a low-key yet elegant life attitude.',
|
||||
tagsZh: ['经典线条', '低调优雅', '奶油色调'],
|
||||
tagsEn: ['Classic Lines', 'Elegant', 'Cream Tone'],
|
||||
cover: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_19.png',
|
||||
plan: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_18.png',
|
||||
planNameZh: '143方户型平面图',
|
||||
planNameEn: '143m² Floor Plan',
|
||||
buildings: ['E1', 'E2', 'E3'],
|
||||
floors: ['1F', '2F', '3F', '4F'],
|
||||
roomTypes: [
|
||||
{
|
||||
id: 'european-suite',
|
||||
nameZh: '奶油轻奢套房',
|
||||
nameEn: 'Cream Luxury Suite',
|
||||
area: '96㎡',
|
||||
monthlyRent: '5200',
|
||||
images: [
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_19.png',
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_18.png'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'european-premium',
|
||||
nameZh: '对称大平层',
|
||||
nameEn: 'Symmetry Premium Suite',
|
||||
area: '143㎡',
|
||||
monthlyRent: '7600',
|
||||
images: [
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_19.png',
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_14.png'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'modern',
|
||||
nameZh: '现代风',
|
||||
nameEn: 'Modern Minimalist',
|
||||
descZh: '极简高级 · 动线流畅 · 隐形收纳',
|
||||
descEn: 'Premium Minimal · Smooth Flow · Hidden Storage',
|
||||
longDescZh: '追求极致的简约与实用,强调空间的动线流畅与收纳的隐蔽性。黑白灰的经典搭配,彰显出都市精英的干练与品味。',
|
||||
longDescEn: 'Pursuing extreme simplicity and practicality, emphasizing smooth spatial flow and hidden storage. The classic match of black, white, and gray highlights the capability and taste of urban elites.',
|
||||
tagsZh: ['隐蔽收纳', '动线流畅', '极简主义'],
|
||||
tagsEn: ['Hidden Storage', 'Smooth Flow', 'Minimalism'],
|
||||
cover: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_25.png',
|
||||
plan: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_24.png',
|
||||
planNameZh: '65方户型平面图',
|
||||
planNameEn: '65m² Floor Plan',
|
||||
buildings: ['M1', 'M2'],
|
||||
floors: ['1F', '2F', '3F', '4F', '5F', '6F'],
|
||||
roomTypes: [
|
||||
{
|
||||
id: 'modern-studio',
|
||||
nameZh: '极简单身公寓',
|
||||
nameEn: 'Minimal Studio',
|
||||
area: '65㎡',
|
||||
monthlyRent: '4300',
|
||||
images: [
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_25.png',
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_24.png'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'modern-loft',
|
||||
nameZh: '高层 loft 套房',
|
||||
nameEn: 'High-rise Loft',
|
||||
area: '98㎡',
|
||||
monthlyRent: '5800',
|
||||
images: [
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_24.png',
|
||||
'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_25.png'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export function getStyleById(styleId) {
|
||||
return STYLE_LIST.find(item => item.id === styleId) || STYLE_LIST[0]
|
||||
}
|
||||
Reference in New Issue
Block a user