Files
yxe-mp/packageUser/travel/index.vue
T
weicw-dshsa 40fed38f83 fix(小程序): 修正 v-for :key 表达式告警,恢复列表 wx:key
非 h5 平台的 :key 只接受简单属性名,模板字面量与运算表达式会被编译器
整条丢弃(uni-template-compiler 的 getForKey:Identifier 走 *this 或原名,
MemberExpression 取属性名,其余一律 warning + 返回空串)。

所以这批告警不只是文案问题:产物里对应的 <block wx:for> 后面确实没有
wx:key,列表项因此失去复用标识,增删时会整段重建节点。本次按编译器
接受的写法逐一改正,共 13 个文件。

改法:
- 纯 URL 字符串数组(打卡/门头/小票/凭证/巡店照片)直接用元素自身
  :key="img",编译为 wx:key="*this"
- 对象数组用其已有唯一字段:连锁/门店 item.id、抄送用户 user.id、
  审计销量行 detail.key、编辑销量项 detail._key
- 本身无唯一字段的巡店列表补 _key:task/detail 沿用 visit.vue 已验证的
  _rowKey(自增 seed,增删稳定),records 与 AuditVisitReportItem 用序号

sales.vue 另有一处隐蔽问题:row 是 {item, index} 包装对象,
:key="row.item._key || row.index" 即便写成简单形式也会生成 wx:key="_key",
在 row 上取不到值。改为在 visibleSalesItems 里把 key 提到包装对象顶层,
_key 缺失时用序号兜底。create.vue 的销量摘要同样补 __key 兜底,避免重复 key。

注:task/detail 的 storeList 提交走 sanitizeStoreList() 显式白名单,
_rowKey 不会外泄到后端。

验证:HBuilderX cli 重新编译 —— 编译成功,非 h5 平台 :key 告警 0 条
(修改前满屏 40+ 条),产物中 wx:for 与 wx:key 数量逐文件相等。
2026-10-12 18:18:11 +08:00

