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:
@@ -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(() => {
|
||||
@@ -204,6 +265,76 @@
|
||||
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 })
|
||||
.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
|
||||
this.resolveApplicationState()
|
||||
.then((applicationState = {}) => {
|
||||
return orderSearchedMePage({ PageIndex: 1, PageSize: 100, IsExport: false })
|
||||
.then(res => {
|
||||
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')
|
||||
@@ -587,6 +806,86 @@
|
||||
background-color: #111111;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user