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:
liangjian
2026-07-24 09:35:46 +08:00
parent 9fea127de1
commit 445bd850f4
36 changed files with 9281 additions and 340 deletions

View File

@@ -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>