Files
QSY-lvjuxcx/pkgService/my_application/my_application.vue
liangjian 6e721ccc7e refactor: 国际化文本迁移至全局 locales/messages.js
将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。

- 新增 utils/i18n.js 国际化工具函数
- 更新 store/$t.mixin.js 翻译混入逻辑
- 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
2026-07-24 13:43:41 +08:00

670 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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