Files
yxe-mp/pages/visit/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

990 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="visit-page">
<view class="page-hero">
<AppNavbar title="客户拜访" show-back/>
<view class="intro-card">
<view class="tip-icon">
<u-icon name="info-circle-fill" size="40rpx" :color="brandColor"/>
</view>
<view class="tip-body">
<text class="tip-title">温馨提示</text>
<text class="tip-text">请如实填写本次拜访信息,系统将自动记录您的姓名,拜访对象请填写实际到访人员或地点/经理。</text>
</view>
</view>
</view>
<!-- 我的信息 -->
<view class="card block">
<view class="form-card-head">
<view class="head-icon">
<u-icon name="account-fill" size="32rpx" :color="brandColor"/>
</view>
<text class="head-title">我的信息</text>
</view>
<view class="field-row">
<text class="field-label is-required"><text class="field-star">*</text>姓名</text>
<view class="field-box">
<u-icon name="account" size="32rpx" color="#9aa7b8"/>
<input
v-model="visitForm.submitterName"
class="field-input"
type="text"
maxlength="30"
placeholder="请输入您的姓名"
placeholder-class="field-placeholder"
/>
</view>
</view>
</view>
<!-- 拜访对象 -->
<view class="card block">
<view class="form-card-head">
<view class="head-icon">
<u-icon name="account" size="32rpx" :color="brandColor"/>
</view>
<text class="head-title">拜访对象</text>
</view>
<view class="field-row">
<text class="field-label is-required"><text class="field-star">*</text>类型</text>
<view class="type-tabs">
<view
v-for="tab in visitTypeTabs"
:key="tab.value"
:class="['type-tab', visitForm.visitType === tab.value ? 'active' : '']"
@tap="visitForm.visitType = tab.value"
>
<u-icon :name="tab.icon" size="28rpx" :color="visitForm.visitType === tab.value ? brandColor : '#9aa7b8'"/>
<text>{{ tab.label }}</text>
</view>
</view>
</view>
<view class="field-row">
<text class="field-label is-required"><text class="field-star">*</text>姓名/地点</text>
<view class="field-box">
<input
v-model="visitForm.name"
class="field-input"
type="text"
maxlength="30"
:placeholder="targetPlaceholder"
placeholder-class="field-placeholder"
/>
<u-icon name="arrow-right" size="26rpx" color="#b6c2d1"/>
</view>
</view>
<view class="field-row">
<text class="field-label is-required"><text class="field-star">*</text>连锁</text>
<view class="field-box" @tap="openChainPicker">
<text class="field-text" :class="{placeholder: !visitForm.chain}">
{{ visitForm.chain || (chainLoading ? '连锁加载中...' : '请选择连锁') }}
</text>
<u-icon name="arrow-right" size="26rpx" color="#b6c2d1"/>
</view>
</view>
</view>
<!-- 拜访信息 -->
<view class="card block">
<view class="form-card-head">
<view class="head-icon">
<u-icon name="map-fill" size="32rpx" :color="brandColor"/>
</view>
<text class="head-title">拜访信息</text>
</view>
<view class="field-row">
<text class="field-label is-required"><text class="field-star">*</text>定位信息</text>
<view class="field-box" @tap="fetchLocation(false, true)">
<u-icon name="map" size="32rpx" color="#9aa7b8"/>
<text class="field-text" :class="{placeholder: !visitForm.location}">
{{ visitForm.location || '请点击获取当前位置' }}
</text>
<u-icon name="reload" size="30rpx" color="#b6c2d1"/>
</view>
</view>
<view v-if="visitForm.lat || visitForm.lng" class="field-meta">
<text>{{ formatCoordinate('纬度', visitForm.lat) }}</text>
<text>{{ formatCoordinate('经度', visitForm.lng) }}</text>
</view>
<view class="field-row">
<text class="field-label is-required"><text class="field-star">*</text>拜访日期</text>
<view class="field-box" @tap="showVisitDatePicker = true">
<u-icon name="calendar" size="32rpx" color="#9aa7b8"/>
<text class="field-text" :class="{placeholder: !visitForm.visitDate}">
{{ visitForm.visitDate || '请选择拜访日期' }}
</text>
<u-icon name="arrow-right" size="26rpx" color="#b6c2d1"/>
</view>
</view>
</view>
<!-- 其他信息 -->
<view class="card block">
<view class="form-card-head">
<view class="head-icon">
<u-icon name="edit-pen-fill" size="32rpx" :color="brandColor"/>
</view>
<text class="head-title">其他信息</text>
</view>
<view class="field-row textarea-row">
<text class="field-label is-required"><text class="field-star">*</text>沟通内容</text>
<view class="field-box textarea-box">
<textarea
v-model="visitForm.content"
class="field-textarea"
:maxlength="contentMaxLength"
placeholder="请输入沟通内容..."
placeholder-class="field-placeholder"
/>
<text class="counter">{{ (visitForm.content || '').length }}/{{ contentMaxLength }}</text>
</view>
</view>
<view class="field-row">
<text class="field-label is-required"><text class="field-star">*</text>拍照上传</text>
<view class="field-box" @tap="uploadPhotos">
<u-icon name="camera-fill" size="32rpx" color="#9aa7b8"/>
<text class="field-text" :class="{placeholder: !photos.length}">
{{ photos.length ? `已上传 ${photos.length} 张,点击继续拍照` : '点击拍照上传,最多 9 张' }}
</text>
<u-icon name="arrow-right" size="26rpx" color="#b6c2d1"/>
</view>
</view>
<view v-if="photos.length" class="photo-list">
<view class="photo-item" v-for="(photo, index) in photos" :key="photo">
<image class="photo-image" :src="photo" mode="aspectFill" @tap="previewPhoto(index)"/>
<view class="photo-remove" @tap.stop="removePhoto(index)">x</view>
</view>
</view>
<view class="field-row textarea-row">
<text class="field-label is-required"><text class="field-star">*</text>活动小结</text>
<view class="field-box textarea-box">
<textarea
v-model="visitForm.summary"
class="field-textarea"
maxlength="300"
placeholder="请输入活动小结"
placeholder-class="field-placeholder"
/>
<text class="counter">{{ (visitForm.summary || '').length }}/300</text>
</view>
</view>
</view>
<view class="submit-wrap">
<view class="primary-btn submit" @tap="submitVisit">
<text>提交拜访</text>
<u-icon name="arrow-right" size="30rpx" color="#ffffff"/>
</view>
</view>
<view class="card block">
<view class="form-card-head">
<view class="head-icon">
<u-icon name="list-dot" size="32rpx" :color="brandColor"/>
</view>
<text class="head-title">历史记录</text>
</view>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="visitHistory.length === 0" class="empty">暂无记录</view>
<view v-else>
<view class="history" v-for="item in visitHistory" :key="item.id">
<view class="history-head" @tap="toggleHistoryDetail(item.id)">
<view class="history-main">
<view class="history-title">{{ item.name || '未命名拜访' }}</view>
<view class="history-meta">{{ item.createdAt || '暂无时间' }}</view>
</view>
<view class="history-side">
<view class="history-status" :class="item.statusClass">{{ 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-line">拜访类型:{{ item.visitType || '-' }}</view>
<view class="history-line">拜访日期:{{ item.visitDate || '-' }}</view>
<view class="history-line">定位:{{ item.location || '-' }}</view>
<view class="history-line">纬度:{{ item.lat || '-' }}</view>
<view class="history-line">经度:{{ item.lng || '-' }}</view>
<view class="history-line">连锁:{{ item.chain || '-' }}</view>
<view class="history-line">沟通内容:{{ item.content || '-' }}</view>
<view class="history-line">活动小结:{{ item.summary || '-' }}</view>
<view class="history-line">照片:</view>
<view v-if="item.photos.length" class="history-photo-list">
<view class="history-photo-item" v-for="(photo, photoIndex) in item.photos" :key="photo">
<image
class="history-photo-image"
:src="photo"
mode="aspectFill"
@tap="previewHistoryPhoto(item.photos, photoIndex)"
/>
</view>
</view>
<view v-else class="history-line">-</view>
</view>
</view>
</view>
</view>
<u-picker
:show="showChainPicker"
:columns="[chainOptions]"
:loading="chainLoading"
:defaultIndex="chainPickerIndex"
keyName="text"
@confirm="onChainConfirm"
@cancel="closeChainPicker"
@close="closeChainPicker"
/>
<u-datetime-picker
:show="showVisitDatePicker"
:value="visitDatePickerValue"
mode="date"
@confirm="onVisitDateConfirm"
@cancel="showVisitDatePicker = false"
@close="showVisitDatePicker = false"
/>
</view>
</template>
<script>
import dayjs from "dayjs";
import AppNavbar from "@/components/AppNavbar.vue";
import {chooseImg, getLocationOnly} from "@/util";
import {visitAddReq, visitListReq} from "@/api/visit";
import {merchantGroupByChainReq} from "@/api/shop";
import {userInfoReq} from "@/api/user";
export default {
components: {AppNavbar},
data() {
return {
brandColor: '#2fc6a7',
contentMaxLength: 500,
visitTypeTabs: [
{label: '地点', value: '地点', icon: 'map'},
{label: '门店经理', value: '门店经理', icon: 'account'},
{label: '其他', value: '其他', icon: 'more-dot-fill'}
],
visitForm: this.buildForm(),
photos: [],
visitHistory: [],
chainOptions: [],
chainLoading: false,
showChainPicker: false,
showVisitDatePicker: false,
loading: false,
locating: false,
}
},
computed: {
chainPickerIndex() {
if (!this.chainOptions.length) return [0]
const idx = this.chainOptions.findIndex(item => String(item.id) === String(this.visitForm.chainId))
return [idx < 0 ? 0 : idx]
},
visitDatePickerValue() {
return this.visitForm.visitDate ? dayjs(this.visitForm.visitDate).valueOf() : Date.now()
},
targetPlaceholder() {
if (this.visitForm.visitType === '门店经理') return '请输入门店经理姓名'
if (this.visitForm.visitType === '其他') return '请输入拜访人员姓名'
return '请输入拜访人员姓名 / 地点名称 / 经理姓名'
}
},
async onShow() {
await Promise.all([
this.fetchLocation(true),
this.fetchHistory(),
this.fetchChainOptions(),
this.fetchSubmitterName()
])
},
methods: {
buildForm() {
return {
lat: '',
lng: '',
location: '',
chainId: '',
chain: '',
name: '',
visitType: '地点',
visitDate: dayjs().format('YYYY-MM-DD'),
submitterName: '',
content: '',
summary: ''
}
},
async fetchSubmitterName() {
if (this.visitForm.submitterName) return
try {
const {data} = await userInfoReq()
const info = data || {}
const name = info.realName || info.real_name || info.nickname || info.name || ''
if (name && !this.visitForm.submitterName) {
this.visitForm.submitterName = name
}
} catch (err) {
}
},
async fetchChainOptions() {
this.chainLoading = true
try {
const {data} = await merchantGroupByChainReq()
const list = this.unwrapList(data)
this.chainOptions = list.map((item, index) => this.normalizeChainOption(item, index))
.filter(item => item.id !== undefined && item.id !== null && String(item.id).trim() !== '')
if (this.visitForm.chainId && !this.chainOptions.find(item => String(item.id) === String(this.visitForm.chainId))) {
this.visitForm.chainId = ''
this.visitForm.chain = ''
}
} catch (err) {
this.chainOptions = []
} finally {
this.chainLoading = false
}
},
openChainPicker() {
this.showChainPicker = true
},
closeChainPicker() {
this.showChainPicker = false
},
onChainConfirm(event) {
const selected = event && event.value && event.value.length ? event.value[0] : null
this.visitForm.chainId = selected ? selected.id : ''
this.visitForm.chain = selected ? selected.text : ''
this.showChainPicker = false
},
onVisitDateConfirm(event) {
const raw = event && event.value !== undefined ? event.value : Date.now()
this.visitForm.visitDate = dayjs(raw).format('YYYY-MM-DD')
this.showVisitDatePicker = false
},
formatCoordinate(label, value) {
if (value === '' || value === null || value === undefined) return `${label}:-`
return `${label}:${value}`
},
async fetchLocation(silent = false, force = false) {
if (this.locating) return
if (!force && this.visitForm.location && this.visitForm.lat && this.visitForm.lng) return
this.locating = true
if (!silent) {
uni.showLoading({
title: '定位中'
})
}
try {
const res = await getLocationOnly()
this.visitForm.lat = res.latitude
this.visitForm.lng = res.longitude
this.visitForm.location = res.full || [res.province_str, res.city_str, res.area_str].filter(Boolean).join(' ')
} catch (err) {
if (!silent) {
uni.showToast({
title: '定位失败',
icon: 'none'
})
}
} finally {
if (!silent) uni.hideLoading()
this.locating = false
}
},
async uploadPhotos() {
const remain = 9 - this.photos.length
if (remain <= 0) {
uni.showToast({
title: '最多上传9张',
icon: 'none'
})
return
}
const res = await chooseImg(remain, ['camera'])
this.photos = [
...this.photos,
...res.map(item => item.path).filter(Boolean)
]
},
removePhoto(index) {
this.photos.splice(index, 1)
},
previewPhoto(index) {
if (!this.photos.length) return
uni.previewImage({
urls: this.photos,
current: this.photos[index] || this.photos[0]
})
},
async submitVisit() {
if (!String(this.visitForm.submitterName || '').trim()) {
uni.showToast({
title: '请填写您的姓名',
icon: 'none'
})
return
}
if (!this.visitForm.visitType) {
uni.showToast({
title: '请选择拜访对象类型',
icon: 'none'
})
return
}
if (!String(this.visitForm.name || '').trim()) {
uni.showToast({
title: '请填写拜访对象姓名/地点',
icon: 'none'
})
return
}
if (!this.visitForm.chainId || !this.visitForm.chain) {
uni.showToast({
title: '请选择连锁',
icon: 'none'
})
return
}
if (!this.visitForm.lat || !this.visitForm.lng || !this.visitForm.location) {
uni.showToast({
title: '请先获取定位',
icon: 'none'
})
return
}
if (!this.visitForm.visitDate) {
uni.showToast({
title: '请选择拜访日期',
icon: 'none'
})
return
}
if (!this.visitForm.content || !this.photos.length || !this.visitForm.summary) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
try {
await visitAddReq({
lat: this.visitForm.lat,
lng: this.visitForm.lng,
location: this.visitForm.location,
chainId: this.visitForm.chainId,
chain: this.visitForm.chain,
name: this.visitForm.name,
visitType: this.visitForm.visitType,
visitDate: this.visitForm.visitDate,
submitterName: this.visitForm.submitterName,
content: this.visitForm.content,
photos: JSON.stringify(this.photos),
summary: this.visitForm.summary
})
uni.showToast({
title: '提交成功',
icon: 'success'
})
const snapshot = {
lat: this.visitForm.lat,
lng: this.visitForm.lng,
location: this.visitForm.location,
chainId: this.visitForm.chainId,
chain: this.visitForm.chain,
submitterName: this.visitForm.submitterName
}
this.visitForm = {
...this.buildForm(),
...snapshot
}
this.photos = []
await this.fetchHistory()
} catch (err) {
uni.showToast({
title: '提交失败',
icon: 'none'
})
}
},
async fetchHistory() {
this.loading = true
try {
const {data} = await visitListReq()
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
this.visitHistory = list.map(this.normalizeHistory)
} catch (err) {
uni.showToast({
title: '记录加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
normalizeHistory(item) {
const statusValue = Number(item.status)
let status = '待处理'
let statusClass = 'pending'
if (statusValue === 0) {
status = '审核中'
statusClass = 'pending'
} else if (statusValue === 1) {
status = '已通过'
statusClass = 'success'
} else if (statusValue === 2 || statusValue === 3) {
status = '已驳回'
statusClass = 'reject'
} else if (item.statusText || item.status_text) {
status = item.statusText || item.status_text
statusClass = ''
}
return {
id: item.id,
name: item.name || item.visitName || '',
content: item.content || item.remark || '',
summary: item.summary || item.result || '',
visitType: item.visitType || item.visit_type || '',
visitDate: item.visitDate || item.visit_date || '',
location: item.location || item.address || '',
lat: item.lat || item.latitude || '',
lng: item.lng || item.longitude || '',
chainId: item.chainId || item.chain_id || item.chainCode || item.chain_code || '',
chain: item.chain || item.chain_name || item.chain || item.chainAbb || '',
photos: this.parsePhotos(item.photos || item.images || item.image),
createdAt: item.createTime || item.createdAt || item.create_date || '',
status,
statusClass,
expanded: false
}
},
parsePhotos(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.parsePhotos(JSON.parse(text))
} catch (err) {
}
}
if (text.includes(',')) {
return text.split(',').map(item => item.trim()).filter(Boolean)
}
return [text]
},
toggleHistoryDetail(id) {
this.visitHistory = this.visitHistory.map(item => {
if (item.id !== id) return item
return {
...item,
expanded: !item.expanded
}
})
},
previewHistoryPhoto(list = [], index = 0) {
if (!Array.isArray(list) || !list.length) return
uni.previewImage({
urls: list,
current: list[index] || list[0]
})
},
normalizeChainOption(item, index) {
const text = item.chain || item.chain || item.chainAbb || item.name || item.title || item.text || `连锁${index + 1}`
const rawId = item.chainId ?? item.chain_id ?? item.id ?? item.chainCode ?? item.chain_code ?? ''
const id = rawId === undefined || rawId === null || String(rawId).trim() === ''
? `chain_${index}`
: rawId
return {
id,
text
}
},
unwrapList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result', 'chainList']
for (let i = 0; i < keys.length; i++) {
const value = data[keys[i]]
if (Array.isArray(value)) return value
if (value && typeof value === 'object') {
const nested = this.unwrapList(value)
if (nested.length) return nested
}
}
return [data]
}
}
}
</script>
<style lang="scss" scoped>
.visit-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 40rpx;
}
.page-hero {
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat top center / 100% auto;
}
.intro-card {
margin: 18rpx 20rpx 0;
padding: 22rpx 20rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
display: flex;
align-items: flex-start;
gap: 16rpx;
}
.tip-icon {
flex-shrink: 0;
margin-top: 2rpx;
}
.tip-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.tip-title {
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.tip-text {
font-size: 25rpx;
color: #6b7280;
line-height: 1.6;
}
.block {
margin: 18rpx 20rpx 0;
}
.form-card-head {
display: flex;
align-items: center;
gap: 14rpx;
padding-bottom: 20rpx;
margin-bottom: 6rpx;
border-bottom: 1rpx solid #eef2f7;
}
.head-icon {
width: 56rpx;
height: 56rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(47, 198, 167, 0.12);
}
.head-title {
font-size: 30rpx;
font-weight: 700;
color: var(--text-main);
}
.field-row {
display: flex;
align-items: center;
gap: 20rpx;
padding: 14rpx 0;
}
.field-row.textarea-row {
align-items: flex-start;
}
.field-label {
flex-shrink: 0;
width: 158rpx;
font-size: 28rpx;
color: var(--text-main);
}
/* 注意:不要用 .required 作类名 —— 全局 common.scss 里的 .required
(经 uni.scss 全局引入)@extend 了 .pos-a,会带上 position:absolute /
left:-9px / top:15px,导致标签脱离文档流堆到卡片左上角。 */
.field-label.is-required {
display: flex;
align-items: center;
}
.field-star {
color: #ff3b30;
margin-right: 6rpx;
font-size: 26rpx;
}
.field-box {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12rpx;
min-height: 80rpx;
padding: 0 20rpx;
border-radius: 16rpx;
background: #f5f7fa;
}
.textarea-box {
flex-direction: column;
align-items: stretch;
padding: 16rpx 20rpx;
}
.field-input {
flex: 1;
min-width: 0;
height: 80rpx;
font-size: 28rpx;
color: var(--text-main);
}
.field-textarea {
width: 100%;
min-height: 180rpx;
font-size: 28rpx;
line-height: 1.6;
color: var(--text-main);
}
.field-text {
flex: 1;
min-width: 0;
font-size: 28rpx;
color: var(--text-main);
line-height: 1.5;
word-break: break-all;
}
.field-text.placeholder,
.field-placeholder {
color: #9aa7b8;
}
.counter {
align-self: flex-end;
margin-top: 6rpx;
font-size: 24rpx;
color: #9aa7b8;
}
.field-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4rpx;
margin: -6rpx 0 6rpx 178rpx;
font-size: 22rpx;
color: var(--text-sub);
}
.type-tabs {
flex: 1;
display: flex;
gap: 12rpx;
}
.type-tab {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6rpx;
height: 80rpx;
border-radius: 16rpx;
background: #f5f7fa;
color: var(--text-sub);
font-size: 25rpx;
}
.type-tab.active {
background: rgba(47, 198, 167, 0.14);
color: var(--brand-primary);
font-weight: 700;
}
.photo-list {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin: 6rpx 0 6rpx 178rpx;
}
.photo-item {
position: relative;
width: 150rpx;
height: 150rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f8fafc;
}
.photo-image {
width: 100%;
height: 100%;
display: block;
}
.photo-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-wrap {
margin: 24rpx 20rpx 0;
}
.submit {
gap: 10rpx;
padding: 24rpx 32rpx;
font-size: 32rpx;
font-weight: 700;
}
.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-title {
font-weight: 700;
color: #1f2937;
}
.history-meta {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 6rpx;
}
.history-side {
display: flex;
align-items: center;
gap: 8rpx;
}
.history-status {
font-weight: 700;
color: #ff8354;
}
.history-status.success {
color: #22c55e;
}
.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: 10rpx;
padding: 12rpx;
border-radius: 12rpx;
background: #f8fafc;
}
.history-line {
margin-top: 6rpx;
font-size: 24rpx;
color: #4b5563;
line-height: 1.6;
word-break: break-all;
}
.history-line:first-child {
margin-top: 0;
}
.history-photo-list {
margin-top: 10rpx;
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.history-photo-item {
width: 140rpx;
height: 140rpx;
border-radius: 10rpx;
overflow: hidden;
background: #ffffff;
}
.history-photo-image {
width: 100%;
height: 100%;
display: block;
}
.empty {
color: var(--text-sub);
text-align: center;
padding: 16rpx 0;
}
</style>