将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。 - 新增 utils/i18n.js 国际化工具函数 - 更新 store/$t.mixin.js 翻译混入逻辑 - 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
1053 lines
28 KiB
Vue
1053 lines
28 KiB
Vue
<template>
|
||
<view class="app-page">
|
||
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="#FFFFFF">
|
||
<!-- Empty slot for title to keep it clean, or use default -->
|
||
</tn-nav-bar>
|
||
|
||
<!-- Header -->
|
||
<view class="page-header" :style="{ paddingTop: vuex_custom_bar_height ? 'calc(' + vuex_custom_bar_height + 'px + 32rpx)' : '32rpx' }">
|
||
<view class="title-wrap">
|
||
<text class="title-zh">{{ $tr('验房') }}</text>
|
||
<text class="title-en">Inspection</text>
|
||
</view>
|
||
<view v-if="activeInspection" class="status-tag">
|
||
{{ activeInspection.statusText }}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Inspection Tabs (Only show if > 1) -->
|
||
<view v-if="inspectionList.length > 1" class="tab-wrap">
|
||
<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-type">{{ item.typeText }}</text>
|
||
<text class="tab-room font-mono">{{ item.roomNo || '--' }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<view v-if="activeInspectionDetail" class="content-wrap">
|
||
<!-- High Density Info Grid -->
|
||
<view class="info-grid">
|
||
<view class="info-cell">
|
||
<text class="info-label">{{ $tr('房间号') }}</text>
|
||
<text class="info-value font-mono">{{ activeInspectionDetail.roomNo || '--' }}</text>
|
||
</view>
|
||
<view class="info-cell">
|
||
<text class="info-label">{{ $tr('验房类型') }}</text>
|
||
<text class="info-value">{{ activeInspectionDetail.typeText }}</text>
|
||
</view>
|
||
<view class="info-cell full-width">
|
||
<text class="info-label">{{ $tr('发起时间') }}</text>
|
||
<text class="info-value font-mono">{{ activeInspectionDetail.createTimeText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Remarks -->
|
||
<view class="remarks-wrap">
|
||
<textarea
|
||
class="remarks-input"
|
||
:value="remarksDraft"
|
||
:placeholder="$tr('可填写本次验房的补充说明')"
|
||
maxlength="300"
|
||
auto-height
|
||
@input="handleRemarksInput"
|
||
/>
|
||
</view>
|
||
|
||
<!-- Items -->
|
||
<view v-if="activeInspectionDetail.items.length" class="items-wrap">
|
||
<view class="section-title">
|
||
<text class="section-title-zh">{{ $tr('检查项') }}</text>
|
||
<text class="section-title-en">Items</text>
|
||
</view>
|
||
|
||
<view class="item-list">
|
||
<view
|
||
v-for="(item, index) in activeInspectionDetail.items"
|
||
:key="item.itemId || index"
|
||
class="inspect-item"
|
||
>
|
||
<view class="item-header">
|
||
<view class="item-title-col">
|
||
<text class="item-name">{{ item.deviceName || $tr('未命名设备') }}</text>
|
||
<text class="item-meta" v-if="item.assetSourceText || item.brandSnapshot">
|
||
{{ [item.assetSourceText, item.brandSnapshot].filter(Boolean).join(' / ') }}
|
||
</text>
|
||
</view>
|
||
<text v-if="item.isRequiredSnapshot" class="item-required">{{ $tr('必查') }}</text>
|
||
</view>
|
||
|
||
<view class="item-body">
|
||
<view class="row-flex">
|
||
<view class="col-half">
|
||
<text class="field-label">{{ $tr('应有数量') }}</text>
|
||
<text class="field-value font-mono">{{ item.expectedQuantityText }}</text>
|
||
</view>
|
||
<view class="col-half">
|
||
<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-row mt-20">
|
||
<text class="field-label">{{ $tr('检查结果') }}</text>
|
||
<view class="radio-group">
|
||
<view
|
||
v-for="option in inspectionResultOptions"
|
||
:key="option.key"
|
||
class="radio-btn"
|
||
:class="{ 'radio-btn--active': String(item.resultDraft) === String(option.key) }"
|
||
@click="updateItemField(index, 'resultDraft', option.key)"
|
||
>
|
||
{{ option.value }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="field-row mt-20">
|
||
<textarea
|
||
class="field-textarea"
|
||
:value="item.problemDetailDraft"
|
||
:placeholder="$tr('请描述异常、缺失或损坏情况')"
|
||
maxlength="300"
|
||
auto-height
|
||
@input="updateItemField(index, 'problemDetailDraft', $event.detail.value)"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field-row mt-20">
|
||
<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>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else-if="!loading" class="empty-state">
|
||
<text class="empty-title">{{ $tr('暂无检查项') }}</text>
|
||
<text class="empty-desc">{{ $tr('当前验房单还没有检查项明细。') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!loading && !inspectionList.length" class="empty-state">
|
||
<text class="empty-title">{{ $tr('暂无验房单') }}</text>
|
||
<text class="empty-desc">{{ $tr('当前订单还没有可查看的验房记录。') }}</text>
|
||
</view>
|
||
|
||
<view v-if="activeInspectionDetail" class="bottom-action">
|
||
<view class="btn-submit" @click="submitInspection">
|
||
{{ submitting ? $tr('提交中...') : $tr('提交验房') }}
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="loading" class="global-loading">
|
||
<tn-loading mode="circle" color="#0F172A" 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: ''
|
||
}
|
||
},
|
||
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)
|
||
},
|
||
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
|
||
},
|
||
async submitInspection() {
|
||
const inspectionNo = String(this.activeInspectionNo || '').trim()
|
||
if (!inspectionNo || this.submitting) {
|
||
return
|
||
}
|
||
if (!this.validateBeforeSubmit()) {
|
||
return
|
||
}
|
||
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-color: #F8FAFC;
|
||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.page-header {
|
||
padding: 32rpx 32rpx 24rpx;
|
||
background: #FFFFFF;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.title-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.title-zh {
|
||
font-size: 40rpx;
|
||
font-weight: 600;
|
||
color: #0F172A;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.title-en {
|
||
font-size: 22rpx;
|
||
font-weight: 500;
|
||
color: #94A3B8;
|
||
margin-top: 8rpx;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.status-tag {
|
||
padding: 8rpx 16rpx;
|
||
background: #F1F5F9;
|
||
color: #475569;
|
||
border-radius: 8rpx;
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tab-wrap {
|
||
background: #FFFFFF;
|
||
border-top: 1rpx solid #F1F5F9;
|
||
padding: 0 32rpx 24rpx;
|
||
}
|
||
|
||
.tab-scroll {
|
||
width: 100%;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tab-list {
|
||
display: inline-flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 16rpx 24rpx;
|
||
background: #F8FAFC;
|
||
border-radius: 12rpx;
|
||
border: 1rpx solid #E2E8F0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 180rpx;
|
||
}
|
||
|
||
.tab-item--active {
|
||
background: #F0F9FF;
|
||
border-color: #BAE6FD;
|
||
}
|
||
|
||
.tab-item--active .tab-type {
|
||
color: #0284C7;
|
||
}
|
||
|
||
.tab-type {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: #334155;
|
||
}
|
||
|
||
.tab-room {
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.content-wrap {
|
||
padding: 24rpx 32rpx;
|
||
}
|
||
|
||
/* Info Grid */
|
||
.info-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
background: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
border: 1rpx solid #E2E8F0;
|
||
overflow: hidden;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.info-cell {
|
||
width: 50%;
|
||
padding: 20rpx 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
border-bottom: 1rpx solid #E2E8F0;
|
||
border-right: 1rpx solid #E2E8F0;
|
||
}
|
||
|
||
.info-cell:nth-child(even) {
|
||
border-right: none;
|
||
}
|
||
|
||
.info-cell.full-width {
|
||
width: 100%;
|
||
border-right: none;
|
||
border-bottom: none;
|
||
}
|
||
|
||
.info-cell:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 26rpx;
|
||
color: #0F172A;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Remarks */
|
||
.remarks-wrap {
|
||
background: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
border: 1rpx solid #E2E8F0;
|
||
padding: 20rpx 24rpx;
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.remarks-input {
|
||
width: 100%;
|
||
min-height: 80rpx;
|
||
font-size: 26rpx;
|
||
color: #0F172A;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Section Title */
|
||
.section-title {
|
||
display: flex;
|
||
align-items: baseline;
|
||
margin-bottom: 20rpx;
|
||
padding-left: 8rpx;
|
||
}
|
||
|
||
.section-title-zh {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #0F172A;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.section-title-en {
|
||
font-size: 20rpx;
|
||
font-weight: 500;
|
||
color: #94A3B8;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* Inspect Items */
|
||
.item-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.inspect-item {
|
||
background: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
border: 1rpx solid #E2E8F0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.item-header {
|
||
padding: 20rpx 24rpx;
|
||
background: #F8FAFC;
|
||
border-bottom: 1rpx solid #E2E8F0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.item-title-col {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
padding-right: 20rpx;
|
||
}
|
||
|
||
.item-name {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #0F172A;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.item-meta {
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
.item-required {
|
||
padding: 4rpx 12rpx;
|
||
background: #FEF3C7;
|
||
color: #0369A1;
|
||
border-radius: 6rpx;
|
||
font-size: 20rpx;
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.item-body {
|
||
padding: 24rpx;
|
||
}
|
||
|
||
.row-flex {
|
||
display: flex;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.col-half {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.field-label {
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.field-value {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #0F172A;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.field-input {
|
||
height: 64rpx;
|
||
background: #F8FAFC;
|
||
border: 1rpx solid #E2E8F0;
|
||
border-radius: 8rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 28rpx;
|
||
color: #0F172A;
|
||
}
|
||
|
||
.mt-20 {
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.radio-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.radio-btn {
|
||
padding: 12rpx 28rpx;
|
||
background: #F1F5F9;
|
||
border-radius: 8rpx;
|
||
font-size: 24rpx;
|
||
color: #475569;
|
||
font-weight: 500;
|
||
border: 1rpx solid transparent;
|
||
}
|
||
|
||
.radio-btn--active {
|
||
background: #F0F9FF;
|
||
color: #0284C7;
|
||
border-color: #BAE6FD;
|
||
}
|
||
|
||
.field-textarea {
|
||
width: 100%;
|
||
min-height: 80rpx;
|
||
background: #F8FAFC;
|
||
border: 1rpx solid #E2E8F0;
|
||
border-radius: 8rpx;
|
||
padding: 16rpx 20rpx;
|
||
font-size: 26rpx;
|
||
color: #0F172A;
|
||
box-sizing: border-box;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.photo-uploader {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.photo-item,
|
||
.photo-add {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.photo-item {
|
||
background: #F1F5F9;
|
||
border: 1rpx solid #E2E8F0;
|
||
}
|
||
|
||
.photo-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.photo-del {
|
||
position: absolute;
|
||
top: 6rpx;
|
||
right: 6rpx;
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
border-radius: 50%;
|
||
color: #FFFFFF;
|
||
font-size: 28rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
line-height: 1;
|
||
}
|
||
|
||
.photo-add {
|
||
background: #F8FAFC;
|
||
border: 1rpx dashed #CBD5E1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.photo-add-icon {
|
||
font-size: 48rpx;
|
||
color: #94A3B8;
|
||
font-weight: 300;
|
||
}
|
||
|
||
/* Empty State */
|
||
.empty-state {
|
||
padding: 80rpx 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.empty-title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #334155;
|
||
}
|
||
|
||
.empty-desc {
|
||
font-size: 24rpx;
|
||
color: #94A3B8;
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
/* Bottom Action */
|
||
.bottom-action {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 24rpx 32rpx;
|
||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||
background: #FFFFFF;
|
||
border-top: 1rpx solid #E2E8F0;
|
||
z-index: 100;
|
||
}
|
||
|
||
.btn-submit {
|
||
height: 88rpx;
|
||
background: #0F172A;
|
||
color: #FFFFFF;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
box-shadow: 0 8rpx 16rpx rgba(15, 23, 42, 0.15);
|
||
}
|
||
|
||
.btn-submit:active {
|
||
opacity: 0.9;
|
||
transform: translateY(2rpx);
|
||
}
|
||
|
||
/* Global Loading */
|
||
.global-loading {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
backdrop-filter: blur(4px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 999;
|
||
}
|
||
|
||
.loading-text {
|
||
margin-top: 24rpx;
|
||
font-size: 26rpx;
|
||
color: #475569;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.font-mono {
|
||
font-family: "DIN Alternate", "SF Pro Text", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
||
}
|
||
</style>
|