Files
QSY-lvjuxcx/pkgService/room_inspection/room_inspection.vue
liangjian 1111ac148b feat: 重构核心租住流程页面与交互体验
统一注册、看房、申请与入住等核心页面的视觉和流程衔接,补齐支付、签约与状态流转闭环,提升移动端操作连贯性。
2026-07-29 09:05:38 +08:00

1406 lines
38 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="app-page">
<view class="page-bg">
<view class="bg-orb bg-orb--primary"></view>
<view class="bg-orb bg-orb--soft"></view>
</view>
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
<view class="nav-title">{{ $tr('验房') }}</view>
</tn-nav-bar>
<view class="page-shell" :style="{ paddingTop: vuex_custom_bar_height ? 'calc(' + vuex_custom_bar_height + 'px + 16rpx)' : '16rpx' }">
<view class="hero-panel">
<view class="hero-top">
<view class="hero-title-wrap">
<text class="hero-kicker">{{ $tr('交付检查') }}</text>
<text class="hero-title">{{ $tr('验房') }}</text>
</view>
<view v-if="activeInspection" class="hero-status">
{{ activeInspection.statusText }}
</view>
</view>
</view>
<view v-if="inspectionList.length > 1" class="tab-wrap">
<view class="section-head">
<text class="section-head__title">{{ $tr('验房单切换') }}</text>
<text class="section-head__meta">{{ inspectionList.length }} {{ $tr('份记录') }}</text>
</view>
<scroll-view scroll-x class="tab-scroll" :show-scrollbar="false">
<view class="tab-list">
<view
v-for="item in inspectionList"
:key="item.inspectionNo"
class="tab-item"
:class="{ 'tab-item--active': item.inspectionNo === activeInspectionNo }"
@click="switchInspection(item)"
>
<text class="tab-item__type">{{ item.typeText }}</text>
<text class="tab-item__room font-mono">{{ item.roomNo || '--' }}</text>
<text class="tab-item__time font-mono">{{ item.createTimeText }}</text>
</view>
</view>
</scroll-view>
</view>
<view v-if="activeInspectionDetail" class="content-wrap">
<view v-if="activeInspectionDetail.items.length" class="items-wrap">
<view class="section-head section-head--main">
<view>
<text class="section-head__title">{{ $tr('检查项') }}</text>
<text class="section-head__desc">{{ $tr('建议逐项填写,异常情况请补充描述并上传照片。') }}</text>
</view>
<view class="section-head__actions">
<view class="section-action-btn" @click="markAllItemsNormal">
{{ $tr('全部正常') }}
</view>
<text class="section-head__meta">{{ activeInspectionDetail.items.length }} {{ $tr('项') }}</text>
</view>
</view>
<view class="item-list">
<view
v-for="(item, index) in activeInspectionDetail.items"
:key="item.itemId || index"
class="inspect-item"
:class="{ 'inspect-item--expanded': shouldShowItemDetail(item) }"
>
<view class="inspect-item__glow"></view>
<view class="item-header">
<view class="item-order">{{ String(index + 1).padStart(2, '0') }}</view>
<view class="item-title-col">
<view class="item-title-row">
<text class="item-name">{{ item.deviceName || $tr('未命名设备') }}</text>
<text v-if="item.isRequiredSnapshot" class="item-required">{{ $tr('必查') }}</text>
</view>
</view>
<view class="item-header__right">
<view class="radio-group radio-group--compact">
<view
v-for="option in inspectionResultOptions"
:key="option.key"
class="radio-btn radio-btn--compact"
:class="{ 'radio-btn--active': String(item.resultDraft) === String(option.key) }"
@click="updateItemField(index, 'resultDraft', option.key)"
>
{{ option.value }}
</view>
</view>
</view>
</view>
<view v-if="shouldShowItemDetail(item)" class="item-body">
<text class="item-meta item-meta--detail" v-if="item.assetSourceText || item.brandSnapshot">
{{ [item.assetSourceText, item.brandSnapshot].filter(Boolean).join(' / ') }}
</text>
<view class="data-grid">
<view class="data-card">
<text class="field-label">{{ $tr('应有数量') }}</text>
<text class="field-value font-mono">{{ item.expectedQuantityText }}</text>
</view>
<view class="data-card data-card--input">
<text class="field-label">{{ $tr('实测数量') }}</text>
<input
class="field-input font-mono"
type="number"
:value="item.checkedQuantityDraft"
:placeholder="$tr('请输入数量')"
@input="updateItemField(index, 'checkedQuantityDraft', $event.detail.value)"
/>
</view>
</view>
<view class="field-block">
<view class="field-block__head">
<text class="field-block__title">{{ $tr('异常说明') }}</text>
<text class="field-block__hint">{{ $tr('缺失、损坏或无法使用时请补充描述') }}</text>
</view>
<textarea
class="field-textarea"
:value="item.problemDetailDraft"
:placeholder="$tr('请描述异常、缺失或损坏情况')"
maxlength="300"
auto-height
@input="updateItemField(index, 'problemDetailDraft', $event.detail.value)"
/>
</view>
<view class="field-block">
<view class="field-block__head">
<text class="field-block__title">{{ $tr('现场照片') }}</text>
<text class="field-block__hint">{{ $tr('建议上传关键部位照片,便于后续确认') }}</text>
</view>
<view class="photo-uploader">
<view
v-for="(url, imageIndex) in item.imageUrlsDraft"
:key="`${item.itemId || index}-${imageIndex}`"
class="photo-item"
>
<image
class="photo-img"
:src="resolveImageUrl(url)"
mode="aspectFill"
@click="previewImages(item.imageUrlsDraft, imageIndex)"
/>
<view class="photo-del" @click.stop="removeImage(index, imageIndex)">×</view>
</view>
<view class="photo-add" @click="chooseImages(index)">
<text class="photo-add-icon">+</text>
<text class="photo-add-text">{{ $tr('上传照片') }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view v-else-if="!loading" class="empty-card">
<text class="empty-title">{{ $tr('暂无检查项') }}</text>
<text class="empty-desc">{{ $tr('当前验房单还没有检查项明细。') }}</text>
</view>
</view>
<view v-if="!loading && !inspectionList.length" class="empty-card empty-card--page">
<text class="empty-title">{{ $tr('暂无验房单') }}</text>
<text class="empty-desc">{{ $tr('当前订单还没有可查看的验房记录。') }}</text>
</view>
</view>
<view v-if="activeInspectionDetail" class="bottom-action">
<view class="btn-submit" @click="submitInspection">
{{ submitting ? $tr('提交中...') : $tr('提交验房') }}
</view>
</view>
<view v-if="remarksDialogVisible" class="remarks-dialog">
<view class="remarks-dialog__mask" @click="closeRemarksDialog"></view>
<view class="remarks-dialog__panel">
<view class="remarks-dialog__title">{{ $tr('提交前确认') }}</view>
<view class="remarks-dialog__desc">{{ $tr('如有交接备注、异常背景或补充情况,可在提交前补充说明。') }}</view>
<textarea
class="remarks-dialog__input"
:value="remarksDraft"
:placeholder="$tr('可填写本次验房的补充说明')"
maxlength="300"
auto-height
@input="handleRemarksInput"
/>
<view class="remarks-dialog__footer">
<view class="remarks-dialog__btn remarks-dialog__btn--ghost" @click="submitWithoutRemarks">
{{ $tr('暂不填写') }}
</view>
<view class="remarks-dialog__btn remarks-dialog__btn--primary" @click="confirmSubmitInspection">
{{ $tr('确认提交') }}
</view>
</view>
</view>
</view>
<view v-if="loading" class="global-loading">
<tn-loading mode="circle" color="#2B8EFF" size="48"></tn-loading>
<text class="loading-text">{{ $tr('正在同步验房数据...') }}</text>
</view>
</view>
</template>
<script>
import {
roomInspectionDetail,
roomInspectionMyPage,
roomInspectionSubmit,
uploadCommonFile
} from '@/api/index.js'
import { fetchEnums, getEnumLabel, getEnumOptions } from '@/utils/enum-store'
import { addFilePrefix } from '@/utils/common'
import { getAuthToken } from '@/utils/app-state'
function getResponsePayload(response) {
const payload = response && response.data ? response.data : response
if (typeof payload !== 'string') {
return payload
}
const text = payload.trim()
if (!text) {
return payload
}
if (!((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']')))) {
return payload
}
try {
return JSON.parse(text)
} catch (error) {
return payload
}
}
function getArrayPayload(response) {
const payload = getResponsePayload(response)
if (Array.isArray(payload)) {
return payload
}
if (payload && Array.isArray(payload.data)) {
return payload.data
}
if (payload && Array.isArray(payload.items)) {
return payload.items
}
return []
}
function getObjectPayload(response) {
const payload = getResponsePayload(response)
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
return payload
}
return {}
}
function formatDateTime(value) {
if (!value) {
return '--'
}
const text = String(value).replace('T', ' ')
return text.length > 16 ? text.slice(0, 16) : text
}
function resolveUploadedFileUrl(uploadResult) {
const candidates = [
uploadResult && uploadResult.data && uploadResult.data.url,
uploadResult && uploadResult.data && uploadResult.data.fileUrl,
uploadResult && uploadResult.data && uploadResult.data.path,
uploadResult && uploadResult.data && uploadResult.data.filePath,
uploadResult && uploadResult.data,
uploadResult && uploadResult.url,
uploadResult && uploadResult.fileUrl,
uploadResult && uploadResult.path,
uploadResult && uploadResult.filePath,
typeof uploadResult === 'string' ? uploadResult : ''
]
return candidates.find(item => typeof item === 'string' && item) || ''
}
export default {
data() {
return {
loading: false,
submitting: false,
orderNo: '',
inspectionResultOptions: [],
inspectionList: [],
activeInspectionNo: '',
detailMap: {},
remarksDraft: '',
remarksDialogVisible: false
}
},
watch: {
language() {
this.detailMap = {}
this.loadPage(true)
}
},
computed: {
language() {
return this.$getLanguage()
},
activeInspection() {
return this.inspectionList.find(item => item.inspectionNo === this.activeInspectionNo) || null
},
activeInspectionDetail() {
return this.detailMap[this.activeInspectionNo] || null
},
},
onLoad(options = {}) {
this.orderNo = String(options.orderNo || '').trim()
this.activeInspectionNo = String(options.inspectionNo || '').trim()
this.loadPage(true)
},
onPullDownRefresh() {
this.loadPage(true).finally(() => {
uni.stopPullDownRefresh()
})
},
methods: {
syncInspectionResultOptions() {
this.inspectionResultOptions = this.getEnumOptionsWithAliases([
'InspectionItemResultEnum',
'INSPECTIONITEMRESULTENUM'
])
},
getCheckTypeLabel(value, fallback = '') {
return getEnumLabel('CheckTypeEnum', value, fallback, this.language)
},
getInspectionStatusLabel(value, fallback = '') {
return getEnumLabel('InspectionStatusEnum', value, fallback, this.language)
},
getInspectionItemResultLabel(value, fallback = '') {
return this.getEnumLabelWithAliases([
'InspectionItemResultEnum',
'INSPECTIONITEMRESULTENUM'
], value, fallback)
},
getAssetSourceLabel(value, fallback = '') {
return getEnumLabel('RoomAssetSourceTypeEnum', value, fallback, this.language)
},
getEnumOptionsWithAliases(codes = []) {
const codeList = Array.isArray(codes) ? codes : [codes]
for (let index = 0; index < codeList.length; index += 1) {
const code = String(codeList[index] || '').trim()
if (!code) {
continue
}
const options = getEnumOptions(code, this.language)
if (Array.isArray(options) && options.length) {
return options
}
}
return []
},
getEnumLabelWithAliases(codes = [], value = '', fallback = '') {
const codeList = Array.isArray(codes) ? codes : [codes]
for (let index = 0; index < codeList.length; index += 1) {
const code = String(codeList[index] || '').trim()
if (!code) {
continue
}
const label = getEnumLabel(code, value, '', this.language)
if (label !== '') {
return label
}
}
return fallback
},
normalizeListItem(item = {}) {
const inspectionNo = String(item.no || item.inspectionNo || '').trim()
return {
inspectionNo,
roomNo: item.roomNo || '',
type: item.type,
status: item.status,
createTime: item.createTime || '',
createTimeText: formatDateTime(item.createTime),
typeText: this.getCheckTypeLabel(item.type, item.typeDes || '--'),
statusText: this.getInspectionStatusLabel(item.status, item.statusDes || '--')
}
},
normalizeDetail(detail = {}) {
const inspectionNo = String(detail.no || detail.inspectionNo || '').trim()
const items = Array.isArray(detail.items)
? [...detail.items]
.sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0))
.map(item => ({
itemId: item.id,
deviceName: item.deviceName || '',
brandSnapshot: item.brandSnapshot || '',
isRequiredSnapshot: !!item.isRequiredSnapshot,
expectedQuantityText: item.expectedQuantity === null || item.expectedQuantity === undefined ? '--' : String(item.expectedQuantity),
checkedQuantityDraft: item.checkedQuantity === null || item.checkedQuantity === undefined ? '' : String(item.checkedQuantity),
resultDraft: item.result === null || item.result === undefined ? '' : String(item.result),
problemDetailDraft: item.problemDetail || '',
imageUrlsDraft: Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [],
assetSourceText: this.getAssetSourceLabel(item.assetSourceType, ''),
resultText: this.getInspectionItemResultLabel(item.result, item.resultDes || '')
}))
: []
return {
inspectionNo,
roomNo: detail.roomNo || '',
typeText: this.getCheckTypeLabel(detail.type, detail.typeDes || '--'),
statusText: this.getInspectionStatusLabel(detail.status, detail.statusDes || '--'),
createTimeText: formatDateTime(detail.createTime),
remarksDraft: detail.remarks || '',
items
}
},
async loadPage(force = false) {
this.loading = true
try {
await fetchEnums(force, this.language).catch(() => {})
this.syncInspectionResultOptions()
await this.fetchInspectionList()
if (!this.activeInspectionNo && this.inspectionList.length) {
this.activeInspectionNo = this.inspectionList[0].inspectionNo
}
if (this.activeInspectionNo) {
await this.fetchInspectionDetail(this.activeInspectionNo, force)
}
} finally {
this.loading = false
}
},
async fetchInspectionList() {
if (!this.orderNo) {
this.inspectionList = []
this.activeInspectionNo = ''
return
}
const response = await roomInspectionMyPage({
orderNo: this.orderNo
}, this.language)
const list = getArrayPayload(response)
.map(item => this.normalizeListItem(item))
.filter(item => item.inspectionNo)
.sort((prev, next) => new Date(next.createTime || 0).getTime() - new Date(prev.createTime || 0).getTime())
this.inspectionList = list
if (!list.some(item => item.inspectionNo === this.activeInspectionNo)) {
this.activeInspectionNo = list.length ? list[0].inspectionNo : ''
}
},
async fetchInspectionDetail(inspectionNo = '', force = false) {
const targetNo = String(inspectionNo || '').trim()
if (!targetNo) {
return
}
if (!force && this.detailMap[targetNo]) {
this.remarksDraft = this.detailMap[targetNo].remarksDraft || ''
return
}
const response = await roomInspectionDetail({
inspectionNo: targetNo
}, this.language)
const normalized = this.normalizeDetail(getObjectPayload(response))
this.$set(this.detailMap, targetNo, normalized)
this.remarksDraft = normalized.remarksDraft || ''
},
async switchInspection(item = {}) {
const inspectionNo = String(item.inspectionNo || '').trim()
if (!inspectionNo || inspectionNo === this.activeInspectionNo) {
return
}
this.activeInspectionNo = inspectionNo
await this.fetchInspectionDetail(inspectionNo)
},
handleRemarksInput(event) {
const value = event && event.detail ? event.detail.value : ''
this.remarksDraft = value
if (this.activeInspectionDetail) {
this.activeInspectionDetail.remarksDraft = value
}
},
updateItemField(index, field, value) {
const detail = this.activeInspectionDetail
if (!detail || !Array.isArray(detail.items) || !detail.items[index]) {
return
}
this.$set(detail.items[index], field, value)
},
getInspectionResultCategory(value = '') {
const result = String(value || '').trim()
if (!result) {
return 'pending'
}
const label = this.getInspectionItemResultLabel(result, '')
if (/未检|待检|未查/i.test(label)) {
return 'pending'
}
if (/问题|异常|损坏|缺失|故障/i.test(label)) {
return 'abnormal'
}
if (/正常|可用|完好/i.test(label)) {
return 'normal'
}
return 'done'
},
getNormalResultOption() {
return (Array.isArray(this.inspectionResultOptions) ? this.inspectionResultOptions : []).find((option) => {
return this.getInspectionResultCategory(option && option.key) === 'normal'
}) || null
},
shouldShowItemDetail(item = {}) {
return this.getInspectionResultCategory(item.resultDraft) === 'abnormal'
},
markAllItemsNormal() {
const detail = this.activeInspectionDetail
const normalOption = this.getNormalResultOption()
if (!detail || !Array.isArray(detail.items) || !detail.items.length || !normalOption) {
uni.showToast({
title: this.$tr('未找到正常结果选项'),
icon: 'none'
})
return
}
detail.items.forEach((item, index) => {
this.$set(detail.items[index], 'resultDraft', normalOption.key)
})
uni.showToast({
title: this.$tr('已全部勾选正常'),
icon: 'success'
})
},
buildSubmitItems() {
const detail = this.activeInspectionDetail
if (!detail) {
return []
}
return (Array.isArray(detail.items) ? detail.items : [])
.filter(item => item && item.itemId)
.map(item => {
const resultText = String(item.resultDraft || '').trim()
const checkedQuantityText = String(item.checkedQuantityDraft || '').trim()
return {
itemId: item.itemId,
result: resultText === '' ? null : Number(resultText),
checkedQuantity: checkedQuantityText === '' ? null : Number(checkedQuantityText),
problemDetail: String(item.problemDetailDraft || '').trim(),
imageUrls: Array.isArray(item.imageUrlsDraft) ? item.imageUrlsDraft.filter(Boolean) : []
}
})
},
validateBeforeSubmit() {
const items = this.buildSubmitItems()
const hasMissingResult = items.some(item => item.result === null || Number.isNaN(item.result))
if (hasMissingResult) {
uni.showToast({
title: this.$tr('请先完成所有检查项结果选择'),
icon: 'none'
})
return false
}
return true
},
closeRemarksDialog() {
this.remarksDialogVisible = false
},
submitInspection() {
const inspectionNo = String(this.activeInspectionNo || '').trim()
if (!inspectionNo || this.submitting) {
return
}
if (!this.validateBeforeSubmit()) {
return
}
this.remarksDialogVisible = true
},
submitWithoutRemarks() {
this.remarksDraft = ''
if (this.activeInspectionDetail) {
this.activeInspectionDetail.remarksDraft = ''
}
this.confirmSubmitInspection()
},
async confirmSubmitInspection() {
const inspectionNo = String(this.activeInspectionNo || '').trim()
if (!inspectionNo || this.submitting) {
return
}
this.remarksDialogVisible = false
this.submitting = true
try {
await roomInspectionSubmit({
inspectionNo,
remarks: this.remarksDraft,
items: this.buildSubmitItems()
}, this.language)
uni.showToast({
title: this.$tr('验房单已提交'),
icon: 'success'
})
setTimeout(() => {
uni.$emit('reservation-flow-force-refresh', {
source: 'room-inspection-submit',
orderNo: this.orderNo,
inspectionNo
})
uni.navigateBack()
}, 500)
} catch (error) {
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('提交失败'),
icon: 'none'
})
} finally {
this.submitting = false
}
},
chooseImages(index) {
uni.chooseImage({
count: 9,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempFilePaths = Array.isArray(res.tempFilePaths) ? res.tempFilePaths : []
if (!tempFilePaths.length) {
return
}
uni.showLoading({
title: this.$tr('正在同步验房数据...'),
mask: true
})
try {
const uploadedUrls = []
for (let fileIndex = 0; fileIndex < tempFilePaths.length; fileIndex += 1) {
const uploadResult = await uploadCommonFile(
tempFilePaths[fileIndex],
{},
'file',
{
...(getAuthToken() ? { Authorization: `Bearer ${getAuthToken()}` } : {})
}
)
const remoteUrl = resolveUploadedFileUrl(uploadResult)
if (!remoteUrl) {
throw new Error(this.$tr('图片上传失败'))
}
uploadedUrls.push(addFilePrefix(remoteUrl))
}
const detail = this.activeInspectionDetail
if (!detail || !Array.isArray(detail.items) || !detail.items[index]) {
return
}
const nextUrls = (detail.items[index].imageUrlsDraft || []).concat(uploadedUrls)
this.$set(detail.items[index], 'imageUrlsDraft', nextUrls)
} catch (error) {
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('图片上传失败'),
icon: 'none'
})
} finally {
uni.hideLoading()
}
},
fail: () => {
uni.showToast({
title: this.$tr('选择图片失败'),
icon: 'none'
})
}
})
},
removeImage(itemIndex, imageIndex) {
const detail = this.activeInspectionDetail
if (!detail || !Array.isArray(detail.items) || !detail.items[itemIndex]) {
return
}
const nextUrls = (detail.items[itemIndex].imageUrlsDraft || []).filter((_, index) => index !== imageIndex)
this.$set(detail.items[itemIndex], 'imageUrlsDraft', nextUrls)
},
previewImages(urls, currentIndex = 0) {
const safeUrls = Array.isArray(urls) ? urls.filter(Boolean).map(item => this.resolveImageUrl(item)) : []
if (!safeUrls.length) {
return
}
uni.previewImage({
urls: safeUrls,
current: safeUrls[currentIndex] || safeUrls[0]
})
},
resolveImageUrl(url = '') {
return addFilePrefix(url)
}
}
}
</script>
<style scoped lang="scss">
.app-page {
min-height: 100vh;
background: linear-gradient(180deg, #F2F5FC 0%, #F7F9FC 42%, #FBFCFE 100%);
padding-bottom: calc(188rpx + env(safe-area-inset-bottom));
position: relative;
overflow: hidden;
}
.page-bg {
position: fixed;
inset: 0;
pointer-events: none;
}
.bg-orb {
position: absolute;
border-radius: 999rpx;
filter: blur(12rpx);
}
.bg-orb--primary {
top: 120rpx;
right: -80rpx;
width: 320rpx;
height: 320rpx;
background: radial-gradient(circle, rgba(43, 142, 255, 0.18) 0%, rgba(43, 142, 255, 0.02) 72%, rgba(43, 142, 255, 0) 100%);
}
.bg-orb--soft {
top: 420rpx;
left: -120rpx;
width: 360rpx;
height: 360rpx;
background: radial-gradient(circle, rgba(133, 189, 255, 0.16) 0%, rgba(133, 189, 255, 0.02) 72%, rgba(133, 189, 255, 0) 100%);
}
.page-shell {
position: relative;
z-index: 1;
padding: 0 24rpx 24rpx;
}
.nav-title {
font-size: 30rpx;
font-weight: 600;
color: #10213A;
}
.hero-panel {
padding: 24rpx 28rpx;
border-radius: 36rpx;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.86) 0%, rgba(255, 255, 255, 0.97) 100%);
box-shadow: 0 18rpx 44rpx rgba(31, 80, 146, 0.08);
border: 1rpx solid rgba(255, 255, 255, 0.85);
backdrop-filter: blur(18rpx);
}
.hero-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 24rpx;
}
.hero-title-wrap {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
}
.hero-kicker {
font-size: 22rpx;
font-weight: 600;
color: #7E95B7;
letter-spacing: 8rpx;
}
.hero-title {
margin-top: 10rpx;
font-size: 52rpx;
line-height: 1.08;
font-weight: 700;
color: #12233C;
}
.hero-status {
padding: 12rpx 20rpx;
border-radius: 999rpx;
background: rgba(43, 142, 255, 0.1);
color: #2B8EFF;
font-size: 22rpx;
font-weight: 600;
white-space: nowrap;
}
.tab-wrap,
.empty-card {
margin-top: 24rpx;
border-radius: 30rpx;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 16rpx 36rpx rgba(31, 80, 146, 0.06);
border: 1rpx solid rgba(224, 233, 247, 0.95);
}
.section-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20rpx;
}
.section-head__title {
font-size: 28rpx;
font-weight: 700;
color: #14253F;
}
.section-head__meta {
font-size: 24rpx;
font-weight: 600;
color: #6E83A6;
}
.section-head__actions {
display: flex;
align-items: center;
gap: 14rpx;
}
.section-action-btn {
height: 52rpx;
padding: 0 22rpx;
border-radius: 999rpx;
background: rgba(43, 142, 255, 0.1);
color: #2B8EFF;
font-size: 22rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
}
.tab-wrap,
.empty-card {
padding: 24rpx;
}
.tab-scroll {
width: 100%;
margin-top: 18rpx;
white-space: nowrap;
}
.tab-list {
display: inline-flex;
gap: 18rpx;
padding-right: 8rpx;
}
.tab-item {
min-width: 220rpx;
padding: 22rpx 24rpx;
border-radius: 24rpx;
background: linear-gradient(180deg, #F8FBFF 0%, #FFFFFF 100%);
border: 1rpx solid rgba(221, 231, 246, 0.95);
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.tab-item--active {
background: linear-gradient(135deg, rgba(43, 142, 255, 0.14) 0%, rgba(255, 255, 255, 0.98) 100%);
border-color: rgba(43, 142, 255, 0.22);
box-shadow: 0 10rpx 24rpx rgba(43, 142, 255, 0.1);
}
.tab-item__type {
font-size: 26rpx;
font-weight: 700;
color: #1B2C46;
}
.tab-item__room {
margin-top: 10rpx;
font-size: 26rpx;
color: #4C6285;
}
.tab-item__time {
margin-top: 12rpx;
font-size: 20rpx;
color: #8A9AB4;
}
.content-wrap {
position: relative;
z-index: 1;
margin-top: 24rpx;
}
.section-head__desc {
display: block;
margin-top: 10rpx;
font-size: 22rpx;
line-height: 1.6;
color: #7B8EAA;
}
.items-wrap {
margin-top: 12rpx;
}
.section-head--main {
margin-bottom: 20rpx;
}
.item-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.inspect-item {
position: relative;
overflow: hidden;
border-radius: 32rpx;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 16rpx 34rpx rgba(31, 80, 146, 0.07);
border: 1rpx solid rgba(224, 233, 247, 0.96);
}
.inspect-item__glow {
position: absolute;
top: -40rpx;
right: -30rpx;
width: 180rpx;
height: 180rpx;
border-radius: 999rpx;
background: radial-gradient(circle, rgba(43, 142, 255, 0.12) 0%, rgba(43, 142, 255, 0) 72%);
pointer-events: none;
}
.item-header {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 18rpx;
padding: 22rpx 24rpx;
}
.item-order {
width: 56rpx;
height: 56rpx;
border-radius: 18rpx;
background: linear-gradient(180deg, #EAF4FF 0%, #F5FAFF 100%);
color: #2B8EFF;
font-size: 22rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.item-title-col {
min-width: 0;
flex: 1;
}
.item-title-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.item-name {
font-size: 30rpx;
line-height: 1.35;
font-weight: 700;
color: #152742;
}
.item-meta {
display: block;
font-size: 22rpx;
color: #7E90AA;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-meta--detail {
margin-bottom: 18rpx;
}
.item-required {
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: rgba(255, 181, 72, 0.16);
color: #D47D17;
font-size: 20rpx;
font-weight: 600;
}
.item-header__right {
flex-shrink: 0;
max-width: 56%;
}
.item-body {
position: relative;
z-index: 1;
padding: 0 24rpx 24rpx;
border-top: 1rpx solid rgba(224, 233, 247, 0.9);
}
.data-grid {
display: flex;
gap: 16rpx;
}
.data-card {
flex: 1;
padding: 22rpx;
border-radius: 24rpx;
background: linear-gradient(180deg, #F8FBFF 0%, #FFFFFF 100%);
border: 1rpx solid rgba(221, 231, 246, 0.95);
box-sizing: border-box;
}
.data-card--input {
background: linear-gradient(180deg, #F5FAFF 0%, #FFFFFF 100%);
}
.field-label {
display: block;
font-size: 22rpx;
color: #7D8FAC;
}
.field-value {
display: block;
margin-top: 12rpx;
font-size: 34rpx;
line-height: 1.2;
font-weight: 700;
color: #152742;
}
.field-input {
width: 100%;
height: 74rpx;
margin-top: 12rpx;
padding: 0 20rpx;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.96);
border: 1rpx solid rgba(205, 220, 241, 0.95);
box-sizing: border-box;
font-size: 30rpx;
color: #152742;
}
.field-block {
margin-top: 18rpx;
padding: 22rpx;
border-radius: 24rpx;
background: linear-gradient(180deg, rgba(248, 251, 255, 0.96) 0%, rgba(255, 255, 255, 0.98) 100%);
border: 1rpx solid rgba(221, 231, 246, 0.95);
}
.field-block__head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 20rpx;
}
.field-block__title {
font-size: 25rpx;
font-weight: 700;
color: #172944;
}
.field-block__hint {
font-size: 20rpx;
color: #8A9AB4;
text-align: right;
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 18rpx;
}
.radio-group--compact {
margin-top: 0;
justify-content: flex-end;
gap: 10rpx;
}
.radio-btn {
min-width: 146rpx;
padding: 16rpx 24rpx;
border-radius: 18rpx;
background: #FFFFFF;
border: 1rpx solid rgba(205, 220, 241, 0.95);
font-size: 24rpx;
font-weight: 600;
color: #5A6F93;
text-align: center;
box-sizing: border-box;
}
.radio-btn--compact {
min-width: auto;
padding: 12rpx 18rpx;
border-radius: 999rpx;
font-size: 22rpx;
line-height: 1.2;
}
.radio-btn--active {
background: linear-gradient(135deg, rgba(43, 142, 255, 0.12) 0%, rgba(255, 255, 255, 0.98) 100%);
border-color: rgba(43, 142, 255, 0.22);
color: #2B8EFF;
box-shadow: 0 10rpx 22rpx rgba(43, 142, 255, 0.08);
}
.field-textarea {
width: 100%;
min-height: 112rpx;
margin-top: 18rpx;
padding: 18rpx 20rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.96);
border: 1rpx solid rgba(205, 220, 241, 0.95);
box-sizing: border-box;
font-size: 26rpx;
color: #152742;
line-height: 1.6;
}
.photo-uploader {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 18rpx;
}
.photo-item,
.photo-add {
width: 148rpx;
height: 148rpx;
border-radius: 24rpx;
overflow: hidden;
position: relative;
}
.photo-item {
background: #EFF5FD;
border: 1rpx solid rgba(221, 231, 246, 0.95);
}
.photo-img {
width: 100%;
height: 100%;
}
.photo-del {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 34rpx;
height: 34rpx;
border-radius: 999rpx;
background: rgba(10, 24, 45, 0.55);
color: #FFFFFF;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.photo-add {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #F9FBFF 0%, #FFFFFF 100%);
border: 1rpx dashed rgba(141, 179, 232, 0.95);
color: #7E95B7;
}
.photo-add-icon {
font-size: 44rpx;
line-height: 1;
}
.photo-add-text {
margin-top: 8rpx;
font-size: 20rpx;
}
.empty-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.empty-card--page {
margin: 24rpx;
min-height: 320rpx;
}
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: #1A2D49;
}
.empty-desc {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.7;
color: #8496B2;
}
.bottom-action {
position: fixed;
left: 24rpx;
right: 24rpx;
bottom: calc(20rpx + env(safe-area-inset-bottom));
z-index: 100;
padding: 18rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 18rpx 40rpx rgba(31, 80, 146, 0.14);
backdrop-filter: blur(16rpx);
border: 1rpx solid rgba(224, 233, 247, 0.95);
display: flex;
align-items: center;
justify-content: center;
}
.btn-submit {
width: 100%;
height: 92rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 700;
color: #FFFFFF;
background: linear-gradient(135deg, #4AA2FF 0%, #2B8EFF 58%, #1E6FE0 100%);
box-shadow: 0 16rpx 32rpx rgba(43, 142, 255, 0.28);
}
.btn-submit:active {
transform: translateY(2rpx);
}
.remarks-dialog {
position: fixed;
inset: 0;
z-index: 260;
}
.remarks-dialog__mask {
position: absolute;
inset: 0;
background: rgba(10, 24, 45, 0.28);
backdrop-filter: blur(8px);
}
.remarks-dialog__panel {
position: absolute;
left: 24rpx;
right: 24rpx;
bottom: calc(24rpx + env(safe-area-inset-bottom));
padding: 28rpx;
border-radius: 32rpx;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 20rpx 48rpx rgba(15, 37, 68, 0.18);
}
.remarks-dialog__title {
font-size: 32rpx;
font-weight: 700;
color: #152742;
}
.remarks-dialog__desc {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.7;
color: #7285A4;
}
.remarks-dialog__input {
width: 100%;
min-height: 160rpx;
margin-top: 24rpx;
padding: 22rpx 24rpx;
border-radius: 24rpx;
background: linear-gradient(180deg, #F7FAFF 0%, #FFFFFF 100%);
border: 1rpx solid rgba(221, 231, 246, 0.95);
box-sizing: border-box;
font-size: 26rpx;
line-height: 1.7;
color: #152742;
}
.remarks-dialog__footer {
margin-top: 24rpx;
display: flex;
gap: 16rpx;
}
.remarks-dialog__btn {
flex: 1;
height: 84rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
}
.remarks-dialog__btn--ghost {
background: #F3F7FE;
color: #5E7397;
}
.remarks-dialog__btn--primary {
background: linear-gradient(135deg, #4AA2FF 0%, #2B8EFF 58%, #1E6FE0 100%);
color: #FFFFFF;
}
.global-loading {
position: fixed;
inset: 0;
background: rgba(247, 250, 255, 0.82);
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 999;
}
.loading-text {
margin-top: 24rpx;
font-size: 26rpx;
color: #5E7397;
font-weight: 600;
}
.font-mono {
font-family: "DIN Alternate", "SF Pro Text", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
}
@media screen and (max-width: 360px) {
.hero-top,
.field-block__head {
flex-direction: column;
align-items: flex-start;
}
.data-grid,
.remarks-dialog__footer {
flex-direction: column;
}
.item-header {
align-items: flex-start;
flex-direction: column;
}
.item-header__right {
max-width: 100%;
width: 100%;
}
.radio-group--compact {
justify-content: flex-start;
}
.section-head__actions {
width: 100%;
justify-content: space-between;
}
.btn-submit {
width: 100%;
}
}
</style>