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

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

988 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<view class="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">{{ $tr('申请详情') }}</view>
</tn-nav-bar>
<view class="page-main tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '48rpx' }">
<view class="hero-panel tn-margin-bottom-lg" :class="heroThemeClass">
<view class="hero-top-row">
<view class="hero-badge" :class="heroBadgeClass">{{ applicationTypeText }}</view>
<view class="hero-status" :class="['hero-status--' + statusType]">{{ statusText }}</view>
</view>
<view class="hero-title tn-margin-top">{{ displayTitle }}</view>
<view class="hero-meta-row tn-margin-top">
<view class="hero-meta-chip">
<text class="tn-icon-time tn-margin-right-xs"></text>
<text>{{ formatDateTime(detail.createTime) }}</text>
</view>
<view class="hero-meta-chip">
<text class="tn-icon-location tn-margin-right-xs"></text>
<text>{{ detail.intendedAreaName || '--' }}</text>
</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--info': isWaitingRoomAssignment(detail)
}"
>
<view class="status-reminder-panel__icon">
<text :class="shouldPayDeposit(detail) ? 'tn-icon-warning-fill' : 'tn-icon-time-fill'"></text>
</view>
<view class="status-reminder-panel__content">
<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">{{ $tr('基础信息') }}</view>
<view class="meta-row tn-margin-top">
<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">{{ $tr('复制') }}</text>
</view>
</view>
</view>
<view class="meta-row">
<text class="meta-label">{{ $tr('申请类型') }}</text>
<text class="meta-value">{{ applicationTypeText }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ $tr('当前状态') }}</text>
<text class="meta-value">{{ statusText }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ $tr('申请人') }}</text>
<text class="meta-value">{{ detail.userName || detail.phone || '--' }}</text>
</view>
<view class="meta-row">
<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 ? $tr('意向房型') : $tr('申请房型') }}</text>
<text class="meta-value">{{ detail.intendedRoomTypeName }}</text>
</view>
<view class="meta-row">
<text class="meta-label">{{ isViewingApplication ? $tr('预约时间') : $tr('预计租期') }}</text>
<text class="meta-value">{{ displayDateRange }}</text>
</view>
<view class="meta-row">
<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">{{ $tr('带看信息') }}</view>
<view
v-for="item in viewingMeta"
:key="item.label"
class="meta-row tn-margin-top"
>
<text class="meta-label">{{ item.label }}</text>
<text class="meta-value">{{ item.value }}</text>
</view>
</view>
<view class="content-panel tn-margin-bottom-lg">
<view class="panel-title">{{ isViewingApplication ? $tr('申请说明') : $tr('补充说明') }}</view>
<view class="remark-panel tn-margin-top">
<view
v-for="(line, index) in remarkLines"
:key="`${line}-${index}`"
class="remark-line"
>
{{ line }}
</view>
</view>
</view>
<view class="action-row">
<view class="ghost-btn" @click="goBackList">
<text class="tn-text-bold">{{ $tr('返回列表') }}</text>
</view>
<view
v-if="canCancelOrder"
class="danger-btn"
:class="{ 'danger-btn--loading': canceling }"
@click.stop="handleCancelOrder"
>
<text class="tn-text-bold">{{ canceling ? $tr('取消中...') : $tr('取消订单') }}</text>
</view>
<view
v-if="currentStepActionText"
class="action-btn"
@click.stop="handleCurrentStepAction"
>
<text class="tn-text-bold">{{ currentStepActionText }}</text>
</view>
<view
v-else-if="isReservationCheckinProcessing()"
class="action-btn action-btn--ghost"
@click.stop="refreshCurrentPage"
>
<text class="tn-text-bold">{{ $tr('刷新') }}</text>
</view>
<view
v-else-if="canViewBill"
class="action-btn"
@click.stop="goToBillPage"
>
<text class="tn-text-bold">{{ $tr('查看关联账单') }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { contractDetail, orderSearchedReservationFlow, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
import { getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
import {
RESERVATION_FLOW_STEP,
attachReservationFlow,
buildReservationFlowMap,
getReservationCurrentActionText,
getReservationCurrentStepText,
getReservationOrderNo,
parseReservationFlowResponse
} from '@/utils/reservation-flow'
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间[::]|意向房型[::]|意向区域[::]|viewing request|viewing time[::]|preferred room type[::]|preferred area[::]/i
function isViewingApplicationRecord(item = {}) {
if (!item || typeof item !== 'object') {
return false
}
const typeText = [
item.businessType,
item.applicationType,
item.applicationTypeDesc,
item.formType,
item.bizType,
item.type,
item.typeDesc
]
.filter(value => value !== null && typeof value !== 'undefined' && String(value).trim())
.join(' ')
if (/viewing|看房|带看/i.test(typeText)) {
return true
}
if ([item.appointmentViewingTime, item.viewingTime, item.bookViewingTime].some(value => String(value || '').trim())) {
return true
}
const remark = item && item.remark ? String(item.remark) : ''
return VIEWING_APPLICATION_PATTERN.test(remark)
}
function normalizeApplicationList(list = []) {
if (!Array.isArray(list)) {
return []
}
return list
.filter(item => item && typeof item === 'object')
.map((item) => {
return {
...item,
isViewingApplication: isViewingApplicationRecord(item)
}
})
}
function parseJsonStringMaybe(value) {
if (typeof value !== 'string') {
return value
}
const text = value.trim()
if (!text) {
return value
}
if (!((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']')))) {
return value
}
try {
return JSON.parse(text)
} catch (error) {
return value
}
}
export default {
data() {
return {
detailCode: '',
detail: {},
canceling: false,
reservationContractDetail: null,
reservationInspectionDetail: null
}
},
watch: {
language() {
this.loadReservationFlow()
}
},
computed: {
isViewingApplication() {
return !!(this.detail && this.detail.isViewingApplication)
},
displayTitle() {
if (this.isViewingApplication) {
return this.detail.intendedRoomTypeName || this.$tr('预约看房申请')
}
return this.detail.intendedRoomTypeName || this.$tr('入住申请')
},
applicationTypeText() {
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.$tr('待审核'),
10: this.$tr('已通过'),
20: this.$tr('已拒绝'),
99: this.$tr('已结束')
}
return map[this.detail.status] || this.$tr('处理中')
},
statusType() {
if (this.detail.status === 10) {
return 'approved'
}
if (this.detail.status === 20) {
return 'rejected'
}
if (this.detail.status === 99) {
return 'finished'
}
return 'pending'
},
heroThemeClass() {
return this.isViewingApplication ? 'hero-viewing' : 'hero-stay'
},
heroBadgeClass() {
return this.isViewingApplication ? 'badge-viewing' : 'badge-stay'
},
displayDateRange() {
if (!this.detail.start && !this.detail.end) {
return '--'
}
if (this.isViewingApplication) {
return `${this.formatDateTime(this.detail.start)}${this.$tr(' 至 ')}${this.formatDateTime(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.$tr('暂无补充说明')]
}
return text.split(/\r?\n/).filter(Boolean)
},
viewingMeta() {
if (!this.isViewingApplication) {
return []
}
return this.parseViewingRemark()
},
currentStepActionText() {
if (this.isViewingApplication) {
return ''
}
if (this.isReservationInspectionPendingReview()) {
return ''
}
if (this.isReservationContractAwaitingReview()) {
return ''
}
if (this.isReservationCheckinProcessing()) {
return ''
}
return getReservationCurrentActionText(this.detail, this.language)
},
statusReminderText() {
if (this.isReservationInspectionPendingReview()) {
return this.getReservationInspectionStatusText()
}
if (this.isReservationContractAwaitingReview()) {
return this.getReservationContractStatusText()
}
if (this.isReservationCheckinProcessing()) {
return this.$tr('宿管正在办理入住,请耐心等待')
}
const reservationStepText = getReservationCurrentStepText(this.detail, this.language)
if (reservationStepText) {
return reservationStepText
}
if (this.shouldPayDeposit(this.detail)) {
return this.$tr('请支付定金')
}
if (this.isWaitingRoomAssignment(this.detail)) {
return this.$tr('等待预订中心分配房间')
}
return ''
},
canViewBill() {
return this.shouldPayDeposit(this.detail)
&& !this.isViewingApplication
&& this.detail
&& this.detail.detailCode
&& this.statusType === 'pending'
},
canCancelOrder() {
return this.shouldPayDeposit(this.detail)
&& !this.isViewingApplication
&& this.detail
&& this.detail.detailCode
&& !this.canceling
}
},
onLoad(options = {}) {
this.detailCode = options.code || ''
this.restoreDetail(options.payload || '')
},
onShow() {
this.loadReservationFlow()
},
methods: {
isPaidApplication(item = {}) {
return !item.isViewingApplication && Number(item.payStatus) === 10
},
shouldPayDeposit(item = {}) {
return !item.isViewingApplication
&& Number(item.status) === 0
&& Number(item.payStatus) !== 10
},
isWaitingRoomAssignment(item = {}) {
return !item.isViewingApplication
&& Number(item.status) === 0
&& Number(item.payStatus) === 10
},
getReservationCurrentStepValue() {
return Number(this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode.step : 0)
},
isReservationCheckinProcessing() {
return this.getReservationCurrentStepValue() === RESERVATION_FLOW_STEP.CHECKIN
},
getReservationInspectionNo() {
const currentNode = this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode : {}
if (Number(currentNode.step || 0) !== RESERVATION_FLOW_STEP.INSPECTION) {
return ''
}
return String(currentNode.refNo || '').trim()
},
getReservationContractNo() {
const currentNode = this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode : {}
if (Number(currentNode.step || 0) !== RESERVATION_FLOW_STEP.CONTRACT) {
return ''
}
return String(currentNode.refNo || '').trim()
},
normalizeInspectionDetail(response) {
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
return payload
}
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
return payload.data
}
return {}
},
normalizeContractDetail(response) {
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
return payload
}
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
return payload.data
}
return {}
},
isReservationInspectionPendingReview() {
if (this.getReservationCurrentStepValue() !== RESERVATION_FLOW_STEP.INSPECTION) {
return false
}
const detail = this.reservationInspectionDetail || {}
return Number(detail.status) === 2
},
getReservationInspectionStatusText() {
return this.$tr('inspectionPendingReviewHint')
},
isReservationContractAwaitingReview() {
if (this.getReservationCurrentStepValue() !== RESERVATION_FLOW_STEP.CONTRACT) {
return false
}
const detail = this.reservationContractDetail || {}
const queryStatus = Number(detail.queryStatus)
const approvalStatus = Number(detail.approvalStatus)
const hasSigned = !!String(detail.signTime || detail.signImageUrl || '').trim()
return queryStatus === 1 || (hasSigned && approvalStatus === 0)
},
getReservationContractStatusText() {
const detail = this.reservationContractDetail || {}
return detail.queryStatusDesc || detail.approvalStatusDesc || detail.statusDesc || ''
},
async ensureReservationInspectionDetail() {
const inspectionNo = this.getReservationInspectionNo()
if (!inspectionNo) {
this.reservationInspectionDetail = null
return null
}
try {
const detail = this.normalizeInspectionDetail(await roomInspectionDetail({
inspectionNo
}, this.language))
this.reservationInspectionDetail = detail && Object.keys(detail).length ? detail : null
return this.reservationInspectionDetail
} catch (error) {
console.error('load inspection detail failed', error)
this.reservationInspectionDetail = null
return null
}
},
async ensureReservationContractDetail() {
const contractNo = this.getReservationContractNo()
if (!contractNo) {
this.reservationContractDetail = null
return null
}
try {
const detail = this.normalizeContractDetail(await contractDetail({
contractNo
}, this.language))
this.reservationContractDetail = detail && Object.keys(detail).length ? detail : null
return this.reservationContractDetail
} catch (error) {
console.error('load contract detail failed', error)
this.reservationContractDetail = null
return null
}
},
restoreDetail(payload = '') {
if (payload) {
try {
const parsed = JSON.parse(decodeURIComponent(payload))
this.detail = parsed && typeof parsed === 'object' ? parsed : {}
return
} catch (error) {
console.error('parse application detail payload error', error)
}
}
const list = normalizeApplicationList(getMyApplicationList())
const matched = list.find(item => String(item.detailCode || '') === String(this.detailCode || ''))
this.detail = matched || {
detailCode: this.detailCode,
isViewingApplication: false
}
},
async loadReservationFlow() {
if (!this.detail || this.detail.isViewingApplication) {
return
}
const orderNo = getReservationOrderNo(this.detail)
if (!orderNo) {
this.detail = attachReservationFlow(this.detail, {})
this.reservationInspectionDetail = null
this.reservationContractDetail = null
return
}
try {
const response = await orderSearchedReservationFlow({
orderNos: [orderNo]
}, this.language)
const flowMap = buildReservationFlowMap(parseReservationFlowResponse(response))
this.detail = attachReservationFlow(this.detail, flowMap)
await Promise.all([
this.ensureReservationInspectionDetail(),
this.ensureReservationContractDetail()
])
const list = normalizeApplicationList(getMyApplicationList())
if (list.length) {
saveMyApplicationList(list.map((item) => {
if (String(item.detailCode || '') !== String(this.detail.detailCode || '')) {
return item
}
return this.detail
}))
}
} catch (error) {
console.error('load reservation flow failed', error)
this.detail = attachReservationFlow(this.detail, {})
this.reservationInspectionDetail = null
this.reservationContractDetail = null
}
},
parseViewingRemark() {
const lines = this.remarkLines
const targets = [
{ zh: '看房时间', en: 'Viewing Time' },
{ zh: '预计租住时长', en: 'Expected Stay Duration' },
{ zh: '租金预算', en: 'Budget' },
{ zh: '工作/通勤地点', en: 'Work / Commute Location' },
{ zh: '意向区域', en: 'Preferred Area' },
{ zh: '意向房型', en: 'Preferred Room Type' },
{ zh: '支付方式', en: 'Payment Method' }
]
return lines
.map((line) => {
const matched = targets.find(item => {
return line.startsWith(`${item.zh}:`)
|| line.startsWith(`${item.zh}:`)
|| line.startsWith(`${item.en}:`)
|| line.startsWith(`${item.en}:`)
})
if (!matched) {
return null
}
const localizedLabelMap = {
'看房时间': this.$tr('看房时间'),
'预计租住时长': this.$tr('预计租住时长'),
'租金预算': this.$tr('租金预算'),
'工作/通勤地点': this.$tr('工作/通勤地点'),
'意向区域': this.$tr('意向区域'),
'意向房型': this.$tr('意向房型'),
'支付方式': this.$tr('支付方式')
}
const rawValue = line
.replace(`${matched.zh}:`, '')
.replace(`${matched.zh}:`, '')
.replace(`${matched.en}:`, '')
.replace(`${matched.en}:`, '')
return {
label: localizedLabelMap[matched.zh] || matched.en,
value: rawValue || '--'
}
})
.filter(Boolean)
},
formatDateTime(value) {
if (!value) {
return '--'
}
const text = String(value).replace('T', ' ')
return text.length > 16 ? text.slice(0, 16) : text
},
formatDate(value) {
if (!value) {
return '--'
}
return String(value).slice(0, 10)
},
copyCode() {
if (!this.detail.detailCode) {
return
}
uni.setClipboardData({
data: String(this.detail.detailCode),
success: () => {
uni.showToast({
title: this.$tr('申请编号已复制'),
icon: 'none'
})
}
})
},
confirmCancelOrder() {
return new Promise((resolve) => {
uni.showModal({
title: this.$tr('取消订单'),
content: this.$tr('取消后将关闭当前未支付的定金订单,是否继续?'),
confirmColor: '#ef4444',
success: (res) => resolve(!!(res && res.confirm)),
fail: () => resolve(false)
})
})
},
syncCanceledDetailToCache() {
const list = normalizeApplicationList(getMyApplicationList())
if (!list.length) {
return
}
const nextList = list.map((item) => {
if (String(item.detailCode || '') !== String(this.detail.detailCode || '')) {
return item
}
return {
...item,
status: 99,
statusDesc: this.$tr('已结束')
}
})
saveMyApplicationList(nextList)
},
handleCancelOrder() {
if (!this.canCancelOrder || !this.detail.detailCode) {
return
}
this.confirmCancelOrder()
.then((confirmed) => {
if (!confirmed) {
return
}
this.canceling = true
return userDetailCancel({
detailCode: this.detail.detailCode,
code: this.detail.detailCode,
referenceTypeNo: this.detail.detailCode
}, this.language)
.then(() => {
this.detail = {
...this.detail,
status: 99,
statusDesc: this.$tr('已结束')
}
this.syncCanceledDetailToCache()
uni.showToast({
title: this.$tr('订单已取消'),
icon: 'success'
})
})
})
.catch((error) => {
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('取消订单失败'),
icon: 'none'
})
})
.finally(() => {
this.canceling = false
})
},
goBackList() {
uni.navigateBack()
},
goToBillPage() {
if (!this.detail.detailCode) {
uni.showToast({
title: this.$tr('申请单编号缺失'),
icon: 'none'
})
return
}
saveActiveApplicationCode(this.detail.detailCode)
uni.navigateTo({
url: `/pkgService/utility_recharge/utility_recharge?referenceTypeNo=${encodeURIComponent(this.detail.detailCode)}`
})
},
refreshCurrentPage() {
this.loadReservationFlow()
},
handleCurrentStepAction() {
const step = this.getReservationCurrentStepValue()
if (step === RESERVATION_FLOW_STEP.BILL) {
this.goToBillPage()
return
}
if (step === RESERVATION_FLOW_STEP.CONTRACT) {
if (this.isReservationContractAwaitingReview()) {
return
}
const contractNo = String(this.detail && this.detail.reservationCurrentNode && this.detail.reservationCurrentNode.refNo || '').trim()
if (!contractNo) {
uni.showToast({
title: this.$tr('合同编号缺失'),
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pkgService/contract_sign/contract_sign?no=${encodeURIComponent(contractNo)}`
})
return
}
if (step === RESERVATION_FLOW_STEP.CHECKIN) {
uni.reLaunch({
url: '/pages/index/index?pageId=stay-service'
})
return
}
if (step === RESERVATION_FLOW_STEP.INSPECTION) {
if (this.isReservationInspectionPendingReview()) {
return
}
const orderNo = getReservationOrderNo(this.detail)
uni.navigateTo({
url: orderNo
? `/pkgService/room_inspection/room_inspection?orderNo=${encodeURIComponent(orderNo)}`
: '/pkgService/room_inspection/room_inspection'
})
}
}
}
}
</script>
<style scoped lang="scss">
.application-detail-page {
min-height: 100vh;
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
}
.hero-panel,
.content-panel {
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
border: 1px solid rgba($brand-primary, 0.05);
}
.hero-viewing {
background: linear-gradient(135deg, rgba($brand-primary, 0.12) 0%, rgba($brand-primary, 0.04) 100%);
}
.hero-stay {
background: linear-gradient(135deg, rgba(64, 203, 242, 0.12) 0%, rgba($brand-primary, 0.04) 100%);
}
.status-reminder-panel {
display: flex;
align-items: flex-start;
gap: 20rpx;
padding: 28rpx 32rpx;
border-radius: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
}
.status-reminder-panel--warning {
background: linear-gradient(180deg, rgba(255, 247, 237, 0.96) 0%, rgba(255, 251, 245, 0.98) 100%);
border: 1px solid rgba(249, 115, 22, 0.16);
}
.status-reminder-panel--info {
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
border: 1px solid rgba(59, 130, 246, 0.14);
}
.status-reminder-panel__icon {
width: 68rpx;
height: 68rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 34rpx;
}
.status-reminder-panel--warning .status-reminder-panel__icon {
color: #C2410C;
background: rgba(249, 115, 22, 0.12);
}
.status-reminder-panel--info .status-reminder-panel__icon {
color: #1D4ED8;
background: rgba(59, 130, 246, 0.12);
}
.status-reminder-panel__content {
flex: 1;
min-width: 0;
}
.status-reminder-panel__title {
font-size: 26rpx;
font-weight: 700;
color: #0F172A;
}
.status-reminder-panel__text {
margin-top: 8rpx;
font-size: 30rpx;
line-height: 1.6;
font-weight: 600;
color: #334155;
}
.hero-top-row,
.hero-meta-row,
.meta-row,
.action-row {
display: flex;
align-items: center;
}
.hero-top-row,
.meta-row {
justify-content: space-between;
}
.hero-meta-row,
.action-row {
gap: 16rpx;
flex-wrap: wrap;
}
.hero-badge,
.hero-status,
.hero-meta-chip,
.copy-btn,
.ghost-btn,
.action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
}
.hero-badge,
.hero-status {
padding: 10rpx 22rpx;
border-radius: 999rpx;
font-size: 22rpx;
font-weight: 700;
}
.badge-viewing,
.badge-stay {
background: rgba($brand-primary, 0.14);
color: $brand-primary;
}
.hero-status--pending {
background: rgba(245, 158, 11, 0.12);
color: #B45309;
}
.hero-status--approved {
background: rgba($brand-primary, 0.12);
color: $brand-primary;
}
.hero-status--rejected {
background: rgba(239, 68, 68, 0.12);
color: #B91C1C;
}
.hero-status--finished {
background: rgba(100, 116, 139, 0.12);
color: #475569;
}
.hero-title,
.panel-title {
font-size: 34rpx;
font-weight: 700;
color: #1A2233;
}
.hero-meta-chip {
height: 56rpx;
padding: 0 20rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.78);
color: #51607A;
font-size: 22rpx;
}
.meta-row {
padding: 18rpx 0;
border-bottom: 1px dashed rgba($brand-primary, 0.14);
}
.meta-row:last-child {
border-bottom: none;
}
.meta-label {
color: #6A7A99;
font-size: 26rpx;
}
.meta-value {
max-width: 62%;
text-align: right;
color: #1A2233;
font-size: 26rpx;
font-weight: 500;
}
.meta-action-wrap {
display: flex;
align-items: center;
gap: 12rpx;
}
.copy-btn {
height: 48rpx;
padding: 0 18rpx;
border-radius: 999rpx;
background: rgba($brand-primary, 0.1);
color: $brand-primary;
}
.remark-panel {
padding: 24rpx;
border-radius: 20rpx;
background: rgba($brand-primary, 0.03);
border: 1rpx solid rgba($brand-primary, 0.12);
}
.remark-line {
color: #1A2233;
line-height: 1.8;
font-size: 27rpx;
}
.ghost-btn,
.danger-btn,
.action-btn {
flex: 1;
min-height: 92rpx;
border-radius: 999rpx;
}
.ghost-btn {
background: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.2);
color: $brand-primary;
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.06);
}
.danger-btn {
background: #FFFFFF;
border: 1rpx solid rgba(239, 68, 68, 0.22);
color: #DC2626;
box-shadow: 0 6rpx 16rpx rgba(239, 68, 68, 0.08);
}
.danger-btn--loading {
opacity: 0.72;
}
.action-btn {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #FFFFFF;
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.25);
}
.action-btn--ghost {
background: rgba($brand-primary, 0.08);
color: $brand-primary;
border: 1rpx solid rgba($brand-primary, 0.18);
box-shadow: none;
}
</style>