894 lines
27 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="travel-page">
<view class="page-hero">
<AppNavbar title="差旅报销" show-back/>
<view class="intro-card">
<view class="card-title">
<view class="title-bar"></view>
<text>差旅报销</text>
</view>
<text class="intro-text">填写出差时间、出发地和目的地,上传凭证后进入审核。</text>
</view>
</view>
<view class="card block">
<view class="section-title">新增报销</view>
<view class="form">
<view class="form-row">
<text>开始时间</text>
<view class="picker-value" @tap="openDatePicker('startDate')">{{ travelForm.startDate || '点击选择' }}</view>
</view>
<view class="form-row">
<text>结束时间</text>
<view class="picker-value" @tap="openDatePicker('endDate')">{{ travelForm.endDate || '点击选择' }}</view>
</view>
<view class="form-row">
<text>出发地</text>
<view class="picker-value" :class="{placeholder: !travelForm.fromProvince}" @tap="openRegionPicker('from')">
{{ fromRegionText }}
</view>
</view>
<view class="form-row">
<text>目的地</text>
<view class="picker-value" :class="{placeholder: !travelForm.toProvince}" @tap="openRegionPicker('to')">
{{ toRegionText }}
</view>
</view>
<view class="form-row">
<text>交通费</text>
<input type="number" v-model="travelForm.transportCosts" placeholder="元"/>
</view>
<view class="form-row">
<text>住宿费</text>
<input type="number" v-model="travelForm.liveCosts" placeholder="元"/>
</view>
<view class="form-row">
<text>其他费用</text>
<input type="number" v-model="travelForm.otherCosts" placeholder="元"/>
</view>
<view class="form-row textarea-row">
<text>备注</text>
<textarea
v-model="travelForm.remark"
class="remark-input"
maxlength="200"
auto-height
placeholder="请输入备注(选填)"
/>
</view>
<view class="upload" @tap="uploadProof">
<view>
<view class="upload-title">出差凭证</view>
<view class="upload-meta">{{ images.length ? '已上传,点击继续上传' : '点击上传照片' }}</view>
</view>
<view class="chip">必填</view>
</view>
<view v-if="images.length" class="proof-list">
<view class="proof-item" v-for="(img, idx) in images" :key="img">
<image class="proof-image" :src="img" mode="aspectFill"/>
<view class="proof-remove" @tap.stop="removeProof(idx)">x</view>
</view>
</view>
<view class="primary-btn submit" @tap="submitTravel">提交报销</view>
</view>
</view>
<view class="card block">
<view class="section-title">历史报销</view>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="travelHistory.length === 0" class="empty">暂无记录</view>
<view v-else>
<view class="history" v-for="item in travelHistory" :key="item.id">
<view class="history-head" @tap="toggleHistoryDetail(item.id)">
<view class="history-main">
<view class="history-title">{{ item.fromText }} → {{ item.toText }}</view>
<view class="history-meta">{{ item.start }} 至 {{ item.end }}</view>
</view>
<view class="history-side">
<view class="history-status" :class="{'pending': item.status === '审核中', 'reject': item.status === '已驳回'}">
{{ item.status }}
</view>
<u-icon name="arrow-right" size="24" color="#9ca3af" :class="['history-arrow', item.expanded ? 'open' : '']"/>
</view>
</view>
<view v-if="item.expanded" class="history-detail">
<view class="history-meta">交通 ¥{{ item.traffic }} · 住宿 ¥{{ item.hotel }}</view>
<view class="history-meta">其他 ¥{{ item.other }}</view>
<view class="history-meta">备注:{{ item.remark || '-' }}</view>
<view class="history-meta">凭证:</view>
<view v-if="item.proofs && item.proofs.length" class="history-proof-list">
<view class="history-proof-item" v-for="(proof, proofIdx) in item.proofs" :key="proof">
<image
class="history-proof-image"
:src="proof"
mode="aspectFill"
@tap="previewHistoryProof(item.proofs, proofIdx)"
/>
</view>
</view>
<view v-else class="history-meta">-</view>
</view>
</view>
</view>
</view>
<u-datetime-picker
:show="showStartPicker"
:value="startPickerValue"
mode="date"
@confirm="e => onDateConfirm('startDate', e)"
@cancel="closeDatePicker('startDate')"
@close="closeDatePicker('startDate')"
/>
<u-datetime-picker
:show="showEndPicker"
:value="endPickerValue"
mode="date"
@confirm="e => onDateConfirm('endDate', e)"
@cancel="closeDatePicker('endDate')"
@close="closeDatePicker('endDate')"
/>
<view v-if="showRegionPicker" class="form-popup-mask" @tap="cancelRegionPicker"></view>
<view v-if="showRegionPicker" class="region-picker-popup card" @tap.stop>
<view class="region-picker-head">
<view class="region-picker-title">选择{{ activeRegionField === 'from' ? '出发地' : '目的地' }}</view>
<view class="region-picker-actions">
<view class="region-picker-btn" @tap="cancelRegionPicker">取消</view>
<view class="region-picker-btn primary" @tap="confirmRegionPicker">确定</view>
</view>
</view>
<view v-if="areaLoading" class="region-loading">加载中...</view>
<picker-view v-else class="picker-view" :value="areaIndex" @change="onAreaPickerChange">
<picker-view-column>
<view v-for="item in areaColumns.provinces" :key="item.id" class="picker-item">{{ item.name }}</view>
</picker-view-column>
<picker-view-column>
<view v-for="item in areaColumns.cities" :key="item.id" class="picker-item">{{ item.name }}</view>
</picker-view-column>
<picker-view-column>
<view v-for="item in areaColumns.regions" :key="item.id" class="picker-item">{{ item.name }}</view>
</picker-view-column>
</picker-view>
</view>
</view>
</template>
<script>
import {reimburseAddReq, reimburseUserListReq} from "@/api/reimburse";
import {areaListByFidReq} from "@/api/common";
import {chooseImg} from "@/util";
import dayjs from "dayjs";
import AppNavbar from "@/components/AppNavbar.vue";
export default {
components: {AppNavbar},
data() {
return {
travelForm: this.buildForm(),
images: [],
travelHistory: [],
loading: false,
showStartPicker: false,
showEndPicker: false,
showRegionPicker: false,
activeRegionField: 'from',
areaLoading: false,
areaIndex: [0, 0, 0],
areaColumns: {
provinces: [],
cities: [],
regions: []
}
}
},
computed: {
startPickerValue() {
return this.travelForm.startDate || Date.now()
},
endPickerValue() {
return this.travelForm.endDate || Date.now()
},
fromRegionText() {
return this.composeRegionText(this.travelForm.fromProvince, this.travelForm.fromCity, this.travelForm.fromRegion) || '请选择出发地'
},
toRegionText() {
return this.composeRegionText(this.travelForm.toProvince, this.travelForm.toCity, this.travelForm.toRegion) || '请选择目的地'
}
},
async onShow() {
await this.ensureDefaultProvince()
await this.fetchHistory()
},
methods: {
buildForm() {
return {
startDate: '',
endDate: '',
fromProvince: '',
fromCity: '',
fromRegion: '',
toProvince: '',
toCity: '',
toRegion: '',
transportCosts: '',
liveCosts: '',
otherCosts: '',
remark: '',
images: ''
}
},
composeRegionText(province, city, region) {
return [province, city, region].filter(Boolean).join(' ')
},
getFieldArea(field) {
if (field === 'from') {
return {
province: this.travelForm.fromProvince,
city: this.travelForm.fromCity,
region: this.travelForm.fromRegion
}
}
return {
province: this.travelForm.toProvince,
city: this.travelForm.toCity,
region: this.travelForm.toRegion
}
},
setFieldArea(field, province, city, region) {
if (field === 'from') {
this.travelForm.fromProvince = province || ''
this.travelForm.fromCity = city || ''
this.travelForm.fromRegion = region || ''
return
}
this.travelForm.toProvince = province || ''
this.travelForm.toCity = city || ''
this.travelForm.toRegion = region || ''
},
normalizeAreaItem(item) {
return {
id: item.areaId || item.code || item.value || item.id || null,
name: item.name || item.title || item.areaName || item.label || ''
}
},
async fetchAreaListByFid(fid) {
const {data} = await areaListByFidReq({fid})
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
return list.map(this.normalizeAreaItem)
},
findGuangxiProvince(provinces = []) {
return provinces.find(item => String(item.name || '').includes('广西')) || null
},
async buildDefaultArea(provinces = [], preferredProvinceName = '') {
let province = null
if (preferredProvinceName) {
province = provinces.find(item => item.name === preferredProvinceName)
}
if (!province) {
province = this.findGuangxiProvince(provinces) || provinces[0] || null
}
if (!province) {
return {province: '', city: '', region: ''}
}
const cities = await this.fetchAreaListByFid(province.id)
const city = cities[0] || null
const regions = city ? await this.fetchAreaListByFid(city.id) : []
const region = regions[0] || null
return {
province: province.name || '',
city: city ? city.name : '',
region: region ? region.name : ''
}
},
async ensureDefaultProvince() {
if (
this.travelForm.fromProvince && this.travelForm.fromCity && this.travelForm.fromRegion &&
this.travelForm.toProvince && this.travelForm.toCity && this.travelForm.toRegion
) {
return
}
try {
const provinces = await this.fetchAreaListByFid(0)
const fromDefault = await this.buildDefaultArea(provinces, this.travelForm.fromProvince)
const toDefault = await this.buildDefaultArea(provinces, this.travelForm.toProvince)
if (!this.travelForm.fromProvince) this.travelForm.fromProvince = fromDefault.province
if (!this.travelForm.fromCity) this.travelForm.fromCity = fromDefault.city
if (!this.travelForm.fromRegion) this.travelForm.fromRegion = fromDefault.region
if (!this.travelForm.toProvince) this.travelForm.toProvince = toDefault.province
if (!this.travelForm.toCity) this.travelForm.toCity = toDefault.city
if (!this.travelForm.toRegion) this.travelForm.toRegion = toDefault.region
} catch (err) {
if (!this.travelForm.fromProvince) this.travelForm.fromProvince = '广西壮族自治区'
if (!this.travelForm.fromCity) this.travelForm.fromCity = ''
if (!this.travelForm.fromRegion) this.travelForm.fromRegion = ''
if (!this.travelForm.toProvince) this.travelForm.toProvince = '广西壮族自治区'
if (!this.travelForm.toCity) this.travelForm.toCity = ''
if (!this.travelForm.toRegion) this.travelForm.toRegion = ''
}
},
async initAreaColumns(field) {
this.areaLoading = true
try {
const provinces = await this.fetchAreaListByFid(0)
this.areaColumns.provinces = provinces
const current = this.getFieldArea(field)
const defaultProvince = this.findGuangxiProvince(provinces)
let provinceIndex = provinces.findIndex(item => item.name === current.province)
if (provinceIndex < 0 && current.province) {
provinceIndex = provinces.findIndex(item => current.province.includes(item.name) || item.name.includes(current.province))
}
if (provinceIndex < 0 && defaultProvince) {
provinceIndex = provinces.findIndex(item => item.id === defaultProvince.id)
}
if (provinceIndex < 0) provinceIndex = 0
const province = provinces[provinceIndex]
const cities = province ? await this.fetchAreaListByFid(province.id) : []
this.areaColumns.cities = cities
let cityIndex = cities.findIndex(item => item.name === current.city)
if (cityIndex < 0) cityIndex = 0
const city = cities[cityIndex]
const regions = city ? await this.fetchAreaListByFid(city.id) : []
this.areaColumns.regions = regions
let regionIndex = regions.findIndex(item => item.name === current.region)
if (regionIndex < 0) regionIndex = 0
this.areaIndex = [provinceIndex, cityIndex, regionIndex]
} catch (err) {
uni.showToast({
title: '地区加载失败',
icon: 'none'
})
} finally {
this.areaLoading = false
}
},
async updateCitiesByProvince(provinceIndex) {
const province = this.areaColumns.provinces[provinceIndex]
this.areaColumns.cities = province ? await this.fetchAreaListByFid(province.id) : []
this.areaColumns.regions = []
this.areaIndex = [provinceIndex, 0, 0]
await this.updateRegionsByCity(provinceIndex, 0)
},
async updateRegionsByCity(provinceIndex, cityIndex) {
const city = this.areaColumns.cities[cityIndex]
this.areaColumns.regions = city ? await this.fetchAreaListByFid(city.id) : []
this.areaIndex = [provinceIndex, cityIndex, 0]
},
async openRegionPicker(field) {
this.activeRegionField = field
this.showRegionPicker = true
await this.initAreaColumns(field)
},
cancelRegionPicker() {
this.showRegionPicker = false
},
confirmRegionPicker() {
const [pIndex, cIndex, rIndex] = this.areaIndex
const province = this.areaColumns.provinces[pIndex]
const city = this.areaColumns.cities[cIndex]
const region = this.areaColumns.regions[rIndex]
this.setFieldArea(this.activeRegionField, province ? province.name : '', city ? city.name : '', region ? region.name : '')
this.showRegionPicker = false
},
async onAreaPickerChange(e) {
const value = e.detail.value || [0, 0, 0]
const [pIndex, cIndex] = value
const [prevP, prevC] = this.areaIndex
if (pIndex !== prevP) {
await this.updateCitiesByProvince(pIndex)
return
}
if (cIndex !== prevC) {
await this.updateRegionsByCity(pIndex, cIndex)
return
}
this.areaIndex = value
},
async uploadProof() {
const remain = 9 - this.images.length
if (remain <= 0) return
const res = await chooseImg(remain)
this.images = [
...this.images,
...res.map(i => i.path)
]
},
removeProof(idx) {
this.images.splice(idx, 1)
},
async submitTravel() {
if (
!this.travelForm.startDate ||
!this.travelForm.endDate ||
!this.travelForm.fromProvince || !this.travelForm.fromCity || !this.travelForm.fromRegion ||
!this.travelForm.toProvince || !this.travelForm.toCity || !this.travelForm.toRegion ||
!this.images.length
) {
uni.showToast({
title: '请填写必填项并上传凭证',
icon: 'none'
})
return
}
const start = dayjs(this.travelForm.startDate)
const end = dayjs(this.travelForm.endDate)
if (start.isValid() && end.isValid() && end.isBefore(start, 'day')) {
uni.showToast({
title: '结束时间不能早于开始时间',
icon: 'none'
})
return
}
const fromText = this.composeRegionText(this.travelForm.fromProvince, this.travelForm.fromCity, this.travelForm.fromRegion)
const toText = this.composeRegionText(this.travelForm.toProvince, this.travelForm.toCity, this.travelForm.toRegion)
try {
await reimburseAddReq({
startDate: this.travelForm.startDate,
endDate: this.travelForm.endDate,
fromProvince: this.travelForm.fromProvince,
fromCity: this.travelForm.fromCity,
fromRegion: this.travelForm.fromRegion,
departureProvince: this.travelForm.fromProvince,
departureCity: this.travelForm.fromCity,
departureRegion: this.travelForm.fromRegion,
departure: fromText,
toProvince: this.travelForm.toProvince,
toCity: this.travelForm.toCity,
toRegion: this.travelForm.toRegion,
destinationProvince: this.travelForm.toProvince,
destinationCity: this.travelForm.toCity,
destinationRegion: this.travelForm.toRegion,
destination: toText,
city: toText,
transportCosts: this.travelForm.transportCosts,
liveCosts: this.travelForm.liveCosts,
otherCosts: this.travelForm.otherCosts,
remark: this.travelForm.remark,
images: JSON.stringify(this.images)
})
uni.showToast({
title: '报销已提交审核',
icon: 'success'
})
const keepFromProvince = this.travelForm.fromProvince
const keepFromCity = this.travelForm.fromCity
const keepFromRegion = this.travelForm.fromRegion
const keepToProvince = this.travelForm.toProvince
const keepToCity = this.travelForm.toCity
const keepToRegion = this.travelForm.toRegion
this.travelForm = this.buildForm()
this.travelForm.fromProvince = keepFromProvince
this.travelForm.fromCity = keepFromCity
this.travelForm.fromRegion = keepFromRegion
this.travelForm.toProvince = keepToProvince
this.travelForm.toCity = keepToCity
this.travelForm.toRegion = keepToRegion
this.images = []
await this.fetchHistory()
} catch (err) {
uni.showToast({
title: '提交失败',
icon: 'none'
})
}
},
async fetchHistory() {
this.loading = true
try {
const {data} = await reimburseUserListReq()
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
this.travelHistory = list.map(this.normalizeHistory)
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
normalizeHistory(item) {
const statusValue = Number(item.status)
let statusLabel = '待审核'
if (statusValue === 0) statusLabel = '审核中'
if (statusValue === 1) statusLabel = '已通过'
if (statusValue === 2) statusLabel = '已驳回'
const fromProvince = item.fromProvince || item.departureProvince || item.startProvince || ''
const fromCity = item.fromCity || item.departureCity || item.startCity || ''
const fromRegion = item.fromRegion || item.departureRegion || item.startRegion || ''
const toProvince = item.toProvince || item.destinationProvince || ''
const toCity = item.toCity || item.destinationCity || ''
const toRegion = item.toRegion || item.destinationRegion || ''
const fromText = this.composeRegionText(fromProvince, fromCity, fromRegion) || item.departure || item.from || '-'
const toText = this.composeRegionText(toProvince, toCity, toRegion) || item.city || item.destination || '-'
const proofs = this.parseHistoryProofs(item.images || item.image || item.proof || item.proofs || item.receiptPhoto || item.receiptPhotos)
return {
id: item.id,
start: item.startDate || item.beginDate || '',
end: item.endDate || '',
fromText,
toText,
traffic: item.transportCosts || 0,
hotel: item.liveCosts || 0,
other: item.otherCosts || 0,
remark: item.remark || item.note || item.memo || '',
proofs,
status: statusLabel,
expanded: false
}
},
parseHistoryProofs(value) {
if (value === null || value === undefined || value === '') return []
if (Array.isArray(value)) {
return value
.map(item => {
if (!item) return ''
if (typeof item === 'string') return item
return item.url || item.path || item.image || item.img || ''
})
.filter(Boolean)
}
if (typeof value === 'object') {
const url = value.url || value.path || value.image || value.img
return url ? [url] : []
}
const text = String(value).trim()
if (!text) return []
if (text.startsWith('[') || text.startsWith('{')) {
try {
return this.parseHistoryProofs(JSON.parse(text))
} catch (err) {
}
}
if (text.includes(',')) {
return text.split(',').map(item => item.trim()).filter(Boolean)
}
return [text]
},
previewHistoryProof(list = [], index = 0) {
if (!Array.isArray(list) || list.length === 0) return
uni.previewImage({
urls: list,
current: list[index] || list[0]
})
},
toggleHistoryDetail(id) {
this.travelHistory = this.travelHistory.map(item => {
if (item.id !== id) return item
return {
...item,
expanded: !item.expanded
}
})
},
openDatePicker(field) {
if (field === 'startDate') this.showStartPicker = true
if (field === 'endDate') this.showEndPicker = true
},
closeDatePicker(field) {
if (field === 'startDate') this.showStartPicker = false
if (field === 'endDate') this.showEndPicker = false
},
onDateConfirm(field, e) {
const value = dayjs(e.value).format('YYYY-MM-DD')
this.travelForm[field] = value
this.closeDatePicker(field)
}
}
}
</script>
<style lang="scss" scoped>
.travel-page {
min-height: 100vh;
background: #f5f5f5;
}
.page-hero {
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat
top center / 100% auto;
}
.page-navbar {
padding: 0 6rpx;
}
.intro-card {
margin: 18rpx 20rpx 0;
padding: 20rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.title-bar {
width: 6rpx;
height: 30rpx;
border-radius: 999rpx;
background: #2fc6a7;
}
.intro-text {
margin-top: 12rpx;
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
}
.block {
margin: 18rpx 20rpx 0;
}
.form {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.form-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.form-row input {
text-align: right;
color: var(--text-main);
}
.picker-value {
color: var(--text-main);
font-weight: 600;
}
.picker-value.placeholder {
color: var(--text-sub);
}
.textarea-row {
align-items: flex-start;
padding-top: 14rpx;
}
.remark-input {
flex: 1;
min-height: 100rpx;
text-align: right;
font-size: 26rpx;
color: var(--text-main);
}
.upload {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.upload-title {
font-weight: 700;
}
.upload-meta {
color: var(--text-sub);
font-size: 24rpx;
}
.proof-list {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 10rpx;
}
.proof-item {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
overflow: hidden;
}
.proof-image {
width: 100%;
height: 100%;
display: block;
}
.proof-remove {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 34rpx;
height: 34rpx;
background: rgba(0, 0, 0, 0.6);
color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
line-height: 1;
}
.submit {
margin-top: 12rpx;
padding: 14rpx;
text-align: center;
}
.history {
padding: 12rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.history:last-child {
border-bottom: none;
}
.history-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.history-main {
flex: 1;
}
.history-side {
display: flex;
align-items: center;
gap: 8rpx;
}
.history-title {
font-weight: 700;
}
.history-meta {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 4rpx;
}
.history-status {
color: var(--brand-primary);
font-weight: 700;
margin: 8rpx 0;
}
.history-status.pending {
color: #ff8354;
}
.history-status.reject {
color: #ff3b30;
}
.history-arrow {
transform: rotate(0deg);
transition: transform 0.2s ease;
}
.history-arrow.open {
transform: rotate(90deg);
}
.history-detail {
margin-top: 8rpx;
padding: 8rpx 10rpx;
border-radius: 12rpx;
background: #f8fafc;
}
.history-proof-list {
margin-top: 8rpx;
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.history-proof-item {
width: 140rpx;
height: 140rpx;
border-radius: 10rpx;
overflow: hidden;
background: #ffffff;
}
.history-proof-image {
width: 100%;
height: 100%;
display: block;
}
.empty {
color: var(--text-sub);
text-align: center;
padding: 16rpx 0;
}
.form-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 30;
}
.region-picker-popup {
position: fixed;
left: 50%;
bottom: 40rpx;
transform: translateX(-50%);
width: calc(100vw - 40rpx);
max-height: 80vh;
z-index: 31;
border-radius: 18rpx;
overflow: hidden;
padding: 0;
}
.region-picker-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 12rpx;
background: #f6f8fc;
}
.region-picker-title {
font-size: 24rpx;
color: var(--text-main);
}
.region-picker-actions {
display: flex;
gap: 12rpx;
}
.region-picker-btn {
font-size: 24rpx;
color: var(--text-sub);
}
.region-picker-btn.primary {
color: var(--brand-primary);
}
.region-loading {
padding: 16rpx 0;
text-align: center;
color: var(--text-sub);
font-size: 24rpx;
}
.picker-view {
height: 360rpx;
}
.picker-item {
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 26rpx;
color: var(--text-main);
}
</style>