将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。 - 新增 utils/i18n.js 国际化工具函数 - 更新 store/$t.mixin.js 翻译混入逻辑 - 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
670 lines
18 KiB
Vue
670 lines
18 KiB
Vue
<template>
|
||
<view class="my-application-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">{{ $tr('我的申请') }}</view>
|
||
</tn-nav-bar>
|
||
|
||
<view class="page-main tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '48rpx' }">
|
||
<scroll-view class="filter-scroll" scroll-x :show-scrollbar="false">
|
||
<view class="filter-row tn-margin-bottom-lg">
|
||
<view
|
||
class="filter-chip"
|
||
v-for="item in statusOptions"
|
||
:key="item.value"
|
||
:class="{ 'filter-chip--active': status === item.value }"
|
||
@click="changeStatus(item.value)"
|
||
>
|
||
{{ item.label }}
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view class="toolbar-row tn-margin-bottom-lg">
|
||
<view class="toolbar-summary">{{ $tr('共 {total} 条申请', { total }) }}</view>
|
||
<view
|
||
class="refresh-btn"
|
||
:class="{ 'refresh-btn--loading': loading }"
|
||
@click="refreshStatus"
|
||
>
|
||
<text
|
||
class="tn-text-bold tn-flex tn-flex-row-center tn-flex-col-center"
|
||
>
|
||
<text v-if="loading" class="tn-icon-loading tn-margin-right-xs spin"></text>
|
||
{{ loading ? $tr('刷新中...') : $tr('刷新状态') }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="list.length">
|
||
<view
|
||
v-for="item in list"
|
||
:key="getApplicationKey(item)"
|
||
class="application-card tn-margin-bottom-lg"
|
||
@click="goToApplicationDetail(item)"
|
||
>
|
||
<view class="card-head tn-flex tn-flex-row-between tn-flex-col-center">
|
||
<view class="card-title-wrap">
|
||
<view class="card-title">{{ getApplicationTitle(item) }}</view>
|
||
<view class="card-code tn-margin-top-xs">{{ getApplicationCode(item) }}</view>
|
||
</view>
|
||
<view :class="['status-badge', 'status-' + resolveStatusType(item.status)]">
|
||
{{ getDisplayStatusText(item) }}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card-body tn-margin-top">
|
||
<view class="info-row tn-flex tn-flex-row-between">
|
||
<text class="info-label">{{ $tr('所属公寓') }}</text>
|
||
<text class="info-value">{{ item.intendedAreaName || '--' }}</text>
|
||
</view>
|
||
<view class="info-row tn-flex tn-flex-row-between">
|
||
<text class="info-label">{{ $tr('入住人') }}</text>
|
||
<text class="info-value">{{ item.userName || item.phone || '--' }}</text>
|
||
</view>
|
||
<view class="info-row tn-flex tn-flex-row-between">
|
||
<text class="info-label">{{ item.isViewingApplication ? $tr('预约时间') : $tr('预计租期') }}</text>
|
||
<text class="info-value">{{ formatDateRange(item.start, item.end) }}</text>
|
||
</view>
|
||
<view class="info-row tn-flex tn-flex-row-between">
|
||
<text class="info-label">{{ $tr('提交时间') }}</text>
|
||
<text class="info-value">{{ formatDateTime(item.createTime) }}</text>
|
||
</view>
|
||
<view v-if="item.remark" class="remark-panel tn-margin-top">
|
||
<view class="remark-label">{{ $tr('补充说明') }}</view>
|
||
<view class="remark-text tn-margin-top-xs">{{ item.remark }}</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="canViewBill(item)" class="card-footer tn-margin-top-lg">
|
||
<view
|
||
class="card-action-btn card-action-btn--ghost"
|
||
:data-reference-type-no="resolveReferenceTypeNo(item)"
|
||
@click="goToBillPage"
|
||
>
|
||
<text class="tn-text-bold">{{ $tr('查看关联账单') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="load-state">
|
||
<text v-if="loadingMore">{{ $tr('加载中...') }}</text>
|
||
<text v-else-if="!hasMore">{{ $tr('没有更多申请了') }}</text>
|
||
<text v-else>{{ $tr('上拉加载更多') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else-if="!loading" class="empty-panel tn-padding-lg">
|
||
<view class="empty-title">{{ $tr('暂无申请记录') }}</view>
|
||
<view class="empty-desc tn-margin-top-sm">{{ $tr('您还没有提交任何申请,去选房后可发起入住申请或预约看房。') }}</view>
|
||
<view class="action-btn tn-margin-top-lg" @click="goSelectRoom">
|
||
<text class="tn-text-bold">{{ $tr('去选房') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="loading && !list.length" class="empty-panel tn-padding-lg">
|
||
<view class="empty-title">{{ $tr('申请列表加载中') }}</view>
|
||
<view class="empty-desc tn-margin-top-sm">{{ $tr('正在同步您的入住申请记录,请稍候。') }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
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 []
|
||
}
|
||
return list
|
||
.filter(item => item && typeof item === 'object')
|
||
.map((item) => {
|
||
return {
|
||
...item,
|
||
isViewingApplication: isViewingApplicationRecord(item)
|
||
}
|
||
})
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
status: '',
|
||
pageIndex: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
list: [],
|
||
loading: false,
|
||
loadingMore: false,
|
||
hasMore: true,
|
||
statusOptions: []
|
||
}
|
||
},
|
||
onShow() {
|
||
this.reloadList()
|
||
},
|
||
onPullDownRefresh() {
|
||
this.reloadList().finally(() => {
|
||
uni.stopPullDownRefresh()
|
||
})
|
||
},
|
||
onReachBottom() {
|
||
this.loadMore()
|
||
},
|
||
created() {
|
||
this.syncStatusOptions()
|
||
},
|
||
watch: {
|
||
language() {
|
||
this.syncStatusOptions()
|
||
this.reloadList(true)
|
||
}
|
||
},
|
||
computed: {
|
||
language() {
|
||
return this.$getLanguage()
|
||
}
|
||
},
|
||
methods: {
|
||
syncStatusOptions() {
|
||
this.statusOptions = [
|
||
{ label: this.$tr('全部'), value: '' },
|
||
{ label: this.$tr('待审核'), value: 0 },
|
||
{ label: this.$tr('已通过'), value: 10 },
|
||
{ label: this.$tr('已拒绝'), value: 20 },
|
||
{ label: this.$tr('已结束'), value: 99 }
|
||
]
|
||
},
|
||
getStatusText(status) {
|
||
const map = {
|
||
0: this.$tr('待审核'),
|
||
10: this.$tr('已通过'),
|
||
20: this.$tr('已拒绝'),
|
||
99: this.$tr('已结束')
|
||
}
|
||
return map[status] || this.$tr('处理中')
|
||
},
|
||
getDisplayStatusText(item = {}) {
|
||
if (this.language !== 'en' && item.statusDesc) {
|
||
return item.statusDesc
|
||
}
|
||
return this.getStatusText(item.status)
|
||
},
|
||
resolveStatusType(status) {
|
||
if (status === 10) {
|
||
return 'approved'
|
||
}
|
||
if (status === 20) {
|
||
return 'rejected'
|
||
}
|
||
if (status === 99) {
|
||
return 'finished'
|
||
}
|
||
return 'pending'
|
||
},
|
||
formatDateTime(value) {
|
||
if (!value) {
|
||
return '--'
|
||
}
|
||
const text = String(value).replace('T', ' ')
|
||
return text.length > 16 ? text.slice(0, 16) : text
|
||
},
|
||
formatDate(value) {
|
||
if (!value) {
|
||
return '--'
|
||
}
|
||
return String(value).slice(0, 10)
|
||
},
|
||
formatDateRange(start, end) {
|
||
if (!start && !end) {
|
||
return '--'
|
||
}
|
||
if (start && end) {
|
||
const startText = this.formatDateTime(start)
|
||
const endText = this.formatDateTime(end)
|
||
if (String(start).includes('T') || String(end).includes('T')) {
|
||
return `${startText}${this.$tr(' 至 ')}${endText}`
|
||
}
|
||
}
|
||
return `${this.formatDate(start)}${this.$tr(' 至 ')}${this.formatDate(end)}`
|
||
},
|
||
async refreshStatus() {
|
||
if (this.loading || this.loadingMore) {
|
||
return
|
||
}
|
||
await this.reloadList()
|
||
if (this.list.length || this.total === 0) {
|
||
uni.showToast({
|
||
title: this.$tr('申请状态已刷新'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
async reloadList() {
|
||
this.pageIndex = 1
|
||
this.hasMore = true
|
||
this.list = []
|
||
await this.fetchList(true)
|
||
},
|
||
async loadMore() {
|
||
if (!this.hasMore || this.loading || this.loadingMore) {
|
||
return
|
||
}
|
||
this.pageIndex += 1
|
||
await this.fetchList(false)
|
||
},
|
||
async fetchList(isReload = false) {
|
||
if (isReload) {
|
||
this.loading = true
|
||
} else {
|
||
this.loadingMore = true
|
||
}
|
||
|
||
const params = {
|
||
PageIndex: this.pageIndex,
|
||
PageSize: this.pageSize
|
||
}
|
||
if (this.status !== '' && this.status !== null && typeof this.status !== 'undefined') {
|
||
params.Status = this.status
|
||
}
|
||
|
||
await applicationFormMePage(params, this.language)
|
||
.then(async (response) => {
|
||
const pageData = formatPageResponse(response)
|
||
console.log('pageData', pageData)
|
||
const nextList = normalizeApplicationList(pageData.data || [])
|
||
const total = Number(pageData.total || 0)
|
||
|
||
this.total = total
|
||
this.list = normalizeApplicationList(isReload ? nextList : this.list.concat(nextList))
|
||
this.hasMore = this.list.length < total && nextList.length > 0
|
||
|
||
const activeCode = this.resolvePreferredReferenceTypeNo(this.list)
|
||
if (activeCode) {
|
||
saveActiveApplicationCode(activeCode)
|
||
}
|
||
|
||
await this.loadReservationFlowsForList(this.list)
|
||
saveMyApplicationList(this.list)
|
||
})
|
||
.catch((error) => {
|
||
if (!isReload) {
|
||
this.pageIndex = Math.max(1, this.pageIndex - 1)
|
||
}
|
||
uni.showToast({
|
||
title: (error && (error.message || error.msg)) || this.$tr('申请记录加载失败'),
|
||
icon: 'none'
|
||
})
|
||
})
|
||
.finally(() => {
|
||
this.loading = false
|
||
this.loadingMore = false
|
||
})
|
||
},
|
||
changeStatus(value) {
|
||
if (this.status === value) {
|
||
return
|
||
}
|
||
this.status = value
|
||
this.reloadList()
|
||
},
|
||
getApplicationCode(item = {}) {
|
||
return item.detailCode || '--'
|
||
},
|
||
getApplicationKey(item = {}) {
|
||
return item.detailCode || item.formId || `${item.createTime || 'application'}-${item.phone || item.userName || 'unknown'}`
|
||
},
|
||
getApplicationTitle(item = {}) {
|
||
if (item.isViewingApplication) {
|
||
return this.$tr('预约看房申请')
|
||
}
|
||
return item.intendedRoomTypeName || this.$tr('入住申请')
|
||
},
|
||
resolveReferenceTypeNo(item = {}) {
|
||
return item.detailCode || ''
|
||
},
|
||
canViewBill(item = {}) {
|
||
return !item.isViewingApplication && this.resolveReferenceTypeNo(item) && this.resolveStatusType(item.status) === 'pending'
|
||
},
|
||
resolvePreferredReferenceTypeNo(list = []) {
|
||
const sortedList = [...list]
|
||
.filter(item => !item.isViewingApplication && this.resolveReferenceTypeNo(item))
|
||
.sort((a, b) => new Date(b.createTime || 0).getTime() - new Date(a.createTime || 0).getTime())
|
||
|
||
if (!sortedList.length) {
|
||
return ''
|
||
}
|
||
|
||
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
|
||
? String(dataset.referenceTypeNo)
|
||
: ''
|
||
if (!referenceTypeNo) {
|
||
uni.showToast({
|
||
title: this.$tr('申请单编号缺失'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
saveActiveApplicationCode(referenceTypeNo)
|
||
uni.navigateTo({
|
||
url: `/pkgService/utility_recharge/utility_recharge?referenceTypeNo=${encodeURIComponent(referenceTypeNo)}`
|
||
})
|
||
},
|
||
goToApplicationDetail(item = {}) {
|
||
const code = item.detailCode || ''
|
||
if (!code) {
|
||
return
|
||
}
|
||
uni.navigateTo({
|
||
url: `/pkgService/application_detail/application_detail?code=${encodeURIComponent(code)}&payload=${encodeURIComponent(JSON.stringify(item))}`
|
||
})
|
||
},
|
||
goSelectRoom() {
|
||
uni.reLaunch({
|
||
url: '/pages/index/index?index=0&pageId=select-room'
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.my-application-page {
|
||
min-height: 100vh;
|
||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||
}
|
||
|
||
.application-card,
|
||
.empty-panel {
|
||
background: #FFFFFF;
|
||
border-radius: 24rpx;
|
||
padding: 32rpx;
|
||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
||
border: 1px solid rgba($brand-primary, 0.05);
|
||
}
|
||
|
||
.empty-title,
|
||
.card-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #1A2233;
|
||
}
|
||
|
||
.empty-desc,
|
||
.card-code,
|
||
.info-label,
|
||
.remark-label,
|
||
.load-state {
|
||
color: #6A7A99;
|
||
}
|
||
|
||
.filter-scroll {
|
||
width: 100%;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-row {
|
||
display: inline-flex;
|
||
flex-wrap: nowrap;
|
||
gap: 16rpx;
|
||
padding: 4rpx;
|
||
}
|
||
|
||
.toolbar-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.toolbar-summary {
|
||
flex: 1;
|
||
font-size: 24rpx;
|
||
color: #6A7A99;
|
||
}
|
||
|
||
.refresh-btn {
|
||
min-width: 188rpx;
|
||
height: 72rpx;
|
||
padding: 0 24rpx;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 999rpx;
|
||
background: #FFFFFF;
|
||
border: 1rpx solid rgba($brand-primary, 0.18);
|
||
color: $brand-primary;
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.06);
|
||
}
|
||
|
||
.refresh-btn--loading {
|
||
opacity: 0.88;
|
||
}
|
||
|
||
.filter-chip {
|
||
flex-shrink: 0;
|
||
padding: 14rpx 28rpx;
|
||
border-radius: 999rpx;
|
||
background: #FFFFFF;
|
||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||
color: #6A7A99;
|
||
font-size: 24rpx;
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.filter-chip--active {
|
||
background: #FFFFFF;
|
||
border-color: $brand-primary;
|
||
color: $brand-primary;
|
||
font-weight: bold;
|
||
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
||
}
|
||
|
||
.card-code {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 8rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.status-pending {
|
||
background: rgba(245, 158, 11, 0.12);
|
||
color: #B45309;
|
||
}
|
||
|
||
.status-approved {
|
||
background: rgba($brand-primary, 0.12);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.status-rejected {
|
||
background: rgba(239, 68, 68, 0.12);
|
||
color: #B91C1C;
|
||
}
|
||
|
||
.status-finished {
|
||
background: rgba(100, 116, 139, 0.12);
|
||
color: #475569;
|
||
}
|
||
|
||
.info-row {
|
||
padding: 16rpx 0;
|
||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
||
}
|
||
|
||
.info-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.info-value {
|
||
max-width: 62%;
|
||
text-align: right;
|
||
color: #1A2233;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.remark-panel {
|
||
padding: 24rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba($brand-primary, 0.02);
|
||
border: 1rpx solid rgba($brand-primary, 0.1);
|
||
}
|
||
|
||
.remark-text {
|
||
color: #1A2233;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.card-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.card-action-btn {
|
||
min-width: 220rpx;
|
||
height: 84rpx;
|
||
padding: 0 28rpx;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 18rpx;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.card-action-btn--ghost {
|
||
background: #ffffff;
|
||
color: $brand-primary;
|
||
border: 1rpx solid rgba($brand-primary, 0.3);
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||
}
|
||
|
||
.action-btn {
|
||
width: 100%;
|
||
height: 96rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 999rpx;
|
||
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
|
||
color: #FFFFFF;
|
||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.25);
|
||
}
|
||
|
||
.load-state {
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
padding: 12rpx 0 24rpx;
|
||
color: #6A7A99;
|
||
}
|
||
|
||
.spin {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
/* 隐藏 scroll-view 滚动条 */
|
||
::-webkit-scrollbar {
|
||
display: none;
|
||
width: 0 !important;
|
||
height: 0 !important;
|
||
-webkit-appearance: none;
|
||
background: transparent;
|
||
}
|
||
</style>
|