Compare commits

..

4 Commits

Author SHA1 Message Date
liangjian
9fee2c3a74 feat: 优化首页服务入口与租住业务流程
补齐首页待办跳转、押金支付引导和多类租住服务页面能力,统一租客端关键业务流程体验。
2026-08-25 13:53:12 +08:00
liangjian
1111ac148b feat: 重构核心租住流程页面与交互体验
统一注册、看房、申请与入住等核心页面的视觉和流程衔接,补齐支付、签约与状态流转闭环,提升移动端操作连贯性。
2026-07-29 09:05:38 +08:00
liangjian
da6bdc6a02 chore: 忽略 .trae IDE 状态目录 2026-07-24 13:47:17 +08:00
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
77 changed files with 16863 additions and 7132 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.dbg/
.hbuilderx/
.history/
.trae/
unpackage/
.DS_Store

View File

@@ -30,6 +30,10 @@ function buildIotGatewayUrl(path = '') {
return `${iotBaseUrl}${normalizedPath}`
}
function shouldIgnoreGatewayRoom404(error = {}) {
return Number(error && error.statusCode) === 404
}
function normalizeUploadResponse(data) {
if (typeof data !== 'string') {
return Promise.resolve(data)
@@ -237,6 +241,21 @@ export function applicationFormMePage(params = {}, language) {
})
}
export function applicationFormDetail(detailCode = '', language) {
const normalizedDetailCode = String(detailCode || '').trim()
if (!normalizedDetailCode) {
return Promise.reject(new Error('申请明细编码不能为空'))
}
return request({
url: `/applicationform/detail/${encodeURIComponent(normalizedDetailCode)}`,
method: 'GET',
header: {
'X-Requested-Language': resolveLanguageHeader(language)
}
})
}
// --- 工单 ---
export function ticketOrderPage(params = {}, language) {
return request({
@@ -430,14 +449,15 @@ export function orderSearchedReservationFlow(data = {}, language) {
})
}
export function roomInspectionMyPage(params = {}, language) {
export function roomInspectionMyPage(params = {}, language, options = {}) {
return request({
url: '/roominspection/my/page',
method: 'GET',
params: params || {},
header: {
'X-Requested-Language': resolveLanguageHeader(language)
}
},
...options
})
}
@@ -526,6 +546,24 @@ export function accountQueryBill(params = {}, language) {
})
}
export function accountQueryBillPreview(params = {}, language) {
const query = params || {}
const orderNo = String(query.orderNo || query.OrderNo || '').trim()
if (!orderNo) {
return Promise.reject(new Error('订单号不能为空'))
}
return request({
url: '/accountquery/bill-preview',
method: 'GET',
params: {
OrderNo: orderNo
},
header: {
'X-Requested-Language': resolveLanguageHeader(language)
}
})
}
export function paymentBillPay(data = {}, language) {
const payload = data || {}
return request({
@@ -576,9 +614,13 @@ export function paymentStatusByBillNo(params = {}, language) {
}
export function userDetailCancel(data = {}, language) {
const payload = data || {}
const source = data || {}
const payload = {
detailCode: String(source.detailCode || source.code || source.referenceTypeNo || '').trim(),
remark: source.remark == null ? '' : String(source.remark)
}
return request({
url: '/ApplicationForm/user/detail/cancel',
url: '/applicationform/user/detail/cancel',
method: 'POST',
data: payload,
header: {
@@ -696,6 +738,18 @@ export function notificationRead(code, language) {
})
}
export function getMyTodoList(params = {}, language, options = {}) {
return request({
url: '/todo/my/list',
method: 'GET',
params: params || {},
header: {
'X-Requested-Language': resolveLanguageHeader(language)
},
...options
})
}
// --- 房源与区域 ---
export function areaPagination(params = {}, language) {
return request({
@@ -709,6 +763,18 @@ export function areaPagination(params = {}, language) {
})
}
export function areaDetailPagination(params = {}, language) {
return request({
url: '/area/pagination',
method: 'GET',
params,
header: {
'X-Requested-Language': resolveLanguageHeader(language)
},
withAuth: false
})
}
export function facilityManagers(params = {}, language) {
return request({
url: '/facility/managers',
@@ -818,8 +884,15 @@ export function gatewayRoomDeviceList(brandId = '', roomCode = '', language) {
method: 'POST',
header: {
'X-Requested-Language': resolveLanguageHeader(language)
}
},
hideErrorToast: true
})
.catch((error) => {
if (shouldIgnoreGatewayRoom404(error)) {
return []
}
return Promise.reject(error)
})
}
function stringifyGatewayExecuteValue(value = '') {
@@ -922,6 +995,38 @@ export function gatewaySyncDeviceInfo(brandId = '', deviceNo = '', language) {
})
}
// --- 门锁 ---
export function deviceGetDevicesByRoomAsync(params = {}, language) {
const roomCode = String((params && params.roomCode) || '').trim()
if (!roomCode) {
return Promise.reject(new Error('roomCode 不能为空'))
}
return request({
url: '/device/getdevicesbyroomasync',
method: 'GET',
params: {
roomCode
},
header: {
'X-Requested-Language': resolveLanguageHeader(language)
},
hideErrorToast: true
})
}
export function lockHxjOpenLock(data = {}, language) {
return createJsonPatchRequest('/LockHxj/OpenLock', data, language)
}
export function lockHxjGetKeysByUserCode(data = {}, language) {
return createJsonPatchRequest('/LockHxj/GetKeysByUserCode', data, language)
}
export function lockHxjAddFingerKey(data = {}, language) {
return createJsonPatchRequest('/LockHxj/AddFingerKey', data, language)
}
// --- 内容 ---
export function bannerAll(language) {
return request({

View File

@@ -57,24 +57,24 @@
<view class="contract-sign-dialog__toolbar">
<view class="tool-btn tool-btn--soft" :class="{ disabled: !canUndo }" @tap="handleUndo">
<text class="ri-play-reverse-line"></text>
<text>{{ t('undo') }}</text>
<text>{{ $tr('撤销') }}</text>
</view>
<view class="tool-btn tool-btn--soft" :class="{ disabled: !hasSignature }" @tap="handleClear">
<text class="ri-delete-bin-line"></text>
<text>{{ t('rewrite') }}</text>
<text>{{ $tr('重写') }}</text>
</view>
<view class="tool-btn tool-btn--ghost" @tap="handleRequestClose">
<text class="ri-close-line"></text>
<text>{{ t('close') }}</text>
<text>{{ $tr('关闭') }}</text>
</view>
<view class="tool-btn tool-btn--primary" :class="{ disabled: !hasSignature }" @tap="handleSave">
<text class="ri-check-line-circle"></text>
{{ t('confirmSignature') }}
{{ $tr('确认签名') }}
</view>
</view>
<view v-if="!isLandscape" class="contract-sign-dialog__footer-tip">
<text class="ri-shield-check-line-fill"></text>
<text>{{ t('footerTip') }}</text>
<text>{{ $tr('签名前请确认您已完整阅读合同内容') }}</text>
</view>
</view>
<view
@@ -84,13 +84,13 @@
>
<view class="contract-sign-dialog__confirm-mask"></view>
<view class="contract-sign-dialog__confirm-card">
<view class="contract-sign-dialog__confirm-title">{{ t('closeBoardTitle') }}</view>
<view class="contract-sign-dialog__confirm-title">{{ $tr('关闭提示') }}</view>
<view class="contract-sign-dialog__confirm-desc">
{{ t('closeBoardDesc') }}
{{ $tr('您还未完成签名,确认要关闭吗?') }}
</view>
<view class="contract-sign-dialog__confirm-actions">
<view class="contract-sign-dialog__confirm-btn is-cancel" @tap="cancelRequestClose">{{ t('continueSigning') }}</view>
<view class="contract-sign-dialog__confirm-btn is-confirm" @tap="confirmRequestClose">{{ t('continueClosing') }}</view>
<view class="contract-sign-dialog__confirm-btn is-cancel" @tap="cancelRequestClose">{{ $tr('继续签名') }}</view>
<view class="contract-sign-dialog__confirm-btn is-confirm" @tap="confirmRequestClose">{{ $tr('确认关闭') }}</view>
</view>
</view>
</view>
@@ -116,46 +116,6 @@ export default {
default: ''
}
},
i18n: {
zh: {
undo: '撤销',
rewrite: '重写',
close: '关闭',
confirmSignature: '确认签名',
footerTip: '签名前请确认您已完整阅读合同内容',
closeBoardTitle: '关闭提示',
closeBoardDesc: '您还未完成签名,确认要关闭吗?',
continueSigning: '继续签名',
continueClosing: '确认关闭',
missingSignerName: '签名人缺失',
writingHintZh: '请在此处手写中文正楷签名',
writingHintEn: 'Please write your signature here',
signerNameMissingToast: '签名人信息缺失',
signatureRequiredToast: '请先完成签名',
boundsFailedToast: '获取签名范围失败',
signatureTooSmallToast: '签名范围太小,请重新签名',
exportFailedToast: '导出签名失败,请重试'
},
en: {
undo: 'Undo',
rewrite: 'Rewrite',
close: 'Close',
confirmSignature: 'Confirm',
footerTip: 'Please confirm that you have fully read the contract before signing',
closeBoardTitle: 'Notice',
closeBoardDesc: 'You have not finished signing. Are you sure to close?',
continueSigning: 'Continue Signing',
continueClosing: 'Close',
missingSignerName: 'Missing Signer',
writingHintZh: 'Please write your Chinese signature here',
writingHintEn: 'Please write your signature here',
signerNameMissingToast: 'Missing signer name',
signatureRequiredToast: 'Please sign first',
boundsFailedToast: 'Failed to get signature bounds',
signatureTooSmallToast: 'Signature is too small, please try again',
exportFailedToast: 'Failed to export signature, please try again'
}
},
data() {
return {
canvasId: 'contract-sign-canvas',
@@ -179,7 +139,7 @@ export default {
},
displaySignerName() {
const name = String(this.signerName || '').trim()
return name || this.t('missingSignerName')
return name || this.$tr('签名人缺失')
},
hasSignerName() {
return !!String(this.signerName || '').trim()
@@ -199,7 +159,7 @@ export default {
return this.strokes.length > 0
},
writingHint() {
return this.isChineseName ? this.t('writingHintZh') : this.t('writingHintEn')
return this.isChineseName ? this.$tr('请在此处手写中文正楷签名') : this.$tr('Please write your signature here')
},
guideFontSize() {
const length = this.displaySignerName.length
@@ -281,9 +241,6 @@ export default {
this.ctx = null
},
methods: {
t(key) {
return this.$translate(this.$options.i18n, key)
},
clearSignatureContent() {
this.strokes = []
this.currentStroke = []
@@ -561,14 +518,14 @@ export default {
handleSave() {
if (!this.hasSignerName) {
uni.showToast({
title: this.t('signerNameMissingToast'),
title: this.$tr('签名人信息缺失'),
icon: 'none'
})
return
}
if (!this.hasSignature) {
uni.showToast({
title: this.t('signatureRequiredToast'),
title: this.$tr('请先完成签名'),
icon: 'none'
})
return
@@ -576,14 +533,14 @@ export default {
const cropRect = this.getSignatureBounds()
if (!cropRect) {
uni.showToast({
title: this.t('boundsFailedToast'),
title: this.$tr('获取签名范围失败'),
icon: 'none'
})
return
}
if (!this.isSignatureQualified(cropRect)) {
uni.showToast({
title: this.t('signatureTooSmallToast'),
title: this.$tr('签名范围太小,请重新签名'),
icon: 'none'
})
return
@@ -601,7 +558,7 @@ export default {
.catch(error => {
console.error('导出签名图片失败', error)
uni.showToast({
title: this.t('exportFailedToast'),
title: this.$tr('导出签名失败,请重试'),
icon: 'none'
})
})

View File

@@ -0,0 +1,376 @@
<template>
<tn-popup :value="value" mode="bottom" length="84%" :borderRadius="32" :closeBtn="true" @close="close" @input="handleInput">
<view class="deposit-other-payment-popup">
<view class="popup-header">
<view class="popup-handle"></view>
<view class="popup-title">{{ getLocalizedText('其他支付方式', 'Other Payment Methods') }}</view>
</view>
<scroll-view scroll-y class="popup-body" :show-scrollbar="false">
<view class="popup-content">
<view v-if="hasConfiguredContent" class="hero-card">
<view class="hero-badge">{{ getLocalizedText('线下支付说明', 'Alternative Payment') }}</view>
<view class="hero-title">{{ getLocalizedText('定金支付备用方案', 'Deposit payment backup plan') }}</view>
<view class="hero-desc">
{{ getLocalizedText(
'当线上支付渠道暂不可用时,可通过下方联系电话、二维码或转账说明继续完成定金支付。',
'Use the configured contact, QR code, or transfer instructions below when the online payment channel is unavailable.'
) }}
</view>
</view>
<view v-if="loading" class="loading-card">
<view class="loading-chip">{{ getLocalizedText('正在同步其他支付配置,请稍候...', 'Syncing alternative payment details...') }}</view>
</view>
<template v-else>
<view v-if="phone" class="contact-card">
<view class="contact-main">
<view class="contact-label">{{ getLocalizedText('联系电话', 'Contact Phone') }}</view>
<view class="contact-value">{{ phone }}</view>
</view>
<view class="contact-action" @click="handleCallPhone">
<text class="tn-text-bold">{{ getLocalizedText('拨打电话', 'Call') }}</text>
</view>
</view>
<view v-if="qrCodeUrl" class="qr-card">
<view class="section-title">{{ getLocalizedText('支付二维码', 'Payment QR Code') }}</view>
<image class="qr-image" :src="qrCodeUrl" mode="widthFix" @click="previewQrCode"></image>
<view class="qr-hint">
{{ getLocalizedText('点击二维码图片可查看大图。', 'Tap the image to preview the code in full size.') }}
</view>
</view>
<view v-if="richTextNodes" class="rich-text-card">
<view class="section-title">{{ getLocalizedText('支付说明', 'Payment Instructions') }}</view>
<rich-text class="rich-text-content" :nodes="richTextNodes"></rich-text>
</view>
<view v-if="!phone && !qrCodeUrl && !richTextNodes" class="empty-card">
<view class="empty-title">{{ getLocalizedText('暂未配置展示内容', 'Configuration Pending') }}</view>
<view class="empty-desc">
{{ getLocalizedText(
'当前暂未同步到其他支付配置,请联系公寓工作人员处理。',
'Alternative payment details are not available yet. Please contact the property staff for manual payment assistance.'
) }}
</view>
</view>
</template>
</view>
</scroll-view>
<view class="popup-footer">
<view class="notice-action-btn" @click="close">
<text class="tn-text-bold">{{ getLocalizedText('我知道了', 'Got It') }}</text>
</view>
</view>
</view>
</tn-popup>
</template>
<script>
import { addFilePrefix } from '@/utils/common'
import { normalizeRichTextImages } from '@/utils/rich-text'
function escapeHtml(value = '') {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function normalizeContentToRichText(value = '') {
const text = String(value || '').trim()
if (!text) {
return ''
}
if (/<[a-z][\s\S]*>/i.test(text)) {
return normalizeRichTextImages(
text,
'display:block;width:100%;max-width:100%;height:auto;box-sizing:border-box;border-radius:20rpx;margin:18rpx 0;vertical-align:top;'
)
}
return `<div style="white-space:pre-wrap;line-height:1.9;color:#334155;font-size:28rpx;">${escapeHtml(text)}</div>`
}
export default {
name: 'DepositOtherPaymentPopup',
props: {
value: {
type: Boolean,
default: false
},
config: {
type: Object,
default: () => ({})
},
loading: {
type: Boolean,
default: false
}
},
computed: {
language() {
return this.$getLanguage()
},
phone() {
return String((this.config && this.config.phone) || '').trim()
},
qrCodeUrl() {
return addFilePrefix(String((this.config && this.config.qrCode) || '').trim())
},
localizedContent() {
if (this.language === 'en') {
return this.config.contentEn || this.config.contentZh || ''
}
return this.config.contentZh || this.config.contentEn || ''
},
hasConfiguredContent() {
return !!(this.phone || this.qrCodeUrl || this.richTextNodes)
},
richTextNodes() {
return normalizeContentToRichText(this.localizedContent)
}
},
methods: {
getLocalizedText(zh, en) {
return this.language === 'en' ? en : zh
},
handleInput(value) {
this.$emit('input', value)
this.$emit('update:modelValue', value)
},
close() {
this.handleInput(false)
},
handleCallPhone() {
if (!this.phone) {
uni.showToast({
title: this.getLocalizedText('暂无联系电话', 'No contact phone available'),
icon: 'none'
})
return
}
uni.makePhoneCall({
phoneNumber: this.phone
})
},
previewQrCode() {
if (!this.qrCodeUrl) {
return
}
uni.previewImage({
urls: [this.qrCodeUrl]
})
}
}
}
</script>
<style scoped>
.deposit-other-payment-popup {
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
height: 80vh;
}
.popup-header {
padding: 18rpx 88rpx 20rpx;
text-align: center;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(251, 253, 255, 0.96) 100%);
}
.popup-handle {
width: 88rpx;
height: 8rpx;
margin: 0 auto 18rpx;
border-radius: 999rpx;
background: rgba(148, 163, 184, 0.35);
}
.popup-title {
font-size: 34rpx;
font-weight: 600;
line-height: 1.2;
letter-spacing: 1rpx;
color: #1a2233;
}
.popup-body {
flex: 1;
height: 0;
min-height: 0;
padding: 28rpx 28rpx 24rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #f7faff 0%, #f2f5fc 100%);
}
.popup-content {
display: flex;
flex-direction: column;
gap: 24rpx;
padding-bottom: 24rpx;
}
.hero-card {
padding: 28rpx;
border-radius: 32rpx;
background:
radial-gradient(circle at top right, rgba(43, 142, 255, 0.16) 0, rgba(43, 142, 255, 0) 38%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(246, 250, 255, 0.96) 100%);
border: 1rpx solid rgba(43, 142, 255, 0.1);
box-shadow: 0 24rpx 60rpx rgba(43, 142, 255, 0.1);
}
.hero-badge {
display: inline-flex;
align-items: center;
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: rgba(43, 142, 255, 0.1);
color: #2b6fd6;
font-size: 22rpx;
font-weight: 700;
}
.hero-title {
margin-top: 18rpx;
font-size: 36rpx;
line-height: 1.25;
font-weight: 700;
color: #183b73;
}
.hero-desc {
margin-top: 12rpx;
font-size: 25rpx;
line-height: 1.7;
color: #607089;
}
.loading-card,
.contact-card,
.qr-card,
.rich-text-card,
.empty-card {
padding: 24rpx 28rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.88);
border: 1rpx solid rgba(43, 142, 255, 0.08);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.85);
}
.loading-chip {
display: inline-flex;
padding: 12rpx 20rpx;
border-radius: 999rpx;
background: rgba(43, 142, 255, 0.08);
color: #4c6a95;
font-size: 24rpx;
}
.contact-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.contact-main {
flex: 1;
min-width: 0;
}
.contact-label,
.section-title {
font-size: 24rpx;
color: #7c8ca5;
font-weight: 600;
}
.contact-value {
margin-top: 10rpx;
font-size: 32rpx;
line-height: 1.4;
font-weight: 700;
color: #22324d;
word-break: break-all;
}
.contact-action {
padding: 0 28rpx;
height: 76rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #2b8eff 0%, #1f6fe5 100%);
color: #ffffff;
box-shadow: 0 14rpx 28rpx rgba(43, 142, 255, 0.18);
flex-shrink: 0;
}
.qr-card {
text-align: center;
}
.qr-image {
width: 360rpx;
max-width: 100%;
margin: 18rpx auto 0;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 18rpx 40rpx rgba(15, 23, 42, 0.1);
}
.qr-hint {
margin-top: 16rpx;
font-size: 22rpx;
line-height: 1.6;
color: #7c8ca5;
}
.rich-text-content {
margin-top: 18rpx;
}
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: #22324d;
}
.empty-desc {
margin-top: 10rpx;
font-size: 24rpx;
line-height: 1.7;
color: #7c8ca5;
}
.popup-footer {
padding: 18rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.92) 26%, #ffffff 100%);
display: flex;
justify-content: center;
backdrop-filter: blur(8rpx);
}
.notice-action-btn {
width: 100%;
height: 88rpx;
padding: 0 40rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
background: linear-gradient(135deg, #2b8eff 0%, #1f6fe5 100%);
color: #ffffff;
box-shadow: 0 14rpx 28rpx rgba(43, 142, 255, 0.22);
}
</style>

View File

@@ -36,9 +36,9 @@ export default {
return null
}
return {
title: this.t('pendingTitle'),
desc: this.t('pendingDesc', { count: this.pendingContracts.length }),
actionText: this.t('actionText')
title: this.$tr('待签合同提醒'),
desc: this.$tr('您有 {count} 份合同待签署', { count: this.pendingContracts.length }),
actionText: this.$tr('去签署')
}
}
},
@@ -59,9 +59,6 @@ export default {
this.clearCollapseTimer()
},
methods: {
t(key, params) {
return this.$translate(this.i18n, key, params)
},
clearCollapseTimer() {
if (this.collapseTimer) {
clearTimeout(this.collapseTimer)

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"name" : "测试",
"name" : "绿居公寓手机端",
"appid" : "__UNI__056D753",
"description" : "",
"versionName" : "1.0.0",

View File

@@ -156,6 +156,12 @@
"navigationBarTitleText": "Utility Recharge"
}
},
{
"path": "lock_control/lock_control",
"style": {
"navigationBarTitleText": "Smart Lock"
}
},
{
"path": "smart_control/smart_control",
"style": {

View File

@@ -1,12 +1,12 @@
<template>
<view class="message-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="false" :bottomShadow="false" backgroundColor="transparent" :customBackStyle="{ transition: 'all 0.3s ease' }">
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('title') }}</view>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('消息中心') }}</view>
</tn-nav-bar>
<view class="message-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view class="page-header tn-margin-bottom-xl">
<view class="tn-text-bold tn-color-black page-title">{{ t('title') }}</view>
<view class="tn-text-bold tn-color-black page-title">{{ $tr('消息中心') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ subtitleText }}</view>
</view>
@@ -16,31 +16,31 @@
<text class="tn-icon-notice-fill tn-text-xxl"></text>
<view v-if="stats.systemUnread" class="badge tn-flex tn-flex-col-center tn-flex-row-center">{{ stats.systemUnread }}</view>
</view>
<view class="tn-text-sm tn-margin-top-sm tn-text-bold tn-color-black">{{ t('system') }}</view>
<view class="tn-text-sm tn-margin-top-sm tn-text-bold tn-color-black">{{ $tr('系统通知') }}</view>
</view>
<view class="category-item tn-flex tn-flex-direction-column tn-flex-col-center">
<view class="category-icon-box normal-box">
<text class="tn-icon-coupon-fill tn-text-xxl"></text>
<view v-if="stats.paymentUnread" class="badge tn-flex tn-flex-col-center tn-flex-row-center">{{ stats.paymentUnread }}</view>
</view>
<view class="tn-text-sm tn-margin-top-sm tn-text-bold tn-color-gray">{{ t('payment') }}</view>
<view class="tn-text-sm tn-margin-top-sm tn-text-bold tn-color-gray">{{ $tr('缴费账单') }}</view>
</view>
<view class="category-item tn-flex tn-flex-direction-column tn-flex-col-center">
<view class="category-icon-box normal-box">
<text class="tn-icon-service-fill tn-text-xxl"></text>
<view v-if="stats.butlerUnread" class="badge tn-flex tn-flex-col-center tn-flex-row-center">{{ stats.butlerUnread }}</view>
</view>
<view class="tn-text-sm tn-margin-top-sm tn-text-bold tn-color-gray">{{ t('butler') }}</view>
<view class="tn-text-sm tn-margin-top-sm tn-text-bold tn-color-gray">{{ $tr('管家消息') }}</view>
</view>
</view>
<view v-if="inspectionList.length" class="section-card tn-margin-bottom">
<view class="section-head">
<view>
<view class="section-title">{{ t('inspectionTitle') }}</view>
<view class="section-desc">{{ t('inspectionDesc') }}</view>
<view class="section-title">{{ $tr('巡查提醒') }}</view>
<view class="section-desc">{{ $tr('根据当前入住订单自动汇总巡查提醒记录') }}</view>
</view>
<view class="section-chip is-primary">{{ inspectionReminderCount }} {{ t('inspectionCountUnit') }}</view>
<view class="section-chip is-primary">{{ inspectionReminderCount }} {{ $tr('条提醒') }}</view>
</view>
<view
@@ -56,7 +56,7 @@
</view>
<view class="inspection-main">
<view class="inspection-title">{{ record.typeText }}</view>
<view class="inspection-meta">{{ record.roomCode || t('unassignedRoom') }} · {{ record.timeText }}</view>
<view class="inspection-meta">{{ record.roomCode || $tr('未关联房间') }} · {{ record.timeText }}</view>
</view>
</view>
<view class="inspection-chips">
@@ -66,23 +66,23 @@
</view>
<view class="inspection-desc">{{ record.summaryText }}</view>
<view class="inspection-footer">
<view class="inspection-footer-text">{{ t('checkerLabel') }}{{ record.checkerUserCode || '--' }}</view>
<view class="inspection-action">{{ t('viewDetail') }}</view>
<view class="inspection-footer-text">{{ $tr('检查人:') }}{{ record.checkerUserCode || '--' }}</view>
<view class="inspection-action">{{ $tr('查看详情') }}</view>
</view>
</view>
</view>
<view class="tn-flex tn-flex-row-between tn-flex-col-bottom tn-margin-bottom">
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('recentMessages') }}</view>
<view class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('近期消息') }}</view>
<view class="tn-text-sm tn-color-gray" @click="handleReadAll">
<text class="tn-icon-delete tn-margin-right-xs"></text>{{ t('markNotificationRead') }}
<text class="tn-icon-delete tn-margin-right-xs"></text>{{ $tr('通知已读') }}
</view>
</view>
<view class="msg-list">
<view v-if="loading" class="empty-card">
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('loadingTitle') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ t('loadingDesc') }}</view>
<view class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('消息加载中') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ $tr('正在同步系统通知和巡查提醒,请稍候。') }}</view>
</view>
<view
v-for="(msg, index) in messages"
@@ -103,8 +103,8 @@
</view>
</view>
<view v-if="!loading && !messages.length && !inspectionList.length" class="empty-card">
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('emptyTitle') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ t('emptyDesc') }}</view>
<view class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('暂无消息') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ $tr('系统通知和巡查提醒会在这里显示。') }}</view>
</view>
</view>
@@ -117,7 +117,7 @@
<view class="inspect-popup-head">
<view>
<view class="inspect-popup-title">{{ activeInspectionRecord.typeText }}</view>
<view class="inspect-popup-subtitle">{{ activeInspectionRecord.roomCode || t('unassignedRoom') }} · {{ activeInspectionRecord.timeText }}</view>
<view class="inspect-popup-subtitle">{{ activeInspectionRecord.roomCode || $tr('未关联房间') }} · {{ activeInspectionRecord.timeText }}</view>
</view>
<view class="inspect-popup-close" @click="closeInspectionPopup">
<text class="tn-icon-close"></text>
@@ -127,39 +127,39 @@
<scroll-view scroll-y class="inspect-popup-body">
<view class="inspect-detail-card">
<view class="inspect-detail-row">
<text class="inspect-detail-label">{{ t('inspectionChecker') }}</text>
<text class="inspect-detail-label">{{ $tr('检查人') }}</text>
<text class="inspect-detail-value">{{ activeInspectionRecord.checkerUserCode || '--' }}</text>
</view>
<view class="inspect-detail-row">
<text class="inspect-detail-label">{{ t('inspectionResult') }}</text>
<text class="inspect-detail-label">{{ $tr('检查结果') }}</text>
<text class="inspect-detail-value">{{ activeInspectionRecord.resultText }}</text>
</view>
<view class="inspect-detail-row">
<text class="inspect-detail-label">{{ t('inspectionStatus') }}</text>
<text class="inspect-detail-label">{{ $tr('当前状态') }}</text>
<text class="inspect-detail-value">{{ activeInspectionRecord.statusText }}</text>
</view>
</view>
<view v-if="activeInspectionRecord.remark" class="inspect-block">
<view class="inspect-block-title">{{ t('inspectionRemarkTitle') }}</view>
<view class="inspect-block-title">{{ $tr('检查备注') }}</view>
<view class="inspect-block-content">{{ activeInspectionRecord.remark }}</view>
</view>
<view v-if="activeInspectionRecord.items.length" class="inspect-block">
<view class="inspect-block-title">{{ t('inspectionItemsTitle') }}</view>
<view class="inspect-block-title">{{ $tr('检查项明细') }}</view>
<view
v-for="(detail, index) in activeInspectionRecord.items"
:key="detail.code || index"
class="inspect-item"
>
<view class="inspect-item-head">
<view class="inspect-item-title">{{ t('inspectionItemPrefix', { index: index + 1 }) }}</view>
<view class="inspect-item-title">{{ $tr('检查项 {index}', { index: index + 1 }) }}</view>
<view class="inspect-item-tag">{{ getCheckTypeLabel(detail.checkType) }}</view>
</view>
<view v-if="detail.problemDescription" class="inspect-item-text">{{ t('problemLabel') }}{{ detail.problemDescription }}</view>
<view v-if="detail.remark" class="inspect-item-text">{{ t('remarkLabel') }}{{ detail.remark }}</view>
<view v-if="detail.problemDescription" class="inspect-item-text">{{ $tr('问题:') }}{{ detail.problemDescription }}</view>
<view v-if="detail.remark" class="inspect-item-text">{{ $tr('备注:') }}{{ detail.remark }}</view>
<view v-if="detail.compensationAmount !== '' && detail.compensationAmount !== null && detail.compensationAmount !== undefined" class="inspect-item-text">
{{ t('compensationLabel') }}{{ detail.compensationAmount }}
{{ $tr('赔付金额:') }}{{ detail.compensationAmount }}
</view>
<view v-if="detail.imageUrls.length" class="inspect-image-list">
<image
@@ -176,7 +176,7 @@
</scroll-view>
<view class="inspect-popup-foot">
<view class="inspect-popup-btn" @click="closeInspectionPopup">{{ t('acknowledged') }}</view>
<view class="inspect-popup-btn" @click="closeInspectionPopup">{{ $tr('我知道了') }}</view>
</view>
</view>
</view>
@@ -214,104 +214,6 @@ export default {
paymentUnread: 0,
butlerUnread: 0
},
i18n: {
zh: {
title: '消息中心',
subtitle: '你有 {count} 条未读消息待处理',
system: '系统通知',
payment: '缴费账单',
butler: '管家消息',
inspectionTitle: '巡查提醒',
inspectionDesc: '根据当前入住订单自动汇总巡查提醒记录',
inspectionCountUnit: '条提醒',
recentMessages: '近期消息',
markNotificationRead: '通知已读',
loadingTitle: '消息加载中',
loadingDesc: '正在同步系统通知和巡查提醒,请稍候。',
emptyTitle: '暂无消息',
emptyDesc: '系统通知和巡查提醒会在这里显示。',
noUnreadNotification: '暂无未读通知',
inspectionNeedManualHandle: '巡查提醒需单独查看处理',
remoteMarkedRead: '系统通知已全部标记为已读',
remoteMarkedReadWithInspection: '系统通知已标记,巡查提醒需单独处理',
unassignedRoom: '未关联房间',
checkerLabel: '检查人:',
viewDetail: '查看详情',
inspectionChecker: '检查人',
inspectionResult: '检查结果',
inspectionStatus: '当前状态',
inspectionRemarkTitle: '检查备注',
inspectionItemsTitle: '检查项明细',
inspectionItemPrefix: '检查项 {index}',
problemLabel: '问题:',
remarkLabel: '备注:',
compensationLabel: '赔付金额:',
acknowledged: '我知道了',
notificationFallbackTitle: '系统通知',
notificationFallbackDesc: '您有一条新的系统消息',
inspectionSummaryFallback: '巡查提醒,请查看详情',
checkTypeRoutine: '日常检查',
checkTypeCheckout: '退房检查',
checkTypeSpecial: '专项检查',
checkTypeDefault: '检查记录',
checkStatusPending: '待确认',
checkStatusProcessing: '处理中',
checkStatusDone: '已完成',
checkStatusDefault: '状态待定',
checkResultNormal: '正常',
checkResultFix: '需整改',
checkResultReview: '待复核',
checkResultDefault: '检查结果'
},
en: {
title: 'Messages',
subtitle: 'You have {count} unread messages',
system: 'System',
payment: 'Payment',
butler: 'Butler',
inspectionTitle: 'Inspection',
inspectionDesc: 'Inspection reminders are summarized automatically.',
inspectionCountUnit: 'alerts',
recentMessages: 'Recent',
markNotificationRead: 'Read Notifications',
loadingTitle: 'Loading Messages',
loadingDesc: 'Syncing notifications and inspections.',
emptyTitle: 'No Messages',
emptyDesc: 'Notifications and inspection reminders will appear here.',
noUnreadNotification: 'No unread notifications',
inspectionNeedManualHandle: 'Inspection reminders need manual handling',
remoteMarkedRead: 'Notifications marked as read',
remoteMarkedReadWithInspection: 'Notifications marked as read, inspection reminders still need handling',
unassignedRoom: 'Room Unlinked',
checkerLabel: 'Inspector: ',
viewDetail: 'Details',
inspectionChecker: 'Inspector',
inspectionResult: 'Result',
inspectionStatus: 'Status',
inspectionRemarkTitle: 'Inspection Remark',
inspectionItemsTitle: 'Inspection Items',
inspectionItemPrefix: 'Item {index}',
problemLabel: 'Problem: ',
remarkLabel: 'Remark: ',
compensationLabel: 'Compensation: ',
acknowledged: 'Got It',
notificationFallbackTitle: 'System Notification',
notificationFallbackDesc: 'You have a new system message',
inspectionSummaryFallback: 'Inspection reminder, tap to view details.',
checkTypeRoutine: 'Routine Check',
checkTypeCheckout: 'Checkout Check',
checkTypeSpecial: 'Special Check',
checkTypeDefault: 'Inspection Record',
checkStatusPending: 'Pending',
checkStatusProcessing: 'Processing',
checkStatusDone: 'Completed',
checkStatusDefault: 'Status Pending',
checkResultNormal: 'Normal',
checkResultFix: 'Needs Fix',
checkResultReview: 'Pending Review',
checkResultDefault: 'Inspection Result'
}
}
}
},
computed: {
@@ -319,7 +221,7 @@ export default {
return this.$getLanguage()
},
subtitleText() {
const text = this.t('subtitle').replace('{count}', this.stats.totalUnread)
const text = this.$tr('你有 {count} 条未读消息待处理').replace('{count}', this.stats.totalUnread)
return this.language === 'en' ? compactEnglishText(text, 48) : text
},
inspectionReminderCount() {
@@ -338,9 +240,6 @@ export default {
load(force = false) {
return this.refreshMessages(force)
},
t(key, params) {
return this.$tm('pages.index.component.MessageCenterTab', key, params)
},
getStoreUser() {
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
},
@@ -471,15 +370,15 @@ export default {
id: item.code || item.id || `remote-${Date.now()}`,
code: item.code || '',
category: this.resolveRemoteCategory(item, title, desc),
titleZh: rawTitleZh || this.t('notificationFallbackTitle'),
titleEn: rawTitleEn || this.t('notificationFallbackTitle'),
descZh: rawDescZh || this.t('notificationFallbackDesc'),
descEn: rawDescEn || this.t('notificationFallbackDesc'),
contentZh: rawDescZh || this.t('notificationFallbackDesc'),
contentEn: rawDescEn || this.t('notificationFallbackDesc'),
title: title || this.t('notificationFallbackTitle'),
desc: desc || this.t('notificationFallbackDesc'),
content: desc || this.t('notificationFallbackDesc'),
titleZh: rawTitleZh || this.$tr('系统通知'),
titleEn: rawTitleEn || this.$tr('系统通知'),
descZh: rawDescZh || this.$tr('您有一条新的系统消息'),
descEn: rawDescEn || this.$tr('您有一条新的系统消息'),
contentZh: rawDescZh || this.$tr('您有一条新的系统消息'),
contentEn: rawDescEn || this.$tr('您有一条新的系统消息'),
title: title || this.$tr('系统通知'),
desc: desc || this.$tr('您有一条新的系统消息'),
content: desc || this.$tr('您有一条新的系统消息'),
time: formatMessageDateTime(item.createTime || item.updateTime || ''),
rawTime: item.createTime || item.updateTime || '',
unread: item.isRead !== true,
@@ -501,7 +400,7 @@ export default {
}))
: []
const issueTexts = items.map(item => item.problemDescription || item.remark || '').filter(Boolean)
const summaryText = record.remark || issueTexts[0] || this.t('inspectionSummaryFallback')
const summaryText = record.remark || issueTexts[0] || this.$tr('巡查提醒,请查看详情')
return {
code: record.code || '',
roomCode: record.roomCode || '',
@@ -545,27 +444,27 @@ export default {
},
getCheckTypeLabel(value) {
const map = {
0: this.t('checkTypeRoutine'),
1: this.t('checkTypeCheckout'),
2: this.t('checkTypeSpecial')
0: this.$tr('日常检查'),
1: this.$tr('退房检查'),
2: this.$tr('专项检查')
}
return map[Number(value)] || this.t('checkTypeDefault')
return map[Number(value)] || this.$tr('检查记录')
},
getCheckRecordStatusLabel(value) {
const map = {
0: this.t('checkStatusPending'),
1: this.t('checkStatusProcessing'),
2: this.t('checkStatusDone')
0: this.$tr('待确认'),
1: this.$tr('处理中'),
2: this.$tr('已完成')
}
return map[Number(value)] || this.t('checkStatusDefault')
return map[Number(value)] || this.$tr('状态待定')
},
getCheckResultStatusLabel(value) {
const map = {
0: this.t('checkResultNormal'),
1: this.t('checkResultFix'),
2: this.t('checkResultReview')
0: this.$tr('正常'),
1: this.$tr('需整改'),
2: this.$tr('待复核')
}
return map[Number(value)] || this.t('checkResultDefault')
return map[Number(value)] || this.$tr('检查结果')
},
getCheckResultStatusClass(value) {
const map = {
@@ -618,8 +517,8 @@ export default {
if (!unreadRemoteCodes.length) {
uni.showToast({
title: this.inspectionReminderCount
? this.t('inspectionNeedManualHandle')
: this.t('noUnreadNotification'),
? this.$tr('巡查提醒需单独查看处理')
: this.$tr('暂无未读通知'),
icon: 'none'
})
return
@@ -642,8 +541,8 @@ export default {
this.buildMessageView()
uni.showToast({
title: this.inspectionReminderCount
? this.t('remoteMarkedReadWithInspection')
: this.t('remoteMarkedRead'),
? this.$tr('系统通知已标记,巡查提醒需单独处理')
: this.$tr('系统通知已全部标记为已读'),
icon: 'none'
})
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="profile-page tn-safe-area-inset-bottom">
<!-- 头部背景与用户信息 (悬浮质感设计) -->
@@ -43,7 +43,7 @@
</view>
<view class="tn-margin-left-md">
<view class="user-name clamp-text-1">
{{ t('guest') }}
{{ $tr('游客') }}
</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ guestHeaderDesc }}</view>
</view>
@@ -62,17 +62,17 @@
<view class="stats-row tn-flex tn-flex-row-between">
<view class="stat-item">
<view class="stat-value">{{ getStayDaysText() }}</view>
<view class="stat-label">{{ t('daysIn') }}</view>
<view class="stat-label">{{ $tr('入住天数') }}</view>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<view class="stat-value">{{ getActivityCountText() }}</view>
<view class="stat-label">{{ t('activities') }}</view>
<view class="stat-label">{{ $tr('参与活动') }}</view>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<view class="stat-value">{{ getPointsText() }}</view>
<view class="stat-label">{{ t('points') }}</view>
<view class="stat-label">{{ $tr('积分') }}</view>
</view>
</view>
</view>
@@ -84,40 +84,40 @@
<view class="guest-registration-card tn-padding-lg tn-margin-bottom-lg" v-if="!isLogin">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view>
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('registrationRecordTitle') }}</view>
<view class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('注册记录') }}</view>
<view class="tn-margin-top-sm tn-text-sm tn-color-gray">
{{ registrationApplication ? t('registrationRecordSubmittedDesc') : t('registrationRecordGuestDesc') }}
{{ registrationApplication ? $tr('当前微信账号已提交注册申请,可查看历史记录与审批状态。') : $tr('未登录也可查看当前微信账号提交的注册申请记录。') }}
</view>
</view>
<view class="custom-btn btn-primary guest-registration-btn" @click="goToRegistrationRecords">
<text class="tn-text-sm tn-text-bold">{{ t('viewNow') }}</text>
<text class="tn-text-sm tn-text-bold">{{ $tr('立即查看') }}</text>
</view>
</view>
</view>
<view class="application-status-card tn-padding-lg tn-margin-bottom-lg" v-if="isLogin && pendingContractCount">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('contractStatusTitle') }}</view>
<view class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('合同状态') }}</view>
<view class="status-tag status-pending">
{{ t('contractPendingTag') + ' ' + pendingContractCount }}
{{ $tr('待签约') + ' ' + pendingContractCount }}
</view>
</view>
<view class="tn-margin-top-sm tn-color-gray tn-text-sm">
{{ t('contractPendingDesc') }}
{{ $tr('您当前有待签署合同,请尽快查看并完成签约。') }}
</view>
<view class="tn-margin-top tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-color-gray tn-text-xs">
{{ t('pendingCountText') + ':' + pendingContractCount }}
{{ $tr('待签合同') + ':' + pendingContractCount }}
</view>
<view class="custom-btn btn-primary" style="padding: 8rpx 28rpx;" @click="goToContractList">
<text class="tn-text-sm tn-text-bold">{{ t('viewDetail') }}</text>
<text class="tn-text-sm tn-text-bold">{{ $tr('查看详情') }}</text>
</view>
</view>
</view>
<!-- 核心服务 (宫格) -->
<view class="core-services-card tn-padding-lg tn-margin-bottom-lg" v-if="isLogin">
<view class="tn-text-bold tn-text-lg tn-color-black tn-margin-bottom">{{ t('coreServices') }}</view>
<view class="tn-text-bold tn-text-lg tn-color-black tn-margin-bottom">{{ $tr('我的服务') }}</view>
<view class="tn-flex tn-flex-row-between">
<view
class="core-service-item tn-flex tn-flex-direction-column tn-flex-col-center"
@@ -128,7 +128,7 @@
<view class="service-icon-box tn-flex tn-flex-col-center tn-flex-row-center">
<text :class="item.icon" class="service-icon-symbol"></text>
</view>
<text class="tn-margin-top-sm tn-text-sm tn-color-black">{{ t(item.i18nKey) }}</text>
<text class="tn-margin-top-sm tn-text-sm tn-color-black">{{ $tr(item.i18nKey) }}</text>
</view>
</view>
</view>
@@ -146,16 +146,16 @@
<text class="tn-margin-left-sm tn-text-md tn-color-black tn-text-bold">{{ getMenuLabel(item) }}</text>
</view>
<view class="tn-flex tn-flex-col-center">
<view v-if="item.available === false" class="menu-coming-soon">{{ t('comingSoon') }}</view>
<view v-if="item.available === false" class="menu-coming-soon">{{ $tr('建设中') }}</view>
<text class="tn-icon-right menu-arrow"></text>
</view>
</view>
<view class="menu-item tn-flex tn-flex-row-between tn-flex-col-center" @click="handleLanguageToggle">
<view class="tn-flex tn-flex-col-center">
<view class="menu-icon"><text class="tn-icon-set menu-icon-symbol"></text></view>
<text class="tn-margin-left-sm tn-text-md tn-color-black tn-text-bold">{{ t('languageSwitch') }}</text>
<text class="tn-margin-left-sm tn-text-md tn-color-black tn-text-bold">{{ $tr('语言切换') }}</text>
</view>
<text class="tn-text-sm tn-color-gray">{{ t('languageToggleLabel') }}</text>
<text class="tn-text-sm tn-color-gray">{{ $tr('EN') }}</text>
</view>
</view>
@@ -163,7 +163,7 @@
<!-- 退出登录 -->
<view class="tn-margin-top-xl tn-text-center" v-if="isLogin">
<view class="custom-btn btn-danger" style="width: 80%; height: 90rpx;" @click="handleLogout">
<text class="tn-text-bold">{{ t('logout') }}</text>
<text class="tn-text-bold">{{ $tr('退出登录') }}</text>
</view>
</view>
@@ -192,7 +192,7 @@ export default {
return typeof this.vuex_custom_bar_height === 'number' ? this.vuex_custom_bar_height : 0
},
displayUserName() {
return this.userInfo.name || this.userInfo.account || this.userInfo.username || this.userInfo.mobile || this.t('userFallback')
return this.userInfo.name || this.userInfo.account || this.userInfo.username || this.userInfo.mobile || this.$tr('用户')
},
displayUserInitial() {
const firstChar = this.userInfo.firstChar
@@ -217,21 +217,21 @@ export default {
},
guestHeaderDesc() {
return this.registrationApplication
? `${this.t('registrationStatusTitle')}:${this.getRegistrationStatusText(this.registrationStatus)}`
: this.t('clickToLogin')
? `${this.$tr('注册申请进度')}:${this.getRegistrationStatusText(this.registrationStatus)}`
: this.$tr('点击登录')
},
registrationStatusDesc() {
if (!this.registrationApplication) return this.t('clickToLogin')
if (!this.registrationApplication) return this.$tr('点击登录')
if (this.registrationStatus === 'pending') {
return this.t('registrationPendingDesc')
return this.$tr('注册申请审核中,审批通过后才可登录')
}
if (this.registrationStatus === 'approved') {
return this.t('registrationApprovedDesc')
return this.$tr('注册申请已通过,可前往登录')
}
if (this.registrationStatus === 'rejected') {
return this.registrationApplication.approveReason || this.t('registrationRejectedDesc')
return this.registrationApplication.approveReason || this.$tr('注册申请已驳回,请查看原因后重新提交')
}
return this.t('registrationEmptyDesc')
return this.$tr('暂未提交注册申请')
},
currentBookingCard() {
if (!this.isLogin) {
@@ -272,119 +272,11 @@ export default {
{ key: 'butler', label: '联系管家', labelEn: 'Contact Butler', icon: 'tn-icon-service-fill', title: '联系管家', titleEn: 'Contact Butler', desc: '联系专属管家,沟通入住和住中服务事项。', descEn: 'Contact your butler about move-in and in-stay services.', available: true }
],
coreServices: [
{ key: 'application', i18nKey: 'application', icon: 'tn-icon-edit-form' },
{ key: 'bookingList', i18nKey: 'bookingList', icon: 'tn-icon-order-fill' },
{ key: 'contract', i18nKey: 'contract', icon: 'tn-icon-copy' },
{ key: 'bill', i18nKey: 'bill', icon: 'tn-icon-money' }
{ key: 'application', i18nKey: '入住申请', icon: 'tn-icon-edit-form' },
{ key: 'bookingList', i18nKey: '预订记录', icon: 'tn-icon-order-fill' },
{ key: 'contract', i18nKey: '我的合同', icon: 'tn-icon-copy' },
{ key: 'bill', i18nKey: '账单', icon: 'tn-icon-money' }
],
i18n: {
zh: {
edit: '编辑资料',
guest: '游客',
clickToLogin: '点击登录',
daysIn: '入住天数',
activities: '参与活动',
points: '积分',
applicationStatus: '申请进度',
registrationStatusTitle: '注册申请进度',
notSubmittedYet: '暂未提交注册申请',
registrationPendingDesc: '注册申请审核中,审批通过后才可登录',
registrationApprovedDesc: '注册申请已通过,可前往登录',
registrationRejectedDesc: '注册申请已驳回,请查看原因后重新提交',
registrationEmptyDesc: '暂未提交注册申请',
registrationRecordTitle: '注册记录',
registrationRecordSubmittedDesc: '当前微信账号已提交注册申请,可查看历史记录与审批状态。',
registrationRecordGuestDesc: '未登录也可查看当前微信账号提交的注册申请记录。',
viewNow: '立即查看',
statusPending: '审核中',
statusApproved: '已通过',
statusRejected: '已拒绝',
remind: '催一下',
done: '已完成',
viewDetail: '查看详情',
exclusiveButler: '专属管家',
onlineService: '24小时在线服务',
contact: '联系',
application: '入住申请',
bookingList: '预订记录',
bookingStatusTitle: '预订审核进度',
contractStatusTitle: '合同状态',
bookingNo: '预定单号',
bookingPlaceholder: '支付后生成的预定单会显示在这里',
bookingProgressDesc: '支付完成后会进入审核,审核通过后分配管家并等待分房号。',
contractPendingTag: '待签约',
contractPendingDesc: '您当前有待签署合同,请尽快查看并完成签约。',
pendingCountText: '待签合同',
contract: '我的合同',
bill: '账单',
coreServices: '我的服务',
comingSoon: '建设中',
settings: '偏好设置',
languageSwitch: '语言切换',
languageToggleLabel: 'EN',
logout: '退出登录',
logoutConfirmTitle: '提示',
logoutConfirmContent: '确定要退出登录吗?',
logoutSuccess: '已退出',
userFallback: '用户',
submitRequest: '提交申请',
completeFormBeforeSubmit: '请完善表单信息后提交。',
comingSoonDesc: '{title} 功能正在建设中,后续开放后会在这里提供正式入口和数据。'
},
en: {
edit: 'Edit',
guest: 'Guest',
clickToLogin: 'Click to Sign In',
daysIn: 'Days Stayed',
activities: 'Activities',
points: 'Points',
applicationStatus: 'Application Status',
registrationStatusTitle: 'Registration Status',
notSubmittedYet: 'No registration submitted',
registrationPendingDesc: 'Registration is under review',
registrationApprovedDesc: 'Registration approved, ready to sign in',
registrationRejectedDesc: 'Registration rejected, please review the reason',
registrationEmptyDesc: 'No registration submitted',
registrationRecordTitle: 'Registration Records',
registrationRecordSubmittedDesc: 'A registration has been submitted with this Wechat account. You can review its history and approval status.',
registrationRecordGuestDesc: 'You can still review registration records submitted with this Wechat account before signing in.',
viewNow: 'View Now',
statusPending: 'Under Review',
statusApproved: 'Approved',
statusRejected: 'Rejected',
remind: 'Remind',
done: 'Done',
viewDetail: 'Details',
exclusiveButler: 'Exclusive Butler',
onlineService: '24/7 Online Service',
contact: 'Contact',
application: 'Applications',
bookingList: 'Bookings',
bookingStatusTitle: 'Booking Progress',
contractStatusTitle: 'Contract Status',
bookingNo: 'Booking No.',
bookingPlaceholder: 'Your booking will appear here after payment',
bookingProgressDesc: 'Booking will be reviewed after payment. Butler will be assigned upon approval.',
contractPendingTag: 'Pending',
contractPendingDesc: 'You have contracts waiting for signature. Please review them soon.',
pendingCountText: 'Pending Contracts',
contract: 'My Contract',
bill: 'Billing',
coreServices: 'My Services',
comingSoon: 'Soon',
settings: 'Preferences',
languageSwitch: 'Language',
languageToggleLabel: '中文',
logout: 'Log Out',
logoutConfirmTitle: 'Notice',
logoutConfirmContent: 'Are you sure to log out?',
logoutSuccess: 'Logged out',
userFallback: 'User',
submitRequest: 'Submit Request',
completeFormBeforeSubmit: 'Please complete the form before submitting.',
comingSoonDesc: '{title} is under development. The formal entry and data will be available here later.'
}
}
}
},
async mounted() {
@@ -398,9 +290,6 @@ export default {
else if (key === 'contract') this.goToContractList();
else if (key === 'bill') this.goToUtilityRecharge();
},
t(key, params) {
return this.$tm('pages.index.component.ProfileCenterTab', key, params)
},
getMenuLabel(item = {}) {
return this.language === 'en'
? (item.labelEnShort || item.labelEn || item.label || '')
@@ -419,7 +308,7 @@ export default {
return '-'
}
const currentOrder = this.getCurrentOrder() || {}
const startText = currentOrder.startDate || currentOrder.start || currentOrder.checkinDate || ''
const startText = currentOrder.contractStartTime || currentOrder.startDate || currentOrder.start || currentOrder.checkinDate || ''
if (!startText) {
return '--'
}
@@ -511,14 +400,14 @@ export default {
},
handleLogout() {
uni.showModal({
title: this.t('logoutConfirmTitle'),
content: this.t('logoutConfirmContent'),
title: this.$tr('提示'),
content: this.$tr('确定要退出登录吗?'),
success: (res) => {
if (res.confirm) {
clearLoginSession()
this.checkLoginStatus();
this.$parent.changeTabbar && this.$parent.changeTabbar(0, 'select-room');
uni.showToast({ title: this.t('logoutSuccess'), icon: 'success' });
uni.showToast({ title: this.$tr('已退出'), icon: 'success' });
}
}
});
@@ -531,13 +420,13 @@ export default {
},
getRegistrationStatusText(status) {
if (this.registrationApplication && typeof this.registrationApplication.approvalStatus === 'number') {
return getApprovalStatusLabel(this.registrationApplication.approvalStatus, this.t('notSubmittedYet'))
return getApprovalStatusLabel(this.registrationApplication.approvalStatus, this.$tr('暂未提交注册申请'))
}
const map = {
unsubmitted: this.t('notSubmittedYet'),
pending: this.t('statusPending'),
approved: this.t('statusApproved'),
rejected: this.t('statusRejected')
unsubmitted: this.$tr('暂未提交注册申请'),
pending: this.$tr('审核中'),
approved: this.$tr('已通过'),
rejected: this.$tr('已拒绝')
}
return map[status] || map.unsubmitted
},
@@ -581,7 +470,7 @@ export default {
},
goToSubmitRequest(title, desc, payload = {}, titleZh = '', titleEn = '', descZh = '', descEn = '') {
uni.navigateTo({
url: `/pkgService/submit_request/submit_request?title=${encodeURIComponent(title || this.t('submitRequest'))}&titleZh=${encodeURIComponent(titleZh || title || this.t('submitRequest'))}&titleEn=${encodeURIComponent(titleEn || title || this.t('submitRequest'))}&desc=${encodeURIComponent(desc || this.t('completeFormBeforeSubmit'))}&descZh=${encodeURIComponent(descZh || desc || this.t('completeFormBeforeSubmit'))}&descEn=${encodeURIComponent(descEn || desc || this.t('completeFormBeforeSubmit'))}&payload=${encodeURIComponent(JSON.stringify(payload || {}))}`
url: `/pkgService/submit_request/submit_request?title=${encodeURIComponent(title || this.$tr('提交申请'))}&titleZh=${encodeURIComponent(titleZh || title || this.$tr('提交申请'))}&titleEn=${encodeURIComponent(titleEn || title || this.$tr('提交申请'))}&desc=${encodeURIComponent(desc || this.$tr('请完善表单信息后提交。'))}&descZh=${encodeURIComponent(descZh || desc || this.$tr('请完善表单信息后提交。'))}&descEn=${encodeURIComponent(descEn || desc || this.$tr('请完善表单信息后提交。'))}&payload=${encodeURIComponent(JSON.stringify(payload || {}))}`
})
},
handleApplicationEntry() {
@@ -620,6 +509,12 @@ export default {
this.goToRegistrationRecords()
return
}
if (item.key === 'lock') {
uni.navigateTo({
url: '/pkgService/lock_control/lock_control'
})
return
}
if (item.key === 'butler') {
this.goToSubmitRequest(itemTitle, itemDesc, {
businessType: 'butler-contact',
@@ -627,9 +522,9 @@ export default {
}, itemTitleZh, itemTitleEn, itemDescZh, itemDescEn)
return
}
const comingSoonDesc = this.t('comingSoonDesc', { title: itemTitle })
const comingSoonDescZh = this.$tm('pages.index.component.ProfileCenterTab', 'comingSoonDesc', { title: itemTitleZh || itemTitle })
const comingSoonDescEn = this.$tm('pages.index.component.ProfileCenterTab', 'comingSoonDesc', { title: itemTitleEn || itemTitle })
const comingSoonDesc = this.$tr('{title} 功能正在建设中,后续开放后会在这里提供正式入口和数据。', { title: itemTitle })
const comingSoonDescZh = this.$tr('{title} 功能正在建设中,后续开放后会在这里提供正式入口和数据。', { title: itemTitleZh || itemTitle })
const comingSoonDescEn = this.$tr('{title} 功能正在建设中,后续开放后会在这里提供正式入口和数据。', { title: itemTitleEn || itemTitle })
uni.navigateTo({
url: `/pkgService/service_stub/service_stub?title=${encodeURIComponent(itemTitle)}&titleZh=${encodeURIComponent(itemTitleZh || itemTitle)}&titleEn=${encodeURIComponent(itemTitleEn || itemTitle)}&desc=${encodeURIComponent(comingSoonDesc)}&descZh=${encodeURIComponent(comingSoonDescZh)}&descEn=${encodeURIComponent(comingSoonDescEn)}&key=${item.key}`
});

View File

@@ -28,7 +28,7 @@
</view>
<view class="search-box tn-flex tn-flex-col-center">
<text class="tn-icon-search search-icon"></text>
<text class="search-placeholder">{{ t('searchApartment') }}</text>
<text class="search-placeholder">{{ $tr('搜索区域、公寓或房型') }}</text>
</view>
<view
class="sort-trigger tn-flex tn-flex-col-center tn-flex-row-center"
@@ -44,7 +44,7 @@
<view class="quick-filter-head tn-flex tn-flex-row-between tn-flex-col-center">
<view class="quick-filter-title-box tn-flex tn-flex-col-center">
<view class="quick-filter-dot"></view>
<text class="quick-filter-title">{{ t('tagFilter') }}</text>
<text class="quick-filter-title">{{ $tr('标签筛选') }}</text>
</view>
<text class="quick-filter-current">{{ displayTag(currentTagLabel) }}</text>
</view>
@@ -57,7 +57,7 @@
:class="{ 'quick-tag--active': !selectedTag }"
@click="$emit('select-tag', '')"
>
{{ t('all') }}
{{ $tr('全部') }}
</view>
<view
v-for="tag in availableFilterTags"
@@ -81,17 +81,17 @@
class="room-status-card tn-margin-bottom-lg"
>
<view class="room-status-card__title">
{{ isAreaLoading ? t('loadingAreaStatus') : t('areaStatus') }}
{{ isAreaLoading ? $tr('区域加载中') : $tr('区域状态') }}
</view>
<view class="room-status-card__desc">
{{ roomLoadError || areaLoadError || t('areaStatusDesc') }}
{{ roomLoadError || areaLoadError || $tr('暂无符合筛选条件的区域') }}
</view>
</view>
<view v-if="!isAreaLoading && !displayAreaSections.length" class="empty-state-card tn-margin-bottom-lg">
<view class="empty-state-card__title">{{ bannerList.length ? t('emptyAreaTitle') : t('emptyBannerTitle') }}</view>
<view class="empty-state-card__title">{{ bannerList.length ? $tr('暂无匹配房源') : $tr('暂无轮播图') }}</view>
<view class="empty-state-card__desc">
{{ bannerList.length ? t('emptyAreaDesc') : t('emptyBannerDesc') }}
{{ bannerList.length ? $tr('当前筛选条件下暂无可用房源') : $tr('暂无可展示的轮播图') }}
</view>
</view>
@@ -102,7 +102,7 @@
<view class="store-title-bar tn-flex tn-flex-row-between tn-flex-col-center">
<view class="store-title tn-text-ellipsis">{{ getAreaName(area) }}</view>
<view class="store-detail-btn tn-flex tn-flex-col-center">
<text class="btn-text">{{ t('viewCommunity') }}</text>
<text class="btn-text">{{ $tr('查看详情') }}</text>
<text class="tn-icon-right btn-icon"></text>
</view>
</view>
@@ -118,7 +118,7 @@
</view>
<text class="address-text">{{ getAreaAddress(area) || defaultAreaAddress(getAreaName(area)) }}</text>
</view>
<view class="store-distance">{{ t('distancePlaceholder') }}</view>
<view class="store-distance">{{ $tr('附近') }}</view>
</view>
</view>
@@ -145,8 +145,8 @@
<view class="room-price-row tn-flex tn-flex-row-between tn-flex-col-bottom">
<view class="price-content">
<text class="price-symbol">¥</text>
<text class="price-val">{{ formatDisplayPrice(roomType.price) || t('pricePending') }}</text>
<text class="price-unit">{{ t('priceUnit') }}</text>
<text class="price-val">{{ formatDisplayPrice(roomType.price) || $tr('待定') }}</text>
<text class="price-unit">{{ $tr('元/月起') }}</text>
</view>
<view class="room-go-icon tn-flex tn-flex-col-center tn-flex-row-center">
<text class="tn-icon-right"></text>
@@ -252,6 +252,10 @@ export default {
type: Number,
default: 0
},
isActive: {
type: Boolean,
default: false
},
language: {
type: String,
default: 'zh'
@@ -272,9 +276,6 @@ export default {
}
return localizeHousingTag(text, this.language)
},
t(key) {
return this.$tm('pages.index.component.SelectRoomTab', key)
},
formatRoomTypeName(name) {
const normalizedName = String(name || '')
return normalizedName.replace(/^[^·]+·/, '')
@@ -321,7 +322,7 @@ export default {
return [...new Set(roomTypeTags)].slice(0, 4)
},
showTagActionSheet() {
const tagOptions = [this.t('allTags'), ...this.availableFilterTags]
const tagOptions = [this.$tr('全部标签'), ...this.availableFilterTags]
if (!tagOptions.length) {
return
}
@@ -335,10 +336,10 @@ export default {
},
showSortActionSheet() {
const sortOptions = [
{ label: this.t('sortDefault'), value: 'default' },
{ label: this.t('sortPriceAsc'), value: 'price-asc' },
{ label: this.t('sortPriceDesc'), value: 'price-desc' },
{ label: this.t('sortNameAsc'), value: 'name-asc' }
{ label: this.$tr('默认排序'), value: 'default' },
{ label: this.$tr('价格从低到高'), value: 'price-asc' },
{ label: this.$tr('价格从高到低'), value: 'price-desc' },
{ label: this.$tr('名称排序'), value: 'name-asc' }
]
uni.showActionSheet({
itemList: sortOptions.map((item) => item.label),
@@ -351,7 +352,14 @@ export default {
})
},
handleBannerChange(e) {
this.$emit('banner-change', e.detail.current)
if (!this.isActive) {
return
}
const nextIndex = Number(e && e.detail && e.detail.current)
if (!Number.isFinite(nextIndex)) {
return
}
this.$emit('banner-change', nextIndex)
},
openStoreDetail(area) {
uni.navigateTo({
@@ -360,9 +368,9 @@ export default {
},
defaultAreaAddress(areaName = '') {
if (this.language === 'en') {
return `${this.t('defaultAreaAddressPrefix')}${areaName} CBD`
return `${this.$tr('位于')}${areaName} CBD`
}
return `${this.t('defaultAreaAddressPrefix')}${areaName}核心商圈`
return `${this.$tr('位于')}${areaName}核心商圈`
}
}
}

View File

@@ -57,12 +57,12 @@
<!-- 底部操作区 -->
<view class="hero-bottom tn-flex tn-flex-direction-column tn-flex-col-center">
<view class="hero-actions tn-flex tn-flex-row-between" :class="{ 'hero-actions--en': language === 'en' }">
<view class="action-btn outline-btn" :class="{ 'action-btn--en': language === 'en' }" @click="$emit('consult')">{{ t('apartmentDetail') }}</view>
<view class="action-btn primary-btn" :class="{ 'action-btn--en': language === 'en' }" @click="$emit('submit')">{{ t('bookViewing') }}</view>
<view class="action-btn outline-btn" :class="{ 'action-btn--en': language === 'en' }" @click="$emit('consult')">{{ $tr('公寓详情') }}</view>
<view class="action-btn primary-btn" :class="{ 'action-btn--en': language === 'en' }" @click="$emit('submit')">{{ $tr('预约看房') }}</view>
</view>
<view class="scroll-hint tn-flex tn-flex-direction-column tn-flex-col-center" @click="$emit('collapse')">
<text class="scroll-hint__text">{{ t('learnMore') }}</text>
<text class="scroll-hint__text">{{ $tr('了解更多') }}</text>
<view class="scroll-hint__double-icon">
<text class="tn-icon-down scroll-hint__icon"></text>
<text class="tn-icon-down scroll-hint__icon scroll-hint__icon--second"></text>
@@ -91,6 +91,10 @@ export default {
type: Number,
default: 0
},
isActive: {
type: Boolean,
default: false
},
language: {
type: String,
default: 'zh'
@@ -136,9 +140,6 @@ export default {
displayTabSubtitle(item = {}) {
return this.getBannerDisplayText(item, 'tab')
},
t(key) {
return this.$tm('pages.index.component.SelectRoomTab', key)
},
formatTabTitle(title) {
const normalizedTitle = String(title || '')
return normalizedTitle
@@ -147,9 +148,19 @@ export default {
.trim()
},
handleBannerChange(e) {
this.$emit('banner-change', e.detail.current)
if (!this.isActive) {
return
}
const nextIndex = Number(e && e.detail && e.detail.current)
if (!Number.isFinite(nextIndex)) {
return
}
this.$emit('banner-change', nextIndex)
},
switchBanner(index) {
if (!this.isActive) {
return
}
this.$emit('banner-switch', index)
}
}

View File

@@ -28,6 +28,7 @@
>
<swiper-item>
<SelectRoomHero
:is-active="currentScreen === 0"
:language="currentLang"
:banner-list="bannerList"
:current-banner="currentBanner"
@@ -48,6 +49,7 @@
:show-scrollbar="false"
>
<SelectRoomAreaPanel
:is-active="currentScreen === 1"
:language="currentLang"
:banner-list="bannerList"
:current-banner-index="currentBannerIndex"
@@ -114,64 +116,6 @@ export default {
SelectRoomHero,
SelectRoomAreaPanel
},
i18n: {
zh: {
shareTitle: '绿居公寓 - 国际化青年社区',
all: '全部',
allAreas: '全部区域',
allTags: '全部标签',
apartmentDetail: '公寓详情',
areaStatus: '区域状态',
areaStatusDesc: '暂无符合筛选条件的区域',
areaUnavailableTitle: '该区域暂未开放',
bookViewing: '预约看房',
defaultAreaAddressPrefix: '位于',
distancePlaceholder: '附近',
emptyAreaDesc: '当前筛选条件下暂无可用房源',
emptyAreaTitle: "暂无匹配房源",
emptyBannerDesc: "暂无可展示的轮播图",
emptyBannerTitle: "暂无轮播图",
learnMore: "了解更多",
loadingAreaStatus: "区域加载中",
pricePending: "待定",
priceUnit: "元/月起",
searchApartment: "搜索区域、公寓或房型",
sortDefault: "默认排序",
sortNameAsc: "名称排序",
sortPriceAsc: "价格从低到高",
sortPriceDesc: "价格从高到低",
tagFilter: "标签筛选",
viewCommunity: "查看详情"
},
en: {
shareTitle: 'Luju Apartments - International Youth Community',
all: "All",
allAreas: "All Areas",
allTags: "All Tags",
apartmentDetail: "Apartment Detail",
areaStatus: "Area Status",
areaStatusDesc: "No areas matched the current filter.",
areaUnavailableTitle: "Area information unavailable",
bookViewing: "Book Viewing",
defaultAreaAddressPrefix: "Located at ",
distancePlaceholder: "Nearby",
emptyAreaDesc: "No rooms available under the current filters.",
emptyAreaTitle: "No matched rooms",
emptyBannerDesc: "No active banners available.",
emptyBannerTitle: "No banners",
learnMore: "Learn More",
loadingAreaStatus: "Loading areas",
pricePending: "TBD",
priceUnit: "/month",
searchApartment: "Search by area, apartment or room type",
sortDefault: "Default",
sortNameAsc: "Name",
sortPriceAsc: "Price: Low to High",
sortPriceDesc: "Price: High to Low",
tagFilter: "Tags",
viewCommunity: "View Details"
}
},
data() {
return {
isLogin: false,
@@ -213,12 +157,12 @@ export default {
return this.getLocalizedAreaText(matchedArea)
}
if (this.currentLang === 'en') {
return this.currentBanner.areaLabelEnDisplay || this.currentBanner.areaLabelEn || this.currentBanner.areaLabel || this.t('allAreas')
return this.currentBanner.areaLabelEnDisplay || this.currentBanner.areaLabelEn || this.currentBanner.areaLabel || this.$tr('全部区域')
}
return this.currentBanner.areaLabel || this.t('allAreas')
return this.currentBanner.areaLabel || this.$tr('全部区域')
},
currentTagLabel() {
return this.selectedTag || this.t('allTags')
return this.selectedTag || this.$tr('全部标签')
},
availableFilterTags() {
const preferredOrder = ['押一付一', '生活便利', '独立厨卫', '商圈环绕', '民水民电', '拎包入住', '近地铁', '智能家居']
@@ -254,17 +198,14 @@ export default {
watch: {
currentLang() {
this.loadBannerList()
this.loadAreaList()
this.refreshSelectRoomState()
}
},
async mounted() {
this.checkLoginStatus()
await Promise.all([this.loadBannerList(), this.loadAreaList()])
await this.loadBannerList()
},
methods: {
t(key) {
return this.$tm('pages.index.component.SelectRoomTab', key)
},
getLocalizedBannerText(banner = {}, field = '') {
const key = String(field || '').trim()
if (!key) {
@@ -297,6 +238,29 @@ export default {
this.isLogin = session.isLogin
this.userInfo = this.isLogin ? getStoredUser() : null
},
hasActiveReservationFlow() {
const storeUser = (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
if (storeUser.hasActiveReservationFlow === true) {
return true
}
if (storeUser.hasActiveReservationFlow === false) {
return false
}
const session = getSessionSnapshot()
const user = session.currentUser || session.registeredUser || session.resolvedUser || {}
if (user.hasActiveReservationFlow === true) {
return true
}
if (user.hasActiveReservationFlow === false) {
return false
}
return false
},
refreshSelectRoomState() {
if (!this.isLogin || !this.hasActiveReservationFlow()) {
this.loadAreaList()
}
},
loadBannerList() {
this.isBannerLoading = true
this.bannerLoadError = ''
@@ -469,10 +433,18 @@ export default {
})
},
handleBannerChange(index) {
this.currentBannerIndex = index
const nextIndex = Number(index)
if (!Number.isFinite(nextIndex) || nextIndex === this.currentBannerIndex) {
return
}
this.currentBannerIndex = nextIndex
},
switchBanner(index) {
this.currentBannerIndex = index
const nextIndex = Number(index)
if (!Number.isFinite(nextIndex) || nextIndex === this.currentBannerIndex) {
return
}
this.currentBannerIndex = nextIndex
},
collapseToTopBanner() {
this.currentScreen = 1
@@ -512,7 +484,7 @@ export default {
} else {
// 容错处理
uni.showToast({
title: this.t('areaUnavailableTitle'),
title: this.$tr('该区域暂未开放'),
icon: 'none'
})
}
@@ -547,13 +519,13 @@ export default {
,
onShareAppMessage() {
return {
title: this.t('shareTitle'),
title: this.$tr('绿居公寓 - 国际化青年社区'),
path: '/pages/index/index'
}
},
onShareTimeline() {
return {
title: this.t('shareTitle')
title: this.$tr('绿居公寓 - 国际化青年社区')
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
<template>
<view class="auth-result-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>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('注册申请结果') }}</view>
</tn-nav-bar>
<view class="page-content tn-padding" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
<view class="status-card fade-in">
@@ -33,7 +33,7 @@
<!-- 提交信息摘要 -->
<view v-if="profileSummary.length" class="summary-box tn-margin-top-xl tn-margin-bottom-lg tn-margin-left-lg tn-margin-right-lg">
<view class="summary-title tn-margin-bottom-sm">{{ t('submittedInfoTitle') }}</view>
<view class="summary-title tn-margin-bottom-sm">{{ $tr('已提交信息') }}</view>
<view v-for="item in profileSummary" :key="item.label" class="summary-row tn-flex tn-flex-row-between tn-padding-top-sm tn-padding-bottom-sm">
<text class="summary-label">{{ item.label }}</text>
<text class="summary-value tn-text-ellipsis">{{ item.value }}</text>
@@ -44,19 +44,19 @@
<!-- 操作按钮 -->
<view class="action-group tn-margin-top-xl fade-in-up">
<view v-if="authStatus === 'approved'" class="custom-btn btn-primary" @click="goToLogin">
<text class="tn-text-bold">{{ t('goLogin') }}</text>
<text class="tn-text-bold">{{ $tr('去登录') }}</text>
</view>
<view v-else-if="authStatus === 'rejected'" class="custom-btn btn-primary" @click="resubmit">
<text class="tn-text-bold">{{ t('resubmitRegister') }}</text>
<text class="tn-text-bold">{{ $tr('重新提交注册申请') }}</text>
</view>
<view v-else class="custom-btn btn-dark" @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('refreshStatus') }}
{{ loading ? $tr('刷新中...') : $tr('刷新审批状态') }}
</text>
</view>
<view class="custom-btn btn-light tn-margin-top" @click="goToRegistrationRecords">
<text class="tn-text-bold">{{ t('viewRegistrationRecords') }}</text>
<text class="tn-text-bold">{{ $tr('查看注册记录') }}</text>
</view>
</view>
</view>
@@ -80,81 +80,7 @@ export default {
loading: false,
applicationInfo: null,
targetCode: '',
sourceType: '',
i18n: {
zh: {
navTitle: '注册申请结果',
submittedInfoTitle: '已提交信息',
goLogin: '去登录',
resubmitRegister: '重新提交注册申请',
refreshing: '刷新中...',
refreshStatus: '刷新审批状态',
viewRegistrationRecords: '查看注册记录',
unsubmitted: '未提交',
titleUnsubmitted: '您还未提交注册申请',
titlePending: '注册申请已提交',
titleApproved: '注册审批已通过',
titleRejected: '注册审批未通过',
descUnsubmitted: '请先提交注册申请单,审批通过后才算注册成功。',
descPendingRecords: '当前展示的是该申请单的审批状态,可下拉或点击按钮刷新最新结果。',
descPendingDefault: '当前申请正在审批中,审批通过前无法登录。',
descApproved: '您的注册申请已审批通过,现在可以前往登录。',
descRejectedRecords: '当前展示的是该申请单的审批结果,请根据驳回原因修改信息后重新提交注册申请。',
descRejectedDefault: '请根据驳回原因修改信息后重新提交注册申请。',
remarkApproved: '通过备注',
remarkRejected: '驳回原因',
remarkPending: '审批备注',
roleForeign: '外籍人员',
roleDomestic: '中国籍',
labelIdentityType: '人员类型',
labelName: '姓名',
labelAccount: '注册账号',
labelCertificateNo: '证件号',
fallbackNone: '-',
latestDetailMissing: '未查到最新详情,已展示列表记录',
approvedPleaseLogin: '审批已通过,请登录',
fetchStatusFailed: '获取审批状态失败',
statusPending: '审核中',
statusApproved: '已通过',
statusRejected: '已驳回'
},
en: {
navTitle: 'Registration Result',
submittedInfoTitle: 'Submitted Information',
goLogin: 'Sign In',
resubmitRegister: 'Submit Registration Again',
refreshing: 'Refreshing...',
refreshStatus: 'Refresh Status',
viewRegistrationRecords: 'View Registration Records',
unsubmitted: 'Not Submitted',
titleUnsubmitted: 'No registration application submitted yet',
titlePending: 'Registration submitted',
titleApproved: 'Registration approved',
titleRejected: 'Registration was not approved',
descUnsubmitted: 'Please submit a registration application first. Registration is completed only after approval.',
descPendingRecords: 'This page shows the approval status of the selected application. Pull down or tap the button to refresh.',
descPendingDefault: 'Your application is under review and sign-in is unavailable before approval.',
descApproved: 'Your registration is approved and you can sign in now.',
descRejectedRecords: 'This page shows the approval result of the selected application. Please update the information and resubmit based on the rejection reason.',
descRejectedDefault: 'Please update the information and resubmit based on the rejection reason.',
remarkApproved: 'Approval Note',
remarkRejected: 'Rejection Reason',
remarkPending: 'Approval Note',
roleForeign: 'Foreign',
roleDomestic: 'Chinese',
labelIdentityType: 'Identity Type',
labelName: 'Name',
labelAccount: 'Account',
labelCertificateNo: 'Certificate No.',
fallbackNone: '-',
latestDetailMissing: 'Latest details are unavailable. Showing the list record instead.',
approvedPleaseLogin: 'Approved. Please sign in.',
fetchStatusFailed: 'Failed to fetch approval status',
statusPending: 'Under Review',
statusApproved: 'Approved',
statusRejected: 'Rejected'
}
}
sourceType: ''
}
},
computed: {
@@ -173,36 +99,36 @@ export default {
if (typeof status === 'number') {
return this.getApprovalStatusLabelLocal(status)
}
return this.t('unsubmitted')
return this.$tr('未提交')
},
titleText() {
const map = {
unsubmitted: this.t('titleUnsubmitted'),
pending: this.t('titlePending'),
approved: this.t('titleApproved'),
rejected: this.t('titleRejected')
unsubmitted: this.$tr('您还未提交注册申请'),
pending: this.$tr('注册申请已提交'),
approved: this.$tr('注册审批已通过'),
rejected: this.$tr('注册审批未通过')
}
return map[this.authStatus] || map.unsubmitted
},
descText() {
const map = {
unsubmitted: this.t('descUnsubmitted'),
unsubmitted: this.$tr('请先提交注册申请单,审批通过后才算注册成功。'),
pending: this.sourceType === 'registration-records'
? this.t('descPendingRecords')
: this.t('descPendingDefault'),
approved: this.t('descApproved'),
? this.$tr('当前展示的是该申请单的审批状态,可下拉或点击按钮刷新最新结果。')
: this.$tr('当前申请正在审批中,审批通过前无法登录。'),
approved: this.$tr('您的注册申请已审批通过,现在可以前往登录。'),
rejected: this.sourceType === 'registration-records'
? this.t('descRejectedRecords')
: this.t('descRejectedDefault')
? this.$tr('当前展示的是该申请单的审批结果,请根据驳回原因修改信息后重新提交注册申请。')
: this.$tr('请根据驳回原因修改信息后重新提交注册申请。')
}
return map[this.authStatus] || map.unsubmitted
},
approvalRemarkTitle() {
const map = {
approved: this.t('remarkApproved'),
rejected: this.t('remarkRejected'),
pending: this.t('remarkPending'),
unsubmitted: this.t('remarkPending')
approved: this.$tr('通过备注'),
rejected: this.$tr('驳回原因'),
pending: this.$tr('审批备注'),
unsubmitted: this.$tr('审批备注')
}
return map[this.authStatus] || map.pending
},
@@ -223,13 +149,13 @@ export default {
profileSummary() {
const profile = this.applicationInfo || {}
const identityType = profile.passportNumber ? 'foreign' : 'domestic'
const roleText = identityType === 'foreign' ? this.t('roleForeign') : identityType === 'domestic' ? this.t('roleDomestic') : this.t('fallbackNone')
const certificateNo = profile.passportNumber || profile.idCard || this.t('fallbackNone')
const roleText = identityType === 'foreign' ? this.$tr('外籍人员') : identityType === 'domestic' ? this.$tr('中国籍') : this.$tr('-')
const certificateNo = profile.passportNumber || profile.idCard || this.$tr('-')
return [
{ label: this.t('labelIdentityType'), value: roleText },
{ label: this.t('labelName'), value: profile.name || this.t('fallbackNone') },
{ label: this.t('labelAccount'), value: profile.account || this.t('fallbackNone') },
{ label: this.t('labelCertificateNo'), value: certificateNo },
{ label: this.$tr('人员类型'), value: roleText },
{ label: this.$tr('姓名'), value: profile.name || this.$tr('-') },
{ label: this.$tr('注册账号'), value: profile.account || this.$tr('-') },
{ label: this.$tr('证件号'), value: certificateNo },
// { label: '国籍', value: profile.country || profile.nationality || '-' },
// { label: '工作单位', value: profile.workUnit || '-' },
// { label: '兴趣爱好', value: hobbyText }
@@ -267,9 +193,6 @@ export default {
this.refreshStatus()
},
methods: {
t(key) {
return this.$tm('pkgAccount.auth_result.auth_result', key)
},
getWechatCode() {
return new Promise((resolve) => {
// #ifdef MP-WEIXIN
@@ -324,7 +247,7 @@ export default {
if (!target || typeof target !== 'object' || !Object.keys(target).length) {
if (this.applicationInfo && this.applicationCode) {
uni.showToast({
title: this.t('latestDetailMissing'),
title: this.$tr('未查到最新详情,已展示列表记录'),
icon: 'none'
})
return
@@ -334,7 +257,7 @@ export default {
approvalStatus: 1
}
uni.showToast({
title: this.t('approvedPleaseLogin'),
title: this.$tr('审批已通过,请登录'),
icon: 'success'
})
setTimeout(() => {
@@ -354,7 +277,7 @@ export default {
} catch (error) {
console.error('refresh registration status failed', error)
uni.showToast({
title: this.t('fetchStatusFailed'),
title: this.$tr('获取审批状态失败'),
icon: 'none'
})
} finally {
@@ -365,12 +288,12 @@ export default {
return getApprovalStatusLabel(
status,
this.authStatus === 'pending'
? this.t('statusPending')
? this.$tr('审核中')
: this.authStatus === 'approved'
? this.t('statusApproved')
? this.$tr('已通过')
: this.authStatus === 'rejected'
? this.t('statusRejected')
: this.t('unsubmitted'),
? this.$tr('已驳回')
: this.$tr('未提交'),
this.language
)
},

View File

@@ -8,20 +8,20 @@
<!-- 页面头部大标题 -->
<view class="page-header tn-margin-bottom-xl tn-margin-top-xl fade-in">
<view class="tn-text-bold tn-color-black title-text">{{ t('pageTitle') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-sm subtitle-text">{{ t('pageDesc') }}</view>
<view class="tn-text-bold tn-color-black title-text">{{ $tr('欢迎回来') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-sm subtitle-text">{{ $tr('登录绿居公寓,探索您的国际化青年社区') }}</view>
</view>
<!-- 登录表单 -->
<view class="form-area tn-margin-top-xl fade-in-up">
<view class="form-item tn-margin-bottom-lg">
<view class="form-label tn-text-bold tn-margin-bottom-sm">{{ t('accountLabel') }}</view>
<view class="form-label tn-text-bold tn-margin-bottom-sm">{{ $tr('账号 / 邮箱') }}</view>
<view class="input-wrapper" :class="{ 'is-focused': accountFocused }">
<text class="tn-icon-my input-icon"></text>
<input
class="form-input"
:placeholder="t('accountPlaceholder')"
placeholder-class="input-placeholder"
<input
class="form-input"
:placeholder="$tr('请输入账号或邮箱')"
placeholder-class="input-placeholder"
v-model="form.account"
@focus="accountFocused = true"
@blur="accountFocused = false"
@@ -30,13 +30,13 @@
</view>
<view class="form-item tn-margin-bottom-xl">
<view class="form-label tn-text-bold tn-margin-bottom-sm">{{ t('passwordLabel') }}</view>
<view class="form-label tn-text-bold tn-margin-bottom-sm">{{ $tr('密码') }}</view>
<view class="input-wrapper" :class="{ 'is-focused': passwordFocused }">
<text class="tn-icon-lock input-icon"></text>
<input
class="form-input"
type="password"
:placeholder="t('passwordPlaceholder')"
:placeholder="$tr('请输入密码')"
placeholder-class="input-placeholder"
v-model="form.password"
@focus="passwordFocused = true"
@@ -50,22 +50,22 @@
<view class="action-area tn-margin-top-xxl fade-in-up" style="animation-delay: 0.2s;">
<view class="custom-btn btn-primary" @click="handleLogin" :class="{ 'btn-disabled': isSubmitting }">
<text v-if="isSubmitting" class="tn-icon-loading tn-margin-right-xs spin"></text>
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? t('submitting') : t('submitLogin') }}</text>
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? $tr('登录中...') : $tr('立即登录') }}</text>
</view>
<view class="secondary-entry-card tn-margin-top-lg" @click="goToRegistrationRecords">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view>
<view class="secondary-entry-title">{{ t('viewRegistrationRecords') }}</view>
<view class="secondary-entry-desc tn-margin-top-xs">{{ t('viewRegistrationRecordsDesc') }}</view>
<view class="secondary-entry-title">{{ $tr('查看注册记录') }}</view>
<view class="secondary-entry-desc tn-margin-top-xs">{{ $tr('未登录也可查看当前微信账号提交的注册申请记录') }}</view>
</view>
<text class="tn-icon-right secondary-entry-arrow"></text>
</view>
</view>
<view class="tn-flex tn-flex-row-center tn-margin-top-xl">
<view class="tn-color-gray tn-text-sm">{{ t('noAccount') }}</view>
<view class="tn-color-black tn-text-bold tn-text-sm tn-margin-left-xs register-link" @click="goToRegister">{{ t('goRegister') }}</view>
<view class="tn-color-gray tn-text-sm">{{ $tr('还没有账号?') }}</view>
<view class="tn-color-black tn-text-bold tn-text-sm tn-margin-left-xs register-link" @click="goToRegister">{{ $tr('去注册') }}</view>
</view>
</view>
@@ -75,7 +75,7 @@
<script>
import { getRegisteredUser, saveRegisteredUser, startAuthorizedLoginSession } from '../../utils/app-state'
import { mergeBackendUserInfo, normalizeUser, resolveRegistrationApplicationCode } from '../../utils/flow'
import { mergeBackendUserInfo, normalizeUser } from '../../utils/flow'
import {
extractLoginSession,
matchRegisteredUser,
@@ -83,8 +83,8 @@ import {
resolveLoginPreparation
} from '../../utils/login-helper'
import { resolveBookingRedirectAfterLogin } from '../../utils/booking-entry'
import { syncLoginStayState } from '../../utils/session-sync'
import { registrationMyorder, userMe, usersLogin } from '@/api/index.js'
import { parseReservationFlowResponse, getReservationCurrentFlow, getReservationCurrentNode, hasEffectiveReservationFlow } from '../../utils/reservation-flow'
import { registrationMyorder, orderSearchedReservationFlow, userMe, usersLogin } from '@/api/index.js'
export default {
data() {
@@ -96,44 +96,6 @@ export default {
form: {
account: '',
password: ''
},
i18n: {
zh: {
pageTitle: '欢迎回来',
pageDesc: '登录绿居公寓,探索您的国际化青年社区',
accountLabel: '账号 / 邮箱',
accountPlaceholder: '请输入账号或邮箱',
passwordLabel: '密码',
passwordPlaceholder: '请输入密码',
submitting: '登录中...',
submitLogin: '立即登录',
viewRegistrationRecords: '查看注册记录',
viewRegistrationRecordsDesc: '未登录也可查看当前微信账号提交的注册申请记录',
noAccount: '还没有账号?',
goRegister: '去注册',
loading: '加载中...',
validateRequired: '请输入账号和密码',
loginSuccess: '登录成功',
loginFailed: '登录失败,请稍后重试'
},
en: {
pageTitle: 'Welcome Back',
pageDesc: 'Sign in to Luju Apartments and explore your international youth community.',
accountLabel: 'Account / Email',
accountPlaceholder: 'Enter account or email',
passwordLabel: 'Password',
passwordPlaceholder: 'Enter password',
submitting: 'Signing In...',
submitLogin: 'Sign In',
viewRegistrationRecords: 'View Registration Records',
viewRegistrationRecordsDesc: 'You can review registration records submitted by this Wechat account even without signing in.',
noAccount: 'No account yet?',
goRegister: 'Register',
loading: 'Loading...',
validateRequired: 'Please enter both account and password',
loginSuccess: 'Signed in successfully',
loginFailed: 'Sign-in failed, please try again later'
}
}
}
},
@@ -143,9 +105,6 @@ export default {
await this.checkPendingRegistration(bypass)
},
methods: {
t(key) {
return this.$tm('pkgAccount.login.login', key)
},
normalizeRedirectUrl(value = '') {
if (!value) {
return ''
@@ -192,7 +151,7 @@ export default {
})
},
async checkPendingRegistration(bypassRedirect = false) {
uni.showLoading({ title: this.t('loading') })
uni.showLoading({ title: this.$tr('加载中...') })
try {
const providerValue = await this.getWechatCode()
if (!providerValue) {
@@ -229,7 +188,7 @@ export default {
}
uni.showToast({
title: this.t('validateRequired'),
title: this.$tr('请输入账号和密码'),
icon: 'none'
})
return false
@@ -276,14 +235,39 @@ export default {
return extractLoginSession(response, this.$getLanguage())
},
fetchUserInfoAfterLogin() {
return userMe({}, this.$getLanguage())
.then(userInfoRes => {
return userInfoRes && userInfoRes.data ? userInfoRes.data : userInfoRes
fetchReservationFlowAfterLogin(userCode = '') {
const resolvedUserCode = String(userCode || '').trim()
if (!resolvedUserCode) {
return Promise.resolve({
flowList: [],
currentFlow: null,
currentNode: null
})
}
return orderSearchedReservationFlow({
userCode: resolvedUserCode
}, this.$getLanguage())
.then(response => {
const flowList = parseReservationFlowResponse(response)
const currentFlow = getReservationCurrentFlow(flowList) || flowList[0] || null
const currentNode = currentFlow ? getReservationCurrentNode(currentFlow) : null
const hasActiveReservationFlow = flowList.some(item => hasEffectiveReservationFlow(item))
return {
flowList,
currentFlow,
currentNode,
hasActiveReservationFlow
}
})
.catch(error => {
console.error('fetch userinfo failed', error)
return null
console.error('fetch reservation flow after login failed', error)
return {
flowList: [],
currentFlow: null,
currentNode: null,
hasActiveReservationFlow: false
}
})
},
rejectHandledLogin(options = {}) {
@@ -315,14 +299,15 @@ export default {
async resolveLoginState() {
const localRegisteredUser = normalizeUser(getRegisteredUser())
const matchedLocalUser = matchRegisteredUser(localRegisteredUser, this.form.account)
const { token, expires } = await this.loginByApi()
const { token, expires, userCode } = await this.loginByApi()
const latestRegistration = await this.fetchLatestRegistration()
const loginPreparation = resolveLoginPreparation({
language: this.$getLanguage(),
matchedLocalUser,
latestRegistration,
account: this.form.account,
password: this.form.password
password: this.form.password,
userCode
})
if (!loginPreparation.ok) {
@@ -342,26 +327,52 @@ export default {
} = loginState
startAuthorizedLoginSession(sessionUser, token, expires)
const backendUserInfo = await this.fetchUserInfoAfterLogin()
const finalUser = mergeBackendUserInfo(sessionUser, backendUserInfo)
// 先获取用户信息(含 userCode),再获取流程
let finalSessionUser = sessionUser
try {
const userInfoRes = await userMe({}, this.$getLanguage())
finalSessionUser = mergeBackendUserInfo(sessionUser, userInfoRes)
startAuthorizedLoginSession(finalSessionUser, token, expires)
} catch (error) {
console.error('fetch user info after login failed', error)
}
const userCode = finalSessionUser.userCode || finalSessionUser.code || ''
const reservationFlow = await this.fetchReservationFlowAfterLogin(userCode)
const currentStep = reservationFlow.currentNode ? reservationFlow.currentNode.step : null
const finalUser = {
...finalSessionUser,
reservationFlowList: reservationFlow.flowList,
currentReservationStep: currentStep,
hasActiveReservationFlow: reservationFlow.hasActiveReservationFlow
}
saveRegisteredUser(finalUser)
startAuthorizedLoginSession(finalUser, token, expires)
let nextRedirectUrl = this.redirectUrl
try {
const stayState = await syncLoginStayState({
language: this.$getLanguage(),
force: true
})
const stayState = {
hasCheckedIn: currentStep === 5,
currentOrder: null,
currentBooking: reservationFlow.hasActiveReservationFlow && reservationFlow.currentFlow
? {
orderNo: reservationFlow.currentFlow.orderNo || '',
refNo: reservationFlow.currentNode ? String(reservationFlow.currentNode.refNo || '').trim() : '',
step: currentStep
}
: null
}
nextRedirectUrl = resolveBookingRedirectAfterLogin(this.redirectUrl, stayState)
} catch (error) {
console.error('sync stay state after login failed', error)
console.error('resolve booking redirect after login failed', error)
}
uni.hideLoading()
uni.showToast({
title: this.t('loginSuccess'),
title: this.$tr('登录成功'),
icon: 'success'
})
setTimeout(() => {
@@ -375,7 +386,7 @@ export default {
}
this.isSubmitting = true
uni.showLoading({ title: this.t('submitting') })
uni.showLoading({ title: this.$tr('登录中...') })
return this.resolveLoginState()
.then(loginState => {
@@ -390,7 +401,7 @@ export default {
uni.hideLoading()
this.isSubmitting = false
uni.showToast({
title: this.t('loginFailed'),
title: this.$tr('登录失败,请稍后重试'),
icon: 'none'
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
<template>
<view class="registration-records-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>
<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' }">
@@ -20,8 +20,8 @@
</scroll-view>
<view v-if="providerUnavailable" class="empty-panel tn-padding-lg">
<view class="empty-title">{{ t('providerUnavailableTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('providerUnavailableDesc') }}</view>
<view class="empty-title">{{ $tr('当前无法获取微信身份') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ $tr('请在微信小程序中打开后,再查看当前微信账号提交的注册记录。') }}</view>
</view>
<view v-else-if="list.length">
@@ -33,7 +33,7 @@
>
<view class="card-head tn-flex tn-flex-row-between tn-flex-col-center">
<view class="card-title-wrap">
<view class="card-title">{{ item.name || item.account || t('registrationFallbackTitle') }}</view>
<view class="card-title">{{ item.name || item.account || $tr('人员注册申请') }}</view>
<view class="card-code tn-margin-top-xs">{{ item.code || '--' }}</view>
</view>
<view :class="['status-badge', 'status-' + resolveStatusType(item.approvalStatus)]">
@@ -43,50 +43,50 @@
<view class="card-body tn-margin-top">
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('accountLabel') }}</text>
<text class="info-label">{{ $tr('注册账号') }}</text>
<text class="info-value">{{ item.account || '--' }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('phoneLabel') }}</text>
<text class="info-label">{{ $tr('手机号') }}</text>
<text class="info-value">{{ item.phone || '--' }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('idNumberLabel') }}</text>
<text class="info-label">{{ $tr('证件号') }}</text>
<text class="info-value">{{ item.idCard || item.passportNumber || '--' }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('submittedAtLabel') }}</text>
<text class="info-label">{{ $tr('提交时间') }}</text>
<text class="info-value">{{ formatDateTime(item.createTime) }}</text>
</view>
<view v-if="item.approveTime" class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('approvedAtLabel') }}</text>
<text class="info-label">{{ $tr('审批时间') }}</text>
<text class="info-value">{{ formatDateTime(item.approveTime) }}</text>
</view>
<view v-if="item.approveReason" class="remark-panel tn-margin-top">
<view class="remark-label">{{ item.approvalStatus === -1 ? t('rejectReasonLabel') : t('approvalRemarkLabel') }}</view>
<view class="remark-label">{{ item.approvalStatus === -1 ? $tr('驳回原因') : $tr('审批意见') }}</view>
<view class="remark-text tn-margin-top-xs">{{ item.approveReason }}</view>
</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>
<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">{{ t('emptyTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('emptyDesc') }}</view>
<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="goToRegister">
<text class="tn-text-bold">{{ t('goRegister') }}</text>
<text class="tn-text-bold">{{ $tr('去提交注册申请') }}</text>
</view>
</view>
<view v-if="loading && !list.length && !providerUnavailable" 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 class="empty-title">{{ $tr('注册记录加载中') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ $tr('正在同步当前微信账号提交的注册申请记录,请稍候。') }}</view>
</view>
</view>
</view>
@@ -152,15 +152,12 @@ export default {
}
},
methods: {
t(key, params) {
return this.$tm('pkgAccount.registration_records', key, params)
},
syncStatusOptions() {
this.statusOptions = [
{ label: this.t('statusAll'), value: '' },
{ label: this.t('statusPending'), value: 0 },
{ label: this.t('statusApproved'), value: 1 },
{ label: this.t('statusRejected'), value: -1 }
{ label: this.$tr('全部'), value: '' },
{ label: this.$tr('审核中'), value: 0 },
{ label: this.$tr('已通过'), value: 1 },
{ label: this.$tr('已驳回'), value: -1 }
]
},
syncNavigationTitle() {
@@ -187,11 +184,11 @@ export default {
},
getStatusText(status) {
const map = {
0: this.t('statusPending'),
1: this.t('statusApproved'),
'-1': this.t('statusRejected')
0: this.$tr('审核中'),
1: this.$tr('已通过'),
'-1': this.$tr('已驳回')
}
return map[status] || this.t('statusProcessing')
return map[status] || this.$tr('处理中')
},
getDisplayStatusText(item = {}) {
if (this.language !== 'en' && item.approvalStatusDesc) {
@@ -301,7 +298,7 @@ export default {
this.pageIndex = Math.max(1, this.pageIndex - 1)
}
uni.showToast({
title: (error && (error.message || error.msg)) || this.t('loadError'),
title: (error && (error.message || error.msg)) || this.$tr('注册记录加载失败'),
icon: 'none'
})
})

View File

@@ -1,7 +1,7 @@
<template>
<view class="book-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="#F8F9FA">
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('title') }}</view>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('预订确认') }}</view>
</tn-nav-bar>
<view class="book-content tn-padding" :style="{paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '180rpx'}">
@@ -13,7 +13,7 @@
</view>
<view class="preview-info tn-flex-1 tn-margin-left tn-flex tn-flex-direction-column tn-flex-row-between">
<view>
<view class="tn-text-sm tn-color-gray">{{ t('bookingFor') }}</view>
<view class="tn-text-sm tn-color-gray">{{ $tr('你正在预约') }}</view>
<view class="tn-text-bold tn-text-lg tn-color-black tn-margin-top-xs clamp-text-2">
{{ displayStyleName }}
</view>
@@ -24,7 +24,7 @@
<view class="tn-flex tn-flex-row-between tn-flex-col-bottom tn-margin-top-sm">
<view class="room-tag tn-text-xs tn-text-bold">{{ displayLocationText }}</view>
<view class="tn-color-black tn-text tn-text-bold">
<text class="tn-text-sm tn-color-gray tn-margin-right-xs">{{ t('deposit') }}</text>$500.00
<text class="tn-text-sm tn-color-gray tn-margin-right-xs">{{ $tr('定金') }}</text>$500.00
</view>
</view>
</view>
@@ -32,15 +32,15 @@
<!-- 填写日期表单 -->
<view class="form-section tn-margin-bottom-xl tn-padding-lg form-card">
<view class="section-title tn-text-bold tn-text-xl tn-margin-bottom-xs">{{ t('checkInDate') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-bottom-lg">{{ t('checkInDesc') }}</view>
<view class="section-title tn-text-bold tn-text-xl tn-margin-bottom-xs">{{ $tr('填写预订时间') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-bottom-lg">{{ $tr('支付后会先生成预定单并进入审核,不会直接占用房间资源。') }}</view>
<view class="form-item tn-margin-bottom-lg">
<view class="form-label tn-text-bold">{{ t('startDate') }}</view>
<view class="form-label tn-text-bold">{{ $tr('入住日期 (Check-in)') }}</view>
<picker mode="date" @change="onStartDateChange">
<view class="form-input tn-flex tn-flex-row-between tn-flex-col-center">
<text :class="form.startDate ? 'tn-color-black tn-text-bold' : 'tn-color-gray'">
{{ form.startDate ? form.startDate : t('selectDate') }}
{{ form.startDate ? form.startDate : $tr('请选择日期') }}
</text>
<text class="tn-icon-calendar tn-color-black tn-text-xl"></text>
</view>
@@ -48,11 +48,11 @@
</view>
<view class="form-item tn-margin-bottom-sm">
<view class="form-label tn-text-bold">{{ t('endDate') }}</view>
<view class="form-label tn-text-bold">{{ $tr('离开日期 (Check-out)') }}</view>
<picker mode="date" @change="onEndDateChange" :start="form.startDate">
<view class="form-input tn-flex tn-flex-row-between tn-flex-col-center">
<text :class="form.endDate ? 'tn-color-black tn-text-bold' : 'tn-color-gray'">
{{ form.endDate ? form.endDate : t('selectDate') }}
{{ form.endDate ? form.endDate : $tr('请选择日期') }}
</text>
<text class="tn-icon-calendar tn-color-black tn-text-xl"></text>
</view>
@@ -61,12 +61,12 @@
<view v-if="stayNights > 0" class="tn-margin-top-lg booking-summary">
<view class="summary-row tn-flex tn-flex-row-between">
<text class="tn-color-gray">{{ t('stayNights') }}</text>
<text class="tn-text-bold tn-color-black">{{ stayNights }} {{ t('nightUnit') }}</text>
<text class="tn-color-gray">{{ $tr('入住晚数') }}</text>
<text class="tn-text-bold tn-color-black">{{ stayNights }} {{ $tr('晚') }}</text>
</view>
<view class="summary-row tn-flex tn-flex-row-between tn-margin-top-sm">
<text class="tn-color-gray">{{ t('bookingStatus') }}</text>
<text class="tn-text-bold tn-color-black">{{ t('pendingPayment') }}</text>
<text class="tn-color-gray">{{ $tr('当前状态') }}</text>
<text class="tn-text-bold tn-color-black">{{ $tr('待支付定金') }}</text>
</view>
</view>
</view>
@@ -75,65 +75,200 @@
<!-- 底部固定按钮区 -->
<view class="fixed-bottom-btn-area tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-flex tn-flex-direction-column">
<view class="tn-text-sm tn-color-gray">{{ t('deposit') }}</view>
<view class="tn-text-sm tn-color-gray">{{ $tr('定金') }}</view>
<view class="tn-text-bold tn-text-xl" style="color: $brand-primary;">$500.00</view>
</view>
<view class="custom-btn btn-dark" style="width: 60%; height: 100rpx;" @click="submitBooking" :class="{ 'btn-disabled': isSubmitting }">
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? (language === 'zh' ? '处理中...' : 'Processing...') : t('submitOrder') }}</text>
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? (language === 'zh' ? '处理中...' : 'Processing...') : $tr('提交并支付') }}</text>
</view>
</view>
<tn-popup v-model="showDepositNotice" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true" @close="closeDepositNotice">
<view class="deposit-notice-popup">
<view class="popup-header">
<view class="popup-title">{{ $tr('租房定金须知') }}</view>
<view v-if="pendingDepositPaymentContext && pendingDepositPaymentContext.paymentNo" class="popup-subtitle">
{{ $tr('支付号') }}:{{ pendingDepositPaymentContext.paymentNo }}
</view>
</view>
<scroll-view scroll-y class="popup-body" @scrolltolower="onNoticeScrollToBottom">
<view class="notice-content">
<view class="notice-item">
<view class="notice-item-zh">1、请在 5 分钟内支付定金或者款项,否则系统将自动取消这个订单,释放锁定的房源。</view>
<view class="notice-item-en">Please pay the deposit or full payment within 5 minutes. Otherwise, the system will automatically cancel this order and release the reserved housing unit.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">2、缴纳线上定金可锁定房源并预约专属看房,平台收取定金后该房源临时下架,不再接待其他客户看房。</view>
<view class="notice-item-en">Payment of the online deposit reserves the housing unit and arranges exclusive on-site viewing for you. After receiving the deposit, we will temporarily remove the property from the rental list and stop arranging viewings for other clients.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">3、请于约定时间完成实地看房,并在看房结束后 3 个工作日内签署租赁合同;若逾期未看房或未完成签约,视作您主动放弃房源,定金不予退还。</view>
<view class="notice-item-en">You shall complete the on-site viewing at the agreed time and sign the rental contract within 3 working days after the viewing. Failure to complete the viewing or sign the contract within the time limit shall be deemed your voluntary waiver of the property, and the deposit will not be refunded.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">4、如因租客个人原因取消看房、看房后不愿承租房屋或拒绝签署租赁合同,定金不予退还。</view>
<view class="notice-item-en">The deposit is non-refundable if you cancel the viewing, refuse to rent the property or decline to sign the contract due to personal reasons.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">5、若因我方房源信息虚假、房屋无法正常交付等我方过错,造成无法看房或签约,定金将全额按原支付路径退还。</view>
<view class="notice-item-en">If the viewing or contract signing cannot be conducted due to our liabilities such as false property information or failure to deliver the housing normally, the full deposit will be refunded via your original payment channel.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">6、待双方正式签署租赁合同后,该定金可直接抵扣首期租金。</view>
<view class="notice-item-en">After both parties formally sign the rental contract, this deposit may be directly deducted from your initial rent.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">7、您支付定金,即代表已完整知晓房源全部信息,并自愿接受本须知全部条款约束。</view>
<view class="notice-item-en">By making the deposit payment, you confirm that you have fully reviewed all property information and voluntarily accept all terms and conditions set forth in this notice.</view>
</view>
</view>
</scroll-view>
<view class="popup-footer">
<view
class="notice-action-btn"
:class="{ 'notice-action-btn--disabled': !hasReadNotice }"
@click="confirmAndPayDeposit"
>
<text class="tn-text-bold">{{ $tr('同意并支付') }}</text>
<text v-if="!hasReadNotice" class="tn-margin-left-xs tn-text-sm">({{ $tr('请滑动阅读至底部') }})</text>
</view>
</view>
</view>
</tn-popup>
</view>
</template>
<script>
import { resolveExistingBookingNavigationTarget } from '../../utils/booking-entry'
import { getSessionSnapshot } from '../../utils/app-state'
import { syncLoginStayState } from '../../utils/session-sync'
import { accountQueryBill, accountQueryBills, applicationFormDetail, paymentBillPay, paymentPayingBills, submitApplicationForm } from '../../api/index.js'
import { saveActiveApplicationCode, saveHomePageTabId } from '../../utils/app-state'
import { compactEnglishText } from '../../utils/select-room-normalizers'
function isPlainObject(value) {
return !!value && typeof value === 'object' && !Array.isArray(value)
}
function normalizeCodeValue(value) {
if (value === undefined || value === null) {
return ''
}
if (typeof value === 'string' || typeof value === 'number') {
return String(value).trim()
}
return ''
}
function extractSubmitApplicationCode(response) {
const payload = response && response.data !== undefined ? response.data : response
const directValues = [
payload,
payload && payload.data,
payload && payload.result,
payload && payload.record,
payload && payload.application
]
for (let i = 0; i < directValues.length; i += 1) {
const directCode = normalizeCodeValue(directValues[i])
if (directCode) {
return directCode
}
}
const sources = directValues.filter(isPlainObject)
const codeFields = ['detailCode', 'code', 'applicationCode', 'formCode', 'referenceTypeNo', 'no']
for (let i = 0; i < sources.length; i += 1) {
const current = sources[i]
for (let j = 0; j < codeFields.length; j += 1) {
const nextCode = normalizeCodeValue(current[codeFields[j]])
if (nextCode) {
return nextCode
}
}
}
return ''
}
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
}
}
function parseExpireTimeToTimestamp(value) {
if (value === null || value === undefined || value === '') {
return 0
}
if (typeof value === 'number') {
return Number.isFinite(value) ? value : 0
}
const text = String(value).trim()
if (!text) {
return 0
}
if (/^\d{13,}$/.test(text)) {
return Number(text)
}
if (/^\d{10}$/.test(text)) {
return Number(text) * 1000
}
const timestamp = new Date(text).getTime()
return Number.isFinite(timestamp) ? timestamp : 0
}
function formatDateTime(date = '', endOfDay = false) {
if (!date) {
return null
}
return `${date}T${endOfDay ? '23:59:59' : '00:00:00'}`
}
function parseResponseObject(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 {}
}
function parseResponseList(response) {
const payload = response && response.data ? response.data : response
if (Array.isArray(payload)) {
return payload.filter(item => item && typeof item === 'object')
}
if (payload && Array.isArray(payload.data)) {
return payload.data.filter(item => item && typeof item === 'object')
}
return []
}
export default {
data() {
return {
isSubmitting: false,
applyData: null,
showDepositNotice: false,
hasReadNotice: false,
isClosingDepositNoticeForPayment: false,
depositNoticeReadyAt: 0,
pendingDepositPaymentContext: null,
form: {
startDate: '',
endDate: ''
},
i18n: {
zh: {
title: '预订确认',
bookingFor: '你正在预约',
deposit: '定金',
checkInDate: '填写预订时间',
checkInDesc: '支付后会先生成预定单并进入审核,不会直接占用房间资源。',
startDate: '入住日期 (Check-in)',
endDate: '离开日期 (Check-out)',
selectDate: '请选择日期',
submitOrder: '进入支付页',
stayNights: '入住晚数',
nightUnit: '晚',
bookingStatus: '当前状态',
pendingPayment: '待支付定金',
roomTypeLabel: '房型:'
},
en: {
title: 'Booking Confirm',
bookingFor: 'Booking For',
deposit: 'Deposit',
checkInDate: 'Booking Period',
checkInDesc: 'Payment will create a booking order first, without occupying room inventory.',
startDate: 'Check-in Date',
endDate: 'Check-out Date',
selectDate: 'Select Date',
submitOrder: 'Go To Payment',
stayNights: 'Stay Nights',
nightUnit: 'nights',
bookingStatus: 'Status',
pendingPayment: 'Deposit Pending',
roomTypeLabel: 'Room Type: '
}
}
}
},
@@ -159,7 +294,7 @@ export default {
const areaText = this.language === 'en'
? (this.applyData.roomAreaEnDisplay || compactEnglishText(this.applyData.roomAreaEn || this.applyData.roomArea || '', 28))
: (this.applyData.roomArea || this.applyData.roomAreaEn || '')
return [roomTypeText ? `${this.t('roomTypeLabel')}${roomTypeText}` : '', areaText].filter(Boolean).join(' | ')
return [roomTypeText ? `${this.$tr('房型:')}${roomTypeText}` : '', areaText].filter(Boolean).join(' | ')
},
displayLocationText() {
if (!this.applyData) {
@@ -197,47 +332,339 @@ export default {
console.error('Failed to parse apply data', e);
})
}
this.redirectIfBookingUnavailable()
},
methods: {
t(key) {
return this.$tm('pkgBooking.book.book', key)
},
redirectIfBookingUnavailable() {
const session = getSessionSnapshot()
if (!session.isLogin) {
return Promise.resolve(false)
}
return syncLoginStayState({
language: this.$getLanguage(),
force: true
})
.then((stayState) => {
const targetUrl = resolveExistingBookingNavigationTarget(stayState)
if (!targetUrl) {
return false
}
if (/^\/pages\/index\/index(?:\?|$)/.test(targetUrl)) {
uni.reLaunch({ url: targetUrl })
return true
}
uni.redirectTo({ url: targetUrl })
return true
})
.catch((error) => {
console.error('check booking entry failed', error)
return false
})
},
onStartDateChange(e) {
this.form.startDate = e.detail.value;
},
onEndDateChange(e) {
this.form.endDate = e.detail.value;
},
goStayServiceFlow() {
saveHomePageTabId('stay-service')
uni.reLaunch({
url: '/pages/index/index?pageId=stay-service'
})
},
buildSubmitPayload() {
return {
intendedAreaCode: this.applyData && this.applyData.areaCode ? this.applyData.areaCode : null,
intendedRoomTypeCode: this.applyData && this.applyData.roomTypeCode ? this.applyData.roomTypeCode : null,
roomSlotHeight: 1,
hobbiesAndInterestsTagKeys: null,
lifestyleHabitsTagKeys: null,
remark: null,
start: formatDateTime(this.form.startDate, false),
end: formatDateTime(this.form.endDate, true)
}
},
normalizeApplicationDetail(response) {
return parseResponseObject(response)
},
normalizeBillDetail(response) {
return parseResponseObject(response)
},
normalizeBillList(response) {
return parseResponseList(response)
},
normalizePayingBills(response) {
return parseResponseList(response)
},
getBillNo(bill = {}) {
return String(bill.no || bill.billNo || '').trim()
},
getBillPayRequests(bill = {}) {
return (Array.isArray(bill.payRequests) ? bill.payRequests : [])
.filter(item => item && typeof item === 'object')
.sort((prev, next) => new Date(next.createTime || 0).getTime() - new Date(prev.createTime || 0).getTime())
},
buildPendingPaymentOrderFromBillDetail(bill = {}) {
const payRequest = this.getBillPayRequests(bill).find(item => !item.paidTime)
if (!payRequest) {
return null
}
return {
amount: Number(payRequest.amount || bill.remainingAmount || bill.totalCharge || 0),
expireTime: payRequest.expireTime || '',
payRequestNo: payRequest.no || '',
paymentNo: payRequest.no || '',
cashier: {
paymentInfo: payRequest.paymentInfo || '',
expireTime: payRequest.expireTime || ''
}
}
},
getPendingPaymentOrderNo(order = {}) {
const cashier = order && order.cashier ? order.cashier : {}
return String(
order.payRequestNo
|| order.payNo
|| order.paymentNo
|| cashier.payRequestNo
|| cashier.payNo
|| cashier.paymentNo
|| ''
).trim()
},
parsePaymentInfo(paymentInfo) {
if (!paymentInfo) {
return {}
}
if (typeof paymentInfo === 'object') {
return paymentInfo
}
const text = String(paymentInfo).trim()
if (!text) {
return {}
}
try {
return JSON.parse(text)
} catch (error) {
return {}
}
},
normalizeWechatPayOptions(paymentInfo) {
const source = this.parsePaymentInfo(paymentInfo)
return {
provider: 'wxpay',
timeStamp: String(source.timeStamp || source.timestamp || source.time_stamp || ''),
nonceStr: source.nonceStr || source.noncestr || source.nonce_str || '',
package: source.package || source.packageValue || source.package_value || '',
signType: source.signType || source.sign_type || 'MD5',
paySign: source.paySign || source.paysign || source.pay_sign || ''
}
},
requestWechatPayment(paymentInfo) {
const payOptions = this.normalizeWechatPayOptions(paymentInfo)
if (!payOptions.timeStamp || !payOptions.nonceStr || !payOptions.package || !payOptions.paySign) {
return Promise.reject(new Error(this.$tr('获取支付参数失败')))
}
return new Promise((resolve, reject) => {
uni.requestPayment({
...payOptions,
success: resolve,
fail: reject
})
})
},
getWechatLoginCode() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
if (!res || !res.code) {
reject(new Error(this.$tr('获取微信登录凭证失败')))
return
}
resolve(res.code)
},
fail: reject
})
})
},
async getPendingPaymentOrder(billNo = '') {
const normalizedBillNo = String(billNo || '').trim()
if (!normalizedBillNo) {
return null
}
const response = await paymentPayingBills({
BillNo: normalizedBillNo
}, this.language)
const orderList = this.normalizePayingBills(response)
return orderList.length ? orderList[0] : null
},
async resolveApplicationDetailPaymentContext(referenceTypeNo = '') {
const normalizedReferenceTypeNo = String(referenceTypeNo || '').trim()
if (!normalizedReferenceTypeNo) {
return null
}
const detail = this.normalizeApplicationDetail(await applicationFormDetail(normalizedReferenceTypeNo, this.language))
if (!detail || !Object.keys(detail).length) {
return null
}
return {
referenceTypeNo: String(detail.detailCode || normalizedReferenceTypeNo).trim(),
billNo: String(detail.billNo || detail.BillNo || '').trim(),
paymentNo: String(detail.paymentNo || detail.payNo || detail.payRequestNo || '').trim(),
detail
}
},
async resolvePaymentPreview(referenceTypeNo = '') {
const context = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
const billNo = context && context.billNo ? context.billNo : ''
const pendingPaymentOrder = billNo ? await this.getPendingPaymentOrder(billNo).catch(() => null) : null
return {
referenceTypeNo: context && context.referenceTypeNo ? context.referenceTypeNo : String(referenceTypeNo || '').trim(),
billNo,
paymentNo: this.getPendingPaymentOrderNo(pendingPaymentOrder) || (context && context.paymentNo ? context.paymentNo : ''),
pendingPaymentOrder
}
},
async payByPaymentContext(context = {}) {
let referenceTypeNo = String(context.referenceTypeNo || '').trim()
let billNo = String(context.billNo || '').trim()
const description = String(context.description || '').trim() || this.$tr('支付定金')
if (!referenceTypeNo && !billNo) {
throw new Error(this.$tr('申请单编号缺失'))
}
if (referenceTypeNo) {
saveActiveApplicationCode(referenceTypeNo)
}
uni.showLoading({ title: this.$tr('正在获取账单...') })
let bill = null
let pendingPaymentOrder = context.pendingPaymentOrder || null
if (referenceTypeNo && !billNo) {
const detailPaymentContext = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
if (detailPaymentContext) {
referenceTypeNo = detailPaymentContext.referenceTypeNo || referenceTypeNo
billNo = detailPaymentContext.billNo || billNo
}
}
if (!pendingPaymentOrder && billNo) {
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
}
if (billNo) {
const billDetail = this.normalizeBillDetail(await accountQueryBill({ billNo }, this.language))
if (billDetail && Object.keys(billDetail).length) {
bill = billDetail
billNo = this.getBillNo(bill) || billNo
referenceTypeNo = referenceTypeNo || String(bill.referenceTypeNo || '').trim()
if (!pendingPaymentOrder) {
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(bill)
}
}
}
if (referenceTypeNo) {
const billRes = await accountQueryBills({
referenceTypeNo,
status: 10,
paymentStatus: 0,
ProviderType: 'WechatMiniProgram'
}, this.language)
const billList = this.normalizeBillList(billRes)
if (Array.isArray(billList) && billList.length) {
bill = billNo
? (billList.find(item => String(this.getBillNo(item) || '').trim() === billNo) || null)
: null
if (!bill) {
bill = billList[0]
}
billNo = this.getBillNo(bill)
referenceTypeNo = referenceTypeNo || String(bill.referenceTypeNo || '').trim()
if (!pendingPaymentOrder && billNo) {
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
}
if (!pendingPaymentOrder) {
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(bill)
}
}
}
if (!billNo) {
throw new Error(this.$tr('暂无待支付定金账单'))
}
uni.showLoading({ title: this.$tr('正在拉起支付...') })
const expireTimestamp = parseExpireTimeToTimestamp(
pendingPaymentOrder && (pendingPaymentOrder.expireTime || (pendingPaymentOrder.cashier && pendingPaymentOrder.cashier.expireTime))
)
const reusablePendingPaymentOrder = pendingPaymentOrder && (!expireTimestamp || expireTimestamp > Date.now())
? pendingPaymentOrder
: null
if (reusablePendingPaymentOrder && reusablePendingPaymentOrder.cashier && reusablePendingPaymentOrder.cashier.paymentInfo) {
await this.requestWechatPayment(reusablePendingPaymentOrder.cashier.paymentInfo)
return
}
if (!referenceTypeNo || !bill) {
throw new Error(this.$tr('获取支付参数失败'))
}
const wechatCode = await this.getWechatLoginCode()
const payRes = await paymentBillPay({
referenceTypeNo,
billNo,
payMethod: 'Wechat',
amount: Number((bill && (bill.remainingAmount || bill.amount)) || 0),
description: (bill && (bill.billName || bill.name)) || description,
ExtraInfo: wechatCode
}, this.language)
const payload = payRes && payRes.data ? payRes.data : payRes
const cashier = payload && payload.cashier ? payload.cashier : {}
await this.requestWechatPayment(cashier.paymentInfo)
},
openDepositNotice(paymentContext = {}) {
this.pendingDepositPaymentContext = {
referenceTypeNo: String(paymentContext.referenceTypeNo || '').trim(),
billNo: String(paymentContext.billNo || '').trim(),
paymentNo: String(paymentContext.paymentNo || '').trim(),
pendingPaymentOrder: paymentContext.pendingPaymentOrder || null
}
this.hasReadNotice = false
this.depositNoticeReadyAt = Date.now() + 600
setTimeout(() => {
this.showDepositNotice = true
}, 80)
},
closeDepositNotice() {
if (this.isClosingDepositNoticeForPayment) {
this.isClosingDepositNoticeForPayment = false
return
}
this.showDepositNotice = false
this.hasReadNotice = false
this.depositNoticeReadyAt = 0
this.pendingDepositPaymentContext = null
},
onNoticeScrollToBottom() {
this.hasReadNotice = true
},
async confirmAndPayDeposit() {
if (!this.hasReadNotice) {
return
}
if (Date.now() < this.depositNoticeReadyAt) {
return
}
const paymentContext = this.pendingDepositPaymentContext || {}
this.isClosingDepositNoticeForPayment = true
this.showDepositNotice = false
try {
await this.payByPaymentContext({
referenceTypeNo: paymentContext.referenceTypeNo,
billNo: paymentContext.billNo,
pendingPaymentOrder: paymentContext.pendingPaymentOrder,
description: this.$tr('支付定金')
})
uni.showToast({ title: this.$tr('支付成功'), icon: 'success' })
} catch (error) {
const errMsg = error && (error.errMsg || error.message || error.msg)
? (error.errMsg || error.message || error.msg)
: this.$tr('支付失败')
if (/cancel/i.test(errMsg)) {
uni.showToast({ title: this.$tr('已取消支付'), icon: 'none' })
} else {
uni.showToast({ title: errMsg, icon: 'none' })
}
} finally {
uni.hideLoading()
this.hasReadNotice = false
this.isClosingDepositNoticeForPayment = false
this.depositNoticeReadyAt = 0
this.pendingDepositPaymentContext = null
setTimeout(() => {
this.goStayServiceFlow()
}, 300)
}
},
submitBooking() {
if (this.isSubmitting) return;
if (!this.form.startDate || !this.form.endDate) {
@@ -250,24 +677,39 @@ export default {
}
this.isSubmitting = true;
const orderData = {
...this.applyData,
startDate: this.form.startDate,
endDate: this.form.endDate,
stayNights: this.stayNights,
sourceType: 'booking-intent'
};
let roomDataStr = encodeURIComponent(JSON.stringify(orderData));
uni.showLoading({ title: this.language === 'zh' ? '正在生成订单...' : 'Creating Order...' });
setTimeout(() => {
uni.hideLoading();
this.isSubmitting = false;
uni.redirectTo({
url: `/pkgBooking/pay/pay?roomData=${roomDataStr}`
uni.showLoading({ title: this.language === 'zh' ? '提交中...' : 'Submitting...' });
const payload = this.buildSubmitPayload()
return submitApplicationForm(payload, this.language)
.then(async (response) => {
const applicationCode = extractSubmitApplicationCode(response)
if (!applicationCode) {
throw new Error(this.$tr('申请单编号缺失'))
}
saveActiveApplicationCode(applicationCode)
const paymentContext = await this.resolvePaymentPreview(applicationCode).catch(() => ({
referenceTypeNo: applicationCode,
billNo: '',
paymentNo: '',
pendingPaymentOrder: null
}))
uni.hideLoading()
this.openDepositNotice({
...paymentContext,
referenceTypeNo: paymentContext.referenceTypeNo || applicationCode
})
})
.catch((error) => {
console.error('submit booking application failed', error)
uni.hideLoading()
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('申请提交失败'),
icon: 'none'
})
})
.finally(() => {
this.isSubmitting = false;
});
}, 1000);
}
}
}
@@ -384,4 +826,92 @@ export default {
opacity: 0.6;
pointer-events: none;
}
.deposit-notice-popup {
background: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
height: 80vh;
}
.popup-header {
padding: 32rpx;
text-align: center;
border-bottom: 1px solid #F1F5F9;
}
.popup-title {
font-size: 36rpx;
font-weight: bold;
color: #1A2233;
}
.popup-subtitle {
margin-top: 12rpx;
font-size: 24rpx;
color: #64748B;
}
.popup-body {
flex: 1;
padding: 32rpx;
overflow-y: auto;
}
.notice-content {
padding-bottom: 40rpx;
}
.notice-item {
margin-bottom: 32rpx;
}
.notice-item-zh {
font-size: 28rpx;
color: #334155;
line-height: 1.6;
margin-bottom: 8rpx;
font-weight: 500;
}
.notice-item-en {
font-size: 24rpx;
color: #64748B;
line-height: 1.5;
}
.popup-footer {
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
border-top: 1px solid #F1F5F9;
background: #FFFFFF;
display: flex;
justify-content: center;
}
.notice-action-btn {
min-width: 420rpx;
max-width: 100%;
height: 92rpx;
padding: 0 40rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
background: linear-gradient(135deg, $brand-primary 0%, #0099CC 100%);
color: #ffffff;
box-shadow: 0 16rpx 34rpx rgba($brand-primary, 0.24);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.notice-action-btn:active {
transform: scale(0.98);
}
.notice-action-btn--disabled {
opacity: 0.6;
background: #94A3B8;
box-shadow: none;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,53 @@
<template>
<view class="room-type-selector">
<view class="selector-header tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
<view class="selector-card tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
<view class="selected-info">
<view class="selected-label">{{ $tr('意向房型') }}</view>
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : t('selectRoomType') }}
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : $tr('请选择意向房型') }}
</view>
<view class="selected-area tn-text-ellipsis" v-if="selectedArea">
<text class="tn-icon-location area-icon"></text>
{{ getAreaDisplayName(selectedArea) }}
</view>
</view>
<text class="tn-icon-right tn-color-gray"></text>
<view class="selector-arrow">
<text class="tn-icon-right"></text>
</view>
</view>
<tn-popup v-model="showPopup" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('popupTitle') }}</text>
<view class="popup-title">{{ $tr('选择意向房型') }}</view>
<view class="popup-subtitle">{{ $tr('选择您心仪的房型与区域') }}</view>
</view>
<scroll-view scroll-y class="area-list-scroll">
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
<tn-loading mode="circle"></tn-loading>
<view class="tn-margin-top-sm tn-color-gray">{{ t('loadingRoomTypes') }}</view>
<tn-loading mode="circle" :size="48"></tn-loading>
<view class="tn-margin-top tn-color-gray">{{ $tr('房型加载中...') }}</view>
</view>
<view v-else-if="!areaList.length" class="empty-state tn-padding-xl tn-text-center">
<text class="tn-color-gray">{{ t('emptyRoomTypes') }}</text>
<view class="empty-icon">
<text class="tn-icon-empty-data"></text>
</view>
<view class="empty-title">{{ $tr('暂无可选房型') }}</view>
<view class="empty-desc">{{ $tr('当前暂无可用房型,请稍后再试') }}</view>
</view>
<view v-else class="area-group" v-for="(area, index) in areaList" :key="area.code">
<view class="area-title tn-flex tn-flex-row-between tn-flex-col-center" @click="toggleArea(index)">
<view class="tn-flex tn-flex-col-center">
<text class="area-dot"></text>
<text>{{ getAreaDisplayName(area) }}</text>
<view class="area-title-left tn-flex tn-flex-col-center">
<view class="area-icon-wrap">
<text class="tn-icon-home"></text>
</view>
<text class="area-title-text">{{ getAreaDisplayName(area) }}</text>
</view>
<view class="area-toggle">
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'"></text>
</view>
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'" class="tn-color-gray"></text>
</view>
<view class="room-type-grid" v-show="!collapsedAreas[index]">
<view
@@ -45,7 +58,15 @@
@click="handleSelect(area, room)"
>
<view class="room-name tn-text-ellipsis">{{ getRoomTypeDisplayName(room) }}</view>
<view class="room-price" v-if="room.price">¥{{ getPriceValue(room.price) }}{{ t('priceUnit') }}</view>
<view class="room-price" v-if="room.price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ getPriceValue(room.price) }}</text>
<text class="price-unit">{{ $tr('/月') }}</text>
</view>
<view v-else class="room-price room-price--empty">{{ $tr('价格面议') }}</view>
<view class="active-mark">
<text class="tn-icon-check"></text>
</view>
</view>
</view>
</view>
@@ -59,25 +80,6 @@
import { areaPagination } from '@/api/index.js'
import { normalizeAreaItem } from '../../../utils/select-room-normalizers'
const I18N = {
zh: {
emptyRoomTypes: '暂无可选房型',
loadFailed: '房型加载失败',
loadingRoomTypes: '房型加载中...',
popupTitle: '选择意向房型',
priceUnit: '/月',
selectRoomType: '请选择房型'
},
en: {
emptyRoomTypes: 'No room types available',
loadFailed: 'Failed to load room types',
loadingRoomTypes: 'Loading room types...',
popupTitle: 'Select Room Type',
priceUnit: '/month',
selectRoomType: 'Select room type'
}
}
export default {
name: 'BookViewingRoomTypeSelector',
props: {
@@ -117,10 +119,6 @@ export default {
this.loadData()
},
methods: {
t(key) {
const lang = I18N[this.language] ? this.language : 'zh'
return I18N[lang][key] || I18N.zh[key] || key
},
openSelector() {
this.showPopup = true
this.loadData()
@@ -149,7 +147,7 @@ export default {
})
.catch((error) => {
console.error('load room types error', error)
uni.showToast({ title: this.t('loadFailed'), icon: 'none' })
uni.showToast({ title: this.$tr('房型加载失败'), icon: 'none' })
})
.finally(() => {
this.isLoading = false
@@ -234,17 +232,30 @@ export default {
</script>
<style scoped lang="scss">
$luxury-gold: #D8B56A;
$text-primary: #1A2233;
$text-secondary: #6A7A99;
$text-tertiary: #A1B0C4;
$surface: #FFFFFF;
$bg-popup: #F8FAFF;
.room-type-selector {
width: 100%;
}
.selector-header {
background: #FFFFFF;
.selector-card {
background: $surface;
border-radius: 20rpx;
padding: 26rpx 32rpx;
border: 1rpx solid rgba($brand-primary, 0.15);
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
min-height: 100rpx;
padding: 26rpx 30rpx;
border: 2rpx solid rgba($brand-primary, 0.12);
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.05);
min-height: 120rpx;
transition: all 0.25s ease;
}
.selector-card:active {
background: #FAFCFF;
border-color: rgba($brand-primary, 0.25);
}
.selected-info {
@@ -252,72 +263,175 @@ export default {
overflow: hidden;
}
.selected-label {
font-size: 22rpx;
color: $text-tertiary;
margin-bottom: 8rpx;
}
.selected-name {
font-size: 28rpx;
color: #1A2233;
font-weight: 600;
font-size: 30rpx;
color: $text-primary;
font-weight: 700;
&.is-empty {
color: #A0B0C0;
color: $text-tertiary;
font-weight: normal;
}
}
.selected-area {
font-size: 24rpx;
color: $text-secondary;
margin-top: 10rpx;
display: flex;
align-items: center;
}
.area-icon {
font-size: 22rpx;
color: #6A7A99;
margin-top: 6rpx;
margin-right: 6rpx;
color: $brand-primary;
}
.selector-arrow {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #F4F8FF;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
flex-shrink: 0;
text {
font-size: 28rpx;
color: $text-secondary;
}
}
.popup-content {
display: flex;
flex-direction: column;
height: 100%;
background: #F8FAFF;
background: $bg-popup;
}
.popup-header {
padding: 40rpx 30rpx 20rpx;
padding: 40rpx 30rpx 28rpx;
text-align: center;
border-bottom: 1px dashed rgba($brand-primary, 0.15);
background: #FFFFFF;
background: $surface;
border-bottom: 1rpx solid rgba($brand-primary, 0.08);
}
.popup-title {
font-size: 32rpx;
font-size: 34rpx;
font-weight: 700;
color: #1A2233;
color: $text-primary;
margin-bottom: 8rpx;
letter-spacing: 1rpx;
}
.popup-subtitle {
font-size: 24rpx;
color: $text-secondary;
}
.area-list-scroll {
flex: 1;
height: 0;
padding: 20rpx 30rpx 60rpx;
padding: 24rpx 30rpx 60rpx;
}
.loading-state,
.empty-state {
padding-top: 120rpx;
}
.empty-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: rgba($brand-primary, 0.08);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24rpx;
text {
font-size: 56rpx;
color: $brand-primary;
}
}
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 24rpx;
color: $text-secondary;
}
.area-group {
margin-bottom: 30rpx;
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
margin-bottom: 28rpx;
background: $surface;
border-radius: 24rpx;
padding: 28rpx;
box-shadow: 0 8rpx 28rpx rgba(26, 34, 51, 0.04);
border: 1rpx solid rgba($brand-primary, 0.06);
}
.area-title {
font-size: 28rpx;
font-weight: 600;
color: #1A2233;
margin-bottom: 20rpx;
margin-bottom: 24rpx;
}
.area-dot {
width: 12rpx;
height: 12rpx;
.area-title-left {
flex: 1;
min-width: 0;
}
.area-icon-wrap {
width: 48rpx;
height: 48rpx;
border-radius: 14rpx;
background: rgba($brand-primary, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
text {
font-size: 26rpx;
color: $brand-primary;
}
}
.area-title-text {
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
}
.area-toggle {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: $brand-primary;
margin-right: 12rpx;
box-shadow: 0 0 8rpx rgba($brand-primary, 0.4);
background: #F4F8FF;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
text {
font-size: 24rpx;
color: $text-secondary;
}
}
.room-type-grid {
@@ -327,17 +441,18 @@ export default {
}
.room-type-item {
padding: 20rpx;
border-radius: 16rpx;
background: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.15);
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
transition: all 0.3s ease;
position: relative;
padding: 24rpx 20rpx;
border-radius: 20rpx;
background: #FAFCFF;
border: 2rpx solid rgba($brand-primary, 0.1);
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
overflow: hidden;
&.is-active {
background: #FFFFFF;
border-color: $brand-primary;
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.14);
.room-name {
color: $brand-primary;
@@ -346,18 +461,66 @@ export default {
.room-price {
color: $brand-primary;
}
.active-mark {
transform: scale(1);
opacity: 1;
}
}
}
.room-name {
font-size: 26rpx;
font-weight: 600;
color: #1A2233;
margin-bottom: 8rpx;
font-size: 28rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: 10rpx;
}
.room-price {
font-size: 24rpx;
color: $text-secondary;
display: flex;
align-items: baseline;
&--empty {
font-size: 22rpx;
color: $text-tertiary;
}
}
.price-symbol {
font-size: 22rpx;
color: #6A7A99;
margin-right: 2rpx;
}
.price-value {
font-size: 28rpx;
font-weight: 700;
margin-right: 4rpx;
}
.price-unit {
font-size: 22rpx;
}
.active-mark {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: $brand-primary;
display: flex;
align-items: center;
justify-content: center;
transform: scale(0);
opacity: 0;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
text {
font-size: 20rpx;
color: #FFFFFF;
}
}
</style>

View File

@@ -1,18 +1,18 @@
<template>
<view class="booking-detail-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="#FFFFFF">
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('预订详情') }}</view>
</tn-nav-bar>
<view class="page-content tn-padding" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
<view v-if="loading" class="empty-card tn-padding-lg">
<view class="booking-title">{{ t('loadingTitle') }}</view>
<view class="demo-desc tn-margin-top-sm">{{ t('loadingDesc') }}</view>
<view class="booking-title">{{ $tr('订单加载中') }}</view>
<view class="demo-desc tn-margin-top-sm">{{ $tr('正在同步该预订的真实详情,请稍候。') }}</view>
</view>
<view v-else-if="booking" class="booking-card tn-padding-lg">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view>
<view class="booking-title">{{ t('orderNoLabel') }}</view>
<view class="booking-title">{{ $tr('预定单号') }}</view>
<view class="booking-order tn-margin-top-xs">{{ booking.orderNo }}</view>
</view>
<view class="status-badge" :class="'status-' + booking.tagType">{{ booking.statusText }}</view>
@@ -20,48 +20,48 @@
<view class="info-box tn-margin-top">
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('roomTypeLabel') }}</text>
<text class="info-label">{{ $tr('风格房型') }}</text>
<text class="info-value">{{ booking.roomTypeText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('buildingFloorLabel') }}</text>
<text class="info-label">{{ $tr('楼栋楼层') }}</text>
<text class="info-value">{{ booking.buildingFloorText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('roomNoLabel') }}</text>
<text class="info-label">{{ $tr('房间号') }}</text>
<text class="info-value">{{ booking.roomNoText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('areaLabel') }}</text>
<text class="info-label">{{ $tr('所属区域') }}</text>
<text class="info-value">{{ booking.areaText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('stayDateLabel') }}</text>
<text class="info-value">{{ booking.startDateText }}{{ t('dateRangeJoiner') }}{{ booking.endDateText }}</text>
<text class="info-label">{{ $tr('入住时间') }}</text>
<text class="info-value">{{ booking.startDateText }}{{ $tr(' 至 ') }}{{ booking.endDateText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('submittedAtLabel') }}</text>
<text class="info-label">{{ $tr('提交时间') }}</text>
<text class="info-value">{{ booking.createdAtText }}</text>
</view>
</view>
<view v-if="booking.rejectReasonText" class="reason-box tn-margin-top">
<view class="reason-title">{{ t('rejectReasonTitle') }}</view>
<view class="reason-title">{{ $tr('驳回原因') }}</view>
<view class="reason-content tn-margin-top-xs">{{ booking.rejectReasonText }}</view>
</view>
<view class="flow-box tn-margin-top">
<view class="flow-title">{{ t('flowTitle') }}</view>
<view class="flow-step tn-margin-top-sm" :class="isStepActive('pending_review') ? 'active' : ''">{{ t('flowStep1') }}</view>
<view class="flow-step tn-margin-top-sm" :class="isStepActive('approved_wait_assign') ? 'active' : ''">{{ t('flowStep2') }}</view>
<view class="flow-step tn-margin-top-sm" :class="isStepActive('assigned_wait_room') ? 'active' : ''">{{ t('flowStep3') }}</view>
<view class="flow-title">{{ $tr('当前流程') }}</view>
<view class="flow-step tn-margin-top-sm" :class="isStepActive('pending_review') ? 'active' : ''">{{ $tr('1. 已支付,等待角色审批') }}</view>
<view class="flow-step tn-margin-top-sm" :class="isStepActive('approved_wait_assign') ? 'active' : ''">{{ $tr('2. 审批通过,等待分配管家') }}</view>
<view class="flow-step tn-margin-top-sm" :class="isStepActive('assigned_wait_room') ? 'active' : ''">{{ $tr('3. 已分配管家,等待分配房号') }}</view>
</view>
</view>
<view v-else class="empty-card tn-padding-lg">
<view class="booking-title">{{ t('emptyTitle') }}</view>
<view class="demo-desc tn-margin-top-sm">{{ t('emptyDesc') }}</view>
<view class="booking-title">{{ $tr('暂无预订记录') }}</view>
<view class="demo-desc tn-margin-top-sm">{{ $tr('当前暂无可查看的预订详情,请先前往预订记录确认订单。') }}</view>
</view>
</view>
</view>
@@ -78,52 +78,6 @@ import {
} from '../../utils/booking-helper'
export default {
i18n: {
zh: {
navTitle: '预订详情',
loadingTitle: '订单加载中',
loadingDesc: '正在同步该预订的真实详情,请稍候。',
orderNoLabel: '预定单号',
roomTypeLabel: '风格房型',
buildingFloorLabel: '楼栋楼层',
roomNoLabel: '房间号',
areaLabel: '所属区域',
stayDateLabel: '入住时间',
submittedAtLabel: '提交时间',
rejectReasonTitle: '驳回原因',
flowTitle: '当前流程',
flowStep1: '1. 已支付,等待角色审批',
flowStep2: '2. 审批通过,等待分配管家',
flowStep3: '3. 已分配管家,等待分配房号',
emptyTitle: '暂无预订记录',
emptyDesc: '当前暂无可查看的预订详情,请先前往预订记录确认订单。',
cachedDetailShown: '网络异常,已展示最近同步详情',
loadFailed: '预订详情加载失败',
dateRangeJoiner: ' 至 '
},
en: {
navTitle: 'Booking Detail',
loadingTitle: 'Loading booking',
loadingDesc: 'Syncing the latest booking details. Please wait.',
orderNoLabel: 'Booking No.',
roomTypeLabel: 'Room Type',
buildingFloorLabel: 'Building / Floor',
roomNoLabel: 'Room No.',
areaLabel: 'Area',
stayDateLabel: 'Stay Dates',
submittedAtLabel: 'Submitted At',
rejectReasonTitle: 'Rejection Reason',
flowTitle: 'Current Flow',
flowStep1: '1. Paid and waiting for approval',
flowStep2: '2. Approved and waiting for butler assignment',
flowStep3: '3. Butler assigned and waiting for room allocation',
emptyTitle: 'No booking record',
emptyDesc: 'No booking detail is available right now. Please check your booking records first.',
cachedDetailShown: 'Network issue detected. Showing the latest synced detail.',
loadFailed: 'Failed to load booking detail',
dateRangeJoiner: ' to '
}
},
data() {
return {
orderNo: '',
@@ -148,9 +102,6 @@ export default {
await this.refreshBooking()
},
methods: {
t(key, params) {
return this.$tm('pkgBooking.booking_detail.booking_detail', key, params)
},
getStoreUser() {
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
},
@@ -205,8 +156,8 @@ export default {
this.booking = this.resolveBookingFromList(normalizeBookingList(cachedList, this.language))
uni.showToast({
title: this.booking
? this.t('cachedDetailShown')
: ((error && (error.message || error.msg)) || this.t('loadFailed')),
? this.$tr('网络异常,已展示最近同步详情')
: ((error && (error.message || error.msg)) || this.$tr('预订详情加载失败')),
icon: 'none'
})
})

View File

@@ -1,13 +1,13 @@
<template>
<view class="booking-list-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>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('预订记录') }}</view>
</tn-nav-bar>
<view class="page-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view v-if="loading" class="empty-card tn-padding-lg">
<view class="empty-title">{{ t('loadingTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('loadingDesc') }}</view>
<view class="empty-title">{{ $tr('订单加载中') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ $tr('正在同步您的真实预订记录,请稍候。') }}</view>
</view>
<view v-else-if="bookingList.length">
<view
@@ -18,7 +18,7 @@
>
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view>
<view class="booking-title">{{ item.roomTypeText || t('bookingFallbackTitle') }}</view>
<view class="booking-title">{{ item.roomTypeText || $tr('房源预订') }}</view>
<view class="booking-subtitle tn-margin-top-xs">{{ item.orderNo }}</view>
</view>
<view class="status-badge" :class="'status-' + item.tagType">
@@ -28,15 +28,15 @@
<view class="info-grid tn-margin-top">
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('buildingFloorLabel') }}</text>
<text class="info-label">{{ $tr('楼栋楼层') }}</text>
<text class="info-value">{{ item.buildingFloorText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('stayDateLabel') }}</text>
<text class="info-value">{{ item.startDateText }}{{ t('dateRangeJoiner') }}{{ item.endDateText }}</text>
<text class="info-label">{{ $tr('入住时间') }}</text>
<text class="info-value">{{ item.startDateText }}{{ $tr(' 至 ') }}{{ item.endDateText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('submittedAtLabel') }}</text>
<text class="info-label">{{ $tr('提交时间') }}</text>
<text class="info-value">{{ item.createdAtText }}</text>
</view>
</view>
@@ -44,10 +44,10 @@
</view>
<view v-else class="empty-card tn-padding-lg">
<view class="empty-title">{{ t('emptyTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('emptyDesc') }}</view>
<view class="empty-title">{{ $tr('暂无预订记录') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ $tr('当前暂无预订记录,完成预订后会在这里展示。') }}</view>
<view class="custom-btn btn-primary tn-margin-top-lg" @click="goSelectRoom">
<text class="tn-text-bold">{{ t('goSelectRoom') }}</text>
<text class="tn-text-bold">{{ $tr('去选房') }}</text>
</view>
</view>
</view>
@@ -60,38 +60,6 @@ import { getSessionSnapshot } from '../../utils/app-state'
import { normalizeBookingList, parseOrderListResponse } from '../../utils/booking-helper'
export default {
i18n: {
zh: {
navTitle: '预订记录',
loadingTitle: '订单加载中',
loadingDesc: '正在同步您的真实预订记录,请稍候。',
bookingFallbackTitle: '房源预订',
buildingFloorLabel: '楼栋楼层',
stayDateLabel: '入住时间',
submittedAtLabel: '提交时间',
emptyTitle: '暂无预订记录',
emptyDesc: '当前暂无预订记录,完成预订后会在这里展示。',
goSelectRoom: '去选房',
cachedListShown: '网络异常,已展示最近同步记录',
loadFailed: '预订记录加载失败',
dateRangeJoiner: ' 至 '
},
en: {
navTitle: 'Booking Records',
loadingTitle: 'Loading bookings',
loadingDesc: 'Syncing your latest booking records. Please wait.',
bookingFallbackTitle: 'Room Booking',
buildingFloorLabel: 'Building / Floor',
stayDateLabel: 'Stay Dates',
submittedAtLabel: 'Submitted At',
emptyTitle: 'No booking records',
emptyDesc: 'No booking record is available yet. Completed bookings will appear here.',
goSelectRoom: 'Select Room',
cachedListShown: 'Network issue detected. Showing the latest synced records.',
loadFailed: 'Failed to load booking records',
dateRangeJoiner: ' to '
}
},
data() {
return {
bookingList: [],
@@ -112,9 +80,6 @@ export default {
await this.fetchBookingList()
},
methods: {
t(key, params) {
return this.$tm('pkgBooking.booking_list.booking_list', key, params)
},
getStoreUser() {
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
},
@@ -156,8 +121,8 @@ export default {
this.applyBookingList(normalizeBookingList(cachedList, this.language))
uni.showToast({
title: this.bookingList.length
? this.t('cachedListShown')
: ((error && (error.message || error.msg)) || this.t('loadFailed')),
? this.$tr('网络异常,已展示最近同步记录')
: ((error && (error.message || error.msg)) || this.$tr('预订记录加载失败')),
icon: 'none'
})
})

View File

@@ -1,14 +1,14 @@
<template>
<view class="pay-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="#F8F9FA">
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('title') }}</view>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('支付定金') }}</view>
</tn-nav-bar>
<view class="pay-content tn-padding" :style="{paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '200rpx'}">
<!-- 支付金额区 -->
<view class="amount-area tn-flex tn-flex-direction-column tn-flex-col-center tn-margin-top-xl tn-margin-bottom-xl">
<view class="tn-color-gray tn-text-md">{{ t('paymentAmount') }}</view>
<view class="tn-color-gray tn-text-md">{{ $tr('应付定金') }}</view>
<view class="tn-text-bold tn-color-black tn-margin-top-sm amount-text">
<text class="tn-text-xl tn-margin-right-xs">$</text>500.00
</view>
@@ -21,48 +21,48 @@
<view class="icon-bg-box tn-flex tn-flex-col-center tn-flex-row-center tn-margin-right-sm">
<text class="tn-icon-building bill-head-icon"></text>
</view>
<view class="tn-color-gray tn-text-md">{{ t('billTo') }}</view>
<view class="tn-color-gray tn-text-md">{{ $tr('收款方') }}</view>
</view>
<view class="tn-text-bold tn-text-md tn-color-black">{{ t('company') }}</view>
<view class="tn-text-bold tn-text-md tn-color-black">{{ $tr('绿居物业管理有限公司') }}</view>
</view>
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom">
<view class="tn-color-gray tn-text-md">{{ t('roomType') }}</view>
<view class="tn-color-gray tn-text-md">{{ $tr('房源风格') }}</view>
<view class="tn-text-bold tn-text-md tn-color-black clamp-text-1" style="max-width: 400rpx; text-align: right;">
{{ displayStyleName }}
</view>
</view>
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom" v-if="roomData && (roomData.roomTypeName || roomData.roomTypeNameEn)">
<view class="tn-color-gray tn-text-md">{{ t('roomTypeNameLabel') }}</view>
<view class="tn-color-gray tn-text-md">{{ $tr('房型') }}</view>
<view class="tn-text-bold tn-text-md tn-color-black clamp-text-1" style="max-width: 400rpx; text-align: right;">
{{ displayRoomTypeName }}
</view>
</view>
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom">
<view class="tn-color-gray tn-text-md">{{ t('location') }}</view>
<view class="tn-color-gray tn-text-md">{{ $tr('楼栋位置') }}</view>
<view class="tn-text-bold tn-text-md tn-color-black clamp-text-1" style="max-width: 400rpx; text-align: right;">{{ displayLocationText }}</view>
</view>
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom" v-if="roomData && roomData.startDate">
<view class="tn-color-gray tn-text-md">{{ t('bookingPeriod') }}</view>
<view class="tn-color-gray tn-text-md">{{ $tr('预订时间') }}</view>
<view class="tn-text-bold tn-text-md tn-color-black clamp-text-1" style="max-width: 400rpx; text-align: right;">
{{ roomData.startDate }} {{ t('toLabel') }} {{ roomData.endDate }}
{{ roomData.startDate }} {{ $tr('至') }} {{ roomData.endDate }}
</view>
</view>
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom" v-if="roomData && roomData.stayNights">
<view class="tn-color-gray tn-text-md">{{ t('stayNights') }}</view>
<view class="tn-text-bold tn-text-md tn-color-black">{{ roomData.stayNights }} {{ t('nightUnit') }}</view>
<view class="tn-color-gray tn-text-md">{{ $tr('入住晚数') }}</view>
<view class="tn-text-bold tn-text-md tn-color-black">{{ roomData.stayNights }} {{ $tr('晚') }}</view>
</view>
<view class="bill-divider tn-margin-top-md tn-margin-bottom"></view>
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-md">
<view class="tn-color-black tn-text-bold tn-text-lg">{{ t('total') }}</view>
<view class="tn-color-black tn-text-bold tn-text-lg">{{ $tr('合计') }}</view>
<view class="tn-text-bold" style="color: $brand-primary; font-size: 40rpx;">$500.00</view>
</view>
</view>
<!-- 支付方式选择 -->
<view class="payment-methods">
<view class="tn-text-bold tn-text-xl tn-color-black tn-margin-bottom-lg">{{ t('paymentMethod') }}</view>
<view class="tn-text-bold tn-text-xl tn-color-black tn-margin-bottom-lg">{{ $tr('选择支付方式') }}</view>
<view class="method-item tn-flex tn-flex-row-between tn-flex-col-center tn-padding tn-margin-bottom"
:class="payMethod === 'Wechat' ? 'method-active' : ''" @click="payMethod = 'Wechat'">
@@ -105,10 +105,10 @@
</view>
<view class="tip-panel tn-padding-lg">
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('fallbackTitle') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-sm">{{ t('fallbackDesc') }}</view>
<view class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('支付接口暂未接入') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-sm">{{ $tr('当前版本还未接通真实定金支付。你可以先改走已接好的入住申请提交流程,后续由工作人员跟进。') }}</view>
<view class="tip-action tn-margin-top-lg" @click="fallbackToApplication">
<text class="tn-text-bold">{{ t('fallbackAction') }}</text>
<text class="tn-text-bold">{{ $tr('改为提交入住申请') }}</text>
</view>
</view>
@@ -117,11 +117,11 @@
<!-- 底部固定支付按钮 -->
<view class="fixed-bottom-btn-area tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-flex tn-flex-direction-column">
<view class="tn-text-sm tn-color-gray">{{ t('total') }}</view>
<view class="tn-text-sm tn-color-gray">{{ $tr('合计') }}</view>
<view class="tn-text-bold tn-text-xl tn-color-black">$500.00</view>
</view>
<view class="custom-btn btn-primary" style="width: 60%; height: 100rpx;" @click="handlePay" :class="{ 'btn-disabled': isSubmitting }">
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? (language === 'zh' ? '支付中...' : 'Processing...') : t('payNow') }}</text>
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? (language === 'zh' ? '支付中...' : 'Processing...') : $tr('确认支付并提交预订') }}</text>
</view>
</view>
@@ -136,77 +136,7 @@ export default {
return {
isSubmitting: false,
payMethod: 'Wechat',
roomData: null,
i18n: {
zh: {
title: '支付定金',
paymentAmount: '应付定金',
billTo: '收款方',
company: '绿居物业管理有限公司',
roomType: '房源风格',
roomTypeNameLabel: '房型',
location: '楼栋位置',
bookingPeriod: '预订时间',
toLabel: '至',
stayNights: '入住晚数',
nightUnit: '晚',
total: '合计',
paymentMethod: '选择支付方式',
payNow: '确认支付并提交预订',
fallbackTitle: '支付接口暂未接入',
fallbackDesc: '当前版本还未接通真实定金支付。你可以先改走已接好的入住申请提交流程,后续由工作人员跟进。',
fallbackAction: '改为提交入住申请',
fallbackNavigateTitle: '入住申请',
fallbackNavigateDesc: '支付接口暂未接入,先提交入住申请,由工作人员继续跟进。',
paying: '支付中...',
pendingTitle: '支付接口待接入',
paymentNoticeTitle: '用户须知',
paymentNoticeMethodLabel: '支付方式',
paymentNoticeAmountLabel: '支付金额',
paymentNoticeItemLabel: '支付项目',
paymentNoticeItemValue: '房源定金',
paymentNoticeLine1: '请确认支付方式和金额无误后再继续操作。',
paymentNoticeLine2: '当前页面为预支付引导页,真实支付接口暂未接入。',
paymentNoticeLine3: '确认后将进入后续提交流程,由工作人员继续跟进。',
paymentNoticeConfirm: '继续',
paymentNoticeCancel: '取消',
laterAction: '我知道了'
},
en: {
title: 'Deposit Payment',
paymentAmount: 'Deposit Amount',
billTo: 'Bill To',
company: 'Lüju Property Management',
roomType: 'Style',
roomTypeNameLabel: 'Room Type',
location: 'Location',
bookingPeriod: 'Booking Period',
toLabel: 'to',
stayNights: 'Stay Nights',
nightUnit: 'nights',
total: 'Total',
paymentMethod: 'Payment Method',
payNow: 'Pay and Submit',
fallbackTitle: 'Payment API Pending',
fallbackDesc: 'The real deposit payment flow is not ready yet. You can submit an application first and let staff follow up.',
fallbackAction: 'Submit Application Instead',
fallbackNavigateTitle: 'Stay Application',
fallbackNavigateDesc: 'The payment API is not ready yet. Submit a stay application first and let staff follow up.',
paying: 'Processing...',
pendingTitle: 'Payment API Pending',
paymentNoticeTitle: 'Payment Notice',
paymentNoticeMethodLabel: 'Payment Method',
paymentNoticeAmountLabel: 'Amount',
paymentNoticeItemLabel: 'Charge Item',
paymentNoticeItemValue: 'Deposit',
paymentNoticeLine1: 'Please confirm the payment method and amount before continuing.',
paymentNoticeLine2: 'This page is a pre-payment guide and the real payment API is not connected yet.',
paymentNoticeLine3: 'After confirmation, the process continues to the follow-up submission flow for staff handling.',
paymentNoticeConfirm: 'Continue',
paymentNoticeCancel: 'Cancel',
laterAction: 'Later'
}
}
roomData: null
}
},
computed: {
@@ -256,26 +186,24 @@ export default {
}
},
methods: {
t(key) {
return this.$tm('pkgBooking.pay.pay', key)
},
confirmPaymentNotice() {
const content = [
`${this.t('paymentNoticeItemLabel')}: ${this.t('paymentNoticeItemValue')}`,
`${this.t('paymentNoticeMethodLabel')}: ${this.payMethod}`,
`${this.t('paymentNoticeAmountLabel')}: $500.00`,
`${this.$tr('支付项目')}: ${this.$tr('房源定金')}`,
`${this.$tr('支付方式')}: ${this.payMethod}`,
`${this.$tr('支付金额')}: $500.00`,
'',
`1. ${this.t('paymentNoticeLine1')}`,
`2. ${this.t('paymentNoticeLine2')}`,
`3. ${this.t('paymentNoticeLine3')}`
`1. ${this.$tr('请在 5 分钟内支付定金或者款项,否则系统将自动取消这个订单,释放锁定的房源。')}`,
`2. ${this.$tr('请确认支付方式和金额无误后再继续操作。')}`,
`3. ${this.$tr('当前页面为预支付引导页,真实支付接口暂未接入。')}`,
`4. ${this.$tr('确认后将进入后续提交流程,由工作人员继续跟进。')}`
].join('\n')
return new Promise((resolve) => {
uni.showModal({
title: this.t('paymentNoticeTitle'),
title: this.$tr('用户须知'),
content,
confirmText: this.t('paymentNoticeConfirm'),
cancelText: this.t('paymentNoticeCancel'),
confirmText: this.$tr('继续'),
cancelText: this.$tr('取消'),
success: (res) => resolve(!!(res && res.confirm)),
fail: () => resolve(false)
})
@@ -314,7 +242,7 @@ export default {
const descZh = '支付接口暂未接入,先提交入住申请,由工作人员继续跟进。'
const descEn = 'The payment API is not ready yet. Submit a stay application first and let staff follow up.'
uni.navigateTo({
url: `/pkgService/submit_request/submit_request?title=${encodeURIComponent(this.t('fallbackNavigateTitle'))}&titleZh=${encodeURIComponent(titleZh)}&titleEn=${encodeURIComponent(titleEn)}&desc=${encodeURIComponent(this.t('fallbackNavigateDesc'))}&descZh=${encodeURIComponent(descZh)}&descEn=${encodeURIComponent(descEn)}&payload=${encodeURIComponent(JSON.stringify(payload))}`
url: `/pkgService/submit_request/submit_request?title=${encodeURIComponent(this.$tr('入住申请'))}&titleZh=${encodeURIComponent(titleZh)}&titleEn=${encodeURIComponent(titleEn)}&desc=${encodeURIComponent(this.$tr('支付接口暂未接入,先提交入住申请,由工作人员继续跟进。'))}&descZh=${encodeURIComponent(descZh)}&descEn=${encodeURIComponent(descEn)}&payload=${encodeURIComponent(JSON.stringify(payload))}`
})
},
handlePay() {
@@ -322,16 +250,16 @@ export default {
this.confirmPaymentNotice().then((confirmed) => {
if (!confirmed) return
this.isSubmitting = true
uni.showLoading({ title: this.t('paying') })
uni.showLoading({ title: this.$tr('支付中...') })
setTimeout(() => {
uni.hideLoading()
this.isSubmitting = false
uni.showModal({
title: this.t('pendingTitle'),
content: this.t('fallbackDesc'),
confirmText: this.t('fallbackAction'),
cancelText: this.t('laterAction'),
title: this.$tr('支付接口待接入'),
content: this.$tr('当前版本还未接通真实定金支付。你可以先改走已接好的入住申请提交流程,后续由工作人员跟进。'),
confirmText: this.$tr('改为提交入住申请'),
cancelText: this.$tr('我知道了'),
success: (res) => {
if (res.confirm) {
this.fallbackToApplication()

View File

@@ -7,7 +7,7 @@
<text class='icon tn-icon-left'></text>
</view>
<view class="tn-flex tn-flex-col-center tn-flex-row-center ">
<text class="tn-text-bold tn-text-lg tn-color-black">{{ t('navTitle') }}</text>
<text class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('资料图库') }}</text>
</view>
</tn-nav-bar>
@@ -17,7 +17,7 @@
<view class="justify-content-item align-content-item" style="width: 100vw;">
<view class="tn-flex tn-flex-col-center" style="border: 1rpx solid #D2D2D2;border-radius: 100rpx;padding: 10rpx 20rpx 10rpx 20rpx;width: 90%;">
<text class="tn-icon-search justify-content-item tn-padding-right-xs tn-color-gray tn-text-lg"></text>
<input class="justify-content-item" :placeholder="t('searchPlaceholder')" name="input" placeholder-style="color:#AAAAAA" style="width: 90%;"></input>
<input class="justify-content-item" :placeholder="$tr('请输入搜索关键词')" name="input" placeholder-style="color:#AAAAAA" style="width: 90%;"></input>
</view>
</view>
@@ -25,7 +25,7 @@
<view class="justify-content-item tn-text-center">
<!-- 没输入前-->
<tn-button backgroundColor="#3C97FF80" shape="round" padding="20rpx 20rpx" width="150rpx" @tap="">
<text class="tn-color-white">{{ t('searchButton') }}</text>
<text class="tn-color-white">{{ $tr('搜 索') }}</text>
</tn-button>
<!-- 输入后 -->
<!-- <tn-button backgroundColor="#3C97FF" shape="round" padding="20rpx 20rpx" width="150rpx" @tap="">
@@ -70,22 +70,6 @@
export default {
name: 'List',
mixins: [template_page_mixin],
i18n: {
zh: {
navTitle: '资料图库',
searchPlaceholder: '请输入搜索关键词',
searchButton: '搜 索',
galleryImage: '游戏攻略图',
galleryImageLong: '游戏攻略图游戏攻略图游戏攻略图'
},
en: {
navTitle: 'Gallery',
searchPlaceholder: 'Enter search keywords',
searchButton: 'Search',
galleryImage: 'Guide Image',
galleryImageLong: 'Guide Image Guide Image Guide Image'
}
},
data(){
return {
list: [
@@ -158,13 +142,10 @@
}
},
methods: {
t(key, params) {
return this.$tm('pkgExplore.download.download', key, params)
},
syncList() {
this.list = this.list.map((item, index) => ({
...item,
desc: index === 1 ? this.t('galleryImageLong') : this.t('galleryImage')
desc: index === 1 ? this.$tr('游戏攻略图游戏攻略图游戏攻略图') : this.$tr('游戏攻略图')
}))
},
// 跳转

View File

@@ -7,31 +7,31 @@
<text class='icon tn-icon-left'></text>
</view>
<view class="tn-flex tn-flex-col-center tn-flex-row-center ">
<text class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</text>
<text class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('应用专区') }}</text>
</view>
</tn-nav-bar>
<view class="top-fixed" :style="{paddingTop: vuex_custom_bar_height + 10 + 'px'}">
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-padding-left-sm tn-padding-right-sm">
<view class="tn-flex tn-flex-col-center tn-padding-sm">
<view class="tn-padding-right-xs">{{ t('sortDefault') }}</view>
<view class="tn-padding-right-xs">{{ $tr('默认') }}</view>
<!-- <view class="ri-bookmark-line tn-color-gray--dark"></view> -->
</view>
<view class="tn-flex tn-flex-col-center tn-padding-sm" @tap="openTypeModal">
<view class="tn-padding-right-xs">{{ t('typeLabel') }}</view>
<view class="tn-padding-right-xs">{{ $tr('类型') }}</view>
<view class="tn-icon-down-fill icon-active" v-if="showTypeModal"></view>
<view class="tn-icon-down-fill tn-color-gray--dark" v-else></view>
</view>
<view class="tn-flex tn-flex-col-center tn-padding-sm">
<view class="tn-padding-right-xs">{{ t('salesLabel') }}</view>
<view class="tn-padding-right-xs">{{ $tr('销量') }}</view>
<view class="tn-icon-sort tn-color-gray--dark"></view>
</view>
<view class="tn-flex tn-flex-col-center tn-padding-sm">
<view class="tn-padding-right-xs">{{ t('priceLabel') }}</view>
<view class="tn-padding-right-xs">{{ $tr('价格') }}</view>
<view class="tn-icon-sort tn-color-gray--dark"></view>
</view>
<view class="tn-flex tn-flex-col-center tn-padding-sm" @tap="openSelectModal">
<view class="tn-padding-right-xs">{{ t('comprehensiveLabel') }}</view>
<view class="tn-padding-right-xs">{{ $tr('综合') }}</view>
<view class="tn-icon-filter tn-color-gray--dark tn-text-sm"></view>
</view>
</view>
@@ -58,7 +58,7 @@
<view class="justify-content-item tn-flex tn-flex-col-center tn-padding-left-sm tn-padding-right-sm" style="margin-left: -6rpx;color: #333333;">
<text class="tn-text-sm">¥</text>
<text class="tn-padding-right-sm tn-text-lg tn-text-bold">{{ item.price }}</text>
<text class="tn-color-gray tn-text-sm" style="color: #C1CACF;"> {{ item.number }} {{ t('purchasedSuffix') }}</text>
<text class="tn-color-gray tn-text-sm" style="color: #C1CACF;"> {{ item.number }} {{ $tr('人已购') }}</text>
</view>
<!-- <view class="justify-content-item tn-flex tn-flex-col-center">
<tn-button size="sm" shape="icon" backgroundColor="#3e486400" fontColor="#333333" padding="10rpx 0 0 0">
@@ -95,40 +95,6 @@
components: { TypeModal , SelectModal },
name: 'TemplateFilter',
mixins: [template_page_mixin],
i18n: {
zh: {
navTitle: '应用专区',
sortDefault: '默认',
typeLabel: '类型',
salesLabel: '销量',
priceLabel: '价格',
comprehensiveLabel: '综合',
purchasedSuffix: '人已购',
product1: '蛋仔派对',
product2: '保卫萝卜',
product3: '旅行青蛙',
product4: '辐射避难所',
product5: '王国保卫战',
product6: '崩坏 · 星穹铁道',
product7: '纪念碑谷'
},
en: {
navTitle: 'App Zone',
sortDefault: 'Default',
typeLabel: 'Type',
salesLabel: 'Sales',
priceLabel: 'Price',
comprehensiveLabel: 'All',
purchasedSuffix: 'purchased',
product1: 'Eggy Party',
product2: 'Carrot Defense',
product3: 'Travel Frog',
product4: 'Fallout Shelter',
product5: 'Kingdom Rush',
product6: 'Honkai: Star Rail',
product7: 'Monument Valley'
}
},
data(){
return {
/* 选择弹窗*/
@@ -197,14 +163,11 @@
}
},
methods: {
t(key, params) {
return this.$tm('pkgExplore.filter.filter', key, params)
},
syncProducts() {
const keys = ['product1', 'product2', 'product3', 'product4', 'product5', 'product6', 'product7']
const names = ['蛋仔派对', '保卫萝卜', '旅行青蛙', '辐射避难所', '王国保卫战', '崩坏 · 星穹铁道', '纪念碑谷']
this.product = this.product.map((item, index) => ({
...item,
desc: this.t(keys[index] || 'product1')
desc: this.$tr(names[index] || '蛋仔派对')
}))
},
// 打开选择弹框

View File

@@ -3,9 +3,9 @@
<view class="section-header tn-flex tn-flex-row-between tn-flex-col-center">
<view class="title-wrap tn-flex tn-flex-col-center">
<view class="title-accent"></view>
<text class="section-title">{{ t('sectionTitle') }}</text>
<text class="section-title">{{ $tr('房间配置') }}</text>
</view>
<text class="section-subtitle">{{ t('sectionSubtitle', { count: assetList.length }) }}</text>
<text class="section-subtitle">{{ $tr('{count} 项配置', { count: assetList.length }) }}</text>
</view>
<view class="asset-grid">
@@ -33,7 +33,7 @@
</view>
</view>
<text class="asset-name tn-text-ellipsis">{{ item.assetName }}</text>
<text class="asset-name tn-text-ellipsis">{{ getAssetDisplayName(item) }}</text>
<text v-if="item.brand" class="asset-brand tn-text-ellipsis">{{ item.brand }}</text>
<text v-else class="asset-brand-placeholder"></text>
</view>
@@ -54,8 +54,11 @@ export default {
}
},
methods: {
t(key, params = {}) {
return this.$tm('pkgExplore.room_detail.room_detail', key, params)
getAssetDisplayName(item = {}) {
if (this.language === 'en') {
return item.assetNameEn || item.assetName || ''
}
return item.assetName || item.assetNameEn || ''
}
}
}

View File

@@ -2,7 +2,7 @@
<view class="room-hero">
<view v-if="showVrPanel" class="hero-vr-panel" @click="openVR">
<image v-if="vrPoster" :src="vrPoster" mode="aspectFill" class="hero-image"></image>
<view v-else class="hero-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ t('noImage') }}</view>
<view v-else class="hero-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ $tr('暂无图片') }}</view>
<view class="hero-vr-glow"></view>
<view class="vr-center-entry">
<view class="vr-center-ring vr-center-ring--outer"></view>
@@ -11,8 +11,8 @@
<text class="tn-icon-vr vr-center-core-icon"></text>
</view>
<view class="vr-center-text">
<view class="vr-center-title">{{ t('vrPreview') }}</view>
<view class="vr-center-desc">{{ t('vrPreviewDesc') }}</view>
<view class="vr-center-title">{{ $tr('VR预览') }}</view>
<view class="vr-center-desc">{{ $tr('切换到沉浸式看房模式。') }}</view>
</view>
</view>
</view>
@@ -30,7 +30,7 @@
</swiper-item>
</swiper>
<view v-else class="hero-empty tn-flex tn-flex-col-center tn-flex-row-center">
{{ t('noImage') }}
{{ $tr('暂无图片') }}
</view>
<view class="hero-mask"></view>
@@ -46,7 +46,7 @@
@click="switchMedia('image')"
>
<text class="tn-icon-image media-tab-icon"></text>
<text>{{ t('imageTab') }}</text>
<text>{{ $tr('图片') }}</text>
</view>
<view
class="media-tab"
@@ -54,7 +54,7 @@
@click="switchMedia('vr')"
>
<text class="tn-icon-vr media-tab-icon"></text>
<text>{{ t('vrPreview') }}</text>
<text>{{ $tr('VR预览') }}</text>
</view>
</view>
@@ -65,7 +65,7 @@
<!-- 引导下滑箭头 -->
<view class="scroll-down-hint tn-flex tn-flex-direction-column tn-flex-col-center">
<text class="hint-text">{{ t('swipeUpHint') }}</text>
<text class="hint-text">{{ $tr('上滑查看详情') }}</text>
<text class="tn-icon-up arrow-icon"></text>
</view>
</view>
@@ -113,9 +113,6 @@ export default {
}
},
methods: {
t(key) {
return this.$tm('pkgExplore.room_detail.room_detail', key)
},
onSwiperChange(e) {
this.currentImageIndex = e.detail.current;
this.$emit('swiper-change', e);

View File

@@ -2,7 +2,7 @@
<view class="room-info-wrapper">
<view class="section-panel" v-if="basicInfoRows.length">
<view class="section-header">
<text class="section-title">{{ t('basicInfo') }}</text>
<text class="section-title">{{ $tr('基础信息') }}</text>
</view>
<view class="info-list">
<view v-for="item in basicInfoRows" :key="item.label" class="info-row tn-flex tn-flex-row-between tn-flex-col-center">
@@ -16,7 +16,7 @@
<view class="section-panel" v-if="displayDescriptions.length">
<view class="section-header">
<text class="section-title">{{ isRoomType ? t('roomTypeDesc') : t('roomDesc') }}</text>
<text class="section-title">{{ isRoomType ? $tr('房型介绍') : $tr('房间介绍') }}</text>
</view>
<view class="desc-wrap" :class="{ 'desc-wrap--collapsed': isDescCollapsed }">
<view
@@ -32,7 +32,7 @@
class="expand-btn tn-flex tn-flex-row-center tn-flex-col-center"
@click="toggleDesc"
>
<text>{{ isDescCollapsed ? t('expandAll') : t('collapse') }}</text>
<text>{{ isDescCollapsed ? $tr('展开全部') : $tr('收起') }}</text>
<text :class="isDescCollapsed ? 'tn-icon-down' : 'tn-icon-up'" class="expand-icon"></text>
</view>
</view>
@@ -69,9 +69,6 @@ export default {
}
},
methods: {
t(key) {
return this.$tm('pkgExplore.room_detail.room_detail', key)
},
toggleDesc() {
this.isDescCollapsed = !this.isDescCollapsed;
}

View File

@@ -1,7 +1,7 @@
<template>
<view class="section-panel" v-if="bookingRuleRows.length">
<view class="section-header">
<text class="section-title">{{ isRoomType ? t('bookingRules') : t('bookingInfo') }}</text>
<text class="section-title">{{ isRoomType ? $tr('预订规则') : $tr('预订信息') }}</text>
</view>
<view class="rule-list">
<view v-for="item in bookingRuleRows" :key="item.label" class="rule-item tn-flex tn-flex-row-between tn-flex-col-center">
@@ -31,11 +31,7 @@ export default {
default: 'zh'
}
},
methods: {
t(key) {
return this.$tm('pkgExplore.room_detail.room_detail', key)
}
}
methods: {}
}
</script>

View File

@@ -6,10 +6,10 @@
<template v-if="hasPrice">
<text class="price-symbol">¥</text>
<text class="price-value">{{ detailData.priceValue }}</text>
<text class="price-unit">{{ t('priceUnit') }}</text>
<text class="price-unit">{{ $tr('/月') }}</text>
</template>
<template v-else>
<text class="price-empty">{{ t('pricePending') }}</text>
<text class="price-empty">{{ $tr('待定') }}</text>
</template>
</view>
@@ -40,7 +40,7 @@
<view class="address-info">
<view class="address-text tn-text-ellipsis">{{ shortAddress }}</view>
<view class="address-sub tn-flex tn-flex-col-center">
<text class="address-sub-text">{{ t('addressHint') }}</text>
<text class="address-sub-text">{{ $tr('查看地图位置') }}</text>
<text class="tn-icon-right address-sub-icon"></text>
</view>
</view>
@@ -48,7 +48,7 @@
<view class="address-right">
<view class="nav-btn tn-flex tn-flex-row-center tn-flex-col-center">
<text class="tn-icon-navigation-fill"></text>
<text class="nav-text">{{ t('navigate') }}</text>
<text class="nav-text">{{ $tr('导航') }}</text>
</view>
</view>
</view>
@@ -91,19 +91,16 @@ export default {
shortAddress() {
const addr = this.displayAddress;
if (!addr) {
return this.t('noAddress')
return this.$tr('暂无地址')
}
if (this.language === 'en') {
return addr
}
const short = addr.replace(/^(?:.*?省|.*?自治区)?(?:.*?市|.*?自治州)?/, '');
return short.trim() ? short : (addr || this.t('noAddress'));
return short.trim() ? short : (addr || this.$tr('暂无地址'));
}
},
methods: {
t(key) {
return this.$tm('pkgExplore.room_detail.room_detail', key)
},
handleAddressClick() {
this.$emit('open-map', this.detailData)
}

View File

@@ -9,7 +9,7 @@
:fixed="true"
class="glass-nav-bar"
>
<view class="nav-title" :style="{ opacity: navTitleOpacity, color: navTitleColor }">{{ displayName || t('navDetail') }}</view>
<view class="nav-title" :style="{ opacity: navTitleOpacity, color: navTitleColor }">{{ displayName || $tr('房型详情') }}</view>
</tn-nav-bar>
<swiper
@@ -50,7 +50,7 @@
<view class="compact-hero-panel">
<view v-if="showVrPanel" class="compact-vr-panel" @click="openVR">
<image v-if="vrPoster" :src="vrPoster" mode="aspectFill" class="compact-hero-image"></image>
<view v-else class="compact-hero-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ t('noImage') }}</view>
<view v-else class="compact-hero-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ $tr('暂无图片') }}</view>
<view class="compact-vr-glow"></view>
</view>
<swiper
@@ -65,7 +65,7 @@
<image class="compact-hero-image" :src="image" mode="aspectFill"></image>
</swiper-item>
</swiper>
<view v-else class="compact-hero-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ t('noImage') }}</view>
<view v-else class="compact-hero-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ $tr('暂无图片') }}</view>
<view class="compact-hero-mask"></view>
<view class="compact-hero-topbar tn-flex tn-flex-row-between tn-flex-col-center">
<view v-if="hasVR" class="compact-media-tabs">
@@ -74,17 +74,17 @@
:class="{ 'compact-media-tab--active': currentMediaType === 'image' }"
@click="switchMedia('image')"
>
{{ t('imageTab') }}
{{ $tr('图片') }}
</view>
<view
class="compact-media-tab"
:class="{ 'compact-media-tab--active': currentMediaType === 'vr' }"
@click="switchMedia('vr')"
>
{{ t('vrTab') }}
{{ $tr('VR') }}
</view>
</view>
<view v-if="galleryList.length" class="compact-hero-counter">{{ t('imageCount', { count: galleryList.length }) }}</view>
<view v-if="galleryList.length" class="compact-hero-counter">{{ $tr('{count} 张图片', { count: galleryList.length }) }}</view>
</view>
<view class="compact-hero-action-layer">
<view v-if="showVrPanel" class="compact-hero-center-action" @click="openVR">
@@ -94,8 +94,8 @@
<text class="tn-icon-vr compact-hero-core-icon"></text>
</view>
<view class="compact-hero-action-text">
<view class="compact-hero-action-title">{{ t('vrPreview') }}</view>
<view class="compact-hero-action-desc">{{ t('vrPreviewDesc') }}</view>
<view class="compact-hero-action-title">{{ $tr('VR预览') }}</view>
<view class="compact-hero-action-desc">{{ $tr('切换到沉浸式看房模式。') }}</view>
</view>
</view>
<view v-else class="compact-hero-center-action" @click="previewImage">
@@ -105,8 +105,8 @@
<text class="tn-icon-image compact-hero-core-icon"></text>
</view>
<view class="compact-hero-action-text">
<view class="compact-hero-action-title">{{ t('viewLargeImage') }}</view>
<view class="compact-hero-action-desc">{{ t('viewLargeImageDesc') }}</view>
<view class="compact-hero-action-title">{{ $tr('查看大图') }}</view>
<view class="compact-hero-action-desc">{{ $tr('点击查看当前图片的大图预览。') }}</view>
</view>
</view>
</view>
@@ -147,7 +147,7 @@
<view class="action-left tn-flex tn-flex-col-center">
<button class="action-icon-btn" open-type="share">
<text class="tn-icon-share-circle action-icon-font"></text>
<text class="action-icon-text">{{ t('share') }}</text>
<text class="action-icon-text">{{ $tr('分享') }}</text>
</button>
</view>
<view class="action-right tn-flex tn-flex-col-center">
@@ -159,7 +159,7 @@
class="action-btn action-btn--primary"
@click="handleApplyStay"
>
<text>{{ t('applyStay') }}</text>
<text>{{ $tr('申请入住') }}</text>
</view>
</view>
</view>
@@ -446,6 +446,7 @@ function normalizeRoomAssets(list = []) {
return null
}
const assetName = normalizeText(item.assetName || item.name || item.label)
const assetNameEn = normalizeText(item.assetNameEn || item.nameEn || item.labelEn)
if (!assetName) {
return null
}
@@ -456,6 +457,7 @@ function normalizeRoomAssets(list = []) {
key: `${item.roomTypeCode || 'asset'}-${item.assetName || item.name || index}`,
roomTypeCode: item.roomTypeCode || '',
assetName,
assetNameEn,
brand: normalizeText(item.brand),
icon: iconClass ? '' : addFilePrefix(rawIcon),
iconClass,
@@ -680,7 +682,7 @@ export default {
return this.detailData.priceValue !== '' && this.detailData.priceValue !== null
},
primaryActionText() {
return this.isRoomType ? this.t('applyViewing') : this.t('bookRoom')
return this.isRoomType ? this.$tr('预约看房') : this.$tr('立即预订')
},
summaryBadges() {
const badges = []
@@ -693,30 +695,30 @@ export default {
badges.push(localizePayMethodText(this.detailData.payMethod, this.language))
}
if (this.isRoomType && this.detailData.needApproval === true) {
badges.push(this.t('needApproval'))
badges.push(this.$tr('需审核'))
}
if (this.isRoomType && this.detailData.needApproval === false) {
badges.push(this.t('noApprovalNeeded'))
badges.push(this.$tr('无需审核'))
}
return badges
},
basicInfoRows() {
const rows = this.isRoomType
? [
{ label: this.t('roomTypeNameLabel'), value: this.displayName },
{ label: this.t('englishNameLabel'), value: this.showEnglishName ? (this.detailData.nameEnDisplay || this.detailData.nameEn) : '' },
{ label: this.t('areaLabel'), value: this.language === 'en' ? (this.detailData.areaNameEnDisplay || this.detailData.areaNameEn || this.detailData.areaName) : (this.detailData.areaName || '') },
{ label: this.t('payMethodLabel'), value: localizePayMethodText(this.detailData.pricePlan.matchedStrategies || this.detailData.payMethod, this.language) },
{ label: this.t('priceLabel'), value: this.hasPrice ? `¥${this.detailData.priceValue}` : '' }
{ label: this.$tr('房型名称'), value: this.displayName },
{ label: this.$tr('英文名称'), value: this.showEnglishName ? (this.detailData.nameEnDisplay || this.detailData.nameEn) : '' },
{ label: this.$tr('所属区域'), value: this.language === 'en' ? (this.detailData.areaNameEnDisplay || this.detailData.areaNameEn || this.detailData.areaName) : (this.detailData.areaName || '') },
{ label: this.$tr('支付方式'), value: localizePayMethodText(this.detailData.pricePlan.matchedStrategies || this.detailData.payMethod, this.language) },
{ label: this.$tr('价格'), value: this.hasPrice ? `¥${this.detailData.priceValue}` : '' }
]
: [
{ label: this.t('roomCodeLabel'), value: this.detailData.roomCode },
{ label: this.t('roomTypeLabel'), value: this.language === 'en' ? (this.detailData.roomTypeNameEnDisplay || this.detailData.roomTypeNameEn || this.detailData.roomTypeName) : (this.detailData.roomTypeName || '') },
{ label: this.t('buildingLabel'), value: this.language === 'en' ? (this.detailData.buildingNameEnDisplay || this.detailData.buildingNameEn || this.detailData.buildingName) : (this.detailData.buildingName || '') },
{ label: this.t('unitLabel'), value: this.language === 'en' ? (this.detailData.buildingSectionNameEnDisplay || this.detailData.buildingSectionNameEn || this.detailData.buildingSectionName) : (this.detailData.buildingSectionName || '') },
{ label: this.t('floorLabel'), value: this.language === 'en' ? (this.detailData.floorNameEnDisplay || this.detailData.floorNameEn || this.detailData.floorName) : (this.detailData.floorName || '') },
{ label: this.t('orientationLabel'), value: this.language === 'en' ? (this.detailData.orientationEn || this.detailData.orientation) : (this.detailData.orientation || '') },
{ label: this.t('statusLabel'), value: localizeKnownStatusText(this.detailData.statusText, this.language) }
{ label: this.$tr('房号'), value: this.detailData.roomCode },
{ label: this.$tr('房型'), value: this.language === 'en' ? (this.detailData.roomTypeNameEnDisplay || this.detailData.roomTypeNameEn || this.detailData.roomTypeName) : (this.detailData.roomTypeName || '') },
{ label: this.$tr('楼栋'), value: this.language === 'en' ? (this.detailData.buildingNameEnDisplay || this.detailData.buildingNameEn || this.detailData.buildingName) : (this.detailData.buildingName || '') },
{ label: this.$tr('单元'), value: this.language === 'en' ? (this.detailData.buildingSectionNameEnDisplay || this.detailData.buildingSectionNameEn || this.detailData.buildingSectionName) : (this.detailData.buildingSectionName || '') },
{ label: this.$tr('楼层'), value: this.language === 'en' ? (this.detailData.floorNameEnDisplay || this.detailData.floorNameEn || this.detailData.floorName) : (this.detailData.floorName || '') },
{ label: this.$tr('朝向'), value: this.language === 'en' ? (this.detailData.orientationEn || this.detailData.orientation) : (this.detailData.orientation || '') },
{ label: this.$tr('状态'), value: localizeKnownStatusText(this.detailData.statusText, this.language) }
]
return rows.filter((item) => item.value)
@@ -798,22 +800,19 @@ export default {
},
onShareAppMessage() {
return {
title: this.displayName || this.t('shareTitle'),
title: this.displayName || this.$tr('房型详情'),
path: this.buildSharePath(),
imageUrl: this.detailData.image || ''
}
},
onShareTimeline() {
return {
title: this.displayName || this.t('shareTitle'),
title: this.displayName || this.$tr('房型详情'),
query: this.buildShareQuery(),
imageUrl: this.detailData.image || ''
}
},
methods: {
t(key, params = {}) {
return this.$tm('pkgExplore.room_detail.room_detail', key, params)
},
scheduleAutoSwitch() {
if (this.hasAutoSwitched || this.currentSwiperIndex !== 0) {
return
@@ -870,7 +869,7 @@ export default {
},
previewImage() {
if (!this.galleryList.length) {
uni.showToast({ title: this.t('noPreviewImage'), icon: 'none' })
uni.showToast({ title: this.$tr('暂无预览图片'), icon: 'none' })
return
}
uni.previewImage({
@@ -892,15 +891,15 @@ export default {
this.sourceAreaData && this.sourceAreaData.lng
)
if (latitude === null || longitude === null) {
uni.showToast({ title: this.t('noLocation'), icon: 'none' });
uni.showToast({ title: this.$tr('暂无位置信息'), icon: 'none' });
return;
}
uni.openLocation({
latitude,
longitude,
name: this.language === 'en'
? (detail.buildingNameEnDisplay || detail.buildingNameEn || detail.areaNameEnDisplay || detail.areaNameEn || detail.buildingName || detail.areaName || this.displayName || this.t('apartmentName'))
: (detail.buildingName || detail.areaName || this.displayName || this.t('apartmentName')),
? (detail.buildingNameEnDisplay || detail.buildingNameEn || detail.areaNameEnDisplay || detail.areaNameEn || detail.buildingName || detail.areaName || this.displayName || this.$tr('公寓'))
: (detail.buildingName || detail.areaName || this.displayName || this.$tr('公寓')),
address: this.language === 'en'
? (detail.addressEnDisplay || detail.addressEn || detail.address || (this.sourceAreaData && (this.sourceAreaData.addressEnDisplay || this.sourceAreaData.addressEn || this.sourceAreaData.address)) || '')
: (detail.address || (this.sourceAreaData && this.sourceAreaData.address) || ''),
@@ -908,14 +907,14 @@ export default {
});
},
openVR() {
const titleZh = `${this.displayName || this.t('roomTypeFallback')} ${this.t('vrViewingTitle')}`
const titleZh = `${this.displayName || this.$tr('房型')} ${this.$tr('VR看房')}`
const englishName = this.detailData.roomTypeNameEnDisplay
|| this.detailData.roomTypeNameEn
|| this.detailData.nameEnDisplay
|| this.detailData.nameEn
|| this.displayName
|| this.t('roomTypeFallback')
const titleEn = `${englishName} ${this.t('vrViewingTitle')}`
|| this.$tr('房型')
const titleEn = `${englishName} ${this.$tr('VR看房')}`
uni.navigateTo({
url: `/pkgExplore/vr_preview/vr_preview?title=${encodeURIComponent(this.language === 'en' ? titleEn : titleZh)}&titleZh=${encodeURIComponent(titleZh)}&titleEn=${encodeURIComponent(titleEn)}&poster=${encodeURIComponent(this.vrPoster || '')}`
})
@@ -1045,10 +1044,10 @@ export default {
if (!ensureBookingAccess({
language: this.$getLanguage(),
loginTitle: this.t('loginTitle'),
loginTitle: this.$tr('请先登录'),
loginContent: this.isRoomType
? this.t('roomTypeApplyLoginContent')
: this.t('allRoomsLoginContent'),
? this.$tr('请先登录后再提交该房型的看房申请。')
: this.$tr('请先登录后再预订该房间。'),
loginRedirectUrl: targetUrl
})) {
return
@@ -1068,8 +1067,8 @@ export default {
handleApplyStay() {
const serviceEntryUrl = this.buildServiceEntryUrl({
path: '/pkgService/submit_request/submit_request',
title: `${this.displayName || this.t('roomTypeFallback')} ${this.t('stayApplicationSuffix')}`,
desc: `${this.t('roomTypeCodePrefix')}${this.detailData.roomTypeCode || ''}.${this.hasPrice ? ` ${this.t('pricePrefix')}¥${this.detailData.priceValue}.` : ''}${this.detailData.pricePlan.matchedStrategies || this.detailData.payMethod ? ` ${this.t('payMethodPrefix')}${localizePayMethodText(this.detailData.pricePlan.matchedStrategies || this.detailData.payMethod, this.language)}.` : ''}`,
title: `${this.displayName || this.$tr('房型')} ${this.$tr('入住申请')}`,
desc: `${this.$tr('房型编码:')}${this.detailData.roomTypeCode || ''}.${this.hasPrice ? ` ${this.$tr('价格:')}¥${this.detailData.priceValue}.` : ''}${this.detailData.pricePlan.matchedStrategies || this.detailData.payMethod ? ` ${this.$tr('支付方式:')}${localizePayMethodText(this.detailData.pricePlan.matchedStrategies || this.detailData.payMethod, this.language)}.` : ''}`,
key: 'roomTypeSubmitList',
payload: {
sourceType: 'area-room-type',
@@ -1091,8 +1090,8 @@ export default {
if (!ensureBookingAccess({
language: this.$getLanguage(),
loginTitle: this.t('loginTitle'),
loginContent: this.t('stayLoginContent'),
loginTitle: this.$tr('请先登录'),
loginContent: this.$tr('请先登录后再提交入住申请。'),
loginRedirectUrl: serviceEntryUrl
})) {
return

View File

@@ -28,10 +28,10 @@
</view>
<view class="tn-margin tn-padding-top-sm" style="color: #001E50;font-size: 50rpx;">
<text class="tn-text-bold">62,888</text>
<text class="tn-padding-left-xs tn-text-xs tn-color-gray">{{ t('pointsUnit') }}</text>
<text class="tn-padding-left-xs tn-text-xs tn-color-gray">{{ $tr('积分') }}</text>
</view>
<view class="tn-margin tn-text-sm" style="color: #001E50;">
{{ t('pointsRecord') }} <text class="tn-icon-right tn-padding-left-xs tn-color-gray"></text>
{{ $tr('积分记录') }} <text class="tn-icon-right tn-padding-left-xs tn-color-gray"></text>
</view>
</view>
@@ -84,7 +84,7 @@
<!-- 未签到-->
<view class="tn-flex-1 justify-content-item tn-margin-xs tn-text-center">
<tn-button shape="round" backgroundColor="$brand-primary" fontColor="#FFFFFF" padding="40rpx 0" width="100%" :fontSize="30" @tap="showLandscape">
<text class="">{{ t('checkInNow') }}</text>
<text class="">{{ $tr('立即签到') }}</text>
</tn-button>
</view>
@@ -96,12 +96,12 @@
</view> -->
<view class="tn-strip-bottom-min tn-padding" style="color: #95A3A8;">
{{ t('checkInRewardHint') }}
{{ $tr('坚持每天连续签到可以获得多重奖励哦') }}
</view>
<view class="tn-padding-left tn-pright tn-padding-top tn-padding-bottom tn-text-sm" style="color: #95A3A8;">
{{ t('warmTip') }}<br>
{{ t('warmTipDesc') }}
{{ $tr('温馨提示:') }}<br>
{{ $tr('每日签到可获得10积分奖励,连续完成7天签到可能获得额外奖励,连续签到满7天后将会重置签到记录。') }}
</view>
</view>
@@ -110,10 +110,10 @@
<view class="tn-padding-top-sm">
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin" @click="tn('')">
<view class="justify-content-item tn-text-bold tn-text-lg tn-padding-left-xs">
{{ t('taskSectionTitle') }}
{{ $tr('做任务领金币') }}
</view>
<view class="justify-content-item tn-text-df" style="color: #C1CACF;">
<text class="tn-padding-xs">{{ t('more') }}</text>
<text class="tn-padding-xs">{{ $tr('更多') }}</text>
<text class="tn-icon-right"></text>
</view>
</view>
@@ -130,7 +130,7 @@
</view>
<view class="justify-content-item">
<tn-button shape="round" size="sm" backgroundColor="$brand-primary" fontColor="#FFFFFF" padding="30rpx 0" width="160rpx">
<text class="tn-text-bold tn-text-sm">{{ t('goComplete') }}</text>
<text class="tn-text-bold tn-text-sm">{{ $tr('去完成') }}</text>
<text class="tn-icon-right-fill tn-padding-left-xs tn-text-lg"></text>
</tn-button>
</view>
@@ -156,19 +156,19 @@
<image src="https://cdn.nlark.com/yuque/0/2024/png/280373/1719936460577-assets/web-upload/697ed6f1-6126-4734-bd80-bed28296e16d.png" mode="aspectFit" style="width: 320rpx;height: 330rpx;"></image>
</view>
<view class="tn-text-xl tn-text-bold" style="padding-top: 120rpx;">
{{ t('checkInSuccess') }}
{{ $tr('签到成功') }}
</view>
<view class="tn-padding tn-color-gray--dark">
<text>{{ t('checkedInFor') }}</text>
<text>{{ $tr('已连续签到') }}</text>
<text class="tn-color-orangered" style="padding-left: 6rpx;padding-right: 6rpx">3</text>
<text>{{ t('daysAndEarned') }}</text>
<text>{{ $tr('天,获得') }}</text>
<text class="tn-color-orangered" style="padding-left: 6rpx;padding-right: 6rpx">20</text>
<text>{{ t('pointsUnit') }}</text>
<text>{{ $tr('积分') }}</text>
</view>
<view class=" tn-padding tn-color-gray">
<text>{{ t('continuousCheckIn') }}</text>
<text>{{ $tr('连续签到') }}</text>
<text class="tn-color-orangered" style="padding-left: 6rpx;padding-right: 6rpx;">7</text>
<text>{{ t('daysRewardHint') }}</text>
<text>{{ $tr('天可获得额外奖励哦!') }}</text>
</view>
</view>
@@ -186,50 +186,6 @@
export default {
name: 'Sign',
mixins: [template_page_mixin],
i18n: {
zh: {
pointsUnit: '积分',
pointsRecord: '积分记录',
checkInNow: '立即签到',
checkInRewardHint: '坚持每天连续签到可以获得多重奖励哦',
warmTip: '温馨提示:',
warmTipDesc: '每日签到可获得10积分奖励,连续完成7天签到可能获得额外奖励,连续签到满7天后将会重置签到记录。',
taskSectionTitle: '做任务领金币',
more: '更多',
goComplete: '去完成',
checkInSuccess: '签到成功',
checkedInFor: '已连续签到',
daysAndEarned: '天,获得',
continuousCheckIn: '连续签到',
daysRewardHint: '天可获得额外奖励哦!',
notice1: '恭喜 付*其 连续7天签到,获得 300积分 奖励',
notice2: '恭喜 徐*圆 连续7天签到,获得 300积分 奖励',
notice3: '恭喜 蔡*东 连续7天签到,获得 300积分 奖励',
taskName: '任务名称',
taskDesc: '抓住那只猪吖'
},
en: {
pointsUnit: 'Points',
pointsRecord: 'Points History',
checkInNow: 'Check In Now',
checkInRewardHint: 'Daily continuous check-ins unlock multiple rewards',
warmTip: 'Tips:',
warmTipDesc: 'Daily check-in grants 10 points. Completing 7 consecutive days may unlock extra rewards, and the record resets after day 7.',
taskSectionTitle: 'Complete Tasks for Coins',
more: 'More',
goComplete: 'Go',
checkInSuccess: 'Check-in Successful',
checkedInFor: 'Checked in for',
daysAndEarned: 'days and earned',
continuousCheckIn: 'Continuous check-in',
daysRewardHint: 'days unlocks extra rewards!',
notice1: 'Congrats to Fu*qi for 7 consecutive check-ins and earning 300 points',
notice2: 'Congrats to Xu*yuan for 7 consecutive check-ins and earning 300 points',
notice3: 'Congrats to Cai*dong for 7 consecutive check-ins and earning 300 points',
taskName: 'Task Name',
taskDesc: 'Catch That Pig'
}
},
data(){
return {
show: true,
@@ -238,19 +194,16 @@
},
computed: {
noticeList() {
return [this.t('notice1'), this.t('notice2'), this.t('notice3')]
return [this.$tr('恭喜 付*其 连续7天签到,获得 300积分 奖励'), this.$tr('恭喜 徐*圆 连续7天签到,获得 300积分 奖励'), this.$tr('恭喜 蔡*东 连续7天签到,获得 300积分 奖励')]
},
taskList() {
return Array.from({ length: 6 }).map(() => ({
title: this.t('taskName'),
desc: this.t('taskDesc')
title: this.$tr('任务名称'),
desc: this.$tr('抓住那只猪吖')
}))
}
},
methods: {
t(key, params) {
return this.$tm('pkgExplore.sign.sign', key, params)
},
// 跳转
tn(e) {
uni.navigateTo({

View File

@@ -1,11 +1,14 @@
<template>
<view class="section-panel" v-if="facilityList && facilityList.length">
<view class="section-header">
<text class="section-title">{{ t('sectionTitle') }}</text>
<view class="title-bar"></view>
<text class="section-title">{{ $tr('配套设施') }}</text>
</view>
<view class="facility-grid tn-flex tn-flex-wrap">
<view class="facility-item tn-flex tn-flex-direction-column tn-flex-col-center" v-for="(item, index) in facilityList" :key="index">
<text :class="item.icon" class="facility-icon"></text>
<view class="icon-wrap tn-flex tn-flex-row-center tn-flex-col-center">
<text :class="item.icon" class="facility-icon"></text>
</view>
<text class="facility-name">{{ item.name }}</text>
</view>
</view>
@@ -24,34 +27,44 @@ export default {
default: 'zh'
}
},
methods: {
t(key) {
return this.$tm('pkgExplore.store_detail.store_detail', key)
}
}
methods: {}
}
</script>
<style scoped lang="scss">
$navy-deep: #1E3A5F;
$gold: #C9A96E;
$gold-light: #D4BC8C;
$text-primary: #0F172A;
$text-secondary: #64748B;
.section-panel {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1rpx solid rgba(255, 255, 255, 0.15);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
background: #FFFFFF;
border: 1rpx solid rgba($navy-deep, 0.08);
box-shadow: 0 8rpx 28rpx rgba(30, 58, 95, 0.06);
margin: 24rpx 32rpx 0;
padding: 32rpx;
border-radius: 24rpx;
border-radius: 28rpx;
}
.section-header {
margin-bottom: 24rpx;
margin-bottom: 28rpx;
display: flex;
align-items: center;
}
.title-bar {
width: 6rpx;
height: 32rpx;
background: linear-gradient(180deg, $gold 0%, $gold-light 100%);
border-radius: 999rpx;
margin-right: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
font-weight: 800;
color: $navy-deep;
}
.facility-grid {
@@ -62,14 +75,23 @@ export default {
width: 25%;
}
.icon-wrap {
width: 88rpx;
height: 88rpx;
background: linear-gradient(135deg, rgba($navy-deep, 0.08) 0%, rgba($navy-deep, 0.03) 100%);
border: 1rpx solid rgba($navy-deep, 0.08);
border-radius: 24rpx;
margin-bottom: 16rpx;
}
.facility-icon {
font-size: 52rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 12rpx;
font-size: 44rpx;
color: $navy-deep;
}
.facility-name {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
color: $text-secondary;
font-weight: 500;
}
</style>

View File

@@ -1,7 +1,10 @@
<template>
<view class="section-panel room-section">
<view class="section-header tn-flex tn-flex-row-between tn-flex-col-center">
<text class="section-title">{{ t('roomSectionTitle', { count: roomTypes.length }) }}</text>
<view class="title-wrap tn-flex tn-flex-col-center">
<view class="title-bar"></view>
<text class="section-title">{{ $tr('在租房型({count})', { count: roomTypes.length }) }}</text>
</view>
</view>
<view class="room-list-vertical">
<view
@@ -28,11 +31,11 @@
<view class="room-bottom-v tn-flex tn-flex-row-between tn-flex-col-bottom">
<view class="room-price-v">
<text class="price-symbol">¥</text>
<text class="price-val">{{ formatPriceValue(room.price) || t('pricePending') }}</text>
<text class="price-unit">{{ t('priceUnit') }}</text>
<text class="price-val">{{ formatPriceValue(room.price) || $tr('待定') }}</text>
<text class="price-unit">{{ $tr('/月') }}</text>
</view>
<view class="go-btn-v tn-flex tn-flex-col-center tn-flex-row-center">
<text>{{ t('viewDetails') }}</text>
<text>{{ $tr('查看详情') }}</text>
<text class="tn-icon-right"></text>
</view>
</view>
@@ -65,9 +68,6 @@ export default {
}
},
methods: {
t(key, params = {}) {
return this.$tm('pkgExplore.store_detail.store_detail', key, params)
},
formatPriceValue(priceText) {
if (!priceText) {
return ''
@@ -102,25 +102,43 @@ export default {
</script>
<style scoped lang="scss">
$navy-deep: #1E3A5F;
$navy-light: #2E4A73;
$gold: #C9A96E;
$gold-light: #D4BC8C;
$text-primary: #0F172A;
$text-secondary: #64748B;
$text-tertiary: #94A3B8;
.section-panel {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1rpx solid rgba(255, 255, 255, 0.15);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
background: #FFFFFF;
border: 1rpx solid rgba($navy-deep, 0.08);
box-shadow: 0 8rpx 28rpx rgba(30, 58, 95, 0.06);
margin: 24rpx 32rpx 0;
padding: 32rpx;
border-radius: 24rpx;
border-radius: 28rpx;
}
.section-header {
margin-bottom: 24rpx;
margin-bottom: 28rpx;
}
.title-wrap {
flex: 1;
}
.title-bar {
width: 6rpx;
height: 32rpx;
background: linear-gradient(180deg, $gold 0%, $gold-light 100%);
border-radius: 999rpx;
margin-right: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
font-weight: 800;
color: $navy-deep;
}
.room-list-vertical {
@@ -132,11 +150,17 @@ export default {
.room-card-v {
width: 100%;
display: flex;
border-radius: 16rpx;
border-radius: 24rpx;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
border: 1rpx solid rgba(255, 255, 255, 0.1);
box-shadow: none;
background: #FFFFFF;
border: 1rpx solid rgba($navy-deep, 0.08);
box-shadow: 0 8rpx 24rpx rgba(30, 58, 95, 0.05);
transition: all 0.2s;
}
.room-card-v:active {
transform: translateY(-4rpx);
box-shadow: 0 16rpx 40rpx rgba(30, 58, 95, 0.1);
}
.room-img-v {
@@ -149,38 +173,38 @@ export default {
.img {
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.1);
background: linear-gradient(135deg, #E2E8F0 0%, #CBD5E1 100%);
}
.pay-method-tag {
position: absolute;
top: 12rpx;
left: 12rpx;
background: rgba(0, 0, 0, 0.6);
background: rgba($navy-deep, 0.85);
backdrop-filter: blur(8rpx);
color: #fff;
color: $gold-light;
font-size: 18rpx;
padding: 4rpx 10rpx;
border-radius: 6rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 500;
}
.room-info-v {
flex: 1;
padding: 16rpx 20rpx;
padding: 20rpx 24rpx;
display: flex;
flex-direction: column;
min-width: 0;
}
.room-header-v {
margin-bottom: 8rpx;
margin-bottom: 10rpx;
}
.room-name-v {
font-size: 30rpx;
font-weight: bold;
color: #fff;
font-weight: 800;
color: $navy-deep;
flex: 1;
}
@@ -191,16 +215,18 @@ export default {
.tag-v {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.85);
background: rgba(255, 255, 255, 0.15);
color: $navy-deep;
background: rgba($navy-deep, 0.06);
padding: 4rpx 12rpx;
border-radius: 6rpx;
border-radius: 8rpx;
font-weight: 500;
}
.room-desc-v {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: auto; /* 将底部价格推下去 */
color: $text-tertiary;
margin-bottom: auto;
line-height: 1.5;
}
.room-bottom-v {
@@ -208,41 +234,45 @@ export default {
}
.room-price-v {
color: #ffb8b8;
color: $navy-deep;
display: flex;
align-items: baseline;
}
.price-symbol {
font-size: 20rpx;
font-weight: bold;
font-weight: 800;
margin-right: 2rpx;
color: $gold;
}
.price-val {
font-size: 36rpx;
font-weight: 800;
line-height: 1;
color: $navy-deep;
}
.price-unit {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.6);
color: $text-tertiary;
margin-left: 4rpx;
font-weight: 500;
}
.go-btn-v {
background: rgba(255, 255, 255, 0.15);
color: #fff;
background: linear-gradient(135deg, $navy-light 0%, $navy-deep 100%);
color: #FFFFFF;
font-size: 22rpx;
padding: 8rpx 20rpx;
padding: 10rpx 22rpx;
border-radius: 30rpx;
border: 1rpx solid rgba(255, 255, 255, 0.2);
font-weight: 600;
box-shadow: 0 6rpx 16rpx rgba($navy-deep, 0.2);
}
.go-btn-v .tn-icon-right {
font-size: 20rpx;
margin-left: 4rpx;
color: $gold-light;
}
</style>

View File

@@ -1,15 +1,18 @@
<template>
<view class="store-info-panel">
<!-- 核心信息卡片 -->
<!-- 核心信息卡片:地址 -->
<view class="info-card tn-flex tn-flex-row-between tn-flex-col-center" @click="openMap">
<view class="address-left tn-flex tn-flex-col-center">
<view class="icon-wrap tn-flex tn-flex-row-center tn-flex-col-center">
<text class="tn-icon-location-fill location-icon"></text>
</view>
<text class="address-title tn-text-ellipsis">{{ shortAddress }}</text>
<view class="address-content">
<text class="address-label">{{ $tr('公寓地址') }}</text>
<text class="address-title tn-text-ellipsis">{{ shortAddress }}</text>
</view>
</view>
<view class="address-right tn-flex tn-flex-col-center">
<text class="nav-text">{{ t('navigate') }}</text>
<text class="nav-text">{{ $tr('导航') }}</text>
<text class="tn-icon-right nav-arrow"></text>
</view>
</view>
@@ -17,7 +20,8 @@
<!-- 小区简介 -->
<view class="section-panel" v-if="displayDescription">
<view class="section-header">
<text class="section-title">{{ t('storeIntro') }}</text>
<view class="title-bar"></view>
<text class="section-title">{{ $tr('小区简介') }}</text>
</view>
<view
class="store-desc"
@@ -29,7 +33,7 @@
<text v-else>{{ displayDescription }}</text>
</view>
<view v-if="shouldShowToggle" class="expand-btn tn-flex tn-flex-row-center tn-flex-col-center" @click="toggleDesc">
<text>{{ isDescCollapsed ? t('expandAll') : t('collapse') }}</text>
<text>{{ isDescCollapsed ? $tr('展开全部') : $tr('收起') }}</text>
<text :class="isDescCollapsed ? 'tn-icon-down' : 'tn-icon-up'" class="expand-icon"></text>
</view>
</view>
@@ -53,14 +57,14 @@ function normalizeRichDescription(value = '') {
html,
'display:block;width:100%;max-width:100%;height:auto;box-sizing:border-box;border-radius:16rpx;margin:16rpx 0;vertical-align:top;'
)
html = html.replace(/<p\b([^>]*)>/gi, '<p$1 style="line-height:1.9;color:rgba(255,255,255,0.78);margin:0 0 20rpx;word-break:break-word;">')
html = html.replace(/<div\b([^>]*)>/gi, '<div$1 style="line-height:1.9;color:rgba(255,255,255,0.78);margin:0 0 20rpx;word-break:break-word;">')
html = html.replace(/<ul\b([^>]*)>/gi, '<ul$1 style="padding-left:28rpx;margin:0 0 20rpx;color:rgba(255,255,255,0.78);line-height:1.9;">')
html = html.replace(/<ol\b([^>]*)>/gi, '<ol$1 style="padding-left:32rpx;margin:0 0 20rpx;color:rgba(255,255,255,0.78);line-height:1.9;">')
html = html.replace(/<p\b([^>]*)>/gi, '<p$1 style="line-height:1.9;color:#475569;margin:0 0 20rpx;word-break:break-word;">')
html = html.replace(/<div\b([^>]*)>/gi, '<div$1 style="line-height:1.9;color:#475569;margin:0 0 20rpx;word-break:break-word;">')
html = html.replace(/<ul\b([^>]*)>/gi, '<ul$1 style="padding-left:28rpx;margin:0 0 20rpx;color:#475569;line-height:1.9;">')
html = html.replace(/<ol\b([^>]*)>/gi, '<ol$1 style="padding-left:32rpx;margin:0 0 20rpx;color:#475569;line-height:1.9;">')
html = html.replace(/<li\b([^>]*)>/gi, '<li$1 style="margin-bottom:12rpx;">')
html = html.replace(/<h([1-6])\b([^>]*)>/gi, '<h$1$2 style="color:#FFFFFF;line-height:1.5;margin:0 0 20rpx;font-weight:600;">')
html = html.replace(/<strong\b([^>]*)>/gi, '<strong$1 style="color:#FFFFFF;font-weight:600;">')
html = html.replace(/<a\b([^>]*)>/gi, '<a$1 style="color:#9EC5FF;text-decoration:underline;">')
html = html.replace(/<h([1-6])\b([^>]*)>/gi, '<h$1$2 style="color:#1E3A5F;line-height:1.5;margin:0 0 20rpx;font-weight:600;">')
html = html.replace(/<strong\b([^>]*)>/gi, '<strong$1 style="color:#1E3A5F;font-weight:600;">')
html = html.replace(/<a\b([^>]*)>/gi, '<a$1 style="color:#2E4A73;text-decoration:underline;">')
return html
}
@@ -104,19 +108,16 @@ export default {
? (this.storeData.addressEnDisplay || this.storeData.addressEn || this.storeData.address || '')
: (this.storeData.address || this.storeData.addressEn || '')
if (!addr) {
return this.t('noAddress')
return this.$tr('暂无地址')
}
if (this.language === 'en') {
return addr
}
const short = addr.replace(/^(?:.*?省|.*?自治区)?(?:.*?市|.*?自治州)?/, '');
return short.trim() ? short : (addr || this.t('noAddress'));
return short.trim() ? short : (addr || this.$tr('暂无地址'));
}
},
methods: {
t(key) {
return this.$tm('pkgExplore.store_detail.store_detail', key)
},
toggleDesc() {
this.isDescCollapsed = !this.isDescCollapsed;
},
@@ -128,6 +129,14 @@ export default {
</script>
<style scoped lang="scss">
$navy-deep: #1E3A5F;
$navy-light: #2E4A73;
$gold: #C9A96E;
$gold-light: #D4BC8C;
$text-primary: #0F172A;
$text-secondary: #64748B;
$text-tertiary: #94A3B8;
.store-info-panel {
padding: 0 32rpx;
margin-top: 24rpx;
@@ -136,18 +145,16 @@ export default {
}
.info-card {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1rpx solid rgba(255, 255, 255, 0.15);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
border-radius: 24rpx;
background: #FFFFFF;
border: 1rpx solid rgba($navy-deep, 0.08);
box-shadow: 0 8rpx 28rpx rgba(30, 58, 95, 0.06);
border-radius: 28rpx;
padding: 32rpx;
transition: all 0.2s;
&:active {
background: rgba(255, 255, 255, 0.12);
transform: scale(0.98);
box-shadow: 0 12rpx 32rpx rgba(30, 58, 95, 0.1);
}
}
@@ -158,69 +165,91 @@ export default {
}
.icon-wrap {
width: 56rpx;
height: 56rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 16rpx;
margin-right: 16rpx;
width: 72rpx;
height: 72rpx;
background: linear-gradient(135deg, rgba($navy-deep, 0.08) 0%, rgba($navy-deep, 0.04) 100%);
border-radius: 20rpx;
margin-right: 20rpx;
flex-shrink: 0;
border: 1rpx solid rgba($navy-deep, 0.08);
}
.location-icon {
color: #fff;
font-size: 32rpx;
color: $navy-deep;
font-size: 36rpx;
}
.address-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.address-label {
font-size: 24rpx;
color: $text-tertiary;
margin-bottom: 6rpx;
}
.address-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
flex: 1;
}
.address-right {
flex-shrink: 0;
padding-left: 20rpx;
border-left: 1rpx solid rgba(255, 255, 255, 0.2);
padding-left: 24rpx;
border-left: 1rpx solid rgba($navy-deep, 0.08);
}
.nav-text {
font-size: 26rpx;
color: #fff;
font-weight: 500;
color: $navy-deep;
font-weight: 600;
}
.nav-arrow {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
color: $gold;
margin-left: 6rpx;
}
.section-panel {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1rpx solid rgba(255, 255, 255, 0.15);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
background: #FFFFFF;
border: 1rpx solid rgba($navy-deep, 0.08);
box-shadow: 0 8rpx 28rpx rgba(30, 58, 95, 0.06);
margin-top: 24rpx;
padding: 32rpx;
border-radius: 24rpx;
border-radius: 28rpx;
}
.section-header {
margin-bottom: 20rpx;
margin-bottom: 24rpx;
display: flex;
align-items: center;
}
.title-bar {
width: 6rpx;
height: 32rpx;
background: linear-gradient(180deg, $gold 0%, $gold-light 100%);
border-radius: 999rpx;
margin-right: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
font-weight: 800;
color: $navy-deep;
}
.store-desc {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.7);
line-height: 1.6;
color: $text-secondary;
line-height: 1.7;
word-break: break-word;
}
@@ -236,14 +265,16 @@ export default {
}
.expand-btn {
margin-top: 16rpx;
font-size: 24rpx;
color: #fff;
margin-top: 20rpx;
font-size: 26rpx;
color: $navy-deep;
font-weight: 600;
}
.expand-icon {
margin-left: 8rpx;
font-size: 28rpx;
line-height: 1;
color: $gold;
}
</style>

View File

@@ -1,71 +1,66 @@
<template>
<view class="store-detail-page">
<!-- 底层:全屏轮播背景 -->
<view class="fullscreen-bg">
<swiper
v-if="storeGalleryList.length"
class="bg-swiper"
circular
autoplay
:interval="5000"
:duration="1000"
@change="onHeroSwiperChange"
>
<swiper-item v-for="(image, index) in storeGalleryList" :key="`${image}-${index}`">
<image class="bg-image" :src="image" mode="aspectFill"></image>
</swiper-item>
</swiper>
<view v-else class="bg-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ t('noImage') }}</view>
<!-- 渐变暗化遮罩,确保上层文字卡片清晰 -->
<view class="bg-mask"></view>
</view>
<!-- 顶部导航栏 -->
<!-- 顶部导航栏:滚动前透明/白色元素,滚动后白底深海蓝标题 -->
<tn-nav-bar
:isBack="true"
:bottomShadow="false"
backgroundColor="transparent"
:backgroundColor="navBarBackgroundColor"
:customBackStyle="navBarBackStyle"
backIconColor="#FFFFFF"
:backIconColor="navBarIconColor"
:fixed="true"
class="transparent-nav-bar"
class="glass-nav-bar"
>
<view class="nav-title" :style="{ opacity: navTitleOpacity }">{{ displayStoreName }}</view>
<view class="nav-title" :style="{ opacity: navTitleOpacity, color: navTitleColor }">{{ displayStoreName }}</view>
</tn-nav-bar>
<!-- 上层:透明可滚动内容区 -->
<scroll-view
class="content-scroll"
scroll-y
:show-scrollbar="false"
@scroll="handleScroll"
enable-back-to-top
>
<view class="scroll-inner">
<!-- 留出足够的顶部空间,让背景图展示出来 -->
<view class="hero-space tn-flex tn-flex-direction-column tn-flex-col-bottom">
<!-- 照片计数器移到店名上方,避免与导航栏重叠 -->
<view v-if="storeGalleryList.length > 1" class="hero-counter tn-flex tn-flex-col-center">
<text class="tn-icon-image counter-icon"></text>
<text>{{ currentImageIndex + 1 }} / {{ storeGalleryList.length }}</text>
</view>
<view class="store-title-wrap">
<view class="store-title">{{ displayStoreName }}</view>
<view v-if="storeTags.length" class="store-tags tn-flex tn-flex-wrap">
<view class="tag-item" v-for="(tag, index) in storeTags" :key="index">{{ tag }}</view>
</view>
</view>
<!-- 可滚动内容区 -->
<scroll-view
class="content-scroll"
scroll-y
:show-scrollbar="false"
@scroll="handleScroll"
enable-back-to-top
>
<view class="scroll-inner">
<!-- Hero 大图区 -->
<view class="hero-panel">
<swiper
v-if="storeGalleryList.length"
class="hero-swiper"
circular
autoplay
:interval="5000"
:duration="1000"
@change="onHeroSwiperChange"
>
<swiper-item v-for="(image, index) in storeGalleryList" :key="`${image}-${index}`">
<image class="hero-image" :src="image" mode="aspectFill"></image>
</swiper-item>
</swiper>
<view v-else class="hero-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ $tr('暂无图片') }}</view>
<view v-if="storeGalleryList.length > 1" class="hero-counter">
<text class="tn-icon-image counter-icon"></text>
<text>{{ currentImageIndex + 1 }} / {{ storeGalleryList.length }}</text>
</view>
</view>
<!-- 悬浮毛玻璃信息流 -->
<view class="floating-content-wrap">
<!-- 店名主卡片 -->
<view class="store-title-card">
<view class="store-title">{{ displayStoreName }}</view>
<view class="title-divider"></view>
<view v-if="storeTags.length" class="store-tags tn-flex tn-flex-wrap">
<view class="tag-item" v-for="(tag, index) in storeTags" :key="index">{{ tag }}</view>
</view>
</view>
<!-- 信息流 -->
<view class="content-body">
<store-info
:storeData="storeData"
:language="language"
@open-map="openMap"
/>
<facility-list
:facilityList="facilityList"
:language="language"
@@ -77,20 +72,20 @@
:language="language"
@go-detail="goToRoomDetail"
/>
<view class="bottom-spacer"></view>
</view>
</view>
</scroll-view>
<!-- 悬浮操作栏 (固定在底部,透明风格) -->
<!-- 底部操作栏 -->
<view class="action-bar-wrap tn-safe-area-inset-bottom">
<view class="action-bar">
<button class="action-btn action-btn--glass" open-type="share">
<button class="action-btn action-btn--secondary" open-type="share">
<text class="tn-icon-share action-btn__icon"></text>
<text>{{ t('share') }}</text>
<text>{{ $tr('分享') }}</text>
</button>
<button class="action-btn action-btn--primary-glass" @click="handleCall">
<button class="action-btn action-btn--primary" @click="handleCall">
<text class="tn-icon-phone action-btn__icon"></text>
<text>{{ contactButtonText }}</text>
</button>
@@ -103,8 +98,8 @@
import StoreInfo from './components/StoreInfo.vue';
import FacilityList from './components/FacilityList.vue';
import RoomList from './components/RoomList.vue';
import { facilityManagers } from '@/api/index.js'
import { localizeFacilityName, localizeHousingTag } from '../../utils/select-room-normalizers'
import { areaDetailPagination, facilityManagers } from '@/api/index.js'
import { localizeFacilityName, localizeHousingTag, normalizeAreaItem } from '../../utils/select-room-normalizers'
function parseCoordinate(...values) {
for (let i = 0; i < values.length; i += 1) {
@@ -145,18 +140,12 @@ export default {
if (Array.isArray(this.storeData.images) && this.storeData.images.length) {
return this.storeData.images.filter(Boolean)
}
if (this.heroImage) {
return [this.heroImage]
}
return []
},
heroImage() {
if (this.storeData.images && this.storeData.images[0]) {
return this.storeData.images[0]
}
if (this.roomTypes.length > 0 && this.roomTypes[0].images && this.roomTypes[0].images[0]) {
return this.roomTypes[0].images[0]
}
return ''
},
storeTags() {
@@ -165,9 +154,9 @@ export default {
},
displayStoreName() {
if (this.language === 'en') {
return this.storeData.nameEnDisplay || this.storeData.nameEn || this.storeData.name || this.t('apartmentName')
return this.storeData.nameEnDisplay || this.storeData.nameEn || this.storeData.name || this.$tr('公寓详情')
}
return this.storeData.name || this.storeData.nameEn || this.t('apartmentName')
return this.storeData.name || this.storeData.nameEn || this.$tr('公寓详情')
},
displayStoreAddress() {
if (this.language === 'en') {
@@ -214,34 +203,47 @@ export default {
},
contactButtonText() {
if (this.isAreaManagerLoading) {
return this.t('loading')
return this.$tr('加载中...')
}
if (this.areaManagerOptions.length > 1) {
return this.t('selectContact')
return this.$tr('选择联系人')
}
if (this.areaManagerName) {
return this.t('contactManager')
return this.$tr('联系管家')
}
return this.t('phoneConsult')
return this.$tr('电话咨询')
},
navBarBackgroundColor() {
const opacity = Math.min(Math.max((this.scrollTop - 20) / 80, 0), 1);
return `rgba(255, 255, 255, ${opacity})`;
},
navBarBackStyle() {
const isDarkBg = this.scrollTop < 100;
const isTop = this.scrollTop < 60;
return {
transition: 'all 0.3s',
background: isDarkBg ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.15)',
backdropFilter: 'blur(10px)',
background: isTop ? 'rgba(255, 255, 255, 0.92)' : '#FFFFFF',
backdropFilter: 'blur(12rpx)',
WebkitBackdropFilter: 'blur(12rpx)',
borderRadius: '50%',
width: '64rpx',
height: '64rpx',
width: '68rpx',
height: '68rpx',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: '12rpx',
border: '1rpx solid rgba(255, 255, 255, 0.25)'
boxShadow: isTop
? '0 8rpx 24rpx rgba(30, 58, 95, 0.12), inset 0 1rpx 0 rgba(255, 255, 255, 0.6)'
: '0 8rpx 20rpx rgba(15, 23, 42, 0.08)'
};
},
navBarIconColor() {
return '#1E3A5F';
},
navTitleColor() {
return '#1E3A5F';
},
navTitleOpacity() {
return Math.min(Math.max((this.scrollTop - 80) / 100, 0), 1);
return Math.min(Math.max((this.scrollTop - 60) / 80, 0), 1);
}
},
onLoad(options) {
@@ -253,33 +255,97 @@ export default {
this.applyStoreData(data)
})
.catch((e) => {
console.error(this.t('parseStoreDataFailed'), e);
console.error(this.$tr('解析小区数据失败'), e);
})
return
}
},
onShareAppMessage() {
return {
title: this.displayStoreName || this.t('shareTitle'),
title: this.displayStoreName || this.$tr('小区详情'),
path: `/pkgExplore/store_detail/store_detail?storeData=${encodeURIComponent(JSON.stringify(this.storeData || {}))}`,
imageUrl: this.heroImage || ''
}
},
onShareTimeline() {
return {
title: this.displayStoreName || this.t('shareTitle'),
title: this.displayStoreName || this.$tr('小区详情'),
query: `storeData=${encodeURIComponent(JSON.stringify(this.storeData || {}))}`,
imageUrl: this.heroImage || ''
}
},
methods: {
t(key) {
return this.$tm('pkgExplore.store_detail.store_detail', key)
},
applyStoreData(data) {
this.storeData = data && typeof data === 'object' ? data : {}
this.roomTypes = this.storeData.roomTypes || []
this.loadAreaManager()
this.currentImageIndex = 0
return this.loadAreaDetail()
.catch((error) => {
console.warn('查询区域详情失败', error)
})
.finally(() => {
this.loadAreaManager()
})
},
pickAreaDetail(rawList = []) {
if (!Array.isArray(rawList) || !rawList.length) {
return null
}
const areaCode = String(this.storeData.code || this.storeData.areaCode || '').trim()
const areaName = String(this.storeData.name || '').trim()
const areaNameEn = String(this.storeData.nameEn || '').trim()
if (areaCode) {
const exactCodeItem = rawList.find((item) => String(item && item.code || '').trim() === areaCode)
if (exactCodeItem) {
return exactCodeItem
}
}
const exactNameItem = rawList.find((item) => {
const name = String(item && item.name || '').trim()
const nameEn = String(item && item.nameEn || '').trim()
return (areaName && name === areaName) || (areaNameEn && nameEn === areaNameEn)
})
if (exactNameItem) {
return exactNameItem
}
return rawList[0] || null
},
loadAreaDetail() {
const areaCode = String(this.storeData.code || this.storeData.areaCode || '').trim()
const searchKeyword = areaCode || String(this.storeData.name || '').trim()
if (!searchKeyword) {
return Promise.resolve()
}
return areaDetailPagination({
PageIndex: 1,
PageSize: areaCode ? 20 : 50,
Search: searchKeyword
}, this.language)
.then((res) => {
const rawList = Array.isArray(res && res.data && res.data.data)
? res.data.data
: Array.isArray(res && res.data)
? res.data
: []
const matchedArea = this.pickAreaDetail(rawList)
if (!matchedArea) {
return
}
const normalizedArea = normalizeAreaItem(matchedArea)
this.storeData = {
...this.storeData,
...normalizedArea,
roomTypes: (this.storeData.roomTypes || []).length ? (this.storeData.roomTypes || []) : normalizedArea.roomTypes,
tags: (this.storeData.tags || []).length ? this.storeData.tags : normalizedArea.tags,
facilities: (this.storeData.facilities || []).length ? this.storeData.facilities : normalizedArea.facilities
}
})
},
loadAreaManager() {
const areaCode = this.storeData.code || this.storeData.areaCode || ''
@@ -333,16 +399,16 @@ export default {
},
getManagerActionText(manager) {
if (!manager) {
return this.t('phoneConsult')
return this.$tr('电话咨询')
}
if (manager.displayName && manager.phone) {
return `${manager.displayName}(${manager.phone})`
}
return manager.displayName || manager.phone || this.t('phoneConsult')
return manager.displayName || manager.phone || this.$tr('电话咨询')
},
makePhoneCall(phoneNumber) {
if (!phoneNumber) {
uni.showToast({ title: this.t('noServicePhone'), icon: 'none' })
uni.showToast({ title: this.$tr('暂无联系电话'), icon: 'none' })
return
}
uni.makePhoneCall({
@@ -360,7 +426,7 @@ export default {
const longitude = parseCoordinate(this.storeData.longitude, this.storeData.lng)
if (latitude === null || longitude === null) {
uni.showToast({
title: this.t('noLocation'),
title: this.$tr('暂无位置信息'),
icon: 'none'
})
return
@@ -368,7 +434,7 @@ export default {
uni.openLocation({
latitude,
longitude,
name: this.displayStoreName || this.t('apartmentName'),
name: this.displayStoreName || this.$tr('公寓详情'),
address: this.displayStoreAddress || '',
scale: 18
});
@@ -390,7 +456,7 @@ export default {
},
handleCall() {
if (this.isAreaManagerLoading && !this.contactPhoneNumber) {
uni.showToast({ title: this.t('loadingManagerInfo'), icon: 'none' })
uni.showToast({ title: this.$tr('正在加载管家信息'), icon: 'none' })
return
}
@@ -415,9 +481,20 @@ export default {
</script>
<style scoped lang="scss">
/* 品牌色变量本地化(深海蓝 + 香槟金) */
$navy-deep: #1E3A5F;
$navy-light: #2E4A73;
$gold: #C9A96E;
$gold-light: #D4BC8C;
$bg-blue: #F0F5FF;
$bg-page: #F8FAFF;
$text-primary: #0F172A;
$text-secondary: #64748B;
$text-tertiary: #94A3B8;
.store-detail-page {
height: 100vh;
background-color: #000; /* 全屏黑色底色,防止滑动露白 */
background: linear-gradient(180deg, $bg-blue 0%, $bg-page 25%, #FFFFFF 100%);
display: flex;
flex-direction: column;
position: relative;
@@ -427,7 +504,6 @@ export default {
.nav-title {
font-size: 34rpx;
font-weight: 700;
color: #fff;
transition: opacity 0.3s;
max-width: 420rpx;
overflow: hidden;
@@ -435,40 +511,14 @@ export default {
white-space: nowrap;
}
.transparent-nav-bar ::v-deep .tn-nav-bar {
background: transparent !important;
.glass-nav-bar ::v-deep .tn-nav-bar {
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
box-shadow: none !important;
border: none !important;
}
/* 底层背景 */
.fullscreen-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.bg-swiper, .bg-image, .bg-empty {
width: 100%;
height: 100%;
}
.bg-empty {
background: #111;
color: rgba(255, 255, 255, 0.4);
}
.bg-mask {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.4) 60%, rgba(0,0,0,0.85) 100%);
pointer-events: none;
}
/* 上层滚动内容区 */
/* 可滚动内容区 */
.content-scroll {
position: relative;
z-index: 10;
@@ -485,28 +535,48 @@ export default {
.scroll-inner {
min-height: 101%;
padding-top: calc(100rpx + env(safe-area-inset-top));
}
/* 留出背景展示区 */
.hero-space {
/* Hero 大图区 */
.hero-panel {
position: relative;
height: 460rpx;
margin: 0 32rpx;
border-radius: 32rpx;
overflow: hidden;
box-shadow: 0 20rpx 60rpx rgba(30, 58, 95, 0.16);
background: linear-gradient(135deg, #E2E8F0 0%, #CBD5E1 100%);
}
.hero-swiper,
.hero-image,
.hero-empty {
width: 100%;
height: 35vh; /* 进一步降低留白高度,确保底部房型列表至少透出一条数据 */
padding: 0 32rpx 40rpx;
justify-content: flex-end; /* 内容沉底 */
height: 100%;
}
.hero-empty {
color: $text-tertiary;
font-size: 28rpx;
}
.hero-counter {
position: absolute;
right: 24rpx;
bottom: 24rpx;
height: 52rpx;
padding: 0 20rpx;
border-radius: 999rpx;
background: rgba(0, 0, 0, 0.3);
background: rgba($navy-deep, 0.85);
backdrop-filter: blur(14px);
color: #ffffff;
color: $gold-light;
font-size: 24rpx;
font-weight: 500;
border: 1rpx solid rgba(255, 255, 255, 0.2);
margin-bottom: 24rpx;
align-self: flex-end; /* 靠右对齐 */
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(30, 58, 95, 0.2);
}
.counter-icon {
@@ -514,59 +584,72 @@ export default {
font-size: 26rpx;
}
.store-title-wrap {
width: 100%;
color: #fff;
/* 店名主卡片 */
.store-title-card {
position: relative;
margin: -60rpx 32rpx 0;
padding: 40rpx;
background: #FFFFFF;
border-radius: 32rpx;
box-shadow: 0 12rpx 40rpx rgba(30, 58, 95, 0.08);
z-index: 20;
}
.store-title {
font-size: 64rpx;
font-weight: bold;
margin-bottom: 24rpx;
text-shadow: 0 4rpx 16rpx rgba(0,0,0,0.6);
font-size: 48rpx;
font-weight: 800;
color: $navy-deep;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.title-divider {
width: 48rpx;
height: 4rpx;
margin: 24rpx 0;
background: linear-gradient(90deg, $gold 0%, $gold-light 100%);
border-radius: 2rpx;
}
.store-tags {
gap: 16rpx;
}
.tag-item {
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 8rpx;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border: 1rpx solid rgba(255, 255, 255, 0.2);
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.3);
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: rgba($navy-deep, 0.06);
border: 1rpx solid rgba($navy-deep, 0.12);
color: $navy-deep;
font-weight: 500;
}
/* 悬浮毛玻璃信息流 */
.floating-content-wrap {
position: relative;
padding-bottom: 180rpx;
/* 信息流 */
.content-body {
padding: 24rpx 0 0;
}
/* 底部操作栏 - 毛玻璃悬浮 */
/* 底部操作栏 */
.action-bar-wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 18rpx 32rpx calc(18rpx + env(safe-area-inset-bottom));
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 100%);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
box-shadow: 0 -8rpx 30rpx rgba(30, 58, 95, 0.06);
z-index: 100;
pointer-events: none; /* 让渐变层不阻挡点击 */
}
.action-bar {
display: flex;
gap: 24rpx;
pointer-events: auto; /* 恢复按钮区域点击 */
}
.action-btn {
@@ -576,35 +659,41 @@ export default {
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: bold;
font-weight: 700;
padding: 0;
border: 1rpx solid rgba(255, 255, 255, 0.3);
line-height: 1;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
transition: all 0.2s;
}
button.action-btn::after {
border: none;
}
.action-btn:active {
transform: scale(0.97);
}
.action-btn__icon {
margin-right: 12rpx;
font-size: 34rpx;
}
.action-btn--glass {
.action-btn--secondary {
flex: 0 0 200rpx;
background: rgba(255, 255, 255, 0.15);
color: #fff;
background: rgba($navy-deep, 0.06);
color: $navy-deep;
border: 1rpx solid rgba($navy-deep, 0.12);
}
.action-btn--primary-glass {
.action-btn--primary {
flex: 1;
background: rgba($brand-primary, 0.85);
color: #fff;
border-color: rgba($brand-primary, 0.5);
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.3);
background: linear-gradient(135deg, $navy-light 0%, $navy-deep 100%);
color: #FFFFFF;
box-shadow: 0 12rpx 32rpx rgba($navy-deep, 0.25);
}
.action-btn--primary .action-btn__icon {
color: $gold-light;
}
.bottom-spacer {

View File

@@ -12,23 +12,23 @@
<view class="vr-stage">
<image v-if="poster" :src="poster" mode="aspectFill" class="vr-poster"></image>
<view v-else class="vr-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ t('emptyPreview') }}</view>
<view v-else class="vr-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ $tr('暂无VR预览画面') }}</view>
<view class="vr-mask"></view>
<view class="vr-mode-chip">{{ t('modeChip') }}</view>
<view class="vr-mode-chip">{{ $tr('VR预览模式') }}</view>
<view class="vr-center-indicator" @click="openPosterPreview">
<view class="indicator-ring"></view>
<view class="indicator-core">
<text class="tn-icon-vr indicator-icon"></text>
</view>
<text class="indicator-text">{{ poster ? t('viewFullscreen') : t('waitingAsset') }}</text>
<text class="indicator-text">{{ poster ? $tr('查看全屏预览') : $tr('等待素材接入') }}</text>
</view>
<view class="vr-bottom-bar tn-safe-area-inset-bottom">
<view class="vr-title-area">
<view class="vr-title">{{ resolvedPageTitle }}</view>
<view class="vr-subtitle">{{ poster ? t('posterSubtitle') : t('placeholderSubtitle') }}</view>
<view class="vr-subtitle">{{ poster ? $tr('当前展示 VR 预览海报,可点击查看大图。') : $tr('当前房型已保留 VR 入口,后续可继续接入更完整的沉浸式资源。') }}</view>
</view>
<view class="vr-action-circle" @click="openPosterPreview">
@@ -41,28 +41,6 @@
<script>
export default {
i18n: {
zh: {
defaultTitle: 'VR看房',
emptyPreview: '暂无VR预览画面',
modeChip: 'VR预览模式',
viewFullscreen: '查看全屏预览',
waitingAsset: '等待素材接入',
posterSubtitle: '当前展示 VR 预览海报,可点击查看大图。',
placeholderSubtitle: '当前房型已保留 VR 入口,后续可继续接入更完整的沉浸式资源。',
noAsset: '暂无VR预览素材'
},
en: {
defaultTitle: 'VR Viewing',
emptyPreview: 'No VR preview available',
modeChip: 'VR Preview',
viewFullscreen: 'View Fullscreen',
waitingAsset: 'Waiting for assets',
posterSubtitle: 'The current VR preview poster is displayed. Tap to view the full image.',
placeholderSubtitle: 'This room type keeps its VR entry reserved for richer immersive assets later.',
noAsset: 'No VR preview asset available'
}
},
data() {
return {
pageTitleZh: '',
@@ -74,9 +52,9 @@ export default {
computed: {
resolvedPageTitle() {
if (this.$getLanguage() === 'en') {
return this.pageTitleEn || this.pageTitleZh || this.rawPageTitle || this.t('defaultTitle')
return this.pageTitleEn || this.pageTitleZh || this.rawPageTitle || this.$tr('VR看房')
}
return this.pageTitleZh || this.pageTitleEn || this.rawPageTitle || this.t('defaultTitle')
return this.pageTitleZh || this.pageTitleEn || this.rawPageTitle || this.$tr('VR看房')
},
navBarBackStyle() {
return {
@@ -112,13 +90,10 @@ export default {
}
},
methods: {
t(key, params) {
return this.$tm('pkgExplore.vr_preview.vr_preview', key, params)
},
openPosterPreview() {
if (!this.poster) {
uni.showToast({
title: this.t('noAsset'),
title: this.$tr('暂无VR预览素材'),
icon: 'none'
})
return

View File

@@ -1,7 +1,7 @@
<template>
<view class="application-detail-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>
<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' }">
@@ -23,66 +23,78 @@
</view>
</view>
<view
v-if="showPaymentStatusPanel"
class="payment-status-panel tn-margin-bottom-lg"
:class="`payment-status-panel--${paymentStatusType}`"
>
<view class="payment-status-panel__label">{{ $tr('支付状态') }}</view>
<view class="payment-status-panel__main">
<view class="payment-status-panel__value">{{ paymentStatusText }}</view>
<view v-if="canViewBill" class="payment-status-panel__hint">{{ $tr('请尽快完成定金支付') }}</view>
</view>
</view>
<view
v-if="statusReminderText"
class="status-reminder-panel tn-margin-bottom-lg"
:class="{
'status-reminder-panel--warning': shouldPayDeposit(detail),
'status-reminder-panel--warning': shouldPayDeposit(detail) && !isWaitingButlerRoomAssignment(detail),
'status-reminder-panel--info': isWaitingRoomAssignment(detail)
}"
>
<view class="status-reminder-panel__icon">
<text :class="shouldPayDeposit(detail) ? 'tn-icon-warning-fill' : 'tn-icon-time-fill'"></text>
<text :class="shouldPayDeposit(detail) && !isWaitingButlerRoomAssignment(detail) ? 'tn-icon-warning-fill' : 'tn-icon-time-fill'"></text>
</view>
<view class="status-reminder-panel__content">
<view class="status-reminder-panel__title">{{ t('statusReminderTitle') }}</view>
<view class="status-reminder-panel__title">{{ $tr('当前进度提醒') }}</view>
<view class="status-reminder-panel__text">{{ statusReminderText }}</view>
</view>
</view>
<view class="content-panel tn-margin-bottom-lg">
<view class="panel-title">{{ t('basicInfoTitle') }}</view>
<view class="panel-title">{{ $tr('基础信息') }}</view>
<view class="meta-row tn-margin-top">
<text class="meta-label">{{ t('applicationCodeLabel') }}</text>
<text class="meta-label">{{ $tr('申请编号') }}</text>
<view class="meta-action-wrap">
<text class="meta-value">{{ detail.detailCode || '--' }}</text>
<view v-if="detail.detailCode" class="copy-btn" @click="copyCode">
<text class="tn-text-xs tn-text-bold">{{ t('copy') }}</text>
<text class="tn-text-xs tn-text-bold">{{ $tr('复制') }}</text>
</view>
</view>
</view>
<view class="meta-row">
<text class="meta-label">{{ t('applicationTypeLabel') }}</text>
<text class="meta-label">{{ $tr('申请类型') }}</text>
<text class="meta-value">{{ applicationTypeText }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ t('statusLabel') }}</text>
<text class="meta-label">{{ $tr('当前状态') }}</text>
<text class="meta-value">{{ statusText }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ t('applicantLabel') }}</text>
<text class="meta-label">{{ $tr('申请人') }}</text>
<text class="meta-value">{{ detail.userName || detail.phone || '--' }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ t('apartmentLabel') }}</text>
<text class="meta-label">{{ $tr('所属公寓') }}</text>
<text class="meta-value">{{ detail.intendedAreaName || '--' }}</text>
</view>
<view class="meta-row" v-if="detail.intendedRoomTypeName">
<text class="meta-label">{{ isViewingApplication ? t('intendedRoomTypeLabel') : t('appliedRoomTypeLabel') }}</text>
<text class="meta-label">{{ isViewingApplication ? $tr('意向房型') : $tr('申请房型') }}</text>
<text class="meta-value">{{ detail.intendedRoomTypeName }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ isViewingApplication ? t('viewingTimeLabel') : t('leaseTermLabel') }}</text>
<text class="meta-label">{{ isViewingApplication ? $tr('预约时间') : $tr('预计租期') }}</text>
<text class="meta-value">{{ displayDateRange }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ t('submittedAtLabel') }}</text>
<text class="meta-label">{{ $tr('提交时间') }}</text>
<text class="meta-value">{{ formatDateTime(detail.createTime) }}</text>
</view>
</view>
<view class="content-panel tn-margin-bottom-lg" v-if="viewingMeta.length">
<view class="panel-title">{{ t('viewingInfoTitle') }}</view>
<view class="panel-title">{{ $tr('带看信息') }}</view>
<view
v-for="item in viewingMeta"
:key="item.label"
@@ -94,7 +106,7 @@
</view>
<view class="content-panel tn-margin-bottom-lg">
<view class="panel-title">{{ isViewingApplication ? t('applicationRemarkTitle') : t('remarkTitle') }}</view>
<view class="panel-title">{{ isViewingApplication ? $tr('申请说明') : $tr('补充说明') }}</view>
<view class="remark-panel tn-margin-top">
<view
v-for="(line, index) in remarkLines"
@@ -108,7 +120,7 @@
<view class="action-row">
<view class="ghost-btn" @click="goBackList">
<text class="tn-text-bold">{{ t('backList') }}</text>
<text class="tn-text-bold">{{ $tr('返回列表') }}</text>
</view>
<view
v-if="canCancelOrder"
@@ -116,7 +128,7 @@
:class="{ 'danger-btn--loading': canceling }"
@click.stop="handleCancelOrder"
>
<text class="tn-text-bold">{{ canceling ? t('cancelOrderLoading') : t('cancelOrder') }}</text>
<text class="tn-text-bold">{{ canceling ? $tr('取消中...') : $tr('取消申请') }}</text>
</view>
<view
v-if="currentStepActionText"
@@ -130,14 +142,14 @@
class="action-btn action-btn--ghost"
@click.stop="refreshCurrentPage"
>
<text class="tn-text-bold">{{ t('refreshAction') }}</text>
<text class="tn-text-bold">{{ $tr('刷新') }}</text>
</view>
<view
v-else-if="canViewBill"
class="action-btn"
@click.stop="goToBillPage"
>
<text class="tn-text-bold">{{ t('viewBill') }}</text>
<text class="tn-text-bold">{{ $tr('查看关联账单') }}</text>
</view>
</view>
</view>
@@ -145,7 +157,7 @@
</template>
<script>
import { contractDetail, orderSearchedReservationFlow, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
import { accountQueryBills, contractDetail, orderSearchedReservationFlow, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
import { getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
import {
RESERVATION_FLOW_STEP,
@@ -202,6 +214,43 @@ function normalizeApplicationList(list = []) {
})
}
function resolveApplicationPaymentStatusInfo(item = {}) {
if (!item || item.isViewingApplication) {
return {
key: 'exempt',
desc: ''
}
}
const desc = String(item.payStatusDesc || item.paymentStatusDesc || '').trim()
const status = Number(
item.payStatus !== undefined && item.payStatus !== null
? item.payStatus
: item.paymentStatus
)
if (status === 10 || /已支付|paid/i.test(desc)) {
return { key: 'paid', desc }
}
if (/退款|refunded?/i.test(desc)) {
return { key: 'refunded', desc }
}
if (/失败|failed?/i.test(desc)) {
return { key: 'failed', desc }
}
if (/处理中|processing/i.test(desc)) {
return { key: 'processing', desc }
}
if (status === 0 || /待支付|未支付|unpaid|pending/i.test(desc)) {
return { key: 'pending', desc }
}
return {
key: desc ? 'processing' : 'pending',
desc
}
}
function parseJsonStringMaybe(value) {
if (typeof value !== 'string') {
return value
@@ -221,112 +270,6 @@ function parseJsonStringMaybe(value) {
}
export default {
i18n: {
zh: {
navTitle: '申请详情',
basicInfoTitle: '基础信息',
applicationCodeLabel: '申请编号',
copy: '复制',
applicationTypeLabel: '申请类型',
statusLabel: '当前状态',
applicantLabel: '申请人',
apartmentLabel: '所属公寓',
intendedRoomTypeLabel: '意向房型',
appliedRoomTypeLabel: '申请房型',
viewingTimeLabel: '预约时间',
leaseTermLabel: '预计租期',
submittedAtLabel: '提交时间',
viewingInfoTitle: '带看信息',
applicationRemarkTitle: '申请说明',
remarkTitle: '补充说明',
backList: '返回列表',
viewBill: '查看关联账单',
viewingApplicationTitle: '预约看房申请',
stayApplicationTitle: '入住申请',
viewingApplicationType: '预约看房',
stayApplicationType: '入住申请',
statusPending: '待审核',
statusApproved: '已通过',
statusRejected: '已拒绝',
statusFinished: '已结束',
statusProcessing: '处理中',
statusPendingPayment: '请支付定金',
statusWaitingRoomAssign: '等待预订中心分配房间',
statusReminderTitle: '当前进度提醒',
emptyRemark: '暂无补充说明',
codeCopied: '申请编号已复制',
missingBillCode: '申请单编号缺失',
missingContractNo: '合同编号缺失',
checkinPendingHint: '宿管正在办理入住,请耐心等待',
refreshAction: '刷新',
cancelOrder: '取消订单',
cancelOrderLoading: '取消中...',
cancelOrderTitle: '取消订单',
cancelOrderContent: '取消后将关闭当前未支付的定金订单,是否继续?',
cancelOrderSuccess: '订单已取消',
cancelOrderFailed: '取消订单失败',
viewingTimeKey: '看房时间',
leaseDurationKey: '预计租住时长',
budgetKey: '租金预算',
commuteKey: '工作/通勤地点',
preferredAreaKey: '意向区域',
preferredRoomTypeKey: '意向房型',
paymentMethodKey: '支付方式',
dateRangeJoiner: ' 至 '
},
en: {
navTitle: 'Application Detail',
basicInfoTitle: 'Basic Information',
applicationCodeLabel: 'Application No.',
copy: 'Copy',
applicationTypeLabel: 'Application Type',
statusLabel: 'Status',
applicantLabel: 'Applicant',
apartmentLabel: 'Apartment',
intendedRoomTypeLabel: 'Preferred Room Type',
appliedRoomTypeLabel: 'Requested Room Type',
viewingTimeLabel: 'Viewing Time',
leaseTermLabel: 'Lease Term',
submittedAtLabel: 'Submitted At',
viewingInfoTitle: 'Viewing Details',
applicationRemarkTitle: 'Application Notes',
remarkTitle: 'Remarks',
backList: 'Back to List',
viewBill: 'View Related Bill',
viewingApplicationTitle: 'Viewing Request',
stayApplicationTitle: 'Stay Application',
viewingApplicationType: 'Viewing Request',
stayApplicationType: 'Stay Application',
statusPending: 'Pending',
statusApproved: 'Approved',
statusRejected: 'Rejected',
statusFinished: 'Closed',
statusProcessing: 'Processing',
statusPendingPayment: 'Please Pay Deposit',
statusWaitingRoomAssign: 'Waiting for Room Assignment',
statusReminderTitle: 'Current Progress',
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',
cancelOrderContent: 'This will close the current unpaid deposit order. Continue?',
cancelOrderSuccess: 'Order canceled',
cancelOrderFailed: 'Failed to cancel order',
viewingTimeKey: 'Viewing Time',
leaseDurationKey: 'Expected Stay Duration',
budgetKey: 'Budget',
commuteKey: 'Work / Commute Location',
preferredAreaKey: 'Preferred Area',
preferredRoomTypeKey: 'Preferred Room Type',
paymentMethodKey: 'Payment Method',
dateRangeJoiner: ' to '
}
},
data() {
return {
detailCode: '',
@@ -339,6 +282,7 @@ export default {
watch: {
language() {
this.loadReservationFlow()
this.loadPendingDepositBillState()
}
},
computed: {
@@ -347,24 +291,24 @@ export default {
},
displayTitle() {
if (this.isViewingApplication) {
return this.detail.intendedRoomTypeName || this.t('viewingApplicationTitle')
return this.detail.intendedRoomTypeName || this.$tr('预约看房申请')
}
return this.detail.intendedRoomTypeName || this.t('stayApplicationTitle')
return this.detail.intendedRoomTypeName || this.$tr('入住申请')
},
applicationTypeText() {
return this.isViewingApplication ? this.t('viewingApplicationType') : this.t('stayApplicationType')
return this.isViewingApplication ? this.$tr('预约看房') : this.$tr('入住申请')
},
statusText() {
if (this.language !== 'en' && this.detail && this.detail.statusDesc) {
return this.detail.statusDesc
}
const map = {
0: this.t('statusPending'),
10: this.t('statusApproved'),
20: this.t('statusRejected'),
99: this.t('statusFinished')
0: this.$tr('待审核'),
10: this.$tr('已通过'),
20: this.$tr('已拒绝'),
99: this.$tr('已结束')
}
return map[this.detail.status] || this.t('statusProcessing')
return map[this.detail.status] || this.$tr('处理中')
},
statusType() {
if (this.detail.status === 10) {
@@ -384,19 +328,55 @@ export default {
heroBadgeClass() {
return this.isViewingApplication ? 'badge-viewing' : 'badge-stay'
},
showPaymentStatusPanel() {
return !this.isViewingApplication
},
paymentStatusText() {
if (this.isWaitingButlerRoomAssignment(this.detail)) {
return this.$tr('等待管家排房')
}
const statusInfo = resolveApplicationPaymentStatusInfo(this.detail)
if (this.$getLanguage() !== 'en' && statusInfo.desc) {
return statusInfo.desc
}
const textMap = {
exempt: this.$tr('无需支付'),
paid: this.$tr('已支付'),
refunded: this.$tr('已退款'),
failed: this.$tr('支付失败'),
processing: this.$tr('处理中'),
pending: this.$tr('待支付')
}
return textMap[statusInfo.key] || statusInfo.desc || this.$tr('待支付')
},
paymentStatusType() {
if (this.isWaitingButlerRoomAssignment(this.detail)) {
return 'info'
}
const statusInfo = resolveApplicationPaymentStatusInfo(this.detail)
const typeMap = {
exempt: 'neutral',
paid: 'success',
refunded: 'neutral',
failed: 'danger',
processing: 'info',
pending: 'warning'
}
return typeMap[statusInfo.key] || 'warning'
},
displayDateRange() {
if (!this.detail.start && !this.detail.end) {
return '--'
}
if (this.isViewingApplication) {
return `${this.formatDateTime(this.detail.start)}${this.t('dateRangeJoiner')}${this.formatDateTime(this.detail.end)}`
return `${this.formatDateTime(this.detail.start)}${this.$tr(' 至 ')}${this.formatDateTime(this.detail.end)}`
}
return `${this.formatDate(this.detail.start)}${this.t('dateRangeJoiner')}${this.formatDate(this.detail.end)}`
return `${this.formatDate(this.detail.start)}${this.$tr(' 至 ')}${this.formatDate(this.detail.end)}`
},
remarkLines() {
const text = this.detail && this.detail.remark ? String(this.detail.remark).trim() : ''
if (!text) {
return [this.t('emptyRemark')]
return [this.$tr('暂无补充说明')]
}
return text.split(/\r?\n/).filter(Boolean)
},
@@ -429,33 +409,45 @@ export default {
return this.getReservationContractStatusText()
}
if (this.isReservationCheckinProcessing()) {
return this.t('checkinPendingHint')
return this.$tr('宿管正在办理入住,请耐心等待')
}
const reservationStepText = getReservationCurrentStepText(this.detail, this.language)
if (reservationStepText) {
return reservationStepText
}
if (this.isWaitingButlerRoomAssignment(this.detail)) {
return this.$tr('等待管家排房')
}
if (this.shouldPayDeposit(this.detail)) {
return this.t('statusPendingPayment')
return this.$tr('请支付定金')
}
if (this.isWaitingRoomAssignment(this.detail)) {
return this.t('statusWaitingRoomAssign')
return this.$tr('等待管家排房')
}
return ''
},
canViewBill() {
return this.shouldPayDeposit(this.detail)
&& this.hasPendingDepositBill(this.detail)
&& !this.isViewingApplication
&& this.detail
&& this.detail.detailCode
&& this.statusType === 'pending'
},
canCancelApplication() {
if (!(this.detail && this.detail.detailCode && Number(this.detail.status) === 0)) {
return false
}
if (this.isViewingApplication) {
return true
}
if (Number(this.detail.payStatus) === 10) {
return false
}
return this.hasPendingDepositBill(this.detail)
},
canCancelOrder() {
return this.shouldPayDeposit(this.detail)
&& !this.isViewingApplication
&& this.detail
&& this.detail.detailCode
&& !this.canceling
return this.canCancelApplication && !this.canceling
}
},
onLoad(options = {}) {
@@ -464,14 +456,39 @@ export default {
},
onShow() {
this.loadReservationFlow()
this.loadPendingDepositBillState()
},
methods: {
t(key, params) {
return this.$tm('pkgService.application_detail.application_detail', key, params)
getBillNo(item = {}) {
return item.no || item.billNo || ''
},
normalizeBillList(response) {
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
if (Array.isArray(payload)) {
return payload.filter(item => item && typeof item === 'object')
}
if (payload && Array.isArray(payload.data)) {
return payload.data.filter(item => item && typeof item === 'object')
}
return []
},
isPaidApplication(item = {}) {
return !item.isViewingApplication && Number(item.payStatus) === 10
},
shouldQueryPendingDepositBill(item = {}) {
return !item.isViewingApplication
&& Number(item.status) === 0
&& Number(item.payStatus) !== 10
&& !!String(item.detailCode || this.detailCode || '').trim()
},
hasPendingDepositBill(item = {}) {
return !!String(this.getBillNo(item && item.pendingDepositBill ? item.pendingDepositBill : {}) || '').trim()
},
isWaitingButlerRoomAssignment(item = {}) {
return this.shouldQueryPendingDepositBill(item)
&& !!(item && item.pendingDepositBillQueried)
&& !this.hasPendingDepositBill(item)
},
shouldPayDeposit(item = {}) {
return !item.isViewingApplication
&& Number(item.status) === 0
@@ -480,7 +497,7 @@ export default {
isWaitingRoomAssignment(item = {}) {
return !item.isViewingApplication
&& Number(item.status) === 0
&& Number(item.payStatus) === 10
&& (Number(item.payStatus) === 10 || this.isWaitingButlerRoomAssignment(item))
},
getReservationCurrentStepValue() {
return Number(this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode.step : 0)
@@ -530,7 +547,7 @@ export default {
return Number(detail.status) === 2
},
getReservationInspectionStatusText() {
return this.t('inspectionPendingReviewHint')
return this.$tr('inspectionPendingReviewHint')
},
isReservationContractAwaitingReview() {
if (this.getReservationCurrentStepValue() !== RESERVATION_FLOW_STEP.CONTRACT) {
@@ -600,6 +617,50 @@ export default {
isViewingApplication: false
}
},
syncDetailToCache(detail = this.detail) {
const list = normalizeApplicationList(getMyApplicationList())
if (!list.length || !detail || !detail.detailCode) {
return
}
saveMyApplicationList(list.map((item) => {
if (String(item.detailCode || '') !== String(detail.detailCode || '')) {
return item
}
return {
...item,
...detail
}
}))
},
async loadPendingDepositBillState() {
const currentDetail = this.detail && typeof this.detail === 'object' ? this.detail : {}
const referenceTypeNo = String(currentDetail.detailCode || this.detailCode || '').trim()
if (!this.shouldQueryPendingDepositBill(currentDetail) || !referenceTypeNo) {
return
}
try {
const response = await accountQueryBills({
referenceTypeNo,
status: 10,
paymentStatus: 0,
ProviderType: 'WechatMiniProgram'
}, this.language)
if (referenceTypeNo !== String(this.detail && this.detail.detailCode || this.detailCode || '').trim()) {
return
}
const billList = this.normalizeBillList(response)
const nextDetail = {
...this.detail,
pendingDepositBill: Array.isArray(billList) && billList.length ? billList[0] : null,
pendingDepositBillQueried: true
}
this.detail = nextDetail
this.syncDetailToCache(nextDetail)
} catch (error) {
console.error('load pending deposit bill failed', error)
}
},
async loadReservationFlow() {
if (!this.detail || this.detail.isViewingApplication) {
return
@@ -663,13 +724,13 @@ export default {
return null
}
const localizedLabelMap = {
'看房时间': this.t('viewingTimeKey'),
'预计租住时长': this.t('leaseDurationKey'),
'租金预算': this.t('budgetKey'),
'工作/通勤地点': this.t('commuteKey'),
'意向区域': this.t('preferredAreaKey'),
'意向房型': this.t('preferredRoomTypeKey'),
'支付方式': this.t('paymentMethodKey')
'看房时间': this.$tr('看房时间'),
'预计租住时长': this.$tr('预计租住时长'),
'租金预算': this.$tr('租金预算'),
'工作/通勤地点': this.$tr('工作/通勤地点'),
'意向区域': this.$tr('意向区域'),
'意向房型': this.$tr('意向房型'),
'支付方式': this.$tr('支付方式')
}
const rawValue = line
.replace(`${matched.zh}:`, '')
@@ -704,17 +765,22 @@ export default {
data: String(this.detail.detailCode),
success: () => {
uni.showToast({
title: this.t('codeCopied'),
title: this.$tr('申请编号已复制'),
icon: 'none'
})
}
})
},
getCancelOrderContent() {
return this.isViewingApplication
? this.$tr('取消后将关闭当前预约看房申请,是否继续?')
: this.$tr('取消后将关闭当前申请单,是否继续?')
},
confirmCancelOrder() {
return new Promise((resolve) => {
uni.showModal({
title: this.t('cancelOrderTitle'),
content: this.t('cancelOrderContent'),
title: this.$tr('取消申请'),
content: this.getCancelOrderContent(),
confirmColor: '#ef4444',
success: (res) => resolve(!!(res && res.confirm)),
fail: () => resolve(false)
@@ -733,7 +799,7 @@ export default {
return {
...item,
status: 99,
statusDesc: this.t('statusFinished')
statusDesc: this.$tr('已结束')
}
})
saveMyApplicationList(nextList)
@@ -749,26 +815,24 @@ export default {
}
this.canceling = true
return userDetailCancel({
detailCode: this.detail.detailCode,
code: this.detail.detailCode,
referenceTypeNo: this.detail.detailCode
detailCode: this.detail.detailCode
}, this.language)
.then(() => {
this.detail = {
...this.detail,
status: 99,
statusDesc: this.t('statusFinished')
statusDesc: this.$tr('已结束')
}
this.syncCanceledDetailToCache()
uni.showToast({
title: this.t('cancelOrderSuccess'),
title: this.$tr('申请已取消'),
icon: 'success'
})
})
})
.catch((error) => {
uni.showToast({
title: (error && (error.message || error.msg)) || this.t('cancelOrderFailed'),
title: (error && (error.message || error.msg)) || this.$tr('取消申请失败'),
icon: 'none'
})
})
@@ -782,7 +846,7 @@ export default {
goToBillPage() {
if (!this.detail.detailCode) {
uni.showToast({
title: this.t('missingBillCode'),
title: this.$tr('申请单编号缺失'),
icon: 'none'
})
return
@@ -810,7 +874,7 @@ export default {
const contractNo = String(this.detail && this.detail.reservationCurrentNode && this.detail.reservationCurrentNode.refNo || '').trim()
if (!contractNo) {
uni.showToast({
title: this.t('missingContractNo'),
title: this.$tr('合同编号缺失'),
icon: 'none'
})
return
@@ -886,6 +950,85 @@ export default {
border: 1px solid rgba(59, 130, 246, 0.14);
}
.payment-status-panel {
padding: 24rpx 28rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
border: 1rpx solid transparent;
}
.payment-status-panel__label {
flex-shrink: 0;
font-size: 24rpx;
color: #64748B;
font-weight: 600;
}
.payment-status-panel__main {
min-width: 0;
text-align: right;
}
.payment-status-panel__value {
font-size: 34rpx;
line-height: 1.2;
font-weight: 700;
}
.payment-status-panel__hint {
margin-top: 8rpx;
font-size: 22rpx;
color: #9A3412;
}
.payment-status-panel--warning {
background: linear-gradient(180deg, rgba(255, 247, 237, 0.96) 0%, rgba(255, 251, 245, 0.98) 100%);
border-color: rgba(249, 115, 22, 0.16);
}
.payment-status-panel--warning .payment-status-panel__value {
color: #C2410C;
}
.payment-status-panel--success {
background: linear-gradient(180deg, rgba(236, 253, 245, 0.96) 0%, rgba(240, 253, 250, 0.98) 100%);
border-color: rgba(16, 185, 129, 0.16);
}
.payment-status-panel--success .payment-status-panel__value {
color: #047857;
}
.payment-status-panel--info {
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
border-color: rgba(59, 130, 246, 0.14);
}
.payment-status-panel--info .payment-status-panel__value {
color: #1D4ED8;
}
.payment-status-panel--danger {
background: linear-gradient(180deg, rgba(254, 242, 242, 0.96) 0%, rgba(255, 245, 245, 0.98) 100%);
border-color: rgba(239, 68, 68, 0.16);
}
.payment-status-panel--danger .payment-status-panel__value {
color: #B91C1C;
}
.payment-status-panel--neutral {
background: linear-gradient(180deg, rgba(241, 245, 249, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
border-color: rgba(148, 163, 184, 0.16);
}
.payment-status-panel--neutral .payment-status-panel__value {
color: #475569;
}
.status-reminder-panel__icon {
width: 68rpx;
height: 68rpx;

View File

@@ -1,24 +1,24 @@
<template>
<view class="bill-panel">
<view class="hero-panel tn-margin-bottom-lg">
<view class="hero-title tn-margin-top">{{ t('heroTitle') }}</view>
<view class="hero-title tn-margin-top">{{ $tr('待支付账单') }}</view>
<view class="hero-desc tn-margin-top-sm">{{ heroDesc }}</view>
<view v-if="currentReferenceTypeNo" class="source-tag tn-margin-top-sm">{{ t('applicationNoPrefix') }}{{ currentReferenceTypeNo }}</view>
<view v-if="currentReferenceTypeNo" class="source-tag tn-margin-top-sm">{{ $tr('申请单号:') }}{{ currentReferenceTypeNo }}</view>
<view class="hero-summary tn-margin-top-lg">
<view class="summary-card">
<view class="summary-label">{{ t('pendingCountLabel') }}</view>
<view class="summary-label">{{ $tr('待支付数量') }}</view>
<view class="summary-value tn-margin-top-sm">{{ billList.length }}</view>
</view>
<view class="summary-card">
<view class="summary-label">{{ t('pendingAmountLabel') }}</view>
<view class="summary-label">{{ $tr('待支付总额') }}</view>
<view class="summary-value tn-margin-top-sm">¥{{ formatMoney(totalRemainingAmount) }}</view>
</view>
</view>
</view>
<view v-if="loading || resolvingReference" 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 class="empty-title">{{ $tr('账单加载中') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ $tr('正在同步您的待支付账单,请稍候。') }}</view>
</view>
<view v-else-if="billList.length">
@@ -39,64 +39,64 @@
<view class="card-body tn-margin-top">
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('relatedBusinessNoLabel') }}</text>
<text class="info-label">{{ $tr('关联业务编号') }}</text>
<text class="info-value">{{ getBillReferenceTypeNo(item) || '--' }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('totalChargeLabel') }}</text>
<text class="info-label">{{ $tr('应收合计') }}</text>
<text class="info-value">¥{{ formatMoney(getBillTotalCharge(item)) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('paidAmountLabel') }}</text>
<text class="info-label">{{ $tr('已付金额') }}</text>
<text class="info-value">¥{{ formatMoney(getBillPaidAmount(item)) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('remainingAmountLabel') }}</text>
<text class="info-label">{{ $tr('待付金额') }}</text>
<text class="info-value info-value--highlight">¥{{ formatMoney(getBillRemainingAmount(item)) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('refundStatusLabel') }}</text>
<text class="info-label">{{ $tr('退款状态') }}</text>
<text class="info-value">{{ getRefundStatusText(getBillRefundStatus(item)) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('createTimeLabel') }}</text>
<text class="info-label">{{ $tr('创建时间') }}</text>
<text class="info-value">{{ formatDateTime(getBillCreateTime(item)) }}</text>
</view>
</view>
<view v-if="getBillRemark(item) || getBillResult(item)" class="remark-panel tn-margin-top">
<view v-if="getBillRemark(item)" class="remark-line">
<text class="remark-label">{{ t('remarkLabel') }}</text>
<text class="remark-label">{{ $tr('备注') }}</text>
<text class="remark-text">{{ getBillRemark(item) }}</text>
</view>
<view v-if="getBillResult(item)" class="remark-line tn-margin-top-sm">
<text class="remark-label">{{ t('resultLabel') }}</text>
<text class="remark-label">{{ $tr('处理结果') }}</text>
<text class="remark-text">{{ getBillResult(item) }}</text>
</view>
</view>
<view v-if="getPendingPaymentOrder(item)" class="pending-payment-panel tn-margin-top">
<view class="pending-payment-title">{{ t('pendingPaymentTitle') }}</view>
<view class="pending-payment-title">{{ $tr('未完成支付订单') }}</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('pendingPaymentOrderNoLabel') }}</text>
<text class="info-label">{{ $tr('支付单号') }}</text>
<text class="info-value">{{ getPendingPaymentOrderNo(item) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('pendingPaymentAmountLabel') }}</text>
<text class="info-label">{{ $tr('订单金额') }}</text>
<text class="info-value info-value--highlight">¥{{ formatMoney(getPendingPaymentAmount(item)) }}</text>
</view>
<view v-if="getPendingPaymentExpireText(item)" class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('pendingPaymentExpireLabel') }}</text>
<text class="info-label">{{ $tr('剩余支付时间') }}</text>
<text class="info-value" :class="{ 'info-value--expired': isPendingPaymentExpired(item) }">
{{ getPendingPaymentExpireText(item) }}
</text>
</view>
<view v-if="getPendingPaymentDescription(item)" class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('pendingPaymentDescLabel') }}</text>
<text class="info-label">{{ $tr('支付说明') }}</text>
<text class="info-value">{{ getPendingPaymentDescription(item) }}</text>
</view>
<view v-if="getPendingPaymentChannel(item)" class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('pendingPaymentChannelLabel') }}</text>
<text class="info-label">{{ $tr('支付渠道') }}</text>
<text class="info-value">{{ getPendingPaymentChannel(item) }}</text>
</view>
</view>
@@ -124,10 +124,10 @@
</view>
<view v-else class="empty-panel tn-padding-lg">
<view class="empty-title">{{ currentReferenceTypeNo ? t('emptyPendingTitle') : t('emptyApplicationTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ currentReferenceTypeNo ? t('emptyPendingDesc') : t('emptyApplicationDesc') }}</view>
<view class="empty-title">{{ currentReferenceTypeNo ? $tr('暂无待支付账单') : $tr('暂无可查询申请单') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ currentReferenceTypeNo ? $tr('当前申请单下没有查询到待支付账单,后续有新账单时会在这里展示。') : $tr('当前未找到可关联的申请单,请先前往“我的申请”选择具体申请后查看账单。') }}</view>
<view v-if="!currentReferenceTypeNo" class="empty-action tn-margin-top-lg" @tap="goToMyApplication">
<text class="tn-text-bold">{{ t('goToMyApplication') }}</text>
<text class="tn-text-bold">{{ $tr('前往我的申请') }}</text>
</view>
</view>
@@ -197,7 +197,7 @@
<view v-if="isPaymentWaitingStage" class="payment-progress-card">
<view class="payment-progress-head tn-flex tn-flex-row-between tn-flex-col-center">
<text class="payment-progress-label">{{ t('paymentProgressLabel') }}</text>
<text class="payment-progress-label">{{ $tr('结果确认进度') }}</text>
<text class="payment-progress-value">{{ paymentProgressText }}</text>
</view>
<view class="payment-progress-track">
@@ -208,11 +208,11 @@
<view class="payment-summary-grid">
<view class="payment-summary-item">
<view class="payment-summary-label">{{ t('billNoLabel') }}</view>
<view class="payment-summary-label">{{ $tr('账单号') }}</view>
<view class="payment-summary-value payment-summary-value--mono">{{ paymentWaitingBillNo || '--' }}</view>
</view>
<view class="payment-summary-item">
<view class="payment-summary-label">{{ t('currentPaymentLabel') }}</view>
<view class="payment-summary-label">{{ $tr('本次支付') }}</view>
<view class="payment-summary-value payment-summary-value--price">¥{{ paymentWaitingAmountText }}</view>
</view>
</view>
@@ -241,7 +241,7 @@ import {
getAccountBillRefundStatusLabel,
getAccountReferenceTypeLabel
} from '@/utils/enum-store.js'
import { getActiveApplicationCode, getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
import { getActiveApplicationCode, getMyApplicationList, getSessionSnapshot, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间:|意向房型:|意向区域:|viewing request|viewing time|preferred room type|preferred area/i
@@ -380,212 +380,6 @@ export default {
lines: [],
confirmText: '',
cancelText: ''
},
i18n: {
zh: {
heroTitle: '待支付账单',
applicationNoPrefix: '申请单号:',
pendingCountLabel: '待支付数量',
pendingAmountLabel: '待支付总额',
loadingTitle: '账单加载中',
loadingDesc: '正在同步您的待支付账单,请稍候。',
relatedBusinessNoLabel: '关联业务编号',
totalChargeLabel: '应收合计',
paidAmountLabel: '已付金额',
remainingAmountLabel: '待付金额',
refundStatusLabel: '退款状态',
createTimeLabel: '创建时间',
remarkLabel: '备注',
resultLabel: '处理结果',
pendingPaymentTitle: '未完成支付订单',
pendingPaymentOrderNoLabel: '支付单号',
pendingPaymentAmountLabel: '订单金额',
pendingPaymentDescLabel: '支付说明',
pendingPaymentChannelLabel: '支付渠道',
pendingPaymentExpireLabel: '剩余支付时间',
pendingPaymentExpired: '已失效',
emptyPendingTitle: '暂无待支付账单',
emptyApplicationTitle: '暂无可查询申请单',
emptyPendingDesc: '当前申请单下没有查询到待支付账单,后续有新账单时会在这里展示。',
emptyApplicationDesc: '当前未找到可关联的申请单,请先前往“我的申请”选择具体申请后查看账单。',
goToMyApplication: '前往我的申请',
paymentProgressLabel: '结果确认进度',
billNoLabel: '账单号',
currentPaymentLabel: '本次支付',
heroDescResolved: '当前页面已按申请单编号查询其关联的待支付账单记录。',
heroDescAutoResolve: '当前将优先读取最近申请单的编号来查询关联账单。',
paymentStageLaunching: '正在唤起微信收银台',
paymentStagePolling: '正在同步支付结果',
paymentStageSuccess: '支付结果已确认',
paymentStageFailed: '支付未完成',
paymentStageDefault: '支付处理中',
paymentTitleLaunching: '请完成微信支付',
paymentTitlePolling: '正在确认是否支付成功',
paymentTitleSuccess: '支付成功',
paymentTitleFailed: '支付结果确认失败',
paymentTitleDefault: '支付处理中',
paymentDescLaunching: '系统正在调起微信支付,请在微信收银台中完成付款后返回。',
paymentDescPolling: '支付完成后,系统会自动向后台查询支付状态,无需手动刷新。',
paymentDescSuccess: '本次支付已同步完成,账单状态已更新。',
paymentDescFailed: '系统暂时未能确认支付成功,您可以查看提示后稍后重试。',
paymentProgressWaiting: '等待支付完成',
paymentHintLaunching: '已为您准备好支付信息,等待微信支付完成。',
paymentHintPolling: '正在与后台核对支付状态,请稍候。',
paymentTipsTitleFailed: '处理建议',
paymentTipsTitleSuccess: '完成说明',
paymentTipsTitleDefault: '温馨提示',
paymentTipsFailed1: '若您已完成付款,请稍后刷新账单,避免重复支付。',
paymentTipsFailed2: '若微信侧已扣款但页面未更新,请联系管家或客服协助核实。',
paymentTipsFailed3: '若未扣款,可返回账单列表后重新发起支付。',
paymentTipsSuccess1: '若当前申请单还有其他待付账单,可继续在当前页完成支付。',
paymentTipsSuccess2: '若当前账单已全部支付完成,系统会自动返回首页。',
paymentTipsSuccess3: '支付记录同步可能略有延迟,刷新后即可看到最新状态。',
paymentTipsDefault1: '支付成功后,系统可能需要几秒钟同步账单状态。',
paymentTipsDefault2: '请不要重复点击支付,也不要提前关闭当前页面。',
paymentTipsDefault3: '如果超时未完成,系统会自动再校验一次账单状态。',
paymentActionAcknowledge: '我知道了',
buttonConfirming: '确认结果中...',
buttonPolling: '查询结果中({current}/{total})',
buttonLaunching: '拉起支付中...',
wechatPayButton: '微信支付',
continuePayButton: '继续支付',
paymentStatusPending: '处理中',
billRecordFallback: '账单记录',
paymentParamIncomplete: '微信支付参数不完整',
wechatCodeFailed: '获取微信登录凭证失败',
billPaymentDesc: '账单支付',
paymentFailedHint: '暂未查询到支付成功结果,请稍后刷新账单确认。',
paymentResultSuccess: '后台确认支付成功',
paymentResultFailed: '后台返回支付失败',
paymentResultPending: '后台仍在处理中',
paymentHintPaidWaiting: '已支付成功,正在等待后台确认账单状态。',
paymentFailedError: '支付失败',
paymentTimeoutError: '支付结果确认超时,请稍后刷新账单确认',
paymentSuccessPendingBills: '支付结果已确认,当前申请单仍有其他待支付账单。',
paymentSuccessAllDone: '支付结果已确认,当前申请单账单已全部完成。',
billInfoIncomplete: '账单信息不完整',
paymentNoticeTitle: '用户须知',
paymentNoticeBillLabel: '账单号',
paymentNoticeAmountLabel: '支付金额',
paymentNoticeItemLabel: '费用项目',
paymentNoticeLine1: '请确认本次支付账单和金额无误后再继续付款。',
paymentNoticeLine2: '支付完成后,账单状态可能需要几秒同步,请勿重复支付。',
paymentNoticeLine3: '若支付中断或页面关闭,可稍后返回账单页查看最新结果。',
paymentNoticeConfirm: '继续支付',
paymentNoticeCancel: '稍后再付',
paymentHintLaunchingToast: '正在唤起微信支付,请在微信内完成付款。',
paymentCanceled: '已取消支付',
paymentResultFailedRetry: '支付结果确认失败,请稍后刷新账单确认',
billLoadFailed: '账单加载失败',
cancelOrderButton: '取消订单',
cancelOrderLoading: '取消中...',
cancelOrderTitle: '取消订单',
cancelOrderContent: '取消后将关闭当前未支付订单,是否继续?',
cancelOrderSuccess: '订单已取消',
cancelOrderFailed: '取消订单失败'
},
en: {
heroTitle: 'Pending Bills',
applicationNoPrefix: 'Application No: ',
pendingCountLabel: 'Pending Count',
pendingAmountLabel: 'Pending Amount',
loadingTitle: 'Loading Bills',
loadingDesc: 'Syncing your pending bills, please wait.',
relatedBusinessNoLabel: 'Related Business No.',
totalChargeLabel: 'Total Charge',
paidAmountLabel: 'Paid Amount',
remainingAmountLabel: 'Remaining Amount',
refundStatusLabel: 'Refund Status',
createTimeLabel: 'Created At',
remarkLabel: 'Remark',
resultLabel: 'Result',
pendingPaymentTitle: 'Unfinished Payment',
pendingPaymentOrderNoLabel: 'Payment Order No.',
pendingPaymentAmountLabel: 'Order Amount',
pendingPaymentDescLabel: 'Description',
pendingPaymentChannelLabel: 'Channel',
pendingPaymentExpireLabel: 'Time Left',
pendingPaymentExpired: 'Expired',
emptyPendingTitle: 'No Pending Bills',
emptyApplicationTitle: 'No Application Available',
emptyPendingDesc: 'No pending bills are found under the current application. New bills will appear here later.',
emptyApplicationDesc: 'No related application is found. Please go to "My Applications" and select an application first.',
goToMyApplication: 'Go to My Applications',
paymentProgressLabel: 'Confirmation Progress',
billNoLabel: 'Bill No.',
currentPaymentLabel: 'This Payment',
heroDescResolved: 'This page is querying pending bills by the current application number.',
heroDescAutoResolve: 'The latest application number will be used first to query related bills.',
paymentStageLaunching: 'Launching Wechat Pay',
paymentStagePolling: 'Syncing Payment Result',
paymentStageSuccess: 'Payment Confirmed',
paymentStageFailed: 'Payment Incomplete',
paymentStageDefault: 'Processing Payment',
paymentTitleLaunching: 'Complete Wechat Pay',
paymentTitlePolling: 'Confirming Payment Result',
paymentTitleSuccess: 'Payment Successful',
paymentTitleFailed: 'Payment Confirmation Failed',
paymentTitleDefault: 'Processing Payment',
paymentDescLaunching: 'Wechat Pay is being launched. Complete the payment in Wechat and return here.',
paymentDescPolling: 'After payment, the system checks the payment status automatically. No manual refresh is needed.',
paymentDescSuccess: 'This payment has been synchronized and the bill status is updated.',
paymentDescFailed: 'The system cannot confirm the payment for now. Please review the tips and try again later.',
paymentProgressWaiting: 'Waiting for Payment',
paymentHintLaunching: 'Payment information is ready. Waiting for Wechat payment to complete.',
paymentHintPolling: 'Checking payment status with the backend, please wait.',
paymentTipsTitleFailed: 'Suggestions',
paymentTipsTitleSuccess: 'Completion Notes',
paymentTipsTitleDefault: 'Tips',
paymentTipsFailed1: 'If you have completed the payment, refresh the bill later to avoid duplicate payment.',
paymentTipsFailed2: 'If Wechat has deducted the amount but the page is not updated, contact your manager or support for verification.',
paymentTipsFailed3: 'If no deduction occurred, return to the bill list and try again.',
paymentTipsSuccess1: 'If there are other pending bills under this application, you can continue paying on this page.',
paymentTipsSuccess2: 'If all bills are paid, the system returns to the home page automatically.',
paymentTipsSuccess3: 'Payment record synchronization may be slightly delayed. Refresh to see the latest status.',
paymentTipsDefault1: 'After successful payment, the system may need a few seconds to sync the bill status.',
paymentTipsDefault2: 'Do not tap the pay button repeatedly or close this page too early.',
paymentTipsDefault3: 'If the process times out, the system checks the bill status again automatically.',
paymentActionAcknowledge: 'Got It',
buttonConfirming: 'Confirming...',
buttonPolling: 'Checking ({current}/{total})',
buttonLaunching: 'Launching...',
wechatPayButton: 'Wechat Pay',
continuePayButton: 'Continue Pay',
paymentStatusPending: 'Processing',
billRecordFallback: 'Bill Record',
paymentParamIncomplete: 'Incomplete Wechat payment parameters',
wechatCodeFailed: 'Failed to get Wechat login code',
billPaymentDesc: 'Bill Payment',
paymentFailedHint: 'Payment success is not confirmed yet. Please refresh the bill later.',
paymentResultSuccess: 'Backend confirmed payment success',
paymentResultFailed: 'Backend returned payment failure',
paymentResultPending: 'Backend is still processing',
paymentHintPaidWaiting: 'Payment succeeded. Waiting for backend confirmation of the bill status.',
paymentFailedError: 'Payment failed',
paymentTimeoutError: 'Payment confirmation timed out. Please refresh the bill later.',
paymentSuccessPendingBills: 'Payment is confirmed. There are still other unpaid bills under this application.',
paymentSuccessAllDone: 'Payment is confirmed. All bills under this application are completed.',
billInfoIncomplete: 'Incomplete bill information',
paymentNoticeTitle: 'Payment Notice',
paymentNoticeBillLabel: 'Bill No.',
paymentNoticeAmountLabel: 'Amount',
paymentNoticeItemLabel: 'Charge Item',
paymentNoticeLine1: 'Please confirm the bill and payment amount before continuing.',
paymentNoticeLine2: 'After payment, the bill status may take a few seconds to sync. Please do not pay repeatedly.',
paymentNoticeLine3: 'If the payment is interrupted or the page is closed, return to the bill page later to check the latest result.',
paymentNoticeConfirm: 'Continue',
paymentNoticeCancel: 'Later',
paymentHintLaunchingToast: 'Launching Wechat Pay. Please complete the payment in Wechat.',
paymentCanceled: 'Payment canceled',
paymentResultFailedRetry: 'Payment confirmation failed. Please refresh the bill later.',
billLoadFailed: 'Failed to load bills',
cancelOrderButton: 'Cancel Order',
cancelOrderLoading: 'Canceling...',
cancelOrderTitle: 'Cancel Order',
cancelOrderContent: 'This will close the current unpaid order. Continue?',
cancelOrderSuccess: 'Order canceled',
cancelOrderFailed: 'Failed to cancel order'
}
}
}
},
@@ -600,9 +394,9 @@ export default {
},
heroDesc() {
if (this.currentReferenceTypeNo) {
return this.t('heroDescResolved')
return this.$tr('当前页面已按申请单编号查询其关联的待支付账单记录。')
}
return this.t('heroDescAutoResolve')
return this.$tr('当前将优先读取最近申请单的编号来查询关联账单。')
},
showPaymentFeedbackPanel() {
return Boolean(this.paymentStage)
@@ -627,18 +421,18 @@ export default {
},
paymentStageLabel() {
if (this.paymentStage === 'launching') {
return this.t('paymentStageLaunching')
return this.$tr('正在唤起微信收银台')
}
if (this.paymentStage === 'polling') {
return this.t('paymentStagePolling')
return this.$tr('正在同步支付结果')
}
if (this.paymentStage === 'success') {
return this.t('paymentStageSuccess')
return this.$tr('支付结果已确认')
}
if (this.paymentStage === 'failed') {
return this.t('paymentStageFailed')
return this.$tr('支付未完成')
}
return this.t('paymentStageDefault')
return this.$tr('支付处理中')
},
paymentBadgeIconClass() {
if (this.isPaymentSuccessStage) {
@@ -660,31 +454,31 @@ export default {
},
paymentWaitingTitle() {
if (this.paymentStage === 'launching') {
return this.t('paymentTitleLaunching')
return this.$tr('请完成微信支付')
}
if (this.paymentStage === 'polling') {
return this.t('paymentTitlePolling')
return this.$tr('正在确认是否支付成功')
}
if (this.paymentStage === 'success') {
return this.t('paymentTitleSuccess')
return this.$tr('支付成功')
}
if (this.paymentStage === 'failed') {
return this.t('paymentTitleFailed')
return this.$tr('支付结果确认失败')
}
return this.t('paymentTitleDefault')
return this.$tr('支付处理中')
},
paymentWaitingDesc() {
if (this.paymentStage === 'launching') {
return this.t('paymentDescLaunching')
return this.$tr('系统正在调起微信支付,请在微信收银台中完成付款后返回。')
}
if (this.paymentStage === 'polling') {
return this.t('paymentDescPolling')
return this.$tr('支付完成后,系统会自动向后台查询支付状态,无需手动刷新。')
}
if (this.paymentStage === 'success') {
return this.t('paymentDescSuccess')
return this.$tr('本次支付已同步完成,账单状态已更新。')
}
if (this.paymentStage === 'failed') {
return this.t('paymentDescFailed')
return this.$tr('系统暂时未能确认支付成功,您可以查看提示后稍后重试。')
}
return ''
},
@@ -704,7 +498,7 @@ export default {
},
paymentProgressText() {
if (this.paymentStage !== 'polling') {
return this.t('paymentProgressWaiting')
return this.$tr('等待支付完成')
}
const current = this.paymentPollingAttempt > 0 ? this.paymentPollingAttempt : 1
return `${current}/${PAYMENT_QUERY_MAX_ATTEMPTS}`
@@ -714,45 +508,45 @@ export default {
return this.paymentStatusHint
}
if (this.paymentStage === 'launching') {
return this.t('paymentHintLaunching')
return this.$tr('已为您准备好支付信息,等待微信支付完成。')
}
return this.t('paymentHintPolling')
return this.$tr('正在与后台核对支付状态,请稍候。')
},
paymentTipsTitle() {
if (this.isPaymentFailedStage) {
return this.t('paymentTipsTitleFailed')
return this.$tr('处理建议')
}
if (this.isPaymentSuccessStage) {
return this.t('paymentTipsTitleSuccess')
return this.$tr('完成说明')
}
return this.t('paymentTipsTitleDefault')
return this.$tr('温馨提示')
},
paymentTips() {
if (this.isPaymentFailedStage) {
return [
this.t('paymentTipsFailed1'),
this.t('paymentTipsFailed2'),
this.t('paymentTipsFailed3')
this.$tr('若您已完成付款,请稍后刷新账单,避免重复支付。'),
this.$tr('若微信侧已扣款但页面未更新,请联系管家或客服协助核实。'),
this.$tr('若未扣款,可返回账单列表后重新发起支付。')
]
}
if (this.isPaymentSuccessStage) {
return [
this.t('paymentTipsSuccess1'),
this.t('paymentTipsSuccess2'),
this.t('paymentTipsSuccess3')
this.$tr('若当前申请单还有其他待付账单,可继续在当前页完成支付。'),
this.$tr('若当前账单已全部支付完成,系统会自动返回首页。'),
this.$tr('支付记录同步可能略有延迟,刷新后即可看到最新状态。')
]
}
return [
this.t('paymentTipsDefault1'),
this.t('paymentTipsDefault2'),
this.t('paymentTipsDefault3')
this.$tr('支付成功后,系统可能需要几秒钟同步账单状态。'),
this.$tr('请不要重复点击支付,也不要提前关闭当前页面。'),
this.$tr('如果超时未完成,系统会自动再校验一次账单状态。')
]
},
showPaymentActionButton() {
return this.isPaymentFailedStage
},
paymentActionText() {
return this.isPaymentFailedStage ? this.t('paymentActionAcknowledge') : ''
return this.isPaymentFailedStage ? this.$tr('我知道了') : ''
}
},
watch: {
@@ -782,22 +576,6 @@ export default {
this.clearCountdownTimer()
},
methods: {
t(key, params) {
const translatedText = this.$tm('pkgService.components.CurrentApplicationBillPanel', key, params)
if (translatedText && translatedText !== key) {
return translatedText
}
const languagePack = this.i18n && this.i18n[this.language] ? this.i18n[this.language] : this.i18n.zh
const fallbackText = languagePack && languagePack[key] ? languagePack[key] : key
if (!params || typeof fallbackText !== 'string') {
return fallbackText
}
return fallbackText.replace(/\{(\w+)\}/g, (match, token) => {
return Object.prototype.hasOwnProperty.call(params, token) ? params[token] : match
})
},
getBillNo(item = {}) {
return item.no || item.billNo || ''
},
@@ -812,7 +590,7 @@ export default {
},
getBillPaymentStatusText(item = {}) {
const translatedStatus = this.getPaymentStatusText(this.getBillPaymentStatus(item))
return translatedStatus || item.paymentStatusDesc || this.t('paymentStatusPending')
return translatedStatus || item.paymentStatusDesc || this.$tr('处理中')
},
getBillRefundStatus(item = {}) {
return item.refundStatus
@@ -918,7 +696,7 @@ export default {
return ''
}
if (expireTimestamp <= this.countdownNow) {
return this.t('pendingPaymentExpired')
return this.$tr('已失效')
}
return this.formatPendingPaymentCountdown(this.getPendingPaymentRemainingSeconds(item))
},
@@ -926,20 +704,20 @@ export default {
const billNo = this.getBillNo(item)
if (this.paymentPollingBillNo && this.paymentPollingBillNo === billNo) {
if (!this.paymentPollingAttempt) {
return this.t('buttonConfirming')
return this.$tr('确认结果中...')
}
return this.t('buttonPolling', {
return this.$tr('查询结果中({current}/{total})', {
current: this.paymentPollingAttempt,
total: PAYMENT_QUERY_MAX_ATTEMPTS
})
}
if (this.payingBillNo === billNo) {
return this.paymentStage === 'launching' ? this.t('buttonLaunching') : this.t('buttonConfirming')
return this.paymentStage === 'launching' ? this.$tr('拉起支付中...') : this.$tr('确认结果中...')
}
if (this.getPendingPaymentOrder(item) && !this.isPendingPaymentExpired(item)) {
return this.t('continuePayButton')
return this.$tr('继续支付')
}
return this.t('wechatPayButton')
return this.$tr('微信支付')
},
canCancelOrder(item = {}) {
if (!this.currentReferenceTypeNo) {
@@ -951,8 +729,8 @@ export default {
},
getCancelButtonText(item = {}) {
return this.cancelingBillNo === this.getBillNo(item)
? this.t('cancelOrderLoading')
: this.t('cancelOrderButton')
? this.$tr('取消中...')
: this.$tr('取消申请')
},
resolveReferenceTypeNo(item = {}) {
return item.code || item.detailCode || ''
@@ -977,6 +755,19 @@ export default {
return Promise.resolve('')
}
const session = getSessionSnapshot()
const sessionUser = session && session.currentUser ? session.currentUser : {}
const currentFlowRefNo = String(
sessionUser.currentReservationRefNo
|| (session && session.resolvedUser && session.resolvedUser.currentReservationRefNo)
|| ''
).trim()
if (currentFlowRefNo) {
this.currentReferenceTypeNo = currentFlowRefNo
saveActiveApplicationCode(currentFlowRefNo)
return Promise.resolve(this.currentReferenceTypeNo)
}
const cachedActiveCode = getActiveApplicationCode()
if (cachedActiveCode) {
this.currentReferenceTypeNo = cachedActiveCode
@@ -1024,7 +815,7 @@ export default {
return text.length > 16 ? text.slice(0, 16) : text
},
getPaymentStatusText(status) {
return getAccountBillPaymentStatusLabel(status, this.t('paymentStatusPending'))
return getAccountBillPaymentStatusLabel(status, this.$tr('处理中'))
},
resolvePaymentStatusType(status) {
if (Number(status) === ACCOUNT_BILL_PAYMENT_STATUS.PAID) {
@@ -1040,7 +831,7 @@ export default {
},
getReferenceTypeText(item = {}) {
const translatedType = getAccountReferenceTypeLabel(item.referenceType, '')
return translatedType || item.referenceTypeDesc || this.t('billRecordFallback')
return translatedType || item.referenceTypeDesc || this.$tr('账单记录')
},
parsePaymentInfo(paymentInfo) {
if (!paymentInfo) {
@@ -1073,7 +864,7 @@ export default {
requestWechatPayment(paymentInfo) {
const payOptions = this.normalizeWechatPayOptions(paymentInfo)
if (!payOptions.timeStamp || !payOptions.nonceStr || !payOptions.package || !payOptions.paySign) {
return Promise.reject(new Error(this.t('paymentParamIncomplete')))
return Promise.reject(new Error(this.$tr('微信支付参数不完整')))
}
return new Promise((resolve, reject) => {
@@ -1143,13 +934,13 @@ export default {
success: (res) => {
const code = res && res.code ? res.code : ''
if (!code) {
reject(new Error(this.t('wechatCodeFailed')))
reject(new Error(this.$tr('获取微信登录凭证失败')))
return
}
resolve(code)
},
fail: (error) => {
reject(error || new Error(this.t('wechatCodeFailed')))
reject(error || new Error(this.$tr('获取微信登录凭证失败')))
}
})
})
@@ -1160,7 +951,7 @@ export default {
billNo: this.getBillNo(item),
payMethod: 'Wechat',
amount: Number(this.getBillRemainingAmount(item) || 0),
description: this.getReferenceTypeText(item) || this.t('billPaymentDesc')
description: this.getReferenceTypeText(item) || this.$tr('账单支付')
}
if (wechatCode) {
payload.ExtraInfo = wechatCode
@@ -1168,10 +959,6 @@ export default {
return payload
},
resolveNoticeText(key, zhText, enText) {
const translatedText = this.t(key)
if (translatedText && translatedText !== key) {
return translatedText
}
return this.language === 'en' ? enText : zhText
},
resetPaymentNoticeContext() {
@@ -1237,8 +1024,8 @@ export default {
confirmCancelOrder() {
return new Promise((resolve) => {
uni.showModal({
title: this.t('cancelOrderTitle'),
content: this.t('cancelOrderContent'),
title: this.$tr('取消申请'),
content: this.$tr('取消后将关闭当前申请单,是否继续?'),
confirmColor: '#ef4444',
success: (res) => resolve(!!(res && res.confirm)),
fail: () => resolve(false)
@@ -1246,25 +1033,15 @@ export default {
})
},
buildCancelOrderPayload(item = {}) {
const pendingOrder = this.getPendingPaymentOrder(item) || {}
const billNo = this.getBillNo(item)
const paymentNo = pendingOrder.paymentNo || pendingOrder.payNo || pendingOrder.payRequestNo || ''
return {
detailCode: this.currentReferenceTypeNo,
code: this.currentReferenceTypeNo,
referenceTypeNo: this.currentReferenceTypeNo,
billNo,
BillNo: billNo,
paymentNo,
payNo: paymentNo,
payRequestNo: pendingOrder.payRequestNo || ''
detailCode: this.currentReferenceTypeNo
}
},
createPaymentSession(item = {}) {
this.paymentSession = {
billNo: this.getBillNo(item),
amount: Number(this.getBillRemainingAmount(item) || 0),
title: this.getReferenceTypeText(item) || this.t('billPaymentDesc')
title: this.getReferenceTypeText(item) || this.$tr('账单支付')
}
},
resetPaymentWaitingState() {
@@ -1277,7 +1054,7 @@ export default {
},
openPaymentFailedState(message = '') {
this.paymentStage = 'failed'
this.paymentStatusHint = message || this.t('paymentFailedHint')
this.paymentStatusHint = message || this.$tr('暂未查询到支付成功结果,请稍后刷新账单确认。')
this.payingBillNo = ''
this.paymentPollingBillNo = this.paymentWaitingBillNo
},
@@ -1312,20 +1089,20 @@ export default {
if (payStatus === PAYMENT_ORDER_STATUS.SUCCESS) {
return {
status: PAYMENT_QUERY_STATUS.SUCCESS,
message: statusDesc || this.t('paymentResultSuccess'),
message: statusDesc || this.$tr('后台确认支付成功'),
data: payload
}
}
if (payStatus === PAYMENT_ORDER_STATUS.FAILED) {
return {
status: PAYMENT_QUERY_STATUS.FAILED,
message: statusDesc || this.t('paymentResultFailed'),
message: statusDesc || this.$tr('后台返回支付失败'),
data: payload
}
}
return {
status: PAYMENT_QUERY_STATUS.PENDING,
message: statusDesc || this.t('paymentResultPending'),
message: statusDesc || this.$tr('后台仍在处理中'),
data: payload
}
})
@@ -1337,7 +1114,7 @@ export default {
this.paymentStage = 'polling'
this.paymentPollingBillNo = queryContext.billNo
this.paymentPollingAttempt = 0
this.paymentStatusHint = this.t('paymentHintPaidWaiting')
this.paymentStatusHint = this.$tr('已支付成功,正在等待后台确认账单状态。')
const executePolling = async () => {
for (let attempt = 1; attempt <= PAYMENT_QUERY_MAX_ATTEMPTS; attempt += 1) {
@@ -1346,15 +1123,15 @@ export default {
}
this.paymentPollingAttempt = attempt
lastResult = await this.queryPaymentResult(queryContext)
this.paymentStatusHint = lastResult && lastResult.message ? lastResult.message : this.t('paymentHintPolling')
this.paymentStatusHint = lastResult && lastResult.message ? lastResult.message : this.$tr('正在与后台核对支付状态,请稍候。')
if (lastResult && lastResult.status === PAYMENT_QUERY_STATUS.SUCCESS) {
return lastResult
}
if (lastResult && lastResult.status === PAYMENT_QUERY_STATUS.FAILED) {
throw new Error(lastResult.message || this.t('paymentFailedError'))
throw new Error(lastResult.message || this.$tr('支付失败'))
}
}
throw new Error((lastResult && lastResult.message) || this.t('paymentTimeoutError'))
throw new Error((lastResult && lastResult.message) || this.$tr('支付结果确认超时,请稍后刷新账单确认'))
}
return executePolling()
@@ -1363,8 +1140,8 @@ export default {
const hasPendingBills = this.billList.length > 0
this.paymentStage = 'success'
this.paymentStatusHint = hasPendingBills
? this.t('paymentSuccessPendingBills')
: this.t('paymentSuccessAllDone')
? this.$tr('支付结果已确认,当前申请单仍有其他待支付账单。')
: this.$tr('支付结果已确认,当前申请单账单已全部完成。')
this.payingBillNo = ''
this.paymentPollingBillNo = this.paymentWaitingBillNo
return this.wait(1200)
@@ -1413,7 +1190,7 @@ export default {
: null
if (!billNo || !this.currentReferenceTypeNo) {
uni.showToast({
title: this.t('billInfoIncomplete'),
title: this.$tr('账单信息不完整'),
icon: 'none'
})
return
@@ -1430,7 +1207,7 @@ export default {
}
this.payingBillNo = billNo
this.paymentStage = 'launching'
this.paymentStatusHint = this.t('paymentHintLaunchingToast')
this.paymentStatusHint = this.$tr('正在唤起微信支付,请在微信内完成付款。')
this.createPaymentSession(item)
})
.then(() => {
@@ -1464,7 +1241,7 @@ export default {
if (/cancel/i.test(errMsg)) {
this.resetPaymentWaitingState()
uni.showToast({
title: this.t('paymentCanceled'),
title: this.$tr('已取消支付'),
icon: 'none'
})
return
@@ -1474,7 +1251,7 @@ export default {
if (hasConfirmedSuccess) {
return
}
this.openPaymentFailedState(errMsg || this.t('paymentResultFailedRetry'))
this.openPaymentFailedState(errMsg || this.$tr('支付结果确认失败,请稍后刷新账单确认'))
})
})
.finally(() => {
@@ -1500,7 +1277,7 @@ export default {
return userDetailCancel(this.buildCancelOrderPayload(item), this.language)
.then(() => {
uni.showToast({
title: this.t('cancelOrderSuccess'),
title: this.$tr('申请已取消'),
icon: 'success'
})
return this.refreshBillList()
@@ -1508,7 +1285,7 @@ export default {
})
.catch((error) => {
uni.showToast({
title: (error && (error.message || error.msg)) || this.t('cancelOrderFailed'),
title: (error && (error.message || error.msg)) || this.$tr('取消申请失败'),
icon: 'none'
})
})
@@ -1554,7 +1331,7 @@ export default {
this.billList = []
this.pendingPaymentOrderMap = {}
uni.showToast({
title: (error && (error.message || error.msg)) || this.t('billLoadFailed'),
title: (error && (error.message || error.msg)) || this.$tr('账单加载失败'),
icon: 'none'
})
return []

View File

@@ -1,15 +1,15 @@
<template>
<view class="contract-detail-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>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('合同详情') }}</view>
</tn-nav-bar>
<view class="page-main" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view class="overview-panel">
<view class="overview-header">
<view>
<view class="overview-title">{{ t('overviewTitle') }}</view>
<view class="overview-subtitle">{{ t('contractNoLabel') }}{{ contractNo || t('notAvailable') }}</view>
<view class="overview-title">{{ contractDisplayName || $tr('合同影像') }}</view>
<view class="overview-subtitle">{{ $tr('合同编号:') }}{{ contractNo || $tr('未获取') }}</view>
</view>
<view class="overview-badge">
<text class="tn-icon-read"></text>
@@ -19,26 +19,26 @@
<view class="overview-tips">
<view class="tip-chip">
<text class="tn-icon-safe-fill"></text>
<text>{{ t('signedChip') }}</text>
<text>{{ $tr('已签合同') }}</text>
</view>
<view class="tip-chip">
<text class="tn-icon-charts-ring"></text>
<text>{{ t('zoomSupportedChip') }}</text>
<text>{{ $tr('支持缩放') }}</text>
</view>
<view class="tip-chip" v-if="imageUrl">
<text class="tn-icon-image-text-fill"></text>
<text>{{ t('imageModeChip') }}</text>
<text>{{ $tr('A4 影像模式') }}</text>
</view>
<view class="tip-chip" v-else-if="contractContent">
<text class="tn-icon-text-new"></text>
<text>{{ t('textModeChip') }}</text>
<text>{{ $tr('正文预览模式') }}</text>
</view>
</view>
</view>
<view v-if="loading" class="empty-panel">
<view class="empty-title">{{ t('loadingTitle') }}</view>
<view class="empty-desc">{{ t('loadingDesc') }}</view>
<view class="empty-title">{{ $tr('合同加载中') }}</view>
<view class="empty-desc">{{ $tr('正在获取合同影像与正文,请稍候。') }}</view>
</view>
<view v-else-if="imageUrl || contractContent" class="preview-panel">
@@ -72,8 +72,8 @@
<view class="empty-icon">
<text class="tn-icon-folder"></text>
</view>
<view class="empty-title">{{ t('emptyTitle') }}</view>
<view class="empty-desc">{{ t('emptyDesc') }}</view>
<view class="empty-title">{{ $tr('暂无合同内容') }}</view>
<view class="empty-desc">{{ $tr('页面未带入影像地址,且通过合同编号也未获取到可预览的合同内容。') }}</view>
</view>
</view>
</view>
@@ -83,6 +83,7 @@
import { addFilePrefix } from '@/utils/common.js'
import { contractPreview } from '@/api/index.js'
import { normalizeRichTextImages } from '../../utils/rich-text'
import { fetchContractDisplayInfo } from '../../utils/contract-display'
function resolvePreviewPayload(response) {
const payload = response && response.data ? response.data : response
@@ -100,39 +101,10 @@ function normalizeRichTextContent(content = '') {
}
export default {
i18n: {
zh: {
navTitle: '合同详情',
overviewTitle: '合同影像',
contractNoLabel: '合同编号:',
notAvailable: '未获取',
signedChip: '已签合同',
zoomSupportedChip: '支持缩放',
imageModeChip: 'A4 影像模式',
textModeChip: '正文预览模式',
loadingTitle: '合同加载中',
loadingDesc: '正在获取合同影像与正文,请稍候。',
emptyTitle: '暂无合同内容',
emptyDesc: '页面未带入影像地址,且通过合同编号也未获取到可预览的合同内容。'
},
en: {
navTitle: 'Contract Detail',
overviewTitle: 'Contract Preview',
contractNoLabel: 'Contract No.: ',
notAvailable: 'N/A',
signedChip: 'Signed Contract',
zoomSupportedChip: 'Zoom Supported',
imageModeChip: 'A4 Image Mode',
textModeChip: 'Text Preview Mode',
loadingTitle: 'Loading contract',
loadingDesc: 'Fetching contract images and content. Please wait.',
emptyTitle: 'No contract content',
emptyDesc: 'No previewable contract content was provided by image URL or contract number.'
}
},
data() {
return {
contractNo: '',
contractDisplayName: '',
orderNo: '',
loading: false,
imageUrl: '',
@@ -144,11 +116,23 @@ export default {
this.contractNo = decodeURIComponent(options.no || '')
this.orderNo = decodeURIComponent(options.orderNo || '')
this.imageUrl = addFilePrefix(decodeURIComponent(options.contentImageUrl || ''))
this.fetchContractDisplayName()
this.fetchContractDetail()
},
methods: {
t(key, params) {
return this.$tm('pkgService.contract_detail.contract_detail', key, params)
fetchContractDisplayName() {
if (!this.contractNo) {
return Promise.resolve()
}
return fetchContractDisplayInfo(this.contractNo, this.$getLanguage())
.then(({ contractDisplayName }) => {
this.contractDisplayName = contractDisplayName || ''
})
.catch((error) => {
console.error('fetch contract display name failed', error)
this.contractDisplayName = ''
})
},
fetchContractDetail() {
if (!this.contractNo || (this.imageUrl && this.contractContent)) {

View File

@@ -1,15 +1,15 @@
<template>
<view class="contract-sign-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>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('合同签约') }}</view>
</tn-nav-bar>
<view class="page-main" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view class="overview-panel">
<view class="overview-header">
<view>
<view class="overview-title">{{ t('overviewTitle') }}</view>
<view class="overview-subtitle">{{ t('contractNoLabel') }}{{ contractNo || t('notAvailable') }}</view>
<view class="overview-title">{{ contractDisplayName || $tr('签约前确认') }}</view>
<view class="overview-subtitle">{{ $tr('合同编号:') }}{{ contractNo || $tr('未获取') }}</view>
</view>
<view class="overview-badge">
<text class="tn-icon-edit-write-fill"></text>
@@ -18,13 +18,13 @@
</view>
<view class="overview-meta">
<text class="tn-icon-safe-fill"></text>
<text>{{ t('overviewMeta') }}</text>
<text>{{ $tr('阅读合同正文后完成签名提交,签约结果会同步到我的合同。') }}</text>
</view>
</view>
<view v-if="loading" class="empty-panel">
<view class="empty-title">{{ t('loadingTitle') }}</view>
<view class="empty-desc">{{ t('loadingDesc') }}</view>
<view class="empty-title">{{ $tr('合同加载中') }}</view>
<view class="empty-desc">{{ $tr('正在获取合同正文,请稍候。') }}</view>
</view>
<view v-else-if="contractContent" class="preview-panel">
@@ -38,7 +38,7 @@
<view class="preview-tip" @click="openFullscreenPreview">
<text class="tn-icon-fullscreen"></text>
<text>{{ t('fullscreenPreview') }}</text>
<text>{{ $tr('点击全屏查看') }}</text>
</view>
<view class="zoom-controls" @click.stop>
@@ -56,17 +56,17 @@
<view class="empty-icon">
<text class="tn-icon-folder"></text>
</view>
<view class="empty-title">{{ t('emptyTitle') }}</view>
<view class="empty-desc">{{ t('emptyDesc') }}</view>
<view class="empty-title">{{ $tr('暂无合同正文') }}</view>
<view class="empty-desc">{{ $tr('当前合同暂未返回可签约正文,请稍后再试。') }}</view>
</view>
<view v-if="contractContent" class="footer-bar">
<view class="footer-tip">
<text class="tn-icon-safe-fill"></text>
<text>{{ t('footerTip') }}</text>
<text>{{ $tr('签名前请确认您已完整阅读合同内容') }}</text>
</view>
<view class="action-btn" @click="openSignBoard">
{{ isSubmitting ? t('submitting') : t('signAction') }}
{{ isSubmitting ? $tr('提交中...') : $tr('我已阅读并同意,去签名') }}
</view>
</view>
</view>
@@ -83,9 +83,9 @@
<view class="fullscreen-header">
<view class="fullscreen-back" @click="closeFullscreenPreview">
<text class="tn-icon-left"></text>
<text>{{ t('back') }}</text>
<text>{{ $tr('返回') }}</text>
</view>
<view class="fullscreen-title">{{ t('fullscreenTitle') }}</view>
<view class="fullscreen-title">{{ $tr('合同全文') }}</view>
<view class="fullscreen-placeholder"></view>
</view>
<scroll-view scroll-y scroll-x class="fullscreen-scroll" :enable-flex="true">
@@ -106,6 +106,7 @@ import { contractPreview, contractSign, uploadCommonFile } from '@/api/index.js'
import { syncLoginStayState } from '../../utils/session-sync'
import ContractSignDialog from '../../components/ContractSignDialog.vue'
import { normalizeRichTextImages } from '../../utils/rich-text'
import { fetchContractDisplayInfo } from '../../utils/contract-display'
function normalizeContractContent(content = '') {
return normalizeRichTextImages(
@@ -138,60 +139,13 @@ function resolveUploadedFileUrl(uploadResult) {
}
export default {
i18n: {
zh: {
navTitle: '合同签约',
overviewTitle: '签约前确认',
contractNoLabel: '合同编号:',
notAvailable: '未获取',
overviewMeta: '阅读合同正文后完成签名提交,签约结果会同步到我的合同。',
loadingTitle: '合同加载中',
loadingDesc: '正在获取合同正文,请稍候。',
fullscreenPreview: '点击全屏查看',
emptyTitle: '暂无合同正文',
emptyDesc: '当前合同暂未返回可签约正文,请稍后再试。',
footerTip: '签名前请确认您已完整阅读合同内容',
submitting: '提交中...',
signAction: '我已阅读并同意,去签名',
back: '返回',
fullscreenTitle: '合同全文',
signerMissing: '未获取到签名人姓名',
contractNoMissing: '缺少合同编号',
signingLoading: '签约提交中...',
uploadFailed: '签名上传失败',
signSuccess: '签约成功',
signFailed: '签约失败'
},
en: {
navTitle: 'Contract Signing',
overviewTitle: 'Before Signing',
contractNoLabel: 'Contract No.: ',
notAvailable: 'N/A',
overviewMeta: 'Read the contract content before submitting your signature. The result will sync to My Contracts.',
loadingTitle: 'Loading contract',
loadingDesc: 'Fetching contract content. Please wait.',
fullscreenPreview: 'Open Fullscreen',
emptyTitle: 'No contract content',
emptyDesc: 'No signable contract content is available at the moment. Please try again later.',
footerTip: 'Please confirm that you have fully read the contract before signing',
submitting: 'Submitting...',
signAction: 'I have read and agree. Sign now',
back: 'Back',
fullscreenTitle: 'Full Contract',
signerMissing: 'Missing signer name',
contractNoMissing: 'Missing contract number',
signingLoading: 'Submitting contract...',
uploadFailed: 'Failed to upload signature',
signSuccess: 'Contract signed successfully',
signFailed: 'Failed to sign contract'
}
},
components: {
ContractSignDialog
},
data() {
return {
contractNo: '',
contractDisplayName: '',
orderNo: '',
contractContent: '',
loading: false,
@@ -206,11 +160,23 @@ export default {
this.contractNo = decodeURIComponent(options.no || '')
this.orderNo = decodeURIComponent(options.orderNo || '')
this.signerName = this.getSignerName()
this.fetchContractDisplayName()
this.fetchContractContent()
},
methods: {
t(key, params) {
return this.$tm('pkgService.contract_sign.contract_sign', key, params)
fetchContractDisplayName() {
if (!this.contractNo) {
return Promise.resolve()
}
return fetchContractDisplayInfo(this.contractNo, this.$getLanguage())
.then(({ contractDisplayName }) => {
this.contractDisplayName = contractDisplayName || ''
})
.catch((error) => {
console.error('fetch contract display name failed', error)
this.contractDisplayName = ''
})
},
fetchContractContent() {
if (!this.contractNo) {
@@ -238,14 +204,14 @@ export default {
}
if (!this.signerName) {
uni.showToast({
title: this.t('signerMissing'),
title: this.$tr('未获取到签名人姓名'),
icon: 'none'
})
return
}
if (!this.contractNo) {
uni.showToast({
title: this.t('contractNoMissing'),
title: this.$tr('缺少合同编号'),
icon: 'none'
})
return
@@ -307,7 +273,7 @@ export default {
this.isSubmitting = true
uni.showLoading({
title: this.t('signingLoading'),
title: this.$tr('签约提交中...'),
mask: true
})
@@ -322,7 +288,7 @@ export default {
.then((uploadResult) => {
const signImageUrl = resolveUploadedFileUrl(uploadResult)
if (!signImageUrl) {
return Promise.reject(new Error((uploadResult && uploadResult.msg) || this.t('uploadFailed')))
return Promise.reject(new Error((uploadResult && uploadResult.msg) || this.$tr('签名上传失败')))
}
return contractSign({
@@ -349,7 +315,7 @@ export default {
.then(() => {
uni.hideLoading()
uni.showToast({
title: this.t('signSuccess'),
title: this.$tr('签约成功'),
icon: 'success'
})
setTimeout(() => {
@@ -362,7 +328,7 @@ export default {
console.error('contract sign failed', error)
uni.hideLoading()
uni.showToast({
title: (error && (error.message || error.msg)) || this.t('signFailed'),
title: (error && (error.message || error.msg)) || this.$tr('签约失败'),
icon: 'none'
})
})

View File

@@ -30,7 +30,7 @@
<view class="sheet-handle"></view>
<view class="sheet-header">
<text class="sheet-title">{{ t('controlPanel') }}</text>
<text class="sheet-title">{{ $tr('控制面板') }}</text>
<text class="sheet-subtitle">{{ functionCountText }}</text>
</view>
@@ -46,7 +46,7 @@
<text class="tn-icon-set panel-icon"></text>
<text>{{ getFunctionLabel(func) }}</text>
</view>
<view class="status-tag" v-if="!isFunctionSupported(func)">{{ t('pendingTag') }}</view>
<view class="status-tag" v-if="!isFunctionSupported(func)">{{ $tr('待完善') }}</view>
</view>
<!-- Switch Theme -->
@@ -60,7 +60,7 @@
<block v-else-if="isSlideFunction(func)">
<view class="slider-container">
<view class="slider-header">
<text class="slider-label">{{ t('sliderLabel') }}</text>
<text class="slider-label">{{ $tr('亮度 / 数值') }}</text>
<text class="slider-value">{{ controlValue }}%</text>
</view>
<slider
@@ -76,7 +76,7 @@
@change="handleControlValueChange"
/>
<view class="slider-apply-btn" :class="{ 'is-disabled': isSubmitting }" @click="submitSlideValue(index)">
<text>{{ t('applySetting') }}</text>
<text>{{ $tr('应用设置') }}</text>
</view>
</view>
</block>
@@ -86,22 +86,22 @@
<view class="control-row curtain-row">
<view class="curtain-btn" :class="{ 'is-active': curtainAction === 'open', 'is-disabled': isSubmitting }" @click="operateCurtain(index, 'open')">
<text class="tn-icon-up"></text>
<text class="btn-label">{{ t('curtainOpen') }}</text>
<text class="btn-label">{{ $tr('打开') }}</text>
</view>
<view class="curtain-btn stop-btn" :class="{ 'is-active': curtainAction === 'stop', 'is-disabled': isSubmitting }" @click="operateCurtain(index, 'stop')">
<text class="tn-icon-minus"></text>
<text class="btn-label">{{ t('curtainPause') }}</text>
<text class="btn-label">{{ $tr('暂停') }}</text>
</view>
<view class="curtain-btn" :class="{ 'is-active': curtainAction === 'close', 'is-disabled': isSubmitting }" @click="operateCurtain(index, 'close')">
<text class="tn-icon-down"></text>
<text class="btn-label">{{ t('curtainClose') }}</text>
<text class="btn-label">{{ $tr('关闭') }}</text>
</view>
</view>
</block>
<view v-else class="unsupported-box" @click="notifyUnsupportedFunction(index)">
<text class="tn-icon-warning"></text>
<text>{{ t('unsupportedBoxText') }}</text>
<text>{{ $tr('当前功能暂未开放使用,点击反馈') }}</text>
</view>
</view>
@@ -173,68 +173,6 @@ function resolveRefreshIntervalMs(value, fallback = 1000) {
}
export default {
i18n: {
zh: {
controlPanel: '控制面板',
pendingTag: '待完善',
sliderLabel: '亮度 / 数值',
applySetting: '应用设置',
curtainOpen: '打开',
curtainPause: '暂停',
curtainClose: '关闭',
unsupportedBoxText: '当前功能暂未开放使用,点击反馈',
deviceDetail: '设备详情',
currentRoom: '当前房间',
functionCount: '{count}项功能',
commandSending: '指令下发中...',
syncHint: '操作完成后会自动刷新设备状态',
deviceMissing: '设备信息缺失',
unsupportedFunction: '{name}操作功能未完善',
unknownSchema: '未识别到参数模板',
schemaApplied: '{name}参数已下发',
actionSuccess: '操作成功',
deviceIdMissing: '设备ID缺失',
functionCodeMissing: '功能编码缺失',
commandFailed: '指令下发失败',
deviceClosed: '已关闭',
deviceEnabled: '已开启',
deviceOpened: '已打开',
curtainOpened: '窗帘已打开',
curtainClosed: '窗帘已关闭',
curtainPaused: '窗帘已暂停',
sliderUpdated: '滑块值已更新'
},
en: {
controlPanel: 'Control Panel',
pendingTag: 'Pending',
sliderLabel: 'Brightness / Value',
applySetting: 'Apply',
curtainOpen: 'Open',
curtainPause: 'Pause',
curtainClose: 'Close',
unsupportedBoxText: 'This function is not available yet. Tap to send feedback.',
deviceDetail: 'Device Details',
currentRoom: 'Current Room',
functionCount: '{count} functions',
commandSending: 'Sending command...',
syncHint: 'Device status refreshes automatically after each action',
deviceMissing: 'Missing device information',
unsupportedFunction: '{name} is not fully supported yet',
unknownSchema: 'No parameter template recognized',
schemaApplied: '{name} parameters sent',
actionSuccess: 'Action completed',
deviceIdMissing: 'Missing device ID',
functionCodeMissing: 'Missing function code',
commandFailed: 'Failed to send command',
deviceClosed: 'Closed',
deviceEnabled: 'Enabled',
deviceOpened: 'Opened',
curtainOpened: 'Curtain opened',
curtainClosed: 'Curtain closed',
curtainPaused: 'Curtain paused',
sliderUpdated: 'Slider value updated'
}
},
data() {
return {
brandId: DEFAULT_BRAND_ID,
@@ -257,10 +195,10 @@ export default {
return this.$getLanguage()
},
deviceName() {
return this.device.deviceName || this.t('deviceDetail')
return this.device.deviceName || this.$tr('设备详情')
},
displayRoomName() {
return this.roomName || this.roomCode || this.t('currentRoom')
return this.roomName || this.roomCode || this.$tr('当前房间')
},
controlType() {
return inferDeviceControlType(this.device)
@@ -273,16 +211,16 @@ export default {
},
functionCountText() {
const count = this.deviceFunctions.length
return count ? this.t('functionCount', { count }) : this.controlTypeLabel
return count ? this.$tr('{count}项功能', { count }) : this.controlTypeLabel
},
statusText() {
return buildDeviceStateText(this.device)
},
syncHintText() {
if (this.isSubmitting) {
return this.t('commandSending')
return this.$tr('指令下发中...')
}
return this.t('syncHint')
return this.$tr('操作完成后会自动刷新设备状态')
},
deviceFunctions() {
return getDeviceFunctions(this.device)
@@ -306,7 +244,7 @@ export default {
const parsedDevice = safeParseDevice(options.device)
if (!parsedDevice) {
uni.showToast({
title: this.t('deviceMissing'),
title: this.$tr('设备信息缺失'),
icon: 'none'
})
return
@@ -327,9 +265,6 @@ export default {
this.stopDevicePolling()
},
methods: {
t(key, params) {
return this.$tm('pkgService.device_control.device_control', key, params)
},
goBack() {
uni.navigateBack()
},
@@ -613,7 +548,7 @@ export default {
? this.getDeviceFunctionByIndex(funcOrIndex)
: funcOrIndex
uni.showToast({
title: this.t('unsupportedFunction', { name: this.getFunctionLabel(func) }),
title: this.$tr('{name}操作功能未完善', { name: this.getFunctionLabel(func) }),
icon: 'none'
})
},
@@ -676,7 +611,7 @@ export default {
const fields = this.getFunctionFields(func, index)
if (!fields.length) {
uni.showToast({
title: this.t('unknownSchema'),
title: this.$tr('未识别到参数模板'),
icon: 'none'
})
return
@@ -688,11 +623,11 @@ export default {
return result
}, {})
this.executeCommand(func, payload, this.t('schemaApplied', { name: this.getFunctionLabel(func) }))
this.executeCommand(func, payload, this.$tr('{name}参数已下发', { name: this.getFunctionLabel(func) }))
},
executeCommand(func, command, successText = this.t('actionSuccess')) {
executeCommand(func, command, successText = this.$tr('操作成功')) {
if (!this.device || !this.device.deviceNo) {
uni.showToast({ title: this.t('deviceIdMissing'), icon: 'none' })
uni.showToast({ title: this.$tr('设备ID缺失'), icon: 'none' })
return Promise.resolve()
}
@@ -703,7 +638,7 @@ export default {
const functionCode = String((func && func.functionCode) || '').trim()
if (!functionCode) {
uni.showToast({ title: this.t('functionCodeMissing'), icon: 'none' })
uni.showToast({ title: this.$tr('功能编码缺失'), icon: 'none' })
return Promise.resolve()
}
@@ -720,7 +655,7 @@ export default {
.catch((error) => {
console.error('execute device command failed', error)
uni.showToast({
title: (error && (error.message || error.msg)) || this.t('commandFailed'),
title: (error && (error.message || error.msg)) || this.$tr('指令下发失败'),
icon: 'none'
})
})
@@ -734,14 +669,14 @@ export default {
if (this.isSubmitting) {
return
}
this.executeCommand(func, this.buildSwitchCommandPayload(index, !this.device.active), this.device.active ? this.t('deviceClosed') : this.t('deviceEnabled'))
this.executeCommand(func, this.buildSwitchCommandPayload(index, !this.device.active), this.device.active ? this.$tr('已关闭') : this.$tr('已开启'))
},
setSwitchState(index = 0, value) {
const func = this.getDeviceFunctionByIndex(index)
if (this.isSubmitting) {
return
}
this.executeCommand(func, this.buildSwitchCommandPayload(index, !!value), value ? this.t('deviceOpened') : this.t('deviceClosed'))
this.executeCommand(func, this.buildSwitchCommandPayload(index, !!value), value ? this.$tr('已打开') : this.$tr('已关闭'))
},
operateCurtain(index = 0, action) {
const func = this.getDeviceFunctionByIndex(index)
@@ -751,7 +686,7 @@ export default {
this.executeCommand(
func,
this.buildCurtainCommandPayload(index, action),
action === 'open' ? this.t('curtainOpened') : (action === 'close' ? this.t('curtainClosed') : this.t('curtainPaused'))
action === 'open' ? this.$tr('窗帘已打开') : (action === 'close' ? this.$tr('窗帘已关闭') : this.$tr('窗帘已暂停'))
)
},
handleControlValueChange(event) {
@@ -765,7 +700,7 @@ export default {
this.executeCommand(
func,
this.buildSlideCommandPayload(index, this.controlValue),
this.t('sliderUpdated')
this.$tr('滑块值已更新')
)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
:customBackStyle="navBarBackStyle"
:fixed="true"
>
<view class="tn-text-bold tn-text-xl tn-color-black" :style="{ opacity: navTitleOpacity }">{{ t('navTitle') }}</view>
<view class="tn-text-bold tn-text-xl tn-color-black" :style="{ opacity: navTitleOpacity }">{{ $tr('失物拾物') }}</view>
</tn-nav-bar>
<scroll-view class="page-scroll" scroll-y :show-scrollbar="false" @scroll="handleScroll">
@@ -15,76 +15,76 @@
<view class="hero-header tn-margin-bottom-xl">
<view class="hero-badge">
<text class="badge-dot"></text>
<text>{{ t('heroBadge') }}</text>
<text>{{ $tr('社区服务') }}</text>
</view>
<view class="hero-title">{{ t('heroTitle') }}</view>
<view class="hero-desc">{{ t('heroDesc') }}</view>
<view class="hero-title">{{ $tr('发布失物或招领信息') }}</view>
<view class="hero-desc">{{ $tr('将失物或拾获信息同步给社区,方便后续核实与快速匹配。') }}</view>
</view>
<view class="form-card">
<view class="block-title">{{ t('formTitle') }}</view>
<view class="block-subtitle">{{ t('formDesc') }}</view>
<view class="block-title">{{ $tr('物品信息') }}</view>
<view class="block-subtitle">{{ $tr('请补充关键字段,必填项越完整,越方便他人联系与认领。') }}</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('typeLabel') }}</view>
<view class="form-label">{{ $tr('发布类型') }}</view>
<picker mode="selector" :range="typeLabels" @change="handleTypeChange">
<view class="form-picker">
{{ selectedTypeLabel || t('typePlaceholder') }}
{{ selectedTypeLabel || $tr('请选择失物或招领') }}
</view>
</picker>
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('itemTypeLabel') }}</view>
<input v-model="form.itemType" class="form-input" :placeholder="t('itemTypePlaceholder')" maxlength="40" />
<view class="form-label">{{ $tr('物品类型') }}</view>
<input v-model="form.itemType" class="form-input" :placeholder="$tr('例如证件、钥匙、电子设备')" maxlength="40" />
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('itemNameLabel') }}</view>
<input v-model="form.itemName" class="form-input" :placeholder="t('itemNamePlaceholder')" maxlength="60" />
<view class="form-label">{{ $tr('物品名称') }}</view>
<input v-model="form.itemName" class="form-input" :placeholder="$tr('请输入物品名称')" maxlength="60" />
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('placeLabel') }}</view>
<input v-model="form.place" class="form-input" :placeholder="t('placePlaceholder')" maxlength="100" />
<view class="form-label">{{ $tr('地点') }}</view>
<input v-model="form.place" class="form-input" :placeholder="$tr('请输入丢失或拾取地点')" maxlength="100" />
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('whenLabel') }}</view>
<view class="form-label">{{ $tr('时间') }}</view>
<view class="picker-row">
<picker mode="date" :value="form.whenDate" @change="handleDateChange">
<view class="form-picker">{{ form.whenDate || t('selectDate') }}</view>
<view class="form-picker">{{ form.whenDate || $tr('选择日期') }}</view>
</picker>
<picker mode="time" :value="form.whenTime" @change="handleTimeChange">
<view class="form-picker form-picker--time">{{ form.whenTime || t('selectTime') }}</view>
<view class="form-picker form-picker--time">{{ form.whenTime || $tr('选择时间') }}</view>
</picker>
</view>
<view class="field-tip">{{ t('whenTip') }}</view>
<view class="field-tip">{{ $tr('可选,若已知大致时间可一并填写。') }}</view>
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('contractLabel') }}</view>
<input v-model="form.contract" class="form-input" :placeholder="t('contractPlaceholder')" maxlength="40" />
<view class="form-label">{{ $tr('联系人') }}</view>
<input v-model="form.contract" class="form-input" :placeholder="$tr('请输入联系人')" maxlength="40" />
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('contractPhoneLabel') }}</view>
<input v-model="form.contractPhone" class="form-input" :placeholder="t('contractPhonePlaceholder')" maxlength="30" />
<view class="form-label">{{ $tr('联系电话') }}</view>
<input v-model="form.contractPhone" class="form-input" :placeholder="$tr('请输入联系电话')" maxlength="30" />
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('remarkLabel') }}</view>
<view class="form-label">{{ $tr('备注') }}</view>
<textarea
v-model="form.remark"
class="form-textarea"
:placeholder="t('remarkPlaceholder')"
:placeholder="$tr('可补充外观特征、认领说明或其他辅助信息')"
maxlength="300"
></textarea>
<view class="field-counter">{{ (form.remark || '').length }}/300</view>
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('imagesLabel') }}</view>
<view class="form-label">{{ $tr('现场图片') }}</view>
<tn-image-upload
:key="uploadInstanceKey"
:fileList="form.photos"
@@ -93,7 +93,8 @@
:formData="uploadFormData"
:max-count="6"
:max-size="5 * 1024 * 1024"
uploadText=""
:uploadText="''"
:customBtn="true"
@on-success="onUploadSuccess"
@on-list-change="onPhotoListChange"
@on-remove="onRemoveImage"
@@ -104,11 +105,11 @@
<template #addBtn>
<view class="upload-trigger">
<text class="tn-icon-camera-fill upload-trigger__icon"></text>
<view class="upload-trigger__text">{{ t('uploadImage') }}</view>
<view class="upload-trigger__text">{{ $tr('上传图片') }}</view>
</view>
</template>
</tn-image-upload>
<view class="field-tip">{{ t('imagesTip') }}</view>
<view class="field-tip">{{ $tr('可选,上传现场图或物品图可提升识别效率。') }}</view>
</view>
</view>
</view>
@@ -116,9 +117,9 @@
<view class="floating-submit-bar">
<view class="floating-submit-bar__inner">
<view class="floating-submit-tip">{{ t('submitTip') }}</view>
<view class="floating-submit-tip">{{ $tr('发布前请确认信息准确,系统会按当前语言提交接口。') }}</view>
<view class="submit-btn btn-primary" :class="{ 'submit-btn--disabled': isSubmitting }" @click="submitForm">
<text class="tn-text-bold">{{ isSubmitting ? t('submitting') : t('submitButton') }}</text>
<text class="tn-text-bold">{{ isSubmitting ? $tr('发布中...') : $tr('立即发布') }}</text>
</view>
</view>
</view>
@@ -180,8 +181,8 @@ export default {
},
typeOptions() {
return [
{ value: 1, label: this.t('typeLost'), labelEn: this.t('typeLost') },
{ value: 2, label: this.t('typeFound'), labelEn: this.t('typeFound') }
{ value: 1, label: this.$tr('失物'), labelEn: this.$tr('失物') },
{ value: 2, label: this.$tr('招领'), labelEn: this.$tr('招领') }
]
},
typeLabels() {
@@ -211,9 +212,6 @@ export default {
this.prefillContact()
},
methods: {
t(key, params) {
return this.$tm('pkgService.lost_found_publish.lost_found_publish', key, params)
},
createDefaultForm() {
return {
type: 1,
@@ -288,20 +286,20 @@ export default {
this.form.imageUrls.splice(index, 1)
},
onUploadError() {
uni.showToast({ title: this.t('uploadFailed'), icon: 'none' })
uni.showToast({ title: this.$tr('图片上传失败'), icon: 'none' })
},
onUploadOversize() {
uni.showToast({ title: this.t('uploadOversize'), icon: 'none' })
uni.showToast({ title: this.$tr('单张图片不能超过 5 MB'), icon: 'none' })
},
onUploadExceed() {
uni.showToast({ title: this.t('uploadExceed'), icon: 'none' })
uni.showToast({ title: this.$tr('最多上传 6 张图片'), icon: 'none' })
},
getValidationMessage() {
if (!String(this.form.itemType || '').trim()) return this.t('validateItemType')
if (!String(this.form.itemName || '').trim()) return this.t('validateItemName')
if (!String(this.form.place || '').trim()) return this.t('validatePlace')
if (!String(this.form.contract || '').trim()) return this.t('validateContract')
if (!String(this.form.contractPhone || '').trim()) return this.t('validateContractPhone')
if (!String(this.form.itemType || '').trim()) return this.$tr('请输入物品类型')
if (!String(this.form.itemName || '').trim()) return this.$tr('请输入物品名称')
if (!String(this.form.place || '').trim()) return this.$tr('请输入地点')
if (!String(this.form.contract || '').trim()) return this.$tr('请输入联系人')
if (!String(this.form.contractPhone || '').trim()) return this.$tr('请输入联系电话')
return ''
},
buildSubmitPayload() {
@@ -335,7 +333,7 @@ export default {
lostAndFoundPublish(this.buildSubmitPayload(), this.language)
.then(() => {
uni.showToast({
title: this.t('submitSuccess'),
title: this.$tr('发布成功'),
icon: 'success'
})
this.resetForm()
@@ -347,7 +345,7 @@ export default {
})
.catch((error) => {
uni.showToast({
title: (error && (error.message || error.msg)) || this.t('submitFailed'),
title: (error && (error.message || error.msg)) || this.$tr('发布失败'),
icon: 'none'
})
})

View File

@@ -1,7 +1,7 @@
<template>
<view class="message-detail-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>
<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' }">
@@ -12,7 +12,7 @@
<text :class="categoryIcon"></text>
</view>
</view>
<view class="hero-title tn-margin-top">{{ localizedTitle || t('messageFallbackTitle') }}</view>
<view class="hero-title tn-margin-top">{{ localizedTitle || $tr('消息通知') }}</view>
<view class="hero-meta-row tn-margin-top">
<view class="hero-meta-chip">
<text class="tn-icon-time tn-margin-right-xs"></text>
@@ -26,7 +26,7 @@
</view>
<view class="article-header tn-margin-bottom-lg">
<view class="article-title">{{ localizedTitle || t('messageFallbackTitle') }}</view>
<view class="article-title">{{ localizedTitle || $tr('消息通知') }}</view>
<view class="article-meta tn-margin-top">
<view class="article-tag">{{ categoryText }}</view>
<view class="article-time">{{ detail.time || '--' }}</view>
@@ -34,42 +34,42 @@
</view>
<view class="content-panel tn-margin-bottom-lg">
<view class="panel-title">{{ t('contentTitle') }}</view>
<view class="panel-title">{{ $tr('通知内容') }}</view>
<rich-text class="article-content tn-margin-top" :nodes="contentNodes"></rich-text>
</view>
<view v-if="localizedDesc && localizedDesc !== localizedContent" class="content-panel tn-margin-bottom-lg">
<view class="panel-title">{{ t('summaryTitle') }}</view>
<view class="panel-title">{{ $tr('摘要') }}</view>
<view class="panel-desc tn-margin-top">{{ localizedDesc }}</view>
</view>
<view class="meta-panel tn-margin-bottom-lg">
<view class="meta-row">
<text class="meta-label">{{ t('sourceLabel') }}</text>
<text class="meta-label">{{ $tr('消息来源') }}</text>
<text class="meta-value">{{ sourceText }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ t('categoryLabel') }}</text>
<text class="meta-label">{{ $tr('消息分类') }}</text>
<text class="meta-value">{{ categoryText }}</text>
</view>
<view v-if="detail.code" class="meta-row">
<text class="meta-label">{{ t('codeLabel') }}</text>
<text class="meta-label">{{ $tr('通知编码') }}</text>
<view class="meta-action-wrap">
<text class="meta-value">{{ detail.code }}</text>
<view class="copy-btn" @click="copyCode">
<text class="tn-text-xs tn-text-bold">{{ t('copy') }}</text>
<text class="tn-text-xs tn-text-bold">{{ $tr('复制') }}</text>
</view>
</view>
</view>
<view v-if="detail.statusHint" class="meta-row">
<text class="meta-label">{{ t('statusLabel') }}</text>
<text class="meta-label">{{ $tr('当前状态') }}</text>
<text class="meta-value">{{ detail.statusHint }}</text>
</view>
</view>
<view class="action-row">
<view class="ghost-btn" @click="goBackList">
<text class="tn-text-bold">{{ t('backList') }}</text>
<text class="tn-text-bold">{{ $tr('返回消息页') }}</text>
</view>
<view v-if="detail.path" class="action-btn" @click="handleGoBusiness">
<text class="tn-text-bold">{{ actionButtonText }}</text>
@@ -111,58 +111,6 @@ function normalizeContentToRichText(value = '', emptyText = '') {
}
export default {
i18n: {
zh: {
navTitle: '消息详情',
messageFallbackTitle: '消息通知',
contentTitle: '通知内容',
summaryTitle: '摘要',
sourceLabel: '消息来源',
categoryLabel: '消息分类',
codeLabel: '通知编码',
copy: '复制',
statusLabel: '当前状态',
backList: '返回消息页',
categorySystem: '系统通知',
categoryPayment: '缴费账单',
categoryButler: '管家消息',
actionSystem: '前往相关页面',
actionPayment: '去处理账单',
actionButler: '去查看服务',
actionDefault: '继续前往业务页',
remoteSource: '系统通知接口',
localSource: '本地业务消息',
expiredDesc: '当前消息详情已失效,请返回消息中心重新打开。',
expiredHint: '消息缓存已失效',
codeCopied: '通知编号已复制',
emptyContent: '暂无详细内容'
},
en: {
navTitle: 'Message Detail',
messageFallbackTitle: 'Notification',
contentTitle: 'Content',
summaryTitle: 'Summary',
sourceLabel: 'Source',
categoryLabel: 'Category',
codeLabel: 'Notice Code',
copy: 'Copy',
statusLabel: 'Status',
backList: 'Back to Messages',
categorySystem: 'System Notice',
categoryPayment: 'Payment Bill',
categoryButler: 'Butler Message',
actionSystem: 'Open Related Page',
actionPayment: 'Handle Bill',
actionButler: 'View Service',
actionDefault: 'Continue to Page',
remoteSource: 'Remote Notification API',
localSource: 'Local Business Message',
expiredDesc: 'This message detail has expired. Please return to the message center and open it again.',
expiredHint: 'Message cache expired',
codeCopied: 'Notice code copied',
emptyContent: 'No detailed content available'
}
},
data() {
return {
lookupId: '',
@@ -205,14 +153,14 @@ export default {
},
categoryText() {
const map = {
system: this.t('categorySystem'),
payment: this.t('categoryPayment'),
butler: this.t('categoryButler')
system: this.$tr('系统通知'),
payment: this.$tr('缴费账单'),
butler: this.$tr('管家消息')
}
return map[this.detail.category] || this.t('messageFallbackTitle')
return map[this.detail.category] || this.$tr('消息通知')
},
contentNodes() {
return normalizeContentToRichText(this.localizedContent || this.localizedDesc || '', this.t('emptyContent'))
return normalizeContentToRichText(this.localizedContent || this.localizedDesc || '', this.$tr('暂无详细内容'))
},
categoryIcon() {
const map = {
@@ -240,14 +188,14 @@ export default {
},
actionButtonText() {
const map = {
system: this.t('actionSystem'),
payment: this.t('actionPayment'),
butler: this.t('actionButler')
system: this.$tr('前往相关页面'),
payment: this.$tr('去处理账单'),
butler: this.$tr('去查看服务')
}
return map[this.detail.category] || this.t('actionDefault')
return map[this.detail.category] || this.$tr('继续前往业务页')
},
sourceText() {
return this.detail.source === 'remote' ? this.t('remoteSource') : this.t('localSource')
return this.detail.source === 'remote' ? this.$tr('系统通知接口') : this.$tr('本地业务消息')
}
},
onLoad(options = {}) {
@@ -264,9 +212,6 @@ export default {
clearMessageDetailPayload(this.language)
},
methods: {
t(key, params) {
return this.$tm('pkgService.message_detail.message_detail', key, params)
},
restoreDetail() {
const payload = getMessageDetailPayload(this.language)
const fallbackPayload = findMessageDetailPayload(this.lookupId, this.lookupCode, this.language)
@@ -284,17 +229,17 @@ export default {
...this.detail,
id: this.lookupId || this.lookupCode || '',
code: this.lookupCode || '',
titleZh: this.t('messageFallbackTitle'),
titleEn: this.t('messageFallbackTitle'),
descZh: this.t('expiredDesc'),
descEn: this.t('expiredDesc'),
contentZh: this.t('expiredDesc'),
contentEn: this.t('expiredDesc'),
title: this.t('messageFallbackTitle'),
desc: this.t('expiredDesc'),
content: this.t('expiredDesc'),
titleZh: this.$tr('消息通知'),
titleEn: this.$tr('消息通知'),
descZh: this.$tr('当前消息详情已失效,请返回消息中心重新打开。'),
descEn: this.$tr('当前消息详情已失效,请返回消息中心重新打开。'),
contentZh: this.$tr('当前消息详情已失效,请返回消息中心重新打开。'),
contentEn: this.$tr('当前消息详情已失效,请返回消息中心重新打开。'),
title: this.$tr('消息通知'),
desc: this.$tr('当前消息详情已失效,请返回消息中心重新打开。'),
content: this.$tr('当前消息详情已失效,请返回消息中心重新打开。'),
source: 'remote',
statusHint: this.t('expiredHint')
statusHint: this.$tr('消息缓存已失效')
}
},
goBackList() {
@@ -308,7 +253,7 @@ export default {
data: String(this.detail.code),
success: () => {
uni.showToast({
title: this.t('codeCopied'),
title: this.$tr('通知编号已复制'),
icon: 'success'
})
}

View File

@@ -1,7 +1,7 @@
<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>
<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' }">
@@ -20,7 +20,7 @@
</scroll-view>
<view class="toolbar-row tn-margin-bottom-lg">
<view class="toolbar-summary">{{ t('toolbarSummary', { total }) }}</view>
<view class="toolbar-summary">{{ $tr('共 {total} 条申请', { total }) }}</view>
<view
class="refresh-btn"
:class="{ 'refresh-btn--loading': loading }"
@@ -30,7 +30,7 @@
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') }}
{{ loading ? $tr('刷新中...') : $tr('刷新状态') }}
</text>
</view>
</view>
@@ -52,25 +52,34 @@
</view>
</view>
<view
v-if="shouldShowApplicationPaymentStatus(item)"
class="payment-status-banner tn-margin-top"
:class="`payment-status-banner--${getApplicationPaymentStatusType(item)}`"
>
<text class="payment-status-banner__label">{{ $tr('支付状态') }}</text>
<text class="payment-status-banner__value">{{ getApplicationPaymentStatusText(item) }}</text>
</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-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">{{ t('occupantLabel') }}</text>
<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 ? t('viewingTimeLabel') : t('leaseTermLabel') }}</text>
<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">{{ t('submittedAtLabel') }}</text>
<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">{{ t('remarkLabel') }}</view>
<view class="remark-label">{{ $tr('补充说明') }}</view>
<view class="remark-text tn-margin-top-xs">{{ item.remark }}</view>
</view>
</view>
@@ -80,29 +89,29 @@
:data-reference-type-no="resolveReferenceTypeNo(item)"
@click="goToBillPage"
>
<text class="tn-text-bold">{{ t('viewBill') }}</text>
<text class="tn-text-bold">{{ $tr('查看关联账单') }}</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>
<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">{{ t('emptyTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('emptyDesc') }}</view>
<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">{{ t('goSelectRoom') }}</text>
<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">{{ t('loadingTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('loadingDesc') }}</view>
<view class="empty-title">{{ $tr('申请列表加载中') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ $tr('正在同步您的入住申请记录,请稍候。') }}</view>
</view>
</view>
</view>
@@ -154,6 +163,43 @@ function isViewingApplicationRecord(item = {}) {
return VIEWING_APPLICATION_PATTERN.test(remark)
}
function resolveApplicationPaymentStatusInfo(item = {}) {
if (!item || item.isViewingApplication) {
return {
key: 'exempt',
desc: ''
}
}
const desc = String(item.payStatusDesc || item.paymentStatusDesc || '').trim()
const status = Number(
item.payStatus !== undefined && item.payStatus !== null
? item.payStatus
: item.paymentStatus
)
if (status === 10 || /已支付|paid/i.test(desc)) {
return { key: 'paid', desc }
}
if (/退款|refunded?/i.test(desc)) {
return { key: 'refunded', desc }
}
if (/失败|failed?/i.test(desc)) {
return { key: 'failed', desc }
}
if (/处理中|processing/i.test(desc)) {
return { key: 'processing', desc }
}
if (status === 0 || /待支付|未支付|unpaid|pending/i.test(desc)) {
return { key: 'pending', desc }
}
return {
key: desc ? 'processing' : 'pending',
desc
}
}
function normalizeApplicationList(list = []) {
if (!Array.isArray(list)) {
return []
@@ -169,76 +215,6 @@ function normalizeApplicationList(list = []) {
}
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: '',
@@ -278,26 +254,23 @@ export default {
}
},
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 }
{ 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.t('statusPending'),
10: this.t('statusApproved'),
20: this.t('statusRejected'),
99: this.t('statusFinished')
0: this.$tr('待审核'),
10: this.$tr('已通过'),
20: this.$tr('已拒绝'),
99: this.$tr('已结束')
}
return map[status] || this.t('statusProcessing')
return map[status] || this.$tr('处理中')
},
getDisplayStatusText(item = {}) {
if (this.language !== 'en' && item.statusDesc) {
@@ -338,10 +311,10 @@ export default {
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 `${startText}${this.$tr(' 至 ')}${endText}`
}
}
return `${this.formatDate(start)}${this.t('dateRangeJoiner')}${this.formatDate(end)}`
return `${this.formatDate(start)}${this.$tr(' 至 ')}${this.formatDate(end)}`
},
async refreshStatus() {
if (this.loading || this.loadingMore) {
@@ -350,7 +323,7 @@ export default {
await this.reloadList()
if (this.list.length || this.total === 0) {
uni.showToast({
title: this.t('refreshSuccess'),
title: this.$tr('申请状态已刷新'),
icon: 'none'
})
}
@@ -407,7 +380,7 @@ export default {
this.pageIndex = Math.max(1, this.pageIndex - 1)
}
uni.showToast({
title: (error && (error.message || error.msg)) || this.t('loadError'),
title: (error && (error.message || error.msg)) || this.$tr('申请记录加载失败'),
icon: 'none'
})
})
@@ -431,9 +404,39 @@ export default {
},
getApplicationTitle(item = {}) {
if (item.isViewingApplication) {
return this.t('viewingApplicationTitle')
return this.$tr('预约看房申请')
}
return item.intendedRoomTypeName || this.t('stayApplicationTitle')
return item.intendedRoomTypeName || this.$tr('入住申请')
},
shouldShowApplicationPaymentStatus(item = {}) {
return !item.isViewingApplication
},
getApplicationPaymentStatusText(item = {}) {
const statusInfo = resolveApplicationPaymentStatusInfo(item)
if (this.$getLanguage() !== 'en' && statusInfo.desc) {
return statusInfo.desc
}
const textMap = {
exempt: this.$tr('无需支付'),
paid: this.$tr('已支付'),
refunded: this.$tr('已退款'),
failed: this.$tr('支付失败'),
processing: this.$tr('处理中'),
pending: this.$tr('待支付')
}
return textMap[statusInfo.key] || statusInfo.desc || this.$tr('待支付')
},
getApplicationPaymentStatusType(item = {}) {
const statusInfo = resolveApplicationPaymentStatusInfo(item)
const typeMap = {
exempt: 'neutral',
paid: 'success',
refunded: 'neutral',
failed: 'danger',
processing: 'info',
pending: 'warning'
}
return typeMap[statusInfo.key] || 'warning'
},
resolveReferenceTypeNo(item = {}) {
return item.detailCode || ''
@@ -495,7 +498,7 @@ export default {
: ''
if (!referenceTypeNo) {
uni.showToast({
title: this.t('missingBillCode'),
title: this.$tr('申请单编号缺失'),
icon: 'none'
})
return
@@ -628,6 +631,72 @@ export default {
font-weight: 700;
}
.payment-status-banner {
padding: 20rpx 24rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
border: 1rpx solid transparent;
}
.payment-status-banner__label {
font-size: 24rpx;
color: #64748B;
font-weight: 600;
}
.payment-status-banner__value {
font-size: 28rpx;
font-weight: 700;
}
.payment-status-banner--warning {
background: linear-gradient(180deg, rgba(255, 247, 237, 0.96) 0%, rgba(255, 251, 245, 0.98) 100%);
border-color: rgba(249, 115, 22, 0.16);
}
.payment-status-banner--warning .payment-status-banner__value {
color: #C2410C;
}
.payment-status-banner--success {
background: linear-gradient(180deg, rgba(236, 253, 245, 0.96) 0%, rgba(240, 253, 250, 0.98) 100%);
border-color: rgba(16, 185, 129, 0.16);
}
.payment-status-banner--success .payment-status-banner__value {
color: #047857;
}
.payment-status-banner--info {
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
border-color: rgba(59, 130, 246, 0.14);
}
.payment-status-banner--info .payment-status-banner__value {
color: #1D4ED8;
}
.payment-status-banner--danger {
background: linear-gradient(180deg, rgba(254, 242, 242, 0.96) 0%, rgba(255, 245, 245, 0.98) 100%);
border-color: rgba(239, 68, 68, 0.16);
}
.payment-status-banner--danger .payment-status-banner__value {
color: #B91C1C;
}
.payment-status-banner--neutral {
background: linear-gradient(180deg, rgba(241, 245, 249, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
border-color: rgba(148, 163, 184, 0.16);
}
.payment-status-banner--neutral .payment-status-banner__value {
color: #475569;
}
.status-pending {
background: rgba(245, 158, 11, 0.12);
color: #B45309;

View File

@@ -1,15 +1,15 @@
<template>
<view class="my-contract-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>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('我的合同') }}</view>
</tn-nav-bar>
<view class="page-main" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view class="overview-panel">
<view class="overview-header">
<view>
<view class="overview-title">{{ t('overviewTitle') }}</view>
<view class="overview-subtitle">{{ t('overviewSubtitle') }}</view>
<view class="overview-title">{{ $tr('合同概览') }}</view>
<view class="overview-subtitle">{{ $tr('查看住宿合同、签约状态与有效期限') }}</view>
</view>
<view class="overview-badge">
<text class="tn-icon-edit-form"></text>
@@ -18,11 +18,11 @@
</view>
<view class="overview-metrics">
<view class="metric-item">
<view class="metric-label">{{ t('pendingMetricLabel') }}</view>
<view class="metric-label">{{ $tr('待签约/审批') }}</view>
<view class="metric-value">{{ summaryStats.pending }}</view>
</view>
<view class="metric-item is-primary">
<view class="metric-label">{{ t('displayMetricLabel') }}</view>
<view class="metric-label">{{ $tr('当前展示') }}</view>
<view class="metric-value">{{ displayList.length }}</view>
</view>
</view>
@@ -41,14 +41,14 @@
</view>
<view v-if="loading" class="empty-panel">
<view class="empty-title">{{ t('loadingTitle') }}</view>
<view class="empty-desc">{{ t('loadingDesc') }}</view>
<view class="empty-title">{{ $tr('合同记录加载中') }}</view>
<view class="empty-desc">{{ $tr('正在同步您的合同数据,请稍候。') }}</view>
</view>
<view v-else-if="displayList.length" class="list-wrapper">
<view
v-for="(item, index) in displayList"
:key="item.no || item.contractNo || item.orderNo || index"
:key="getContractKey(item, index)"
class="contract-card"
:style="{ animationDelay: `${index * 0.08}s` }"
>
@@ -58,8 +58,8 @@
<text class="tn-icon-edit-write-fill"></text>
</view>
<view class="card-title-group">
<view class="card-title">{{ t('contractTitle') }}</view>
<view class="card-code">{{ item.no || item.contractNo || '--' }}</view>
<view class="card-title">{{ item.contractDisplayName || $tr('住宿合同') }}</view>
<view class="card-code">{{ $tr('合同编号:') }}{{ item.no || item.contractNo || '--' }}</view>
</view>
</view>
<view :class="['status-badge', `status-${resolveStatusType(item.approvalStatus)}`]">
@@ -70,28 +70,28 @@
<view class="meta-strip">
<view class="meta-chip">
<view class="meta-chip-label">{{ t('relatedOrderLabel') }}</view>
<view class="meta-chip-label">{{ $tr('关联订单') }}</view>
<view class="meta-chip-value">{{ item.orderNo || '--' }}</view>
</view>
<view class="meta-chip">
<view class="meta-chip-label">{{ t('signStatusLabel') }}</view>
<view class="meta-chip-value">{{ item.signImageUrl ? t('signed') : t('unsigned') }}</view>
<view class="meta-chip-label">{{ $tr('签署状态') }}</view>
<view class="meta-chip-value">{{ item.signImageUrl ? $tr('已签名') : $tr('待签名') }}</view>
</view>
</view>
<view class="detail-panel">
<view class="info-row">
<text class="info-label">{{ t('contractPeriodLabel') }}</text>
<text class="info-label">{{ $tr('合同期限') }}</text>
<text class="info-value">{{ formatDateRange(item.start, item.end) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('recordTipLabel') }}</text>
<text class="info-label">{{ $tr('记录说明') }}</text>
<text class="info-value">{{ getStatusTip(item) }}</text>
</view>
<view v-if="item.signImageUrl" class="sign-row">
<text class="info-label">{{ t('mySignatureLabel') }}</text>
<view v-if="item.contentImageUrl" class="sign-row">
<text class="info-label">{{ $tr('合同图片') }}</text>
<view class="sign-preview">
<image :src="item.signImageUrl" mode="aspectFit"></image>
<image :src="item.contentImageUrl" mode="aspectFit" @click="previewContractImage(index)"></image>
</view>
</view>
</view>
@@ -100,7 +100,7 @@
<view
class="action-btn"
:class="{ 'action-btn--secondary': !shouldShowSignAction(item) }"
@click="handleContractAction(item)"
@click="handleContractAction(index)"
>
{{ resolveActionText(item) }}
</view>
@@ -112,8 +112,8 @@
<view class="empty-icon">
<text class="tn-icon-folder"></text>
</view>
<view class="empty-title">{{ t('emptyTitle') }}</view>
<view class="empty-desc">{{ t('emptyDesc') }}</view>
<view class="empty-title">{{ $tr('暂无对应的合同记录') }}</view>
<view class="empty-desc">{{ $tr('切换筛选条件,或等待系统生成新的合同数据。') }}</view>
</view>
</view>
</view>
@@ -121,8 +121,42 @@
<script>
import { addFilePrefix } from '@/utils/common.js'
import { getMyEffectiveContracts, getMyPendingSignContracts } from '@/api/index.js'
import { getSessionSnapshot } from '../../utils/app-state'
import { syncLoginStayState } from '../../utils/session-sync'
import { enrichContractDisplayNames } from '../../utils/contract-display'
function pickFirstValue(...values) {
for (let index = 0; index < values.length; index += 1) {
const value = values[index]
if (value === undefined || value === null) {
continue
}
const text = String(value).trim()
if (text) {
return text
}
}
return ''
}
function parseContractList(response) {
const payload = response && response.data ? response.data : response
if (Array.isArray(payload)) {
return payload
}
if (payload && Array.isArray(payload.data)) {
return payload.data
}
if (payload && payload.data && Array.isArray(payload.data.data)) {
return payload.data.data
}
return []
}
function normalizeContract(item = {}) {
return {
@@ -133,74 +167,6 @@ function normalizeContract(item = {}) {
}
export default {
i18n: {
zh: {
navTitle: '我的合同',
overviewTitle: '合同概览',
overviewSubtitle: '查看住宿合同、签约状态与有效期限',
pendingMetricLabel: '待签约/审批',
displayMetricLabel: '当前展示',
loadingTitle: '合同记录加载中',
loadingDesc: '正在同步您的合同数据,请稍候。',
contractTitle: '住宿合同',
relatedOrderLabel: '关联订单',
signStatusLabel: '签署状态',
signed: '已签名',
unsigned: '待签名',
contractPeriodLabel: '合同期限',
recordTipLabel: '记录说明',
mySignatureLabel: '我的签名',
emptyTitle: '暂无对应的合同记录',
emptyDesc: '切换筛选条件,或等待系统生成新的合同数据。',
tabAll: '全部',
tabPending: '待签约',
statusRejected: '已拒绝',
statusPending: '待审批',
statusApproved: '已通过',
statusProcessing: '处理中',
dateRangeJoiner: ' 至 ',
tipApproved: '合同已生效,可查看签约结果。',
tipNeedSign: '待您完成签名后进入审批流程。',
tipPending: '已提交签约信息,等待审批结果。',
tipRejected: '合同未通过审批,请联系管理员处理。',
tipDefault: '请关注合同最新状态。',
actionSign: '去签约',
actionViewDetail: '查看详情'
},
en: {
navTitle: 'My Contracts',
overviewTitle: 'Contract Overview',
overviewSubtitle: 'Check stay contracts, signing status, and effective period',
pendingMetricLabel: 'Pending Sign / Review',
displayMetricLabel: 'Displayed',
loadingTitle: 'Loading contracts',
loadingDesc: 'Syncing your contract data. Please wait.',
contractTitle: 'Stay Contract',
relatedOrderLabel: 'Related Order',
signStatusLabel: 'Signing Status',
signed: 'Signed',
unsigned: 'Unsigned',
contractPeriodLabel: 'Contract Period',
recordTipLabel: 'Notes',
mySignatureLabel: 'My Signature',
emptyTitle: 'No contract record found',
emptyDesc: 'Switch filters or wait for new contract data to be generated.',
tabAll: 'All',
tabPending: 'Pending Sign',
statusRejected: 'Rejected',
statusPending: 'Pending Review',
statusApproved: 'Approved',
statusProcessing: 'Processing',
dateRangeJoiner: ' to ',
tipApproved: 'The contract is effective now. You can view the signing result.',
tipNeedSign: 'Please complete your signature to enter the review process.',
tipPending: 'Signing information has been submitted and is waiting for review.',
tipRejected: 'The contract did not pass review. Please contact the administrator.',
tipDefault: 'Please pay attention to the latest contract status.',
actionSign: 'Sign Now',
actionViewDetail: 'View Detail'
}
},
data() {
return {
currentTab: 'all',
@@ -241,13 +207,10 @@ export default {
}
},
methods: {
t(key, params) {
return this.$tm('pkgService.my_contract.my_contract', key, params)
},
syncTabs() {
this.tabs = [
{ label: this.t('tabAll'), value: 'all' },
{ label: this.t('tabPending'), value: 'pending' }
{ label: this.$tr('全部'), value: 'all' },
{ label: this.$tr('待签约'), value: 'pending' }
]
},
getStoreUser() {
@@ -261,6 +224,67 @@ export default {
const list = this.$store && this.$store.state && this.$store.state.vuex_effective_contracts
return Array.isArray(list) ? list : []
},
resolveContractItem(source) {
if (typeof source === 'number') {
return this.displayList[source] || {}
}
if (source && typeof source === 'object') {
return source
}
return {}
},
resolveCurrentOrderNo(currentOrder = null) {
const storeCurrentOrder = this.$store && this.$store.state && this.$store.state.vuex_current_order
const session = getSessionSnapshot()
const sessionCurrentOrder = session && session.resolvedUser && session.resolvedUser.currentOrder
return pickFirstValue(
currentOrder && (currentOrder.orderNo || currentOrder.no || currentOrder.code || currentOrder.OrderNo),
storeCurrentOrder && (storeCurrentOrder.orderNo || storeCurrentOrder.no || storeCurrentOrder.code || storeCurrentOrder.OrderNo),
sessionCurrentOrder && (sessionCurrentOrder.orderNo || sessionCurrentOrder.no || sessionCurrentOrder.code || sessionCurrentOrder.OrderNo)
)
},
fetchLatestContractsByOrderNo(orderNo = '') {
const normalizedOrderNo = String(orderNo || '').trim()
if (!normalizedOrderNo) {
return Promise.resolve(null)
}
const query = {
OrderNo: normalizedOrderNo,
ProviderType: 'WechatMiniProgram'
}
return Promise.all([
getMyPendingSignContracts(query, this.language).catch(() => []),
getMyEffectiveContracts(query, this.language).catch(() => [])
]).then(([pendingContractsResponse, effectiveContractsResponse]) => {
return {
pendingContracts: parseContractList(pendingContractsResponse),
effectiveContracts: parseContractList(effectiveContractsResponse)
}
})
},
updateContractImageUrl(targetNo = '', imageUrl = '') {
const normalizedNo = String(targetNo || '').trim()
const normalizedImageUrl = addFilePrefix(String(imageUrl || '').trim())
if (!normalizedNo || !normalizedImageUrl) {
return
}
const patchList = (list = []) => list.map((contract) => {
const contractNo = String((contract && (contract.no || contract.contractNo)) || '').trim()
if (contractNo !== normalizedNo) {
return contract
}
return {
...contract,
contentImageUrl: normalizedImageUrl
}
})
this.pendingContracts = patchList(this.pendingContracts)
this.effectiveContracts = patchList(this.effectiveContracts)
},
isCurrentLogin() {
const storeUser = this.getStoreUser()
if (storeUser && Object.keys(storeUser).length) {
@@ -272,12 +296,23 @@ export default {
pendingContracts = [],
effectiveContracts = []
} = {}) {
this.pendingContracts = Array.isArray(pendingContracts)
const normalizedPendingContracts = Array.isArray(pendingContracts)
? pendingContracts.map(normalizeContract)
: []
this.effectiveContracts = Array.isArray(effectiveContracts)
const normalizedEffectiveContracts = Array.isArray(effectiveContracts)
? effectiveContracts.map(normalizeContract)
: []
this.pendingContracts = normalizedPendingContracts
this.effectiveContracts = normalizedEffectiveContracts
Promise.all([
enrichContractDisplayNames(normalizedPendingContracts, this.language),
enrichContractDisplayNames(normalizedEffectiveContracts, this.language)
]).then(([nextPendingContracts, nextEffectiveContracts]) => {
this.pendingContracts = nextPendingContracts
this.effectiveContracts = nextEffectiveContracts
})
},
refreshContracts() {
if (!this.isCurrentLogin()) {
@@ -289,20 +324,32 @@ export default {
this.loading = true
return syncLoginStayState({ force: true })
.then((result = {}) => {
this.applyContractState({
const fallbackState = {
pendingContracts: result.pendingContracts,
effectiveContracts: result.effectiveContracts
})
}
const currentOrderNo = this.resolveCurrentOrderNo(result.currentOrder)
return this.fetchLatestContractsByOrderNo(currentOrderNo)
.then((latestState) => {
this.applyContractState(latestState || fallbackState)
})
})
.catch((error) => {
console.error('refresh contracts failed', error)
const storePendingContracts = this.getStorePendingContracts()
const storeEffectiveContracts = this.getStoreEffectiveContracts()
const session = getSessionSnapshot()
this.applyContractState({
const fallbackState = {
pendingContracts: storePendingContracts.length ? storePendingContracts : session.pendingContracts,
effectiveContracts: storeEffectiveContracts.length ? storeEffectiveContracts : session.effectiveContracts
})
}
const currentOrderNo = this.resolveCurrentOrderNo()
return this.fetchLatestContractsByOrderNo(currentOrderNo)
.then((latestState) => {
this.applyContractState(latestState || fallbackState)
})
})
.finally(() => {
this.loading = false
@@ -316,11 +363,11 @@ export default {
},
getStatusText(status) {
const map = {
'-1': this.t('statusRejected'),
'0': this.t('statusPending'),
'1': this.t('statusApproved')
'-1': this.$tr('已拒绝'),
'0': this.$tr('待审批'),
'1': this.$tr('已通过')
}
return map[String(status)] || this.t('statusProcessing')
return map[String(status)] || this.$tr('处理中')
},
getDisplayStatusText(item = {}) {
if (this.language !== 'en' && item.approvalStatusDesc) {
@@ -336,40 +383,112 @@ export default {
formatDate(value) {
return value ? String(value).slice(0, 10) : '--'
},
getContractKey(item = {}, index = 0) {
return item.no || item.contractNo || item.orderNo || `contract-${index}`
},
formatDateRange(start, end) {
if (!start && !end) {
return '--'
}
return `${this.formatDate(start)}${this.t('dateRangeJoiner')}${this.formatDate(end)}`
return `${this.formatDate(start)}${this.$tr(' 至 ')}${this.formatDate(end)}`
},
getStatusTip(item = {}) {
if (Number(item.approvalStatus) === 1) {
return this.t('tipApproved')
return this.$tr('合同已生效,可查看签约结果。')
}
if (Number(item.approvalStatus) === 0 && !item.signImageUrl) {
return this.t('tipNeedSign')
return this.$tr('待您完成签名后进入审批流程。')
}
if (Number(item.approvalStatus) === 0) {
return this.t('tipPending')
return this.$tr('已提交签约信息,等待审批结果。')
}
if (Number(item.approvalStatus) === -1) {
return this.t('tipRejected')
return this.$tr('合同未通过审批,请联系管理员处理。')
}
return this.t('tipDefault')
return this.$tr('请关注合同最新状态。')
},
resolveActionText(item = {}) {
if (this.shouldShowSignAction(item)) {
return this.t('actionSign')
return this.$tr('去签约')
}
return this.t('actionViewDetail')
return this.$tr('查看合同')
},
shouldShowSignAction(item = {}) {
return Number(item.approvalStatus) === 0 && !item.signImageUrl
},
handleContractAction(item = {}) {
previewContractImage(source = {}) {
const item = this.resolveContractItem(source)
const directImageUrl = addFilePrefix(String(item.contentImageUrl || '').trim())
if (directImageUrl) {
uni.previewImage({
urls: [directImageUrl],
current: directImageUrl
})
return Promise.resolve(directImageUrl)
}
const orderNo = String(item.orderNo || '').trim()
const contractNo = String(item.no || item.contractNo || '').trim()
if (!orderNo) {
uni.showToast({
title: this.$tr('暂无合同图片'),
icon: 'none'
})
return Promise.resolve('')
}
uni.showLoading({
title: this.$tr('加载中')
})
return this.fetchLatestContractsByOrderNo(orderNo)
.then((latestState) => {
const effectiveContracts = latestState && Array.isArray(latestState.effectiveContracts)
? latestState.effectiveContracts
: []
const matchedContract = effectiveContracts.find((contract) => {
const currentNo = String((contract && (contract.no || contract.contractNo)) || '').trim()
const currentOrderNo = String((contract && contract.orderNo) || '').trim()
if (contractNo && currentNo) {
return currentNo === contractNo
}
return currentOrderNo === orderNo
}) || effectiveContracts[0] || null
const imageUrl = addFilePrefix(String(matchedContract && matchedContract.contentImageUrl || '').trim())
if (!imageUrl) {
uni.showToast({
title: this.$tr('暂无合同图片'),
icon: 'none'
})
return ''
}
if (contractNo) {
this.updateContractImageUrl(contractNo, imageUrl)
}
uni.previewImage({
urls: [imageUrl],
current: imageUrl
})
return imageUrl
})
.catch((error) => {
console.error('preview contract image failed', error)
uni.showToast({
title: this.$tr('合同图片加载失败'),
icon: 'none'
})
return ''
})
.finally(() => {
uni.hideLoading()
})
},
handleContractAction(source = {}) {
const item = this.resolveContractItem(source)
const contractNo = encodeURIComponent(item.no || item.contractNo || '')
const orderNo = encodeURIComponent(item.orderNo || '')
const contentImageUrl = encodeURIComponent(item.contentImageUrl || '')
if (Number(item.approvalStatus) === 0 && !item.signImageUrl) {
uni.navigateTo({
@@ -378,9 +497,7 @@ export default {
return
}
uni.navigateTo({
url: `/pkgService/contract_detail/contract_detail?no=${contractNo}&orderNo=${orderNo}&contentImageUrl=${contentImageUrl}`
})
this.previewContractImage(item)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,22 +12,22 @@
<view class="tn-bg-gray--light tn-padding-xl tn-border-radius-circle tn-margin-bottom">
<text class="tn-icon-lock tn-color-gray" style="font-size: 80rpx;"></text>
</view>
<view class="hero-title tn-text-bold">{{ t('lockNoAccessTitle') }}</view>
<view class="hero-desc tn-margin-top-sm tn-text-center">{{ t('lockNoAccessDesc') }}</view>
<view class="hero-title tn-text-bold">{{ $tr('暂无门锁权限') }}</view>
<view class="hero-desc tn-margin-top-sm tn-text-center">{{ $tr('您当前暂未分配智能门锁,或门锁暂未联网。\n入住后即可使用门锁密码、指纹录入与临时密码下发功能。') }}</view>
<view class="tn-flex tn-margin-top-xl tn-width-full tn-flex-row-between">
<view class="btn-light tn-flex-1 tn-margin-right-sm tn-flex tn-flex-col-center tn-flex-row-center tn-padding-sm" style="border-radius: 12rpx; opacity: 0.6;">
<text class="tn-icon-key tn-margin-right-xs"></text> {{ t('tempPassword') }}
<text class="tn-icon-key tn-margin-right-xs"></text> {{ $tr('临时密码') }}
</view>
<view class="btn-primary tn-flex-1 tn-margin-left-sm tn-flex tn-flex-col-center tn-flex-row-center tn-padding-sm" style="border-radius: 12rpx; opacity: 0.6;">
<text class="tn-icon-unlock tn-margin-right-xs"></text> {{ t('oneTapUnlock') }}
<text class="tn-icon-fingerprint tn-margin-right-xs"></text> {{ $tr('录入指纹') }}
</view>
</view>
</view>
<view class="record-card tn-margin-top-lg">
<view class="block-title">{{ t('authRecordTitle') }}</view>
<view class="block-title">{{ $tr('授权记录') }}</view>
<view class="tn-margin-top tn-flex tn-flex-row-center tn-padding-xl">
<tn-empty icon="tn-icon-empty-data" :text="t('lockEmptyRecord')"></tn-empty>
<tn-empty icon="tn-icon-empty-data" :text="$tr('暂无门锁操作记录')"></tn-empty>
</view>
</view>
</view>
@@ -36,16 +36,16 @@
<view v-else-if="pageKey === 'points'">
<view class="hero-card" style="background: linear-gradient(135deg, #FFB703 0%, #FF8A00 100%); color: #fff; border: none;">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-text-md">{{ t('pointsAvailable') }}</view>
<view class="tn-text-sm" style="background: rgba(255,255,255,0.2); padding: 4rpx 16rpx; border-radius: 100rpx;">{{ t('redeemRules') }}</view>
<view class="tn-text-md">{{ $tr('当前可用积分') }}</view>
<view class="tn-text-sm" style="background: rgba(255,255,255,0.2); padding: 4rpx 16rpx; border-radius: 100rpx;">{{ $tr('兑换规则') }}</view>
</view>
<view class="tn-margin-top-lg tn-text-bold" style="font-size: 80rpx;">--</view>
<view class="tn-margin-top tn-flex tn-flex-row-between">
<view class="tn-flex tn-flex-col-center">
<text class="tn-icon-calendar tn-margin-right-xs"></text> {{ t('dailyPoints') }}
<text class="tn-icon-calendar tn-margin-right-xs"></text> {{ $tr('每日签到领积分') }}
</view>
<view class="tn-flex tn-flex-col-center" style="opacity: 0.8;">
{{ t('goCheckIn') }} <text class="tn-icon-right tn-margin-left-xs"></text>
{{ $tr('去签到') }} <text class="tn-icon-right tn-margin-left-xs"></text>
</view>
</view>
</view>
@@ -53,20 +53,20 @@
<view class="tn-flex tn-flex-row-between tn-margin-top-lg">
<view class="btn-light tn-flex-1 tn-margin-right-sm tn-flex tn-flex-direction-column tn-flex-col-center tn-padding-lg" style="border-radius: 20rpx;">
<text class="tn-icon-shop tn-color-orange tn-margin-bottom-sm" style="font-size: 60rpx;"></text>
<text class="tn-text-bold tn-color-black">{{ t('pointsMall') }}</text>
<text class="tn-text-xs tn-color-gray tn-margin-top-xs">{{ t('pointsMallDesc') }}</text>
<text class="tn-text-bold tn-color-black">{{ $tr('积分商城') }}</text>
<text class="tn-text-xs tn-color-gray tn-margin-top-xs">{{ $tr('超值好物兑换') }}</text>
</view>
<view class="btn-light tn-flex-1 tn-margin-left-sm tn-flex tn-flex-direction-column tn-flex-col-center tn-padding-lg" style="border-radius: 20rpx;">
<text class="tn-icon-coupon tn-color-purplered tn-margin-bottom-sm" style="font-size: 60rpx;"></text>
<text class="tn-text-bold tn-color-black">{{ t('coupon') }}</text>
<text class="tn-text-xs tn-color-gray tn-margin-top-xs">{{ t('couponDesc') }}</text>
<text class="tn-text-bold tn-color-black">{{ $tr('优惠券') }}</text>
<text class="tn-text-xs tn-color-gray tn-margin-top-xs">{{ $tr('租金水电抵扣') }}</text>
</view>
</view>
<view class="record-card tn-margin-top-lg">
<view class="block-title">{{ t('pointsRecordTitle') }}</view>
<view class="block-title">{{ $tr('积分明细') }}</view>
<view class="tn-margin-top tn-flex tn-flex-row-center tn-padding-xl">
<tn-empty icon="tn-icon-empty-data" :text="t('pointsEmpty')"></tn-empty>
<tn-empty icon="tn-icon-empty-data" :text="$tr('暂无积分明细')"></tn-empty>
</view>
</view>
</view>
@@ -74,28 +74,28 @@
<!-- 一码通管理 Mockup -->
<view v-else-if="pageKey === 'code'">
<view class="hero-card tn-flex tn-flex-direction-column tn-flex-col-center">
<view class="tn-text-md tn-color-black tn-text-bold">{{ t('accessCodeTitle') }}</view>
<view class="tn-text-xs tn-color-gray tn-margin-top-xs">{{ t('accessCodeDesc') }}</view>
<view class="tn-text-md tn-color-black tn-text-bold">{{ $tr('我的通行二维码') }}</view>
<view class="tn-text-xs tn-color-gray tn-margin-top-xs">{{ $tr('可用于社区大门、单元门及电梯通行') }}</view>
<view class="qr-code-box tn-margin-top-xl tn-margin-bottom-lg tn-flex tn-flex-col-center tn-flex-row-center">
<view class="qr-placeholder tn-flex tn-flex-col-center tn-flex-row-center">
<text class="tn-icon-qr-code tn-color-gray" style="font-size: 200rpx; opacity: 0.2;"></text>
</view>
<view class="qr-mask tn-flex tn-flex-col-center tn-flex-row-center">
<text class="tn-color-white tn-text-bold tn-text-lg">{{ t('noAccessPermission') }}</text>
<text class="tn-color-white tn-text-bold tn-text-lg">{{ $tr('未获取通行权限') }}</text>
</view>
</view>
<view class="tn-text-xs tn-color-gray tn-text-center tn-margin-bottom">{{ t('accessAssignDesc') }}</view>
<view class="tn-text-xs tn-color-gray tn-text-center tn-margin-bottom">{{ $tr('请在完成入住签约并录入人脸/实名信息后\n系统将自动为您分配通行权限') }}</view>
</view>
<view class="record-card tn-margin-top-lg">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view class="block-title">{{ t('visitorAccessTitle') }}</view>
<view class="tn-color-gray tn-text-sm">{{ t('visitorAccessAction') }} <text class="tn-icon-right"></text></view>
<view class="block-title">{{ $tr('访客通行') }}</view>
<view class="tn-color-gray tn-text-sm">{{ $tr('临时访客授权') }} <text class="tn-icon-right"></text></view>
</view>
<view class="tn-margin-top tn-flex tn-flex-row-center tn-padding-xl">
<tn-empty icon="tn-icon-empty-data" :text="t('visitorEmpty')"></tn-empty>
<tn-empty icon="tn-icon-empty-data" :text="$tr('暂无访客通行记录')"></tn-empty>
</view>
</view>
</view>
@@ -103,25 +103,25 @@
<!-- 默认通用占位 -->
<view v-else>
<view class="hero-card">
<view class="hero-badge">{{ t('heroBadge') }}</view>
<view class="hero-title tn-margin-top">{{ resolvedPageTitle || t('comingSoonTitle') }}</view>
<view class="hero-badge">{{ $tr('即将开放') }}</view>
<view class="hero-title tn-margin-top">{{ resolvedPageTitle || $tr('功能建设中') }}</view>
<view class="hero-desc tn-margin-top-sm">{{ resolvedPageDesc || resolvedDefaultDesc }}</view>
</view>
<view class="record-card tn-margin-top-xl">
<view class="block-title">{{ t('currentStatusTitle') }}</view>
<view class="block-title">{{ $tr('当前状态') }}</view>
<view class="empty-text tn-margin-top">
{{ t('currentStatusDesc') }}
{{ $tr('该功能当前仍在建设中,暂未开放正式办理入口。你可以先返回上一页,或回到首页继续查看已开放功能。') }}
</view>
<view class="tip-card tn-margin-top-lg">
<view class="tip-title">{{ t('noteTitle') }}</view>
<view class="tip-desc tn-margin-top-xs">{{ t('noteDesc') }}</view>
<view class="tip-title">{{ $tr('说明') }}</view>
<view class="tip-desc tn-margin-top-xs">{{ $tr('后续接入真实能力后,这里会展示正式的业务入口、状态数据和办理记录。') }}</view>
</view>
<view class="submit-btn btn-primary tn-margin-top-xl" @click="goBack">
<text class="tn-text-bold">{{ t('backPrev') }}</text>
<text class="tn-text-bold">{{ $tr('返回上一页') }}</text>
</view>
<view class="submit-btn btn-light tn-margin-top" @click="goHome">
<text class="tn-text-bold">{{ t('backHome') }}</text>
<text class="tn-text-bold">{{ $tr('返回首页') }}</text>
</view>
</view>
</view>
@@ -143,78 +143,6 @@ function safeDecodeText(value, fallback = '') {
}
export default {
i18n: {
zh: {
defaultPageTitle: '业务办理',
defaultPageDesc: '该能力正在完善中,开放后会同步到个人中心的对应入口。',
lockNoAccessTitle: '暂无门锁权限',
lockNoAccessDesc: '您当前暂未分配智能门锁,或门锁暂未联网。\n入住后即可使用一键开门与临时密码下发功能。',
tempPassword: '临时密码',
oneTapUnlock: '一键开锁',
authRecordTitle: '授权记录',
lockEmptyRecord: '暂无门锁操作记录',
pointsAvailable: '当前可用积分',
redeemRules: '兑换规则',
dailyPoints: '每日签到领积分',
goCheckIn: '去签到',
pointsMall: '积分商城',
pointsMallDesc: '超值好物兑换',
coupon: '优惠券',
couponDesc: '租金水电抵扣',
pointsRecordTitle: '积分明细',
pointsEmpty: '暂无积分明细',
accessCodeTitle: '我的通行二维码',
accessCodeDesc: '可用于社区大门、单元门及电梯通行',
noAccessPermission: '未获取通行权限',
accessAssignDesc: '请在完成入住签约并录入人脸/实名信息后\n系统将自动为您分配通行权限',
visitorAccessTitle: '访客通行',
visitorAccessAction: '临时访客授权',
visitorEmpty: '暂无访客通行记录',
heroBadge: '即将开放',
comingSoonTitle: '功能建设中',
currentStatusTitle: '当前状态',
currentStatusDesc: '该功能当前仍在建设中,暂未开放正式办理入口。你可以先返回上一页,或回到首页继续查看已开放功能。',
noteTitle: '说明',
noteDesc: '后续接入真实能力后,这里会展示正式的业务入口、状态数据和办理记录。',
backPrev: '返回上一页',
backHome: '返回首页'
},
en: {
defaultPageTitle: 'Service Center',
defaultPageDesc: 'This capability is being improved and will appear in the related personal center entry when available.',
lockNoAccessTitle: 'No door lock access',
lockNoAccessDesc: 'No smart lock is assigned yet, or the lock is currently offline.\nAfter check-in, one-tap unlock and temporary password features will be available.',
tempPassword: 'Temporary Code',
oneTapUnlock: 'Unlock Now',
authRecordTitle: 'Access Records',
lockEmptyRecord: 'No door lock records',
pointsAvailable: 'Available Points',
redeemRules: 'Redeem Rules',
dailyPoints: 'Daily check-in rewards',
goCheckIn: 'Check In',
pointsMall: 'Points Mall',
pointsMallDesc: 'Redeem for selected items',
coupon: 'Coupons',
couponDesc: 'Offset rent and utilities',
pointsRecordTitle: 'Points History',
pointsEmpty: 'No points record',
accessCodeTitle: 'My Access QR Code',
accessCodeDesc: 'Use it for community gates, unit doors, and elevators',
noAccessPermission: 'No access permission',
accessAssignDesc: 'After completing check-in signing and face / identity enrollment,\nthe system will automatically assign your access permission.',
visitorAccessTitle: 'Visitor Access',
visitorAccessAction: 'Temporary visitor authorization',
visitorEmpty: 'No visitor access records',
heroBadge: 'Coming Soon',
comingSoonTitle: 'Coming Soon',
currentStatusTitle: 'Current Status',
currentStatusDesc: 'This capability is still under development and is not yet officially available. You can go back or return to the home page to browse available features.',
noteTitle: 'Notes',
noteDesc: 'After the real capability is connected, this page will display the formal service entry, status data, and processing records.',
backPrev: 'Go Back',
backHome: 'Back Home'
}
},
data() {
return {
pageTitleZh: '',
@@ -229,9 +157,9 @@ export default {
computed: {
resolvedPageTitle() {
if (this.$getLanguage() === 'en') {
return this.pageTitleEn || this.pageTitleZh || this.rawPageTitle || this.t('defaultPageTitle')
return this.pageTitleEn || this.pageTitleZh || this.rawPageTitle || this.$tr('业务办理')
}
return this.pageTitleZh || this.pageTitleEn || this.rawPageTitle || this.t('defaultPageTitle')
return this.pageTitleZh || this.pageTitleEn || this.rawPageTitle || this.$tr('业务办理')
},
resolvedPageDesc() {
if (this.$getLanguage() === 'en') {
@@ -240,7 +168,7 @@ export default {
return this.pageDescZh || this.pageDescEn || this.rawPageDesc || ''
},
resolvedDefaultDesc() {
return this.t('defaultPageDesc')
return this.$tr('该能力正在完善中,开放后会同步到个人中心的对应入口。')
}
},
onLoad(options) {
@@ -253,9 +181,6 @@ export default {
this.pageKey = safeDecodeText(options && options.key, '')
},
methods: {
t(key, params) {
return this.$tm('pkgService.service_stub.service_stub', key, params)
},
goBack() {
uni.navigateBack()
},

View File

@@ -1,7 +1,7 @@
<template>
<view class="smart-page">
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent" fontColor="#ffffff">
<view class="tn-text-bold tn-text-xl">{{ t('navTitle') }}</view>
<view class="tn-text-bold tn-text-xl">{{ $tr('全屋智能') }}</view>
</tn-nav-bar>
<view class="header-section" :style="{ paddingTop: (vuex_custom_bar_height + 20) + 'px' }">
@@ -13,31 +13,31 @@
</view>
<view class="toolbar">
<view class="toolbar-tip">{{ t('toolbarTip') }}</view>
<view class="toolbar-tip">{{ $tr('点击设备进入详情并下发控制指令') }}</view>
<view class="toolbar-action" @click="reloadDevices">
<text class="tn-icon-refresh" :class="{'spin-anim': isLoading}" style="margin-right: 8rpx;"></text>
<text>{{ isLoading ? t('loadingShort') : t('refreshStatus') }}</text>
<text>{{ isLoading ? $tr('加载中') : $tr('刷新状态') }}</text>
</view>
</view>
<!-- Feedback States -->
<view v-if="isLoading && !devices.length" class="empty-state">
<view class="loading-ring"></view>
<view class="empty-title">{{ t('loadingTitle') }}</view>
<view class="empty-desc">{{ t('loadingDesc') }}</view>
<view class="empty-title">{{ $tr('正在连接设备') }}</view>
<view class="empty-desc">{{ $tr('正在查询当前房间的可控设备...') }}</view>
</view>
<view v-else-if="loadError && !devices.length" class="empty-state error-state">
<text class="tn-icon-warning empty-icon"></text>
<view class="empty-title">{{ t('loadFailedTitle') }}</view>
<view class="empty-title">{{ $tr('设备加载失败') }}</view>
<view class="empty-desc">{{ loadError }}</view>
<view class="retry-btn" @click="reloadDevices">{{ t('retry') }}</view>
<view class="retry-btn" @click="reloadDevices">{{ $tr('重新加载') }}</view>
</view>
<view v-else-if="!devices.length" class="empty-state">
<text class="tn-icon-empty empty-icon"></text>
<view class="empty-title">{{ t('emptyTitle') }}</view>
<view class="empty-desc">{{ t('emptyDesc') }}</view>
<view class="empty-title">{{ $tr('暂无可控设备') }}</view>
<view class="empty-desc">{{ $tr('当前房间未绑定智能设备') }}</view>
</view>
<scroll-view v-else scroll-y class="device-scroll">
@@ -58,7 +58,7 @@
</view>
<view class="card-bottom">
<view class="device-name">{{ item.deviceName }}</view>
<view class="device-state" :class="{'is-active': item.active}">{{ item.stateText || (item.active ? t('deviceRunning') : t('deviceOff')) }}</view>
<view class="device-state" :class="{'is-active': item.active}">{{ item.stateText || (item.active ? $tr('运行中') : $tr('已关闭')) }}</view>
</view>
</view>
</view>
@@ -126,60 +126,6 @@ function resolveDeviceListPollingInterval(deviceList = [], fallback = 1000) {
}
export default {
i18n: {
zh: {
navTitle: '全屋智能',
toolbarTip: '点击设备进入详情并下发控制指令',
loadingShort: '加载中',
refreshStatus: '刷新状态',
loadingTitle: '正在连接设备',
loadingDesc: '正在查询当前房间的可控设备...',
loadFailedTitle: '设备加载失败',
retry: '重新加载',
emptyTitle: '暂无可控设备',
emptyDesc: '当前房间未绑定智能设备',
deviceSummary: '共 {count} 台设备,可进入详情页控制',
deviceRunning: '运行中',
deviceOff: '已关闭',
currentRoom: '当前房间',
missingRoomCode: '缺少房间编码',
syncingStatus: '正在同步设备状态',
unsupportedQuickControl: '设备暂不支持快捷控制',
unsupportedSwitch: '该设备不支持快捷开关',
missingFunctionCode: '功能编码缺失',
sending: '发送中...',
enabled: '已开启',
disabled: '已关闭',
commandFailed: '指令下发失败',
requestFailed: '设备列表请求失败'
},
en: {
navTitle: 'Smart Home',
toolbarTip: 'Tap a device to open details and send control commands',
loadingShort: 'Loading',
refreshStatus: 'Refresh Status',
loadingTitle: 'Connecting devices',
loadingDesc: 'Checking controllable devices in the current room...',
loadFailedTitle: 'Failed to load devices',
retry: 'Retry',
emptyTitle: 'No controllable devices',
emptyDesc: 'No smart devices are bound to this room',
deviceSummary: '{count} devices available. Tap to control in detail view',
deviceRunning: 'Running',
deviceOff: 'Off',
currentRoom: 'Current Room',
missingRoomCode: 'Missing room code',
syncingStatus: 'Syncing device status',
unsupportedQuickControl: 'Quick control is not supported for this device',
unsupportedSwitch: 'This device does not support quick switching',
missingFunctionCode: 'Missing function code',
sending: 'Sending...',
enabled: 'Enabled',
disabled: 'Disabled',
commandFailed: 'Failed to send command',
requestFailed: 'Failed to request device list'
}
},
data() {
return {
brandId: DEFAULT_BRAND_ID,
@@ -202,19 +148,19 @@ export default {
return this.$getLanguage()
},
displayRoomName() {
return this.roomName || this.roomCode || this.t('currentRoom')
return this.roomName || this.roomCode || this.$tr('当前房间')
},
summaryText() {
if (!this.roomCode) {
return this.t('missingRoomCode')
return this.$tr('缺少房间编码')
}
if (this.isLoading) {
return this.t('syncingStatus')
return this.$tr('正在同步设备状态')
}
if (this.devices.length) {
return this.t('deviceSummary', { count: this.devices.length })
return this.$tr('共 {count} 台设备,可进入详情页控制', { count: this.devices.length })
}
return this.t('emptyTitle')
return this.$tr('暂无可控设备')
}
},
onLoad(options) {
@@ -236,9 +182,6 @@ export default {
this.stopPolling()
},
methods: {
t(key, params) {
return this.$tm('pkgService.smart_control.smart_control', key, params)
},
resolvePageContext(options = {}) {
const session = getSessionSnapshot()
const roomData = (session && session.myRoomData) || {}
@@ -321,7 +264,7 @@ export default {
if (!this.roomCode) {
this.devices = []
this.loadError = this.t('missingRoomCode')
this.loadError = this.$tr('缺少房间编码')
if (stopRefresh) {
uni.stopPullDownRefresh()
}
@@ -361,7 +304,7 @@ export default {
this.devices = []
}
if (!silent) {
this.loadError = (error && (error.message || error.msg)) || this.t('requestFailed')
this.loadError = (error && (error.message || error.msg)) || this.$tr('设备列表请求失败')
}
})
.finally(() => {
@@ -397,7 +340,7 @@ export default {
const functions = getDeviceFunctions(item)
if (!functions || !functions.length) {
uni.showToast({ title: this.t('unsupportedQuickControl'), icon: 'none' })
uni.showToast({ title: this.$tr('设备暂不支持快捷控制'), icon: 'none' })
return
}
@@ -409,13 +352,13 @@ export default {
const category = getControlTypeCategory(targetFunc.controlType)
if (category !== 'switch') {
uni.showToast({ title: this.t('unsupportedSwitch'), icon: 'none' })
uni.showToast({ title: this.$tr('该设备不支持快捷开关'), icon: 'none' })
return
}
const functionCode = String((targetFunc && targetFunc.functionCode) || '').trim()
if (!functionCode) {
uni.showToast({ title: this.t('missingFunctionCode'), icon: 'none' })
uni.showToast({ title: this.$tr('功能编码缺失'), icon: 'none' })
return
}
@@ -433,7 +376,7 @@ export default {
}
this.isSubmitting = true
uni.showLoading({ title: this.t('sending') })
uni.showLoading({ title: this.$tr('发送中...') })
gatewayExecuteDeviceCommand(this.brandId, item.deviceNo, functionCode, commandPayload)
.then(() => {
@@ -447,11 +390,11 @@ export default {
renderKey: this.devices[index].renderKey
})
}
uni.showToast({ title: enabled ? this.t('enabled') : this.t('disabled'), icon: 'success' })
uni.showToast({ title: enabled ? this.$tr('已开启') : this.$tr('已关闭'), icon: 'success' })
})
.catch((error) => {
console.error('execute device command failed', error)
uni.showToast({ title: (error && (error.message || error.msg)) || this.t('commandFailed'), icon: 'none' })
uni.showToast({ title: (error && (error.message || error.msg)) || this.$tr('指令下发失败'), icon: 'none' })
})
.finally(() => {
this.isSubmitting = false

View File

@@ -1,32 +1,40 @@
<template>
<view class="building-selector">
<view
class="selector-header tn-flex tn-flex-row-between tn-flex-col-center"
:class="{ 'selector-header--disabled': !areaCode }"
class="selector-card tn-flex tn-flex-row-between tn-flex-col-center"
:class="{ 'selector-card--disabled': !areaCode }"
@click="handleOpen"
>
<view class="selected-info">
<view class="selected-label">{{ $tr('楼栋') }}</view>
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedBuilding }">
{{ selectedBuilding ? getBuildingDisplayName(selectedBuilding) : placeholderText }}
</view>
</view>
<text class="tn-icon-right" :class="areaCode ? 'tn-color-gray' : 'tn-color-disabled'"></text>
<view class="selector-arrow">
<text class="tn-icon-right"></text>
</view>
</view>
<tn-popup v-model="showPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('popupTitle') }}</text>
<view class="popup-title">{{ $tr('选择意向楼栋') }}</view>
<view class="popup-subtitle">{{ $tr('选择您期望入住的楼栋') }}</view>
</view>
<scroll-view scroll-y class="building-list-scroll">
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
<tn-loading mode="circle"></tn-loading>
<view class="tn-margin-top-sm tn-color-gray">{{ t('loadingText') }}</view>
<tn-loading mode="circle" :size="48"></tn-loading>
<view class="tn-margin-top tn-color-gray">{{ $tr('楼栋加载中...') }}</view>
</view>
<view v-else-if="!buildingList.length" class="empty-state tn-padding-xl tn-text-center">
<text class="tn-color-gray">{{ t('emptyText') }}</text>
<view class="empty-icon">
<text class="tn-icon-empty-data"></text>
</view>
<view class="empty-title">{{ $tr('暂无可选楼栋') }}</view>
<view class="empty-desc">{{ $tr('当前暂无可用楼栋,请稍后再试') }}</view>
</view>
<view v-else class="building-list">
@@ -37,8 +45,13 @@
:class="{ 'is-active': selectedBuilding && selectedBuilding.code === item.code }"
@click="handleSelect(item)"
>
<text class="building-name">{{ getBuildingDisplayName(item) }}</text>
<text v-if="selectedBuilding && selectedBuilding.code === item.code" class="tn-icon-success-circle-fill building-check"></text>
<view class="building-name-left tn-flex tn-flex-col-center">
<view class="building-icon-wrap">
<text class="tn-icon-building"></text>
</view>
<text class="building-name">{{ getBuildingDisplayName(item) }}</text>
</view>
<text v-if="selectedBuilding && selectedBuilding.code === item.code" class="tn-icon-check building-check"></text>
</view>
</view>
</scroll-view>
@@ -97,7 +110,7 @@ export default {
return this.$getLanguage ? this.$getLanguage() : 'zh'
},
placeholderText() {
return this.areaCode ? this.t('selectBuilding') : this.t('selectAreaFirst')
return this.areaCode ? this.$tr('请选择楼栋') : this.$tr('请先选择区域')
}
},
watch: {
@@ -120,13 +133,10 @@ export default {
this.loadData()
},
methods: {
t(key, params) {
return this.$tm('pkgService.submit_request.components.BuildingSelector', key, params)
},
handleOpen() {
if (!this.areaCode) {
uni.showToast({
title: this.t('selectAreaFirst'),
title: this.$tr('请先选择区域'),
icon: 'none'
})
return
@@ -189,7 +199,7 @@ export default {
this.buildingList = []
this.selectedBuilding = null
uni.showToast({
title: this.t('loadFailed'),
title: this.$tr('楼栋加载失败'),
icon: 'none'
})
})
@@ -221,20 +231,32 @@ export default {
</script>
<style scoped lang="scss">
$text-primary: #1A2233;
$text-secondary: #6A7A99;
$text-tertiary: #A1B0C4;
$surface: #FFFFFF;
$bg-popup: #F8FAFF;
.building-selector {
width: 100%;
}
.selector-header {
background: #FFFFFF;
.selector-card {
background: $surface;
border-radius: 20rpx;
padding: 26rpx 32rpx;
border: 1rpx solid rgba($brand-primary, 0.15);
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
min-height: 100rpx;
padding: 26rpx 30rpx;
border: 2rpx solid rgba($brand-primary, 0.12);
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.05);
min-height: 120rpx;
transition: all 0.25s ease;
}
.selector-header--disabled {
.selector-card:active {
background: #FAFCFF;
border-color: rgba($brand-primary, 0.25);
}
.selector-card--disabled {
opacity: 0.72;
}
@@ -243,41 +265,104 @@ export default {
overflow: hidden;
}
.selected-label {
font-size: 22rpx;
color: $text-tertiary;
margin-bottom: 8rpx;
}
.selected-name {
font-size: 28rpx;
color: #1A2233;
font-weight: 600;
font-size: 30rpx;
color: $text-primary;
font-weight: 700;
&.is-empty {
color: #A0B0C0;
color: $text-tertiary;
font-weight: normal;
}
}
.selector-arrow {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #F4F8FF;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
flex-shrink: 0;
text {
font-size: 28rpx;
color: $text-secondary;
}
}
.popup-content {
display: flex;
flex-direction: column;
height: 100%;
background: #F8FAFF;
background: $bg-popup;
}
.popup-header {
padding: 40rpx 30rpx 20rpx;
padding: 40rpx 30rpx 28rpx;
text-align: center;
border-bottom: 1px dashed rgba($brand-primary, 0.15);
background: #FFFFFF;
background: $surface;
border-bottom: 1rpx solid rgba($brand-primary, 0.08);
}
.popup-title {
font-size: 32rpx;
font-size: 34rpx;
font-weight: 700;
color: #1A2233;
color: $text-primary;
margin-bottom: 8rpx;
letter-spacing: 1rpx;
}
.popup-subtitle {
font-size: 24rpx;
color: $text-secondary;
}
.building-list-scroll {
flex: 1;
height: 0;
padding: 20rpx 30rpx 60rpx;
padding: 24rpx 30rpx 60rpx;
}
.loading-state,
.empty-state {
padding-top: 120rpx;
}
.empty-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: rgba($brand-primary, 0.08);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24rpx;
text {
font-size: 56rpx;
color: $brand-primary;
}
}
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 24rpx;
color: $text-secondary;
}
.building-list {
@@ -292,23 +377,55 @@ export default {
justify-content: space-between;
background: #FFFFFF;
border-radius: 20rpx;
padding: 28rpx 30rpx;
border: 1rpx solid rgba($brand-primary, 0.12);
transition: all 0.2s ease;
padding: 24rpx 28rpx;
border: 2rpx solid rgba($brand-primary, 0.1);
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
&.is-active {
border-color: rgba($brand-primary, 0.5);
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.08);
background: #FFFFFF;
border-color: $brand-primary;
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.14);
.building-name {
color: $brand-primary;
font-weight: 700;
}
.building-icon-wrap {
background: rgba($brand-primary, 0.15);
}
}
}
.building-name-left {
flex: 1;
min-width: 0;
}
.building-icon-wrap {
width: 44rpx;
height: 44rpx;
border-radius: 12rpx;
background: rgba($brand-primary, 0.08);
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
transition: all 0.25s ease;
text {
font-size: 24rpx;
color: $brand-primary;
}
}
.building-name {
font-size: 28rpx;
color: #1A2233;
color: $text-primary;
}
.building-check {
color: $brand-primary;
font-size: 30rpx;
font-size: 32rpx;
}
</style>

View File

@@ -1,44 +1,40 @@
<template>
<view class="date-range-selector">
<view class="date-item">
<view class="date-label">{{ resolvedStartLabel }}</view>
<picker mode="date" @change="onStartChange">
<view class="date-input" :class="{ 'is-empty': !start }">
{{ start || resolvedStartPlaceholder }}
<view class="date-range-selector tn-flex tn-flex-col-center">
<picker mode="date" @change="onStartChange" class="date-picker date-picker--start">
<view class="date-card" :class="{ 'is-empty': !start, 'is-active': start }">
<view class="date-card-header tn-flex tn-flex-row-between tn-flex-col-center">
<text class="date-label">{{ resolvedStartLabel }}</text>
<view class="date-icon-wrap">
<text class="tn-icon-calendar"></text>
</view>
</view>
</picker>
<view class="date-value">{{ start || resolvedStartPlaceholder }}</view>
</view>
</picker>
<view class="range-connector">
<view class="connector-line"></view>
<text class="connector-text">{{ $tr('至') }}</text>
<view class="connector-line"></view>
</view>
<view class="date-divider">{{ t('dateDivider') }}</view>
<view class="date-item">
<view class="date-label">{{ resolvedEndLabel }}</view>
<picker mode="date" @change="onEndChange">
<view class="date-input" :class="{ 'is-empty': !end }">
{{ end || resolvedEndPlaceholder }}
<picker mode="date" @change="onEndChange" class="date-picker date-picker--end">
<view class="date-card" :class="{ 'is-empty': !end, 'is-active': end }">
<view class="date-card-header tn-flex tn-flex-row-between tn-flex-col-center">
<text class="date-label">{{ resolvedEndLabel }}</text>
<view class="date-icon-wrap">
<text class="tn-icon-calendar"></text>
</view>
</view>
</picker>
</view>
<view class="date-value">{{ end || resolvedEndPlaceholder }}</view>
</view>
</picker>
</view>
</template>
<script>
export default {
name: 'DateRangeSelector',
i18n: {
zh: {
startLabel: '预计入住',
endLabel: '预计离开',
startPlaceholder: '请选择入住日期',
endPlaceholder: '请选择离开日期',
dateDivider: '至'
},
en: {
startLabel: 'Expected Check-in',
endLabel: 'Expected Check-out',
startPlaceholder: 'Select check-in date',
endPlaceholder: 'Select check-out date',
dateDivider: 'to'
}
},
props: {
start: {
type: String,
@@ -67,22 +63,19 @@ export default {
},
computed: {
resolvedStartLabel() {
return this.startLabel || this.t('startLabel')
return this.startLabel || this.$tr('预计入住')
},
resolvedEndLabel() {
return this.endLabel || this.t('endLabel')
return this.endLabel || this.$tr('预计离开')
},
resolvedStartPlaceholder() {
return this.startPlaceholder || this.t('startPlaceholder')
return this.startPlaceholder || this.$tr('请选择入住日期')
},
resolvedEndPlaceholder() {
return this.endPlaceholder || this.t('endPlaceholder')
return this.endPlaceholder || this.$tr('请选择离开日期')
}
},
methods: {
t(key) {
return this.$tm('pkgService.submit_request.components.DateRangeSelector', key)
},
onStartChange(e) {
this.$emit('update:start', e.detail.value)
},
@@ -94,44 +87,92 @@ export default {
</script>
<style scoped lang="scss">
$text-primary: #1A2233;
$text-secondary: #6A7A99;
$text-tertiary: #A1B0C4;
.date-range-selector {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.6);
border-radius: 24rpx;
padding: 24rpx 32rpx;
border: 2rpx solid rgba(226, 232, 240, 0.8);
backdrop-filter: blur(8px);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
width: 100%;
}
.date-item {
.date-picker {
flex: 1;
text-align: center;
min-width: 0;
}
.date-divider {
margin: 0 24rpx;
color: #94A3B8;
font-size: 24rpx;
font-weight: 500;
.date-card {
background: #F4F8FF;
border-radius: 20rpx;
padding: 24rpx;
border: 2rpx solid transparent;
transition: all 0.25s ease;
&.is-active {
background: #FFFFFF;
border-color: rgba($brand-primary, 0.25);
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.08);
.date-value {
color: $text-primary;
font-weight: 700;
}
}
&.is-empty .date-value {
color: $text-tertiary;
font-weight: normal;
}
}
.date-card-header {
margin-bottom: 16rpx;
}
.date-label {
font-size: 22rpx;
color: #64748B;
margin-bottom: 12rpx;
color: $text-secondary;
}
.date-input {
font-size: 28rpx;
color: #1A2B4C;
font-weight: 600;
&.is-empty {
color: #CBD5E1;
font-weight: normal;
.date-icon-wrap {
width: 40rpx;
height: 40rpx;
border-radius: 12rpx;
background: rgba($brand-primary, 0.1);
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 22rpx;
color: $brand-primary;
}
}
.date-value {
font-size: 28rpx;
color: $text-primary;
line-height: 1.4;
}
.range-connector {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 18rpx;
flex-shrink: 0;
}
.connector-line {
width: 2rpx;
height: 24rpx;
background: rgba($brand-primary, 0.25);
}
.connector-text {
font-size: 22rpx;
color: $brand-primary;
font-weight: 700;
margin: 8rpx 0;
}
</style>

View File

@@ -1,31 +1,39 @@
<template>
<view class="floor-selector">
<view
class="selector-header tn-flex tn-flex-row-between tn-flex-col-center"
class="selector-card tn-flex tn-flex-row-between tn-flex-col-center"
@click="handleOpen"
>
<view class="selected-info">
<view class="selected-label">{{ $tr('楼层') }}</view>
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedFloor }">
{{ selectedFloor ? getFloorDisplayName(selectedFloor) : placeholderText }}
</view>
</view>
<text class="tn-icon-right tn-color-gray"></text>
<view class="selector-arrow">
<text class="tn-icon-right"></text>
</view>
</view>
<tn-popup v-model="showPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('popupTitle') }}</text>
<view class="popup-title">{{ $tr('选择意向楼层') }}</view>
<view class="popup-subtitle">{{ $tr('选择您期望入住的楼层') }}</view>
</view>
<scroll-view scroll-y class="floor-list-scroll">
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
<tn-loading mode="circle"></tn-loading>
<view class="tn-margin-top-sm tn-color-gray">{{ t('loadingText') }}</view>
<tn-loading mode="circle" :size="48"></tn-loading>
<view class="tn-margin-top tn-color-gray">{{ $tr('楼层加载中...') }}</view>
</view>
<view v-else-if="!floorList.length" class="empty-state tn-padding-xl tn-text-center">
<text class="tn-color-gray">{{ t('emptyText') }}</text>
<view class="empty-icon">
<text class="tn-icon-empty-data"></text>
</view>
<view class="empty-title">{{ $tr('暂无可选楼层') }}</view>
<view class="empty-desc">{{ $tr('当前暂无可用楼层,请稍后再试') }}</view>
</view>
<view v-else class="floor-list">
@@ -34,8 +42,13 @@
:class="{ 'is-active': !selectedFloor }"
@click="handleClear"
>
<text class="floor-name">{{ t('clearSelection') }}</text>
<text v-if="!selectedFloor" class="tn-icon-success-circle-fill floor-check"></text>
<view class="floor-name-left tn-flex tn-flex-col-center">
<view class="floor-icon-wrap">
<text class="tn-icon-close" style="font-size: 22rpx;"></text>
</view>
<text class="floor-name">{{ $tr('不选择') }}</text>
</view>
<text v-if="!selectedFloor" class="tn-icon-check floor-check"></text>
</view>
<view
v-for="item in floorList"
@@ -44,8 +57,13 @@
:class="{ 'is-active': selectedFloor && selectedFloor.value === item.value }"
@click="handleSelect(item)"
>
<text class="floor-name">{{ getFloorDisplayName(item) }}</text>
<text v-if="selectedFloor && selectedFloor.value === item.value" class="tn-icon-success-circle-fill floor-check"></text>
<view class="floor-name-left tn-flex tn-flex-col-center">
<view class="floor-icon-wrap">
<text class="tn-icon-building"></text>
</view>
<text class="floor-name">{{ getFloorDisplayName(item) }}</text>
</view>
<text v-if="selectedFloor && selectedFloor.value === item.value" class="tn-icon-check floor-check"></text>
</view>
</view>
</scroll-view>
@@ -133,7 +151,7 @@ export default {
return this.$getLanguage ? this.$getLanguage() : 'zh'
},
placeholderText() {
return this.t('selectFloor')
return this.$tr('请选择楼层')
}
},
watch: {
@@ -148,9 +166,6 @@ export default {
this.loadData()
},
methods: {
t(key, params) {
return this.$tm('pkgService.submit_request.components.FloorSelector', key, params)
},
handleOpen() {
this.showPopup = true
this.loadData()
@@ -176,7 +191,7 @@ export default {
this.floorList = []
this.selectedFloor = null
uni.showToast({
title: this.t('loadFailed'),
title: this.$tr('楼层加载失败'),
icon: 'none'
})
})
@@ -215,17 +230,29 @@ export default {
</script>
<style scoped lang="scss">
$text-primary: #1A2233;
$text-secondary: #6A7A99;
$text-tertiary: #A1B0C4;
$surface: #FFFFFF;
$bg-popup: #F8FAFF;
.floor-selector {
width: 100%;
}
.selector-header {
background: #FFFFFF;
.selector-card {
background: $surface;
border-radius: 20rpx;
padding: 26rpx 32rpx;
border: 1rpx solid rgba($brand-primary, 0.15);
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
min-height: 100rpx;
padding: 26rpx 30rpx;
border: 2rpx solid rgba($brand-primary, 0.12);
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.05);
min-height: 120rpx;
transition: all 0.25s ease;
}
.selector-card:active {
background: #FAFCFF;
border-color: rgba($brand-primary, 0.25);
}
.selected-info {
@@ -233,41 +260,104 @@ export default {
overflow: hidden;
}
.selected-label {
font-size: 22rpx;
color: $text-tertiary;
margin-bottom: 8rpx;
}
.selected-name {
font-size: 28rpx;
color: #1A2233;
font-weight: 600;
font-size: 30rpx;
color: $text-primary;
font-weight: 700;
&.is-empty {
color: #A0B0C0;
color: $text-tertiary;
font-weight: normal;
}
}
.selector-arrow {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #F4F8FF;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
flex-shrink: 0;
text {
font-size: 28rpx;
color: $text-secondary;
}
}
.popup-content {
display: flex;
flex-direction: column;
height: 100%;
background: #F8FAFF;
background: $bg-popup;
}
.popup-header {
padding: 40rpx 30rpx 20rpx;
padding: 40rpx 30rpx 28rpx;
text-align: center;
border-bottom: 1px dashed rgba($brand-primary, 0.15);
background: #FFFFFF;
background: $surface;
border-bottom: 1rpx solid rgba($brand-primary, 0.08);
}
.popup-title {
font-size: 32rpx;
font-size: 34rpx;
font-weight: 700;
color: #1A2233;
color: $text-primary;
margin-bottom: 8rpx;
letter-spacing: 1rpx;
}
.popup-subtitle {
font-size: 24rpx;
color: $text-secondary;
}
.floor-list-scroll {
flex: 1;
height: 0;
padding: 20rpx 30rpx 60rpx;
padding: 24rpx 30rpx 60rpx;
}
.loading-state,
.empty-state {
padding-top: 120rpx;
}
.empty-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: rgba($brand-primary, 0.08);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24rpx;
text {
font-size: 56rpx;
color: $brand-primary;
}
}
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 24rpx;
color: $text-secondary;
}
.floor-list {
@@ -282,13 +372,23 @@ export default {
justify-content: space-between;
background: #FFFFFF;
border-radius: 20rpx;
padding: 28rpx 30rpx;
border: 1rpx solid rgba($brand-primary, 0.12);
transition: all 0.2s ease;
padding: 24rpx 28rpx;
border: 2rpx solid rgba($brand-primary, 0.1);
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
&.is-active {
border-color: rgba($brand-primary, 0.5);
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.08);
background: #FFFFFF;
border-color: $brand-primary;
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.14);
.floor-name {
color: $brand-primary;
font-weight: 700;
}
.floor-icon-wrap {
background: rgba($brand-primary, 0.15);
}
}
}
@@ -296,13 +396,35 @@ export default {
border-style: dashed;
}
.floor-name-left {
flex: 1;
min-width: 0;
}
.floor-icon-wrap {
width: 44rpx;
height: 44rpx;
border-radius: 12rpx;
background: rgba($brand-primary, 0.08);
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
transition: all 0.25s ease;
text {
font-size: 24rpx;
color: $brand-primary;
}
}
.floor-name {
font-size: 28rpx;
color: #1A2233;
color: $text-primary;
}
.floor-check {
color: $brand-primary;
font-size: 30rpx;
font-size: 32rpx;
}
</style>

View File

@@ -1,51 +1,72 @@
<template>
<view class="room-type-selector">
<view class="selector-header tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
<view class="selector-card tn-flex tn-flex-row-between tn-flex-col-center" @click="openSelector">
<view class="selected-info">
<view class="selected-label">{{ $tr('意向房型') }}</view>
<view class="selected-name tn-text-ellipsis" :class="{ 'is-empty': !selectedRoomType }">
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : t('selectRoomType') }}
{{ selectedRoomType ? getRoomTypeDisplayName(selectedRoomType) : $tr('请选择意向房型') }}
</view>
<view class="selected-area tn-text-ellipsis" v-if="selectedArea">
<text class="tn-icon-location area-icon"></text>
{{ getAreaDisplayName(selectedArea) }}
</view>
</view>
<text class="tn-icon-right tn-color-gray"></text>
<view class="selector-arrow">
<text class="tn-icon-right"></text>
</view>
</view>
<tn-popup v-model="showPopup" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('popupTitle') }}</text>
<view class="popup-title">{{ $tr('选择意向房型') }}</view>
<view class="popup-subtitle">{{ $tr('选择您心仪的房型与区域') }}</view>
</view>
<scroll-view scroll-y class="area-list-scroll">
<view v-if="isLoading" class="loading-state tn-padding-xl tn-text-center">
<tn-loading mode="circle"></tn-loading>
<view class="tn-margin-top-sm tn-color-gray">{{ t('loadingRoomTypes') }}</view>
<tn-loading mode="circle" :size="48"></tn-loading>
<view class="tn-margin-top tn-color-gray">{{ $tr('房型加载中...') }}</view>
</view>
<view v-else-if="!displayAreaList.length" class="empty-state tn-padding-xl tn-text-center">
<text class="tn-color-gray">{{ t('emptyRoomTypes') }}</text>
<view class="empty-icon">
<text class="tn-icon-empty-data"></text>
</view>
<view class="empty-title">{{ $tr('暂无可选房型') }}</view>
<view class="empty-desc">{{ $tr('当前暂无可用房型,请稍后再试') }}</view>
</view>
<view v-else class="area-group" v-for="(area, index) in displayAreaList" :key="area.code">
<view class="area-title tn-flex tn-flex-row-between tn-flex-col-center" @click="toggleArea(index)">
<view class="tn-flex tn-flex-col-center">
<text class="area-dot"></text>
<text>{{ getAreaDisplayName(area) }}</text>
<view class="area-title-left tn-flex tn-flex-col-center">
<view class="area-icon-wrap">
<text class="tn-icon-home"></text>
</view>
<text class="area-title-text">{{ getAreaDisplayName(area) }}</text>
</view>
<view class="area-toggle">
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'"></text>
</view>
<text :class="collapsedAreas[index] ? 'tn-icon-down' : 'tn-icon-up'" class="tn-color-gray"></text>
</view>
<view class="room-type-grid" v-show="!collapsedAreas[index]">
<view
class="room-type-item"
v-for="room in area.roomTypes"
<view
class="room-type-item"
v-for="room in area.roomTypes"
:key="room.code"
:class="{ 'is-active': selectedRoomType && selectedRoomType.code === room.code }"
@click="handleSelect(area, room)"
>
<view class="room-name tn-text-ellipsis">{{ getRoomTypeDisplayName(room) }}</view>
<view class="room-price" v-if="room.price">¥{{ getPriceValue(room.price) }}{{ t('priceUnit') }}</view>
<view class="room-price" v-if="room.price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ getPriceValue(room.price) }}</text>
<text class="price-unit">{{ $tr('/月') }}</text>
</view>
<view v-else class="room-price room-price--empty">{{ $tr('价格面议') }}</view>
<view class="active-mark">
<text class="tn-icon-check"></text>
</view>
</view>
</view>
</view>
@@ -118,37 +139,34 @@ export default {
this.loadData()
},
methods: {
t(key) {
return this.$translate(this.i18n, key)
},
openSelector() {
this.showPopup = true
this.loadData()
},
loadData(force = false) {
if (!force && this.areaList.length > 0) return
this.isLoading = true
return areaPagination({ PageIndex: 1, PageSize: 100 }, this.language)
.then((res) => {
const rawList = Array.isArray(res?.data?.data) ? res.data.data : (Array.isArray(res?.data) ? res.data : [])
this.areaList = rawList
.map(item => normalizeAreaItem(item))
.filter(area => area.roomTypes && area.roomTypes.length > 0)
// 初始化全部不折叠
const collapsed = {}
this.areaList.forEach((_, index) => {
collapsed[index] = false
})
this.collapsedAreas = collapsed
this.syncSelection()
})
.catch((e) => {
console.error('load room types error', e)
uni.showToast({ title: this.t('loadFailed'), icon: 'none' })
uni.showToast({ title: this.$tr('房型加载失败'), icon: 'none' })
})
.finally(() => {
this.isLoading = false
@@ -156,12 +174,12 @@ export default {
},
syncSelection() {
if (!this.displayAreaList.length) return
let foundArea = null
let foundRoom = null
const previousAreaCode = this.selectedArea && this.selectedArea.code ? this.selectedArea.code : ''
const previousRoomCode = this.selectedRoomType && this.selectedRoomType.code ? this.selectedRoomType.code : ''
// Try to find by room type code
if (this.value) {
for (const area of this.displayAreaList) {
@@ -173,18 +191,18 @@ export default {
}
}
}
// If not found by room, try to find by initial area code
if (!foundRoom && this.initialAreaCode) {
foundArea = this.displayAreaList.find(a => a.code === this.initialAreaCode)
}
if (foundArea) {
this.selectedArea = foundArea
if (foundRoom) {
this.selectedRoomType = foundRoom
}
if (foundRoom) {
if (this.value !== foundRoom.code) {
this.$emit('input', foundRoom.code)
@@ -230,17 +248,30 @@ export default {
</script>
<style scoped lang="scss">
$luxury-gold: #D8B56A;
$text-primary: #1A2233;
$text-secondary: #6A7A99;
$text-tertiary: #A1B0C4;
$surface: #FFFFFF;
$bg-popup: #F8FAFF;
.room-type-selector {
width: 100%;
}
.selector-header {
background: #FFFFFF;
.selector-card {
background: $surface;
border-radius: 20rpx;
padding: 26rpx 32rpx;
border: 1rpx solid rgba($brand-primary, 0.15);
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
min-height: 100rpx;
padding: 26rpx 30rpx;
border: 2rpx solid rgba($brand-primary, 0.12);
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.05);
min-height: 120rpx;
transition: all 0.25s ease;
}
.selector-card:active {
background: #FAFCFF;
border-color: rgba($brand-primary, 0.25);
}
.selected-info {
@@ -248,72 +279,175 @@ export default {
overflow: hidden;
}
.selected-label {
font-size: 22rpx;
color: $text-tertiary;
margin-bottom: 8rpx;
}
.selected-name {
font-size: 28rpx;
color: #1A2233;
font-weight: 600;
font-size: 30rpx;
color: $text-primary;
font-weight: 700;
&.is-empty {
color: #A0B0C0;
color: $text-tertiary;
font-weight: normal;
}
}
.selected-area {
font-size: 24rpx;
color: $text-secondary;
margin-top: 10rpx;
display: flex;
align-items: center;
}
.area-icon {
font-size: 22rpx;
color: #6A7A99;
margin-top: 6rpx;
margin-right: 6rpx;
color: $brand-primary;
}
.selector-arrow {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #F4F8FF;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
flex-shrink: 0;
text {
font-size: 28rpx;
color: $text-secondary;
}
}
.popup-content {
display: flex;
flex-direction: column;
height: 100%;
background: #F8FAFF;
background: $bg-popup;
}
.popup-header {
padding: 40rpx 30rpx 20rpx;
padding: 40rpx 30rpx 28rpx;
text-align: center;
border-bottom: 1px dashed rgba($brand-primary, 0.15);
background: #FFFFFF;
background: $surface;
border-bottom: 1rpx solid rgba($brand-primary, 0.08);
}
.popup-title {
font-size: 32rpx;
font-size: 34rpx;
font-weight: 700;
color: #1A2233;
color: $text-primary;
margin-bottom: 8rpx;
letter-spacing: 1rpx;
}
.popup-subtitle {
font-size: 24rpx;
color: $text-secondary;
}
.area-list-scroll {
flex: 1;
height: 0;
padding: 20rpx 30rpx 60rpx;
padding: 24rpx 30rpx 60rpx;
}
.loading-state,
.empty-state {
padding-top: 120rpx;
}
.empty-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: rgba($brand-primary, 0.08);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24rpx;
text {
font-size: 56rpx;
color: $brand-primary;
}
}
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 24rpx;
color: $text-secondary;
}
.area-group {
margin-bottom: 30rpx;
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
margin-bottom: 28rpx;
background: $surface;
border-radius: 24rpx;
padding: 28rpx;
box-shadow: 0 8rpx 28rpx rgba(26, 34, 51, 0.04);
border: 1rpx solid rgba($brand-primary, 0.06);
}
.area-title {
font-size: 28rpx;
font-weight: 600;
color: #1A2233;
margin-bottom: 20rpx;
margin-bottom: 24rpx;
}
.area-dot {
width: 12rpx;
height: 12rpx;
.area-title-left {
flex: 1;
min-width: 0;
}
.area-icon-wrap {
width: 48rpx;
height: 48rpx;
border-radius: 14rpx;
background: rgba($brand-primary, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
text {
font-size: 26rpx;
color: $brand-primary;
}
}
.area-title-text {
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
}
.area-toggle {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: $brand-primary;
margin-right: 12rpx;
box-shadow: 0 0 8rpx rgba($brand-primary, 0.4);
background: #F4F8FF;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
text {
font-size: 24rpx;
color: $text-secondary;
}
}
.room-type-grid {
@@ -323,36 +457,86 @@ export default {
}
.room-type-item {
padding: 20rpx;
border-radius: 16rpx;
background: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.15);
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
transition: all 0.3s ease;
position: relative;
padding: 24rpx 20rpx;
border-radius: 20rpx;
background: #FAFCFF;
border: 2rpx solid rgba($brand-primary, 0.1);
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
overflow: hidden;
&.is-active {
background: #FFFFFF;
border-color: $brand-primary;
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.14);
.room-name {
color: $brand-primary;
}
.room-price {
color: $brand-primary;
}
.active-mark {
transform: scale(1);
opacity: 1;
}
}
}
.room-name {
font-size: 26rpx;
font-weight: 600;
color: #1A2233;
margin-bottom: 8rpx;
font-size: 28rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: 10rpx;
}
.room-price {
font-size: 24rpx;
color: $text-secondary;
display: flex;
align-items: baseline;
&--empty {
font-size: 22rpx;
color: $text-tertiary;
}
}
.price-symbol {
font-size: 22rpx;
color: #6A7A99;
margin-right: 2rpx;
}
.price-value {
font-size: 28rpx;
font-weight: 700;
margin-right: 4rpx;
}
.price-unit {
font-size: 22rpx;
}
.active-mark {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: $brand-primary;
display: flex;
align-items: center;
justify-content: center;
transform: scale(0);
opacity: 0;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
text {
font-size: 20rpx;
color: #FFFFFF;
}
}
</style>

View File

@@ -11,23 +11,13 @@
{{ item.value }}
</view>
</view>
<view v-else class="empty-tips">{{ loading ? t('loadingText') : resolvedEmptyText }}</view>
<view v-else class="empty-tips">{{ loading ? $tr('标签加载中...') : resolvedEmptyText }}</view>
</view>
</template>
<script>
export default {
name: 'TagSelector',
i18n: {
zh: {
loadingText: '标签加载中...',
emptyText: '暂无标签'
},
en: {
loadingText: 'Loading tags...',
emptyText: 'No tags available'
}
},
props: {
value: {
type: Array,
@@ -48,13 +38,10 @@ export default {
},
computed: {
resolvedEmptyText() {
return this.emptyText || this.t('emptyText')
return this.emptyText || this.$tr('暂无标签')
}
},
methods: {
t(key, params) {
return this.$tm('pkgService.submit_request.components.TagSelector', key, params)
},
toggleTag(key) {
const currentList = [...this.value]
const index = currentList.indexOf(key)
@@ -70,35 +57,38 @@ export default {
</script>
<style scoped lang="scss">
$text-primary: #1A2233;
$text-secondary: #6A7A99;
$text-tertiary: #A1B0C4;
.option-row {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
gap: 20rpx;
}
.option-chip {
padding: 16rpx 28rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.7);
color: #64748B;
font-size: 24rpx;
line-height: 1;
border: 2rpx solid rgba(226, 232, 240, 0.8);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.02);
backdrop-filter: blur(8px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
padding: 0 34rpx;
height: 72rpx;
line-height: 72rpx;
background: #F4F8FF;
border-radius: 36rpx;
font-size: 26rpx;
color: $text-secondary;
border: 2rpx solid transparent;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.option-chip--active {
background: #1A2B4C;
border-color: #1A2B4C;
color: #FFFFFF;
font-weight: 500;
box-shadow: 0 8rpx 24rpx rgba(26, 43, 76, 0.2);
background: linear-gradient(135deg, rgba($brand-primary, 0.1) 0%, rgba($brand-primary, 0.06) 100%);
border-color: rgba($brand-primary, 0.35);
color: $brand-primary;
font-weight: 600;
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.12);
}
.empty-tips {
color: #94A3B8;
color: $text-tertiary;
font-size: 24rpx;
line-height: 1.7;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,17 @@
<template>
<view class="ticket-center-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>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('工单中心') }}</view>
</tn-nav-bar>
<view class="page-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view class="hero-card tn-margin-bottom-lg">
<view class="hero-badge">
<text class="badge-dot"></text>
<text>{{ t('heroBadge') }}</text>
<text>{{ $tr('SERVICE') }}</text>
</view>
<view class="hero-title tn-margin-top">{{ t('heroTitle') }}</view>
<view class="hero-desc tn-margin-top-sm">{{ t('heroDesc') }}</view>
<view class="hero-title tn-margin-top">{{ $tr('住中工单服务') }}</view>
<view class="hero-desc tn-margin-top-sm">{{ $tr('报修、续住、退宿、调宿、送水、清洁和意见建议都统一从这里进入,提交后可继续查看流转记录。') }}</view>
</view>
<view class="service-grid">
@@ -33,20 +33,6 @@
<script>
export default {
name: 'TicketCenterPage',
i18n: {
zh: {
navTitle: '工单中心',
heroBadge: 'SERVICE',
heroTitle: '住中工单服务',
heroDesc: '报修、续住、退宿、调宿、送水、清洁和意见建议都统一从这里进入,提交后可继续查看流转记录。'
},
en: {
navTitle: 'Work Orders',
heroBadge: 'SERVICE',
heroTitle: 'In-stay Ticket Services',
heroDesc: 'Repair, renewal, checkout, room change, water delivery, cleaning, and suggestions are all handled here.'
}
},
data() {
return {
ticketServices: [
@@ -110,9 +96,6 @@ export default {
}
},
methods: {
t(key, params) {
return this.$translate(this.i18n, key, params)
},
getItemText(item, fieldZh, fieldEn) {
return this.$getLanguage && this.$getLanguage() === 'en'
? (item[fieldEn] || item[fieldZh] || '')

View File

@@ -15,7 +15,7 @@
<view class="hero-header tn-margin-bottom-xl">
<view class="hero-badge">
<text class="badge-dot"></text>
<text>{{ t('ticketKicker') }}</text>
<text>{{ $tr('WORK ORDER') }}</text>
</view>
<view class="hero-title">{{ pageTitle }}</view>
<view class="hero-desc">{{ pageDesc }}</view>
@@ -27,35 +27,35 @@
:class="{ 'segment-item--active': activeTab === 'submit' }"
@click="switchTab('submit')"
>
{{ t('submitTab') }}
{{ $tr('提交申请') }}
</view>
<view
class="segment-item"
:class="{ 'segment-item--active': activeTab === 'history' }"
@click="switchTab('history')"
>
{{ t('historyTab') }}
{{ $tr('记录查询') }}
</view>
</view>
<view v-if="activeTab === 'submit'" class="context-card tn-margin-bottom-lg">
<view class="block-title">{{ t('currentStayInfo') }}</view>
<view class="block-title">{{ $tr('当前入住信息') }}</view>
<view class="block-subtitle">{{ currentOrderStatusText }}</view>
<view class="context-grid tn-margin-top">
<view class="context-item">
<view class="context-label">{{ t('roomLabel') }}</view>
<view class="context-label">{{ $tr('当前房间') }}</view>
<view class="context-value">{{ currentRoomText }}</view>
</view>
<view v-if="currentAreaText" class="context-item">
<view class="context-label">{{ t('areaLabel') }}</view>
<view class="context-label">{{ $tr('所属区域') }}</view>
<view class="context-value">{{ currentAreaText }}</view>
</view>
<view v-if="currentLeaseText" class="context-item">
<view class="context-label">{{ t('leaseLabel') }}</view>
<view class="context-label">{{ $tr('租期') }}</view>
<view class="context-value">{{ currentLeaseText }}</view>
</view>
<view v-if="currentRoomTypeText" class="context-item">
<view class="context-label">{{ t('roomTypeLabel') }}</view>
<view class="context-label">{{ $tr('当前房型') }}</view>
<view class="context-value">{{ currentRoomTypeText }}</view>
</view>
</view>
@@ -64,8 +64,8 @@
<view v-if="modeBannerText && activeTab === 'submit'" class="status-card tn-margin-bottom-lg" :class="statusCardClass">
<view class="status-card__title">{{ modeBannerTitle }}</view>
<view class="status-card__desc">{{ modeBannerText }}</view>
<view v-if="fixOnholdMode" class="status-card__action" @click="cancelFixOnhold">{{ t('cancelSupplement') }}</view>
<view v-else-if="transitActionMode" class="status-card__action" @click="cancelTransitAction">{{ t('cancelProcess') }}</view>
<view v-if="fixOnholdMode" class="status-card__action" @click="cancelFixOnhold">{{ $tr('取消补充') }}</view>
<view v-else-if="transitActionMode" class="status-card__action" @click="cancelTransitAction">{{ $tr('退出处理') }}</view>
</view>
<view v-if="activeTab === 'submit'" class="form-card">
@@ -73,7 +73,7 @@
<view class="block-subtitle">{{ submitSectionDesc }}</view>
<view v-if="isChangeService" class="form-item tn-margin-top">
<view class="form-label">{{ t('changeRoomTypeLabel') }}</view>
<view class="form-label">{{ $tr('意向房型') }}</view>
<room-type-selector
v-model="switchForm.roomTypeCode"
:initial-area-code="switchForm.areaCode"
@@ -82,37 +82,37 @@
</view>
<view v-if="isCheckOutService" class="form-item tn-margin-top">
<view class="form-label">{{ t('checkoutTimeLabel') }}</view>
<view class="form-label">{{ $tr('预计退宿时间') }}</view>
<view class="picker-row">
<picker mode="date" :value="form.checkOutDate" @change="onDateChange('checkOutDate', $event)">
<view class="form-picker">{{ form.checkOutDate || t('selectDate') }}</view>
<view class="form-picker">{{ form.checkOutDate || $tr('请选择日期') }}</view>
</picker>
<picker mode="time" :value="form.checkOutTimePart" @change="onTimeChange('checkOutTimePart', $event)">
<view class="form-picker form-picker--time">{{ form.checkOutTimePart || t('selectTime') }}</view>
<view class="form-picker form-picker--time">{{ form.checkOutTimePart || $tr('请选择时间') }}</view>
</picker>
</view>
</view>
<view v-if="isRenewalService" class="form-item tn-margin-top">
<view class="form-label">{{ t('renewalTimeLabel') }}</view>
<view class="form-label">{{ $tr('续住至') }}</view>
<view class="picker-row">
<picker mode="date" :value="form.renewalDate" @change="onDateChange('renewalDate', $event)">
<view class="form-picker">{{ form.renewalDate || t('selectDate') }}</view>
<view class="form-picker">{{ form.renewalDate || $tr('请选择日期') }}</view>
</picker>
<picker mode="time" :value="form.renewalTimePart" @change="onTimeChange('renewalTimePart', $event)">
<view class="form-picker form-picker--time">{{ form.renewalTimePart || t('selectTime') }}</view>
<view class="form-picker form-picker--time">{{ form.renewalTimePart || $tr('请选择时间') }}</view>
</picker>
</view>
</view>
<view v-if="isCleaningService" class="form-item tn-margin-top">
<view class="form-label">{{ t('cleaningTimeLabel') }}</view>
<view class="form-label">{{ $tr('预约清洁时间') }}</view>
<view class="picker-row">
<picker mode="date" :value="form.cleaningDate" @change="onDateChange('cleaningDate', $event)">
<view class="form-picker">{{ form.cleaningDate || t('selectDate') }}</view>
<view class="form-picker">{{ form.cleaningDate || $tr('请选择日期') }}</view>
</picker>
<picker mode="time" :value="form.cleaningTimePart" @change="onTimeChange('cleaningTimePart', $event)">
<view class="form-picker form-picker--time">{{ form.cleaningTimePart || t('selectTime') }}</view>
<view class="form-picker form-picker--time">{{ form.cleaningTimePart || $tr('请选择时间') }}</view>
</picker>
</view>
</view>
@@ -129,26 +129,26 @@
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('remarkLabel') }}</view>
<view class="form-label">{{ $tr('补充说明') }}</view>
<textarea
class="form-textarea form-textarea--remark"
:placeholder="t('remarkPlaceholder')"
:placeholder="$tr('请输入补充说明,帮助工作人员更快处理')"
:maxlength="300"
v-model="form.remark"
></textarea>
</view>
<view v-if="showTransitAssigneeSelector" class="form-item tn-margin-top">
<view class="form-label">{{ t('assigneeLabel') }}</view>
<view class="form-label">{{ $tr('处理目标') }}</view>
<picker mode="selector" :range="transitAssigneePickerLabels" @change="onTransitAssigneeChange">
<view class="form-picker">
{{ transitSelectedAssigneeName || t('selectAssignee') }}
{{ transitSelectedAssigneeName || $tr('请选择处理目标') }}
</view>
</picker>
</view>
<view v-if="fixOnholdMode && fixOnholdFields.length" class="form-item tn-margin-top">
<view class="form-label">{{ t('supplementLabel') }}</view>
<view class="form-label">{{ $tr('补充业务信息') }}</view>
<view
v-for="(item, index) in fixOnholdFields"
:key="`${item.key}-${index}`"
@@ -159,20 +159,20 @@
v-if="shouldUseTextarea(item.value)"
class="form-textarea form-textarea--small"
v-model="item.value"
:placeholder="t('supplementPlaceholder', { label: item.title })"
:placeholder="$tr('请输入{label}', { label: item.title })"
></textarea>
<input
v-else
class="form-input"
type="text"
v-model="item.value"
:placeholder="t('supplementPlaceholder', { label: item.title })"
:placeholder="$tr('请输入{label}', { label: item.title })"
/>
</view>
</view>
<view class="form-item tn-margin-top">
<view class="form-label">{{ t('attachmentsLabel') }}<text v-if="isRepairService" class="required-mark"> *</text></view>
<view class="form-label">{{ $tr('附件图片') }}<text v-if="isRepairService" class="required-mark"> *</text></view>
<tn-image-upload
:key="uploadInstanceKey"
:fileList="form.photos"
@@ -181,7 +181,8 @@
:formData="uploadFormData"
:max-count="6"
:max-size="5 * 1024 * 1024"
uploadText=""
:uploadText="''"
:customBtn="true"
@on-success="onUploadSuccess"
@on-list-change="onPhotoListChange"
@on-remove="onRemoveImage"
@@ -192,7 +193,7 @@
<template #addBtn>
<view class="upload-trigger">
<text class="tn-icon-camera-fill upload-trigger__icon"></text>
<view class="upload-trigger__text">{{ t('uploadImage') }}</view>
<view class="upload-trigger__text">{{ $tr('上传图片') }}</view>
</view>
</template>
</tn-image-upload>
@@ -207,13 +208,13 @@
<input
v-model="searchKeyword"
class="search-box__input"
:placeholder="t('searchPlaceholder')"
:placeholder="$tr('搜索工单标题或内容')"
confirm-type="search"
@confirm="refreshHistory"
/>
</view>
<view class="refresh-mini-btn" @click="refreshHistory">
{{ historyLoading ? t('refreshing') : t('refresh') }}
{{ historyLoading ? $tr('刷新中...') : $tr('刷新') }}
</view>
</view>
@@ -249,26 +250,26 @@
</view>
<view class="history-card__body tn-margin-top">
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('submittedAtLabel') }}</text>
<text class="info-label">{{ $tr('提交时间') }}</text>
<text class="info-value">{{ formatDateTime(item.createTime) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('contentLabel') }}</text>
<text class="info-label">{{ $tr('申请内容') }}</text>
<text class="info-value info-value--multiline">{{ item.content || item.remark || '--' }}</text>
</view>
</view>
</view>
<view class="load-state">
<text v-if="historyLoading">{{ t('loadingMore') }}</text>
<text v-else-if="!hasNext">{{ t('noMore') }}</text>
<text v-else @click="loadMoreHistory">{{ t('loadMore') }}</text>
<text v-if="historyLoading">{{ $tr('加载中...') }}</text>
<text v-else-if="!hasNext">{{ $tr('没有更多了') }}</text>
<text v-else @click="loadMoreHistory">{{ $tr('加载更多') }}</text>
</view>
</view>
<view v-else-if="!historyLoading" class="empty-panel tn-padding-lg">
<view class="empty-title">{{ t('emptyHistoryTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('emptyHistoryDesc') }}</view>
<view class="empty-title">{{ $tr('暂无工单记录') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ $tr('当前类型还没有工单记录。') }}</view>
</view>
</view>
</view>
@@ -278,7 +279,7 @@
<view class="floating-submit-bar__inner">
<view class="floating-submit-tip">{{ submitFooterTip }}</view>
<view class="submit-btn btn-primary" :class="{ 'submit-btn--disabled': submitting }" @click="submitForm">
<text class="tn-text-bold">{{ submitting ? t('submitting') : submitButtonText }}</text>
<text class="tn-text-bold">{{ submitting ? $tr('提交中...') : submitButtonText }}</text>
</view>
</view>
</view>
@@ -298,10 +299,10 @@
<view class="flow-card__status">{{ getFlowStatusText(item) }}</view>
<view class="flow-card__time tn-margin-top-xs">{{ formatDateTime(item.createTime) }}</view>
</view>
<view v-if="isCurrentFlowItem(index)" class="flow-card__tag">{{ t('currentFlowTag') }}</view>
<view v-if="isCurrentFlowItem(index)" class="flow-card__tag">{{ $tr('当前节点') }}</view>
</view>
<view class="flow-card__content tn-margin-top">{{ item.content || item.remark || t('noFlowContent') }}</view>
<view class="flow-card__content tn-margin-top">{{ item.content || item.remark || $tr('暂无处理内容') }}</view>
<view v-if="item.attachments && item.attachments.length" class="flow-attachments tn-margin-top">
<image
@@ -316,7 +317,7 @@
</view>
<view v-if="transitionOptions.length" class="flow-actions">
<view class="block-title">{{ t('flowActionTitle') }}</view>
<view class="block-title">{{ $tr('可执行操作') }}</view>
<view class="flow-action-list tn-margin-top">
<view
v-for="(item, index) in transitionOptions"
@@ -324,14 +325,14 @@
class="flow-action-btn"
@click="startTransitAction(item, getCurrentFlowItem())"
>
{{ item.nextStatusDesc || t('continueProcess') }}
{{ item.nextStatusDesc || $tr('继续处理') }}
</view>
</view>
</view>
<view v-if="canFixOnhold(currentFlowOrder)" class="flow-actions tn-margin-top">
<view class="flow-action-btn flow-action-btn--ghost" @click="startFixOnhold(currentFlowOrder)">
{{ t('supplementAction') }}
{{ $tr('补充信息') }}
</view>
</view>
</scroll-view>
@@ -426,6 +427,25 @@ function pickReadableValue(...values) {
return ''
}
function isOpaqueCodeValue(value = '') {
const text = String(value || '').trim()
if (!text) {
return false
}
return /^[A-Z0-9-]{12,}$/i.test(text) && !/[\u4e00-\u9fa5\s]/.test(text)
}
function pickReadableLabel(...values) {
for (let index = 0; index < values.length; index += 1) {
const text = String(values[index] || '').trim()
if (!text || isOpaqueCodeValue(text)) {
continue
}
return text
}
return ''
}
function safeJsonParse(value, fallback = null) {
if (typeof value !== 'string') {
return value || fallback
@@ -455,158 +475,6 @@ export default {
components: {
RoomTypeSelector
},
i18n: {
zh: {
areaLabel: '所属区域',
assigneeLabel: '处理目标',
attachmentsLabel: '附件图片',
cancelProcess: '退出处理',
cancelSupplement: '取消补充',
changeRoomTypeLabel: '意向房型',
cleaningTimeLabel: '预约清洁时间',
contentLabel: '申请内容',
currentFlowTag: '当前节点',
currentStayInfo: '当前入住信息',
currentStayOrder: '当前入住订单',
emptyHistoryDesc: '当前类型还没有工单记录。',
emptyHistoryTitle: '暂无工单记录',
flowActionTitle: '可执行操作',
historyTab: '记录查询',
leaseLabel: '租期',
loadMore: '加载更多',
loadingMore: '加载中...',
loadOrderFailed: '入住信息加载失败',
noFlowContent: '暂无处理内容',
noMore: '没有更多了',
refresh: '刷新',
refreshing: '刷新中...',
remarkLabel: '补充说明',
remarkPlaceholder: '请输入补充说明,帮助工作人员更快处理',
roomLabel: '当前房间',
roomTypeLabel: '当前房型',
searchPlaceholder: '搜索工单标题或内容',
selectAssignee: '请选择处理目标',
selectDate: '请选择日期',
selectTime: '请选择时间',
submitSectionDefaultDesc: '以下内容将通过工单接口提交并进入后续流转。',
submitTab: '提交申请',
submitTicket: '提交工单',
submitting: '提交中...',
supplementAction: '补充信息',
supplementLabel: '补充业务信息',
supplementPlaceholder: '请输入{label}',
ticketKicker: 'WORK ORDER',
uploadImage: '上传图片',
uploadImageCountExceeded: '最多上传 6 张图片',
uploadImageFailed: '图片上传失败,请重试',
uploadImageOversize: '图片不能超过 5MB',
uploadTip: '支持上传 JPG、PNG 等图片,单张不超过 5MB,最多 6 张。',
uploadTipRepair: '支持上传 JPG、PNG 等图片,单张不超过 5MB,最多 6 张。报修至少上传 1 张图片。',
checkoutTimeLabel: '预计退宿时间',
renewalTimeLabel: '续住至',
submittedAtLabel: '提交时间',
submitSuccess: '工单提交成功',
submitFailed: '工单提交失败',
confirmSubmitTitle: '确认提交',
confirmSubmitContent: '确认提交当前工单吗?',
missingStayContext: '当前缺少入住订单或房间信息,暂无法提交工单',
repairAttachmentRequired: '请上传报修附件图片',
changeAreaRequired: '请选择意向房型',
checkoutTimeRequired: '请选择退宿时间',
checkoutReasonRequired: '请填写退宿原因',
loadHistoryFailed: '工单记录加载失败',
loadFlowsFailed: '工单流转加载失败',
loadActionsFailed: '工单操作项加载失败',
continueProcess: '继续处理',
loadAssigneeFailed: '处理目标加载失败',
submitSectionActionDesc: '当前工单正在处理中,提交后会继续流转。',
submitSectionSupplementDesc: '当前工单处于挂起状态,补充信息后会继续流转。',
currentStatusPending: '待处理',
currentStatusProcessing: '处理中',
currentStatusResolved: '已解决',
currentStatusClosed: '已结束',
allStatus: '全部',
pendingStatus: '待处理',
processingStatus: '处理中',
resolvedStatus: '已解决',
closedStatus: '已结束'
},
en: {
areaLabel: 'Area',
assigneeLabel: 'Assignee',
attachmentsLabel: 'Attachments',
cancelProcess: 'Cancel Action',
cancelSupplement: 'Cancel Supplement',
changeRoomTypeLabel: 'Preferred Room Type',
cleaningTimeLabel: 'Cleaning Time',
contentLabel: 'Content',
currentFlowTag: 'Current',
currentStayInfo: 'Current Stay',
currentStayOrder: 'Current Stay Order',
emptyHistoryDesc: 'No tickets of this type yet.',
emptyHistoryTitle: 'No Ticket Records',
flowActionTitle: 'Available Actions',
historyTab: 'History',
leaseLabel: 'Lease',
loadMore: 'Load More',
loadingMore: 'Loading...',
loadOrderFailed: 'Failed to load stay context',
noFlowContent: 'No handling content',
noMore: 'No more records',
refresh: 'Refresh',
refreshing: 'Refreshing...',
remarkLabel: 'Notes',
remarkPlaceholder: 'Enter notes to help the staff handle this ticket',
roomLabel: 'Room',
roomTypeLabel: 'Room Type',
searchPlaceholder: 'Search title or content',
selectAssignee: 'Select assignee',
selectDate: 'Select date',
selectTime: 'Select time',
submitSectionDefaultDesc: 'The form below will be submitted through the ticket APIs.',
submitTab: 'Submit',
submitTicket: 'Submit Ticket',
submitting: 'Submitting...',
supplementAction: 'Supplement',
supplementLabel: 'Supplementary Fields',
supplementPlaceholder: 'Please enter {label}',
ticketKicker: 'WORK ORDER',
uploadImage: 'Upload Image',
uploadImageCountExceeded: 'Up to 6 images',
uploadImageFailed: 'Failed to upload image',
uploadImageOversize: 'Image must be less than 5MB',
uploadTip: 'Upload JPG or PNG images, up to 6 images and 5MB each.',
uploadTipRepair: 'Upload JPG or PNG images, up to 6 images and 5MB each. Repair requires at least 1 image.',
checkoutTimeLabel: 'Checkout Time',
renewalTimeLabel: 'Renew Until',
submittedAtLabel: 'Submitted At',
submitSuccess: 'Ticket submitted',
submitFailed: 'Failed to submit ticket',
confirmSubmitTitle: 'Confirm',
confirmSubmitContent: 'Confirm submitting this ticket?',
missingStayContext: 'Missing current stay order or room information',
repairAttachmentRequired: 'Please upload repair images',
changeAreaRequired: 'Please select a room type',
checkoutTimeRequired: 'Please select checkout time',
checkoutReasonRequired: 'Please fill in the checkout reason',
loadHistoryFailed: 'Failed to load ticket history',
loadFlowsFailed: 'Failed to load ticket flows',
loadActionsFailed: 'Failed to load ticket actions',
continueProcess: 'Continue',
loadAssigneeFailed: 'Failed to load assignees',
submitSectionActionDesc: 'The ticket is in progress and will continue after submission.',
submitSectionSupplementDesc: 'The ticket is on hold and will continue after supplementing the information.',
currentStatusPending: 'Pending',
currentStatusProcessing: 'Processing',
currentStatusResolved: 'Resolved',
currentStatusClosed: 'Closed',
allStatus: 'All',
pendingStatus: 'Pending',
processingStatus: 'Processing',
resolvedStatus: 'Resolved',
closedStatus: 'Closed'
}
},
data() {
return {
serviceType: 'TicketRepairApply',
@@ -684,18 +552,20 @@ export default {
return 'rgba(255, 255, 255, 0.96)'
},
navBarBackStyle() {
const isDarkBg = this.scrollTop < 40
return {
transition: 'all 0.2s ease',
background: isDarkBg ? 'rgba(0, 0, 0, 0.2)' : 'transparent',
borderRadius: '50%',
width: '64rpx',
height: '64rpx',
background: this.scrollTop < 40 ? 'rgba(255, 255, 255, 0.82)' : 'rgba(255, 255, 255, 0.96)',
border: '1rpx solid rgba(43, 142, 255, 0.08)',
boxShadow: '0 10rpx 28rpx rgba(31, 76, 145, 0.10)',
backdropFilter: 'blur(18rpx)',
borderRadius: '20rpx',
width: '72rpx',
height: '72rpx',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: '12rpx',
color: isDarkBg ? '#FFFFFF' : '#000000'
color: '#1A2233'
}
},
navTitleOpacity() {
@@ -725,7 +595,12 @@ export default {
) || '--'
},
currentAreaText() {
return pickReadableValue(this.currentOrderDetail.areaName, this.currentOrderDetail.campusName, this.currentOrderDetail.areaCode)
return pickReadableLabel(
this.currentOrderDetail.areaText,
this.currentOrderDetail.areaName,
this.currentOrderDetail.areaLabel,
this.currentOrderDetail.campusName
)
},
currentLeaseText() {
const start = String(this.currentOrderDetail.startDate || this.currentOrderDetail.start || '').slice(0, 10)
@@ -737,11 +612,11 @@ export default {
},
currentOrderStatusText() {
const status = Number(this.currentOrderDetail.orderStatus)
if (Number.isNaN(status)) return this.t('currentStayOrder')
if ([0, 1, 2].includes(status)) return this.t('currentStatusPending')
if ([3, 4, 7].includes(status)) return this.t('currentStatusProcessing')
if ([5, 6].includes(status)) return this.t('currentStatusResolved')
return this.t('currentStatusClosed')
if (Number.isNaN(status)) return this.$tr('当前入住订单')
if ([0, 1, 2].includes(status)) return this.$tr('待处理')
if ([3, 4, 7].includes(status)) return this.$tr('处理中')
if ([5, 6].includes(status)) return this.$tr('已解决')
return this.$tr('已结束')
},
contentFieldLabel() {
if (this.isChangeService) return this.language === 'en' ? 'Change Reason' : '调宿说明'
@@ -776,7 +651,7 @@ export default {
}
},
uploadTipText() {
return this.isRepairService ? this.t('uploadTipRepair') : this.t('uploadTip')
return this.isRepairService ? this.$tr('支持上传 JPG、PNG 等图片,单张不超过 5MB,最多 6 张。报修至少上传 1 张图片。') : this.$tr('支持上传 JPG、PNG 等图片,单张不超过 5MB,最多 6 张。')
},
submitButtonText() {
if (this.fixOnholdMode) {
@@ -785,9 +660,9 @@ export default {
if (this.transitActionMode) {
return this.transitActionOption && this.transitActionOption.nextStatusDesc
? this.transitActionOption.nextStatusDesc
: this.t('continueProcess')
: this.$tr('继续处理')
}
return this.t('submitTicket')
return this.$tr('提交工单')
},
submitSectionTitle() {
if (this.fixOnholdMode) return this.language === 'en' ? 'Supplement Information' : '补充工单信息'
@@ -795,9 +670,9 @@ export default {
return this.pageTitle
},
submitSectionDesc() {
if (this.fixOnholdMode) return this.t('submitSectionSupplementDesc')
if (this.transitActionMode) return this.t('submitSectionActionDesc')
return this.t('submitSectionDefaultDesc')
if (this.fixOnholdMode) return this.$tr('当前工单处于挂起状态,补充信息后会继续流转。')
if (this.transitActionMode) return this.$tr('当前工单正在处理中,提交后会继续流转。')
return this.$tr('以下内容将通过工单接口提交并进入后续流转。')
},
modeBannerTitle() {
if (this.fixOnholdMode) return this.language === 'en' ? 'Supplement Required' : '当前工单需要补充信息'
@@ -808,12 +683,12 @@ export default {
if (this.fixOnholdMode) {
return this.fixOnholdTicket && this.fixOnholdTicket.no
? `${this.fixOnholdTicket.no} ${this.language === 'en' ? 'needs more information before continuing.' : '需要补充信息后继续流转。'}`
: this.t('submitSectionSupplementDesc')
: this.$tr('当前工单处于挂起状态,补充信息后会继续流转。')
}
if (this.transitActionMode) {
return this.transitActionOption && this.transitActionOption.nextStatusDesc
? `${this.language === 'en' ? 'Current action:' : '当前操作:'}${this.transitActionOption.nextStatusDesc}`
: this.t('submitSectionActionDesc')
: this.$tr('当前工单正在处理中,提交后会继续流转。')
}
return ''
},
@@ -827,11 +702,11 @@ export default {
},
historyStatusOptions() {
return [
{ value: '', label: this.t('allStatus') },
{ value: '0', label: this.t('pendingStatus') },
{ value: '3', label: this.t('processingStatus') },
{ value: '5', label: this.t('resolvedStatus') },
{ value: '9', label: this.t('closedStatus') }
{ value: '', label: this.$tr('全部') },
{ value: '0', label: this.$tr('待处理') },
{ value: '3', label: this.$tr('处理中') },
{ value: '5', label: this.$tr('已解决') },
{ value: '9', label: this.$tr('已结束') }
]
},
transitAssigneePickerRange() {
@@ -868,9 +743,6 @@ export default {
this.refreshContext()
},
methods: {
t(key, params) {
return this.$translate(this.i18n, key, params)
},
handleScroll(e) {
this.scrollTop = e.detail.scrollTop
},
@@ -905,7 +777,7 @@ export default {
}
})
.catch(() => {
uni.showToast({ title: this.t('loadOrderFailed'), icon: 'none' })
uni.showToast({ title: this.$tr('入住信息加载失败'), icon: 'none' })
})
},
resetForm() {
@@ -1005,28 +877,28 @@ export default {
},
getSubmitValidationMessage() {
if (this.fixOnholdMode && !this.fixOnholdFlowNo) {
return this.t('submitFailed')
return this.$tr('工单提交失败')
}
if (this.transitActionMode && !this.transitActionFlowNo) {
return this.t('submitFailed')
return this.$tr('工单提交失败')
}
if (!this.fixOnholdMode && !this.transitActionMode && !this.getSubmitOrderNo() && !this.getSubmitRoomCode()) {
return this.t('missingStayContext')
return this.$tr('当前缺少入住订单或房间信息,暂无法提交工单')
}
if (this.isRepairService && !(Array.isArray(this.form.attachments) && this.form.attachments.length)) {
return this.t('repairAttachmentRequired')
return this.$tr('请上传报修附件图片')
}
if (this.isChangeService && !this.switchForm.roomTypeCode) {
return this.t('changeAreaRequired')
return this.$tr('请选择意向房型')
}
if (this.isCheckOutService && !this.resolveCheckOutTime()) {
return this.t('checkoutTimeRequired')
return this.$tr('请选择退宿时间')
}
if (this.isCheckOutService && !String(this.form.content || '').trim()) {
return this.t('checkoutReasonRequired')
return this.$tr('请填写退宿原因')
}
if (this.showTransitAssigneeSelector && !this.transitSelectedAssigneeValue) {
return this.t('selectAssignee')
return this.$tr('请选择处理目标')
}
return ''
},
@@ -1278,8 +1150,8 @@ export default {
new Promise((resolve) => {
uni.showModal({
title: this.t('confirmSubmitTitle'),
content: this.t('confirmSubmitContent'),
title: this.$tr('确认提交'),
content: this.$tr('确认提交当前工单吗?'),
success: (res) => resolve(!!res.confirm),
fail: () => resolve(false)
})
@@ -1299,7 +1171,7 @@ export default {
return this.submitWithCompatibilityFallback(api, this.buildSubmitPayload())
})
.then(() => {
uni.showToast({ title: this.t('submitSuccess'), icon: 'success' })
uni.showToast({ title: this.$tr('工单提交成功'), icon: 'success' })
this.cancelFixOnhold()
this.cancelTransitAction()
this.resetForm()
@@ -1311,7 +1183,7 @@ export default {
return
}
uni.showToast({
title: this.getSubmitErrorText(error) || this.t('submitFailed'),
title: this.getSubmitErrorText(error) || this.$tr('工单提交失败'),
icon: 'none'
})
})
@@ -1360,7 +1232,7 @@ export default {
this.historyList = []
}
this.hasNext = false
uni.showToast({ title: this.t('loadHistoryFailed'), icon: 'none' })
uni.showToast({ title: this.$tr('工单记录加载失败'), icon: 'none' })
})
.finally(() => {
this.historyLoading = false
@@ -1380,10 +1252,10 @@ export default {
return 'closed'
},
statusText(status) {
if ([0, 1, 2].includes(status)) return this.t('pendingStatus')
if ([3, 4, 7].includes(status)) return this.t('processingStatus')
if ([5, 6].includes(status)) return this.t('resolvedStatus')
return this.t('closedStatus')
if ([0, 1, 2].includes(status)) return this.$tr('待处理')
if ([3, 4, 7].includes(status)) return this.$tr('处理中')
if ([5, 6].includes(status)) return this.$tr('已解决')
return this.$tr('已结束')
},
openFlows(item) {
this.currentFlowOrder = item
@@ -1398,7 +1270,7 @@ export default {
return this.loadTransitionOptions(this.getCurrentFlowItem())
})
.catch(() => {
uni.showToast({ title: this.t('loadFlowsFailed'), icon: 'none' })
uni.showToast({ title: this.$tr('工单流转加载失败'), icon: 'none' })
})
.finally(() => {
this.flowLoading = false
@@ -1419,7 +1291,7 @@ export default {
})
.catch(() => {
this.transitionOptions = []
uni.showToast({ title: this.t('loadActionsFailed'), icon: 'none' })
uni.showToast({ title: this.$tr('工单操作项加载失败'), icon: 'none' })
return []
})
},
@@ -1624,7 +1496,7 @@ export default {
this.transitAssigneeGroups = this.normalizeTransitAssigneeGroups(response)
})
.catch(() => {
uni.showToast({ title: this.t('loadAssigneeFailed'), icon: 'none' })
uni.showToast({ title: this.$tr('处理目标加载失败'), icon: 'none' })
})
},
onTransitAssigneeChange(event) {
@@ -1671,13 +1543,13 @@ export default {
this.form.attachments.splice(index, 1)
},
onUploadError() {
uni.showToast({ title: this.t('uploadImageFailed'), icon: 'none' })
uni.showToast({ title: this.$tr('图片上传失败,请重试'), icon: 'none' })
},
onUploadOversize() {
uni.showToast({ title: this.t('uploadImageOversize'), icon: 'none' })
uni.showToast({ title: this.$tr('图片不能超过 5MB'), icon: 'none' })
},
onUploadExceed() {
uni.showToast({ title: this.t('uploadImageCountExceeded'), icon: 'none' })
uni.showToast({ title: this.$tr('最多上传 6 张图片'), icon: 'none' })
},
getAttachmentPreview(attachment) {
return addFilePrefix(pickReadableValue(attachment && attachment.resourceUrl, attachment && attachment.url, attachment && attachment.path))

View File

@@ -1,13 +1,14 @@
<template>
<view class="bill-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>
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('账单管理') }}</view>
</tn-nav-bar>
<view class="bill-body tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '48rpx' }">
<current-application-bill-panel
ref="billPanel"
:reference-type-no="referenceTypeNo"
:auto-resolve-reference="!pendingBillNo || !!referenceTypeNo"
@reference-resolved="handleReferenceResolved"
@all-paid="handleAllPaid"
/>
@@ -16,37 +17,64 @@
</template>
<script>
import { accountQueryBill } from '@/api/index.js'
import { fetchEnums } from '@/utils/enum-store.js'
import { saveActiveApplicationCode, saveHomePageTabId } from '@/utils/app-state'
import CurrentApplicationBillPanel from '../components/CurrentApplicationBillPanel.vue'
export default {
i18n: {
zh: {
navTitle: '账单管理'
},
en: {
navTitle: 'Bill Management'
}
},
components: {
CurrentApplicationBillPanel
},
data() {
return {
referenceTypeNo: ''
referenceTypeNo: '',
pendingBillNo: ''
}
},
async onLoad(options) {
await fetchEnums(false, this.$getLanguage())
this.referenceTypeNo = options && options.referenceTypeNo ? decodeURIComponent(options.referenceTypeNo) : ''
this.pendingBillNo = options && options.billNo ? decodeURIComponent(options.billNo) : ''
if (!this.referenceTypeNo && this.pendingBillNo) {
await this.resolveReferenceTypeNoByBillNo(this.pendingBillNo)
}
if (this.referenceTypeNo) {
saveActiveApplicationCode(this.referenceTypeNo)
}
},
methods: {
t(key, params) {
return this.$tm('pkgService.utility_recharge.utility_recharge', key, params)
extractBillDetail(response) {
const payload = response && response.data ? response.data : response
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
return payload.data
}
return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {}
},
async resolveReferenceTypeNoByBillNo(billNo = '') {
const currentBillNo = String(billNo || '').trim()
if (!currentBillNo) {
return ''
}
try {
const response = await accountQueryBill({ billNo: currentBillNo }, this.$getLanguage())
const detail = this.extractBillDetail(response)
const referenceTypeNo = String(
detail.referenceTypeNo
|| detail.ReferenceTypeNo
|| detail.detailCode
|| detail.applicationCode
|| ''
).trim()
if (referenceTypeNo) {
this.referenceTypeNo = referenceTypeNo
saveActiveApplicationCode(referenceTypeNo)
}
return referenceTypeNo
} catch (error) {
console.error('根据账单号解析申请单号失败', error)
return ''
}
},
handleReferenceResolved(referenceTypeNo) {
this.referenceTypeNo = referenceTypeNo || ''

View File

@@ -7,7 +7,8 @@ import {
normalizeLocale,
pickLocalizedText,
translateGlobalMessages,
translateMessages
translateMessages,
translateText
} from '@/utils/i18n'
import { syncNavigationBarTitle } from '@/utils/navigation-title'
@@ -68,6 +69,9 @@ module.exports = {
$tm(namespace, key, params) {
return translateGlobalMessages(namespace, this.$getLocale(), key, params)
},
$tr(text, params = {}) {
return translateText(text, this.$getLocale(), params)
},
$pickText(zh, en, fallback = '') {
return pickLocalizedText({
zh,

View File

@@ -64,7 +64,8 @@ function logRequestError(name, error) {
}
function resolveOrderNo(order = {}) {
const candidates = [order.orderNo, order.no, order.code, order.OrderNo]
const safeOrder = order || {}
const candidates = [safeOrder.orderNo, safeOrder.no, safeOrder.code, safeOrder.OrderNo]
for (let index = 0; index < candidates.length; index += 1) {
const value = candidates[index]
if (value === undefined || value === null) {
@@ -177,7 +178,7 @@ const store = new Vuex.Store({
? getMyPendingSignContracts({ OrderNo: currentOrderNo, ProviderType: 'WechatMiniProgram' }, language).catch((error) => logRequestError('getMyPendingSignContracts', error))
: Promise.resolve([]),
currentOrderNo
? getMyEffectiveContracts({ OrderNo: currentOrderNo }, language).catch((error) => logRequestError('getMyEffectiveContracts', error))
? getMyEffectiveContracts({ OrderNo: currentOrderNo, ProviderType: 'WechatMiniProgram' }, language).catch((error) => logRequestError('getMyEffectiveContracts', error))
: Promise.resolve([])
])
}).then(([validOrders, currentOrder, pendingContractsResponse, effectiveContractsResponse]) => {

View File

@@ -682,7 +682,7 @@
display: inline-block;
&--custom {
width: 100%;
width: auto;
}
}
}

View File

@@ -77,8 +77,8 @@ function buildRoomDataFromOrder(order = {}) {
order.roomTypeName || order.roomType || ''
].filter(Boolean).join(' · ')
const start = String(order.startDate || order.start || order.checkinDate || '').slice(0, 10)
const end = String(order.endDate || order.end || order.checkoutDate || '').slice(0, 10)
const start = String(order.contractStartTime || order.startDate || order.start || order.checkinDate || '').slice(0, 10)
const end = String(order.contractEndTime || order.endDate || order.end || order.checkoutDate || '').slice(0, 10)
return {
...order,

View File

@@ -11,12 +11,8 @@ export function resolveExistingBookingNavigationTarget(stayState = {}) {
const currentBooking = source.currentBooking || null
const hasCheckedIn = source.hasCheckedIn === true
if (currentBooking && currentBooking.orderNo) {
return `/pkgBooking/booking_detail/booking_detail?orderNo=${encodeURIComponent(currentBooking.orderNo)}`
}
if (currentBooking) {
return '/pkgBooking/booking_detail/booking_detail'
return '/pages/index/index?pageId=stay-service'
}
if (hasCheckedIn) {

View File

@@ -13,11 +13,13 @@ function getGuardText(language = 'zh') {
return {
loginTitle: zh ? '提示' : 'Notice',
loginContent: zh ? '您还未登录,请先登录或注册账号。' : 'Please sign in or register an account first.',
loginCancelText: zh ? '取消' : 'Cancel',
loginConfirmText: zh ? '去登录' : 'Sign In',
existingBookingTitle: zh ? '已有进行中的预订' : 'Existing Booking',
existingBookingContent: zh
? '您当前状态为“{status}”,请先查看预订详情。'
: 'Your current booking status is "{status}". Please review the booking detail first.',
existingBookingCancelText: zh ? '取消' : 'Cancel',
viewBooking: zh ? '查看预订' : 'View Booking'
}
}
@@ -69,6 +71,7 @@ export function ensureBookingAccess(options = {}) {
uni.showModal({
title: options.loginTitle || guardText.loginTitle,
content: options.loginContent || guardText.loginContent,
cancelText: options.loginCancelText || guardText.loginCancelText,
confirmText: options.loginConfirmText || guardText.loginConfirmText,
success: (res) => {
if (res.confirm) {
@@ -89,6 +92,7 @@ export function ensureBookingAccess(options = {}) {
uni.showModal({
title: guardText.existingBookingTitle,
content: guardText.existingBookingContent.replace('{status}', statusText),
cancelText: guardText.existingBookingCancelText,
confirmText: guardText.viewBooking,
success: (res) => {
if (res.confirm) {

View File

@@ -144,22 +144,23 @@ export function formatOrderDate(value) {
}
function normalizeStatusKey(order = {}) {
const rawStatus = String(order.status || order.orderStatus || '').trim()
const safeOrder = order || {}
const rawStatus = String(safeOrder.status || safeOrder.orderStatus || '').trim()
if (['pending_review', 'approved_wait_assign', 'assigned_wait_room', 'rejected', 'contract_signed'].includes(rawStatus)) {
return rawStatus
}
const contractStatus = Number(order.contractStatus)
const contractStatus = Number(safeOrder.contractStatus)
if (contractStatus === 1) {
return 'contract_signed'
}
const approvalStatus = Number(
order.approvalStatus !== undefined
? order.approvalStatus
: order.auditStatus !== undefined
? order.auditStatus
: order.statusCode
safeOrder.approvalStatus !== undefined
? safeOrder.approvalStatus
: safeOrder.auditStatus !== undefined
? safeOrder.auditStatus
: safeOrder.statusCode
)
if (approvalStatus === -1 || /reject|refuse|驳回/i.test(rawStatus)) {
@@ -169,12 +170,12 @@ function normalizeStatusKey(order = {}) {
return 'pending_review'
}
if (
order.roomNo ||
order.roomCode ||
order.roomName ||
order.checkinDate ||
order.startDate ||
order.start
safeOrder.roomNo ||
safeOrder.roomCode ||
safeOrder.roomName ||
safeOrder.checkinDate ||
safeOrder.startDate ||
safeOrder.start
) {
return 'assigned_wait_room'
}
@@ -182,17 +183,18 @@ function normalizeStatusKey(order = {}) {
}
function resolveStatusText(order = {}, statusKey, language = getCurrentLanguage()) {
const safeOrder = order || {}
const contractStatus = Number(order.contractStatus)
if (contractStatus === 1 && order.contractStatusDesc && language !== 'en') {
return order.contractStatusDesc
const contractStatus = Number(safeOrder.contractStatus)
if (contractStatus === 1 && safeOrder.contractStatusDesc && language !== 'en') {
return safeOrder.contractStatusDesc
}
if (language !== 'en' && order.approvalStatusDesc) {
return order.approvalStatusDesc
if (language !== 'en' && safeOrder.approvalStatusDesc) {
return safeOrder.approvalStatusDesc
}
if (language !== 'en' && order.statusDesc) {
return order.statusDesc
if (language !== 'en' && safeOrder.statusDesc) {
return safeOrder.statusDesc
}
const map = {
pending_review: getBookingText('pendingReview', language),
@@ -218,35 +220,39 @@ function resolveTagType(statusKey) {
}
function getBuildingFloorText(order = {}, language = getCurrentLanguage()) {
const safeOrder = order || {}
const isEn = language === 'en'
const building = [
isEn
? (order.buildingNameEn || order.buildingEn || order.buildingSectionNameEn || order.buildingName || order.building || order.buildingSectionName || '')
: (order.buildingName || order.building || order.buildingSectionName || ''),
? (safeOrder.buildingNameEn || safeOrder.buildingEn || safeOrder.buildingSectionNameEn || safeOrder.buildingName || safeOrder.building || safeOrder.buildingSectionName || '')
: (safeOrder.buildingName || safeOrder.building || safeOrder.buildingSectionName || ''),
isEn
? (order.floorNameEn || order.floorEn || order.floorName || order.floor || '')
: (order.floorName || order.floor || '')
? (safeOrder.floorNameEn || safeOrder.floorEn || safeOrder.floorName || safeOrder.floor || '')
: (safeOrder.floorName || safeOrder.floor || '')
].filter(Boolean).join(' ')
return building || '--'
}
function getRoomTypeText(order = {}, language = getCurrentLanguage()) {
const safeOrder = order || {}
const isEn = language === 'en'
if (isEn) {
return order.roomTypeNameEn || order.styleNameEn || order.roomNameEn || order.roomTypeName || order.styleNameZh || order.roomType || order.roomName || '--'
return safeOrder.roomTypeNameEn || safeOrder.styleNameEn || safeOrder.roomNameEn || safeOrder.roomTypeName || safeOrder.styleNameZh || safeOrder.roomType || safeOrder.roomName || '--'
}
return order.roomTypeName || order.styleNameZh || order.roomType || order.roomName || '--'
return safeOrder.roomTypeName || safeOrder.styleNameZh || safeOrder.roomType || safeOrder.roomName || '--'
}
function getOrderNo(order = {}) {
return order.orderNo || order.no || order.code || '--'
const safeOrder = order || {}
return safeOrder.orderNo || safeOrder.no || safeOrder.code || '--'
}
export function getOrderStatusCode(order = {}) {
const safeOrder = order || {}
const candidates = [
order.orderStatus,
order.statusCode,
order.status
safeOrder.orderStatus,
safeOrder.statusCode,
safeOrder.status
]
for (let index = 0; index < candidates.length; index += 1) {
@@ -264,14 +270,15 @@ export function getOrderStatusCode(order = {}) {
}
export function getOrderTime(order = {}) {
const checkInPeriod = order.checkInPeriod || {}
const safeOrder = order || {}
const checkInPeriod = safeOrder.checkInPeriod || {}
const candidates = [
order.updateTime,
order.createTime,
order.createdAt,
order.startDate,
order.start,
order.checkinDate,
safeOrder.updateTime,
safeOrder.createTime,
safeOrder.createdAt,
safeOrder.startDate,
safeOrder.start,
safeOrder.checkinDate,
checkInPeriod.start
]
@@ -287,13 +294,14 @@ export function sortOrdersByLatest(orderList = []) {
}
function hasStayFields(order = {}) {
const safeOrder = order || {}
return !!(
order.roomNo
|| order.roomCode
|| order.slotCode
|| order.contractNo
|| order.actualCheckInTime
|| order.checkinTime
safeOrder.roomNo
|| safeOrder.roomCode
|| safeOrder.slotCode
|| safeOrder.contractNo
|| safeOrder.actualCheckInTime
|| safeOrder.checkinTime
)
}
@@ -336,32 +344,33 @@ export function isCurrentBookingOrder(order = {}) {
}
export function normalizeBookingOrder(order = {}, language = getCurrentLanguage()) {
const statusKey = normalizeStatusKey(order)
const statusCode = getOrderStatusCode(order)
const checkInPeriod = order.checkInPeriod || {}
const startDate = order.startDate || order.start || order.checkinDate || checkInPeriod.start
const endDate = order.endDate || order.end || order.checkoutDate || checkInPeriod.end
const safeOrder = order || {}
const statusKey = normalizeStatusKey(safeOrder)
const statusCode = getOrderStatusCode(safeOrder)
const checkInPeriod = safeOrder.checkInPeriod || {}
const startDate = safeOrder.contractStartTime || safeOrder.startDate || safeOrder.start || safeOrder.checkinDate || checkInPeriod.start
const endDate = safeOrder.contractEndTime || safeOrder.endDate || safeOrder.end || safeOrder.checkoutDate || checkInPeriod.end
return {
...order,
orderNo: getOrderNo(order),
roomTypeText: getRoomTypeText(order, language),
buildingFloorText: getBuildingFloorText(order, language),
...safeOrder,
orderNo: getOrderNo(safeOrder),
roomTypeText: getRoomTypeText(safeOrder, language),
buildingFloorText: getBuildingFloorText(safeOrder, language),
startDateText: formatOrderDate(startDate),
endDateText: formatOrderDate(endDate),
createdAtText: formatOrderDateTime(order.createTime || order.updateTime || order.createdAt),
createdAtText: formatOrderDateTime(safeOrder.createTime || safeOrder.updateTime || safeOrder.createdAt),
roomNoText: language === 'en'
? (order.roomNoEn || order.roomCodeEn || order.roomNameEn || order.roomNo || order.roomCode || order.roomName || '--')
: (order.roomNo || order.roomCode || order.roomName || order.roomNoEn || order.roomCodeEn || order.roomNameEn || '--'),
? (safeOrder.roomNoEn || safeOrder.roomCodeEn || safeOrder.roomNameEn || safeOrder.roomNo || safeOrder.roomCode || safeOrder.roomName || '--')
: (safeOrder.roomNo || safeOrder.roomCode || safeOrder.roomName || safeOrder.roomNoEn || safeOrder.roomCodeEn || safeOrder.roomNameEn || '--'),
areaText: language === 'en'
? (order.areaNameEn || order.campusNameEn || order.areaName || order.campusName || '--')
: (order.areaName || order.campusName || '--'),
rejectReasonText: order.rejectReason || order.approveReason || order.remark || '',
? (safeOrder.areaNameEn || safeOrder.campusNameEn || safeOrder.areaName || safeOrder.campusName || '--')
: (safeOrder.areaName || safeOrder.campusName || '--'),
rejectReasonText: safeOrder.rejectReason || safeOrder.approveReason || safeOrder.remark || '',
startDate,
endDate,
status: statusKey,
statusCode,
statusKey,
statusText: resolveStatusText(order, statusKey, language),
statusText: resolveStatusText(safeOrder, statusKey, language),
tagType: resolveTagType(statusKey)
}
}

112
utils/contract-display.js Normal file
View File

@@ -0,0 +1,112 @@
import { contractDetail } from '@/api/index.js'
const contractNameCache = Object.create(null)
const contractDetailCache = Object.create(null)
function pickFirstText(...values) {
for (let index = 0; index < values.length; index += 1) {
const value = values[index]
if (value === undefined || value === null) continue
const text = String(value).trim()
if (text) return text
}
return ''
}
function parseContractDetailResponse(response) {
const payload = response && response.data ? response.data : response
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
return payload.data
}
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
return payload
}
return {}
}
function resolveContractNameFromSource(source = {}) {
return pickFirstText(
source.contractDisplayName,
source.contractName,
source.contractTemplateName,
source.contractTypeName,
source.contractType,
source.type,
source.typeName,
source.templateName,
source.title
)
}
function resolveContractNo(source = {}) {
return pickFirstText(source.no, source.contractNo)
}
export function getContractDisplayName(source = {}, fallbackText = '') {
const directName = resolveContractNameFromSource(source)
if (directName) return directName
const contractNo = resolveContractNo(source)
if (contractNo && contractNameCache[contractNo]) return contractNameCache[contractNo]
return fallbackText || ''
}
export async function fetchContractDisplayInfo(contractNo = '', language) {
const normalizedContractNo = String(contractNo || '').trim()
if (!normalizedContractNo) {
return {
contractDisplayName: '',
detail: {}
}
}
if (contractDetailCache[normalizedContractNo]) {
const cachedDetail = contractDetailCache[normalizedContractNo]
return {
contractDisplayName: resolveContractNameFromSource(cachedDetail) || contractNameCache[normalizedContractNo] || '',
detail: cachedDetail
}
}
const response = await contractDetail({ contractNo: normalizedContractNo }, language)
const detail = parseContractDetailResponse(response)
const contractDisplayName = resolveContractNameFromSource(detail)
contractDetailCache[normalizedContractNo] = detail
contractNameCache[normalizedContractNo] = contractDisplayName
return {
contractDisplayName,
detail
}
}
export async function enrichContractDisplayNames(list = [], language) {
const sourceList = Array.isArray(list) ? list : []
const pendingNos = [...new Set(
sourceList
.filter(item => !resolveContractNameFromSource(item))
.map(item => resolveContractNo(item))
.filter(Boolean)
)]
await Promise.all(
pendingNos.map((contractNo) =>
fetchContractDisplayInfo(contractNo, language).catch(() => ({
contractDisplayName: '',
detail: {}
}))
)
)
return sourceList.map((item) => {
const contractNo = resolveContractNo(item)
return {
...item,
contractDisplayName: getContractDisplayName(item, '')
|| (contractNo ? contractNameCache[contractNo] : '')
|| ''
}
})
}

View File

@@ -0,0 +1,90 @@
import { sysdictPage } from '@/api/index.js'
export const MOBILE_DEPOSIT_OTHER_PAYMENT_CONFIG_TYPE = 'MobileDepositOtherPaymentConfig'
export const MOBILE_PROVIDER_TYPE = 'WechatMiniProgram'
export const DEPOSIT_OTHER_PAYMENT_CONFIG_KEY = 'depositOtherPayment'
function normalizeText(value) {
return String(value || '').trim()
}
function parseJsonStringMaybe(value) {
const text = normalizeText(value)
if (!text) {
return {}
}
try {
const parsed = JSON.parse(text)
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
} catch (error) {
return {}
}
}
function normalizeRows(response) {
const payload = response && response.data !== undefined ? response.data : response
if (Array.isArray(payload)) {
return payload
}
if (payload && Array.isArray(payload.data)) {
return payload.data
}
if (payload && payload.data && Array.isArray(payload.data.data)) {
return payload.data.data
}
return []
}
function getProviderType(row = {}) {
return normalizeText(row.providerType || row.ProviderType)
}
export function createDefaultDepositOtherPaymentConfig() {
return {
code: '',
key: DEPOSIT_OTHER_PAYMENT_CONFIG_KEY,
phone: '',
qrCode: '',
contentZh: '',
contentEn: ''
}
}
export function normalizeDepositOtherPaymentConfig(row = {}) {
const defaults = createDefaultDepositOtherPaymentConfig()
const extra = parseJsonStringMaybe(row.extraInfo || row.extraConfig || row.ExtraConfig)
return {
code: normalizeText(row.code),
key: normalizeText(row.key) || defaults.key,
phone: normalizeText(extra.phone || row.description),
qrCode: normalizeText(extra.qrCode || extra.qrCodeImage || extra.qrImage),
contentZh: normalizeText(extra.contentZh || extra.content || defaults.contentZh),
contentEn: normalizeText(extra.contentEn || defaults.contentEn)
}
}
function findMatchedRow(rows = []) {
const providerRows = rows.filter(item => getProviderType(item) === MOBILE_PROVIDER_TYPE)
const legacyRows = rows.filter(item => !getProviderType(item))
return (
providerRows.find(item => normalizeText(item && item.key) === DEPOSIT_OTHER_PAYMENT_CONFIG_KEY)
|| providerRows[0]
|| legacyRows.find(item => normalizeText(item && item.key) === DEPOSIT_OTHER_PAYMENT_CONFIG_KEY)
|| legacyRows[0]
|| null
)
}
export function loadDepositOtherPaymentConfig(language) {
return sysdictPage({
Type: MOBILE_DEPOSIT_OTHER_PAYMENT_CONFIG_TYPE,
ProviderType: MOBILE_PROVIDER_TYPE,
PageIndex: 1,
PageSize: 100
}, language).then((response) => {
const rows = normalizeRows(response)
const matchedRow = findMatchedRow(rows)
return normalizeDepositOtherPaymentConfig(matchedRow || {})
})
}

View File

@@ -128,3 +128,103 @@ export function pickLocalizedText(options = {}) {
}
return zhText || options.fallback || enText || ''
}
// === 派生扁平字典(中文 → 英文)===
// 从 globalMessages 的 zh/en 双套结构反向派生 {'中文':'英文'} map,
// 供 translateText / $tr 使用。模块加载后懒加载、进程内缓存。
let __flatDictionary = null
let __dictConflicts = null
// 手动裁决的冲突项:键为中文,值为优先采用的英文。
// 当同一中文在 messages.js 不同位置有不同英文译法时,在此指定通用译法;
// 强上下文相关、无法单一裁决的条目,组件内保留 $tm 作为逃生口。
const DICT_OVERRIDES = {}
function buildFlatDictionary() {
if (__flatDictionary) return __flatDictionary
const dict = Object.create(null)
const conflicts = []
const seen = Object.create(null)
// 1. 优先注入手动裁决项
Object.keys(DICT_OVERRIDES).forEach((zh) => {
dict[zh] = DICT_OVERRIDES[zh]
seen[zh] = { en: DICT_OVERRIDES[zh], path: '<override>' }
})
const zhRoot = globalMessages.zh || globalMessages[ZH_LOCALE] || {}
const enRoot = globalMessages.en || globalMessages[EN_LOCALE] || {}
// 2. 平行 DFS:以 zh 叶子(中文)为 key,同路径 en 叶子为 value
function walk(zhNode, enNode, path) {
if (zhNode && typeof zhNode === 'object' && !Array.isArray(zhNode)) {
const keys = Object.keys(zhNode)
for (let i = 0; i < keys.length; i += 1) {
const k = keys[i]
walk(zhNode[k], enNode ? enNode[k] : undefined, path ? path + '.' + k : k)
}
return
}
const zhText = zhNode == null ? '' : String(zhNode)
if (!zhText) return
const enText = enNode == null ? zhText : String(enNode)
if (seen[zhText]) {
if (seen[zhText].en !== enText) {
conflicts.push({
zh: zhText,
enFirst: seen[zhText].en,
enCurrent: enText,
pathFirst: seen[zhText].path,
pathCurrent: path
})
}
// 首次写入优先:已存在则不覆盖
return
}
dict[zhText] = enText
seen[zhText] = { en: enText, path }
}
walk(zhRoot, enRoot, '')
__flatDictionary = dict
__dictConflicts = conflicts
if (process.env.NODE_ENV !== 'production' && conflicts.length) {
console.warn('[i18n] flat dictionary built with ' + conflicts.length + ' conflicts; call getDictionaryConflicts() for details')
}
return dict
}
// 中文即 key 的翻译函数:中文环境原样返回,英文环境查扁平字典,查不到回退中文。
// 支持与 $tm/$translate 一致的 {param} 占位符替换。
export function translateText(text, locale, params = {}) {
if (text === null || text === undefined) return ''
const zhText = String(text)
if (!zhText) return ''
if (normalizeLocale(locale) !== EN_LOCALE) {
return formatI18nText(zhText, params)
}
const dict = buildFlatDictionary()
const enText = dict[zhText]
if (enText === undefined) {
if (process.env.NODE_ENV !== 'production') {
console.warn('[i18n] missing dictionary entry for: ' + zhText)
}
return formatI18nText(zhText, params)
}
return formatI18nText(enText, params)
}
export function getFlatDictionary() {
return buildFlatDictionary()
}
export function getDictionaryConflicts() {
if (!__dictConflicts) buildFlatDictionary()
return __dictConflicts || []
}

View File

@@ -77,9 +77,18 @@ export function extractLoginSession(response = {}, language = 'zh') {
throw new Error(getText(language, '登录成功但未返回 accessToken', 'Login succeeded but accessToken is missing'))
}
const userCode = responseData.userCode
|| responseData.code
|| nestedData.userCode
|| nestedData.code
|| response.userCode
|| response.code
|| ''
return {
token,
expires
expires,
userCode
}
}
@@ -98,11 +107,13 @@ export function buildLoginSessionUser(options = {}) {
const account = source.account || ''
const password = source.password || ''
const language = source.language || 'zh'
const userCode = source.userCode || ''
if (latestRegistration) {
return buildUserFromRegistrationApplication(latestRegistration, {
password,
language
language,
userCode
})
}
@@ -113,7 +124,8 @@ export function buildLoginSessionUser(options = {}) {
return normalizeUser({
account,
username: account,
password
password,
userCode
})
}
@@ -124,6 +136,7 @@ export function resolveLoginPreparation(options = {}) {
const latestRegistration = source.latestRegistration || null
const account = source.account || ''
const password = source.password || ''
const userCode = source.userCode || ''
if (!latestRegistration) {
return {
@@ -132,7 +145,8 @@ export function resolveLoginPreparation(options = {}) {
matchedLocalUser,
account,
password,
language
language,
userCode
})
}
}
@@ -163,8 +177,9 @@ export function resolveLoginPreparation(options = {}) {
latestRegistration,
matchedLocalUser,
account,
password,
language
password,
language,
userCode
})
}
}

View File

@@ -1,8 +1,9 @@
const RESERVATION_FLOW_STEP = {
INSPECTION: 10,
BILL: 20,
CONTRACT: 30,
CHECKIN: 40
APPLICATION: 1,
INSPECTION: 2,
CONTRACT: 3,
BILL: 4,
CHECKIN: 5
}
function getLanguage(language) {
@@ -89,7 +90,87 @@ export function getReservationCurrentNode(flow = {}) {
return null
}
return nodes.find(item => item.isCurrent) || nodes[nodes.length - 1]
const currentNodes = nodes.filter(item => item.isCurrent)
if (currentNodes.length) {
const currentCheckinNode = currentNodes.find(item => (
Number(item.step || 0) === RESERVATION_FLOW_STEP.CHECKIN
&& String(item.refNo || '').trim()
))
if (currentCheckinNode) {
return currentCheckinNode
}
// 后端偶发会把前序节点也标记成 isCurrent,优先取排序最靠后的当前节点
return currentNodes[currentNodes.length - 1]
}
const checkinNode = nodes.find(item => (
Number(item.step || 0) === RESERVATION_FLOW_STEP.CHECKIN
&& String(item.refNo || '').trim()
))
if (checkinNode) {
return checkinNode
}
return nodes[nodes.length - 1]
}
export function getReservationCurrentFlow(flowList = []) {
const flows = normalizeArray(flowList)
.filter(item => item && typeof item === 'object')
.map(item => ({
flow: item,
currentNode: getReservationCurrentNode(item)
}))
.filter(item => item.currentNode)
if (!flows.length) {
return null
}
const compareNode = (target) => {
const node = target.currentNode || {}
const step = Number(node.step || 0)
const sort = Number(node.sort || 0)
const refNo = String(node.refNo || '').trim()
const isCurrent = !!node.isCurrent
const isCheckinWithRefNo = step === RESERVATION_FLOW_STEP.CHECKIN && !!refNo
return [
isCheckinWithRefNo ? 1 : 0,
isCurrent ? 1 : 0,
step,
sort
]
}
return flows
.sort((a, b) => {
const aScore = compareNode(a)
const bScore = compareNode(b)
for (let index = 0; index < aScore.length; index += 1) {
if (aScore[index] !== bScore[index]) {
return bScore[index] - aScore[index]
}
}
return 0
})[0].flow
}
export function hasEffectiveReservationFlow(flow = {}) {
const currentNode = getReservationCurrentNode(flow)
if (!currentNode) {
return false
}
const step = Number(currentNode.step || 0)
const refNo = String(currentNode.refNo || '').trim()
// 当前步骤为申请单(1)且没有 refNo 时,视为尚未提交预约单
if (step === 1 && !refNo) {
return false
}
return step > 0
}
export function buildReservationFlowMap(flowList = []) {
@@ -131,7 +212,7 @@ export function getReservationStepMeta(step, language = 'zh') {
},
[RESERVATION_FLOW_STEP.BILL]: {
label: currentLanguage === 'en' ? 'Bill' : '账单',
actionText: currentLanguage === 'en' ? 'Go to Bill' : '去缴费'
actionText: currentLanguage === 'en' ? 'Go to Payment' : '去支付'
},
[RESERVATION_FLOW_STEP.CONTRACT]: {
label: currentLanguage === 'en' ? 'Contract' : '合同',

View File

@@ -22,6 +22,11 @@ const BANNER_TEXT_FALLBACK_MAP = {
}
const VALUE_TEXT_FALLBACK_MAP = {
apartment: {
'绿居·和山国际公寓': 'Luju Heshan Intl. Apartments',
'和山国际公寓': 'Heshan Intl. Apartments',
'乾晖里': 'Qianhui Residence'
},
tags: {
'押一付一': 'Monthly Deposit',
'大型超市': 'Large Supermarket',
@@ -148,6 +153,10 @@ export function localizeHousingTag(tag = '', language = getCurrentLanguage()) {
return localizeKnownValue('tags', tag, language)
}
export function localizeApartmentName(name = '', language = getCurrentLanguage()) {
return localizeKnownValue('apartment', name, language)
}
export function localizePayMethodText(payMethod = '', language = getCurrentLanguage()) {
return localizeKnownValue('payMethod', payMethod, language)
}
@@ -330,7 +339,19 @@ export function normalizeBannerItem(item = {}, index = 0) {
export function normalizeImageList(item = {}) {
const candidates = []
const listFields = [item.images, item.imageUrls, item.pictures, item.pictureList]
const listFields = [
item.images,
item.imageUrls,
item.pictures,
item.pictureList,
item.bannerImages,
item.carouselImages,
item.imageList,
item.photos,
item.photoList,
item.album,
item.albums
]
listFields.forEach((field) => {
if (Array.isArray(field)) {
@@ -343,7 +364,19 @@ export function normalizeImageList(item = {}) {
}
})
;[item.imageUrl, item.image, item.cover, item.picture].forEach((image) => {
;[
item.imageUrl,
item.image,
item.cover,
item.coverUrl,
item.picture,
item.poster,
item.posterUrl,
item.banner,
item.bannerUrl,
item.areaImage,
item.areaImageUrl
].forEach((image) => {
const normalized = normalizeBannerImage(image)
if (normalized) {
candidates.push(normalized)

View File

@@ -113,7 +113,8 @@ function pickFirstValue(...values) {
}
function buildPendingContractQuery(order = {}) {
const orderNo = pickFirstValue(order.orderNo, order.no, order.code, order.OrderNo)
const safeOrder = order || {}
const orderNo = pickFirstValue(safeOrder.orderNo, safeOrder.no, safeOrder.code, safeOrder.OrderNo)
if (!orderNo) {
return null
}
@@ -124,11 +125,13 @@ function buildPendingContractQuery(order = {}) {
}
function buildEffectiveContractQuery(order = {}) {
const orderNo = pickFirstValue(order.orderNo, order.no, order.code, order.OrderNo)
const safeOrder = order || {}
const orderNo = pickFirstValue(safeOrder.orderNo, safeOrder.no, safeOrder.code, safeOrder.OrderNo)
if (!orderNo) {
return null
}
return {
ProviderType: 'WechatMiniProgram',
OrderNo: orderNo
}
}

64
utils/todo-route.js Normal file
View File

@@ -0,0 +1,64 @@
function ensureArray(value) {
return Array.isArray(value) ? value : []
}
function pickFirstText() {
for (let index = 0; index < arguments.length; index += 1) {
const value = arguments[index]
if (value === null || value === undefined) continue
const text = String(value).trim()
if (text) return text
}
return ''
}
function normalizeNumber(value, fallback = 0) {
const num = Number(value)
return Number.isFinite(num) ? num : fallback
}
function normalizeTodoEntry(raw) {
if (raw && typeof raw === 'object') {
return {
...raw,
_todoCode: pickFirstText(raw.code, raw.no, raw.contractNo, raw.billNo, raw.orderNo, raw.referenceTypeNo)
}
}
const text = pickFirstText(raw)
return text ? { code: text, _todoCode: text } : null
}
function extractTodoEntries(item) {
const sourceList = [item && item.items, item && item.codes, item && item.list, item && item.data]
for (let index = 0; index < sourceList.length; index += 1) {
const current = sourceList[index]
if (!Array.isArray(current)) continue
return current.map(entry => normalizeTodoEntry(entry)).filter(Boolean)
}
return []
}
export function extractTodoRows(response) {
const payload = response && response.data !== undefined ? response.data : response
if (Array.isArray(payload)) return payload
if (!payload || typeof payload !== 'object') return []
if (Array.isArray(payload.data)) return payload.data
if (payload.data && Array.isArray(payload.data.data)) return payload.data.data
return []
}
export function normalizeTodoList(rows = []) {
return ensureArray(rows).map((item) => {
const entries = extractTodoEntries(item)
const count = Math.max(normalizeNumber(item && item.count, entries.length), entries.length)
return {
todoType: String(item && item.todoType !== undefined ? item.todoType : ''),
todoTypeDesc: pickFirstText(item && item.todoTypeDesc),
count,
entries,
codes: entries
.map(entry => ({ code: pickFirstText(entry && entry._todoCode) }))
.filter(entry => entry.code)
}
}).filter(item => item.count > 0 || item.entries.length > 0 || item.todoTypeDesc)
}