diff --git a/api/index.js b/api/index.js index 32514c5..5741b71 100644 --- a/api/index.js +++ b/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', diff --git a/config.js b/config.js index d788dc3..17bc204 100644 --- a/config.js +++ b/config.js @@ -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', diff --git a/locales/messages.js b/locales/messages.js index 9ed18a6..43e6e02 100644 --- a/locales/messages.js +++ b/locales/messages.js @@ -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}", diff --git a/manifest.json b/manifest.json index 7d9638b..78f58d2 100644 --- a/manifest.json +++ b/manifest.json @@ -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 diff --git a/pages.json b/pages.json index d51d408..23230bb 100644 --- a/pages.json +++ b/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": { diff --git a/pages/index/component/MyApplicationTab.vue b/pages/index/component/MyApplicationTab.vue index 94da39f..e1a7719 100644 --- a/pages/index/component/MyApplicationTab.vue +++ b/pages/index/component/MyApplicationTab.vue @@ -5,7 +5,111 @@ {{ t('pageDesc') }} - + + + + + {{ t('reservationFlowTitle') }} + {{ t('reservationFlowDesc') }} + + + {{ t('refreshFlowAction') }} + + + + + + + {{ item.roomTypeText || item.roomTypeName || t('reservationOrderFallback') }} + + + + + + + {{ t('apartmentLabel') }} + {{ activeReservationOrder.areaText || activeReservationOrder.areaName || '--' }} + + + {{ t('reservationRoomLabel') }} + {{ activeReservationOrder.roomTypeText || activeReservationOrder.roomTypeName || '--' }} + + + {{ t('roomNoLabel') }} + {{ activeReservationOrder.roomNoText || activeReservationOrder.roomNo || '--' }} + + + {{ t('reservationStatusLabel') }} + {{ activeReservationOrder.orderStatusDesc || activeReservationOrder.statusText || '--' }} + + + + + + + {{ node.sort || node.step }} + + + + + {{ node.stepDesc || getReservationStepLabel(node.step) }} + {{ t('currentStepTag') }} + + + + + {{ t('pendingPaymentDescLabel') }} + {{ getReservationBillDisplayTitle(getActiveReservationBillDetail()) }} + + + {{ t('pendingPaymentAmountLabel') }} + ¥{{ formatMoney(getReservationBillDisplayAmount(getActiveReservationBillDetail())) }} + + + + + {{ getReservationCurrentStepHint(activeReservationOrder) }} + + + + + {{ t('refreshAction') }} + + + + + + {{ getCurrentStepActionText(activeReservationOrder) }} + + + + + + + - - {{ t('statusReminderTitle') }} + + + {{ t('statusReminderTitle') }} + + {{ t('refreshAction') }} + + {{ t('statusReminderTitle') }} {{ getStatusReminderText(item) }} @@ -90,7 +199,18 @@ + {{ getCurrentStepActionText(item) }} + + + + diff --git a/pkgExplore/store_detail/components/RoomList.vue b/pkgExplore/store_detail/components/RoomList.vue index 7e7bf37..50b1d23 100644 --- a/pkgExplore/store_detail/components/RoomList.vue +++ b/pkgExplore/store_detail/components/RoomList.vue @@ -45,6 +45,14 @@ + + diff --git a/pkgService/my_application/my_application.vue b/pkgService/my_application/my_application.vue index 70b968c..96b5c15 100644 --- a/pkgService/my_application/my_application.vue +++ b/pkgService/my_application/my_application.vue @@ -109,14 +109,51 @@ + + diff --git a/pkgService/submit_request/components/BuildingSelector.vue b/pkgService/submit_request/components/BuildingSelector.vue new file mode 100644 index 0000000..57331af --- /dev/null +++ b/pkgService/submit_request/components/BuildingSelector.vue @@ -0,0 +1,314 @@ + + + + + diff --git a/pkgService/submit_request/components/FloorSelector.vue b/pkgService/submit_request/components/FloorSelector.vue new file mode 100644 index 0000000..2f36032 --- /dev/null +++ b/pkgService/submit_request/components/FloorSelector.vue @@ -0,0 +1,308 @@ + + + + + diff --git a/pkgService/submit_request/components/RoomTypeSelector.vue b/pkgService/submit_request/components/RoomTypeSelector.vue index 0fd8735..57a2fb3 100644 --- a/pkgService/submit_request/components/RoomTypeSelector.vue +++ b/pkgService/submit_request/components/RoomTypeSelector.vue @@ -1,6 +1,6 @@ @@ -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,18 +961,31 @@ 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 this.sourcePayload.areaName = area.name this.sourcePayload.areaNameEn = area.nameEn || '' } - + if (roomType.price) { this.sourcePayload.price = roomType.price } } }, + 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; } + diff --git a/pkgService/ticket_center/ticket_center.vue b/pkgService/ticket_center/ticket_center.vue new file mode 100644 index 0000000..8436704 --- /dev/null +++ b/pkgService/ticket_center/ticket_center.vue @@ -0,0 +1,211 @@ + + + + + diff --git a/pkgService/ticket_workbench/ticket_workbench.vue b/pkgService/ticket_workbench/ticket_workbench.vue new file mode 100644 index 0000000..4fbb757 --- /dev/null +++ b/pkgService/ticket_workbench/ticket_workbench.vue @@ -0,0 +1,2252 @@ + + + + + diff --git a/store/index.js b/store/index.js index 1dc1fd7..2412b1d 100644 --- a/store/index.js +++ b/store/index.js @@ -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 diff --git a/utils/booking-entry.js b/utils/booking-entry.js new file mode 100644 index 0000000..9872188 --- /dev/null +++ b/utils/booking-entry.js @@ -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 +} diff --git a/utils/enum-store.js b/utils/enum-store.js index e696897..b28e3a3 100644 --- a/utils/enum-store.js +++ b/utils/enum-store.js @@ -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)) diff --git a/utils/privacy.js b/utils/privacy.js new file mode 100644 index 0000000..f5aa13b --- /dev/null +++ b/utils/privacy.js @@ -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 +} diff --git a/utils/request.js b/utils/request.js index 783e2ac..d5ef388 100644 --- a/utils/request.js +++ b/utils/request.js @@ -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)) { diff --git a/utils/reservation-flow.js b/utils/reservation-flow.js new file mode 100644 index 0000000..4c672bf --- /dev/null +++ b/utils/reservation-flow.js @@ -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 } diff --git a/utils/select-room-normalizers.js b/utils/select-room-normalizers.js index e43006f..92b2ad8 100644 --- a/utils/select-room-normalizers.js +++ b/utils/select-room-normalizers.js @@ -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), diff --git a/utils/session-sync.js b/utils/session-sync.js index a261f7f..4fb6103 100644 --- a/utils/session-sync.js +++ b/utils/session-sync.js @@ -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 } - - if (userCode) { - query.UserCode = userCode + return { + ProviderType: 'WechatMiniProgram', + OrderNo: orderNo } +} - return query +function buildEffectiveContractQuery(order = {}) { + const orderNo = pickFirstValue(order.orderNo, order.no, order.code, order.OrderNo) + if (!orderNo) { + return null + } + return { + OrderNo: orderNo + } } 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))