- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面 - 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面 - 预约看样流程新增房型/楼栋/楼层选择器组件 - 新增工具模块: booking-entry、privacy、reservation-flow - api/index.js 补充楼层列表、看样提交等接口 - locales/messages.js 补充多语言文案 - manifest.json 开启微信小程序隐私检查 - 完善登录、注册、申请详情、账单等相关页面交互
743 lines
21 KiB
Vue
743 lines
21 KiB
Vue
<template>
|
||
<view class="my-application-page tn-safe-area-inset-bottom">
|
||
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
|
||
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
|
||
</tn-nav-bar>
|
||
|
||
<view class="page-main tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '48rpx' }">
|
||
<scroll-view class="filter-scroll" scroll-x :show-scrollbar="false">
|
||
<view class="filter-row tn-margin-bottom-lg">
|
||
<view
|
||
class="filter-chip"
|
||
v-for="item in statusOptions"
|
||
:key="item.value"
|
||
:class="{ 'filter-chip--active': status === item.value }"
|
||
@click="changeStatus(item.value)"
|
||
>
|
||
{{ item.label }}
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view class="toolbar-row tn-margin-bottom-lg">
|
||
<view class="toolbar-summary">{{ t('toolbarSummary', { total }) }}</view>
|
||
<view
|
||
class="refresh-btn"
|
||
:class="{ 'refresh-btn--loading': loading }"
|
||
@click="refreshStatus"
|
||
>
|
||
<text
|
||
class="tn-text-bold tn-flex tn-flex-row-center tn-flex-col-center"
|
||
>
|
||
<text v-if="loading" class="tn-icon-loading tn-margin-right-xs spin"></text>
|
||
{{ loading ? t('refreshing') : t('refresh') }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="list.length">
|
||
<view
|
||
v-for="item in list"
|
||
:key="getApplicationKey(item)"
|
||
class="application-card tn-margin-bottom-lg"
|
||
@click="goToApplicationDetail(item)"
|
||
>
|
||
<view class="card-head tn-flex tn-flex-row-between tn-flex-col-center">
|
||
<view class="card-title-wrap">
|
||
<view class="card-title">{{ getApplicationTitle(item) }}</view>
|
||
<view class="card-code tn-margin-top-xs">{{ getApplicationCode(item) }}</view>
|
||
</view>
|
||
<view :class="['status-badge', 'status-' + resolveStatusType(item.status)]">
|
||
{{ getDisplayStatusText(item) }}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card-body tn-margin-top">
|
||
<view class="info-row tn-flex tn-flex-row-between">
|
||
<text class="info-label">{{ t('apartmentLabel') }}</text>
|
||
<text class="info-value">{{ item.intendedAreaName || '--' }}</text>
|
||
</view>
|
||
<view class="info-row tn-flex tn-flex-row-between">
|
||
<text class="info-label">{{ t('occupantLabel') }}</text>
|
||
<text class="info-value">{{ item.userName || item.phone || '--' }}</text>
|
||
</view>
|
||
<view class="info-row tn-flex tn-flex-row-between">
|
||
<text class="info-label">{{ item.isViewingApplication ? t('viewingTimeLabel') : t('leaseTermLabel') }}</text>
|
||
<text class="info-value">{{ formatDateRange(item.start, item.end) }}</text>
|
||
</view>
|
||
<view class="info-row tn-flex tn-flex-row-between">
|
||
<text class="info-label">{{ t('submittedAtLabel') }}</text>
|
||
<text class="info-value">{{ formatDateTime(item.createTime) }}</text>
|
||
</view>
|
||
<view v-if="item.remark" class="remark-panel tn-margin-top">
|
||
<view class="remark-label">{{ t('remarkLabel') }}</view>
|
||
<view class="remark-text tn-margin-top-xs">{{ item.remark }}</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="canViewBill(item)" class="card-footer tn-margin-top-lg">
|
||
<view
|
||
class="card-action-btn card-action-btn--ghost"
|
||
:data-reference-type-no="resolveReferenceTypeNo(item)"
|
||
@click="goToBillPage"
|
||
>
|
||
<text class="tn-text-bold">{{ t('viewBill') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="load-state">
|
||
<text v-if="loadingMore">{{ t('loadingMore') }}</text>
|
||
<text v-else-if="!hasMore">{{ t('noMore') }}</text>
|
||
<text v-else>{{ t('pullUpMore') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else-if="!loading" class="empty-panel tn-padding-lg">
|
||
<view class="empty-title">{{ t('emptyTitle') }}</view>
|
||
<view class="empty-desc tn-margin-top-sm">{{ t('emptyDesc') }}</view>
|
||
<view class="action-btn tn-margin-top-lg" @click="goSelectRoom">
|
||
<text class="tn-text-bold">{{ t('goSelectRoom') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="loading && !list.length" class="empty-panel tn-padding-lg">
|
||
<view class="empty-title">{{ t('loadingTitle') }}</view>
|
||
<view class="empty-desc tn-margin-top-sm">{{ t('loadingDesc') }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { applicationFormMePage, orderSearchedReservationFlow } from '@/api/index.js'
|
||
import { saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
|
||
import {
|
||
attachReservationFlow,
|
||
buildReservationFlowMap,
|
||
getReservationOrderNo,
|
||
parseReservationFlowResponse
|
||
} from '@/utils/reservation-flow'
|
||
|
||
function formatPageResponse(response) {
|
||
const payload = response && response.data ? response.data : response
|
||
return payload && typeof payload === 'object' ? payload : {}
|
||
}
|
||
|
||
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间[::]|意向房型[::]|意向区域[::]|viewing request|viewing time[::]|preferred room type[::]|preferred area[::]/i
|
||
|
||
function isViewingApplicationRecord(item = {}) {
|
||
if (!item || typeof item !== 'object') {
|
||
return false
|
||
}
|
||
|
||
const typeText = [
|
||
item.businessType,
|
||
item.applicationType,
|
||
item.applicationTypeDesc,
|
||
item.formType,
|
||
item.bizType,
|
||
item.type,
|
||
item.typeDesc
|
||
]
|
||
.filter(value => value !== null && typeof value !== 'undefined' && String(value).trim())
|
||
.join(' ')
|
||
|
||
if (/viewing|看房|带看/i.test(typeText)) {
|
||
return true
|
||
}
|
||
|
||
if ([item.appointmentViewingTime, item.viewingTime, item.bookViewingTime].some(value => String(value || '').trim())) {
|
||
return true
|
||
}
|
||
|
||
const remark = item && item.remark ? String(item.remark) : ''
|
||
return VIEWING_APPLICATION_PATTERN.test(remark)
|
||
}
|
||
|
||
function normalizeApplicationList(list = []) {
|
||
if (!Array.isArray(list)) {
|
||
return []
|
||
}
|
||
return list
|
||
.filter(item => item && typeof item === 'object')
|
||
.map((item) => {
|
||
return {
|
||
...item,
|
||
isViewingApplication: isViewingApplicationRecord(item)
|
||
}
|
||
})
|
||
}
|
||
|
||
export default {
|
||
i18n: {
|
||
zh: {
|
||
navTitle: '我的申请',
|
||
toolbarSummary: '共 {total} 条申请',
|
||
refreshing: '刷新中...',
|
||
refresh: '刷新状态',
|
||
apartmentLabel: '所属公寓',
|
||
occupantLabel: '入住人',
|
||
viewingTimeLabel: '预约时间',
|
||
leaseTermLabel: '预计租期',
|
||
submittedAtLabel: '提交时间',
|
||
remarkLabel: '补充说明',
|
||
viewBill: '查看关联账单',
|
||
loadingMore: '加载中...',
|
||
noMore: '没有更多申请了',
|
||
pullUpMore: '上拉加载更多',
|
||
emptyTitle: '暂无申请记录',
|
||
emptyDesc: '您还没有提交任何申请,去选房后可发起入住申请或预约看房。',
|
||
goSelectRoom: '去选房',
|
||
loadingTitle: '申请列表加载中',
|
||
loadingDesc: '正在同步您的入住申请记录,请稍候。',
|
||
statusAll: '全部',
|
||
statusPending: '待审核',
|
||
statusApproved: '已通过',
|
||
statusRejected: '已拒绝',
|
||
statusFinished: '已结束',
|
||
statusProcessing: '处理中',
|
||
refreshSuccess: '申请状态已刷新',
|
||
loadError: '申请记录加载失败',
|
||
viewingApplicationTitle: '预约看房申请',
|
||
stayApplicationTitle: '入住申请',
|
||
missingBillCode: '申请单编号缺失',
|
||
missingApplicationCode: '申请编号缺失',
|
||
dateRangeJoiner: ' 至 '
|
||
},
|
||
en: {
|
||
navTitle: 'My Applications',
|
||
toolbarSummary: '{total} applications',
|
||
refreshing: 'Refreshing...',
|
||
refresh: 'Refresh Status',
|
||
apartmentLabel: 'Apartment',
|
||
occupantLabel: 'Resident',
|
||
viewingTimeLabel: 'Viewing Time',
|
||
leaseTermLabel: 'Lease Term',
|
||
submittedAtLabel: 'Submitted At',
|
||
remarkLabel: 'Remarks',
|
||
viewBill: 'View Related Bill',
|
||
loadingMore: 'Loading...',
|
||
noMore: 'No more applications',
|
||
pullUpMore: 'Pull up to load more',
|
||
emptyTitle: 'No applications yet',
|
||
emptyDesc: 'You have not submitted any application yet. Go select a room to start a stay or viewing application.',
|
||
goSelectRoom: 'Select Room',
|
||
loadingTitle: 'Loading applications',
|
||
loadingDesc: 'Syncing your application records. Please wait.',
|
||
statusAll: 'All',
|
||
statusPending: 'Pending',
|
||
statusApproved: 'Approved',
|
||
statusRejected: 'Rejected',
|
||
statusFinished: 'Closed',
|
||
statusProcessing: 'Processing',
|
||
refreshSuccess: 'Application status refreshed',
|
||
loadError: 'Failed to load applications',
|
||
viewingApplicationTitle: 'Viewing Request',
|
||
stayApplicationTitle: 'Stay Application',
|
||
missingBillCode: 'Missing application bill number',
|
||
missingApplicationCode: 'Missing application number',
|
||
dateRangeJoiner: ' to '
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
status: '',
|
||
pageIndex: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
list: [],
|
||
loading: false,
|
||
loadingMore: false,
|
||
hasMore: true,
|
||
statusOptions: []
|
||
}
|
||
},
|
||
onShow() {
|
||
this.reloadList()
|
||
},
|
||
onPullDownRefresh() {
|
||
this.reloadList().finally(() => {
|
||
uni.stopPullDownRefresh()
|
||
})
|
||
},
|
||
onReachBottom() {
|
||
this.loadMore()
|
||
},
|
||
created() {
|
||
this.syncStatusOptions()
|
||
},
|
||
watch: {
|
||
language() {
|
||
this.syncStatusOptions()
|
||
this.reloadList(true)
|
||
}
|
||
},
|
||
computed: {
|
||
language() {
|
||
return this.$getLanguage()
|
||
}
|
||
},
|
||
methods: {
|
||
t(key, params) {
|
||
return this.$tm('pkgService.my_application.my_application', key, params)
|
||
},
|
||
syncStatusOptions() {
|
||
this.statusOptions = [
|
||
{ label: this.t('statusAll'), value: '' },
|
||
{ label: this.t('statusPending'), value: 0 },
|
||
{ label: this.t('statusApproved'), value: 10 },
|
||
{ label: this.t('statusRejected'), value: 20 },
|
||
{ label: this.t('statusFinished'), value: 99 }
|
||
]
|
||
},
|
||
getStatusText(status) {
|
||
const map = {
|
||
0: this.t('statusPending'),
|
||
10: this.t('statusApproved'),
|
||
20: this.t('statusRejected'),
|
||
99: this.t('statusFinished')
|
||
}
|
||
return map[status] || this.t('statusProcessing')
|
||
},
|
||
getDisplayStatusText(item = {}) {
|
||
if (this.language !== 'en' && item.statusDesc) {
|
||
return item.statusDesc
|
||
}
|
||
return this.getStatusText(item.status)
|
||
},
|
||
resolveStatusType(status) {
|
||
if (status === 10) {
|
||
return 'approved'
|
||
}
|
||
if (status === 20) {
|
||
return 'rejected'
|
||
}
|
||
if (status === 99) {
|
||
return 'finished'
|
||
}
|
||
return 'pending'
|
||
},
|
||
formatDateTime(value) {
|
||
if (!value) {
|
||
return '--'
|
||
}
|
||
const text = String(value).replace('T', ' ')
|
||
return text.length > 16 ? text.slice(0, 16) : text
|
||
},
|
||
formatDate(value) {
|
||
if (!value) {
|
||
return '--'
|
||
}
|
||
return String(value).slice(0, 10)
|
||
},
|
||
formatDateRange(start, end) {
|
||
if (!start && !end) {
|
||
return '--'
|
||
}
|
||
if (start && end) {
|
||
const startText = this.formatDateTime(start)
|
||
const endText = this.formatDateTime(end)
|
||
if (String(start).includes('T') || String(end).includes('T')) {
|
||
return `${startText}${this.t('dateRangeJoiner')}${endText}`
|
||
}
|
||
}
|
||
return `${this.formatDate(start)}${this.t('dateRangeJoiner')}${this.formatDate(end)}`
|
||
},
|
||
async refreshStatus() {
|
||
if (this.loading || this.loadingMore) {
|
||
return
|
||
}
|
||
await this.reloadList()
|
||
if (this.list.length || this.total === 0) {
|
||
uni.showToast({
|
||
title: this.t('refreshSuccess'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
async reloadList() {
|
||
this.pageIndex = 1
|
||
this.hasMore = true
|
||
this.list = []
|
||
await this.fetchList(true)
|
||
},
|
||
async loadMore() {
|
||
if (!this.hasMore || this.loading || this.loadingMore) {
|
||
return
|
||
}
|
||
this.pageIndex += 1
|
||
await this.fetchList(false)
|
||
},
|
||
async fetchList(isReload = false) {
|
||
if (isReload) {
|
||
this.loading = true
|
||
} else {
|
||
this.loadingMore = true
|
||
}
|
||
|
||
const params = {
|
||
PageIndex: this.pageIndex,
|
||
PageSize: this.pageSize
|
||
}
|
||
if (this.status !== '' && this.status !== null && typeof this.status !== 'undefined') {
|
||
params.Status = this.status
|
||
}
|
||
|
||
await applicationFormMePage(params, this.language)
|
||
.then(async (response) => {
|
||
const pageData = formatPageResponse(response)
|
||
console.log('pageData', pageData)
|
||
const nextList = normalizeApplicationList(pageData.data || [])
|
||
const total = Number(pageData.total || 0)
|
||
|
||
this.total = total
|
||
this.list = normalizeApplicationList(isReload ? nextList : this.list.concat(nextList))
|
||
this.hasMore = this.list.length < total && nextList.length > 0
|
||
|
||
const activeCode = this.resolvePreferredReferenceTypeNo(this.list)
|
||
if (activeCode) {
|
||
saveActiveApplicationCode(activeCode)
|
||
}
|
||
|
||
await this.loadReservationFlowsForList(this.list)
|
||
saveMyApplicationList(this.list)
|
||
})
|
||
.catch((error) => {
|
||
if (!isReload) {
|
||
this.pageIndex = Math.max(1, this.pageIndex - 1)
|
||
}
|
||
uni.showToast({
|
||
title: (error && (error.message || error.msg)) || this.t('loadError'),
|
||
icon: 'none'
|
||
})
|
||
})
|
||
.finally(() => {
|
||
this.loading = false
|
||
this.loadingMore = false
|
||
})
|
||
},
|
||
changeStatus(value) {
|
||
if (this.status === value) {
|
||
return
|
||
}
|
||
this.status = value
|
||
this.reloadList()
|
||
},
|
||
getApplicationCode(item = {}) {
|
||
return item.detailCode || '--'
|
||
},
|
||
getApplicationKey(item = {}) {
|
||
return item.detailCode || item.formId || `${item.createTime || 'application'}-${item.phone || item.userName || 'unknown'}`
|
||
},
|
||
getApplicationTitle(item = {}) {
|
||
if (item.isViewingApplication) {
|
||
return this.t('viewingApplicationTitle')
|
||
}
|
||
return item.intendedRoomTypeName || this.t('stayApplicationTitle')
|
||
},
|
||
resolveReferenceTypeNo(item = {}) {
|
||
return item.detailCode || ''
|
||
},
|
||
canViewBill(item = {}) {
|
||
return !item.isViewingApplication && this.resolveReferenceTypeNo(item) && this.resolveStatusType(item.status) === 'pending'
|
||
},
|
||
resolvePreferredReferenceTypeNo(list = []) {
|
||
const sortedList = [...list]
|
||
.filter(item => !item.isViewingApplication && this.resolveReferenceTypeNo(item))
|
||
.sort((a, b) => new Date(b.createTime || 0).getTime() - new Date(a.createTime || 0).getTime())
|
||
|
||
if (!sortedList.length) {
|
||
return ''
|
||
}
|
||
|
||
const preferredItem = sortedList.find(item => Number(item.status) !== 99) || sortedList[0]
|
||
return this.resolveReferenceTypeNo(preferredItem)
|
||
},
|
||
async loadReservationFlowsForList(list = []) {
|
||
const baseList = Array.isArray(list) ? list : []
|
||
const orderNos = [...new Set(
|
||
baseList
|
||
.filter(item => item && !item.isViewingApplication)
|
||
.map(item => getReservationOrderNo(item))
|
||
.filter(Boolean)
|
||
)]
|
||
|
||
const requestToken = Date.now()
|
||
this._reservationFlowRequestToken = requestToken
|
||
|
||
const applyFlowMap = (flowMap = {}) => {
|
||
if (this._reservationFlowRequestToken !== requestToken) {
|
||
return
|
||
}
|
||
this.list = (Array.isArray(this.list) ? this.list : []).map(item => attachReservationFlow(item, flowMap))
|
||
}
|
||
|
||
if (!orderNos.length) {
|
||
applyFlowMap({})
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await orderSearchedReservationFlow({
|
||
orderNos
|
||
}, this.language)
|
||
const flowMap = buildReservationFlowMap(parseReservationFlowResponse(response))
|
||
applyFlowMap(flowMap)
|
||
} catch (error) {
|
||
console.log('[my_application] load reservation flow failed', error)
|
||
applyFlowMap({})
|
||
}
|
||
},
|
||
goToBillPage(event) {
|
||
const dataset = event && event.currentTarget ? event.currentTarget.dataset || {} : {}
|
||
const referenceTypeNo = dataset.referenceTypeNo || dataset.referenceTypeNo === 0
|
||
? String(dataset.referenceTypeNo)
|
||
: ''
|
||
if (!referenceTypeNo) {
|
||
uni.showToast({
|
||
title: this.t('missingBillCode'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
saveActiveApplicationCode(referenceTypeNo)
|
||
uni.navigateTo({
|
||
url: `/pkgService/utility_recharge/utility_recharge?referenceTypeNo=${encodeURIComponent(referenceTypeNo)}`
|
||
})
|
||
},
|
||
goToApplicationDetail(item = {}) {
|
||
const code = item.detailCode || ''
|
||
if (!code) {
|
||
return
|
||
}
|
||
uni.navigateTo({
|
||
url: `/pkgService/application_detail/application_detail?code=${encodeURIComponent(code)}&payload=${encodeURIComponent(JSON.stringify(item))}`
|
||
})
|
||
},
|
||
goSelectRoom() {
|
||
uni.reLaunch({
|
||
url: '/pages/index/index?index=0&pageId=select-room'
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.my-application-page {
|
||
min-height: 100vh;
|
||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||
}
|
||
|
||
.application-card,
|
||
.empty-panel {
|
||
background: #FFFFFF;
|
||
border-radius: 24rpx;
|
||
padding: 32rpx;
|
||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
||
border: 1px solid rgba($brand-primary, 0.05);
|
||
}
|
||
|
||
.empty-title,
|
||
.card-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #1A2233;
|
||
}
|
||
|
||
.empty-desc,
|
||
.card-code,
|
||
.info-label,
|
||
.remark-label,
|
||
.load-state {
|
||
color: #6A7A99;
|
||
}
|
||
|
||
.filter-scroll {
|
||
width: 100%;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-row {
|
||
display: inline-flex;
|
||
flex-wrap: nowrap;
|
||
gap: 16rpx;
|
||
padding: 4rpx;
|
||
}
|
||
|
||
.toolbar-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.toolbar-summary {
|
||
flex: 1;
|
||
font-size: 24rpx;
|
||
color: #6A7A99;
|
||
}
|
||
|
||
.refresh-btn {
|
||
min-width: 188rpx;
|
||
height: 72rpx;
|
||
padding: 0 24rpx;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 999rpx;
|
||
background: #FFFFFF;
|
||
border: 1rpx solid rgba($brand-primary, 0.18);
|
||
color: $brand-primary;
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.06);
|
||
}
|
||
|
||
.refresh-btn--loading {
|
||
opacity: 0.88;
|
||
}
|
||
|
||
.filter-chip {
|
||
flex-shrink: 0;
|
||
padding: 14rpx 28rpx;
|
||
border-radius: 999rpx;
|
||
background: #FFFFFF;
|
||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||
color: #6A7A99;
|
||
font-size: 24rpx;
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.filter-chip--active {
|
||
background: #FFFFFF;
|
||
border-color: $brand-primary;
|
||
color: $brand-primary;
|
||
font-weight: bold;
|
||
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
||
}
|
||
|
||
.card-code {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 8rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.status-pending {
|
||
background: rgba(245, 158, 11, 0.12);
|
||
color: #B45309;
|
||
}
|
||
|
||
.status-approved {
|
||
background: rgba($brand-primary, 0.12);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.status-rejected {
|
||
background: rgba(239, 68, 68, 0.12);
|
||
color: #B91C1C;
|
||
}
|
||
|
||
.status-finished {
|
||
background: rgba(100, 116, 139, 0.12);
|
||
color: #475569;
|
||
}
|
||
|
||
.info-row {
|
||
padding: 16rpx 0;
|
||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
||
}
|
||
|
||
.info-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.info-value {
|
||
max-width: 62%;
|
||
text-align: right;
|
||
color: #1A2233;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.remark-panel {
|
||
padding: 24rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba($brand-primary, 0.02);
|
||
border: 1rpx solid rgba($brand-primary, 0.1);
|
||
}
|
||
|
||
.remark-text {
|
||
color: #1A2233;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.card-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.card-action-btn {
|
||
min-width: 220rpx;
|
||
height: 84rpx;
|
||
padding: 0 28rpx;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 18rpx;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.card-action-btn--ghost {
|
||
background: #ffffff;
|
||
color: $brand-primary;
|
||
border: 1rpx solid rgba($brand-primary, 0.3);
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||
}
|
||
|
||
.action-btn {
|
||
width: 100%;
|
||
height: 96rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 999rpx;
|
||
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
|
||
color: #FFFFFF;
|
||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.25);
|
||
}
|
||
|
||
.load-state {
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
padding: 12rpx 0 24rpx;
|
||
color: #6A7A99;
|
||
}
|
||
|
||
.spin {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
/* 隐藏 scroll-view 滚动条 */
|
||
::-webkit-scrollbar {
|
||
display: none;
|
||
width: 0 !important;
|
||
height: 0 !important;
|
||
-webkit-appearance: none;
|
||
background: transparent;
|
||
}
|
||
</style>
|