fix: 修复商品图片管理和缓存一致性问题
- 重构 ImageUpload 组件,使用 rawUrl 跟踪原始路径 - 新增 normalizeStorageImageUrl 避免存储绝对 URL - 商品增删改后同步清除缓存和旧图片文件 - 修复秒杀活动列表商品图片为空的问题
This commit is contained in:
@@ -102,7 +102,10 @@ export const adminApi = {
|
||||
return request.post<ApiResponse<any>>('/api/admin/products', data).then((res) => ({ ...res, data: normalizeAdminProduct(res.data) }))
|
||||
},
|
||||
updateProduct(id: number, data: Record<string, unknown>): Promise<ApiResponse<AdminProductRow>> {
|
||||
return request.put<ApiResponse<any>>(`/api/admin/products/${id}`, data).then((res) => ({ ...res, data: normalizeAdminProduct(res.data) }))
|
||||
return request.put<ApiResponse<any>>(`/api/admin/products/${id}`, data).then((res) => ({
|
||||
...res,
|
||||
data: res.data ? normalizeAdminProduct(res.data) : undefined,
|
||||
}))
|
||||
},
|
||||
deleteProduct(id: number): Promise<ApiResponse> { return request.delete(`/api/admin/products/${id}`) },
|
||||
getReviewStats(): Promise<ApiResponse<AdminReviewStats>> { return request.get('/api/admin/reviews/stats') },
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
:class="{ 'hide-upload': fileList.length >= limit }"
|
||||
:action="uploadUrl"
|
||||
:headers="headers"
|
||||
:with-credentials="true"
|
||||
:file-list="fileList"
|
||||
:limit="limit"
|
||||
:multiple="multiple"
|
||||
@@ -67,7 +68,7 @@ import { ref, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { resolveImageUrl } from '@/utils/image'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import type { UploadFile, UploadFiles, UploadRawFile } from 'element-plus'
|
||||
import type { UploadFile, UploadRawFile } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | string[]
|
||||
@@ -92,6 +93,10 @@ const emit = defineEmits<{
|
||||
change: [value: string | string[]]
|
||||
}>()
|
||||
|
||||
interface UploadFileWithRawUrl extends UploadFile {
|
||||
rawUrl?: string
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 上传相关
|
||||
@@ -100,32 +105,42 @@ const headers = computed(() => ({
|
||||
Authorization: `Bearer ${userStore.token}`
|
||||
}))
|
||||
|
||||
const fileList = ref<UploadFile[]>([])
|
||||
const fileList = ref<UploadFileWithRawUrl[]>([])
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewName = ref('')
|
||||
const previewSize = ref(0)
|
||||
|
||||
const buildFileItem = (rawUrl: string, index: number): UploadFileWithRawUrl => ({
|
||||
name: props.multiple ? `image-${index}` : 'image',
|
||||
url: resolveImageUrl(rawUrl),
|
||||
status: 'success',
|
||||
uid: `${Date.now()}-${index}`,
|
||||
rawUrl,
|
||||
})
|
||||
|
||||
const getRawUrl = (file: UploadFile) => {
|
||||
const currentFile = file as UploadFileWithRawUrl
|
||||
return currentFile.rawUrl?.trim() || file.url?.trim() || ''
|
||||
}
|
||||
|
||||
// 初始化文件列表
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (Array.isArray(val)) {
|
||||
fileList.value = val.map((url, index) => ({
|
||||
name: `image-${index}`,
|
||||
url: resolveImageUrl(url),
|
||||
status: 'success',
|
||||
uid: Date.now() + index
|
||||
} as UploadFile))
|
||||
} else {
|
||||
fileList.value = [{
|
||||
name: 'image',
|
||||
url: resolveImageUrl(val),
|
||||
status: 'success',
|
||||
uid: Date.now()
|
||||
} as UploadFile]
|
||||
}
|
||||
const nextFiles = Array.isArray(val)
|
||||
? val
|
||||
.map((url) => String(url || '').trim())
|
||||
.filter(Boolean)
|
||||
.map((url, index) => buildFileItem(url, index))
|
||||
: val && String(val).trim()
|
||||
? [buildFileItem(String(val).trim(), 0)]
|
||||
: []
|
||||
|
||||
const currentRawUrls = fileList.value.map((file) => getRawUrl(file))
|
||||
const nextRawUrls = nextFiles.map((file) => getRawUrl(file))
|
||||
if (JSON.stringify(currentRawUrls) !== JSON.stringify(nextRawUrls)) {
|
||||
fileList.value = nextFiles
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -185,9 +200,33 @@ const beforeUpload = (rawFile: UploadRawFile) => {
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
const handleSuccess = (response: any, file: UploadFile, files: UploadFiles) => {
|
||||
const handleSuccess = (response: any, file: UploadFile) => {
|
||||
if (response.success) {
|
||||
file.url = resolveImageUrl(response.data?.url || response.imageUrl || response.data?.imageUrl)
|
||||
const rawImageUrl = String(response.imageUrl || response.data?.imageUrl || response.data?.url || '').trim()
|
||||
if (!rawImageUrl) {
|
||||
handleRemove(file)
|
||||
ElMessage.error('上传成功但未返回图片地址')
|
||||
return
|
||||
}
|
||||
|
||||
const uploadedFile = file as UploadFileWithRawUrl
|
||||
uploadedFile.status = 'success'
|
||||
uploadedFile.url = resolveImageUrl(rawImageUrl)
|
||||
uploadedFile.rawUrl = rawImageUrl
|
||||
|
||||
if (props.limit === 1 && !props.multiple) {
|
||||
fileList.value = [uploadedFile]
|
||||
} else {
|
||||
const nextFiles = [...fileList.value]
|
||||
const index = nextFiles.findIndex((item) => item.uid === uploadedFile.uid)
|
||||
if (index > -1) {
|
||||
nextFiles[index] = uploadedFile
|
||||
} else {
|
||||
nextFiles.push(uploadedFile)
|
||||
}
|
||||
fileList.value = nextFiles
|
||||
}
|
||||
|
||||
updateValue()
|
||||
ElMessage.success('上传成功')
|
||||
} else {
|
||||
@@ -197,13 +236,13 @@ const handleSuccess = (response: any, file: UploadFile, files: UploadFiles) => {
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
const handleError = (error: Error, file: UploadFile) => {
|
||||
const handleError = (error: Error) => {
|
||||
ElMessage.error('上传失败,请重试')
|
||||
console.error('Upload error:', error)
|
||||
}
|
||||
|
||||
// 超出数量限制
|
||||
const handleExceed = (files: File[]) => {
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning(`最多只能上传 ${props.limit} 张图片`)
|
||||
}
|
||||
|
||||
@@ -225,18 +264,16 @@ const handleDownload = (file: UploadFile) => {
|
||||
|
||||
// 删除图片
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
const index = fileList.value.findIndex(f => f.uid === file.uid)
|
||||
if (index > -1) {
|
||||
fileList.value.splice(index, 1)
|
||||
updateValue()
|
||||
}
|
||||
fileList.value = fileList.value.filter(f => f.uid !== file.uid)
|
||||
updateValue()
|
||||
}
|
||||
|
||||
// 更新值
|
||||
const updateValue = () => {
|
||||
const urls = fileList.value
|
||||
.filter(f => f.status === 'success' && f.url)
|
||||
.map(f => f.url!)
|
||||
.filter(f => f.status === 'success')
|
||||
.map(f => getRawUrl(f))
|
||||
.filter(Boolean)
|
||||
|
||||
const value = props.multiple ? urls : urls[0] || ''
|
||||
emit('update:modelValue', value)
|
||||
|
||||
@@ -28,6 +28,30 @@ export const resolveImageUrl = (value?: string | null) => {
|
||||
return imageUrl.startsWith('/') ? `${baseUrl}${imageUrl}` : `${baseUrl}/${imageUrl}`
|
||||
}
|
||||
|
||||
export const normalizeStorageImageUrl = (value?: string | null) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const imageUrl = String(value).trim()
|
||||
if (ABSOLUTE_URL_PATTERN.test(imageUrl)) {
|
||||
try {
|
||||
const parsed = new URL(imageUrl.startsWith('//') ? `http:${imageUrl}` : imageUrl)
|
||||
if (
|
||||
parsed.pathname.startsWith('/uploads/') ||
|
||||
parsed.pathname.startsWith('/images/') ||
|
||||
parsed.pathname.startsWith('/static/')
|
||||
) {
|
||||
return parsed.pathname
|
||||
}
|
||||
} catch {
|
||||
return imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
return imageUrl
|
||||
}
|
||||
|
||||
export const applyFallbackImage = (event: Event) => {
|
||||
const target = event.target as HTMLImageElement | null
|
||||
if (!target) return
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
AdminRecentOrderRow,
|
||||
AdminUserRow,
|
||||
} from '@/types/admin'
|
||||
import { DEFAULT_PRODUCT_IMAGE, resolveImageUrl } from '@/utils/image'
|
||||
import { DEFAULT_PRODUCT_IMAGE, normalizeStorageImageUrl, resolveImageUrl } from '@/utils/image'
|
||||
|
||||
const toNumber = (value: unknown, fallback = 0) => {
|
||||
const result = Number(value)
|
||||
@@ -280,7 +280,7 @@ export const normalizeAdminProduct = (product: Record<string, any>): AdminProduc
|
||||
price: toNumber(product.price),
|
||||
stock: toNumber(product.stock),
|
||||
status: toNumber(product.status, 1),
|
||||
imageUrl: resolveImageUrl(toString(product.imageUrl, '')),
|
||||
imageUrl: normalizeStorageImageUrl(toString(product.imageUrl, '')),
|
||||
createdAt: toIsoLikeString(product.createdAt),
|
||||
updatedAt: product.updatedAt ? toIsoLikeString(product.updatedAt) : undefined,
|
||||
totalSales: toNumber(product.totalSales),
|
||||
|
||||
Reference in New Issue
Block a user