完成图片管理功能

This commit is contained in:
aaron 2025-01-09 18:43:16 +08:00
parent e12c22f92a
commit 9b0298c3bd

View File

@ -22,7 +22,7 @@
{{ formatDateTime(record.create_time) }} {{ formatDateTime(record.create_time) }}
</template> </template>
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<a @click="handleManageImages(record)">管理图片</a> <a-button type="link" @click="handleManageImages(record)">管理图片</a-button>
</template> </template>
</template> </template>
</a-table> </a-table>
@ -38,55 +38,6 @@
<div id="map-container" style="height: 500px;"></div> <div id="map-container" style="height: 500px;"></div>
</a-modal> </a-modal>
<!-- 图片管理弹窗 -->
<a-modal
v-model:visible="imageModalVisible"
title="图片管理"
width="800px"
@ok="handleSaveImages"
@cancel="handleCancelImages"
:confirmLoading="imagesSaving"
>
<div class="image-uploader">
<a-upload
v-model:fileList="fileList"
:customRequest="handleUpload"
list-type="picture-card"
:multiple="true"
@preview="handlePreview"
@remove="handleRemove"
accept="image/*"
>
<template #uploadButton>
<div>
<plus-outlined />
<div style="margin-top: 8px">上传图片</div>
</div>
</template>
</a-upload>
</div>
<!-- 图片排序列表 -->
<div class="image-list">
<draggable
v-model="fileList"
item-key="uid"
ghost-class="ghost"
@end="handleDragEnd"
handle=".image-item"
>
<template #item="{ element }">
<div class="image-item">
<img :src="element.url" :alt="element.name" />
<div class="image-actions">
<delete-outlined @click="handleRemove(element)" />
</div>
</div>
</template>
</draggable>
</div>
</a-modal>
<!-- 添加商家模态框 --> <!-- 添加商家模态框 -->
<a-modal <a-modal
v-model:visible="addModalVisible" v-model:visible="addModalVisible"
@ -168,13 +119,42 @@
</a-form> </a-form>
</a-modal> </a-modal>
<!-- 添加图片管理模态框 -->
<a-modal
v-model:visible="imageModalVisible"
title="商家图片管理"
width="800px"
@ok="handleSaveImages"
@cancel="handleCancelImages"
:confirmLoading="imagesSaving"
okText="保存"
>
<div class="image-wall">
<a-upload
v-model:fileList="fileList"
:customRequest="handleUpload"
list-type="picture-card"
:multiple="true"
@preview="handlePreview"
@remove="handleRemove"
accept="image/*"
>
<div v-if="fileList.length < 8">
<plus-outlined />
<div style="margin-top: 8px">上传图片</div>
</div>
</a-upload>
</div>
</a-modal>
<!-- 图片预览模态框 --> <!-- 图片预览模态框 -->
<a-modal <a-modal
v-model:visible="previewVisible" :visible="previewVisible"
:title="previewTitle" :title="previewTitle"
:footer="null" :footer="null"
@cancel="handlePreviewCancel"
> >
<img :src="previewImage" style="width: 100%" /> <img :alt="previewTitle" style="width: 100%" :src="previewImage" />
</a-modal> </a-modal>
</div> </div>
</page-container> </page-container>
@ -182,26 +162,24 @@
<script> <script>
import { defineComponent, ref, onMounted, nextTick } from 'vue' import { defineComponent, ref, onMounted, nextTick } from 'vue'
import { message } from 'ant-design-vue' import { message, Upload, Modal } from 'ant-design-vue'
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import PageContainer from '@/components/PageContainer.vue' import PageContainer from '@/components/PageContainer.vue'
import { loadAMap, createMap, createAutoComplete, createGeocoder } from '@/utils/amap.js' import { loadAMap, createMap, createAutoComplete, createGeocoder } from '@/utils/amap.js'
import draggable from 'vuedraggable'
import request from '@/utils/request' import request from '@/utils/request'
import { PlusOutlined } from '@ant-design/icons-vue'
export default defineComponent({ export default defineComponent({
components: { components: {
PageContainer, PageContainer,
PlusOutlined, PlusOutlined,
DeleteOutlined, AUpload: Upload,
draggable AModal: Modal
}, },
setup() { setup() {
const loading = ref(false) const loading = ref(false)
const tableData = ref([]) const tableData = ref([])
const mapVisible = ref(false) const mapVisible = ref(false)
const imageModalVisible = ref(false)
const currentMap = ref(null) const currentMap = ref(null)
const currentMarker = ref(null) const currentMarker = ref(null)
@ -260,8 +238,9 @@ export default defineComponent({
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 100, width: 120,
align: 'center', align: 'center',
fixed: 'right'
} }
] ]
@ -338,116 +317,6 @@ export default defineComponent({
mapVisible.value = false mapVisible.value = false
} }
//
const imagesSaving = ref(false)
const currentMerchant = ref(null)
const fileList = ref([])
const previewVisible = ref(false)
const previewImage = ref('')
const previewTitle = ref('')
//
const handleManageImages = (record) => {
currentMerchant.value = record
imageModalVisible.value = true
//
if (record.images && record.images.length > 0) {
fileList.value = record.images.map((item, index) => ({
uid: `-${index}`,
name: item.image_url.substring(item.image_url.lastIndexOf('/') + 1),
status: 'done',
url: item.image_url,
sort: item.sort
}))
} else {
fileList.value = []
}
}
//
const handleUpload = async ({ file, onSuccess, onError }) => {
const formData = new FormData()
formData.append('files', file)
try {
const res = await request.post('/api/upload/images', formData)
if (res.data && res.data.urls) {
res.data.urls.forEach((url, index) => {
fileList.value.push({
uid: `new-${Date.now()}-${index}`,
name: url.substring(url.lastIndexOf('/') + 1),
status: 'done',
url: url,
sort: fileList.value.length
})
})
onSuccess()
message.success('上传成功')
}
} catch (error) {
console.error('上传图片失败:', error)
onError()
}
}
//
const handleSaveImages = async () => {
try {
imagesSaving.value = true
const images = fileList.value.map((file, index) => ({
image_url: file.url,
sort: index
}))
await request.put(`/api/merchant/${currentMerchant.value.id}`, { images })
message.success('保存成功')
imageModalVisible.value = false
fetchData()
} catch (error) {
console.error('保存图片失败:', error)
} finally {
imagesSaving.value = false
}
}
//
const handleCancelImages = () => {
fileList.value = []
imageModalVisible.value = false
}
//
const handleDragEnd = () => {
// v-model fileList
}
//
const handlePreview = (file) => {
previewImage.value = file.url || file.preview
previewVisible.value = true
previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1)
}
//
const handleRemove = async (file) => {
try {
const images = fileList.value
.filter(f => f.uid !== file.uid)
.map((f, index) => ({
image_url: f.url,
sort: index
}))
await request.put(`/api/merchant/${currentMerchant.value.id}`, { images })
message.success('删除成功')
fileList.value = fileList.value.filter(f => f.uid !== file.uid)
} catch (error) {
console.error('删除图片失败:', error)
message.error('删除失败')
}
}
// //
const addModalVisible = ref(false) const addModalVisible = ref(false)
const confirmLoading = ref(false) const confirmLoading = ref(false)
@ -619,6 +488,137 @@ export default defineComponent({
} }
} }
//
const imageModalVisible = ref(false)
const imagesSaving = ref(false)
const currentMerchant = ref(null)
const fileList = ref([])
const previewVisible = ref(false)
const previewImage = ref('')
const previewTitle = ref('')
//
const handleManageImages = (record) => {
currentMerchant.value = record
imageModalVisible.value = true
//
if (record.images && record.images.length > 0) {
fileList.value = record.images.map((item, index) => ({
uid: `-${index}`,
name: item.image_url.substring(item.image_url.lastIndexOf('/') + 1),
status: 'done',
url: item.image_url,
image_url: item.image_url,
sort: item.sort
}))
} else {
fileList.value = []
}
}
//
const handleUpload = async ({ file, onSuccess, onError, onProgress }) => {
const formData = new FormData()
formData.append('files', file)
try {
onProgress({ percent: 50 })
const res = await request.post('/api/upload/images', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (res.code === 200 && res.data && res.data.urls && res.data.urls.length > 0) {
const url = res.data.urls[0]
const fileInfo = {
uid: `new-${Date.now()}`,
name: url.substring(url.lastIndexOf('/') + 1),
status: 'done',
response: res.data, //
url: url, //
sort: fileList.value.length
}
//
fileList.value = [...fileList.value, fileInfo]
onSuccess(res.data) //
message.success('上传成功')
} else {
throw new Error('上传失败')
}
} catch (error) {
console.error('上传图片失败:', error)
onError()
message.error('上传失败')
}
}
//
const handlePreview = async (file) => {
previewImage.value = file.url || file.preview
previewVisible.value = true
previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1)
}
//
const handlePreviewCancel = () => {
previewVisible.value = false
previewTitle.value = ''
}
//
const handleRemove = async (file) => {
//
fileList.value = fileList.value.filter(f => f.uid !== file.uid)
return true
}
//
const handleSaveImages = async () => {
try {
imagesSaving.value = true
//
const data = {
images: fileList.value.map((file, index) => {
// URL
// 1. response.urls[0]
// 2. 使 url
const imageUrl = file.response?.urls?.[0] || file.url
return {
image_url: imageUrl,
sort: index
}
})
}
//
console.log('Saving data:', data)
const res = await request.put(`/api/merchant/${currentMerchant.value.id}`, data)
if (res.code === 200) {
message.success('保存成功')
imageModalVisible.value = false
fetchData()
} else {
throw new Error(res.message || '保存失败')
}
} catch (error) {
console.error('保存失败:', error)
message.error(error.message || '保存失败')
} finally {
imagesSaving.value = false
}
}
//
const handleCancelImages = () => {
fileList.value = []
imageModalVisible.value = false
}
onMounted(() => { onMounted(() => {
fetchData() fetchData()
}) })
@ -629,12 +629,10 @@ export default defineComponent({
tableData, tableData,
pagination, pagination,
mapVisible, mapVisible,
imageModalVisible,
handleTableChange, handleTableChange,
showMap, showMap,
closeMap, closeMap,
formatDateTime, formatDateTime,
handleManageImages,
addModalVisible, addModalVisible,
confirmLoading, confirmLoading,
formRef, formRef,
@ -643,22 +641,24 @@ export default defineComponent({
searchAddress, searchAddress,
searchOptions, searchOptions,
searchLoading, searchLoading,
showAddModal,
handleSearch,
handleSelect,
handleAdd,
handleCancel,
imageModalVisible,
imagesSaving,
fileList, fileList,
previewVisible, previewVisible,
previewImage, previewImage,
previewTitle, previewTitle,
showAddModal, handleManageImages,
handleSearch,
handleSelect,
handleUpload, handleUpload,
handlePreview, handlePreview,
handlePreviewCancel,
handleRemove, handleRemove,
handleAdd,
handleCancel,
imagesSaving,
handleSaveImages, handleSaveImages,
handleCancelImages, handleCancelImages
handleDragEnd
} }
} }
}) })
@ -690,49 +690,25 @@ export default defineComponent({
overflow: hidden; overflow: hidden;
} }
:deep(.ant-upload-list-picture-card-container) { .image-wall {
width: 104px; padding: 24px;
height: 104px; background: #fafafa;
margin: 0 8px 8px 0; border-radius: 2px;
min-height: 200px;
} }
:deep(.ant-upload.ant-upload-select-picture-card) { :deep(.ant-upload-select-picture-card) {
width: 104px; width: 104px !important;
height: 104px; height: 104px !important;
margin: 0 8px 8px 0; margin: 0 8px 8px 0;
}
.image-uploader {
padding: 20px;
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-upload.ant-upload-select-picture-card) {
width: 104px;
height: 104px;
margin: 0 8px 8px 0;
cursor: pointer;
border-radius: 4px;
background-color: #fafafa;
border: 1px dashed #d9d9d9; border: 1px dashed #d9d9d9;
border-radius: 2px;
cursor: pointer;
transition: border-color 0.3s; transition: border-color 0.3s;
&:hover { &:hover {
border-color: #1890ff; border-color: #1890ff;
} }
> div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.anticon {
font-size: 24px;
color: #999;
}
} }
:deep(.ant-upload-list-picture-card-container) { :deep(.ant-upload-list-picture-card-container) {
@ -741,70 +717,17 @@ export default defineComponent({
margin: 0 8px 8px 0; margin: 0 8px 8px 0;
} }
:deep(.ant-upload.ant-upload-select-picture-card) {
width: 104px;
height: 104px;
margin: 0 8px 8px 0;
cursor: pointer;
}
:deep(.ant-upload-list-picture-card .ant-upload-list-item) { :deep(.ant-upload-list-picture-card .ant-upload-list-item) {
padding: 4px; padding: 4px;
} }
:deep(.ant-upload.ant-upload-select-picture-card:hover) { :deep(.ant-upload-select-picture-card i) {
border-color: #1890ff; font-size: 32px;
color: #999;
} }
.image-list { :deep(.ant-upload-select-picture-card .ant-upload-text) {
margin-top: 20px; margin-top: 8px;
display: grid; color: #666;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
padding: 16px;
background: #fafafa;
border-radius: 4px;
}
.image-item {
position: relative;
width: 120px;
height: 120px;
border: 1px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
cursor: move;
}
.image-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-actions {
position: absolute;
top: 0;
right: 0;
padding: 4px;
background: rgba(0, 0, 0, 0.45);
border-bottom-left-radius: 4px;
opacity: 0;
transition: opacity 0.3s;
}
.image-item:hover .image-actions {
opacity: 1;
}
.image-actions .anticon {
color: #fff;
font-size: 16px;
cursor: pointer;
}
.ghost {
opacity: 0.5;
background: #c8ebfb;
} }
</style> </style>