feat: 报告页上传进度与重试,预约页宠物自动匹配优化

This commit is contained in:
MaDaLei 2026-04-17 08:09:03 +08:00
parent 9ae9cdb08e
commit 92f1077025
2 changed files with 378 additions and 96 deletions

View File

@ -29,22 +29,25 @@
</picker> </picker>
<text v-if="storeHintLine" class="c-store-hint">{{ storeHintLine }}</text> <text v-if="storeHintLine" class="c-store-hint">{{ storeHintLine }}</text>
</view> </view>
<view v-if="myPets.length" class="c-field">
<text class="app-form-label">使用档案可选</text>
<picker mode="selector" :range="petPickerOptions" range-key="label" @change="onPickMyPet">
<view class="app-form-input app-form-picker">
<text :class="{ 'app-form-placeholder': selectedPetIndex === 0 }">{{ petPickerLabel }}</text>
<text class="app-form-arrow"></text>
</view>
</picker>
</view>
<view class="c-field"> <view class="c-field">
<text class="app-form-label">宠物名字</text> <text class="app-form-label">宠物名字</text>
<input v-model="newAppt.petName" class="app-form-input" placeholder="请输入宠物名字" /> <view class="pet-name-row">
<input
v-model="newAppt.petName"
class="app-form-input pet-name-input"
placeholder="请输入宠物名字"
@input="onPetNameInput"
/>
<button
v-if="myPets.length > 1"
class="app-btn-sm app-btn-soft pet-switch-btn"
@click="openPetSwitcher"
>切换宠物</button>
</view>
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="app-form-label">宠物类型</text> <text class="app-form-label">宠物类型</text>
<picker mode="selector" :range="petTypes" range-key="label" @change="e => newAppt.petType = petTypes[e.detail.value].value"> <picker mode="selector" :range="petTypes" range-key="label" @change="onPetTypeChange">
<view class="app-form-input app-form-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'app-form-placeholder': !newAppt.petType }">{{ newAppt.petType || '请选择' }}</text> <text :class="{ 'app-form-placeholder': !newAppt.petType }">{{ newAppt.petType || '请选择' }}</text>
<text class="app-form-arrow"></text> <text class="app-form-arrow"></text>
@ -92,7 +95,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { createAppointment, getServiceTypeList, getPetList, getStoreList } from '../../api/index.js' import { createAppointment, getServiceTypeList, getPetList, getStoreList, createPet } from '../../api/index.js'
import AppIcon from '../../components/AppIcon.vue' import AppIcon from '../../components/AppIcon.vue'
import { getUserSession, getStoreSession, setStoreSession } from '../../utils/session.js' import { getUserSession, getStoreSession, setStoreSession } from '../../utils/session.js'
import { haversineKm } from '../../utils/geo.js' import { haversineKm } from '../../utils/geo.js'
@ -105,6 +108,7 @@ import {
const userInfo = getUserSession() const userInfo = getUserSession()
const storeInfo = getStoreSession() const storeInfo = getStoreSession()
const LAST_SUBMITTED_PET_KEY = `petstore_last_submitted_pet_${userInfo.id || 'guest'}`
const BOOKING_STORE_ID_KEY = 'petstore_booking_store_id' const BOOKING_STORE_ID_KEY = 'petstore_booking_store_id'
@ -223,24 +227,56 @@ const onStorePickerChange = (e) => {
loadServiceTypesForSelected() loadServiceTypesForSelected()
} }
const myPets = ref([]) const myPets = ref([])
const selectedPetIndex = ref(0) const selectedPetId = ref(null)
const newAppt = ref({ petName: '', petType: '', serviceType: '', appointmentTime: '', remark: '' }) const newAppt = ref({ petName: '', petType: '', serviceType: '', appointmentTime: '', remark: '' })
const creatingAppt = ref(false) const creatingAppt = ref(false)
const petPickerOptions = computed(() => { const normalizeText = (s) => (s || '').toString().trim().toLowerCase()
const head = [{ label: '手动填写,不关联档案', pet: null }]
return head.concat(myPets.value.map((p) => ({ label: `${p.name}${p.petType}`, pet: p })))
})
const petPickerLabel = computed(() => petPickerOptions.value[selectedPetIndex.value]?.label || '请选择') const findMatchedPet = (name, petType) =>
myPets.value.find(
(p) => normalizeText(p.name) === normalizeText(name) && normalizeText(p.petType) === normalizeText(petType)
)
const onPickMyPet = (e) => { const applyPetToForm = (pet) => {
if (!pet) return
selectedPetId.value = pet.id || null
newAppt.value.petName = pet.name || ''
newAppt.value.petType = pet.petType || ''
}
const openPetSwitcher = () => {
if (myPets.value.length <= 1) return
uni.showActionSheet({
itemList: myPets.value.map((p) => `${p.name}${p.petType}`),
success: (res) => {
const pet = myPets.value[res.tapIndex]
if (pet) applyPetToForm(pet)
}
})
}
const onPetNameInput = (e) => {
const currentName = e?.detail?.value ?? newAppt.value.petName
if (!selectedPetId.value) return
const cur = myPets.value.find((p) => p.id === selectedPetId.value)
if (!cur) {
selectedPetId.value = null
return
}
if (normalizeText(currentName) !== normalizeText(cur.name)) {
selectedPetId.value = null
}
}
const onPetTypeChange = (e) => {
const i = Number(e.detail.value) const i = Number(e.detail.value)
selectedPetIndex.value = i const nextType = petTypes[i]?.value || ''
const opt = petPickerOptions.value[i] newAppt.value.petType = nextType
if (opt?.pet) { if (!selectedPetId.value) return
newAppt.value.petName = opt.pet.name const cur = myPets.value.find((p) => p.id === selectedPetId.value)
newAppt.value.petType = opt.pet.petType if (!cur || normalizeText(nextType) !== normalizeText(cur.petType)) {
selectedPetId.value = null
} }
} }
@ -284,6 +320,56 @@ const loadServiceTypesForSelected = async () => {
} }
} }
const hydrateDefaultPet = () => {
if (!myPets.value.length) return
let picked = null
try {
const saved = JSON.parse(uni.getStorageSync(LAST_SUBMITTED_PET_KEY) || 'null')
if (saved) {
if (saved.petId) picked = myPets.value.find((p) => String(p.id) === String(saved.petId)) || null
if (!picked && saved.name && saved.petType) picked = findMatchedPet(saved.name, saved.petType) || null
}
} catch (_) {}
if (!picked) picked = myPets.value[0]
applyPetToForm(picked)
}
const rememberLastSubmittedPet = (pet) => {
if (!pet) return
uni.setStorageSync(
LAST_SUBMITTED_PET_KEY,
JSON.stringify({ petId: pet.id || null, name: pet.name || '', petType: pet.petType || '' })
)
}
const ensurePetProfile = async (name, petType) => {
const current = myPets.value.find((p) => String(p.id) === String(selectedPetId.value))
if (
current &&
normalizeText(current.name) === normalizeText(name) &&
normalizeText(current.petType) === normalizeText(petType)
) {
return current
}
const matched = findMatchedPet(name, petType)
if (matched) {
selectedPetId.value = matched.id || null
return matched
}
const res = await createPet({
ownerUserId: userInfo.id,
name: name.trim(),
petType: petType
})
if (res.code !== 200 || !res.data) {
return null
}
const created = res.data
myPets.value.unshift(created)
selectedPetId.value = created.id || null
return created
}
const confirmNewAppt = async () => { const confirmNewAppt = async () => {
const a = newAppt.value const a = newAppt.value
if (!a.petName) { uni.showToast({ title: '请输入宠物名字', icon: 'none' }); return } if (!a.petName) { uni.showToast({ title: '请输入宠物名字', icon: 'none' }); return }
@ -297,12 +383,18 @@ const confirmNewAppt = async () => {
return return
} }
creatingAppt.value = true creatingAppt.value = true
const ensuredPet = await ensurePetProfile(a.petName, a.petType)
if (!ensuredPet?.id) {
creatingAppt.value = false
uni.showToast({ title: '宠物档案保存失败,请重试', icon: 'none' })
return
}
const payload = { ...a, storeId, userId: userInfo.id } const payload = { ...a, storeId, userId: userInfo.id }
const opt = petPickerOptions.value[selectedPetIndex.value] payload.petId = ensuredPet.id
if (opt?.pet?.id) payload.petId = opt.pet.id
const res = await createAppointment(payload) const res = await createAppointment(payload)
creatingAppt.value = false creatingAppt.value = false
if (res.code === 200) { if (res.code === 200) {
rememberLastSubmittedPet(ensuredPet)
uni.showToast({ title: '预约成功', icon: 'success' }) uni.showToast({ title: '预约成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 600) setTimeout(() => uni.navigateBack(), 600)
} else { } else {
@ -318,7 +410,10 @@ const goBack = () => {
const loadMyPets = async () => { const loadMyPets = async () => {
const res = await getPetList({ ownerUserId: userInfo.id }) const res = await getPetList({ ownerUserId: userInfo.id })
if (res.code === 200) myPets.value = res.data || [] if (res.code === 200) {
myPets.value = res.data || []
hydrateDefaultPet()
}
} }
onMounted(async () => { onMounted(async () => {
@ -372,6 +467,21 @@ onMounted(async () => {
.c-field { margin-bottom: 16px; } .c-field { margin-bottom: 16px; }
.c-time-row { display: flex; gap: 10px; } .c-time-row { display: flex; gap: 10px; }
.c-time-half { flex: 1; } .c-time-half { flex: 1; }
.pet-name-row {
display: flex;
align-items: center;
gap: 8px;
}
.pet-name-input {
flex: 1;
min-width: 0;
}
.pet-switch-btn {
width: 68px !important;
padding: 0 !important;
color: #2db96d !important;
flex-shrink: 0;
}
.c-store-hint { .c-store-hint {
display: block; display: block;
margin-top: 6px; margin-top: 6px;

View File

@ -67,6 +67,12 @@
<view class="photo-grid"> <view class="photo-grid">
<view v-for="(item, idx) in report.beforeMedia" :key="'b'+idx" class="photo-thumb"> <view v-for="(item, idx) in report.beforeMedia" :key="'b'+idx" class="photo-thumb">
<image :src="item.url" class="thumb-img" mode="aspectFill" /> <image :src="item.url" class="thumb-img" mode="aspectFill" />
<view v-if="item.uploading" class="media-mask">
<text class="media-mask-text">上传中</text>
</view>
<view v-else-if="item.failed" class="media-mask media-mask--error" @click="retryUpload('beforeMedia', idx)">
<text class="media-mask-text">上传失败点此重试</text>
</view>
<view class="thumb-del" @click="removeMedia('beforeMedia', idx)"></view> <view class="thumb-del" @click="removeMedia('beforeMedia', idx)"></view>
</view> </view>
<view v-if="report.beforeMedia.length < 4" class="photo-add" @click="uploadPhoto('beforeMedia')"> <view v-if="report.beforeMedia.length < 4" class="photo-add" @click="uploadPhoto('beforeMedia')">
@ -85,6 +91,12 @@
<view class="photo-grid"> <view class="photo-grid">
<view v-for="(item, idx) in report.afterMedia" :key="'a'+idx" class="photo-thumb"> <view v-for="(item, idx) in report.afterMedia" :key="'a'+idx" class="photo-thumb">
<image :src="item.url" class="thumb-img" mode="aspectFill" /> <image :src="item.url" class="thumb-img" mode="aspectFill" />
<view v-if="item.uploading" class="media-mask">
<text class="media-mask-text">上传中</text>
</view>
<view v-else-if="item.failed" class="media-mask media-mask--error" @click="retryUpload('afterMedia', idx)">
<text class="media-mask-text">上传失败点此重试</text>
</view>
<view class="thumb-del" @click="removeMedia('afterMedia', idx)"></view> <view class="thumb-del" @click="removeMedia('afterMedia', idx)"></view>
</view> </view>
<view v-if="report.afterMedia.length < 4" class="photo-add" @click="uploadPhoto('afterMedia')"> <view v-if="report.afterMedia.length < 4" class="photo-add" @click="uploadPhoto('afterMedia')">
@ -123,6 +135,12 @@
<view v-if="item.mediaType === 'video'" class="video-badge"> <view v-if="item.mediaType === 'video'" class="video-badge">
<text>视频</text> <text>视频</text>
</view> </view>
<view v-if="item.uploading" class="media-mask">
<text class="media-mask-text">上传中</text>
</view>
<view v-else-if="item.failed" class="media-mask media-mask--error" @click="retryUpload('duringMedia', idx)">
<text class="media-mask-text">上传失败点此重试</text>
</view>
<view class="thumb-del" @click="removeMedia('duringMedia', idx)"></view> <view class="thumb-del" @click="removeMedia('duringMedia', idx)"></view>
</view> </view>
</view> </view>
@ -219,6 +237,15 @@ const submitFixedStyle = computed(() => {
return `bottom:${bottom};` return `bottom:${bottom};`
}) })
const allMediaItems = computed(() => ([
...report.value.beforeMedia,
...report.value.afterMedia,
...report.value.duringMedia
]))
const hasUploadingMedia = computed(() => allMediaItems.value.some((i) => !!i.uploading))
const hasFailedMedia = computed(() => allMediaItems.value.some((i) => !!i.failed))
const serviceColumns = computed(() => const serviceColumns = computed(() =>
serviceTypes.value.map(s => ({ label: s.name, value: s.name })) serviceTypes.value.map(s => ({ label: s.name, value: s.name }))
) )
@ -232,6 +259,90 @@ const loadServiceTypes = async () => {
/** /**
* 上传照片用于服务前/ * 上传照片用于服务前/
*/ */
const friendlyUploadError = (rawMsg) => {
const msg = (rawMsg || '').toString()
if (!msg) return '上传失败'
if (msg.includes('request:fail')) return '网络异常,请重试'
if (msg.includes('timeout')) return '上传超时,请重试'
if (msg.includes('只能上传图片或视频')) return '格式不支持'
if (msg.includes('413') || msg.includes('Too Large')) return '文件过大'
return msg
}
const buildPendingItem = (localPath, mediaType) => ({
url: localPath,
localPath,
mediaType,
uploading: true,
failed: false,
error: ''
})
const setUploadSuccess = (item, uploadedUrl) => {
item.url = imgUrl(uploadedUrl)
item.uploading = false
item.failed = false
item.error = ''
}
const setUploadFail = (item, reason) => {
item.uploading = false
item.failed = true
item.error = reason || '上传失败'
}
const uploadSingleFile = (filePath) =>
new Promise((resolve) => {
uni.uploadFile({
url: `${BASE_URL}/upload/image`,
filePath,
name: 'file',
success: (uploadRes) => {
try {
const data = JSON.parse(uploadRes.data || '{}')
if (data.code === 200 && data.data?.url) {
resolve({ ok: true, url: data.data.url })
} else {
resolve({ ok: false, message: friendlyUploadError(data.message) })
}
} catch (_) {
resolve({ ok: false, message: '返回解析失败' })
}
},
fail: (err) => resolve({ ok: false, message: friendlyUploadError(err?.errMsg) })
})
})
const showBatchUploadFail = (count, firstReason) => {
uni.showToast({
title: `${count}个上传失败:${firstReason || '请重试'}`.slice(0, 20),
icon: 'none'
})
}
const retryUpload = async (field, idx) => {
const item = report.value[field]?.[idx]
if (!item || item.uploading) return
const filePath = item.localPath || item.url
if (!filePath) {
uni.showToast({ title: '缺少本地文件,无法重试', icon: 'none' })
return
}
item.uploading = true
item.failed = false
item.error = ''
uni.showLoading({ title: '重试上传...' })
const r = await uploadSingleFile(filePath)
uni.hideLoading()
if (r.ok) {
setUploadSuccess(item, r.url)
uni.showToast({ title: '重试成功', icon: 'success' })
} else {
setUploadFail(item, r.message)
uni.showToast({ title: `重试失败:${r.message}`.slice(0, 20), icon: 'none' })
}
}
const uploadPhoto = (field) => { const uploadPhoto = (field) => {
const fieldMax = 4 const fieldMax = 4
const maxTotal = 12 const maxTotal = 12
@ -241,34 +352,32 @@ const uploadPhoto = (field) => {
const count = Math.min(fieldMax - report.value[field].length, maxTotal - report.value[field].length, 4) const count = Math.min(fieldMax - report.value[field].length, maxTotal - report.value[field].length, 4)
uni.chooseImage({ uni.chooseImage({
count, count,
success: (res) => { success: async (res) => {
const tempPaths = res.tempFilePaths const tempPaths = res.tempFilePaths
if (!tempPaths?.length) return
uni.showLoading({ title: '上传中...' }) uni.showLoading({ title: '上传中...' })
let failCount = 0 let failCount = 0
const tasks = tempPaths.map(tempPath => { let firstReason = ''
return new Promise((resolve) => { for (const tempPath of tempPaths) {
uni.uploadFile({ const item = buildPendingItem(tempPath, 'photo')
url: `${BASE_URL}/upload/image`, report.value[field].push(item)
filePath: tempPath, const r = await uploadSingleFile(tempPath)
name: 'file', if (r.ok) {
success: (uploadRes) => { setUploadSuccess(item, r.url)
try { } else {
const data = JSON.parse(uploadRes.data) failCount++
if (data.code === 200) { if (!firstReason) firstReason = r.message
report.value[field].push({ url: imgUrl(data.data.url), mediaType: 'photo' }) setUploadFail(item, r.message)
} else { failCount++ }
} catch (e) { failCount++ }
resolve()
},
fail: () => { failCount++; resolve() }
})
})
})
Promise.all(tasks).then(() => {
uni.hideLoading()
if (failCount > 0) uni.showToast({ title: `${failCount}张上传失败`, icon: 'none' })
})
} }
}
uni.hideLoading()
if (failCount > 0) showBatchUploadFail(failCount, firstReason)
},
fail: (err) => {
const msg = err?.errMsg || ''
if (msg.includes('cancel')) return
uni.showToast({ title: friendlyUploadError(msg), icon: 'none' })
},
}) })
} }
@ -283,61 +392,101 @@ const uploadVideo = (field) => {
uni.chooseMedia({ uni.chooseMedia({
count: 1, count: 1,
mediaType: ['video'], mediaType: ['video'],
success: (res) => { success: async (res) => {
const tempFiles = res.tempFiles const tempFiles = res.tempFiles
if (!tempFiles?.length) return
uni.showLoading({ title: '上传中...' }) uni.showLoading({ title: '上传中...' })
let failCount = 0 let failCount = 0
const tasks = tempFiles.map(f => { let firstReason = ''
return new Promise((resolve) => { for (const f of tempFiles) {
const item = buildPendingItem(f.tempFilePath, 'video')
report.value[field].push(item)
const sizeMB = (f.size || 0) / 1024 / 1024 const sizeMB = (f.size || 0) / 1024 / 1024
if (sizeMB > 100) { if (sizeMB > 100) {
failCount++ failCount++
uni.showToast({ title: '视频不能超过100MB', icon: 'none' }) if (!firstReason) firstReason = '视频不能超过100MB'
resolve() setUploadFail(item, '视频不能超过100MB')
return continue
}
const r = await uploadSingleFile(f.tempFilePath)
if (r.ok) {
setUploadSuccess(item, r.url)
} else {
failCount++
if (!firstReason) firstReason = r.message
setUploadFail(item, r.message)
}
} }
uni.uploadFile({
url: `${BASE_URL}/upload/image`,
filePath: f.tempFilePath,
name: 'file',
success: (uploadRes) => {
try {
const data = JSON.parse(uploadRes.data)
if (data.code === 200) {
report.value[field].push({ url: imgUrl(data.data.url), mediaType: 'video' })
} else { failCount++ }
} catch (e) { failCount++ }
resolve()
},
fail: () => { failCount++; resolve() }
})
})
})
Promise.all(tasks).then(() => {
uni.hideLoading() uni.hideLoading()
if (failCount > 0) uni.showToast({ title: `${failCount}个上传失败`, icon: 'none' }) if (failCount > 0) showBatchUploadFail(failCount, firstReason)
}) },
} fail: (err) => {
const msg = err?.errMsg || ''
if (msg.includes('cancel')) return
uni.showToast({ title: friendlyUploadError(msg), icon: 'none' })
},
}) })
} }
/** /**
* 上传过程中媒体照片或视频弹窗选择 * 上传过程中媒体一次选择支持照片和视频混选
*/ */
const uploadDuring = () => { const uploadDuring = () => {
const maxTotal = 12 const maxTotal = 12
if (report.value.duringMedia.length >= maxTotal) { if (report.value.duringMedia.length >= maxTotal) {
return uni.showToast({ title: `最多上传${maxTotal}`, icon: 'none' }) return uni.showToast({ title: `最多上传${maxTotal}`, icon: 'none' })
} }
uni.showActionSheet({ const remain = maxTotal - report.value.duringMedia.length
itemList: ['照片', '视频'], uni.chooseMedia({
success: (res) => { count: Math.min(remain, 9),
if (res.tapIndex === 0) { mediaType: ['image', 'video'],
uploadPhoto('duringMedia') success: async (res) => {
const tempFiles = res.tempFiles || []
if (!tempFiles.length) return
uni.showLoading({ title: '上传中...' })
let failCount = 0
let firstReason = ''
for (const f of tempFiles) {
const path = f.tempFilePath || ''
const lowerPath = path.toLowerCase()
const isVideo =
f.fileType === 'video' ||
lowerPath.endsWith('.mp4') ||
lowerPath.endsWith('.mov') ||
lowerPath.endsWith('.m4v') ||
lowerPath.endsWith('.webm') ||
lowerPath.endsWith('.avi')
const mediaType = isVideo ? 'video' : 'photo'
const item = buildPendingItem(path, mediaType)
report.value.duringMedia.push(item)
if (isVideo) {
const sizeMB = (f.size || 0) / 1024 / 1024
if (sizeMB > 100) {
failCount++
if (!firstReason) firstReason = '视频不能超过100MB'
setUploadFail(item, '视频不能超过100MB')
continue
}
}
const r = await uploadSingleFile(path)
if (r.ok) {
setUploadSuccess(item, r.url)
} else { } else {
uploadVideo('duringMedia') failCount++
if (!firstReason) firstReason = r.message
setUploadFail(item, r.message)
} }
} }
uni.hideLoading()
if (failCount > 0) showBatchUploadFail(failCount, firstReason)
},
fail: (err) => {
const msg = err?.errMsg || ''
if (msg.includes('cancel')) return
uni.showToast({ title: friendlyUploadError(msg), icon: 'none' })
},
}) })
} }
@ -379,6 +528,8 @@ const onServiceTimeChange = (e) => {
} }
const submitReport = async () => { const submitReport = async () => {
if (hasUploadingMedia.value) return uni.showToast({ title: '还有素材上传中,请稍候', icon: 'none' })
if (hasFailedMedia.value) return uni.showToast({ title: '有上传失败项,请重试或删除', icon: 'none' })
if (!report.value.petName) return uni.showToast({ title: '请输入宠物名字', icon: 'none' }) if (!report.value.petName) return uni.showToast({ title: '请输入宠物名字', icon: 'none' })
if (!report.value.serviceType) return uni.showToast({ title: '请选择服务类型', icon: 'none' }) if (!report.value.serviceType) return uni.showToast({ title: '请选择服务类型', icon: 'none' })
if (!report.value.appointmentTime) return uni.showToast({ title: '请选择服务时间', icon: 'none' }) if (!report.value.appointmentTime) return uni.showToast({ title: '请选择服务时间', icon: 'none' })
@ -386,13 +537,13 @@ const submitReport = async () => {
// //
const images = [] const images = []
report.value.beforeMedia.forEach((item, i) => { report.value.beforeMedia.filter((item) => !item.uploading && !item.failed).forEach((item, i) => {
images.push({ photoUrl: item.url, photoType: 'before', mediaType: item.mediaType || 'photo', sortOrder: i }) images.push({ photoUrl: item.url, photoType: 'before', mediaType: item.mediaType || 'photo', sortOrder: i })
}) })
report.value.afterMedia.forEach((item, i) => { report.value.afterMedia.filter((item) => !item.uploading && !item.failed).forEach((item, i) => {
images.push({ photoUrl: item.url, photoType: 'after', mediaType: item.mediaType || 'photo', sortOrder: i }) images.push({ photoUrl: item.url, photoType: 'after', mediaType: item.mediaType || 'photo', sortOrder: i })
}) })
report.value.duringMedia.forEach((item, i) => { report.value.duringMedia.filter((item) => !item.uploading && !item.failed).forEach((item, i) => {
images.push({ photoUrl: item.url, photoType: 'during', mediaType: item.mediaType || 'photo', sortOrder: i }) images.push({ photoUrl: item.url, photoType: 'during', mediaType: item.mediaType || 'photo', sortOrder: i })
}) })
@ -615,6 +766,27 @@ const onRemarkBlur = () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.media-mask {
position: absolute;
inset: 0;
z-index: 2;
background: rgba(0, 0, 0, 0.42);
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
box-sizing: border-box;
}
.media-mask--error {
background: rgba(188, 46, 46, 0.72);
}
.media-mask-text {
color: #fff;
font-size: 11px;
font-weight: 600;
text-align: center;
line-height: 1.3;
}
.photo-add { .photo-add {
width: calc(50% - 4px); width: calc(50% - 4px);
aspect-ratio: 1; aspect-ratio: 1;