feat: 新增工单中心、房间巡查、失物招领等业务模块
- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面 - 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面 - 预约看样流程新增房型/楼栋/楼层选择器组件 - 新增工具模块: booking-entry、privacy、reservation-flow - api/index.js 补充楼层列表、看样提交等接口 - locales/messages.js 补充多语言文案 - manifest.json 开启微信小程序隐私检查 - 完善登录、注册、申请详情、账单等相关页面交互
This commit is contained in:
269
api/index.js
269
api/index.js
@@ -67,6 +67,18 @@ function buildRequestCacheKey(prefix, params = {}, language) {
|
||||
return `${prefix}:${language}:${JSON.stringify(normalizeRequestCacheValue(params || {}))}`
|
||||
}
|
||||
|
||||
function createJsonPatchRequest(url, data = {}, language) {
|
||||
return request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
header: {
|
||||
'Content-Type': 'application/json-patch+json',
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const notificationPageRequestCache = new Map()
|
||||
const notificationPageResponseCache = new Map()
|
||||
const NOTIFICATION_PAGE_CACHE_TTL = 1200
|
||||
@@ -121,6 +133,17 @@ export function getEnums(params = {}, language) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getFloorList(params = {}, language) {
|
||||
return request({
|
||||
url: '/floor/list',
|
||||
method: 'GET',
|
||||
params,
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- 用户与登录 ---
|
||||
export function usersLogin(data = {}, language) {
|
||||
return request({
|
||||
@@ -169,6 +192,18 @@ export function submitApplicationForm(data = {}, language) {
|
||||
})
|
||||
}
|
||||
|
||||
export function submitViewingApplicationForm(data = {}, language) {
|
||||
return request({
|
||||
url: '/applicationform/viewing/submit',
|
||||
method: 'POST',
|
||||
data,
|
||||
header: {
|
||||
'Content-Type': 'application/json-patch+json',
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function registrationApplicationMePage(params = {}, language) {
|
||||
return request({
|
||||
url: '/applicationform/registration/me/page',
|
||||
@@ -202,6 +237,110 @@ export function applicationFormMePage(params = {}, language) {
|
||||
})
|
||||
}
|
||||
|
||||
// --- 工单 ---
|
||||
export function ticketOrderPage(params = {}, language) {
|
||||
return request({
|
||||
url: '/ticketorder/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function ticketOrderMyPage(params = {}, language) {
|
||||
return request({
|
||||
url: '/ticketorder/my/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function ticketOrderDetail(flowNo, language) {
|
||||
return request({
|
||||
url: `/ticketorder/${flowNo}/flows`,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function ticketOrderFlows(orderNo, language) {
|
||||
return request({
|
||||
url: `/ticketorder/${orderNo}/flows`,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function ticketOrderTransitionOptions(flowNo, language) {
|
||||
return request({
|
||||
url: `/ticketorder/${flowNo}/transition-options`,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function ticketFlowTransitionStepRuleSelectTypeItems(params = {}, language) {
|
||||
return request({
|
||||
url: '/ticketflowtransitionsteprule/selecttypeitems',
|
||||
method: 'GET',
|
||||
params,
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function ticketOrderFixonhold(data = {}, language) {
|
||||
return createJsonPatchRequest('/ticketorder/fixonhold', data, language)
|
||||
}
|
||||
|
||||
export function ticketOrderTransit(data = {}, language) {
|
||||
return createJsonPatchRequest('/ticketorder/transit', data, language)
|
||||
}
|
||||
|
||||
export function ticketOrderSubmitChangeroom(data = {}, language) {
|
||||
return createJsonPatchRequest('/ticketordersubmit/changeroom', data, language)
|
||||
}
|
||||
|
||||
export function ticketOrderSubmitCheckout(data = {}, language) {
|
||||
return createJsonPatchRequest('/ticketordersubmit/checkout', data, language)
|
||||
}
|
||||
|
||||
export function ticketOrderSubmitRepair(data = {}, language) {
|
||||
return createJsonPatchRequest('/ticketordersubmit/repair', data, language)
|
||||
}
|
||||
|
||||
export function ticketOrderSubmitSuggestion(data = {}, language) {
|
||||
return createJsonPatchRequest('/ticketordersubmit/suggestion', data, language)
|
||||
}
|
||||
|
||||
export function ticketOrderSubmitRenewal(data = {}, language) {
|
||||
return createJsonPatchRequest('/ticketordersubmit/renewal', data, language)
|
||||
}
|
||||
|
||||
export function ticketOrderSubmitWaterdelivery(data = {}, language) {
|
||||
return createJsonPatchRequest('/ticketordersubmit/waterdelivery', data, language)
|
||||
}
|
||||
|
||||
export function ticketOrderSubmitCleaning(data = {}, language) {
|
||||
return createJsonPatchRequest('/ticketordersubmit/cleaning', data, language)
|
||||
}
|
||||
|
||||
export function lostAndFoundPublish(data = {}, language) {
|
||||
return createJsonPatchRequest('/lostandfound/publish', data, language)
|
||||
}
|
||||
|
||||
// --- 订单与入住 ---
|
||||
export function orderSearchedHistoryPage(params = {}, language) {
|
||||
return request({
|
||||
@@ -225,6 +364,17 @@ export function orderSearchedMePage(params = {}, language) {
|
||||
})
|
||||
}
|
||||
|
||||
export function orderSearchedMyPage(params = {}, language) {
|
||||
return request({
|
||||
url: '/order/searched/me/page',
|
||||
method: 'GET',
|
||||
params: params || {},
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function orderSearchedMeHistoryPage(params = {}, language) {
|
||||
return request({
|
||||
url: '/order/searched/me/history/page',
|
||||
@@ -268,6 +418,64 @@ export function orderSearchedReservationPage(params = {}, language) {
|
||||
})
|
||||
}
|
||||
|
||||
export function orderSearchedReservationFlow(data = {}, language) {
|
||||
return request({
|
||||
url: '/order/searched/reservation/flow',
|
||||
method: 'POST',
|
||||
data,
|
||||
header: {
|
||||
'Content-Type': 'application/json-patch+json',
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function roomInspectionMyPage(params = {}, language) {
|
||||
return request({
|
||||
url: '/roominspection/my/page',
|
||||
method: 'GET',
|
||||
params: params || {},
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function roomInspectionDetail(params = {}, language) {
|
||||
return request({
|
||||
url: '/roominspection/detail',
|
||||
method: 'GET',
|
||||
params: params || {},
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function roomInspectionUpdateItems(data = {}, language) {
|
||||
return request({
|
||||
url: '/roominspection/update/items',
|
||||
method: 'POST',
|
||||
data,
|
||||
header: {
|
||||
'Content-Type': 'application/json-patch+json',
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function roomInspectionSubmit(data = {}, language) {
|
||||
return request({
|
||||
url: '/roominspection/submit',
|
||||
method: 'POST',
|
||||
data,
|
||||
header: {
|
||||
'Content-Type': 'application/json-patch+json',
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function checkRecordByChecked(params = {}, language) {
|
||||
const query = params || {}
|
||||
const orderNo = String(query.orderNo || '').trim()
|
||||
@@ -300,6 +508,24 @@ export function accountQueryBills(params = {}, language) {
|
||||
})
|
||||
}
|
||||
|
||||
export function accountQueryBill(params = {}, language) {
|
||||
const query = params || {}
|
||||
const billNo = String(query.billNo || query.BillNo || '').trim()
|
||||
if (!billNo) {
|
||||
return Promise.reject(new Error('账单号不能为空'))
|
||||
}
|
||||
return request({
|
||||
url: '/accountquery/bill',
|
||||
method: 'GET',
|
||||
params: {
|
||||
billNo
|
||||
},
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function paymentBillPay(data = {}, language) {
|
||||
const payload = data || {}
|
||||
return request({
|
||||
@@ -385,6 +611,17 @@ export function getMyEffectiveContracts(params = {}, language) {
|
||||
})
|
||||
}
|
||||
|
||||
export function contractDetail(params = {}, language) {
|
||||
return request({
|
||||
url: '/contract/detail',
|
||||
method: 'GET',
|
||||
params,
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function contractPreview(data = {}, language) {
|
||||
return request({
|
||||
url: '/contract/preview',
|
||||
@@ -484,6 +721,38 @@ export function facilityManagers(params = {}, language) {
|
||||
})
|
||||
}
|
||||
|
||||
// --- 宿管与管理员联系人 ---
|
||||
export function myDormitoryManagers(language) {
|
||||
return request({
|
||||
url: '/usercampus/my-dormitory-managers',
|
||||
method: 'GET',
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function myCounselor(language) {
|
||||
return request({
|
||||
url: '/usercampus/my-counselor',
|
||||
method: 'GET',
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function buildingPagination(params = {}, language) {
|
||||
return request({
|
||||
url: '/building/pagination',
|
||||
method: 'GET',
|
||||
params,
|
||||
header: {
|
||||
'X-Requested-Language': resolveLanguageHeader(language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function roomtypeList(params = {}, language) {
|
||||
return request({
|
||||
url: '/roomtype/list',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const config = {
|
||||
// 根据不同环境维护接口基准地址
|
||||
// baseURL: 'http://192.168.199.115:5003/api',
|
||||
// baseURL: 'http://192.168.199.182:5003/api',
|
||||
// baseURL: 'http://192.168.199.124:5111/admin/api',
|
||||
// baseURL: 'https://api.xhct.yaens.com/admin/api',
|
||||
baseURL: 'https://apartment.lvjuwuye.cn/admin/api',
|
||||
|
||||
@@ -25,8 +25,12 @@ const messages = {
|
||||
"pkgService/message_detail/message_detail": "Message Detail",
|
||||
"pkgService/my_application/my_application": "My Applications",
|
||||
"pkgService/my_contract/my_contract": "My Contracts",
|
||||
"pkgService/room_inspection/room_inspection": "Inspection",
|
||||
"pkgService/service_stub/service_stub": "Service Center",
|
||||
"pkgService/smart_control/smart_control": "Smart Control",
|
||||
"pkgService/ticket_center/ticket_center": "Work Orders",
|
||||
"pkgService/ticket_workbench/ticket_workbench": "Work Order",
|
||||
"pkgService/lost_found_publish/lost_found_publish": "Lost and Found",
|
||||
"pkgService/submit_request/submit_request": "Submit Request",
|
||||
"pkgService/utility_recharge/utility_recharge": "Utility Recharge"
|
||||
}
|
||||
@@ -92,6 +96,7 @@ const messages = {
|
||||
cancelOrderSuccess: "Order canceled",
|
||||
cancelOrderTitle: "Cancel Order",
|
||||
continuePay: "Continue Pay",
|
||||
currentStepTag: "Current",
|
||||
dateRangeJoiner: " to ",
|
||||
depositNoticeTitle: "Rental Deposit Notice",
|
||||
emptyDesc: "You have not submitted any application yet. Go select a room to start a stay or viewing application.",
|
||||
@@ -102,11 +107,21 @@ const messages = {
|
||||
loadingDesc: "Syncing your application records. Please wait.",
|
||||
missingApplicationCode: "Missing application number",
|
||||
missingBillCode: "Missing application bill number",
|
||||
missingContractNo: "Missing contract number",
|
||||
noPendingDepositBill: "No pending deposit bill available",
|
||||
noMore: "No more applications",
|
||||
occupantLabel: "Resident",
|
||||
pageDesc: "Your stay and viewing application records",
|
||||
pageTitle: "My Applications",
|
||||
payDeposit: "Pay Deposit",
|
||||
fetchPaymentParamsFailed: "Failed to get payment parameters",
|
||||
fetchWechatLoginCodeFailed: "Failed to get WeChat login code",
|
||||
loadingBill: "Loading bill...",
|
||||
loadingPayment: "Opening payment...",
|
||||
depositPaymentDesc: "Deposit Payment",
|
||||
paymentSuccess: "Payment succeeded",
|
||||
paymentCanceled: "Payment canceled",
|
||||
paymentFailed: "Payment failed",
|
||||
pendingPaymentAmountLabel: "Order Amount",
|
||||
pendingPaymentDescLabel: "Description",
|
||||
pendingPaymentExpireLabel: "Time Left",
|
||||
@@ -118,8 +133,20 @@ const messages = {
|
||||
refreshEmptySuccess: "No application found. Reservation records synced.",
|
||||
refreshSuccess: "Application status refreshed",
|
||||
refreshing: "Refreshing...",
|
||||
refreshAction: "Refresh",
|
||||
remarkLabel: "Remarks",
|
||||
reservationFlowDesc: "Progress of the current reservation order",
|
||||
reservationFlowTitle: "Reservation Flow",
|
||||
reservationOrderFallback: "Reservation",
|
||||
reservationOrderNoLabel: "Order No.",
|
||||
roomNoLabel: "Room No.",
|
||||
reservationRoomLabel: "Room Type",
|
||||
reservationStatusLabel: "Order Status",
|
||||
contractPendingReviewHint: "Contract signed, waiting for review",
|
||||
inspectionPendingReviewHint: "Inspection under review",
|
||||
checkinPendingHint: "Move-in is being processed, please wait",
|
||||
scrollToRead: "Scroll to bottom",
|
||||
refreshFlowAction: "Refresh Flow",
|
||||
statusApproved: "Approved",
|
||||
statusFinished: "Closed",
|
||||
statusPending: "Pending",
|
||||
@@ -249,9 +276,14 @@ const messages = {
|
||||
loadingManagerInfo: "Loading butler info",
|
||||
loadingShort: "Syncing",
|
||||
managerContactSummary: "{count} butlers are available in this area",
|
||||
managementContacts: "Dorm Managers & Admins",
|
||||
dormitoryManagerTag: "Dorm Mgr",
|
||||
counselorTag: "Admin",
|
||||
noManagerContacts: "No management contacts",
|
||||
managerFallback: "Community Butler",
|
||||
managerInitialFallback: "B",
|
||||
managerWechatCopied: "Butler Wechat copied",
|
||||
mealDeliveryJumpFailed: "Failed to open the ordering mini-program",
|
||||
multiButlerName: "{name} +{count}",
|
||||
myBill: "Monthly Bill",
|
||||
noButlerInfo: "No butler information yet",
|
||||
@@ -268,7 +300,7 @@ const messages = {
|
||||
pendingContractFound: "Pending contract found",
|
||||
pendingSync: "Pending",
|
||||
primaryButlerTag: "Lead",
|
||||
publicService: "Facility Booking",
|
||||
publicService: "Premium Services",
|
||||
refreshFailed: "Refresh failed, please try again later",
|
||||
refreshStatus: "Refresh Status",
|
||||
refreshing: "Refreshing...",
|
||||
@@ -366,6 +398,7 @@ const messages = {
|
||||
},
|
||||
register: {
|
||||
register: {
|
||||
admissionNoticeAttachmentLabel: "Admission Notice (Employment Certificate)",
|
||||
accountLabel: "Account",
|
||||
accountPlaceholder: "Enter account name",
|
||||
accountSection: "Account Information",
|
||||
@@ -436,6 +469,7 @@ const messages = {
|
||||
submitRegister: "Submit Registration",
|
||||
submitting: "Submitting...",
|
||||
uploadFailed: "Upload failed, please retry",
|
||||
uploadAdmissionNotice: "Upload admission notice or employment certificate",
|
||||
uploadIdBack: "Upload ID card back",
|
||||
uploadIdFront: "Upload ID card front",
|
||||
uploadImageOption: "Upload Image",
|
||||
@@ -521,6 +555,7 @@ const messages = {
|
||||
duration5: "Over 1 year",
|
||||
durationLabel: "Expected Stay Duration",
|
||||
navTitle: "On-site Viewing",
|
||||
roomTypeLabel: "Preferred Room Type",
|
||||
remarkArea: "Preferred Area: {area}",
|
||||
remarkBudget: "Budget: {budget} CNY/month",
|
||||
remarkDuration: "Expected Stay: {duration}",
|
||||
@@ -795,6 +830,10 @@ const messages = {
|
||||
leaseDurationKey: "Expected Stay Duration",
|
||||
leaseTermLabel: "Lease Term",
|
||||
missingBillCode: "Missing application bill number",
|
||||
missingContractNo: "Missing contract number",
|
||||
inspectionPendingReviewHint: "Inspection under review",
|
||||
checkinPendingHint: "Move-in is being processed, please wait",
|
||||
refreshAction: "Refresh",
|
||||
navTitle: "Application Detail",
|
||||
paymentMethodKey: "Payment Method",
|
||||
preferredAreaKey: "Preferred Area",
|
||||
@@ -1147,8 +1186,62 @@ const messages = {
|
||||
unsupportedSwitch: "This device does not support quick switching"
|
||||
}
|
||||
},
|
||||
lost_found_publish: {
|
||||
lost_found_publish: {
|
||||
navTitle: "Lost and Found",
|
||||
heroBadge: "Community Service",
|
||||
heroTitle: "Publish lost or found item details",
|
||||
heroDesc: "Share lost or found information with the community so that follow-up and matching can happen faster.",
|
||||
formTitle: "Item Information",
|
||||
formDesc: "Please complete the key details below. Required fields help others contact you faster.",
|
||||
typeLabel: "Publish Type",
|
||||
typePlaceholder: "Select lost or found",
|
||||
typeLost: "Lost Item",
|
||||
typeFound: "Found Item",
|
||||
itemTypeLabel: "Item Category",
|
||||
itemTypePlaceholder: "For example: documents, keys, electronics",
|
||||
itemNameLabel: "Item Name",
|
||||
itemNamePlaceholder: "Please enter the item name",
|
||||
placeLabel: "Location",
|
||||
placePlaceholder: "Please enter where it was lost or found",
|
||||
whenLabel: "Time",
|
||||
selectDate: "Select date",
|
||||
selectTime: "Select time",
|
||||
whenTip: "Optional. Fill in the approximate time if available.",
|
||||
contractLabel: "Contact Person",
|
||||
contractPlaceholder: "Please enter the contact person",
|
||||
contractPhoneLabel: "Contact Phone",
|
||||
contractPhonePlaceholder: "Please enter the contact phone",
|
||||
remarkLabel: "Remarks",
|
||||
remarkPlaceholder: "Add more details such as appearance, claim notes, or identifying features",
|
||||
imagesLabel: "Images",
|
||||
uploadImage: "Upload Image",
|
||||
imagesTip: "Optional. Upload on-site or item photos to help identify the item.",
|
||||
submitTip: "Please verify the information before publishing. We will submit it with the current language.",
|
||||
submitting: "Submitting...",
|
||||
submitButton: "Publish Now",
|
||||
uploadFailed: "Image upload failed",
|
||||
uploadOversize: "Each image must be within 5 MB",
|
||||
uploadExceed: "You can upload up to 6 images",
|
||||
validateItemType: "Please enter the item category",
|
||||
validateItemName: "Please enter the item name",
|
||||
validatePlace: "Please enter the location",
|
||||
validateContract: "Please enter the contact person",
|
||||
validateContractPhone: "Please enter the contact phone",
|
||||
submitSuccess: "Published successfully",
|
||||
submitFailed: "Failed to publish"
|
||||
}
|
||||
},
|
||||
submit_request: {
|
||||
components: {
|
||||
BuildingSelector: {
|
||||
emptyText: "No buildings available",
|
||||
loadFailed: "Failed to load buildings",
|
||||
loadingText: "Loading buildings...",
|
||||
popupTitle: "Select Building",
|
||||
selectAreaFirst: "Please select an area first",
|
||||
selectBuilding: "Select building"
|
||||
},
|
||||
DateRangeSelector: {
|
||||
dateDivider: "to",
|
||||
endLabel: "Expected Check-out",
|
||||
@@ -1156,6 +1249,14 @@ const messages = {
|
||||
startLabel: "Expected Check-in",
|
||||
startPlaceholder: "Select check-in date"
|
||||
},
|
||||
FloorSelector: {
|
||||
clearSelection: "Not specified",
|
||||
emptyText: "No floors available",
|
||||
loadFailed: "Failed to load floor list",
|
||||
loadingText: "Loading floors...",
|
||||
popupTitle: "Select Floor",
|
||||
selectFloor: "Select floor"
|
||||
},
|
||||
TagSelector: {
|
||||
emptyText: "No tags available",
|
||||
loadingText: "Loading tags..."
|
||||
@@ -1167,12 +1268,14 @@ const messages = {
|
||||
applyRemarkPlaceholder: "Enter your additional notes",
|
||||
applyRoomType: "Requested Room Type",
|
||||
areaLabel: "Area",
|
||||
buildingLabel: "Preferred Building",
|
||||
currentRoom: "Current Room",
|
||||
dateCannotBefore: "{endLabel} cannot be earlier than {startLabel}",
|
||||
emptyHobbyTags: "No hobby tags",
|
||||
emptyLifestyleTags: "No lifestyle tags",
|
||||
expectedCheckin: "Expected Check-in",
|
||||
expectedCheckout: "Expected Checkout",
|
||||
floorLabel: "Preferred Floor",
|
||||
hobbyLabel: "Hobbies",
|
||||
leaseRange: "Lease Period",
|
||||
lifestyleLabel: "Lifestyle Habits",
|
||||
@@ -1184,6 +1287,10 @@ const messages = {
|
||||
roomTypeApplyBlockTitle: "Application Details",
|
||||
roomTypeApplyDesc: "Confirm the room type, area, preference tags, and lease period before submitting.",
|
||||
roomTypeApplyTitle: "Room Type Application",
|
||||
switchRoomType: "Switch",
|
||||
summaryTitle: "Application Summary",
|
||||
expandSummary: "Expand",
|
||||
collapseSummary: "Collapse",
|
||||
selectCheckinDate: "Select check-in date",
|
||||
selectCheckoutDate: "Select checkout date",
|
||||
selectDateEnd: "Please select {label}",
|
||||
@@ -1231,8 +1338,12 @@ const messages = {
|
||||
"pkgService/message_detail/message_detail": "消息详情",
|
||||
"pkgService/my_application/my_application": "我的申请",
|
||||
"pkgService/my_contract/my_contract": "我的合同",
|
||||
"pkgService/room_inspection/room_inspection": "验房",
|
||||
"pkgService/service_stub/service_stub": "业务办理",
|
||||
"pkgService/smart_control/smart_control": "智能控制",
|
||||
"pkgService/ticket_center/ticket_center": "工单中心",
|
||||
"pkgService/ticket_workbench/ticket_workbench": "工单处理",
|
||||
"pkgService/lost_found_publish/lost_found_publish": "失物拾物",
|
||||
"pkgService/submit_request/submit_request": "我要提交",
|
||||
"pkgService/utility_recharge/utility_recharge": "水电充值"
|
||||
}
|
||||
@@ -1298,6 +1409,7 @@ const messages = {
|
||||
cancelOrderSuccess: "订单已取消",
|
||||
cancelOrderTitle: "取消订单",
|
||||
continuePay: "继续支付",
|
||||
currentStepTag: "当前",
|
||||
dateRangeJoiner: " 至 ",
|
||||
depositNoticeTitle: "租房定金须知",
|
||||
emptyDesc: "您还没有提交任何申请,去选房后可发起入住申请或预约看房。",
|
||||
@@ -1308,11 +1420,21 @@ const messages = {
|
||||
loadingDesc: "正在同步您的入住申请记录,请稍候。",
|
||||
missingApplicationCode: "申请编号缺失",
|
||||
missingBillCode: "申请单编号缺失",
|
||||
missingContractNo: "合同编号缺失",
|
||||
noPendingDepositBill: "暂无待支付定金账单",
|
||||
noMore: "没有更多申请了",
|
||||
occupantLabel: "入住人",
|
||||
pageDesc: "您的入住和预约看房申请记录",
|
||||
pageTitle: "我的申请",
|
||||
payDeposit: "支付定金",
|
||||
fetchPaymentParamsFailed: "获取支付参数失败",
|
||||
fetchWechatLoginCodeFailed: "获取微信登录凭证失败",
|
||||
loadingBill: "正在获取账单...",
|
||||
loadingPayment: "正在拉起支付...",
|
||||
depositPaymentDesc: "支付定金",
|
||||
paymentSuccess: "支付成功",
|
||||
paymentCanceled: "已取消支付",
|
||||
paymentFailed: "支付失败",
|
||||
pendingPaymentAmountLabel: "订单金额",
|
||||
pendingPaymentDescLabel: "支付说明",
|
||||
pendingPaymentExpireLabel: "剩余支付时间",
|
||||
@@ -1324,8 +1446,20 @@ const messages = {
|
||||
refreshEmptySuccess: "未查到申请单,已同步预订单",
|
||||
refreshSuccess: "申请状态已刷新",
|
||||
refreshing: "刷新中...",
|
||||
refreshAction: "刷新",
|
||||
remarkLabel: "补充说明",
|
||||
reservationFlowDesc: "当前预定单的流程进度",
|
||||
reservationFlowTitle: "预定流程",
|
||||
reservationOrderFallback: "预定单",
|
||||
reservationOrderNoLabel: "预定单号",
|
||||
roomNoLabel: "房间号",
|
||||
reservationRoomLabel: "房型",
|
||||
reservationStatusLabel: "订单状态",
|
||||
contractPendingReviewHint: "合同已签,等待宿管审核",
|
||||
inspectionPendingReviewHint: "验房审批中",
|
||||
checkinPendingHint: "宿管正在办理入住,请耐心等待",
|
||||
scrollToRead: "请滑动阅读至底部",
|
||||
refreshFlowAction: "刷新流程",
|
||||
statusApproved: "已通过",
|
||||
statusFinished: "已结束",
|
||||
statusPending: "待审核",
|
||||
@@ -1455,9 +1589,14 @@ const messages = {
|
||||
loadingManagerInfo: "正在加载管家信息",
|
||||
loadingShort: "同步中",
|
||||
managerContactSummary: "当前区域共有 {count} 位管家可联系",
|
||||
managementContacts: "宿管与管理员",
|
||||
dormitoryManagerTag: "宿管",
|
||||
counselorTag: "管理员",
|
||||
noManagerContacts: "暂无管理联系人",
|
||||
managerFallback: "社区管家",
|
||||
managerInitialFallback: "管",
|
||||
managerWechatCopied: "管家微信已复制",
|
||||
mealDeliveryJumpFailed: "打开点餐小程序失败",
|
||||
multiButlerName: "{name} 等{count}位管家",
|
||||
myBill: "本月账单",
|
||||
noButlerInfo: "暂未获取到管家信息",
|
||||
@@ -1474,7 +1613,7 @@ const messages = {
|
||||
pendingContractFound: "发现待签合同",
|
||||
pendingSync: "待同步",
|
||||
primaryButlerTag: "主责",
|
||||
publicService: "社区设施预约",
|
||||
publicService: "公寓精选服务",
|
||||
refreshFailed: "刷新失败,请稍后重试",
|
||||
refreshStatus: "刷新状态",
|
||||
refreshing: "刷新中...",
|
||||
@@ -1572,6 +1711,7 @@ const messages = {
|
||||
},
|
||||
register: {
|
||||
register: {
|
||||
admissionNoticeAttachmentLabel: "入学通知书(在职证明)附件",
|
||||
accountLabel: "登录账号",
|
||||
accountPlaceholder: "请输入登录账号",
|
||||
accountSection: "账号信息",
|
||||
@@ -1642,6 +1782,7 @@ const messages = {
|
||||
submitRegister: "提交注册申请",
|
||||
submitting: "提交中...",
|
||||
uploadFailed: "上传失败,请重试",
|
||||
uploadAdmissionNotice: "点击上传入学通知书/在职证明",
|
||||
uploadIdBack: "点击上传身份证反面",
|
||||
uploadIdFront: "点击上传身份证正面",
|
||||
uploadImageOption: "上传图片",
|
||||
@@ -1727,6 +1868,7 @@ const messages = {
|
||||
duration5: "1年以上",
|
||||
durationLabel: "预计租住时长",
|
||||
navTitle: "预约现场带看",
|
||||
roomTypeLabel: "意向房型",
|
||||
remarkArea: "意向区域:{area}",
|
||||
remarkBudget: "租金预算:{budget} 元/月",
|
||||
remarkDuration: "预计租住时长:{duration}",
|
||||
@@ -1745,7 +1887,7 @@ const messages = {
|
||||
selectViewingTimeDesc: "请选择您方便的时间段",
|
||||
submitBooking: "提交预约",
|
||||
submitFailed: "预约提交失败",
|
||||
submitSuccessContent: "您的预约看房申请已提交。\n系统会自动转发给对应区域有房间权限的宿管,宿管确认带看后可直接完结订单,或选定房型房号后转为预定单发送给租客支付。",
|
||||
submitSuccessContent: "您的预约看房申请已提交,请等待管家联系",
|
||||
submitSuccessTitle: "预约成功",
|
||||
submitting: "提交中...",
|
||||
today: "今天",
|
||||
@@ -2001,6 +2143,10 @@ const messages = {
|
||||
leaseDurationKey: "预计租住时长",
|
||||
leaseTermLabel: "预计租期",
|
||||
missingBillCode: "申请单编号缺失",
|
||||
missingContractNo: "合同编号缺失",
|
||||
inspectionPendingReviewHint: "验房审批中",
|
||||
checkinPendingHint: "宿管正在办理入住,请耐心等待",
|
||||
refreshAction: "刷新",
|
||||
navTitle: "申请详情",
|
||||
paymentMethodKey: "支付方式",
|
||||
preferredAreaKey: "意向区域",
|
||||
@@ -2353,8 +2499,62 @@ const messages = {
|
||||
unsupportedSwitch: "该设备不支持快捷开关"
|
||||
}
|
||||
},
|
||||
lost_found_publish: {
|
||||
lost_found_publish: {
|
||||
navTitle: "失物拾物",
|
||||
heroBadge: "社区服务",
|
||||
heroTitle: "发布失物或招领信息",
|
||||
heroDesc: "将失物或拾获信息同步给社区,方便后续核实与快速匹配。",
|
||||
formTitle: "物品信息",
|
||||
formDesc: "请补充关键字段,必填项越完整,越方便他人联系与认领。",
|
||||
typeLabel: "发布类型",
|
||||
typePlaceholder: "请选择失物或招领",
|
||||
typeLost: "失物",
|
||||
typeFound: "招领",
|
||||
itemTypeLabel: "物品类型",
|
||||
itemTypePlaceholder: "例如证件、钥匙、电子设备",
|
||||
itemNameLabel: "物品名称",
|
||||
itemNamePlaceholder: "请输入物品名称",
|
||||
placeLabel: "地点",
|
||||
placePlaceholder: "请输入丢失或拾取地点",
|
||||
whenLabel: "时间",
|
||||
selectDate: "选择日期",
|
||||
selectTime: "选择时间",
|
||||
whenTip: "可选,若已知大致时间可一并填写。",
|
||||
contractLabel: "联系人",
|
||||
contractPlaceholder: "请输入联系人",
|
||||
contractPhoneLabel: "联系电话",
|
||||
contractPhonePlaceholder: "请输入联系电话",
|
||||
remarkLabel: "备注",
|
||||
remarkPlaceholder: "可补充外观特征、认领说明或其他辅助信息",
|
||||
imagesLabel: "现场图片",
|
||||
uploadImage: "上传图片",
|
||||
imagesTip: "可选,上传现场图或物品图可提升识别效率。",
|
||||
submitTip: "发布前请确认信息准确,系统会按当前语言提交接口。",
|
||||
submitting: "发布中...",
|
||||
submitButton: "立即发布",
|
||||
uploadFailed: "图片上传失败",
|
||||
uploadOversize: "单张图片不能超过 5 MB",
|
||||
uploadExceed: "最多上传 6 张图片",
|
||||
validateItemType: "请输入物品类型",
|
||||
validateItemName: "请输入物品名称",
|
||||
validatePlace: "请输入地点",
|
||||
validateContract: "请输入联系人",
|
||||
validateContractPhone: "请输入联系电话",
|
||||
submitSuccess: "发布成功",
|
||||
submitFailed: "发布失败"
|
||||
}
|
||||
},
|
||||
submit_request: {
|
||||
components: {
|
||||
BuildingSelector: {
|
||||
emptyText: "暂无可选楼栋",
|
||||
loadFailed: "楼栋列表加载失败",
|
||||
loadingText: "楼栋加载中...",
|
||||
popupTitle: "选择意向楼栋",
|
||||
selectAreaFirst: "请先选择区域",
|
||||
selectBuilding: "请选择楼栋"
|
||||
},
|
||||
DateRangeSelector: {
|
||||
dateDivider: "至",
|
||||
endLabel: "预计离开",
|
||||
@@ -2362,6 +2562,14 @@ const messages = {
|
||||
startLabel: "预计入住",
|
||||
startPlaceholder: "请选择入住日期"
|
||||
},
|
||||
FloorSelector: {
|
||||
clearSelection: "不选择",
|
||||
emptyText: "暂无可选楼层",
|
||||
loadFailed: "楼层列表加载失败",
|
||||
loadingText: "楼层加载中...",
|
||||
popupTitle: "选择意向楼层",
|
||||
selectFloor: "请选择楼层"
|
||||
},
|
||||
TagSelector: {
|
||||
emptyText: "暂无标签",
|
||||
loadingText: "标签加载中..."
|
||||
@@ -2373,12 +2581,14 @@ const messages = {
|
||||
applyRemarkPlaceholder: "请输入您的申请补充说明",
|
||||
applyRoomType: "申请房型",
|
||||
areaLabel: "所属区域",
|
||||
buildingLabel: "意向楼栋",
|
||||
currentRoom: "当前房间",
|
||||
dateCannotBefore: "{endLabel}不能早于{startLabel}",
|
||||
emptyHobbyTags: "暂无兴趣爱好标签",
|
||||
emptyLifestyleTags: "暂无生活习惯标签",
|
||||
expectedCheckin: "预计入住",
|
||||
expectedCheckout: "预计离开",
|
||||
floorLabel: "意向楼层",
|
||||
hobbyLabel: "兴趣爱好",
|
||||
leaseRange: "租期时间",
|
||||
lifestyleLabel: "生活习惯",
|
||||
@@ -2390,6 +2600,10 @@ const messages = {
|
||||
roomTypeApplyBlockTitle: "房型申请信息",
|
||||
roomTypeApplyDesc: "请确认房型、区域、偏好标签和租期信息后提交申请。",
|
||||
roomTypeApplyTitle: "房型申请",
|
||||
switchRoomType: "切换",
|
||||
summaryTitle: "申请摘要",
|
||||
expandSummary: "展开",
|
||||
collapseSummary: "收起",
|
||||
selectCheckinDate: "请选择入住日期",
|
||||
selectCheckoutDate: "请选择离开日期",
|
||||
selectDateEnd: "请选择{label}",
|
||||
|
||||
@@ -51,11 +51,15 @@
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx92fd9a1cd56f1185",
|
||||
"__usePrivacyCheck__" : true,
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
"urlCheck" : false,
|
||||
"minified" : true,
|
||||
"postcss" : true
|
||||
},
|
||||
"usingComponents" : true,
|
||||
"requiredPrivateInfos" : [ "chooseAddress" ]
|
||||
"requiredPrivateInfos" : [ "chooseAddress" ],
|
||||
"permission" : {}
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
|
||||
24
pages.json
24
pages.json
@@ -96,6 +96,12 @@
|
||||
"navigationBarTitleText": "My Contracts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "room_inspection/room_inspection",
|
||||
"style": {
|
||||
"navigationBarTitleText": "Inspection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "contract_detail/contract_detail",
|
||||
"style": {
|
||||
@@ -114,6 +120,24 @@
|
||||
"navigationBarTitleText": "Message Detail"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "ticket_center/ticket_center",
|
||||
"style": {
|
||||
"navigationBarTitleText": "Work Orders"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "ticket_workbench/ticket_workbench",
|
||||
"style": {
|
||||
"navigationBarTitleText": "Work Order"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "lost_found_publish/lost_found_publish",
|
||||
"style": {
|
||||
"navigationBarTitleText": "Lost and Found"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "submit_request/submit_request",
|
||||
"style": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<view class="profile-page tn-safe-area-inset-bottom">
|
||||
|
||||
<!-- 头部背景与用户信息 (悬浮质感设计) -->
|
||||
@@ -574,6 +574,11 @@ export default {
|
||||
url: '/pkgService/utility_recharge/utility_recharge'
|
||||
});
|
||||
},
|
||||
goToTicketCenter() {
|
||||
uni.navigateTo({
|
||||
url: '/pkgService/ticket_center/ticket_center'
|
||||
})
|
||||
},
|
||||
goToSubmitRequest(title, desc, payload = {}, titleZh = '', titleEn = '', descZh = '', descEn = '') {
|
||||
uni.navigateTo({
|
||||
url: `/pkgService/submit_request/submit_request?title=${encodeURIComponent(title || this.t('submitRequest'))}&titleZh=${encodeURIComponent(titleZh || title || this.t('submitRequest'))}&titleEn=${encodeURIComponent(titleEn || title || this.t('submitRequest'))}&desc=${encodeURIComponent(desc || this.t('completeFormBeforeSubmit'))}&descZh=${encodeURIComponent(descZh || desc || this.t('completeFormBeforeSubmit'))}&descEn=${encodeURIComponent(descEn || desc || this.t('completeFormBeforeSubmit'))}&payload=${encodeURIComponent(JSON.stringify(payload || {}))}`
|
||||
@@ -608,7 +613,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (item.key === 'serviceOrder') {
|
||||
this.handleApplicationEntry()
|
||||
this.goToTicketCenter()
|
||||
return
|
||||
}
|
||||
if (item.key === 'registrationRecord') {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<view class="service-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar :isBack="false" :bottomShadow="false" backgroundColor="transparent" :customBackStyle="{ transition: 'all 0.3s ease' }">
|
||||
<tn-nav-bar :isBack="false" :bottomShadow="false" :backgroundColor="navBarBackgroundColor" :customBackStyle="{ transition: 'all 0.3s ease' }">
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('title') }}</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<scroll-view class="service-scroll" scroll-y :style="{paddingTop: vuex_custom_bar_height + 'px'}">
|
||||
<scroll-view class="service-scroll" scroll-y :style="{paddingTop: vuex_custom_bar_height + 'px'}" @scroll="handleScroll">
|
||||
<view v-if="!hasCheckedIn" class="empty-state tn-padding animation-fade-in">
|
||||
<!-- 无预订:去选房 -->
|
||||
<view v-if="!currentBooking" class="no-booking-card">
|
||||
@@ -112,21 +112,6 @@
|
||||
|
||||
<!-- 已入住状态下的服务大厅内容 -->
|
||||
<view v-else class="animation-fade-in tn-padding-bottom">
|
||||
<view class="tn-margin-top tn-margin-left tn-margin-right">
|
||||
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ t('stayService') }}</view>
|
||||
<view class="tn-flex tn-flex-row-between">
|
||||
<view
|
||||
v-for="item in stayActions"
|
||||
:key="item.key"
|
||||
class="stay-action-card"
|
||||
@click="openServicePage(item)"
|
||||
>
|
||||
<view class="stay-action-icon"><text :class="item.icon" class="stay-action-icon-symbol"></text></view>
|
||||
<view class="stay-action-title tn-margin-top-sm">{{ pickItemText(item, 'label', 'labelEn') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 融合房卡与智能控制的新卡片 -->
|
||||
<view class="integrated-room-card tn-margin-left tn-margin-right tn-margin-top">
|
||||
<!-- 房卡背景 -->
|
||||
@@ -181,6 +166,32 @@
|
||||
</view>
|
||||
|
||||
<view class="tn-margin-top tn-margin-left tn-margin-right">
|
||||
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ t('stayService') }}</view>
|
||||
|
||||
<view class="tn-flex tn-flex-wrap tn-flex-row-between">
|
||||
<view class="service-item tn-margin-bottom-sm" v-for="item in stayActions" :key="item.key" @click="openServicePage(item)">
|
||||
<view class="service-card-visual">
|
||||
<image :src="item.image" mode="aspectFill" class="service-card-bg"></image>
|
||||
<view class="service-card-mask"></view>
|
||||
<view class="service-card-content tn-padding-sm tn-flex tn-flex-direction-column tn-flex-row-between" style="height: 100%;">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="service-icon-box tn-flex tn-flex-col-center tn-flex-row-center">
|
||||
<text :class="item.icon" class="service-icon-symbol"></text>
|
||||
</view>
|
||||
<view class="status-tag tn-text-xs" :class="item.isBusy ? 'tag-busy' : 'tag-free'">
|
||||
{{ pickItemText(item, 'statusZh', 'statusEn') }}
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<view class="tn-text-bold tn-text tn-color-white">{{ pickItemText(item, 'label', 'labelEn') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tn-margin-top-sm tn-margin-left tn-margin-right">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-bottom tn-margin-bottom">
|
||||
<view class="tn-text-bold tn-text tn-color-black">{{ t('publicService') }}</view>
|
||||
<view class="tn-text-sm tn-color-gray">{{ t('viewAll') }} <text class="tn-icon-right section-arrow-icon"></text></view>
|
||||
@@ -238,36 +249,38 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 专属管家 (原活动位置) -->
|
||||
<!-- 宿管与管理员列表 -->
|
||||
<view class="butler-card-new tn-margin-top tn-margin-left tn-margin-right">
|
||||
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ t('butler') }}</view>
|
||||
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ t('managementContacts') }}</view>
|
||||
<view class="butler-card-new__inner">
|
||||
<view v-if="areaManagers.length" class="butler-card-new__list">
|
||||
<view v-if="managerContacts.length" class="butler-card-new__list">
|
||||
<view
|
||||
v-for="(manager, index) in areaManagers"
|
||||
:key="manager.userCode || manager.userName || manager.name || index"
|
||||
v-for="(contact, index) in managerContacts"
|
||||
:key="contact.contactKey || index"
|
||||
class="butler-member-new"
|
||||
>
|
||||
<view class="butler-member-new__identity">
|
||||
<view class="butler-avatar-new">
|
||||
<image v-if="getManagerAvatar(manager)" :src="getManagerAvatar(manager)" mode="aspectFill"></image>
|
||||
<image v-if="getManagerAvatar(contact)" :src="getManagerAvatar(contact)" mode="aspectFill"></image>
|
||||
<view v-else class="butler-avatar-new-fallback tn-flex tn-flex-col-center tn-flex-row-center">
|
||||
<text class="butler-avatar-new-fallback-text">{{ getManagerInitial(manager) }}</text>
|
||||
<text class="butler-avatar-new-fallback-text">{{ getManagerInitial(contact) }}</text>
|
||||
</view>
|
||||
<view class="online-dot"></view>
|
||||
</view>
|
||||
<view class="butler-member-new__info">
|
||||
<view class="butler-member-new__name">
|
||||
{{ getManagerName(manager) }}
|
||||
<text v-if="index === 0" class="butler-member-new__tag">{{ t('primaryButlerTag') }}</text>
|
||||
{{ getManagerName(contact) }}
|
||||
<text class="butler-member-new__tag butler-member-new__tag--type" :class="contact.contactType === 'counselor' ? 'tag-counselor' : 'tag-dormitory'">
|
||||
{{ contact.contactType === 'counselor' ? t('counselorTag') : t('dormitoryManagerTag') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="butler-member-new__phone">{{ getManagerPhone(manager) || t('noContactPhone') }}</view>
|
||||
<view class="butler-member-new__phone">{{ getManagerPhone(contact) || t('noContactPhone') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="butler-member-new__action"
|
||||
:class="{ 'disabled': !getManagerPhone(manager) }"
|
||||
@click.stop="handleAreaManagerCall(manager)"
|
||||
:class="{ 'disabled': !getManagerPhone(contact) }"
|
||||
@click.stop="handleAreaManagerCall(contact)"
|
||||
>
|
||||
<text class="tn-icon-phone-fill"></text>
|
||||
</view>
|
||||
@@ -278,12 +291,12 @@
|
||||
<view class="butler-member-new__identity">
|
||||
<view class="butler-avatar-new">
|
||||
<view class="butler-avatar-new-fallback tn-flex tn-flex-col-center tn-flex-row-center">
|
||||
<text class="tn-icon-service-fill" style="font-size: 32rpx; color: #fff;"></text>
|
||||
<text class="tn-icon-my-fill" style="font-size: 32rpx; color: #fff;"></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="butler-member-new__info">
|
||||
<view class="butler-member-new__name">{{ t('butler') }}</view>
|
||||
<view class="butler-member-new__phone">{{ isAreaManagerLoading ? t('loadingButlerInfo') : t('noButlerInfo') }}</view>
|
||||
<view class="butler-member-new__name">{{ t('managementContacts') }}</view>
|
||||
<view class="butler-member-new__phone">{{ isManagementContactsLoading ? t('loadingButlerInfo') : t('noManagerContacts') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -296,7 +309,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { accountQueryBills, facilityManagers, gatewayRoomDeviceList, orderSearchedDetail } from '../../../api/index.js'
|
||||
import { accountQueryBills, facilityManagers, myDormitoryManagers, myCounselor, gatewayRoomDeviceList, orderSearchedDetail } from '../../../api/index.js'
|
||||
import { getSessionSnapshot, saveMyRoomData } from '../../../utils/app-state'
|
||||
import { ACCOUNT_BILL_PAYMENT_STATUS, ACCOUNT_BILL_STATUS, fetchEnums, getAccountReferenceTypeLabel, getApplicationDetailStatusLabel } from '../../../utils/enum-store'
|
||||
import { getBookingStatusText } from '../../../utils/flow'
|
||||
@@ -400,14 +413,62 @@ export default {
|
||||
areaManager: null,
|
||||
areaManagers: [],
|
||||
isAreaManagerLoading: false,
|
||||
// 宿管与管理员联系人
|
||||
dormitoryManagers: [],
|
||||
counselor: null,
|
||||
isManagementContactsLoading: false,
|
||||
// 导航栏滚动背景色
|
||||
navBarBackgroundColor: 'transparent',
|
||||
unpaidBillList: [],
|
||||
smartDevices: [],
|
||||
isSmartDevicesLoading: false,
|
||||
smartDeviceError: '',
|
||||
stayActions: [
|
||||
{ key: 'change-room', label: '换房申请', labelEn: 'Change Room', icon: 'tn-icon-home-smile-fill', title: '换房申请', titleEn: 'Change Room Request', titleEnShort: 'Room Change', desc: '提交换房原因、期望房型和管家审核说明。', descEn: 'Submit your reason, expected room type, and butler review notes.' },
|
||||
{ key: 'renew-stay', label: '续住申请', labelEn: 'Renew Stay', icon: 'tn-icon-calendar-fill', title: '续住申请', titleEn: 'Renew Stay Request', titleEnShort: 'Renew Stay', desc: '提交续住时长、续住原因和新的租期需求。', descEn: 'Submit extension duration, reason, and new lease request.' },
|
||||
{ key: 'checkout', label: '退房申请', labelEn: 'Check Out', icon: 'tn-icon-home-in-fill', title: '退房申请', titleEn: 'Checkout Request', titleEnShort: 'Check Out', desc: '提交退房时间、离场说明和验房申请。', descEn: 'Submit checkout time, leave notes, and inspection request.' }
|
||||
{
|
||||
key: 'change-room',
|
||||
label: '换房申请',
|
||||
labelEn: 'Change Room',
|
||||
icon: 'tn-icon-home-smile-fill',
|
||||
statusZh: '可提交',
|
||||
statusEn: 'Available',
|
||||
isBusy: false,
|
||||
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_11.png',
|
||||
title: '换房申请',
|
||||
titleEn: 'Change Room Request',
|
||||
titleEnShort: 'Room Change',
|
||||
desc: '提交换房原因、期望房型和管家审核说明。',
|
||||
descEn: 'Submit your reason, expected room type, and butler review notes.'
|
||||
},
|
||||
{
|
||||
key: 'renew-stay',
|
||||
label: '续住申请',
|
||||
labelEn: 'Renew Stay',
|
||||
icon: 'tn-icon-calendar-fill',
|
||||
statusZh: '可提交',
|
||||
statusEn: 'Available',
|
||||
isBusy: false,
|
||||
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_16.png',
|
||||
title: '续住申请',
|
||||
titleEn: 'Renew Stay Request',
|
||||
titleEnShort: 'Renew Stay',
|
||||
desc: '提交续住时长、续住原因和新的租期需求。',
|
||||
descEn: 'Submit extension duration, reason, and new lease request.'
|
||||
},
|
||||
{
|
||||
key: 'checkout',
|
||||
label: '退房申请',
|
||||
labelEn: 'Check Out',
|
||||
icon: 'tn-icon-home-in-fill',
|
||||
statusZh: '可提交',
|
||||
statusEn: 'Available',
|
||||
isBusy: false,
|
||||
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_17.png',
|
||||
title: '退房申请',
|
||||
titleEn: 'Checkout Request',
|
||||
titleEnShort: 'Check Out',
|
||||
desc: '提交退房时间、离场说明和验房申请。',
|
||||
descEn: 'Submit checkout time, leave notes, and inspection request.'
|
||||
}
|
||||
],
|
||||
services: [
|
||||
{
|
||||
@@ -440,6 +501,21 @@ export default {
|
||||
desc: '填写备注说明需要几桶水后提交申请。',
|
||||
descEn: 'Submit the quantity of water needed and any notes.'
|
||||
},
|
||||
{
|
||||
key: 'meal-delivery',
|
||||
icon: 'tn-icon-cart-fill',
|
||||
nameZh: '送餐服务',
|
||||
nameEn: 'Food Delivery',
|
||||
statusZh: '可下单',
|
||||
statusEn: 'Order Now',
|
||||
isBusy: false,
|
||||
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_15.png',
|
||||
title: '送餐服务',
|
||||
titleEn: 'Food Delivery',
|
||||
titleEnShort: 'Food',
|
||||
desc: '一键跳转点餐小程序,挑选餐品并下单配送。',
|
||||
descEn: 'Open the ordering mini-program to pick meals and get delivery.'
|
||||
},
|
||||
{
|
||||
key: 'cleaning',
|
||||
icon: 'tn-icon-clean',
|
||||
@@ -503,7 +579,7 @@ export default {
|
||||
butler: '专属社区管家',
|
||||
online: '当前在线 · 响应时间 < 5分钟',
|
||||
callNow: '拨打',
|
||||
publicService: '社区设施预约',
|
||||
publicService: '公寓精选服务',
|
||||
viewAll: '全部',
|
||||
leaseTerm: '租期',
|
||||
activeStatus: '使用中',
|
||||
@@ -525,6 +601,10 @@ export default {
|
||||
noContactPhone: '暂无联系电话',
|
||||
loadingButlerInfo: '正在同步管家信息...',
|
||||
noButlerInfo: '暂未获取到管家信息',
|
||||
managementContacts: '宿管与管理员',
|
||||
dormitoryManagerTag: '宿管',
|
||||
counselorTag: '管理员',
|
||||
noManagerContacts: '暂无管理联系人',
|
||||
managerContactSummary: '当前区域共有 {count} 位管家可联系',
|
||||
contactPhonePrefix: '联系电话 ',
|
||||
loadingShort: '同步中',
|
||||
@@ -583,7 +663,7 @@ export default {
|
||||
butler: 'Community Butler',
|
||||
online: 'Online · Response < 5 mins',
|
||||
callNow: 'Call',
|
||||
publicService: 'Facility Booking',
|
||||
publicService: 'Premium Services',
|
||||
viewAll: 'View All',
|
||||
leaseTerm: 'Lease',
|
||||
activeStatus: 'Active',
|
||||
@@ -605,6 +685,10 @@ export default {
|
||||
noContactPhone: 'No contact number',
|
||||
loadingButlerInfo: 'Syncing butler info...',
|
||||
noButlerInfo: 'No butler information yet',
|
||||
managementContacts: 'Dorm Managers & Admins',
|
||||
dormitoryManagerTag: 'Dorm Mgr',
|
||||
counselorTag: 'Admin',
|
||||
noManagerContacts: 'No management contacts',
|
||||
managerContactSummary: '{count} butlers are available in this area',
|
||||
contactPhonePrefix: 'Phone ',
|
||||
loadingShort: 'Syncing',
|
||||
@@ -884,6 +968,25 @@ export default {
|
||||
butlerPhoneNumber() {
|
||||
const manager = this.primaryAreaManager || {}
|
||||
return this.getManagerPhone(manager)
|
||||
},
|
||||
// 合并宿管列表与管理员(辅导员)为统一联系人列表
|
||||
managerContacts() {
|
||||
const contacts = Array.isArray(this.dormitoryManagers)
|
||||
? this.dormitoryManagers.map((item, index) => ({
|
||||
...this.normalizeManager(item),
|
||||
contactType: 'dormitory-manager',
|
||||
contactKey: String(item.code || item.userCode || item.phone || index)
|
||||
}))
|
||||
: []
|
||||
const counselor = this.counselor
|
||||
if (counselor && typeof counselor === 'object' && (counselor.name || counselor.phone || counselor.code)) {
|
||||
contacts.push({
|
||||
...this.normalizeManager(counselor),
|
||||
contactType: 'counselor',
|
||||
contactKey: String(counselor.code || counselor.userCode || counselor.phone || 'counselor')
|
||||
})
|
||||
}
|
||||
return contacts
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -1123,6 +1226,58 @@ export default {
|
||||
|
||||
return this._loadAreaManagerPromise
|
||||
},
|
||||
// 获取宿管列表与管理员(辅导员)联系人
|
||||
fetchManagementContacts() {
|
||||
if (!this.isCurrentLogin()) {
|
||||
this.dormitoryManagers = []
|
||||
this.counselor = null
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
if (this._fetchManagementContactsPromise) {
|
||||
return this._fetchManagementContactsPromise
|
||||
}
|
||||
|
||||
this.isManagementContactsLoading = true
|
||||
this._fetchManagementContactsPromise = Promise.allSettled([
|
||||
myDormitoryManagers(this.language),
|
||||
myCounselor(this.language)
|
||||
])
|
||||
.then(([dormitoryManagersResult, counselorResult]) => {
|
||||
this.dormitoryManagers = dormitoryManagersResult.status === 'fulfilled' && Array.isArray(dormitoryManagersResult.value && dormitoryManagersResult.value.data)
|
||||
? dormitoryManagersResult.value.data
|
||||
: []
|
||||
this.counselor = counselorResult.status === 'fulfilled' && counselorResult.value && counselorResult.value.data && typeof counselorResult.value.data === 'object'
|
||||
? counselorResult.value.data
|
||||
: null
|
||||
if (dormitoryManagersResult.status === 'rejected') {
|
||||
console.warn('获取宿管联系人失败', dormitoryManagersResult.reason)
|
||||
}
|
||||
if (counselorResult.status === 'rejected') {
|
||||
console.warn('获取管理员信息失败', counselorResult.reason)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.dormitoryManagers = []
|
||||
this.counselor = null
|
||||
console.warn('获取管理联系人失败', error)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isManagementContactsLoading = false
|
||||
this._fetchManagementContactsPromise = null
|
||||
})
|
||||
|
||||
return this._fetchManagementContactsPromise
|
||||
},
|
||||
// 滚动事件处理:顶部导航栏加背景色
|
||||
handleScroll(e) {
|
||||
const scrollTop = (e && e.detail && e.detail.scrollTop) || 0
|
||||
if (scrollTop > 20) {
|
||||
this.navBarBackgroundColor = '#F8FAFF'
|
||||
} else {
|
||||
this.navBarBackgroundColor = 'transparent'
|
||||
}
|
||||
},
|
||||
fetchCurrentOrderDetail() {
|
||||
if (!this.hasCheckedIn) {
|
||||
return Promise.resolve()
|
||||
@@ -1165,10 +1320,14 @@ export default {
|
||||
this.hasCheckedIn = false
|
||||
this.currentBooking = null
|
||||
this.myRoomData = null
|
||||
this.dormitoryManagers = []
|
||||
this.counselor = null
|
||||
return
|
||||
}
|
||||
|
||||
this.currentBooking = storeUser.currentBooking || session.currentBooking || null
|
||||
// 已登录即获取宿管与管理员联系人
|
||||
this.fetchManagementContacts()
|
||||
if (this.getStoreCheckinStatus() || session.hasCheckedIn) {
|
||||
this.hasCheckedIn = true
|
||||
this.myRoomData = buildRoomDataFromOrder(storeCurrentOrder) || buildRoomDataFromOrder(session.myRoomData) || session.myRoomData || null
|
||||
@@ -1294,17 +1453,65 @@ export default {
|
||||
}
|
||||
return `/pkgService/submit_request/submit_request?title=${encodeURIComponent(title || this.t('submitRequest'))}&titleZh=${encodeURIComponent(titleZh)}&titleEn=${encodeURIComponent(titleEn)}&desc=${encodeURIComponent(desc || this.t('completeFormBeforeSubmit'))}&descZh=${encodeURIComponent(descZh)}&descEn=${encodeURIComponent(descEn)}&payload=${encodeURIComponent(JSON.stringify(payload))}`
|
||||
},
|
||||
buildTicketWorkbenchUrl(item = {}) {
|
||||
const serviceTypeMap = {
|
||||
repair: 'TicketRepairApply',
|
||||
complaint: 'TicketSuggestionApply',
|
||||
'change-room': 'TicketChangeApply',
|
||||
checkout: 'TicketCheckOutApply',
|
||||
'renew-stay': 'TicketRenewalApply',
|
||||
'water-delivery': 'TicketWaterDeliveryApply',
|
||||
cleaning: 'TicketCleaningApply'
|
||||
}
|
||||
const serviceType = serviceTypeMap[item.key]
|
||||
if (!serviceType) {
|
||||
return ''
|
||||
}
|
||||
return `/pkgService/ticket_workbench/ticket_workbench?serviceType=${encodeURIComponent(serviceType)}`
|
||||
},
|
||||
openServicePage(item) {
|
||||
if (item.key === 'meal-delivery') {
|
||||
this.openMealDeliveryMiniProgram()
|
||||
return
|
||||
}
|
||||
if (item.key === 'utility') {
|
||||
uni.navigateTo({
|
||||
url: '/pkgService/utility_recharge/utility_recharge'
|
||||
});
|
||||
return
|
||||
}
|
||||
if (item.key === 'lost-found') {
|
||||
uni.navigateTo({
|
||||
url: '/pkgService/lost_found_publish/lost_found_publish'
|
||||
});
|
||||
return
|
||||
}
|
||||
const ticketUrl = this.buildTicketWorkbenchUrl(item)
|
||||
if (ticketUrl) {
|
||||
uni.navigateTo({
|
||||
url: ticketUrl
|
||||
});
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: this.buildServiceSubmitUrl(item)
|
||||
});
|
||||
},
|
||||
// 送餐服务:跳转外部点餐小程序
|
||||
openMealDeliveryMiniProgram() {
|
||||
uni.navigateToMiniProgram({
|
||||
appId: 'wxf221ee13f2abd417',
|
||||
path: 'pages/scan/index?scene=1005&toMenuList=5&entityId=00684887&multiMenuId=006848879dd2e038019e017ba6e330d5',
|
||||
success: () => {},
|
||||
fail: (error) => {
|
||||
console.error('跳转送餐小程序失败', error)
|
||||
uni.showToast({
|
||||
title: this.t('mealDeliveryJumpFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
getManagerName(manager = {}) {
|
||||
return manager.displayName || manager.name || manager.userName || manager.nickname || manager.userCode || manager.phone || ''
|
||||
},
|
||||
@@ -1400,6 +1607,10 @@ export default {
|
||||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
/* 导航栏背景色过渡 */
|
||||
.service-page ::v-deep .tn-custom-nav-bar__bar {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.service-scroll {
|
||||
height: calc(100vh - 100rpx);
|
||||
}
|
||||
@@ -1729,43 +1940,6 @@ export default {
|
||||
box-shadow: 0 6rpx 14rpx rgba($brand-primary, 0.16);
|
||||
}
|
||||
|
||||
.stay-action-card {
|
||||
width: 31%;
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 12rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 10rpx 26rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.stay-action-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, $icon-surface-brand-soft 0%, rgba($brand-primary, 0.04) 100%);
|
||||
color: $icon-tone-brand-deep;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 34rpx;
|
||||
box-shadow: inset 0 0 0 1rpx rgba($brand-primary, 0.08);
|
||||
}
|
||||
|
||||
.stay-action-icon-symbol {
|
||||
font-size: 32rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stay-action-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #222222;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.half-btn {
|
||||
width: 48%;
|
||||
height: 92rpx;
|
||||
@@ -2044,6 +2218,18 @@ export default {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
/* 宿管标签 - 绿色 */
|
||||
.butler-member-new__tag--type.tag-dormitory {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
/* 管理员标签 - 琥珀色 */
|
||||
.butler-member-new__tag--type.tag-counselor {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #F59E0B;
|
||||
}
|
||||
|
||||
.butler-member-new__phone {
|
||||
font-size: 24rpx;
|
||||
color: #888888;
|
||||
|
||||
@@ -12,6 +12,30 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view v-if="showPrivacyPopup" class="privacy-mask">
|
||||
<view class="privacy-dialog">
|
||||
<view class="privacy-title">{{ t('privacyTitle') }}</view>
|
||||
<view class="privacy-content">
|
||||
{{ t('privacyContentPrefix') }}
|
||||
<text class="privacy-link" @click="openPrivacyContract">{{ t('privacyContractName') }}</text>
|
||||
{{ t('privacyContentSuffix') }}
|
||||
</view>
|
||||
<view class="privacy-action-row">
|
||||
<view class="privacy-action-btn" @click="handlePrivacyDisagree">{{ t('privacyDisagree') }}</view>
|
||||
<button
|
||||
id="index-privacy-agree-btn"
|
||||
class="privacy-action-btn is-primary"
|
||||
open-type="agreePrivacyAuthorization"
|
||||
@agreeprivacyauthorization="handleAgreePrivacyAuthorization"
|
||||
>
|
||||
{{ t('privacyAgree') }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 二级页面 -->
|
||||
<view v-if="hasPage('select-room')" v-show="currentPageId === 'select-room'">
|
||||
<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
|
||||
@@ -87,9 +111,24 @@
|
||||
import MyApplicationTab from './component/MyApplicationTab.vue'
|
||||
import ContractAlert from '../../components/contract-alert/contract-alert.vue'
|
||||
import { clearLegacyDemoCache, getHomePageTabId, getSessionSnapshot, saveHomePageTabId } from '../../utils/app-state'
|
||||
import { checkRecordByChecked, notificationPage, applicationFormMePage } from '@/api/index.js'
|
||||
import { applicationFormMePage, checkRecordByChecked, notificationPage, orderSearchedMePage, orderSearchedReservationFlow } from '@/api/index.js'
|
||||
import { parseOrderListResponse, getOrderStatusCode } from '@/utils/booking-helper'
|
||||
import { bindPrivacyAuthorizationListener, getPrivacyAuthorizationStatus, openWechatPrivacyContract, unbindPrivacyAuthorizationListener } from '@/utils/privacy.js'
|
||||
import { countPendingInspectionRecords, extractInspectionList, resolveNotificationUnreadTotal } from '../../utils/message-helper'
|
||||
import { syncLoginStayState } from '../../utils/session-sync'
|
||||
import { getReservationOrderNo, getReservationCurrentNode, parseReservationFlowResponse } from '@/utils/reservation-flow'
|
||||
|
||||
function formatPageResponse(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
return payload && typeof payload === 'object' ? payload : {}
|
||||
}
|
||||
|
||||
function normalizeApplicationList(list = []) {
|
||||
if (!Array.isArray(list)) {
|
||||
return []
|
||||
}
|
||||
return list.filter(item => item && typeof item === 'object')
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -113,6 +152,7 @@
|
||||
hasCheckedIn: false,
|
||||
hasStayOrder: false,
|
||||
hasPendingApplication: false,
|
||||
hasActiveReservationFlow: false,
|
||||
pendingContracts: []
|
||||
},
|
||||
tabDefinitions: [
|
||||
@@ -125,6 +165,9 @@
|
||||
loadedPages: ['select-room'],
|
||||
isAppInitLoading: true,
|
||||
isTabSwitching: false,
|
||||
showPrivacyPopup: false,
|
||||
resolvePrivacyAuthorization: null,
|
||||
isCheckingPrivacyAuthorization: false,
|
||||
i18n: {
|
||||
zh: {
|
||||
loadingBrand: '绿居公寓',
|
||||
@@ -133,7 +176,13 @@
|
||||
tabStayService: '入 住',
|
||||
tabMyApplication: '申请单',
|
||||
tabMessageCenter: '消 息',
|
||||
tabProfileCenter: '我 的'
|
||||
tabProfileCenter: '我 的',
|
||||
privacyTitle: '隐私授权提示',
|
||||
privacyContentPrefix: '进入小程序前,请先阅读并同意',
|
||||
privacyContractName: '《用户隐私保护指引》',
|
||||
privacyContentSuffix: ',同意后才能继续使用相关功能。',
|
||||
privacyAgree: '同意并继续',
|
||||
privacyDisagree: '暂不授权'
|
||||
},
|
||||
en: {
|
||||
loadingBrand: 'Luju Apartments',
|
||||
@@ -142,7 +191,13 @@
|
||||
tabStayService: 'Stay',
|
||||
tabMyApplication: 'Applications',
|
||||
tabMessageCenter: 'Messages',
|
||||
tabProfileCenter: 'Profile'
|
||||
tabProfileCenter: 'Profile',
|
||||
privacyTitle: 'Privacy Authorization',
|
||||
privacyContentPrefix: 'Before entering the mini program, please read and agree to the ',
|
||||
privacyContractName: 'Privacy Protection Guide',
|
||||
privacyContentSuffix: ' to continue using related features.',
|
||||
privacyAgree: 'Agree and Continue',
|
||||
privacyDisagree: 'Not Now'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,12 +215,12 @@
|
||||
const tabs = []
|
||||
if (this.sessionState.hasStayOrder) {
|
||||
tabs.push('stay-service')
|
||||
} else if (this.sessionState.hasPendingApplication) {
|
||||
} else if (this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow) {
|
||||
tabs.push('my-application')
|
||||
} else {
|
||||
tabs.push('select-room')
|
||||
}
|
||||
if (this.sessionState.hasPendingApplication && !tabs.includes('my-application')) {
|
||||
if ((this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow) && !tabs.includes('my-application')) {
|
||||
tabs.push('my-application')
|
||||
}
|
||||
tabs.push('message-center', 'profile-center')
|
||||
@@ -186,14 +241,20 @@
|
||||
},
|
||||
onShow() {
|
||||
this.handlePageShow()
|
||||
this.checkEntryPrivacyAuthorization()
|
||||
},
|
||||
onLoad(options) {
|
||||
clearLegacyDemoCache()
|
||||
uni.$on('messageBadgeCountChange', this.handleMessageBadgeCountChange)
|
||||
uni.$on('reservation-flow-force-refresh', this.handleReservationFlowForceRefresh)
|
||||
this.initPrivacyAuthorization()
|
||||
this.checkEntryPrivacyAuthorization()
|
||||
this.handlePageLoad(options)
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('messageBadgeCountChange', this.handleMessageBadgeCountChange)
|
||||
uni.$off('reservation-flow-force-refresh', this.handleReservationFlowForceRefresh)
|
||||
this.releasePrivacyAuthorization()
|
||||
},
|
||||
onReady() {
|
||||
this.$nextTick(() => {
|
||||
@@ -203,6 +264,76 @@
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pages.index.index', key)
|
||||
},
|
||||
initPrivacyAuthorization() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (typeof wx === 'undefined' || !wx.onNeedPrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
this.handlePrivacyAuthorizationRequest = (resolve) => {
|
||||
this.resolvePrivacyAuthorization = resolve
|
||||
this.showPrivacyPopup = true
|
||||
}
|
||||
bindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
|
||||
// #endif
|
||||
},
|
||||
releasePrivacyAuthorization() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!this.handlePrivacyAuthorizationRequest) {
|
||||
return
|
||||
}
|
||||
unbindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
|
||||
this.handlePrivacyAuthorizationRequest = null
|
||||
this.resolvePrivacyAuthorization = null
|
||||
this.showPrivacyPopup = false
|
||||
// #endif
|
||||
},
|
||||
checkEntryPrivacyAuthorization() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (this.isCheckingPrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
this.isCheckingPrivacyAuthorization = true
|
||||
getPrivacyAuthorizationStatus()
|
||||
.then((res) => {
|
||||
if (!this.resolvePrivacyAuthorization) {
|
||||
this.showPrivacyPopup = !!(res && res.needAuthorization)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.isCheckingPrivacyAuthorization = false
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
openPrivacyContract() {
|
||||
// #ifdef MP-WEIXIN
|
||||
openWechatPrivacyContract().catch((error) => {
|
||||
console.error('open privacy contract failed', error)
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
handleAgreePrivacyAuthorization() {
|
||||
this.showPrivacyPopup = false
|
||||
if (!this.resolvePrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
|
||||
this.resolvePrivacyAuthorization = null
|
||||
resolvePrivacyAuthorization({
|
||||
buttonId: 'index-privacy-agree-btn',
|
||||
event: 'agree'
|
||||
})
|
||||
},
|
||||
handlePrivacyDisagree() {
|
||||
this.showPrivacyPopup = false
|
||||
if (!this.resolvePrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
|
||||
this.resolvePrivacyAuthorization = null
|
||||
resolvePrivacyAuthorization({
|
||||
event: 'disagree'
|
||||
})
|
||||
},
|
||||
getStoreUser() {
|
||||
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
|
||||
@@ -238,6 +369,58 @@
|
||||
console.error('sync login stay state failed', error)
|
||||
})
|
||||
},
|
||||
resolveApplicationState() {
|
||||
return applicationFormMePage({
|
||||
Status: 0,
|
||||
PageIndex: 1,
|
||||
PageSize: 1
|
||||
}, this.$getLanguage())
|
||||
.then((response) => {
|
||||
const pageData = formatPageResponse(response)
|
||||
const list = normalizeApplicationList(pageData.data || [])
|
||||
const total = Number(pageData.total || 0)
|
||||
const hasPendingApplication = list.some(item => Number(item && item.status) === 0)
|
||||
return {
|
||||
hasPendingApplication: total > 0 || hasPendingApplication
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('resolve application state failed', error)
|
||||
return {
|
||||
hasPendingApplication: false
|
||||
}
|
||||
})
|
||||
},
|
||||
resolveApplicationFlowState(list = []) {
|
||||
const orderList = normalizeApplicationList(list)
|
||||
const reservationOrderList = orderList.filter(item => getOrderStatusCode(item) === 0)
|
||||
const orderNos = [...new Set(reservationOrderList.map(item => getReservationOrderNo(item)).filter(Boolean))]
|
||||
|
||||
const result = {
|
||||
hasPendingApplication: false,
|
||||
hasActiveReservationFlow: false
|
||||
}
|
||||
|
||||
if (!orderNos.length) {
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
return orderSearchedReservationFlow({
|
||||
orderNos
|
||||
}, this.$getLanguage())
|
||||
.then((response) => {
|
||||
const flowList = parseReservationFlowResponse(response)
|
||||
const hasActiveReservationFlow = flowList.some(item => !!getReservationCurrentNode(item))
|
||||
return {
|
||||
hasPendingApplication: result.hasPendingApplication || hasActiveReservationFlow,
|
||||
hasActiveReservationFlow
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('resolve application flow state failed', error)
|
||||
return result
|
||||
})
|
||||
},
|
||||
refreshShellState(force = false, options = {}) {
|
||||
const { skipMessageBadge = false } = options
|
||||
const tasks = [
|
||||
@@ -249,18 +432,30 @@
|
||||
}
|
||||
if (this.isCurrentLogin()) {
|
||||
tasks.push(
|
||||
applicationFormMePage({ Status: 0, PageIndex: 1, PageSize: 1 })
|
||||
this.resolveApplicationState()
|
||||
.then((applicationState = {}) => {
|
||||
return orderSearchedMePage({ PageIndex: 1, PageSize: 100, IsExport: false })
|
||||
.then(res => {
|
||||
const payload = res && res.data ? res.data : res
|
||||
const total = payload && payload.total ? Number(payload.total) : (payload && payload.data && payload.data.length ? payload.data.length : 0)
|
||||
this.sessionState.hasPendingApplication = total > 0
|
||||
const orderList = parseOrderListResponse(res)
|
||||
return this.resolveApplicationFlowState(orderList)
|
||||
.then((flowState = {}) => ({
|
||||
hasPendingApplication: !!applicationState.hasPendingApplication,
|
||||
hasActiveReservationFlow: !!flowState.hasActiveReservationFlow
|
||||
}))
|
||||
})
|
||||
})
|
||||
.then((flowState = {}) => {
|
||||
this.sessionState.hasPendingApplication = !!flowState.hasPendingApplication
|
||||
this.sessionState.hasActiveReservationFlow = !!flowState.hasActiveReservationFlow
|
||||
})
|
||||
.catch(() => {
|
||||
this.sessionState.hasPendingApplication = false
|
||||
this.sessionState.hasActiveReservationFlow = false
|
||||
})
|
||||
)
|
||||
} else {
|
||||
this.sessionState.hasPendingApplication = false
|
||||
this.sessionState.hasActiveReservationFlow = false
|
||||
}
|
||||
return Promise.all(tasks).finally(() => {
|
||||
this.refreshSessionState()
|
||||
@@ -314,6 +509,7 @@
|
||||
hasCheckedIn: storeHasCheckedIn || session.hasCheckedIn,
|
||||
hasStayOrder: hasStayOrder || pendingContracts.length > 0,
|
||||
hasPendingApplication: !!this.sessionState.hasPendingApplication,
|
||||
hasActiveReservationFlow: !!this.sessionState.hasActiveReservationFlow,
|
||||
pendingContracts
|
||||
}
|
||||
},
|
||||
@@ -396,6 +592,13 @@
|
||||
const stayServiceTab = list.find(item => item.id === 'stay-service')
|
||||
if (stayServiceTab) return stayServiceTab
|
||||
}
|
||||
const shouldPreferApplicationFlow = this.sessionState.isLogin
|
||||
&& !this.sessionState.hasStayOrder
|
||||
&& (this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow)
|
||||
if (shouldPreferApplicationFlow && (!preferredPageId || preferredPageId === 'select-room')) {
|
||||
const applicationTab = list.find(item => item.id === 'my-application')
|
||||
if (applicationTab) return applicationTab
|
||||
}
|
||||
if (preferredPageId) {
|
||||
const matchedTab = list.find(item => item.id === preferredPageId)
|
||||
if (matchedTab) return matchedTab
|
||||
@@ -438,6 +641,22 @@
|
||||
this.getTabbarItemInfo()
|
||||
})
|
||||
},
|
||||
async handleReservationFlowForceRefresh() {
|
||||
await this.refreshShellState(true)
|
||||
this.ensurePageLoaded('my-application')
|
||||
this.$nextTick(() => {
|
||||
const myApplicationTab = this.getPageRef('myApplicationTab')
|
||||
if (myApplicationTab && typeof myApplicationTab.load === 'function') {
|
||||
myApplicationTab.load(true)
|
||||
} else {
|
||||
this.refreshTabPageState('my-application')
|
||||
}
|
||||
if (this.currentPageId) {
|
||||
this.refreshTabPageState(this.currentPageId)
|
||||
}
|
||||
this.getTabbarItemInfo()
|
||||
})
|
||||
},
|
||||
tabbarPageScrollLower(e) {
|
||||
if (this.currentPageId === 'select-room') {
|
||||
const selectRoomTab = this.getPageRef('selectRoomTab')
|
||||
@@ -588,6 +807,86 @@
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.privacy-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100000;
|
||||
background: rgba(17, 24, 39, 0.56);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.privacy-dialog {
|
||||
width: 100%;
|
||||
max-width: 640rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 28rpx;
|
||||
padding: 40rpx 32rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 24rpx 80rpx rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.privacy-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1A2233;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.privacy-content {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
color: #5C6B8A;
|
||||
}
|
||||
|
||||
.privacy-link {
|
||||
color: $brand-primary;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.privacy-action-row {
|
||||
display: flex;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.privacy-action-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $brand-primary;
|
||||
background: rgba($brand-primary, 0.08);
|
||||
border: 1rpx solid rgba($brand-primary, 0.12);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.privacy-action-btn + .privacy-action-btn {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
button.privacy-action-btn {
|
||||
padding: 0;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
|
||||
button.privacy-action-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.privacy-action-btn.is-primary {
|
||||
color: #FFFFFF;
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.92) 0%, $brand-primary 100%);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,8 @@ import {
|
||||
pickLatestRegistration,
|
||||
resolveLoginPreparation
|
||||
} from '../../utils/login-helper'
|
||||
import { resolveBookingRedirectAfterLogin } from '../../utils/booking-entry'
|
||||
import { syncLoginStayState } from '../../utils/session-sync'
|
||||
import { registrationMyorder, userMe, usersLogin } from '@/api/index.js'
|
||||
|
||||
export default {
|
||||
@@ -161,8 +163,7 @@ export default {
|
||||
isHomePageUrl(url = '') {
|
||||
return /^\/pages\/index\/index(?:\?|$)/.test(String(url || ''))
|
||||
},
|
||||
navigateAfterLogin() {
|
||||
const targetUrl = this.redirectUrl
|
||||
navigateAfterLogin(targetUrl = this.redirectUrl) {
|
||||
const pages = getCurrentPages()
|
||||
if (targetUrl) {
|
||||
if (this.isHomePageUrl(targetUrl)) {
|
||||
@@ -346,6 +347,18 @@ export default {
|
||||
|
||||
saveRegisteredUser(finalUser)
|
||||
startAuthorizedLoginSession(finalUser, token, expires)
|
||||
let nextRedirectUrl = this.redirectUrl
|
||||
|
||||
try {
|
||||
const stayState = await syncLoginStayState({
|
||||
language: this.$getLanguage(),
|
||||
force: true
|
||||
})
|
||||
nextRedirectUrl = resolveBookingRedirectAfterLogin(this.redirectUrl, stayState)
|
||||
} catch (error) {
|
||||
console.error('sync stay state after login failed', error)
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: this.t('loginSuccess'),
|
||||
@@ -353,7 +366,7 @@ export default {
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.isSubmitting = false
|
||||
this.navigateAfterLogin()
|
||||
this.navigateAfterLogin(nextRedirectUrl)
|
||||
}, 600)
|
||||
},
|
||||
handleLogin() {
|
||||
|
||||
@@ -53,10 +53,12 @@
|
||||
<view class="role-switch-wrap">
|
||||
<view class="role-switch tn-flex">
|
||||
<view class="role-tab tn-flex-1" :class="form.identityType === 'ForeignStudents' ? 'role-active' : ''" @click="switchIdentityType('ForeignStudents')">
|
||||
{{ t('foreignIdentity') }}
|
||||
<view>{{ t('foreignIdentity') }}</view>
|
||||
<view v-if="language !== 'en'" class="role-tab-subtitle">{{ t('foreignIdentityHint') }}</view>
|
||||
</view>
|
||||
<view class="role-tab tn-flex-1" :class="form.identityType === 'DomesticStudents' ? 'role-active' : ''" @click="switchIdentityType('DomesticStudents')">
|
||||
{{ t('domesticIdentity') }}
|
||||
<view>{{ t('domesticIdentity') }}</view>
|
||||
<view v-if="language !== 'en'" class="role-tab-subtitle">{{ t('domesticIdentityHint') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -112,6 +114,36 @@
|
||||
<input class="form-input" :placeholder="t('workUnitPlaceholder')" placeholder-class="input-placeholder" v-model="form.workUnit" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">{{ t('admissionNoticeAttachmentLabel') }}</view>
|
||||
<view v-if="!form.admissionNoticeAttachment.serverValue" class="upload-box" @click="uploadAttachment('admissionNoticeAttachment')">
|
||||
<view class="upload-placeholder">
|
||||
<view class="upload-icon-wrap"><text class="tn-icon-upload"></text></view>
|
||||
<view class="tn-margin-top-sm upload-text">{{ t('uploadAdmissionNotice') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="attachment-file-card">
|
||||
<image
|
||||
class="attachment-preview-image"
|
||||
:src="getAttachmentPreviewUrl(form.admissionNoticeAttachment)"
|
||||
mode="aspectFill"
|
||||
@click.stop="previewAttachment('admissionNoticeAttachment')"
|
||||
/>
|
||||
<view class="attachment-file-info">
|
||||
<view class="attachment-name">{{ form.admissionNoticeAttachment.name }}</view>
|
||||
<view class="attachment-type tn-margin-top-xs">{{ t('imageUploadedTip') }}</view>
|
||||
</view>
|
||||
<view class="attachment-action-row">
|
||||
<view class="attachment-action-btn" @click.stop="previewAttachment('admissionNoticeAttachment')">
|
||||
{{ t('viewImage') }}
|
||||
</view>
|
||||
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('admissionNoticeAttachment')">
|
||||
{{ t('reupload') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">{{ t('hangzhouPropertyLabel') }} <text class="required-mark">*</text></view>
|
||||
<view class="tn-flex tn-flex-wrap">
|
||||
@@ -120,6 +152,20 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">{{ t('idCardTypeLabel') }} <text class="required-mark">*</text></view>
|
||||
<view v-if="isDomesticIdentity" class="form-input picker-input picker-input-disabled">
|
||||
<text class="text-black">{{ selectedIdCardTypeLabel || (language === 'en' ? 'ID Card' : '身份证') }}</text>
|
||||
<text class="picker-fixed-text">{{ t('idCardTypeFixedText') }}</text>
|
||||
</view>
|
||||
<picker v-else mode="selector" :range="idCardTypeRange" @change="onIdCardTypeChange">
|
||||
<view class="form-input picker-input">
|
||||
<text :class="selectedIdCardTypeLabel ? 'text-black' : 'text-gray'">{{ selectedIdCardTypeLabel || t('idCardTypePlaceholder') }}</text>
|
||||
<text class="tn-icon-right tn-color-gray"></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<template v-if="form.identityType === 'DomesticStudents'">
|
||||
<view class="form-item">
|
||||
<view class="form-label">{{ t('idCardLabel') }} <text class="required-mark">*</text></view>
|
||||
@@ -189,12 +235,12 @@
|
||||
|
||||
<template v-else>
|
||||
<view class="form-item">
|
||||
<view class="form-label">{{ t('passportLabel') }} <text class="required-mark">*</text></view>
|
||||
<input class="form-input" :placeholder="t('passportPlaceholder')" placeholder-class="input-placeholder" v-model="form.passportNumber" />
|
||||
<view class="form-label">{{ foreignDocumentNumberLabel }} <text class="required-mark">*</text></view>
|
||||
<input class="form-input" :placeholder="foreignDocumentNumberPlaceholder" placeholder-class="input-placeholder" v-model="form.passportNumber" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">{{ t('passportExpiryLabel') }} <text class="required-mark">*</text></view>
|
||||
<view class="form-label">{{ foreignDocumentExpiryLabel }} <text class="required-mark">*</text></view>
|
||||
<picker mode="date" @change="onPassportDateChange">
|
||||
<view class="form-input picker-input">
|
||||
<text :class="form.passportExpiry ? 'text-black' : 'text-gray'">{{ form.passportExpiry || t('selectDate') }}</text>
|
||||
@@ -209,11 +255,11 @@
|
||||
</view>
|
||||
|
||||
<view class="form-item" style="margin-bottom: 0;">
|
||||
<view class="form-label">{{ t('passportAttachmentLabel') }} <text class="required-mark">*</text></view>
|
||||
<view class="form-label">{{ foreignDocumentAttachmentLabel }} <text class="required-mark">*</text></view>
|
||||
<view v-if="!form.passportAttachment.serverValue" class="upload-box" @click="uploadAttachment('passportAttachment')">
|
||||
<view v-if="!form.passportAttachment.serverValue" class="upload-placeholder">
|
||||
<view class="upload-icon-wrap"><text class="tn-icon-upload"></text></view>
|
||||
<view class="tn-margin-top-sm upload-text">{{ t('uploadPassport') }}</view>
|
||||
<view class="tn-margin-top-sm upload-text">{{ foreignDocumentUploadText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="attachment-file-card">
|
||||
@@ -230,7 +276,7 @@
|
||||
<view class="attachment-file-info">
|
||||
<view class="attachment-name">{{ form.passportAttachment.name }}</view>
|
||||
<view class="attachment-type tn-margin-top-xs">
|
||||
{{ isImageAttachment(form.passportAttachment) ? t('imageUploadedTip') : t('passportImageOnlyTip') }}
|
||||
{{ isImageAttachment(form.passportAttachment) ? t('imageUploadedTip') : foreignDocumentImageOnlyTip }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="attachment-action-row">
|
||||
@@ -308,6 +354,30 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view v-if="showPrivacyPopup" class="privacy-mask">
|
||||
<view class="privacy-dialog">
|
||||
<view class="privacy-title">{{ t('privacyTitle') }}</view>
|
||||
<view class="privacy-content">
|
||||
{{ t('privacyContentPrefix') }}
|
||||
<text class="privacy-link" @click="openPrivacyContract">{{ t('privacyContractName') }}</text>
|
||||
{{ t('privacyContentSuffix') }}
|
||||
</view>
|
||||
<view class="privacy-action-row">
|
||||
<view class="privacy-action-btn" @click="handlePrivacyDisagree">{{ t('privacyDisagree') }}</view>
|
||||
<button
|
||||
id="register-privacy-agree-btn"
|
||||
class="privacy-action-btn is-primary"
|
||||
open-type="agreePrivacyAuthorization"
|
||||
@agreeprivacyauthorization="handleAgreePrivacyAuthorization"
|
||||
>
|
||||
{{ t('privacyAgree') }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -320,7 +390,8 @@ import {
|
||||
} from '../../utils/app-state'
|
||||
import { getCountryList, submitRegistrationApplication, sysdictPage, uploadCommonFile, registrationMyorder } from '@/api/index.js'
|
||||
import { addFilePrefix } from '@/utils/common.js'
|
||||
import { getGenderLabel } from '../../utils/enum-store'
|
||||
import { bindPrivacyAuthorizationListener, openWechatPrivacyContract, unbindPrivacyAuthorizationListener } from '@/utils/privacy.js'
|
||||
import { fetchEnums, getEnumOptions, getGenderLabel } from '../../utils/enum-store'
|
||||
import { resolveRegistrationApplicationCode } from '../../utils/flow'
|
||||
|
||||
function createAttachmentState() {
|
||||
@@ -342,6 +413,7 @@ function createDefaultForm() {
|
||||
gender: 1,
|
||||
phone: '',
|
||||
country: '',
|
||||
idCardType: 2,
|
||||
idCard: '',
|
||||
workUnit: '',
|
||||
hasHangzhouProperty: null,
|
||||
@@ -351,6 +423,7 @@ function createDefaultForm() {
|
||||
passportExpiry: '',
|
||||
school: '',
|
||||
religion: '',
|
||||
admissionNoticeAttachment: createAttachmentState(),
|
||||
passportAttachment: createAttachmentState(),
|
||||
idCardFrontAttachment: createAttachmentState(),
|
||||
idCardBackAttachment: createAttachmentState()
|
||||
@@ -374,7 +447,9 @@ function normalizeRegistrationDraftForm(draft) {
|
||||
...createDefaultForm(),
|
||||
...source,
|
||||
gender: source.gender === 0 || source.gender === 1 ? Number(source.gender) : 1,
|
||||
idCardType: Number(source.idCardType || source.IdCardType || 0) || 2,
|
||||
hasHangzhouProperty: typeof source.hasHangzhouProperty === 'boolean' ? source.hasHangzhouProperty : null,
|
||||
admissionNoticeAttachment: normalizeAttachmentDraft(source.admissionNoticeAttachment),
|
||||
passportAttachment: normalizeAttachmentDraft(source.passportAttachment),
|
||||
idCardFrontAttachment: normalizeAttachmentDraft(source.idCardFrontAttachment),
|
||||
idCardBackAttachment: normalizeAttachmentDraft(source.idCardBackAttachment)
|
||||
@@ -391,21 +466,27 @@ export default {
|
||||
draftPersistTimer: null,
|
||||
draftTipText: '',
|
||||
lastForeignCountry: '',
|
||||
lastForeignIdCardType: 2,
|
||||
showPrivacyPopup: false,
|
||||
resolvePrivacyAuthorization: null,
|
||||
form: createDefaultForm(),
|
||||
rawCountryList: [],
|
||||
rawHobbyList: [],
|
||||
rawDietaryHabitList: [],
|
||||
rawReligionList: [],
|
||||
rawIdCardTypeList: [],
|
||||
countryOptions: [],
|
||||
hobbyOptions: [],
|
||||
dietaryHabitOptions: [],
|
||||
religionOptions: [],
|
||||
idCardTypeOptions: [],
|
||||
i18n: {
|
||||
zh: {
|
||||
navTitle: '注册账号',
|
||||
pageTitle: '注册并提交实名资料',
|
||||
pageDesc: '账号信息和实名认证资料需要一起填写完成,提交的是注册申请单,审批通过后才算注册成功。',
|
||||
accountSection: '账号信息',
|
||||
admissionNoticeAttachmentLabel: '入学通知书(在职证明)附件',
|
||||
accountLabel: '登录账号',
|
||||
accountPlaceholder: '请输入登录账号',
|
||||
passwordLabel: '登录密码',
|
||||
@@ -416,7 +497,9 @@ export default {
|
||||
emailCodePlaceholder: '请输入邮箱验证码',
|
||||
basicSection: '基础实名信息',
|
||||
foreignIdentity: '外籍人员',
|
||||
foreignIdentityHint: 'Foreign',
|
||||
domesticIdentity: '中国籍',
|
||||
domesticIdentityHint: 'Chinese',
|
||||
nameLabel: '姓名',
|
||||
namePlaceholder: '请输入真实姓名',
|
||||
genderLabel: '性别',
|
||||
@@ -434,6 +517,9 @@ export default {
|
||||
hangzhouPropertyLabel: '杭州有无房产',
|
||||
yesText: '有',
|
||||
noText: '无',
|
||||
idCardTypeLabel: '证件类型',
|
||||
idCardTypePlaceholder: '请选择证件类型',
|
||||
idCardTypeFixedText: '中国籍固定',
|
||||
idCardLabel: '身份证号',
|
||||
idCardPlaceholder: '请输入身份证号',
|
||||
idCardFrontAttachmentLabel: '身份证正面附件',
|
||||
@@ -446,12 +532,18 @@ export default {
|
||||
passportLabel: '护照号',
|
||||
passportPlaceholder: '请输入护照号',
|
||||
passportExpiryLabel: '护照到期时间',
|
||||
documentNumberLabel: '证件号',
|
||||
documentNumberPlaceholder: '请输入证件号',
|
||||
documentExpiryLabel: '证件到期时间',
|
||||
selectDate: '请选择日期',
|
||||
schoolLabel: '就读学校',
|
||||
schoolPlaceholder: '请输入就读学校(选填)',
|
||||
passportAttachmentLabel: '护照附件',
|
||||
documentAttachmentLabel: '证件附件',
|
||||
uploadPassport: '点击上传护照图片',
|
||||
uploadDocument: '点击上传证件图片',
|
||||
passportImageOnlyTip: '护照附件仅支持图片,请重新上传',
|
||||
documentImageOnlyTip: '证件附件仅支持图片,请重新上传',
|
||||
preferenceSection: '偏好信息',
|
||||
dietaryLabel: '饮食习惯',
|
||||
hobbyLabel: '兴趣爱好',
|
||||
@@ -478,20 +570,32 @@ export default {
|
||||
uploading: '上传中...',
|
||||
uploadSuccess: '上传成功',
|
||||
uploadFailed: '上传失败,请重试',
|
||||
privacyTitle: '隐私授权提示',
|
||||
privacyContentPrefix: '上传证件图片前,请先阅读并同意',
|
||||
privacyContractName: '《用户隐私保护指引》',
|
||||
privacyContentSuffix: ',同意后才能继续选择图片。',
|
||||
privacyAgree: '同意并继续',
|
||||
privacyDisagree: '暂不授权',
|
||||
privacyAuthorizeFailed: '未完成隐私授权,暂时无法选择图片',
|
||||
attachmentNotImage: '当前附件不是图片',
|
||||
uploadImageOption: '上传图片',
|
||||
submitPending: '申请已提交,待审批',
|
||||
submitFailed: '提交失败,请稍后重试',
|
||||
draftTipDefault: '未完成内容会自动保存,可随时返回继续填写',
|
||||
draftTipSaved: '当前填写内容已自动保存',
|
||||
uploadAdmissionNotice: '点击上传入学通知书/在职证明',
|
||||
validateRequiredBasic: '请先填写账号、密码、邮箱和姓名',
|
||||
validatePasswordLength: '登录密码至少 6 位',
|
||||
validateEmail: '请输入正确的邮箱地址',
|
||||
validateIdentityBasic: '请完善性别、手机号和国籍',
|
||||
validatePhone: '请输入正确的手机号',
|
||||
validateProfile: '请完善工作单位和杭州房产情况',
|
||||
validateIdCardType: '请选择证件类型',
|
||||
validateIdCard: '请填写身份证号',
|
||||
validateIdCardAttachments: '请上传身份证正反面附件',
|
||||
validateDocumentBasic: '请填写证件号和证件到期时间',
|
||||
validateDocumentAttachment: '请上传证件图片',
|
||||
validateDocumentAttachmentImage: '证件附件仅支持图片格式',
|
||||
validatePassportBasic: '请填写护照号和护照到期时间',
|
||||
validatePassportAttachment: '请上传护照图片',
|
||||
validatePassportAttachmentImage: '护照附件仅支持图片格式'
|
||||
@@ -501,6 +605,7 @@ export default {
|
||||
pageTitle: 'Register and Submit Verification',
|
||||
pageDesc: 'Account information and identity verification materials must be completed together. Registration succeeds only after approval.',
|
||||
accountSection: 'Account Information',
|
||||
admissionNoticeAttachmentLabel: 'Admission Notice (Employment Certificate)',
|
||||
accountLabel: 'Account',
|
||||
accountPlaceholder: 'Enter account name',
|
||||
passwordLabel: 'Password',
|
||||
@@ -511,7 +616,9 @@ export default {
|
||||
emailCodePlaceholder: 'Enter email verification code',
|
||||
basicSection: 'Basic Verification',
|
||||
foreignIdentity: 'Foreign',
|
||||
foreignIdentityHint: 'Foreign',
|
||||
domesticIdentity: 'Chinese',
|
||||
domesticIdentityHint: 'Chinese',
|
||||
nameLabel: 'Name',
|
||||
namePlaceholder: 'Enter your real name',
|
||||
genderLabel: 'Gender',
|
||||
@@ -529,6 +636,9 @@ export default {
|
||||
hangzhouPropertyLabel: 'Property in Hangzhou',
|
||||
yesText: 'Yes',
|
||||
noText: 'No',
|
||||
idCardTypeLabel: 'Document Type',
|
||||
idCardTypePlaceholder: 'Select document type',
|
||||
idCardTypeFixedText: 'Fixed for Chinese users',
|
||||
idCardLabel: 'ID Card Number',
|
||||
idCardPlaceholder: 'Enter ID card number',
|
||||
idCardFrontAttachmentLabel: 'ID Card Front',
|
||||
@@ -541,12 +651,18 @@ export default {
|
||||
passportLabel: 'Passport Number',
|
||||
passportPlaceholder: 'Enter passport number',
|
||||
passportExpiryLabel: 'Passport Expiry Date',
|
||||
documentNumberLabel: 'Document Number',
|
||||
documentNumberPlaceholder: 'Enter document number',
|
||||
documentExpiryLabel: 'Document Expiry Date',
|
||||
selectDate: 'Select date',
|
||||
schoolLabel: 'School',
|
||||
schoolPlaceholder: 'Enter school (optional)',
|
||||
passportAttachmentLabel: 'Passport Attachment',
|
||||
documentAttachmentLabel: 'Document Attachment',
|
||||
uploadPassport: 'Upload passport image',
|
||||
uploadDocument: 'Upload document image',
|
||||
passportImageOnlyTip: 'Passport attachment only supports images, please re-upload',
|
||||
documentImageOnlyTip: 'Document attachment only supports images, please re-upload',
|
||||
preferenceSection: 'Preferences',
|
||||
dietaryLabel: 'Dietary Habit',
|
||||
hobbyLabel: 'Hobby',
|
||||
@@ -573,20 +689,32 @@ export default {
|
||||
uploading: 'Uploading...',
|
||||
uploadSuccess: 'Upload successful',
|
||||
uploadFailed: 'Upload failed, please retry',
|
||||
privacyTitle: 'Privacy Authorization',
|
||||
privacyContentPrefix: 'Before uploading document images, please read and agree to the ',
|
||||
privacyContractName: 'Privacy Protection Guide',
|
||||
privacyContentSuffix: ' to continue selecting images.',
|
||||
privacyAgree: 'Agree and Continue',
|
||||
privacyDisagree: 'Not Now',
|
||||
privacyAuthorizeFailed: 'Privacy authorization is required before selecting images',
|
||||
attachmentNotImage: 'The current attachment is not an image',
|
||||
uploadImageOption: 'Upload Image',
|
||||
submitPending: 'Application submitted, pending approval',
|
||||
submitFailed: 'Submission failed, please try again later',
|
||||
draftTipDefault: 'Unfinished content is auto-saved and can be continued later',
|
||||
draftTipSaved: 'Current content has been auto-saved',
|
||||
uploadAdmissionNotice: 'Upload admission notice or employment certificate',
|
||||
validateRequiredBasic: 'Please complete the account, password, email, and name fields first',
|
||||
validatePasswordLength: 'Password must be at least 6 characters',
|
||||
validateEmail: 'Please enter a valid email address',
|
||||
validateIdentityBasic: 'Please complete gender, phone number, and country',
|
||||
validatePhone: 'Please enter a valid phone number',
|
||||
validateProfile: 'Please complete work unit and property status in Hangzhou',
|
||||
validateIdCardType: 'Please select a document type',
|
||||
validateIdCard: 'Please enter your ID card number',
|
||||
validateIdCardAttachments: 'Please upload both sides of the ID card',
|
||||
validateDocumentBasic: 'Please enter the document number and expiry date',
|
||||
validateDocumentAttachment: 'Please upload the document image',
|
||||
validateDocumentAttachmentImage: 'Document attachment only supports image files',
|
||||
validatePassportBasic: 'Please enter passport number and expiry date',
|
||||
validatePassportAttachment: 'Please upload the passport image',
|
||||
validatePassportAttachmentImage: 'Passport attachment only supports image files'
|
||||
@@ -607,6 +735,9 @@ export default {
|
||||
countryRange() {
|
||||
return this.countryOptions.length ? this.countryOptions.map(item => item.value) : [this.t('noSelectableOptions')]
|
||||
},
|
||||
idCardTypeRange() {
|
||||
return this.idCardTypeOptions.length ? this.idCardTypeOptions.map(item => item.value) : [this.t('noSelectableOptions')]
|
||||
},
|
||||
dietaryHabitRange() {
|
||||
return this.dietaryHabitOptions.length ? this.dietaryHabitOptions.map(item => item.value) : [this.t('noSelectableOptions')]
|
||||
},
|
||||
@@ -616,6 +747,31 @@ export default {
|
||||
religionRange() {
|
||||
return this.religionOptions.length ? this.religionOptions.map(item => item.value) : [this.t('noSelectableOptions')]
|
||||
},
|
||||
selectedIdCardTypeLabel() {
|
||||
const matched = this.idCardTypeOptions.find(item => String(item.key) === String(this.form.idCardType))
|
||||
return matched ? matched.value : ''
|
||||
},
|
||||
isPassportType() {
|
||||
return String(this.form.idCardType) === '2'
|
||||
},
|
||||
foreignDocumentNumberLabel() {
|
||||
return this.isPassportType ? this.t('passportLabel') : this.t('documentNumberLabel')
|
||||
},
|
||||
foreignDocumentNumberPlaceholder() {
|
||||
return this.isPassportType ? this.t('passportPlaceholder') : this.t('documentNumberPlaceholder')
|
||||
},
|
||||
foreignDocumentExpiryLabel() {
|
||||
return this.isPassportType ? this.t('passportExpiryLabel') : this.t('documentExpiryLabel')
|
||||
},
|
||||
foreignDocumentAttachmentLabel() {
|
||||
return this.isPassportType ? this.t('passportAttachmentLabel') : this.t('documentAttachmentLabel')
|
||||
},
|
||||
foreignDocumentUploadText() {
|
||||
return this.isPassportType ? this.t('uploadPassport') : this.t('uploadDocument')
|
||||
},
|
||||
foreignDocumentImageOnlyTip() {
|
||||
return this.isPassportType ? this.t('passportImageOnlyTip') : this.t('documentImageOnlyTip')
|
||||
},
|
||||
genderOptions() {
|
||||
return [
|
||||
{ key: 1, value: this.t('genderMale') },
|
||||
@@ -637,6 +793,7 @@ export default {
|
||||
},
|
||||
async onLoad(options) {
|
||||
this.draftTipText = this.t('draftTipDefault')
|
||||
this.initPrivacyAuthorization()
|
||||
const bypass = options && options.bypass === '1'
|
||||
const hasRedirected = await this.checkPendingRegistration(bypass)
|
||||
if (hasRedirected) return
|
||||
@@ -645,7 +802,6 @@ export default {
|
||||
this.loadDictOptions(),
|
||||
this.loadCountryOptions()
|
||||
])
|
||||
|
||||
this.tryRestoreFailedDraft()
|
||||
},
|
||||
onHide() {
|
||||
@@ -654,6 +810,8 @@ export default {
|
||||
onUnload() {
|
||||
this.persistRegistrationDraft()
|
||||
this.clearDraftPersistTimer()
|
||||
this.resolvePrivacyAuthorization = null
|
||||
this.releasePrivacyAuthorization()
|
||||
},
|
||||
onPageScroll(e) {
|
||||
let opacity = e.scrollTop / 80
|
||||
@@ -733,6 +891,10 @@ export default {
|
||||
if (!this.isDomesticIdentity && this.form.country && !this.isChinaCountry(this.form.country)) {
|
||||
this.lastForeignCountry = this.form.country
|
||||
}
|
||||
if (!this.isDomesticIdentity && String(this.form.idCardType || '') !== '1') {
|
||||
this.lastForeignIdCardType = Number(this.form.idCardType) || 2
|
||||
}
|
||||
this.applyIdentityDocumentTypeRule()
|
||||
this.applyIdentityCountryRule()
|
||||
uni.showToast({
|
||||
title: this.t('restoreDraftSuccess'),
|
||||
@@ -760,6 +922,7 @@ export default {
|
||||
},
|
||||
hasDraftContent() {
|
||||
const attachmentFields = [
|
||||
'admissionNoticeAttachment',
|
||||
'passportAttachment',
|
||||
'idCardFrontAttachment',
|
||||
'idCardBackAttachment'
|
||||
@@ -820,6 +983,7 @@ export default {
|
||||
this.lastForeignCountry = this.form.country
|
||||
}
|
||||
this.form.identityType = type
|
||||
this.applyIdentityDocumentTypeRule()
|
||||
this.applyIdentityCountryRule()
|
||||
},
|
||||
onPassportDateChange(e) {
|
||||
@@ -846,15 +1010,19 @@ export default {
|
||||
return Promise.all([
|
||||
sysdictPage({ Type: 'UserHobbiesAndInterestsTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage()),
|
||||
sysdictPage({ Type: 'UserDietaryHabitsTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage()),
|
||||
sysdictPage({ Type: 'UserReligiousBeliefTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage())
|
||||
sysdictPage({ Type: 'UserReligiousBeliefTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage()),
|
||||
fetchEnums(false, this.$getLanguage())
|
||||
])
|
||||
.then(([hobbyRes, dietaryRes, religionRes]) => {
|
||||
this.rawHobbyList = this.extractResponseList(hobbyRes)
|
||||
this.rawDietaryHabitList = this.extractResponseList(dietaryRes)
|
||||
this.rawReligionList = this.extractResponseList(religionRes)
|
||||
this.rawIdCardTypeList = getEnumOptions('IdCardTypeEnum', this.$getLanguage())
|
||||
this.hobbyOptions = this.normalizeDictList(this.rawHobbyList)
|
||||
this.dietaryHabitOptions = this.normalizeDictList(this.rawDietaryHabitList)
|
||||
this.religionOptions = this.normalizeDictList(this.rawReligionList)
|
||||
this.idCardTypeOptions = this.normalizeDictList(this.rawIdCardTypeList)
|
||||
this.applyIdentityDocumentTypeRule()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('load dict options failed', error)
|
||||
@@ -933,13 +1101,27 @@ export default {
|
||||
this.hobbyOptions = this.normalizeDictList(this.rawHobbyList)
|
||||
this.dietaryHabitOptions = this.normalizeDictList(this.rawDietaryHabitList)
|
||||
this.religionOptions = this.normalizeDictList(this.rawReligionList)
|
||||
this.idCardTypeOptions = this.normalizeDictList(this.rawIdCardTypeList)
|
||||
this.form.country = this.remapSelectedValue(this.form.country, this.rawCountryList)
|
||||
this.lastForeignCountry = this.remapSelectedValue(this.lastForeignCountry, this.rawCountryList)
|
||||
this.form.hobby = this.remapSelectedValue(this.form.hobby, this.rawHobbyList)
|
||||
this.form.dietaryHabit = this.remapSelectedValue(this.form.dietaryHabit, this.rawDietaryHabitList)
|
||||
this.form.religion = this.remapSelectedValue(this.form.religion, this.rawReligionList)
|
||||
this.applyIdentityDocumentTypeRule()
|
||||
this.applyIdentityCountryRule()
|
||||
},
|
||||
applyIdentityDocumentTypeRule() {
|
||||
if (this.isDomesticIdentity) {
|
||||
if (String(this.form.idCardType || '') !== '1' && String(this.form.idCardType || '')) {
|
||||
this.lastForeignIdCardType = Number(this.form.idCardType) || 2
|
||||
}
|
||||
this.form.idCardType = 1
|
||||
return
|
||||
}
|
||||
if (!this.form.idCardType || String(this.form.idCardType) === '1') {
|
||||
this.form.idCardType = this.lastForeignIdCardType || 2
|
||||
}
|
||||
},
|
||||
getChinaCountryOption() {
|
||||
return this.countryOptions.find(item => String(item.key || '').toUpperCase() === 'CHN' || this.isChinaCountry(item.value)) || null
|
||||
},
|
||||
@@ -970,6 +1152,21 @@ export default {
|
||||
const selected = options[event.detail.value]
|
||||
this.form[field] = selected ? selected.value : ''
|
||||
},
|
||||
onIdCardTypeChange(event) {
|
||||
if (!this.idCardTypeOptions.length) {
|
||||
uni.showToast({
|
||||
title: this.t('noSelectableOptions'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
const selected = this.idCardTypeOptions[event.detail.value]
|
||||
const nextValue = selected ? Number(selected.key) : 0
|
||||
this.form.idCardType = nextValue || this.form.idCardType
|
||||
if (!this.isDomesticIdentity && nextValue && nextValue !== 1) {
|
||||
this.lastForeignIdCardType = nextValue
|
||||
}
|
||||
},
|
||||
onCountryChange(event) {
|
||||
if (this.isDomesticIdentity) return
|
||||
if (!this.countryOptions.length) {
|
||||
@@ -983,8 +1180,106 @@ export default {
|
||||
this.form.country = selected ? selected.value : ''
|
||||
this.lastForeignCountry = this.form.country || ''
|
||||
},
|
||||
initPrivacyAuthorization() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (typeof wx === 'undefined' || !wx.onNeedPrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
this.handlePrivacyAuthorizationRequest = (resolve) => {
|
||||
this.resolvePrivacyAuthorization = resolve
|
||||
this.showPrivacyPopup = true
|
||||
}
|
||||
bindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
|
||||
// #endif
|
||||
},
|
||||
releasePrivacyAuthorization() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!this.handlePrivacyAuthorizationRequest) {
|
||||
return
|
||||
}
|
||||
unbindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
|
||||
this.handlePrivacyAuthorizationRequest = null
|
||||
this.showPrivacyPopup = false
|
||||
// #endif
|
||||
},
|
||||
ensurePrivacyAuthorized() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (typeof wx === 'undefined' || !wx.requirePrivacyAuthorize) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const triggerPrivacyAuthorize = () => {
|
||||
wx.requirePrivacyAuthorize({
|
||||
success: () => {
|
||||
this.showPrivacyPopup = false
|
||||
resolve()
|
||||
},
|
||||
fail: (error) => {
|
||||
reject({
|
||||
...error,
|
||||
privacyDenied: true
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!wx.getPrivacySetting) {
|
||||
triggerPrivacyAuthorize()
|
||||
return
|
||||
}
|
||||
|
||||
wx.getPrivacySetting({
|
||||
success: (res) => {
|
||||
if (!res.needAuthorization) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
triggerPrivacyAuthorize()
|
||||
},
|
||||
fail: () => {
|
||||
triggerPrivacyAuthorize()
|
||||
}
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
return Promise.resolve()
|
||||
// #endif
|
||||
},
|
||||
openPrivacyContract() {
|
||||
// #ifdef MP-WEIXIN
|
||||
openWechatPrivacyContract().catch((error) => {
|
||||
console.error('open privacy contract failed', error)
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
handleAgreePrivacyAuthorization() {
|
||||
this.showPrivacyPopup = false
|
||||
if (!this.resolvePrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
|
||||
this.resolvePrivacyAuthorization = null
|
||||
resolvePrivacyAuthorization({
|
||||
buttonId: 'register-privacy-agree-btn',
|
||||
event: 'agree'
|
||||
})
|
||||
},
|
||||
handlePrivacyDisagree() {
|
||||
this.showPrivacyPopup = false
|
||||
if (!this.resolvePrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
|
||||
this.resolvePrivacyAuthorization = null
|
||||
resolvePrivacyAuthorization({
|
||||
event: 'disagree'
|
||||
})
|
||||
},
|
||||
uploadAttachment(field) {
|
||||
return this.chooseImageFile()
|
||||
return this.ensurePrivacyAuthorized()
|
||||
.then(() => this.chooseImageFile())
|
||||
.then((file) => {
|
||||
if (!file || !file.path) return
|
||||
uni.showLoading({
|
||||
@@ -1008,6 +1303,13 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error && error.cancel) return
|
||||
if (error && error.privacyDenied) {
|
||||
uni.showToast({
|
||||
title: this.t('privacyAuthorizeFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
console.error('upload attachment failed', error)
|
||||
uni.showToast({
|
||||
title: this.t('uploadFailed'),
|
||||
@@ -1104,8 +1406,6 @@ export default {
|
||||
title: this.t('submitting')
|
||||
})
|
||||
|
||||
const authProfile = this.buildAuthProfile()
|
||||
|
||||
return this.getWechatCode().then((providerValue) => {
|
||||
const submitPayload = this.buildSubmitPayload(providerValue)
|
||||
return submitRegistrationApplication(submitPayload)
|
||||
@@ -1179,6 +1479,7 @@ export default {
|
||||
if (field === 'passportAttachment') return 'passport-attachment'
|
||||
if (field === 'idCardFrontAttachment') return 'id-card-front'
|
||||
if (field === 'idCardBackAttachment') return 'id-card-back'
|
||||
if (field === 'admissionNoticeAttachment') return 'admission-notice-attachment'
|
||||
return this.getFileNameFromPath(filePath)
|
||||
},
|
||||
getFileNameFromPath(filePath = '') {
|
||||
@@ -1204,6 +1505,9 @@ export default {
|
||||
if (!this.form.workUnit || this.form.hasHangzhouProperty === null) {
|
||||
return this.t('validateProfile')
|
||||
}
|
||||
if (!this.form.idCardType) {
|
||||
return this.t('validateIdCardType')
|
||||
}
|
||||
|
||||
if (this.form.identityType === 'DomesticStudents') {
|
||||
if (!this.form.idCard) {
|
||||
@@ -1216,13 +1520,13 @@ export default {
|
||||
}
|
||||
|
||||
if (!this.form.passportNumber || !this.form.passportExpiry) {
|
||||
return this.t('validatePassportBasic')
|
||||
return this.isPassportType ? this.t('validatePassportBasic') : this.t('validateDocumentBasic')
|
||||
}
|
||||
if (!this.form.passportAttachment.serverValue) {
|
||||
return this.t('validatePassportAttachment')
|
||||
return this.isPassportType ? this.t('validatePassportAttachment') : this.t('validateDocumentAttachment')
|
||||
}
|
||||
if (!this.isImageAttachment(this.form.passportAttachment)) {
|
||||
return this.t('validatePassportAttachmentImage')
|
||||
return this.isPassportType ? this.t('validatePassportAttachmentImage') : this.t('validateDocumentAttachmentImage')
|
||||
}
|
||||
return ''
|
||||
},
|
||||
@@ -1246,6 +1550,8 @@ export default {
|
||||
genderText: getGenderLabel(this.form.gender, this.form.gender === 1 ? this.t('genderMale') : this.form.gender === 0 ? this.t('genderFemale') : ''),
|
||||
phone: this.form.phone,
|
||||
country: this.form.country,
|
||||
idCardType: Number(this.form.idCardType) || null,
|
||||
idCardTypeText: this.selectedIdCardTypeLabel || '',
|
||||
idCard: this.form.identityType === 'DomesticStudents' ? this.form.idCard : '',
|
||||
workUnit: this.form.workUnit,
|
||||
hasHangzhouProperty: this.form.hasHangzhouProperty,
|
||||
@@ -1260,6 +1566,7 @@ export default {
|
||||
school: this.form.identityType === 'ForeignStudents' ? this.form.school : '',
|
||||
religion: this.form.religion,
|
||||
religionTags: this.buildSingleTag(this.form.religion, this.religionOptions),
|
||||
admissionNoticeAttachment: this.form.admissionNoticeAttachment.serverValue || '',
|
||||
passportAttachment: this.form.identityType === 'ForeignStudents' ? this.form.passportAttachment.serverValue : '',
|
||||
idCardFrontAttachment: this.form.identityType === 'DomesticStudents' ? this.form.idCardFrontAttachment.serverValue : '',
|
||||
idCardBackAttachment: this.form.identityType === 'DomesticStudents' ? this.form.idCardBackAttachment.serverValue : ''
|
||||
@@ -1278,6 +1585,8 @@ export default {
|
||||
gender: profile.gender,
|
||||
phone: profile.phone,
|
||||
country: profile.country,
|
||||
idCardType: profile.idCardType || null,
|
||||
IdCardType: profile.idCardType || null,
|
||||
idCard: profile.idCard || null,
|
||||
workUnit: profile.workUnit,
|
||||
hasHangzhouProperty: profile.hasHangzhouProperty,
|
||||
@@ -1287,6 +1596,7 @@ export default {
|
||||
passportExpiry: profile.passportExpiry || null,
|
||||
school: profile.school || null,
|
||||
religionTags: profile.religionTags.length ? profile.religionTags : null,
|
||||
admissionNoticeAttachment: profile.admissionNoticeAttachment || null,
|
||||
passportAttachment: profile.passportAttachment || null,
|
||||
idCardFrontAttachment: profile.idCardFrontAttachment || null,
|
||||
idCardBackAttachment: profile.idCardBackAttachment || null
|
||||
@@ -1469,11 +1779,21 @@ export default {
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.role-tab-subtitle {
|
||||
margin-top: 6rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: #8A99B3;
|
||||
}
|
||||
.role-active {
|
||||
background-color: #FFFFFF;
|
||||
color: $brand-primary;
|
||||
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.15);
|
||||
}
|
||||
.role-active .role-tab-subtitle {
|
||||
color: rgba($brand-primary, 0.82);
|
||||
}
|
||||
|
||||
/* 性别按钮 */
|
||||
.gender-btn {
|
||||
@@ -1736,4 +2056,84 @@ export default {
|
||||
.login-link:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.privacy-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 999;
|
||||
background: rgba(17, 24, 39, 0.56);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.privacy-dialog {
|
||||
width: 100%;
|
||||
max-width: 640rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 28rpx;
|
||||
padding: 40rpx 32rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 24rpx 80rpx rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.privacy-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1A2233;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.privacy-content {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
color: #5C6B8A;
|
||||
}
|
||||
|
||||
.privacy-link {
|
||||
color: $brand-primary;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.privacy-action-row {
|
||||
display: flex;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.privacy-action-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $brand-primary;
|
||||
background: rgba($brand-primary, 0.08);
|
||||
border: 1rpx solid rgba($brand-primary, 0.12);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.privacy-action-btn + .privacy-action-btn {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
button.privacy-action-btn {
|
||||
padding: 0;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
|
||||
button.privacy-action-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.privacy-action-btn.is-primary {
|
||||
color: #FFFFFF;
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.92) 0%, $brand-primary 100%);
|
||||
border-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { resolveExistingBookingNavigationTarget } from '../../utils/booking-entry'
|
||||
import { getSessionSnapshot } from '../../utils/app-state'
|
||||
import { syncLoginStayState } from '../../utils/session-sync'
|
||||
import { compactEnglishText } from '../../utils/select-room-normalizers'
|
||||
|
||||
export default {
|
||||
@@ -194,11 +197,41 @@ export default {
|
||||
console.error('Failed to parse apply data', e);
|
||||
})
|
||||
}
|
||||
this.redirectIfBookingUnavailable()
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgBooking.book.book', key)
|
||||
},
|
||||
redirectIfBookingUnavailable() {
|
||||
const session = getSessionSnapshot()
|
||||
if (!session.isLogin) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
return syncLoginStayState({
|
||||
language: this.$getLanguage(),
|
||||
force: true
|
||||
})
|
||||
.then((stayState) => {
|
||||
const targetUrl = resolveExistingBookingNavigationTarget(stayState)
|
||||
if (!targetUrl) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (/^\/pages\/index\/index(?:\?|$)/.test(targetUrl)) {
|
||||
uni.reLaunch({ url: targetUrl })
|
||||
return true
|
||||
}
|
||||
|
||||
uni.redirectTo({ url: targetUrl })
|
||||
return true
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('check booking entry failed', error)
|
||||
return false
|
||||
})
|
||||
},
|
||||
onStartDateChange(e) {
|
||||
this.form.startDate = e.detail.value;
|
||||
},
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="time-grid">
|
||||
<scroll-view class="time-scroll" scroll-x :show-scrollbar="false">
|
||||
<view class="time-flex">
|
||||
<view
|
||||
v-for="(item, index) in timeSlots"
|
||||
:key="index"
|
||||
@@ -52,6 +53,7 @@
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 租住需求卡片 -->
|
||||
@@ -63,6 +65,15 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field-item">
|
||||
<view class="field-label">{{ t('roomTypeLabel') }}</view>
|
||||
<room-type-selector
|
||||
v-model="formData.intendedRoomTypeCode"
|
||||
:initial-area-code="formData.intendedAreaCode"
|
||||
@change="handleRoomTypeChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="field-item">
|
||||
<view class="field-label">{{ t('durationLabel') }}</view>
|
||||
<view class="tag-grid">
|
||||
@@ -119,8 +130,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { submitApplicationForm } from '@/api/index.js'
|
||||
import { submitViewingApplicationForm } from '@/api/index.js'
|
||||
import { resolveExistingBookingNavigationTarget } from '../../utils/booking-entry'
|
||||
import { getSessionSnapshot } from '../../utils/app-state'
|
||||
import { syncLoginStayState } from '../../utils/session-sync'
|
||||
import { compactEnglishText, localizePayMethodText } from '../../utils/select-room-normalizers'
|
||||
import RoomTypeSelector from './components/RoomTypeSelector.vue'
|
||||
|
||||
function buildViewingDateTime(date = '', time = '', isEnd = false) {
|
||||
if (!date || !time) {
|
||||
@@ -134,7 +149,44 @@ function buildViewingDateTime(date = '', time = '', isEnd = false) {
|
||||
return `${date}T${selected}:00`
|
||||
}
|
||||
|
||||
function formatDatePart(value) {
|
||||
return String(value).padStart(2, '0')
|
||||
}
|
||||
|
||||
function shiftDate(date = '', days = 0) {
|
||||
if (!date) {
|
||||
return ''
|
||||
}
|
||||
const next = new Date(`${date}T00:00:00`)
|
||||
if (!Number.isFinite(next.getTime())) {
|
||||
return ''
|
||||
}
|
||||
next.setDate(next.getDate() + days)
|
||||
return `${next.getFullYear()}-${formatDatePart(next.getMonth() + 1)}-${formatDatePart(next.getDate())}`
|
||||
}
|
||||
|
||||
function buildExpectedStayRange(date = '', durationIndex = 0) {
|
||||
if (!date) {
|
||||
return {
|
||||
start: null,
|
||||
end: null
|
||||
}
|
||||
}
|
||||
|
||||
const durationDays = [90, 180, 270, 365, 365]
|
||||
const normalizedIndex = durationIndex >= 0 ? durationIndex : 0
|
||||
const endDate = shiftDate(date, durationDays[normalizedIndex] || durationDays[0])
|
||||
|
||||
return {
|
||||
start: `${date}T00:00:00`,
|
||||
end: endDate ? `${endDate}T23:59:59` : null
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RoomTypeSelector
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
@@ -177,6 +229,8 @@ export default {
|
||||
currentDateIndex: 0,
|
||||
currentTimeIndex: -1,
|
||||
formData: {
|
||||
intendedAreaCode: '',
|
||||
intendedRoomTypeCode: '',
|
||||
budget: '',
|
||||
duration: '',
|
||||
workplace: ''
|
||||
@@ -191,6 +245,7 @@ export default {
|
||||
selectViewingTimeDesc: '请选择您方便的时间段',
|
||||
rentalNeeds: '您的租住需求',
|
||||
rentalNeedsDesc: '帮助管家更好地为您推荐房源',
|
||||
roomTypeLabel: '意向房型',
|
||||
durationLabel: '预计租住时长',
|
||||
budgetLabel: '租金预算 (元/月)',
|
||||
budgetPlaceholder: '请输入您的心理价位',
|
||||
@@ -226,7 +281,7 @@ export default {
|
||||
selectDurationRequired: '请选择预计租住时长',
|
||||
submitting: '提交中...',
|
||||
submitSuccessTitle: '预约成功',
|
||||
submitSuccessContent: '您的预约看房申请已提交。\n系统会自动转发给对应区域有房间权限的宿管,宿管确认带看后可直接完结订单,或选定房型房号后转为预定单发送给租客支付。',
|
||||
submitSuccessContent: '您的预约看房申请已提交,请等待管家联系',
|
||||
submitFailed: '预约提交失败'
|
||||
},
|
||||
en: {
|
||||
@@ -236,6 +291,7 @@ export default {
|
||||
selectViewingTimeDesc: 'Choose a time slot that works for you',
|
||||
rentalNeeds: 'Rental Needs',
|
||||
rentalNeedsDesc: 'Help the manager recommend a suitable room',
|
||||
roomTypeLabel: 'Preferred Room Type',
|
||||
durationLabel: 'Expected Stay Duration',
|
||||
budgetLabel: 'Budget (CNY/month)',
|
||||
budgetPlaceholder: 'Enter your expected budget',
|
||||
@@ -283,15 +339,47 @@ export default {
|
||||
if (options.payload) {
|
||||
try {
|
||||
this.sourceData = JSON.parse(decodeURIComponent(options.payload))
|
||||
this.formData.intendedAreaCode = this.sourceData && this.sourceData.areaCode ? this.sourceData.areaCode : ''
|
||||
this.formData.intendedRoomTypeCode = this.sourceData && this.sourceData.roomTypeCode ? this.sourceData.roomTypeCode : ''
|
||||
} catch (e) {
|
||||
console.error('Failed to parse payload', e)
|
||||
}
|
||||
}
|
||||
this.redirectIfBookingUnavailable()
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgBooking.book_viewing.book_viewing', key, params)
|
||||
},
|
||||
redirectIfBookingUnavailable() {
|
||||
const session = getSessionSnapshot()
|
||||
if (!session.isLogin) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
return syncLoginStayState({
|
||||
language: this.$getLanguage(),
|
||||
force: true
|
||||
})
|
||||
.then((stayState) => {
|
||||
const targetUrl = resolveExistingBookingNavigationTarget(stayState)
|
||||
if (!targetUrl) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (/^\/pages\/index\/index(?:\?|$)/.test(targetUrl)) {
|
||||
uni.reLaunch({ url: targetUrl })
|
||||
return true
|
||||
}
|
||||
|
||||
uni.redirectTo({ url: targetUrl })
|
||||
return true
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('check viewing booking entry failed', error)
|
||||
return false
|
||||
})
|
||||
},
|
||||
syncDurationOptions() {
|
||||
this.durationOptions = [
|
||||
this.t('duration1'),
|
||||
@@ -339,50 +427,26 @@ export default {
|
||||
selectTimeSlot(index) {
|
||||
this.currentTimeIndex = index
|
||||
},
|
||||
handleRoomTypeChange({ area, roomType }) {
|
||||
this.formData.intendedAreaCode = area && area.code ? area.code : ''
|
||||
this.formData.intendedRoomTypeCode = roomType && roomType.code ? roomType.code : ''
|
||||
if (!this.sourceData) {
|
||||
this.sourceData = {}
|
||||
}
|
||||
if (area) {
|
||||
this.sourceData.areaCode = area.code || ''
|
||||
this.sourceData.areaName = area.name || ''
|
||||
this.sourceData.areaNameEn = area.nameEn || ''
|
||||
}
|
||||
if (roomType) {
|
||||
this.sourceData.roomTypeCode = roomType.code || ''
|
||||
this.sourceData.roomTypeName = roomType.name || ''
|
||||
this.sourceData.roomTypeNameEn = roomType.nameEn || ''
|
||||
}
|
||||
},
|
||||
selectDuration(item) {
|
||||
this.formData.duration = item
|
||||
},
|
||||
buildRemark(selectedDate, selectedTime) {
|
||||
const lines = [
|
||||
this.t('remarkTitle'),
|
||||
this.t('remarkTime', { date: selectedDate, time: selectedTime }),
|
||||
this.t('remarkDuration', { duration: this.formData.duration })
|
||||
]
|
||||
|
||||
if (this.formData.budget) {
|
||||
lines.push(this.t('remarkBudget', { budget: this.formData.budget }))
|
||||
}
|
||||
if (this.formData.workplace) {
|
||||
lines.push(this.t('remarkWorkplace', { workplace: this.formData.workplace }))
|
||||
}
|
||||
if (this.sourceData && (this.sourceData.areaName || this.sourceData.areaNameEn)) {
|
||||
lines.push(this.t('remarkArea', {
|
||||
area: this.language === 'en'
|
||||
? (this.sourceData.areaNameEnDisplay || this.sourceData.areaLabelEnDisplay || this.sourceData.areaNameEn || this.sourceData.areaLabelEn || this.sourceData.areaName || this.sourceData.areaLabel || '')
|
||||
: (this.sourceData.areaName || this.sourceData.areaLabel || this.sourceData.areaNameEn || this.sourceData.areaLabelEn || '')
|
||||
}))
|
||||
}
|
||||
if (this.sourceData && (this.sourceData.roomTypeName || this.sourceData.roomTypeNameEn)) {
|
||||
lines.push(this.t('remarkRoomType', {
|
||||
roomType: this.language === 'en'
|
||||
? (this.sourceData.roomTypeNameEnDisplay || this.sourceData.roomTypeNameEn || this.sourceData.roomTypeName || '')
|
||||
: (this.sourceData.roomTypeName || this.sourceData.roomTypeNameEn || '')
|
||||
}))
|
||||
}
|
||||
if (this.sourceData && (this.sourceData.roomName || this.sourceData.roomNameEn)) {
|
||||
lines.push(this.t('remarkSource', {
|
||||
room: this.language === 'en'
|
||||
? (this.sourceData.roomNameEnDisplay || this.sourceData.roomNameEn || this.sourceData.roomName || '')
|
||||
: (this.sourceData.roomName || this.sourceData.roomNameEn || '')
|
||||
}))
|
||||
}
|
||||
if (this.sourceData && this.sourceData.payMethod) {
|
||||
lines.push(this.t('remarkPayMethod', { payMethod: localizePayMethodText(this.sourceData.payMethod, this.language) }))
|
||||
}
|
||||
|
||||
lines.push(this.t('remarkTail'))
|
||||
return lines.join('\n')
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.isSubmitting) {
|
||||
return
|
||||
@@ -398,20 +462,24 @@ export default {
|
||||
|
||||
const selectedDate = this.dateList[this.currentDateIndex].date
|
||||
const selectedTime = this.timeSlots[this.currentTimeIndex]
|
||||
const appointmentViewingTime = buildViewingDateTime(selectedDate, selectedTime, false)
|
||||
const durationIndex = this.durationOptions.findIndex(item => item === this.formData.duration)
|
||||
const expectedStayRange = buildExpectedStayRange(selectedDate, durationIndex)
|
||||
const payload = {
|
||||
intendedAreaCode: this.sourceData && this.sourceData.areaCode ? this.sourceData.areaCode : null,
|
||||
intendedRoomTypeCode: this.sourceData && this.sourceData.roomTypeCode ? this.sourceData.roomTypeCode : null,
|
||||
intendedAreaCode: this.formData.intendedAreaCode || (this.sourceData && this.sourceData.areaCode ? this.sourceData.areaCode : null),
|
||||
intendedRoomTypeCode: this.formData.intendedRoomTypeCode || (this.sourceData && this.sourceData.roomTypeCode ? this.sourceData.roomTypeCode : null),
|
||||
roomSlotHeight: 1,
|
||||
hobbiesAndInterestsTagKeys: null,
|
||||
lifestyleHabitsTagKeys: null,
|
||||
remark: this.buildRemark(selectedDate, selectedTime),
|
||||
start: buildViewingDateTime(selectedDate, selectedTime, false),
|
||||
end: buildViewingDateTime(selectedDate, selectedTime, true)
|
||||
};
|
||||
remark: null,
|
||||
start: expectedStayRange.start,
|
||||
end: expectedStayRange.end,
|
||||
appointmentViewingTime
|
||||
}
|
||||
|
||||
this.isSubmitting = true
|
||||
uni.showLoading({ title: this.t('submitting'), mask: true })
|
||||
return submitApplicationForm(payload)
|
||||
return submitViewingApplicationForm(payload)
|
||||
.then(() => {
|
||||
uni.hideLoading()
|
||||
uni.showModal({
|
||||
@@ -582,14 +650,18 @@ export default {
|
||||
}
|
||||
|
||||
/* 时间段选择 */
|
||||
.time-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.time-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.time-flex {
|
||||
display: inline-flex;
|
||||
gap: 20rpx;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.time-slot {
|
||||
width: calc(33.33% - 14rpx);
|
||||
width: 220rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
@@ -599,6 +671,7 @@ export default {
|
||||
color: #4E5969;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-slot--active {
|
||||
|
||||
363
pkgBooking/book_viewing/components/RoomTypeSelector.vue
Normal file
363
pkgBooking/book_viewing/components/RoomTypeSelector.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<view class="room-type-selector">
|
||||
<view class="selector-header tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
|
||||
<view class="selected-info">
|
||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
|
||||
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : t('selectRoomType') }}
|
||||
</view>
|
||||
<view class="selected-area tn-text-ellipsis" v-if="selectedArea">
|
||||
{{ getAreaDisplayName(selectedArea) }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="tn-icon-right tn-color-gray"></text>
|
||||
</view>
|
||||
|
||||
<tn-popup v-model="showPopup" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true">
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">{{ t('popupTitle') }}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="area-list-scroll">
|
||||
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
||||
<tn-loading mode="circle"></tn-loading>
|
||||
<view class="tn-margin-top-sm tn-color-gray">{{ t('loadingRoomTypes') }}</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!areaList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||
<text class="tn-color-gray">{{ t('emptyRoomTypes') }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="area-group" v-for="(area, index) in areaList" :key="area.code">
|
||||
<view class="area-title tn-flex tn-flex-row-between tn-flex-col-center" @click="toggleArea(index)">
|
||||
<view class="tn-flex tn-flex-col-center">
|
||||
<text class="area-dot"></text>
|
||||
<text>{{ getAreaDisplayName(area) }}</text>
|
||||
</view>
|
||||
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'" class="tn-color-gray"></text>
|
||||
</view>
|
||||
<view class="room-type-grid" v-show="!collapsedAreas[index]">
|
||||
<view
|
||||
class="room-type-item"
|
||||
v-for="room in area.roomTypes"
|
||||
:key="room.code"
|
||||
:class="{ 'is-active': selectedRoomType && selectedRoomType.code === room.code }"
|
||||
@click="handleSelect(area, room)"
|
||||
>
|
||||
<view class="room-name tn-text-ellipsis">{{ getRoomTypeDisplayName(room) }}</view>
|
||||
<view class="room-price" v-if="room.price">¥{{ getPriceValue(room.price) }}{{ t('priceUnit') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</tn-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { areaPagination } from '@/api/index.js'
|
||||
import { normalizeAreaItem } from '../../../utils/select-room-normalizers'
|
||||
|
||||
const I18N = {
|
||||
zh: {
|
||||
emptyRoomTypes: '暂无可选房型',
|
||||
loadFailed: '房型加载失败',
|
||||
loadingRoomTypes: '房型加载中...',
|
||||
popupTitle: '选择意向房型',
|
||||
priceUnit: '/月',
|
||||
selectRoomType: '请选择房型'
|
||||
},
|
||||
en: {
|
||||
emptyRoomTypes: 'No room types available',
|
||||
loadFailed: 'Failed to load room types',
|
||||
loadingRoomTypes: 'Loading room types...',
|
||||
popupTitle: 'Select Room Type',
|
||||
priceUnit: '/month',
|
||||
selectRoomType: 'Select room type'
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'BookViewingRoomTypeSelector',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
initialAreaCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
isLoading: false,
|
||||
areaList: [],
|
||||
selectedArea: null,
|
||||
selectedRoomType: null,
|
||||
collapsedAreas: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage ? this.$getLanguage() : 'zh'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.syncSelection()
|
||||
},
|
||||
language() {
|
||||
this.loadData(true)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
const lang = I18N[this.language] ? this.language : 'zh'
|
||||
return I18N[lang][key] || I18N.zh[key] || key
|
||||
},
|
||||
openSelector() {
|
||||
this.showPopup = true
|
||||
this.loadData()
|
||||
},
|
||||
loadData(force = false) {
|
||||
if (!force && this.areaList.length > 0) {
|
||||
this.syncSelection()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
return areaPagination({ PageIndex: 1, PageSize: 100 }, this.language)
|
||||
.then((res) => {
|
||||
const payload = res && res.data ? res.data : res
|
||||
const rawList = Array.isArray(payload && payload.data) ? payload.data : (Array.isArray(payload) ? payload : [])
|
||||
this.areaList = rawList
|
||||
.map(item => normalizeAreaItem(item))
|
||||
.filter(area => Array.isArray(area.roomTypes) && area.roomTypes.length > 0)
|
||||
|
||||
const collapsed = {}
|
||||
this.areaList.forEach((_, index) => {
|
||||
collapsed[index] = false
|
||||
})
|
||||
this.collapsedAreas = collapsed
|
||||
this.syncSelection()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('load room types error', error)
|
||||
uni.showToast({ title: this.t('loadFailed'), icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
syncSelection() {
|
||||
if (!this.areaList.length) {
|
||||
this.selectedArea = null
|
||||
this.selectedRoomType = null
|
||||
return
|
||||
}
|
||||
|
||||
let foundArea = null
|
||||
let foundRoom = null
|
||||
const previousAreaCode = this.selectedArea && this.selectedArea.code ? this.selectedArea.code : ''
|
||||
const previousRoomCode = this.selectedRoomType && this.selectedRoomType.code ? this.selectedRoomType.code : ''
|
||||
|
||||
if (this.value) {
|
||||
for (const area of this.areaList) {
|
||||
const room = Array.isArray(area.roomTypes) ? area.roomTypes.find(item => item.code === this.value) : null
|
||||
if (room) {
|
||||
foundArea = area
|
||||
foundRoom = room
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundRoom && this.initialAreaCode) {
|
||||
foundArea = this.areaList.find(area => area.code === this.initialAreaCode) || null
|
||||
}
|
||||
|
||||
if (!foundArea) {
|
||||
this.selectedArea = null
|
||||
this.selectedRoomType = null
|
||||
return
|
||||
}
|
||||
|
||||
this.selectedArea = foundArea
|
||||
this.selectedRoomType = foundRoom || null
|
||||
|
||||
if (foundRoom) {
|
||||
if (this.value !== foundRoom.code) {
|
||||
this.$emit('input', foundRoom.code)
|
||||
}
|
||||
if (previousAreaCode !== foundArea.code || previousRoomCode !== foundRoom.code) {
|
||||
this.$emit('change', { area: foundArea, roomType: foundRoom })
|
||||
}
|
||||
}
|
||||
},
|
||||
handleSelect(area, roomType) {
|
||||
this.selectedArea = area
|
||||
this.selectedRoomType = roomType
|
||||
this.$emit('input', roomType.code)
|
||||
this.$emit('change', { area, roomType })
|
||||
this.showPopup = false
|
||||
},
|
||||
getAreaDisplayName(area = {}) {
|
||||
if (this.language === 'en') {
|
||||
return area.nameEnDisplay || area.nameEn || area.name || ''
|
||||
}
|
||||
return area.name || area.nameEn || ''
|
||||
},
|
||||
getRoomTypeDisplayName(roomType = {}) {
|
||||
if (this.language === 'en') {
|
||||
return roomType.nameEnDisplay || roomType.nameEn || roomType.name || ''
|
||||
}
|
||||
return roomType.name || roomType.nameEn || ''
|
||||
},
|
||||
toggleArea(index) {
|
||||
this.$set(this.collapsedAreas, index, !this.collapsedAreas[index])
|
||||
},
|
||||
getPriceValue(priceText) {
|
||||
if (!priceText) {
|
||||
return '--'
|
||||
}
|
||||
const matched = String(priceText).match(/(\d+(\.\d+)?)/)
|
||||
return matched ? matched[1] : '--'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.room-type-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 26rpx 32rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||||
min-height: 100rpx;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selected-name {
|
||||
font-size: 28rpx;
|
||||
color: #1A2233;
|
||||
font-weight: 600;
|
||||
|
||||
&.is-empty {
|
||||
color: #A0B0C0;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-area {
|
||||
font-size: 22rpx;
|
||||
color: #6A7A99;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #F8FAFF;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
padding: 40rpx 30rpx 20rpx;
|
||||
text-align: center;
|
||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.area-list-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 20rpx 30rpx 60rpx;
|
||||
}
|
||||
|
||||
.area-group {
|
||||
margin-bottom: 30rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.04);
|
||||
border: 1rpx solid rgba($brand-primary, 0.05);
|
||||
}
|
||||
|
||||
.area-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1A2233;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.area-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: $brand-primary;
|
||||
margin-right: 12rpx;
|
||||
box-shadow: 0 0 8rpx rgba($brand-primary, 0.4);
|
||||
}
|
||||
|
||||
.room-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.room-type-item {
|
||||
padding: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #FFFFFF;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.is-active {
|
||||
background: #FFFFFF;
|
||||
border-color: $brand-primary;
|
||||
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
||||
|
||||
.room-name {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.room-price {
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #1A2233;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.room-price {
|
||||
font-size: 22rpx;
|
||||
color: #6A7A99;
|
||||
}
|
||||
</style>
|
||||
@@ -45,6 +45,14 @@
|
||||
<script>
|
||||
import { localizeHousingTag, localizePayMethodText } from '../../../utils/select-room-normalizers'
|
||||
|
||||
function stripHtmlTags(value = '') {
|
||||
return String(value || '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
roomTypes: {
|
||||
@@ -80,13 +88,14 @@ export default {
|
||||
return room.name || room.nameEn || ''
|
||||
},
|
||||
getRoomDescription(room = {}) {
|
||||
if (this.language === 'en') {
|
||||
return room.descriptionEnDisplay || room.descriptionEn || room.description || ''
|
||||
}
|
||||
return room.description || room.descriptionEn || ''
|
||||
const rawDescription = this.language === 'en'
|
||||
? (room.descriptionEnDisplay || room.descriptionEn || room.description || '')
|
||||
: (room.description || room.descriptionEn || '')
|
||||
|
||||
return stripHtmlTags(rawDescription)
|
||||
},
|
||||
goToRoomDetail(room) {
|
||||
this.$emit('go-detail', room);
|
||||
this.$emit('go-detail', room)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,16 @@
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ t('storeIntro') }}</text>
|
||||
</view>
|
||||
<view class="store-desc" :class="{'store-desc--collapsed': isDescCollapsed}">
|
||||
{{ displayDescription }}
|
||||
<view
|
||||
class="store-desc"
|
||||
:class="{
|
||||
'store-desc--collapsed': !isRichDescription && isDescCollapsed
|
||||
}"
|
||||
>
|
||||
<rich-text v-if="isRichDescription" class="store-desc-rich" :nodes="displayDescriptionNodes"></rich-text>
|
||||
<text v-else>{{ displayDescription }}</text>
|
||||
</view>
|
||||
<view class="expand-btn tn-flex tn-flex-row-center tn-flex-col-center" @click="toggleDesc">
|
||||
<view v-if="shouldShowToggle" class="expand-btn tn-flex tn-flex-row-center tn-flex-col-center" @click="toggleDesc">
|
||||
<text>{{ isDescCollapsed ? t('expandAll') : t('collapse') }}</text>
|
||||
<text :class="isDescCollapsed ? 'tn-icon-down' : 'tn-icon-up'" class="expand-icon"></text>
|
||||
</view>
|
||||
@@ -31,6 +37,33 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { normalizeRichTextImages } from '../../../utils/rich-text'
|
||||
|
||||
function hasHtmlTag(value = '') {
|
||||
return /<\/?(p|div|img|br|span|h[1-6]|ul|li|ol|table|td|tr|strong|em|section|article|blockquote)[^>]*>/i.test(String(value || ''))
|
||||
}
|
||||
|
||||
function normalizeRichDescription(value = '') {
|
||||
let html = String(value || '').trim()
|
||||
if (!html) {
|
||||
return ''
|
||||
}
|
||||
|
||||
html = normalizeRichTextImages(
|
||||
html,
|
||||
'display:block;width:100%;max-width:100%;height:auto;box-sizing:border-box;border-radius:16rpx;margin:16rpx 0;vertical-align:top;'
|
||||
)
|
||||
html = html.replace(/<p\b([^>]*)>/gi, '<p$1 style="line-height:1.9;color:rgba(255,255,255,0.78);margin:0 0 20rpx;word-break:break-word;">')
|
||||
html = html.replace(/<div\b([^>]*)>/gi, '<div$1 style="line-height:1.9;color:rgba(255,255,255,0.78);margin:0 0 20rpx;word-break:break-word;">')
|
||||
html = html.replace(/<ul\b([^>]*)>/gi, '<ul$1 style="padding-left:28rpx;margin:0 0 20rpx;color:rgba(255,255,255,0.78);line-height:1.9;">')
|
||||
html = html.replace(/<ol\b([^>]*)>/gi, '<ol$1 style="padding-left:32rpx;margin:0 0 20rpx;color:rgba(255,255,255,0.78);line-height:1.9;">')
|
||||
html = html.replace(/<li\b([^>]*)>/gi, '<li$1 style="margin-bottom:12rpx;">')
|
||||
html = html.replace(/<h([1-6])\b([^>]*)>/gi, '<h$1$2 style="color:#FFFFFF;line-height:1.5;margin:0 0 20rpx;font-weight:600;">')
|
||||
html = html.replace(/<strong\b([^>]*)>/gi, '<strong$1 style="color:#FFFFFF;font-weight:600;">')
|
||||
html = html.replace(/<a\b([^>]*)>/gi, '<a$1 style="color:#9EC5FF;text-decoration:underline;">')
|
||||
return html
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
storeData: {
|
||||
@@ -54,6 +87,18 @@ export default {
|
||||
}
|
||||
return this.storeData.description || this.storeData.descriptionEn || ''
|
||||
},
|
||||
isRichDescription() {
|
||||
return hasHtmlTag(this.displayDescription)
|
||||
},
|
||||
displayDescriptionNodes() {
|
||||
return this.isRichDescription ? normalizeRichDescription(this.displayDescription) : ''
|
||||
},
|
||||
shouldShowToggle() {
|
||||
if (this.isRichDescription) {
|
||||
return false
|
||||
}
|
||||
return String(this.displayDescription || '').length > 90
|
||||
},
|
||||
shortAddress() {
|
||||
const addr = this.language === 'en'
|
||||
? (this.storeData.addressEnDisplay || this.storeData.addressEn || this.storeData.address || '')
|
||||
@@ -176,6 +221,7 @@ export default {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.store-desc--collapsed {
|
||||
@@ -185,6 +231,10 @@ export default {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.store-desc-rich {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
|
||||
@@ -114,14 +114,28 @@
|
||||
v-if="canCancelOrder"
|
||||
class="danger-btn"
|
||||
:class="{ 'danger-btn--loading': canceling }"
|
||||
@tap.stop="handleCancelOrder"
|
||||
@click.stop="handleCancelOrder"
|
||||
>
|
||||
<text class="tn-text-bold">{{ canceling ? t('cancelOrderLoading') : t('cancelOrder') }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="canViewBill"
|
||||
v-if="currentStepActionText"
|
||||
class="action-btn"
|
||||
@tap.stop="goToBillPage"
|
||||
@click.stop="handleCurrentStepAction"
|
||||
>
|
||||
<text class="tn-text-bold">{{ currentStepActionText }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-else-if="isReservationCheckinProcessing()"
|
||||
class="action-btn action-btn--ghost"
|
||||
@click.stop="refreshCurrentPage"
|
||||
>
|
||||
<text class="tn-text-bold">{{ t('refreshAction') }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-else-if="canViewBill"
|
||||
class="action-btn"
|
||||
@click.stop="goToBillPage"
|
||||
>
|
||||
<text class="tn-text-bold">{{ t('viewBill') }}</text>
|
||||
</view>
|
||||
@@ -131,8 +145,48 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { userDetailCancel } from '@/api/index.js'
|
||||
import { contractDetail, orderSearchedReservationFlow, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
|
||||
import { getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
|
||||
import {
|
||||
RESERVATION_FLOW_STEP,
|
||||
attachReservationFlow,
|
||||
buildReservationFlowMap,
|
||||
getReservationCurrentActionText,
|
||||
getReservationCurrentStepText,
|
||||
getReservationOrderNo,
|
||||
parseReservationFlowResponse
|
||||
} from '@/utils/reservation-flow'
|
||||
|
||||
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间[::]|意向房型[::]|意向区域[::]|viewing request|viewing time[::]|preferred room type[::]|preferred area[::]/i
|
||||
|
||||
function isViewingApplicationRecord(item = {}) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const typeText = [
|
||||
item.businessType,
|
||||
item.applicationType,
|
||||
item.applicationTypeDesc,
|
||||
item.formType,
|
||||
item.bizType,
|
||||
item.type,
|
||||
item.typeDesc
|
||||
]
|
||||
.filter(value => value !== null && typeof value !== 'undefined' && String(value).trim())
|
||||
.join(' ')
|
||||
|
||||
if (/viewing|看房|带看/i.test(typeText)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ([item.appointmentViewingTime, item.viewingTime, item.bookViewingTime].some(value => String(value || '').trim())) {
|
||||
return true
|
||||
}
|
||||
|
||||
const remark = item && item.remark ? String(item.remark) : ''
|
||||
return VIEWING_APPLICATION_PATTERN.test(remark)
|
||||
}
|
||||
|
||||
function normalizeApplicationList(list = []) {
|
||||
if (!Array.isArray(list)) {
|
||||
@@ -141,15 +195,31 @@ function normalizeApplicationList(list = []) {
|
||||
return list
|
||||
.filter(item => item && typeof item === 'object')
|
||||
.map((item) => {
|
||||
const remark = item && item.remark ? String(item.remark) : ''
|
||||
const isViewingApplication = /预约看房申请|看房时间[::]|意向房型[::]|意向区域[::]|viewing request|viewing time[::]|preferred room type[::]|preferred area[::]/i.test(remark)
|
||||
return {
|
||||
...item,
|
||||
isViewingApplication
|
||||
isViewingApplication: isViewingApplicationRecord(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function parseJsonStringMaybe(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return value
|
||||
}
|
||||
const text = value.trim()
|
||||
if (!text) {
|
||||
return value
|
||||
}
|
||||
if (!((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']')))) {
|
||||
return value
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (error) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
zh: {
|
||||
@@ -186,6 +256,9 @@ export default {
|
||||
emptyRemark: '暂无补充说明',
|
||||
codeCopied: '申请编号已复制',
|
||||
missingBillCode: '申请单编号缺失',
|
||||
missingContractNo: '合同编号缺失',
|
||||
checkinPendingHint: '宿管正在办理入住,请耐心等待',
|
||||
refreshAction: '刷新',
|
||||
cancelOrder: '取消订单',
|
||||
cancelOrderLoading: '取消中...',
|
||||
cancelOrderTitle: '取消订单',
|
||||
@@ -235,6 +308,9 @@ export default {
|
||||
emptyRemark: 'No additional remarks',
|
||||
codeCopied: 'Application number copied',
|
||||
missingBillCode: 'Missing application bill number',
|
||||
missingContractNo: 'Missing contract number',
|
||||
checkinPendingHint: 'Move-in is being processed, please wait',
|
||||
refreshAction: 'Refresh',
|
||||
cancelOrder: 'Cancel Order',
|
||||
cancelOrderLoading: 'Canceling...',
|
||||
cancelOrderTitle: 'Cancel Order',
|
||||
@@ -255,7 +331,14 @@ export default {
|
||||
return {
|
||||
detailCode: '',
|
||||
detail: {},
|
||||
canceling: false
|
||||
canceling: false,
|
||||
reservationContractDetail: null,
|
||||
reservationInspectionDetail: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
language() {
|
||||
this.loadReservationFlow()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -323,7 +406,35 @@ export default {
|
||||
}
|
||||
return this.parseViewingRemark()
|
||||
},
|
||||
currentStepActionText() {
|
||||
if (this.isViewingApplication) {
|
||||
return ''
|
||||
}
|
||||
if (this.isReservationInspectionPendingReview()) {
|
||||
return ''
|
||||
}
|
||||
if (this.isReservationContractAwaitingReview()) {
|
||||
return ''
|
||||
}
|
||||
if (this.isReservationCheckinProcessing()) {
|
||||
return ''
|
||||
}
|
||||
return getReservationCurrentActionText(this.detail, this.language)
|
||||
},
|
||||
statusReminderText() {
|
||||
if (this.isReservationInspectionPendingReview()) {
|
||||
return this.getReservationInspectionStatusText()
|
||||
}
|
||||
if (this.isReservationContractAwaitingReview()) {
|
||||
return this.getReservationContractStatusText()
|
||||
}
|
||||
if (this.isReservationCheckinProcessing()) {
|
||||
return this.t('checkinPendingHint')
|
||||
}
|
||||
const reservationStepText = getReservationCurrentStepText(this.detail, this.language)
|
||||
if (reservationStepText) {
|
||||
return reservationStepText
|
||||
}
|
||||
if (this.shouldPayDeposit(this.detail)) {
|
||||
return this.t('statusPendingPayment')
|
||||
}
|
||||
@@ -351,6 +462,9 @@ export default {
|
||||
this.detailCode = options.code || ''
|
||||
this.restoreDetail(options.payload || '')
|
||||
},
|
||||
onShow() {
|
||||
this.loadReservationFlow()
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgService.application_detail.application_detail', key, params)
|
||||
@@ -368,6 +482,106 @@ export default {
|
||||
&& Number(item.status) === 0
|
||||
&& Number(item.payStatus) === 10
|
||||
},
|
||||
getReservationCurrentStepValue() {
|
||||
return Number(this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode.step : 0)
|
||||
},
|
||||
isReservationCheckinProcessing() {
|
||||
return this.getReservationCurrentStepValue() === RESERVATION_FLOW_STEP.CHECKIN
|
||||
},
|
||||
getReservationInspectionNo() {
|
||||
const currentNode = this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode : {}
|
||||
if (Number(currentNode.step || 0) !== RESERVATION_FLOW_STEP.INSPECTION) {
|
||||
return ''
|
||||
}
|
||||
return String(currentNode.refNo || '').trim()
|
||||
},
|
||||
getReservationContractNo() {
|
||||
const currentNode = this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode : {}
|
||||
if (Number(currentNode.step || 0) !== RESERVATION_FLOW_STEP.CONTRACT) {
|
||||
return ''
|
||||
}
|
||||
return String(currentNode.refNo || '').trim()
|
||||
},
|
||||
normalizeInspectionDetail(response) {
|
||||
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||||
return payload.data
|
||||
}
|
||||
return {}
|
||||
},
|
||||
normalizeContractDetail(response) {
|
||||
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||||
return payload.data
|
||||
}
|
||||
return {}
|
||||
},
|
||||
isReservationInspectionPendingReview() {
|
||||
if (this.getReservationCurrentStepValue() !== RESERVATION_FLOW_STEP.INSPECTION) {
|
||||
return false
|
||||
}
|
||||
const detail = this.reservationInspectionDetail || {}
|
||||
return Number(detail.status) === 2
|
||||
},
|
||||
getReservationInspectionStatusText() {
|
||||
return this.t('inspectionPendingReviewHint')
|
||||
},
|
||||
isReservationContractAwaitingReview() {
|
||||
if (this.getReservationCurrentStepValue() !== RESERVATION_FLOW_STEP.CONTRACT) {
|
||||
return false
|
||||
}
|
||||
const detail = this.reservationContractDetail || {}
|
||||
const queryStatus = Number(detail.queryStatus)
|
||||
const approvalStatus = Number(detail.approvalStatus)
|
||||
const hasSigned = !!String(detail.signTime || detail.signImageUrl || '').trim()
|
||||
return queryStatus === 1 || (hasSigned && approvalStatus === 0)
|
||||
},
|
||||
getReservationContractStatusText() {
|
||||
const detail = this.reservationContractDetail || {}
|
||||
return detail.queryStatusDesc || detail.approvalStatusDesc || detail.statusDesc || ''
|
||||
},
|
||||
async ensureReservationInspectionDetail() {
|
||||
const inspectionNo = this.getReservationInspectionNo()
|
||||
if (!inspectionNo) {
|
||||
this.reservationInspectionDetail = null
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const detail = this.normalizeInspectionDetail(await roomInspectionDetail({
|
||||
inspectionNo
|
||||
}, this.language))
|
||||
this.reservationInspectionDetail = detail && Object.keys(detail).length ? detail : null
|
||||
return this.reservationInspectionDetail
|
||||
} catch (error) {
|
||||
console.error('load inspection detail failed', error)
|
||||
this.reservationInspectionDetail = null
|
||||
return null
|
||||
}
|
||||
},
|
||||
async ensureReservationContractDetail() {
|
||||
const contractNo = this.getReservationContractNo()
|
||||
if (!contractNo) {
|
||||
this.reservationContractDetail = null
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const detail = this.normalizeContractDetail(await contractDetail({
|
||||
contractNo
|
||||
}, this.language))
|
||||
this.reservationContractDetail = detail && Object.keys(detail).length ? detail : null
|
||||
return this.reservationContractDetail
|
||||
} catch (error) {
|
||||
console.error('load contract detail failed', error)
|
||||
this.reservationContractDetail = null
|
||||
return null
|
||||
}
|
||||
},
|
||||
restoreDetail(payload = '') {
|
||||
if (payload) {
|
||||
try {
|
||||
@@ -386,6 +600,46 @@ export default {
|
||||
isViewingApplication: false
|
||||
}
|
||||
},
|
||||
async loadReservationFlow() {
|
||||
if (!this.detail || this.detail.isViewingApplication) {
|
||||
return
|
||||
}
|
||||
|
||||
const orderNo = getReservationOrderNo(this.detail)
|
||||
if (!orderNo) {
|
||||
this.detail = attachReservationFlow(this.detail, {})
|
||||
this.reservationInspectionDetail = null
|
||||
this.reservationContractDetail = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await orderSearchedReservationFlow({
|
||||
orderNos: [orderNo]
|
||||
}, this.language)
|
||||
const flowMap = buildReservationFlowMap(parseReservationFlowResponse(response))
|
||||
this.detail = attachReservationFlow(this.detail, flowMap)
|
||||
await Promise.all([
|
||||
this.ensureReservationInspectionDetail(),
|
||||
this.ensureReservationContractDetail()
|
||||
])
|
||||
|
||||
const list = normalizeApplicationList(getMyApplicationList())
|
||||
if (list.length) {
|
||||
saveMyApplicationList(list.map((item) => {
|
||||
if (String(item.detailCode || '') !== String(this.detail.detailCode || '')) {
|
||||
return item
|
||||
}
|
||||
return this.detail
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('load reservation flow failed', error)
|
||||
this.detail = attachReservationFlow(this.detail, {})
|
||||
this.reservationInspectionDetail = null
|
||||
this.reservationContractDetail = null
|
||||
}
|
||||
},
|
||||
parseViewingRemark() {
|
||||
const lines = this.remarkLines
|
||||
const targets = [
|
||||
@@ -537,6 +791,54 @@ export default {
|
||||
uni.navigateTo({
|
||||
url: `/pkgService/utility_recharge/utility_recharge?referenceTypeNo=${encodeURIComponent(this.detail.detailCode)}`
|
||||
})
|
||||
},
|
||||
refreshCurrentPage() {
|
||||
this.loadReservationFlow()
|
||||
},
|
||||
handleCurrentStepAction() {
|
||||
const step = this.getReservationCurrentStepValue()
|
||||
|
||||
if (step === RESERVATION_FLOW_STEP.BILL) {
|
||||
this.goToBillPage()
|
||||
return
|
||||
}
|
||||
|
||||
if (step === RESERVATION_FLOW_STEP.CONTRACT) {
|
||||
if (this.isReservationContractAwaitingReview()) {
|
||||
return
|
||||
}
|
||||
const contractNo = String(this.detail && this.detail.reservationCurrentNode && this.detail.reservationCurrentNode.refNo || '').trim()
|
||||
if (!contractNo) {
|
||||
uni.showToast({
|
||||
title: this.t('missingContractNo'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pkgService/contract_sign/contract_sign?no=${encodeURIComponent(contractNo)}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (step === RESERVATION_FLOW_STEP.CHECKIN) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?pageId=stay-service'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (step === RESERVATION_FLOW_STEP.INSPECTION) {
|
||||
if (this.isReservationInspectionPendingReview()) {
|
||||
return
|
||||
}
|
||||
const orderNo = getReservationOrderNo(this.detail)
|
||||
uni.navigateTo({
|
||||
url: orderNo
|
||||
? `/pkgService/room_inspection/room_inspection?orderNo=${encodeURIComponent(orderNo)}`
|
||||
: '/pkgService/room_inspection/room_inspection'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,4 +1086,11 @@ export default {
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.25);
|
||||
}
|
||||
|
||||
.action-btn--ghost {
|
||||
background: rgba($brand-primary, 0.08);
|
||||
color: $brand-primary;
|
||||
border: 1rpx solid rgba($brand-primary, 0.18);
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -245,6 +245,35 @@ import { getActiveApplicationCode, getMyApplicationList, saveActiveApplicationCo
|
||||
|
||||
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间:|意向房型:|意向区域:|viewing request|viewing time|preferred room type|preferred area/i
|
||||
|
||||
function isViewingApplicationRecord(item = {}) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const typeText = [
|
||||
item.businessType,
|
||||
item.applicationType,
|
||||
item.applicationTypeDesc,
|
||||
item.formType,
|
||||
item.bizType,
|
||||
item.type,
|
||||
item.typeDesc
|
||||
]
|
||||
.filter(value => value !== null && typeof value !== 'undefined' && String(value).trim())
|
||||
.join(' ')
|
||||
|
||||
if (/viewing|看房|带看/i.test(typeText)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ([item.appointmentViewingTime, item.viewingTime, item.bookViewingTime].some(value => String(value || '').trim())) {
|
||||
return true
|
||||
}
|
||||
|
||||
const remark = item && item.remark ? String(item.remark) : ''
|
||||
return VIEWING_APPLICATION_PATTERN.test(remark)
|
||||
}
|
||||
|
||||
function normalizeBillList(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
if (Array.isArray(payload)) {
|
||||
@@ -263,11 +292,9 @@ function normalizeApplicationList(list = []) {
|
||||
return list
|
||||
.filter(item => item && typeof item === 'object')
|
||||
.map((item) => {
|
||||
const remark = item && item.remark ? String(item.remark) : ''
|
||||
const isViewingApplication = VIEWING_APPLICATION_PATTERN.test(remark)
|
||||
return {
|
||||
...item,
|
||||
isViewingApplication
|
||||
isViewingApplication: isViewingApplicationRecord(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -344,6 +344,7 @@ export default {
|
||||
uni.$emit('contractSignSuccess', {
|
||||
contractNo: this.contractNo
|
||||
})
|
||||
uni.$emit('refresh-shell-state')
|
||||
})
|
||||
.then(() => {
|
||||
uni.hideLoading()
|
||||
@@ -352,8 +353,8 @@ export default {
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pkgService/my_contract/my_contract'
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}, 600)
|
||||
})
|
||||
|
||||
554
pkgService/lost_found_publish/lost_found_publish.vue
Normal file
554
pkgService/lost_found_publish/lost_found_publish.vue
Normal file
@@ -0,0 +1,554 @@
|
||||
<template>
|
||||
<view class="lost-found-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar
|
||||
:isBack="true"
|
||||
:bottomShadow="false"
|
||||
:backgroundColor="navBarBackgroundColor"
|
||||
:customBackStyle="navBarBackStyle"
|
||||
:fixed="true"
|
||||
>
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black" :style="{ opacity: navTitleOpacity }">{{ t('navTitle') }}</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y :show-scrollbar="false" @scroll="handleScroll">
|
||||
<view class="page-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '220rpx' }">
|
||||
<view class="hero-header tn-margin-bottom-xl">
|
||||
<view class="hero-badge">
|
||||
<text class="badge-dot"></text>
|
||||
<text>{{ t('heroBadge') }}</text>
|
||||
</view>
|
||||
<view class="hero-title">{{ t('heroTitle') }}</view>
|
||||
<view class="hero-desc">{{ t('heroDesc') }}</view>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="block-title">{{ t('formTitle') }}</view>
|
||||
<view class="block-subtitle">{{ t('formDesc') }}</view>
|
||||
|
||||
<view class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('typeLabel') }}</view>
|
||||
<picker mode="selector" :range="typeLabels" @change="handleTypeChange">
|
||||
<view class="form-picker">
|
||||
{{ selectedTypeLabel || t('typePlaceholder') }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('itemTypeLabel') }}</view>
|
||||
<input v-model="form.itemType" class="form-input" :placeholder="t('itemTypePlaceholder')" maxlength="40" />
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('itemNameLabel') }}</view>
|
||||
<input v-model="form.itemName" class="form-input" :placeholder="t('itemNamePlaceholder')" maxlength="60" />
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('placeLabel') }}</view>
|
||||
<input v-model="form.place" class="form-input" :placeholder="t('placePlaceholder')" maxlength="100" />
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('whenLabel') }}</view>
|
||||
<view class="picker-row">
|
||||
<picker mode="date" :value="form.whenDate" @change="handleDateChange">
|
||||
<view class="form-picker">{{ form.whenDate || t('selectDate') }}</view>
|
||||
</picker>
|
||||
<picker mode="time" :value="form.whenTime" @change="handleTimeChange">
|
||||
<view class="form-picker form-picker--time">{{ form.whenTime || t('selectTime') }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="field-tip">{{ t('whenTip') }}</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('contractLabel') }}</view>
|
||||
<input v-model="form.contract" class="form-input" :placeholder="t('contractPlaceholder')" maxlength="40" />
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('contractPhoneLabel') }}</view>
|
||||
<input v-model="form.contractPhone" class="form-input" :placeholder="t('contractPhonePlaceholder')" maxlength="30" />
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('remarkLabel') }}</view>
|
||||
<textarea
|
||||
v-model="form.remark"
|
||||
class="form-textarea"
|
||||
:placeholder="t('remarkPlaceholder')"
|
||||
maxlength="300"
|
||||
></textarea>
|
||||
<view class="field-counter">{{ (form.remark || '').length }}/300</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('imagesLabel') }}</view>
|
||||
<tn-image-upload
|
||||
:key="uploadInstanceKey"
|
||||
:fileList="form.photos"
|
||||
:action="uploadAction"
|
||||
:header="uploadHeaders"
|
||||
:formData="uploadFormData"
|
||||
:max-count="6"
|
||||
:max-size="5 * 1024 * 1024"
|
||||
uploadText=""
|
||||
@on-success="onUploadSuccess"
|
||||
@on-list-change="onPhotoListChange"
|
||||
@on-remove="onRemoveImage"
|
||||
@on-error="onUploadError"
|
||||
@on-oversize="onUploadOversize"
|
||||
@on-exceed="onUploadExceed"
|
||||
>
|
||||
<template #addBtn>
|
||||
<view class="upload-trigger">
|
||||
<text class="tn-icon-camera-fill upload-trigger__icon"></text>
|
||||
<view class="upload-trigger__text">{{ t('uploadImage') }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</tn-image-upload>
|
||||
<view class="field-tip">{{ t('imagesTip') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="floating-submit-bar">
|
||||
<view class="floating-submit-bar__inner">
|
||||
<view class="floating-submit-tip">{{ t('submitTip') }}</view>
|
||||
<view class="submit-btn btn-primary" :class="{ 'submit-btn--disabled': isSubmitting }" @click="submitForm">
|
||||
<text class="tn-text-bold">{{ isSubmitting ? t('submitting') : t('submitButton') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { API_BASE_URL } from '@/utils/request'
|
||||
import { getAuthToken } from '@/utils/app-state'
|
||||
import { addFilePrefix } from '@/utils/common'
|
||||
import { lostAndFoundPublish } from '@/api/index.js'
|
||||
import { getStoredUser } from '@/utils/flow'
|
||||
|
||||
function pickReadableValue(...values) {
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const text = String(values[index] || '').trim()
|
||||
if (text) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'LostFoundPublishPage',
|
||||
data() {
|
||||
return {
|
||||
scrollTop: 0,
|
||||
isSubmitting: false,
|
||||
uploadInstanceKey: 0,
|
||||
form: this.createDefaultForm()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage ? this.$getLanguage() : 'zh'
|
||||
},
|
||||
navBarBackgroundColor() {
|
||||
if (this.scrollTop < 40) return 'rgba(255, 255, 255, 0)'
|
||||
return 'rgba(255, 255, 255, 0.96)'
|
||||
},
|
||||
navBarBackStyle() {
|
||||
const isDarkBg = this.scrollTop < 40
|
||||
return {
|
||||
transition: 'all 0.2s ease',
|
||||
background: isDarkBg ? 'rgba(0, 0, 0, 0.2)' : 'transparent',
|
||||
borderRadius: '50%',
|
||||
width: '64rpx',
|
||||
height: '64rpx',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '12rpx',
|
||||
color: isDarkBg ? '#FFFFFF' : '#000000'
|
||||
}
|
||||
},
|
||||
navTitleOpacity() {
|
||||
return Math.min(Math.max((this.scrollTop - 40) / 50, 0), 1)
|
||||
},
|
||||
typeOptions() {
|
||||
return [
|
||||
{ value: 1, label: this.t('typeLost'), labelEn: this.t('typeLost') },
|
||||
{ value: 2, label: this.t('typeFound'), labelEn: this.t('typeFound') }
|
||||
]
|
||||
},
|
||||
typeLabels() {
|
||||
return this.typeOptions.map(item => item.label)
|
||||
},
|
||||
selectedTypeLabel() {
|
||||
const current = this.typeOptions.find(item => item.value === this.form.type)
|
||||
return current ? current.label : ''
|
||||
},
|
||||
uploadAction() {
|
||||
return `${String(API_BASE_URL || '').replace(/\/$/, '')}/file/common/upload`
|
||||
},
|
||||
uploadHeaders() {
|
||||
const token = getAuthToken()
|
||||
return {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
'X-Requested-Language': this.language === 'en' ? 'en' : 'zh'
|
||||
}
|
||||
},
|
||||
uploadFormData() {
|
||||
return {
|
||||
ProviderType: 'WechatMiniProgram'
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.prefillContact()
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgService.lost_found_publish.lost_found_publish', key, params)
|
||||
},
|
||||
createDefaultForm() {
|
||||
return {
|
||||
type: 1,
|
||||
itemType: '',
|
||||
itemName: '',
|
||||
place: '',
|
||||
whenDate: '',
|
||||
whenTime: '',
|
||||
contract: '',
|
||||
contractPhone: '',
|
||||
remark: '',
|
||||
photos: [],
|
||||
imageUrls: []
|
||||
}
|
||||
},
|
||||
prefillContact() {
|
||||
const user = getStoredUser() || {}
|
||||
this.form.contract = this.form.contract || pickReadableValue(user.name, user.realName, user.username)
|
||||
this.form.contractPhone = this.form.contractPhone || pickReadableValue(user.phone, user.mobile, user.tel)
|
||||
},
|
||||
handleScroll(e) {
|
||||
this.scrollTop = e.detail.scrollTop
|
||||
},
|
||||
handleTypeChange(event) {
|
||||
const index = Number(event && event.detail ? event.detail.value : 0)
|
||||
const target = this.typeOptions[index]
|
||||
this.form.type = target ? target.value : 1
|
||||
},
|
||||
handleDateChange(event) {
|
||||
this.form.whenDate = event && event.detail ? event.detail.value : ''
|
||||
},
|
||||
handleTimeChange(event) {
|
||||
this.form.whenTime = event && event.detail ? event.detail.value : ''
|
||||
},
|
||||
resolveWhenValue() {
|
||||
if (!this.form.whenDate) {
|
||||
return null
|
||||
}
|
||||
const timeText = this.form.whenTime || '00:00'
|
||||
return `${this.form.whenDate}T${timeText}:00`
|
||||
},
|
||||
resolveUploadedFileUrl(uploadResult) {
|
||||
return pickReadableValue(
|
||||
uploadResult && uploadResult.data && uploadResult.data.url,
|
||||
uploadResult && uploadResult.data && uploadResult.data.fileUrl,
|
||||
uploadResult && uploadResult.data && uploadResult.data.path,
|
||||
uploadResult && uploadResult.data,
|
||||
uploadResult && uploadResult.url,
|
||||
uploadResult && uploadResult.fileUrl,
|
||||
uploadResult && uploadResult.path,
|
||||
typeof uploadResult === 'string' ? uploadResult : ''
|
||||
)
|
||||
},
|
||||
onUploadSuccess(data, index, list) {
|
||||
let parsedData = data
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
parsedData = JSON.parse(data)
|
||||
} catch (error) {}
|
||||
}
|
||||
const resourceUrl = this.resolveUploadedFileUrl(parsedData)
|
||||
if (!resourceUrl) {
|
||||
return
|
||||
}
|
||||
this.form.photos = Array.isArray(list) ? list : this.form.photos
|
||||
this.form.imageUrls.splice(index, 1, resourceUrl)
|
||||
},
|
||||
onPhotoListChange(list) {
|
||||
this.form.photos = Array.isArray(list) ? list : []
|
||||
},
|
||||
onRemoveImage(index) {
|
||||
this.form.imageUrls.splice(index, 1)
|
||||
},
|
||||
onUploadError() {
|
||||
uni.showToast({ title: this.t('uploadFailed'), icon: 'none' })
|
||||
},
|
||||
onUploadOversize() {
|
||||
uni.showToast({ title: this.t('uploadOversize'), icon: 'none' })
|
||||
},
|
||||
onUploadExceed() {
|
||||
uni.showToast({ title: this.t('uploadExceed'), icon: 'none' })
|
||||
},
|
||||
getValidationMessage() {
|
||||
if (!String(this.form.itemType || '').trim()) return this.t('validateItemType')
|
||||
if (!String(this.form.itemName || '').trim()) return this.t('validateItemName')
|
||||
if (!String(this.form.place || '').trim()) return this.t('validatePlace')
|
||||
if (!String(this.form.contract || '').trim()) return this.t('validateContract')
|
||||
if (!String(this.form.contractPhone || '').trim()) return this.t('validateContractPhone')
|
||||
return ''
|
||||
},
|
||||
buildSubmitPayload() {
|
||||
return {
|
||||
type: Number(this.form.type) || 1,
|
||||
itemType: String(this.form.itemType || '').trim(),
|
||||
itemName: String(this.form.itemName || '').trim(),
|
||||
place: String(this.form.place || '').trim(),
|
||||
when: this.resolveWhenValue(),
|
||||
contract: String(this.form.contract || '').trim(),
|
||||
contractPhone: String(this.form.contractPhone || '').trim(),
|
||||
remark: String(this.form.remark || '').trim() || null,
|
||||
imageUrls: this.form.imageUrls.length ? this.form.imageUrls.slice() : null
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.uploadInstanceKey += 1
|
||||
this.form = this.createDefaultForm()
|
||||
this.prefillContact()
|
||||
},
|
||||
submitForm() {
|
||||
if (this.isSubmitting) {
|
||||
return
|
||||
}
|
||||
const validationMessage = this.getValidationMessage()
|
||||
if (validationMessage) {
|
||||
uni.showToast({ title: validationMessage, icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.isSubmitting = true
|
||||
lostAndFoundPublish(this.buildSubmitPayload(), this.language)
|
||||
.then(() => {
|
||||
uni.showToast({
|
||||
title: this.t('submitSuccess'),
|
||||
icon: 'success'
|
||||
})
|
||||
this.resetForm()
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}, 600)
|
||||
})
|
||||
.catch((error) => {
|
||||
uni.showToast({
|
||||
title: (error && (error.message || error.msg)) || this.t('submitFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.isSubmitting = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.lost-found-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||||
}
|
||||
|
||||
.page-scroll {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.hero-header {
|
||||
padding: 24rpx 8rpx 0;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 18rpx;
|
||||
background: rgba($brand-primary, 0.08);
|
||||
border-radius: 999rpx;
|
||||
color: $brand-primary;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: $brand-primary;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
margin-top: 22rpx;
|
||||
font-size: 52rpx;
|
||||
line-height: 1.2;
|
||||
color: #1A2233;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
margin-top: 16rpx;
|
||||
color: #6A7A99;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 28rpx;
|
||||
box-shadow: 0 12rpx 36rpx rgba(43, 142, 255, 0.06);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.8);
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.block-subtitle {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #7C8AA5;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1A2233;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-picker,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
background: #F8FAFF;
|
||||
border-radius: 22rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.08);
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-picker {
|
||||
min-height: 96rpx;
|
||||
padding: 0 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 180rpx;
|
||||
padding: 26rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.picker-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.picker-row picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-picker--time {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.field-tip {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #8A97AE;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.field-counter {
|
||||
margin-top: 12rpx;
|
||||
text-align: right;
|
||||
font-size: 22rpx;
|
||||
color: #9AA6BC;
|
||||
}
|
||||
|
||||
.upload-trigger {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 22rpx;
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.08) 0%, rgba($brand-primary, 0.03) 100%);
|
||||
border: 1rpx dashed rgba($brand-primary, 0.26);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-trigger__icon {
|
||||
font-size: 40rpx;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.upload-trigger__text {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.floating-submit-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: linear-gradient(180deg, rgba(248, 250, 255, 0) 0%, rgba(248, 250, 255, 0.98) 36%, rgba(248, 250, 255, 1) 100%);
|
||||
}
|
||||
|
||||
.floating-submit-bar__inner {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-radius: 28rpx;
|
||||
padding: 22rpx;
|
||||
box-shadow: 0 12rpx 36rpx rgba(43, 142, 255, 0.08);
|
||||
}
|
||||
|
||||
.floating-submit-tip {
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #7C8AA5;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.92) 0%, $brand-primary 100%);
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 12rpx 28rpx rgba($brand-primary, 0.24);
|
||||
}
|
||||
|
||||
.submit-btn--disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -109,14 +109,51 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { applicationFormMePage } from '@/api/index.js'
|
||||
import { applicationFormMePage, orderSearchedReservationFlow } from '@/api/index.js'
|
||||
import { saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
|
||||
import {
|
||||
attachReservationFlow,
|
||||
buildReservationFlowMap,
|
||||
getReservationOrderNo,
|
||||
parseReservationFlowResponse
|
||||
} from '@/utils/reservation-flow'
|
||||
|
||||
function formatPageResponse(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
return payload && typeof payload === 'object' ? payload : {}
|
||||
}
|
||||
|
||||
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间[::]|意向房型[::]|意向区域[::]|viewing request|viewing time[::]|preferred room type[::]|preferred area[::]/i
|
||||
|
||||
function isViewingApplicationRecord(item = {}) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const typeText = [
|
||||
item.businessType,
|
||||
item.applicationType,
|
||||
item.applicationTypeDesc,
|
||||
item.formType,
|
||||
item.bizType,
|
||||
item.type,
|
||||
item.typeDesc
|
||||
]
|
||||
.filter(value => value !== null && typeof value !== 'undefined' && String(value).trim())
|
||||
.join(' ')
|
||||
|
||||
if (/viewing|看房|带看/i.test(typeText)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ([item.appointmentViewingTime, item.viewingTime, item.bookViewingTime].some(value => String(value || '').trim())) {
|
||||
return true
|
||||
}
|
||||
|
||||
const remark = item && item.remark ? String(item.remark) : ''
|
||||
return VIEWING_APPLICATION_PATTERN.test(remark)
|
||||
}
|
||||
|
||||
function normalizeApplicationList(list = []) {
|
||||
if (!Array.isArray(list)) {
|
||||
return []
|
||||
@@ -124,11 +161,9 @@ function normalizeApplicationList(list = []) {
|
||||
return list
|
||||
.filter(item => item && typeof item === 'object')
|
||||
.map((item) => {
|
||||
const remark = item && item.remark ? String(item.remark) : ''
|
||||
const isViewingApplication = /预约看房申请|看房时间[::]|意向房型[::]|意向区域[::]|viewing request|viewing time[::]|preferred room type[::]|preferred area[::]/i.test(remark)
|
||||
return {
|
||||
...item,
|
||||
isViewingApplication
|
||||
isViewingApplication: isViewingApplicationRecord(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -349,7 +384,7 @@ export default {
|
||||
}
|
||||
|
||||
await applicationFormMePage(params, this.language)
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
const pageData = formatPageResponse(response)
|
||||
console.log('pageData', pageData)
|
||||
const nextList = normalizeApplicationList(pageData.data || [])
|
||||
@@ -358,12 +393,14 @@ export default {
|
||||
this.total = total
|
||||
this.list = normalizeApplicationList(isReload ? nextList : this.list.concat(nextList))
|
||||
this.hasMore = this.list.length < total && nextList.length > 0
|
||||
saveMyApplicationList(this.list)
|
||||
|
||||
const activeCode = this.resolvePreferredReferenceTypeNo(this.list)
|
||||
if (activeCode) {
|
||||
saveActiveApplicationCode(activeCode)
|
||||
}
|
||||
|
||||
await this.loadReservationFlowsForList(this.list)
|
||||
saveMyApplicationList(this.list)
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!isReload) {
|
||||
@@ -416,6 +453,41 @@ export default {
|
||||
const preferredItem = sortedList.find(item => Number(item.status) !== 99) || sortedList[0]
|
||||
return this.resolveReferenceTypeNo(preferredItem)
|
||||
},
|
||||
async loadReservationFlowsForList(list = []) {
|
||||
const baseList = Array.isArray(list) ? list : []
|
||||
const orderNos = [...new Set(
|
||||
baseList
|
||||
.filter(item => item && !item.isViewingApplication)
|
||||
.map(item => getReservationOrderNo(item))
|
||||
.filter(Boolean)
|
||||
)]
|
||||
|
||||
const requestToken = Date.now()
|
||||
this._reservationFlowRequestToken = requestToken
|
||||
|
||||
const applyFlowMap = (flowMap = {}) => {
|
||||
if (this._reservationFlowRequestToken !== requestToken) {
|
||||
return
|
||||
}
|
||||
this.list = (Array.isArray(this.list) ? this.list : []).map(item => attachReservationFlow(item, flowMap))
|
||||
}
|
||||
|
||||
if (!orderNos.length) {
|
||||
applyFlowMap({})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await orderSearchedReservationFlow({
|
||||
orderNos
|
||||
}, this.language)
|
||||
const flowMap = buildReservationFlowMap(parseReservationFlowResponse(response))
|
||||
applyFlowMap(flowMap)
|
||||
} catch (error) {
|
||||
console.log('[my_application] load reservation flow failed', error)
|
||||
applyFlowMap({})
|
||||
}
|
||||
},
|
||||
goToBillPage(event) {
|
||||
const dataset = event && event.currentTarget ? event.currentTarget.dataset || {} : {}
|
||||
const referenceTypeNo = dataset.referenceTypeNo || dataset.referenceTypeNo === 0
|
||||
@@ -437,10 +509,6 @@ export default {
|
||||
goToApplicationDetail(item = {}) {
|
||||
const code = item.detailCode || ''
|
||||
if (!code) {
|
||||
uni.showToast({
|
||||
title: this.t('missingApplicationCode'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
|
||||
1130
pkgService/room_inspection/room_inspection.vue
Normal file
1130
pkgService/room_inspection/room_inspection.vue
Normal file
File diff suppressed because it is too large
Load Diff
314
pkgService/submit_request/components/BuildingSelector.vue
Normal file
314
pkgService/submit_request/components/BuildingSelector.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<view class="building-selector">
|
||||
<view
|
||||
class="selector-header tn-flex tn-flex-row-between tn-flex-col-center"
|
||||
:class="{ 'selector-header--disabled': !areaCode }"
|
||||
@click="handleOpen"
|
||||
>
|
||||
<view class="selected-info">
|
||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedBuilding }">
|
||||
{{ selectedBuilding ? getBuildingDisplayName(selectedBuilding) : placeholderText }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="tn-icon-right" :class="areaCode ? 'tn-color-gray' : 'tn-color-disabled'"></text>
|
||||
</view>
|
||||
|
||||
<tn-popup v-model="showPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true">
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">{{ t('popupTitle') }}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="building-list-scroll">
|
||||
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
||||
<tn-loading mode="circle"></tn-loading>
|
||||
<view class="tn-margin-top-sm tn-color-gray">{{ t('loadingText') }}</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!buildingList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||
<text class="tn-color-gray">{{ t('emptyText') }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="building-list">
|
||||
<view
|
||||
v-for="item in buildingList"
|
||||
:key="item.code"
|
||||
class="building-item"
|
||||
:class="{ 'is-active': selectedBuilding && selectedBuilding.code === item.code }"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<text class="building-name">{{ getBuildingDisplayName(item) }}</text>
|
||||
<text v-if="selectedBuilding && selectedBuilding.code === item.code" class="tn-icon-success-circle-fill building-check"></text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</tn-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { buildingPagination } from '@/api/index.js'
|
||||
|
||||
function normalizeText(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return ''
|
||||
}
|
||||
return String(value).trim()
|
||||
}
|
||||
|
||||
function normalizeBuildingItem(item = {}) {
|
||||
return {
|
||||
areaCode: normalizeText(item.areaCode),
|
||||
code: normalizeText(item.code),
|
||||
name: normalizeText(item.name),
|
||||
nameEn: normalizeText(item.nameEn),
|
||||
sort: Number(item.sort || 0),
|
||||
buildingSections: Array.isArray(item.buildingSections) ? item.buildingSections : []
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'BuildingSelector',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
areaCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
roomTypeCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
isLoading: false,
|
||||
buildingList: [],
|
||||
selectedBuilding: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage ? this.$getLanguage() : 'zh'
|
||||
},
|
||||
placeholderText() {
|
||||
return this.areaCode ? this.t('selectBuilding') : this.t('selectAreaFirst')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.syncSelection()
|
||||
},
|
||||
language() {
|
||||
this.loadData(true)
|
||||
},
|
||||
areaCode() {
|
||||
this.handleAreaChange()
|
||||
},
|
||||
roomTypeCode(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.handleRoomTypeChange()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgService.submit_request.components.BuildingSelector', key, params)
|
||||
},
|
||||
handleOpen() {
|
||||
if (!this.areaCode) {
|
||||
uni.showToast({
|
||||
title: this.t('selectAreaFirst'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.showPopup = true
|
||||
this.loadData()
|
||||
},
|
||||
handleAreaChange() {
|
||||
this.buildingList = []
|
||||
this.selectedBuilding = null
|
||||
if (this.value) {
|
||||
this.$emit('input', '')
|
||||
this.$emit('change', { building: null })
|
||||
}
|
||||
if (!this.areaCode) {
|
||||
return
|
||||
}
|
||||
this.loadData(true)
|
||||
},
|
||||
handleRoomTypeChange() {
|
||||
this.buildingList = []
|
||||
this.selectedBuilding = null
|
||||
if (this.value) {
|
||||
this.$emit('input', '')
|
||||
this.$emit('change', { building: null })
|
||||
}
|
||||
if (!this.areaCode) {
|
||||
return
|
||||
}
|
||||
this.loadData(true)
|
||||
},
|
||||
loadData(force = false) {
|
||||
if (!this.areaCode) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (!force && this.buildingList.length) {
|
||||
this.syncSelection()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
return buildingPagination({
|
||||
AreaCode: this.areaCode,
|
||||
PageIndex: 1,
|
||||
PageSize: 100
|
||||
}, this.language)
|
||||
.then((res) => {
|
||||
const payload = res && res.data ? res.data : res
|
||||
const rawList = Array.isArray(payload && payload.data)
|
||||
? payload.data
|
||||
: (Array.isArray(payload) ? payload : [])
|
||||
this.buildingList = rawList
|
||||
.map(item => normalizeBuildingItem(item))
|
||||
.filter(item => item.code)
|
||||
.sort((a, b) => a.sort - b.sort || a.code.localeCompare(b.code))
|
||||
this.syncSelection()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('load building list error', error)
|
||||
this.buildingList = []
|
||||
this.selectedBuilding = null
|
||||
uni.showToast({
|
||||
title: this.t('loadFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
syncSelection() {
|
||||
if (!this.value || !this.buildingList.length) {
|
||||
this.selectedBuilding = null
|
||||
return
|
||||
}
|
||||
this.selectedBuilding = this.buildingList.find(item => item.code === this.value) || null
|
||||
},
|
||||
handleSelect(item) {
|
||||
this.selectedBuilding = item
|
||||
this.$emit('input', item.code)
|
||||
this.$emit('change', { building: item })
|
||||
this.showPopup = false
|
||||
},
|
||||
getBuildingDisplayName(item = {}) {
|
||||
if (this.language === 'en') {
|
||||
return item.nameEn || item.name || item.code || ''
|
||||
}
|
||||
return item.name || item.nameEn || item.code || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.building-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 26rpx 32rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||||
min-height: 100rpx;
|
||||
}
|
||||
|
||||
.selector-header--disabled {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selected-name {
|
||||
font-size: 28rpx;
|
||||
color: #1A2233;
|
||||
font-weight: 600;
|
||||
|
||||
&.is-empty {
|
||||
color: #A0B0C0;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #F8FAFF;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
padding: 40rpx 30rpx 20rpx;
|
||||
text-align: center;
|
||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.building-list-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 20rpx 30rpx 60rpx;
|
||||
}
|
||||
|
||||
.building-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.building-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 30rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.12);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.is-active {
|
||||
border-color: rgba($brand-primary, 0.5);
|
||||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.building-name {
|
||||
font-size: 28rpx;
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.building-check {
|
||||
color: $brand-primary;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
</style>
|
||||
308
pkgService/submit_request/components/FloorSelector.vue
Normal file
308
pkgService/submit_request/components/FloorSelector.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<view class="floor-selector">
|
||||
<view
|
||||
class="selector-header tn-flex tn-flex-row-between tn-flex-col-center"
|
||||
@click="handleOpen"
|
||||
>
|
||||
<view class="selected-info">
|
||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedFloor }">
|
||||
{{ selectedFloor ? getFloorDisplayName(selectedFloor) : placeholderText }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="tn-icon-right tn-color-gray"></text>
|
||||
</view>
|
||||
|
||||
<tn-popup v-model="showPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true">
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">{{ t('popupTitle') }}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="floor-list-scroll">
|
||||
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
|
||||
<tn-loading mode="circle"></tn-loading>
|
||||
<view class="tn-margin-top-sm tn-color-gray">{{ t('loadingText') }}</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!floorList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||
<text class="tn-color-gray">{{ t('emptyText') }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="floor-list">
|
||||
<view
|
||||
class="floor-item floor-item--clear"
|
||||
:class="{ 'is-active': !selectedFloor }"
|
||||
@click="handleClear"
|
||||
>
|
||||
<text class="floor-name">{{ t('clearSelection') }}</text>
|
||||
<text v-if="!selectedFloor" class="tn-icon-success-circle-fill floor-check"></text>
|
||||
</view>
|
||||
<view
|
||||
v-for="item in floorList"
|
||||
:key="item.value"
|
||||
class="floor-item"
|
||||
:class="{ 'is-active': selectedFloor && selectedFloor.value === item.value }"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<text class="floor-name">{{ getFloorDisplayName(item) }}</text>
|
||||
<text v-if="selectedFloor && selectedFloor.value === item.value" class="tn-icon-success-circle-fill floor-check"></text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</tn-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getEnums } from '@/api/index.js'
|
||||
|
||||
function pickFirstDefined(...values) {
|
||||
const matched = values.find(value => value !== undefined && value !== null && value !== '')
|
||||
return matched === undefined ? '' : matched
|
||||
}
|
||||
|
||||
function normalizeFloorValue(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return ''
|
||||
}
|
||||
return String(value).trim()
|
||||
}
|
||||
|
||||
function resolveFloorList(response) {
|
||||
const payload = response && response.data !== undefined ? response.data : response
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
const floorEnum = payload.find(item => item && item.code === 'FloorLevelEnum')
|
||||
if (floorEnum && Array.isArray(floorEnum.data)) {
|
||||
return floorEnum.data
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
if (payload && Array.isArray(payload.data)) {
|
||||
return payload.data
|
||||
}
|
||||
|
||||
if (payload && payload.data && Array.isArray(payload.data.data)) {
|
||||
return payload.data.data
|
||||
}
|
||||
|
||||
if (payload && Array.isArray(payload.items)) {
|
||||
return payload.items
|
||||
}
|
||||
|
||||
if (payload && Array.isArray(payload.records)) {
|
||||
return payload.records
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function normalizeFloorItem(item = {}) {
|
||||
const code = normalizeFloorValue(pickFirstDefined(item.key, item.code, item.value))
|
||||
const name = normalizeFloorValue(pickFirstDefined(item.cname, item.nameZh, item.labelZh, item.name, item.value))
|
||||
const nameEn = normalizeFloorValue(pickFirstDefined(item.ename, item.nameEn, item.labelEn, item.descriptionEn, ''))
|
||||
return {
|
||||
value: code || name,
|
||||
code,
|
||||
name,
|
||||
nameEn,
|
||||
sort: Number(pickFirstDefined(item.sort, item.key, 0))
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'FloorSelector',
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
isLoading: false,
|
||||
floorList: [],
|
||||
selectedFloor: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage ? this.$getLanguage() : 'zh'
|
||||
},
|
||||
placeholderText() {
|
||||
return this.t('selectFloor')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.syncSelection()
|
||||
},
|
||||
language() {
|
||||
this.loadData(true)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgService.submit_request.components.FloorSelector', key, params)
|
||||
},
|
||||
handleOpen() {
|
||||
this.showPopup = true
|
||||
this.loadData()
|
||||
},
|
||||
loadData(force = false) {
|
||||
if (!force && this.floorList.length) {
|
||||
this.syncSelection()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
return getEnums({}, this.language)
|
||||
.then((res) => {
|
||||
const rawList = resolveFloorList(res)
|
||||
this.floorList = rawList
|
||||
.map(item => normalizeFloorItem(item))
|
||||
.filter(item => item.value)
|
||||
.sort((a, b) => a.sort - b.sort || a.value.localeCompare(b.value))
|
||||
this.syncSelection()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('load floor list error', error)
|
||||
this.floorList = []
|
||||
this.selectedFloor = null
|
||||
uni.showToast({
|
||||
title: this.t('loadFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
syncSelection() {
|
||||
const currentValue = normalizeFloorValue(this.value)
|
||||
if (!currentValue || !this.floorList.length) {
|
||||
this.selectedFloor = null
|
||||
return
|
||||
}
|
||||
this.selectedFloor = this.floorList.find(item => item.value === currentValue) || null
|
||||
},
|
||||
handleSelect(item) {
|
||||
this.selectedFloor = item
|
||||
this.$emit('input', item.value)
|
||||
this.$emit('change', { floor: item })
|
||||
this.showPopup = false
|
||||
},
|
||||
handleClear() {
|
||||
this.selectedFloor = null
|
||||
this.$emit('input', '')
|
||||
this.$emit('change', { floor: null })
|
||||
this.showPopup = false
|
||||
},
|
||||
getFloorDisplayName(item = {}) {
|
||||
if (this.language === 'en') {
|
||||
return item.nameEn || item.name || item.code || ''
|
||||
}
|
||||
return item.name || item.nameEn || item.code || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.floor-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 26rpx 32rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||||
min-height: 100rpx;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selected-name {
|
||||
font-size: 28rpx;
|
||||
color: #1A2233;
|
||||
font-weight: 600;
|
||||
|
||||
&.is-empty {
|
||||
color: #A0B0C0;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #F8FAFF;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
padding: 40rpx 30rpx 20rpx;
|
||||
text-align: center;
|
||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.floor-list-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 20rpx 30rpx 60rpx;
|
||||
}
|
||||
|
||||
.floor-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.floor-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 30rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.12);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.is-active {
|
||||
border-color: rgba($brand-primary, 0.5);
|
||||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.floor-item--clear {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.floor-name {
|
||||
font-size: 28rpx;
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.floor-check {
|
||||
color: $brand-primary;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="room-type-selector">
|
||||
<view class="selector-header tn-flex tn-flex-row-between tn-flex-col-center" @click="showPopup = true">
|
||||
<view class="selector-header tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
|
||||
<view class="selected-info">
|
||||
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
|
||||
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : t('selectRoomType') }}
|
||||
@@ -24,11 +24,11 @@
|
||||
<view class="tn-margin-top-sm tn-color-gray">{{ t('loadingRoomTypes') }}</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!areaList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||
<view v-else-if="!displayAreaList.length" class="empty-state tn-padding-xl tn-text-center">
|
||||
<text class="tn-color-gray">{{ t('emptyRoomTypes') }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="area-group" v-for="(area, index) in areaList" :key="area.code">
|
||||
<view v-else class="area-group" v-for="(area, index) in displayAreaList" :key="area.code">
|
||||
<view class="area-title tn-flex tn-flex-row-between tn-flex-col-center" @click="toggleArea(index)">
|
||||
<view class="tn-flex tn-flex-col-center">
|
||||
<text class="area-dot"></text>
|
||||
@@ -69,6 +69,10 @@ export default {
|
||||
initialAreaCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
buildingCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -84,6 +88,19 @@ export default {
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage ? this.$getLanguage() : 'zh'
|
||||
},
|
||||
displayAreaList() {
|
||||
if (!this.buildingCode) {
|
||||
return this.areaList
|
||||
}
|
||||
return this.areaList
|
||||
.map(area => ({
|
||||
...area,
|
||||
roomTypes: Array.isArray(area.roomTypes)
|
||||
? area.roomTypes.filter(roomType => !roomType.buildingCode || roomType.buildingCode === this.buildingCode)
|
||||
: []
|
||||
}))
|
||||
.filter(area => area.roomTypes && area.roomTypes.length > 0)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -92,6 +109,9 @@ export default {
|
||||
},
|
||||
language() {
|
||||
this.loadData(true)
|
||||
},
|
||||
buildingCode() {
|
||||
this.syncSelection()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -101,6 +121,10 @@ export default {
|
||||
t(key) {
|
||||
return this.$translate(this.i18n, key)
|
||||
},
|
||||
openSelector() {
|
||||
this.showPopup = true
|
||||
this.loadData()
|
||||
},
|
||||
loadData(force = false) {
|
||||
if (!force && this.areaList.length > 0) return
|
||||
|
||||
@@ -131,14 +155,16 @@ export default {
|
||||
})
|
||||
},
|
||||
syncSelection() {
|
||||
if (!this.areaList.length) return
|
||||
if (!this.displayAreaList.length) return
|
||||
|
||||
let foundArea = null
|
||||
let foundRoom = null
|
||||
const previousAreaCode = this.selectedArea && this.selectedArea.code ? this.selectedArea.code : ''
|
||||
const previousRoomCode = this.selectedRoomType && this.selectedRoomType.code ? this.selectedRoomType.code : ''
|
||||
|
||||
// Try to find by room type code
|
||||
if (this.value) {
|
||||
for (const area of this.areaList) {
|
||||
for (const area of this.displayAreaList) {
|
||||
const room = area.roomTypes.find(r => r.code === this.value)
|
||||
if (room) {
|
||||
foundArea = area
|
||||
@@ -150,7 +176,7 @@ export default {
|
||||
|
||||
// If not found by room, try to find by initial area code
|
||||
if (!foundRoom && this.initialAreaCode) {
|
||||
foundArea = this.areaList.find(a => a.code === this.initialAreaCode)
|
||||
foundArea = this.displayAreaList.find(a => a.code === this.initialAreaCode)
|
||||
}
|
||||
|
||||
if (foundArea) {
|
||||
@@ -159,12 +185,18 @@ export default {
|
||||
this.selectedRoomType = foundRoom
|
||||
}
|
||||
|
||||
// If we found the room, or we just want to initialize the area but no room yet
|
||||
if (foundRoom && this.value !== foundRoom.code) {
|
||||
if (foundRoom) {
|
||||
if (this.value !== foundRoom.code) {
|
||||
this.$emit('input', foundRoom.code)
|
||||
}
|
||||
if (previousAreaCode !== foundArea.code || previousRoomCode !== foundRoom.code) {
|
||||
this.$emit('change', { area: foundArea, roomType: foundRoom })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.selectedArea = null
|
||||
this.selectedRoomType = null
|
||||
}
|
||||
},
|
||||
handleSelect(area, roomType) {
|
||||
this.selectedArea = area
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</tn-nav-bar>
|
||||
|
||||
<scroll-view class="page-scroll" scroll-y @scroll="handleScroll" :show-scrollbar="false">
|
||||
<view class="page-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '60rpx' }">
|
||||
<view class="page-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: pageBottomPadding }">
|
||||
<view class="hero-header tn-margin-bottom-xl">
|
||||
<view class="hero-badge">
|
||||
<text class="badge-dot"></text>
|
||||
@@ -20,13 +20,6 @@
|
||||
</view>
|
||||
<view class="hero-title">{{ pageTitle }}</view>
|
||||
<view class="hero-desc">{{ pageDesc }}</view>
|
||||
|
||||
<view v-if="sourceSummaryList.length" class="tn-margin-top-lg">
|
||||
<view v-for="item in sourceSummaryList" :key="item.label" class="source-row tn-flex tn-flex-row-between">
|
||||
<text class="source-row__label">{{ item.label }}</text>
|
||||
<text class="source-row__value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-card tn-margin-top-xl">
|
||||
@@ -42,6 +35,14 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="showFloorSelector" class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ floorLabel }}</view>
|
||||
<floor-selector
|
||||
v-model="form.intendedFloorLevel"
|
||||
@change="handleFloorChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="showPreferenceSelectors" class="form-item tn-margin-top">
|
||||
<view class="form-label">{{ t('hobbyLabel') }}</view>
|
||||
<tag-selector
|
||||
@@ -79,13 +80,22 @@
|
||||
<textarea class="form-textarea" :placeholder="remarkPlaceholder" v-model="form.remark"></textarea>
|
||||
</view>
|
||||
|
||||
<view class="submit-btn btn-primary tn-margin-top-xl" :class="{ 'submit-btn--disabled': isSubmitting }" @click="submitRequest">
|
||||
<view v-if="!showFloatingSubmitBtn" class="submit-btn btn-primary tn-margin-top-xl" :class="{ 'submit-btn--disabled': isSubmitting }" @click="submitRequest">
|
||||
<text class="tn-text-bold">{{ isSubmitting ? t('submitting') : primaryActionText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="showFloatingSubmitBtn" class="floating-submit-bar">
|
||||
<view class="floating-submit-bar__inner">
|
||||
<view class="submit-btn btn-primary" :class="{ 'submit-btn--disabled': isSubmitting }" @click="submitRequest">
|
||||
<text class="tn-text-bold">{{ isSubmitting ? t('submitting') : primaryActionText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -98,6 +108,7 @@ import { getStoredUser } from '../../utils/flow'
|
||||
import TagSelector from './components/TagSelector.vue'
|
||||
import DateRangeSelector from './components/DateRangeSelector.vue'
|
||||
import RoomTypeSelector from './components/RoomTypeSelector.vue'
|
||||
import FloorSelector from './components/FloorSelector.vue'
|
||||
|
||||
function createDefaultForm() {
|
||||
const now = new Date()
|
||||
@@ -107,6 +118,7 @@ function createDefaultForm() {
|
||||
return {
|
||||
intendedAreaCode: '',
|
||||
intendedRoomTypeCode: '',
|
||||
intendedFloorLevel: '',
|
||||
roomSlotHeight: 1,
|
||||
hobbiesAndInterestsTagKeys: [],
|
||||
lifestyleHabitsTagKeys: [],
|
||||
@@ -564,7 +576,8 @@ export default {
|
||||
components: {
|
||||
TagSelector,
|
||||
DateRangeSelector,
|
||||
RoomTypeSelector
|
||||
RoomTypeSelector,
|
||||
FloorSelector
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -743,12 +756,27 @@ export default {
|
||||
roomTypeLabel() {
|
||||
return this.getBusinessText('roomTypeLabel') || this.t('applyRoomType')
|
||||
},
|
||||
pageBottomPadding() {
|
||||
return this.showFloatingSubmitBtn ? '220rpx' : '60rpx'
|
||||
},
|
||||
showFloatingSubmitBtn() {
|
||||
return this.isRoomTypeApply
|
||||
},
|
||||
floorLabel() {
|
||||
return this.getBusinessText('floorLabel') || this.t('floorLabel')
|
||||
},
|
||||
showRoomTypeSelector() {
|
||||
if (this.isRoomTypeApply) {
|
||||
return true
|
||||
}
|
||||
return this.businessConfig ? this.businessConfig.showRoomTypeSelector !== false : true
|
||||
},
|
||||
showFloorSelector() {
|
||||
if (this.isRoomTypeApply) {
|
||||
return true
|
||||
}
|
||||
return this.businessConfig ? this.businessConfig.showFloorSelector !== false : true
|
||||
},
|
||||
showPreferenceSelectors() {
|
||||
if (this.isRoomTypeApply) {
|
||||
return true
|
||||
@@ -775,41 +803,6 @@ export default {
|
||||
},
|
||||
dateRangeEndPlaceholder() {
|
||||
return this.getBusinessText('dateRangeEndPlaceholder') || this.t('selectCheckoutDate')
|
||||
},
|
||||
sourceSummaryList() {
|
||||
const payload = this.sourcePayload || {}
|
||||
if (!Object.keys(payload).length) {
|
||||
return []
|
||||
}
|
||||
const roomTypeName = this.language === 'en'
|
||||
? (payload.roomTypeNameEn || payload.roomTypeName || '')
|
||||
: (payload.roomTypeName || '')
|
||||
const roomLabel = this.language === 'en'
|
||||
? (payload.roomLabelEn || payload.currentRoomLabelEn || payload.roomLabel || payload.currentRoomLabel || '')
|
||||
: (payload.roomLabel || payload.currentRoomLabel || '')
|
||||
const areaName = this.language === 'en'
|
||||
? (payload.areaLabelEn || payload.areaNameEn || payload.areaLabel || payload.areaName || '')
|
||||
: (payload.areaLabel || payload.areaName || '')
|
||||
const list = []
|
||||
if (roomTypeName) {
|
||||
list.push({ label: this.t('applyRoomType'), value: roomTypeName })
|
||||
}
|
||||
if (roomLabel) {
|
||||
list.push({ label: this.t('currentRoom'), value: roomLabel })
|
||||
}
|
||||
if (areaName) {
|
||||
list.push({ label: this.t('areaLabel'), value: areaName })
|
||||
}
|
||||
if (payload.price) {
|
||||
list.push({ label: this.t('referencePrice'), value: payload.price })
|
||||
}
|
||||
if (payload.payMethod) {
|
||||
list.push({ label: this.t('paymentMethod'), value: localizePayMethodText(payload.payMethod, this.language) })
|
||||
}
|
||||
if (this.pageTitle) {
|
||||
list.push({ label: this.t('applicationType'), value: this.pageTitle })
|
||||
}
|
||||
return list
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -943,6 +936,7 @@ export default {
|
||||
...defaultForm,
|
||||
intendedAreaCode: this.sourcePayload.areaCode || '',
|
||||
intendedRoomTypeCode: this.sourcePayload.roomTypeCode || '',
|
||||
intendedFloorLevel: this.sourcePayload.intendedFloorLevel || this.sourcePayload.IntendedFloorLevel || '',
|
||||
remark: '',
|
||||
start: this.sourcePayload.start || defaultForm.start,
|
||||
end: this.sourcePayload.end || ''
|
||||
@@ -967,6 +961,10 @@ export default {
|
||||
this.sourcePayload.roomTypeCode = roomType.code
|
||||
this.sourcePayload.roomTypeName = roomType.name
|
||||
this.sourcePayload.roomTypeNameEn = roomType.nameEn || ''
|
||||
this.sourcePayload.selectedRoomTypeBuildingCode = roomType.buildingCode || ''
|
||||
this.sourcePayload.selectedRoomTypeBuildingSectionCode = roomType.buildingSectionCode || ''
|
||||
this.sourcePayload.selectedRoomTypeBuildingName = roomType.buildingName || ''
|
||||
this.sourcePayload.selectedRoomTypeBuildingSectionName = roomType.buildingSectionName || ''
|
||||
|
||||
if (area) {
|
||||
this.sourcePayload.areaCode = area.code
|
||||
@@ -979,6 +977,15 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
handleFloorChange({ floor }) {
|
||||
if (!this.sourcePayload) {
|
||||
this.sourcePayload = {}
|
||||
}
|
||||
this.sourcePayload.intendedFloorLevel = floor && floor.value ? floor.value : ''
|
||||
this.sourcePayload.intendedFloorName = floor
|
||||
? (this.language === 'en' ? (floor.nameEn || floor.name || '') : (floor.name || floor.nameEn || ''))
|
||||
: ''
|
||||
},
|
||||
loadDictOptions() {
|
||||
this.dictLoading = true
|
||||
return Promise.all([
|
||||
@@ -1052,6 +1059,7 @@ export default {
|
||||
return {
|
||||
intendedAreaCode: this.form.intendedAreaCode || null,
|
||||
intendedRoomTypeCode: this.form.intendedRoomTypeCode || null,
|
||||
IntendedFloorLevel: this.form.intendedFloorLevel || null,
|
||||
roomSlotHeight: Number(this.form.roomSlotHeight),
|
||||
hobbiesAndInterestsTagKeys: this.showPreferenceSelectors && this.form.hobbiesAndInterestsTagKeys.length ? this.form.hobbiesAndInterestsTagKeys : null,
|
||||
lifestyleHabitsTagKeys: this.showPreferenceSelectors && this.form.lifestyleHabitsTagKeys.length ? this.form.lifestyleHabitsTagKeys : null,
|
||||
@@ -1276,6 +1284,22 @@ export default {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.floating-submit-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 30;
|
||||
padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: linear-gradient(180deg, rgba(248, 250, 255, 0) 0%, rgba(248, 250, 255, 0.92) 24%, #F8FAFF 100%);
|
||||
backdrop-filter: blur(16rpx);
|
||||
-webkit-backdrop-filter: blur(16rpx);
|
||||
}
|
||||
|
||||
.floating-submit-bar__inner {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.source-row {
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
||||
@@ -1286,6 +1310,10 @@ export default {
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.source-row--interactive:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.source-row__label {
|
||||
color: #6A7A99;
|
||||
font-size: 26rpx;
|
||||
@@ -1301,4 +1329,5 @@ export default {
|
||||
margin-left: 24rpx;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
211
pkgService/ticket_center/ticket_center.vue
Normal file
211
pkgService/ticket_center/ticket_center.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<view class="ticket-center-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="page-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
|
||||
<view class="hero-card tn-margin-bottom-lg">
|
||||
<view class="hero-badge">
|
||||
<text class="badge-dot"></text>
|
||||
<text>{{ t('heroBadge') }}</text>
|
||||
</view>
|
||||
<view class="hero-title tn-margin-top">{{ t('heroTitle') }}</view>
|
||||
<view class="hero-desc tn-margin-top-sm">{{ t('heroDesc') }}</view>
|
||||
</view>
|
||||
|
||||
<view class="service-grid">
|
||||
<view
|
||||
v-for="item in ticketServices"
|
||||
:key="item.serviceType"
|
||||
class="service-card"
|
||||
@click="openTicketWorkbench(item)"
|
||||
>
|
||||
<view class="service-card__icon"><text :class="item.icon"></text></view>
|
||||
<view class="service-card__title tn-margin-top">{{ getItemText(item, 'title', 'titleEn') }}</view>
|
||||
<view class="service-card__desc tn-margin-top-xs">{{ getItemText(item, 'desc', 'descEn') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TicketCenterPage',
|
||||
i18n: {
|
||||
zh: {
|
||||
navTitle: '工单中心',
|
||||
heroBadge: 'SERVICE',
|
||||
heroTitle: '住中工单服务',
|
||||
heroDesc: '报修、续住、退宿、调宿、送水、清洁和意见建议都统一从这里进入,提交后可继续查看流转记录。'
|
||||
},
|
||||
en: {
|
||||
navTitle: 'Work Orders',
|
||||
heroBadge: 'SERVICE',
|
||||
heroTitle: 'In-stay Ticket Services',
|
||||
heroDesc: 'Repair, renewal, checkout, room change, water delivery, cleaning, and suggestions are all handled here.'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ticketServices: [
|
||||
{
|
||||
serviceType: 'TicketRepairApply',
|
||||
icon: 'tn-icon-set-fill',
|
||||
title: '报修申请',
|
||||
titleEn: 'Repair Request',
|
||||
desc: '设备故障、设施损坏等问题报修',
|
||||
descEn: 'Report device or facility issues'
|
||||
},
|
||||
{
|
||||
serviceType: 'TicketRenewalApply',
|
||||
icon: 'tn-icon-calendar-fill',
|
||||
title: '续住申请',
|
||||
titleEn: 'Renewal',
|
||||
desc: '提交续住时间与诉求',
|
||||
descEn: 'Submit renewal details'
|
||||
},
|
||||
{
|
||||
serviceType: 'TicketCheckOutApply',
|
||||
icon: 'tn-icon-home-in-fill',
|
||||
title: '退宿申请',
|
||||
titleEn: 'Checkout',
|
||||
desc: '提交退宿时间与原因',
|
||||
descEn: 'Submit checkout request'
|
||||
},
|
||||
{
|
||||
serviceType: 'TicketChangeApply',
|
||||
icon: 'tn-icon-home-smile-fill',
|
||||
title: '调宿申请',
|
||||
titleEn: 'Room Change',
|
||||
desc: '选择意向房型并提交调宿需求',
|
||||
descEn: 'Submit room change request'
|
||||
},
|
||||
{
|
||||
serviceType: 'TicketWaterDeliveryApply',
|
||||
icon: 'tn-icon-shop-fill',
|
||||
title: '送水申请',
|
||||
titleEn: 'Water Delivery',
|
||||
desc: '填写送水桶数和送达说明',
|
||||
descEn: 'Request water delivery'
|
||||
},
|
||||
{
|
||||
serviceType: 'TicketCleaningApply',
|
||||
icon: 'tn-icon-clean',
|
||||
title: '清洁服务',
|
||||
titleEn: 'Cleaning Service',
|
||||
desc: '提交清洁需求或预约时间',
|
||||
descEn: 'Submit cleaning request'
|
||||
},
|
||||
{
|
||||
serviceType: 'TicketSuggestionApply',
|
||||
icon: 'tn-icon-message-fill',
|
||||
title: '意见建议',
|
||||
titleEn: 'Suggestion',
|
||||
desc: '反馈建议、投诉或其他诉求',
|
||||
descEn: 'Feedback, suggestions, or complaints'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$translate(this.i18n, key, params)
|
||||
},
|
||||
getItemText(item, fieldZh, fieldEn) {
|
||||
return this.$getLanguage && this.$getLanguage() === 'en'
|
||||
? (item[fieldEn] || item[fieldZh] || '')
|
||||
: (item[fieldZh] || item[fieldEn] || '')
|
||||
},
|
||||
openTicketWorkbench(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pkgService/ticket_workbench/ticket_workbench?serviceType=${encodeURIComponent(item.serviceType)}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ticket-center-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
padding: 32rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 30rpx;
|
||||
box-shadow: 0 16rpx 40rpx rgba(43, 142, 255, 0.08);
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba($brand-primary, 0.08);
|
||||
color: $brand-primary;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: $brand-primary;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 44rpx;
|
||||
color: #1A2233;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
font-size: 26rpx;
|
||||
color: #6A7A99;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20rpx;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
padding: 28rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 28rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(43, 142, 255, 0.06);
|
||||
}
|
||||
|
||||
.service-card__icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.14) 0%, rgba($brand-primary, 0.05) 100%);
|
||||
color: $brand-primary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.service-card__title {
|
||||
font-size: 30rpx;
|
||||
color: #1A2233;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.service-card__desc {
|
||||
font-size: 22rpx;
|
||||
color: #7C8AA5;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
2252
pkgService/ticket_workbench/ticket_workbench.vue
Normal file
2252
pkgService/ticket_workbench/ticket_workbench.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { getEnums, getMyEffectiveContracts, getMyPendingSignContracts, orderSearchedMePage } from '@/api/index.js'
|
||||
import { normalizeBookingList, parseOrderListResponse, pickCurrentBooking, pickCurrentStayOrder } from '@/utils/booking-helper'
|
||||
import { getBuiltInEnums } from '@/utils/enum-store'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
@@ -62,6 +63,21 @@ function logRequestError(name, error) {
|
||||
return []
|
||||
}
|
||||
|
||||
function resolveOrderNo(order = {}) {
|
||||
const candidates = [order.orderNo, order.no, order.code, order.OrderNo]
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
const value = candidates[index]
|
||||
if (value === undefined || value === null) {
|
||||
continue
|
||||
}
|
||||
const text = String(value).trim()
|
||||
if (text) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
let lastFetchStatusTime = 0
|
||||
let fetchUserGlobalStatusPromise = null
|
||||
let globalEnumsPromise = null
|
||||
@@ -89,7 +105,7 @@ const store = new Vuex.Store({
|
||||
vuex_effective_contracts: [],
|
||||
vuex_valid_orders: [],
|
||||
vuex_current_order: lifeData.vuex_current_order ? lifeData.vuex_current_order : null,
|
||||
vuex_enums: {}
|
||||
vuex_enums: getBuiltInEnums(lifeData.vuex_locale || 'zh-Hans')
|
||||
},
|
||||
mutations: {
|
||||
$tStore(state, payload) {
|
||||
@@ -148,14 +164,25 @@ const store = new Vuex.Store({
|
||||
lastFetchStatusTime = now
|
||||
|
||||
fetchUserGlobalStatusPromise = Promise.all([
|
||||
getMyPendingSignContracts({ UserCode: userCode }, language).catch((error) => logRequestError('getMyPendingSignContracts', error)),
|
||||
getMyEffectiveContracts({ UserCode: userCode }, language).catch((error) => logRequestError('getMyEffectiveContracts', error)),
|
||||
orderSearchedMePage({ PageIndex: 1, PageSize: 100, IsExport: false }, language).catch((error) => logRequestError('orderSearchedMePage', error))
|
||||
]).then(([pendingContractsResponse, effectiveContractsResponse, orderResponse]) => {
|
||||
const pendingContracts = parseContractList(pendingContractsResponse)
|
||||
const effectiveContracts = parseContractList(effectiveContractsResponse)
|
||||
]).then(([orderResponse]) => {
|
||||
const validOrders = normalizeBookingList(parseOrderListResponse(orderResponse), language)
|
||||
const currentOrder = pickCurrentStayOrder(validOrders)
|
||||
const currentOrderNo = resolveOrderNo(currentOrder)
|
||||
|
||||
return Promise.all([
|
||||
Promise.resolve(validOrders),
|
||||
Promise.resolve(currentOrder),
|
||||
currentOrderNo
|
||||
? getMyPendingSignContracts({ OrderNo: currentOrderNo, ProviderType: 'WechatMiniProgram' }, language).catch((error) => logRequestError('getMyPendingSignContracts', error))
|
||||
: Promise.resolve([]),
|
||||
currentOrderNo
|
||||
? getMyEffectiveContracts({ OrderNo: currentOrderNo }, language).catch((error) => logRequestError('getMyEffectiveContracts', error))
|
||||
: Promise.resolve([])
|
||||
])
|
||||
}).then(([validOrders, currentOrder, pendingContractsResponse, effectiveContractsResponse]) => {
|
||||
const pendingContracts = parseContractList(pendingContractsResponse)
|
||||
const effectiveContracts = parseContractList(effectiveContractsResponse)
|
||||
const currentBooking = pickCurrentBooking(validOrders)
|
||||
const nextUser = {
|
||||
...currentUser,
|
||||
@@ -208,7 +235,7 @@ const store = new Vuex.Store({
|
||||
globalEnumsPromise = getEnums({}, language)
|
||||
.then((res) => {
|
||||
const data = res && res.data ? res.data : res
|
||||
const enumsMap = {}
|
||||
const enumsMap = getBuiltInEnums(language)
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(item => {
|
||||
enumsMap[item.code] = item.data || []
|
||||
@@ -222,7 +249,7 @@ const store = new Vuex.Store({
|
||||
return enumsMap
|
||||
})
|
||||
.catch(() => {
|
||||
return hasEnums ? state.vuex_enums : {}
|
||||
return hasEnums ? state.vuex_enums : getBuiltInEnums(language)
|
||||
})
|
||||
.finally(() => {
|
||||
globalEnumsPromise = null
|
||||
|
||||
36
utils/booking-entry.js
Normal file
36
utils/booking-entry.js
Normal file
@@ -0,0 +1,36 @@
|
||||
function normalizeUrl(url = '') {
|
||||
return String(url || '').trim()
|
||||
}
|
||||
|
||||
export function isBookingEntryUrl(url = '') {
|
||||
return /^\/pkgBooking\/(book_viewing\/book_viewing|book\/book)(?:\?|$)/.test(normalizeUrl(url))
|
||||
}
|
||||
|
||||
export function resolveExistingBookingNavigationTarget(stayState = {}) {
|
||||
const source = stayState || {}
|
||||
const currentBooking = source.currentBooking || null
|
||||
const hasCheckedIn = source.hasCheckedIn === true
|
||||
|
||||
if (currentBooking && currentBooking.orderNo) {
|
||||
return `/pkgBooking/booking_detail/booking_detail?orderNo=${encodeURIComponent(currentBooking.orderNo)}`
|
||||
}
|
||||
|
||||
if (currentBooking) {
|
||||
return '/pkgBooking/booking_detail/booking_detail'
|
||||
}
|
||||
|
||||
if (hasCheckedIn) {
|
||||
return '/pages/index/index?pageId=stay-service'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function resolveBookingRedirectAfterLogin(targetUrl = '', stayState = {}) {
|
||||
const normalizedTargetUrl = normalizeUrl(targetUrl)
|
||||
if (!isBookingEntryUrl(normalizedTargetUrl)) {
|
||||
return normalizedTargetUrl
|
||||
}
|
||||
|
||||
return resolveExistingBookingNavigationTarget(stayState) || normalizedTargetUrl
|
||||
}
|
||||
@@ -2,15 +2,110 @@ import { getEnums } from '@/api/index.js'
|
||||
import { getCurrentLanguage } from '@/utils/i18n'
|
||||
|
||||
const ENUMS_CACHE_KEY = 'mini_enums_cache_v1'
|
||||
const BUILTIN_ENUMS = {
|
||||
zh: {
|
||||
IdCardTypeEnum: [
|
||||
{ key: 3, value: '港澳通行证', valueEn: 'Mainland Travel Permit for Hong Kong and Macao Residents' },
|
||||
{ key: 2, value: '护照', valueEn: 'Passport' },
|
||||
{ key: 99, value: '其他', valueEn: 'Other' },
|
||||
{ key: 1, value: '身份证', valueEn: 'ID Card' },
|
||||
{ key: 4, value: '台湾通行证', valueEn: 'Mainland Travel Permit for Taiwan Residents' }
|
||||
]
|
||||
},
|
||||
en: {
|
||||
IdCardTypeEnum: [
|
||||
{ key: 3, value: 'Mainland Travel Permit for Hong Kong and Macao Residents', valueZh: '港澳通行证' },
|
||||
{ key: 2, value: 'Passport', valueZh: '护照' },
|
||||
{ key: 99, value: 'Other', valueZh: '其他' },
|
||||
{ key: 1, value: 'ID Card', valueZh: '身份证' },
|
||||
{ key: 4, value: 'Mainland Travel Permit for Taiwan Residents', valueZh: '台湾通行证' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let enumsMap = {}
|
||||
let enumsLanguage = ''
|
||||
let fetchPromise = null
|
||||
|
||||
function hasRemoteEnumData(map = {}, language) {
|
||||
const currentLanguage = getNormalizedLanguage(language)
|
||||
const builtinKeys = Object.keys(BUILTIN_ENUMS[currentLanguage] || {})
|
||||
const sourceMap = map && typeof map === 'object' ? map : {}
|
||||
return Object.keys(sourceMap).some(code => !builtinKeys.includes(code))
|
||||
}
|
||||
|
||||
function mergeEnumOptionList(builtinList = [], customList = []) {
|
||||
const nextList = []
|
||||
const customMap = new Map()
|
||||
|
||||
;(Array.isArray(customList) ? customList : []).forEach((item) => {
|
||||
if (!item || typeof item !== 'object') return
|
||||
customMap.set(String(item.key), item)
|
||||
})
|
||||
|
||||
;(Array.isArray(builtinList) ? builtinList : []).forEach((item) => {
|
||||
const key = String(item && item.key)
|
||||
const customItem = customMap.get(key)
|
||||
if (customItem) {
|
||||
nextList.push({
|
||||
...item,
|
||||
...customItem
|
||||
})
|
||||
customMap.delete(key)
|
||||
return
|
||||
}
|
||||
nextList.push(item)
|
||||
})
|
||||
|
||||
customMap.forEach((item) => {
|
||||
nextList.push(item)
|
||||
})
|
||||
|
||||
return nextList
|
||||
}
|
||||
|
||||
function mergeBuiltinEnums(map = {}, language) {
|
||||
const currentLanguage = getNormalizedLanguage(language)
|
||||
const builtinMap = BUILTIN_ENUMS[currentLanguage] || {}
|
||||
const mergedMap = {
|
||||
...builtinMap,
|
||||
...(map || {})
|
||||
}
|
||||
|
||||
Object.keys(builtinMap).forEach((code) => {
|
||||
if (!Array.isArray(mergedMap[code])) {
|
||||
mergedMap[code] = builtinMap[code]
|
||||
return
|
||||
}
|
||||
mergedMap[code] = mergeEnumOptionList(builtinMap[code], mergedMap[code])
|
||||
})
|
||||
|
||||
return mergedMap
|
||||
}
|
||||
|
||||
function parseJsonStringMaybe(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return value
|
||||
}
|
||||
const text = value.trim()
|
||||
if (!text) {
|
||||
return value
|
||||
}
|
||||
if (!((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']')))) {
|
||||
return value
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (error) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeEnumMap(data) {
|
||||
const normalizedData = parseJsonStringMaybe(data)
|
||||
const map = {}
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((item) => {
|
||||
if (Array.isArray(normalizedData)) {
|
||||
normalizedData.forEach((item) => {
|
||||
if (item && item.code) {
|
||||
map[item.code] = Array.isArray(item.data) ? item.data : []
|
||||
}
|
||||
@@ -76,26 +171,30 @@ function writeEnumsCache(map, language) {
|
||||
export function initEnumsFromCache(language) {
|
||||
const currentLanguage = getNormalizedLanguage(language)
|
||||
if (Object.keys(enumsMap).length && enumsLanguage === currentLanguage) return enumsMap
|
||||
enumsMap = readEnumsCache(currentLanguage)
|
||||
enumsMap = mergeBuiltinEnums(readEnumsCache(currentLanguage), 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 (!force && Object.keys(enumsMap).length && enumsLanguage === currentLanguage && hasRemoteEnumData(enumsMap, 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
|
||||
if (Object.keys(enumsMap).length && hasRemoteEnumData(enumsMap, currentLanguage)) {
|
||||
return enumsMap
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getEnums({}, currentLanguage)
|
||||
const data = res && res.data ? res.data : res
|
||||
const map = normalizeEnumMap(data)
|
||||
const map = mergeBuiltinEnums(normalizeEnumMap(data), currentLanguage)
|
||||
if (Object.keys(map).length) {
|
||||
enumsMap = map
|
||||
enumsLanguage = currentLanguage
|
||||
@@ -118,6 +217,10 @@ export function getEnumOptions(code, language) {
|
||||
return enumsMap[code] || []
|
||||
}
|
||||
|
||||
export function getBuiltInEnums(language) {
|
||||
return mergeBuiltinEnums({}, language)
|
||||
}
|
||||
|
||||
export function getEnumLabel(code, key, fallback = '', language) {
|
||||
const options = getEnumOptions(code, language)
|
||||
const matched = options.find(item => String(item.key) === String(key))
|
||||
|
||||
86
utils/privacy.js
Normal file
86
utils/privacy.js
Normal file
@@ -0,0 +1,86 @@
|
||||
const privacyAuthorizationHandlers = []
|
||||
let hasBoundPrivacyAuthorizationListener = false
|
||||
|
||||
function getActivePrivacyAuthorizationHandler() {
|
||||
return privacyAuthorizationHandlers.length
|
||||
? privacyAuthorizationHandlers[privacyAuthorizationHandlers.length - 1]
|
||||
: null
|
||||
}
|
||||
|
||||
export function bindPrivacyAuthorizationListener(handler) {
|
||||
if (typeof handler !== 'function') {
|
||||
return
|
||||
}
|
||||
|
||||
const existedIndex = privacyAuthorizationHandlers.indexOf(handler)
|
||||
if (existedIndex !== -1) {
|
||||
privacyAuthorizationHandlers.splice(existedIndex, 1)
|
||||
}
|
||||
privacyAuthorizationHandlers.push(handler)
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
if (hasBoundPrivacyAuthorizationListener || typeof wx === 'undefined' || !wx.onNeedPrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
|
||||
wx.onNeedPrivacyAuthorization((resolve) => {
|
||||
const activeHandler = getActivePrivacyAuthorizationHandler()
|
||||
if (typeof activeHandler === 'function') {
|
||||
activeHandler(resolve)
|
||||
}
|
||||
})
|
||||
hasBoundPrivacyAuthorizationListener = true
|
||||
// #endif
|
||||
}
|
||||
|
||||
export function unbindPrivacyAuthorizationListener(handler) {
|
||||
const existedIndex = privacyAuthorizationHandlers.lastIndexOf(handler)
|
||||
if (existedIndex !== -1) {
|
||||
privacyAuthorizationHandlers.splice(existedIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
export function getPrivacyAuthorizationStatus() {
|
||||
// #ifdef MP-WEIXIN
|
||||
return new Promise((resolve) => {
|
||||
if (typeof wx === 'undefined' || !wx.getPrivacySetting) {
|
||||
resolve({ needAuthorization: false })
|
||||
return
|
||||
}
|
||||
|
||||
wx.getPrivacySetting({
|
||||
success: (res) => {
|
||||
resolve({
|
||||
...res,
|
||||
needAuthorization: !!(res && res.needAuthorization)
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
resolve({ needAuthorization: false })
|
||||
}
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
return Promise.resolve({ needAuthorization: false })
|
||||
// #endif
|
||||
}
|
||||
|
||||
export function openWechatPrivacyContract() {
|
||||
// #ifdef MP-WEIXIN
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof wx === 'undefined' || !wx.openPrivacyContract) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
wx.openPrivacyContract({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
return Promise.resolve()
|
||||
// #endif
|
||||
}
|
||||
@@ -84,6 +84,29 @@ function shouldSkipUnauthorizedRedirect() {
|
||||
return route === 'pkgAccount/login/login' || route === 'pages/login/index' || route === 'pages/login/login'
|
||||
}
|
||||
|
||||
function normalizeRequestedLanguageHeaderValue(language) {
|
||||
return String(language || getCurrentLanguage() || 'zh').trim().toLowerCase() === 'en' ? 'en' : 'zh'
|
||||
}
|
||||
|
||||
function buildRequestHeader(header = {}, token = '') {
|
||||
const normalizedHeader = { ...(header || {}) }
|
||||
const requestedLanguageHeaderKey = Object.keys(normalizedHeader).find(key => String(key || '').toLowerCase() === 'x-requested-language')
|
||||
|
||||
if (requestedLanguageHeaderKey && requestedLanguageHeaderKey !== 'X-Requested-Language') {
|
||||
delete normalizedHeader[requestedLanguageHeaderKey]
|
||||
}
|
||||
|
||||
normalizedHeader['X-Requested-Language'] = normalizeRequestedLanguageHeaderValue(
|
||||
requestedLanguageHeaderKey ? normalizedHeader[requestedLanguageHeaderKey] : normalizedHeader['X-Requested-Language']
|
||||
)
|
||||
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...normalizedHeader
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnauthorized(responseData) {
|
||||
clearLoginSession()
|
||||
|
||||
@@ -126,12 +149,7 @@ export function request(options = {}) {
|
||||
data: uppercaseMethod === 'GET'
|
||||
? (params !== undefined ? normalizedParams : normalizedData)
|
||||
: normalizedData,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-Language': getCurrentLanguage(),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...header
|
||||
},
|
||||
header: buildRequestHeader(header, token),
|
||||
success: (res) => {
|
||||
const { statusCode, data: responseData } = res
|
||||
if ((statusCode === 401) || (responseData && Number(responseData.code) === 401)) {
|
||||
|
||||
176
utils/reservation-flow.js
Normal file
176
utils/reservation-flow.js
Normal file
@@ -0,0 +1,176 @@
|
||||
const RESERVATION_FLOW_STEP = {
|
||||
INSPECTION: 10,
|
||||
BILL: 20,
|
||||
CONTRACT: 30,
|
||||
CHECKIN: 40
|
||||
}
|
||||
|
||||
function getLanguage(language) {
|
||||
return language === 'en' ? 'en' : 'zh'
|
||||
}
|
||||
|
||||
function parseJsonStringMaybe(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
const text = value.trim()
|
||||
if (!text) {
|
||||
return value
|
||||
}
|
||||
|
||||
const isJsonLike = (
|
||||
(text.startsWith('{') && text.endsWith('}'))
|
||||
|| (text.startsWith('[') && text.endsWith(']'))
|
||||
)
|
||||
|
||||
if (!isJsonLike) {
|
||||
return value
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (error) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeArray(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function parseReservationFlowResponse(response) {
|
||||
const rawPayload = response && response.data ? response.data : response
|
||||
const payload = parseJsonStringMaybe(rawPayload)
|
||||
|
||||
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 []
|
||||
}
|
||||
|
||||
export function getReservationOrderNo(source = {}) {
|
||||
const candidates = [
|
||||
source.orderNo,
|
||||
source.OrderNo,
|
||||
source.reservationOrderNo,
|
||||
source.searchedOrderNo,
|
||||
source.bookingOrderNo
|
||||
]
|
||||
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
const value = String(candidates[index] || '').trim()
|
||||
if (value) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function getReservationCurrentNode(flow = {}) {
|
||||
const nodes = normalizeArray(flow && flow.flowNodes)
|
||||
.filter(item => item && typeof item === 'object')
|
||||
.sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0))
|
||||
|
||||
if (!nodes.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return nodes.find(item => item.isCurrent) || nodes[nodes.length - 1]
|
||||
}
|
||||
|
||||
export function buildReservationFlowMap(flowList = []) {
|
||||
return normalizeArray(flowList)
|
||||
.filter(item => item && typeof item === 'object')
|
||||
.reduce((result, item) => {
|
||||
const orderNo = getReservationOrderNo(item)
|
||||
if (!orderNo) {
|
||||
return result
|
||||
}
|
||||
|
||||
result[orderNo] = {
|
||||
...item,
|
||||
orderNo,
|
||||
currentNode: getReservationCurrentNode(item)
|
||||
}
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function attachReservationFlow(item = {}, flowMap = {}) {
|
||||
const orderNo = getReservationOrderNo(item)
|
||||
const matchedFlow = orderNo ? flowMap[orderNo] : null
|
||||
|
||||
return {
|
||||
...item,
|
||||
reservationOrderNo: orderNo,
|
||||
reservationFlow: matchedFlow || null,
|
||||
reservationCurrentNode: matchedFlow && matchedFlow.currentNode ? matchedFlow.currentNode : null
|
||||
}
|
||||
}
|
||||
|
||||
export function getReservationStepMeta(step, language = 'zh') {
|
||||
const currentLanguage = getLanguage(language)
|
||||
const map = {
|
||||
[RESERVATION_FLOW_STEP.INSPECTION]: {
|
||||
label: currentLanguage === 'en' ? 'Inspection' : '验房',
|
||||
actionText: currentLanguage === 'en' ? 'View Inspection' : '查看验房'
|
||||
},
|
||||
[RESERVATION_FLOW_STEP.BILL]: {
|
||||
label: currentLanguage === 'en' ? 'Bill' : '账单',
|
||||
actionText: currentLanguage === 'en' ? 'Go to Bill' : '去缴费'
|
||||
},
|
||||
[RESERVATION_FLOW_STEP.CONTRACT]: {
|
||||
label: currentLanguage === 'en' ? 'Contract' : '合同',
|
||||
actionText: currentLanguage === 'en' ? 'Go to Contract' : '去签约'
|
||||
},
|
||||
[RESERVATION_FLOW_STEP.CHECKIN]: {
|
||||
label: currentLanguage === 'en' ? 'Check-in' : '入住',
|
||||
actionText: currentLanguage === 'en' ? 'Go to Check-in' : '去入住'
|
||||
}
|
||||
}
|
||||
|
||||
return map[Number(step)] || {
|
||||
label: currentLanguage === 'en' ? 'Current Step' : '当前步骤',
|
||||
actionText: currentLanguage === 'en' ? 'View Current Step' : '查看当前步骤'
|
||||
}
|
||||
}
|
||||
|
||||
export function getReservationCurrentStepText(source = {}, language = 'zh') {
|
||||
const node = source && source.reservationCurrentNode
|
||||
? source.reservationCurrentNode
|
||||
: getReservationCurrentNode(source)
|
||||
|
||||
if (!node) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return String(node.stepDesc || '').trim() || getReservationStepMeta(node.step, language).label
|
||||
}
|
||||
|
||||
export function getReservationCurrentActionText(source = {}, language = 'zh') {
|
||||
const node = source && source.reservationCurrentNode
|
||||
? source.reservationCurrentNode
|
||||
: getReservationCurrentNode(source)
|
||||
|
||||
if (!node) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return getReservationStepMeta(node.step, language).actionText
|
||||
}
|
||||
|
||||
export { RESERVATION_FLOW_STEP }
|
||||
@@ -364,6 +364,11 @@ export function normalizeAreaRoomType(item = {}) {
|
||||
|
||||
return {
|
||||
code: item.code || '',
|
||||
areaCode: item.areaCode || '',
|
||||
buildingCode: item.buildingCode || item.buildingNo || '',
|
||||
buildingName: item.buildingName || '',
|
||||
buildingSectionCode: item.buildingSectionCode || item.unitCode || '',
|
||||
buildingSectionName: item.buildingSectionName || item.unitName || '',
|
||||
name: item.name || '',
|
||||
nameEn: item.nameEn || '',
|
||||
nameEnDisplay: compactEnglishText(item.nameEn || '', 22),
|
||||
|
||||
@@ -112,18 +112,25 @@ function pickFirstValue(...values) {
|
||||
return ''
|
||||
}
|
||||
|
||||
function buildPendingContractQuery() {
|
||||
const user = getCurrentUser() || {}
|
||||
const userCode = pickFirstValue(user.userCode, user.code, user.UserCode)
|
||||
const query = {
|
||||
ProviderType: 'WechatMiniProgram'
|
||||
function buildPendingContractQuery(order = {}) {
|
||||
const orderNo = pickFirstValue(order.orderNo, order.no, order.code, order.OrderNo)
|
||||
if (!orderNo) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
ProviderType: 'WechatMiniProgram',
|
||||
OrderNo: orderNo
|
||||
}
|
||||
}
|
||||
|
||||
if (userCode) {
|
||||
query.UserCode = userCode
|
||||
function buildEffectiveContractQuery(order = {}) {
|
||||
const orderNo = pickFirstValue(order.orderNo, order.no, order.code, order.OrderNo)
|
||||
if (!orderNo) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
OrderNo: orderNo
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
function enrichBookingLanguageFields(booking = {}) {
|
||||
@@ -233,15 +240,23 @@ function requestStayState(language = 'zh') {
|
||||
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 pendingContractQuery = buildPendingContractQuery(currentOrder)
|
||||
const effectiveContractQuery = buildEffectiveContractQuery(currentOrder)
|
||||
|
||||
return Promise.all([
|
||||
Promise.resolve(orderList),
|
||||
Promise.resolve(currentOrder),
|
||||
pendingContractQuery
|
||||
? getMyPendingSignContracts(pendingContractQuery, language).catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
effectiveContractQuery
|
||||
? getMyEffectiveContracts(effectiveContractQuery, language).catch(() => [])
|
||||
: Promise.resolve([])
|
||||
])
|
||||
})
|
||||
.then(([orderList, currentOrder, pendingContractsResponse, effectiveContractsResponse]) => {
|
||||
const pendingContracts = parseContractList(pendingContractsResponse)
|
||||
const effectiveContracts = parseContractList(effectiveContractsResponse)
|
||||
const currentBooking = enrichBookingLanguageFields(pickCurrentBooking(orderList) || buildContractStepBooking(pendingContracts))
|
||||
|
||||
Reference in New Issue
Block a user