chore: initial import

This commit is contained in:
liangjian
2026-06-29 09:42:53 +08:00
commit 9fea127de1
241 changed files with 82767 additions and 0 deletions

View File

@@ -0,0 +1,674 @@
<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">{{ t('navTitle') }}</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">{{ t('toolbarSummary', { 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 ? t('refreshing') : t('refresh') }}
</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">{{ t('apartmentLabel') }}</text>
<text class="info-value">{{ item.intendedAreaName || '--' }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('occupantLabel') }}</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 ? t('viewingTimeLabel') : t('leaseTermLabel') }}</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">{{ t('submittedAtLabel') }}</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">{{ t('remarkLabel') }}</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">{{ t('viewBill') }}</text>
</view>
</view>
</view>
<view class="load-state">
<text v-if="loadingMore">{{ t('loadingMore') }}</text>
<text v-else-if="!hasMore">{{ t('noMore') }}</text>
<text v-else>{{ t('pullUpMore') }}</text>
</view>
</view>
<view v-else-if="!loading" class="empty-panel tn-padding-lg">
<view class="empty-title">{{ t('emptyTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('emptyDesc') }}</view>
<view class="action-btn tn-margin-top-lg" @click="goSelectRoom">
<text class="tn-text-bold">{{ t('goSelectRoom') }}</text>
</view>
</view>
<view v-if="loading && !list.length" class="empty-panel tn-padding-lg">
<view class="empty-title">{{ t('loadingTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('loadingDesc') }}</view>
</view>
</view>
</view>
</template>
<script>
import { applicationFormMePage } from '@/api/index.js'
import { saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
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')
.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
}
})
}
export default {
i18n: {
zh: {
navTitle: '我的申请',
toolbarSummary: '共 {total} 条申请',
refreshing: '刷新中...',
refresh: '刷新状态',
apartmentLabel: '所属公寓',
occupantLabel: '入住人',
viewingTimeLabel: '预约时间',
leaseTermLabel: '预计租期',
submittedAtLabel: '提交时间',
remarkLabel: '补充说明',
viewBill: '查看关联账单',
loadingMore: '加载中...',
noMore: '没有更多申请了',
pullUpMore: '上拉加载更多',
emptyTitle: '暂无申请记录',
emptyDesc: '您还没有提交任何申请,去选房后可发起入住申请或预约看房。',
goSelectRoom: '去选房',
loadingTitle: '申请列表加载中',
loadingDesc: '正在同步您的入住申请记录,请稍候。',
statusAll: '全部',
statusPending: '待审核',
statusApproved: '已通过',
statusRejected: '已拒绝',
statusFinished: '已结束',
statusProcessing: '处理中',
refreshSuccess: '申请状态已刷新',
loadError: '申请记录加载失败',
viewingApplicationTitle: '预约看房申请',
stayApplicationTitle: '入住申请',
missingBillCode: '申请单编号缺失',
missingApplicationCode: '申请编号缺失',
dateRangeJoiner: ' 至 '
},
en: {
navTitle: 'My Applications',
toolbarSummary: '{total} applications',
refreshing: 'Refreshing...',
refresh: 'Refresh Status',
apartmentLabel: 'Apartment',
occupantLabel: 'Resident',
viewingTimeLabel: 'Viewing Time',
leaseTermLabel: 'Lease Term',
submittedAtLabel: 'Submitted At',
remarkLabel: 'Remarks',
viewBill: 'View Related Bill',
loadingMore: 'Loading...',
noMore: 'No more applications',
pullUpMore: 'Pull up to load more',
emptyTitle: 'No applications yet',
emptyDesc: 'You have not submitted any application yet. Go select a room to start a stay or viewing application.',
goSelectRoom: 'Select Room',
loadingTitle: 'Loading applications',
loadingDesc: 'Syncing your application records. Please wait.',
statusAll: 'All',
statusPending: 'Pending',
statusApproved: 'Approved',
statusRejected: 'Rejected',
statusFinished: 'Closed',
statusProcessing: 'Processing',
refreshSuccess: 'Application status refreshed',
loadError: 'Failed to load applications',
viewingApplicationTitle: 'Viewing Request',
stayApplicationTitle: 'Stay Application',
missingBillCode: 'Missing application bill number',
missingApplicationCode: 'Missing application number',
dateRangeJoiner: ' to '
}
},
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: {
t(key, params) {
return this.$tm('pkgService.my_application.my_application', key, params)
},
syncStatusOptions() {
this.statusOptions = [
{ label: this.t('statusAll'), value: '' },
{ label: this.t('statusPending'), value: 0 },
{ label: this.t('statusApproved'), value: 10 },
{ label: this.t('statusRejected'), value: 20 },
{ label: this.t('statusFinished'), value: 99 }
]
},
getStatusText(status) {
const map = {
0: this.t('statusPending'),
10: this.t('statusApproved'),
20: this.t('statusRejected'),
99: this.t('statusFinished')
}
return map[status] || this.t('statusProcessing')
},
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.t('dateRangeJoiner')}${endText}`
}
}
return `${this.formatDate(start)}${this.t('dateRangeJoiner')}${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.t('refreshSuccess'),
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((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
saveMyApplicationList(this.list)
const activeCode = this.resolvePreferredReferenceTypeNo(this.list)
if (activeCode) {
saveActiveApplicationCode(activeCode)
}
})
.catch((error) => {
if (!isReload) {
this.pageIndex = Math.max(1, this.pageIndex - 1)
}
uni.showToast({
title: (error && (error.message || error.msg)) || this.t('loadError'),
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.t('viewingApplicationTitle')
}
return item.intendedRoomTypeName || this.t('stayApplicationTitle')
},
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)
},
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.t('missingBillCode'),
icon: 'none'
})
return
}
saveActiveApplicationCode(referenceTypeNo)
uni.navigateTo({
url: `/pkgService/utility_recharge/utility_recharge?referenceTypeNo=${encodeURIComponent(referenceTypeNo)}`
})
},
goToApplicationDetail(item = {}) {
const code = item.detailCode || ''
if (!code) {
uni.showToast({
title: this.t('missingApplicationCode'),
icon: 'none'
})
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>