添加商家页面实现。

This commit is contained in:
aaron 2025-01-09 18:14:22 +08:00
parent 9b6e38edce
commit e12c22f92a
4 changed files with 317 additions and 109 deletions

23
package-lock.json generated
View File

@ -1,11 +1,11 @@
{
"name": "dm-admin",
"name": "beefast-admin",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dm-admin",
"name": "beefast-admin",
"version": "0.1.0",
"dependencies": {
"ant-design-vue": "^3.2.20",
@ -13,6 +13,7 @@
"nprogress": "^0.2.0",
"vue": "^3.3.4",
"vue-router": "^4.5.0",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.2"
},
"devDependencies": {
@ -7694,6 +7695,12 @@
"websocket-driver": "^0.7.4"
}
},
"node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -8490,6 +8497,18 @@
"vue": "^3.0.0"
}
},
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"license": "MIT",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
}
},
"node_modules/vuex": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.2.tgz",

View File

@ -8,6 +8,7 @@
"nprogress": "^0.2.0",
"vue": "^3.3.4",
"vue-router": "^4.5.0",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.2"
},
"devDependencies": {

View File

@ -3,7 +3,10 @@ import { message } from 'ant-design-vue'
const request = axios.create({
baseURL: 'http://127.0.0.1:8000',
timeout: 5000
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
@ -13,6 +16,12 @@ request.interceptors.request.use(
if (token) {
config.headers.authorization = `Bearer ${token}`
}
// 处理文件上传
if (config.data instanceof FormData) {
delete config.headers['Content-Type']
}
return config
},
error => {
@ -23,7 +32,13 @@ request.interceptors.request.use(
// 响应拦截器
request.interceptors.response.use(
response => {
return response.data
const res = response.data
if (res.code === 200) {
return res
} else {
message.error(res.message || '请求失败')
return Promise.reject(new Error(res.message || '请求失败'))
}
},
error => {
message.error(error.response?.data?.message || '请求失败')

View File

@ -42,10 +42,49 @@
<a-modal
v-model:visible="imageModalVisible"
title="图片管理"
:footer="null"
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>
<!-- 添加商家模态框 -->
@ -126,22 +165,6 @@
/>
</a-input-group>
</a-form-item>
<a-form-item label="商家图片" name="images">
<a-upload
v-model:fileList="fileList"
:customRequest="handleUpload"
list-type="picture-card"
:multiple="true"
@preview="handlePreview"
@remove="handleRemove"
>
<div v-if="fileList.length < 8">
<plus-outlined />
<div style="margin-top: 8px">上传</div>
</div>
</a-upload>
</a-form-item>
</a-form>
</a-modal>
@ -160,15 +183,19 @@
<script>
import { defineComponent, ref, onMounted, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import dayjs from 'dayjs'
import PageContainer from '@/components/PageContainer.vue'
import { loadAMap, createMap, createAutoComplete, createGeocoder } from '@/utils/amap.js'
import draggable from 'vuedraggable'
import request from '@/utils/request'
export default defineComponent({
components: {
PageContainer,
PlusOutlined
PlusOutlined,
DeleteOutlined,
draggable
},
setup() {
const loading = ref(false)
@ -253,18 +280,13 @@ export default defineComponent({
limit: pagination.value.pageSize
}
const res = await fetch('/api/merchant?' + new URLSearchParams(params))
const result = await res.json()
if (result.code === 200) {
tableData.value = result.data
pagination.value.total = result.total || result.data.length
} else {
message.error(result.message || '获取数据失败')
const res = await request.get('/api/merchant', { params })
if (res.data) {
tableData.value = res.data.items || []
pagination.value.total = res.data.total || 0
}
} catch (error) {
console.error('获取商家列表失败:', error)
message.error('获取数据失败')
} finally {
loading.value = false
}
@ -284,10 +306,10 @@ export default defineComponent({
await nextTick()
try {
const AMap = await initAMap()
await loadAMap()
if (!currentMap.value) {
currentMap.value = new AMap.Map('map-container', {
currentMap.value = createMap('map-container', {
zoom: 15,
viewMode: '3D'
})
@ -297,15 +319,14 @@ export default defineComponent({
currentMap.value.setCenter(position)
if (currentMarker.value) {
currentMap.value.remove(currentMarker.value)
currentMarker.value.remove()
}
currentMarker.value = new AMap.Marker({
currentMarker.value = new window.AMap.Marker({
position,
map: currentMap.value,
title: record.name
})
currentMap.value.add(currentMarker.value)
} catch (error) {
console.error('加载地图失败:', error)
message.error('加载地图失败')
@ -317,10 +338,114 @@ export default defineComponent({
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('删除失败')
}
}
//
@ -332,10 +457,6 @@ export default defineComponent({
const searchLoading = ref(false)
const addMap = ref(null)
const addMarker = ref(null)
const fileList = ref([])
const previewVisible = ref(false)
const previewImage = ref('')
const previewTitle = ref('')
const formState = ref({
name: '',
@ -343,8 +464,7 @@ export default defineComponent({
address: '',
longitude: null,
latitude: null,
phone: '',
images: []
phone: ''
})
const rules = {
@ -472,72 +592,17 @@ export default defineComponent({
}
}
//
const handleUpload = async ({ file, onSuccess, onError }) => {
const formData = new FormData()
formData.append('files', file)
try {
const response = await fetch('/api/upload/images', {
method: 'POST',
body: formData
})
const result = await response.json()
if (result.code === 200) {
const imageUrl = result.data[0] // URL
formState.value.images.push(imageUrl)
onSuccess(result)
} else {
onError()
message.error('上传失败')
}
} catch (error) {
console.error('上传图片失败:', error)
onError()
message.error('上传失败')
}
}
//
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 = (file) => {
const index = formState.value.images.indexOf(file.url)
if (index > -1) {
formState.value.images.splice(index, 1)
}
}
//
const handleAdd = () => {
formRef.value.validate().then(async () => {
try {
confirmLoading.value = true
const res = await fetch('/api/merchant', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formState.value)
})
const result = await res.json()
if (result.code === 200) {
message.success('添加成功')
addModalVisible.value = false
fetchData() //
} else {
message.error(result.message || '添加失败')
}
await request.post('/api/merchant', formState.value)
message.success('添加成功')
addModalVisible.value = false
fetchData()
} catch (error) {
console.error('添加商家失败:', error)
message.error('添加失败')
} finally {
confirmLoading.value = false
}
@ -547,8 +612,6 @@ export default defineComponent({
//
const handleCancel = () => {
formRef.value?.resetFields()
fileList.value = []
formState.value.images = []
addModalVisible.value = false
if (addMarker.value) {
addMarker.value.setMap(null)
@ -591,7 +654,11 @@ export default defineComponent({
handlePreview,
handleRemove,
handleAdd,
handleCancel
handleCancel,
imagesSaving,
handleSaveImages,
handleCancelImages,
handleDragEnd
}
}
})
@ -634,4 +701,110 @@ export default defineComponent({
height: 104px;
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;
transition: border-color 0.3s;
&:hover {
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) {
width: 104px;
height: 104px;
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) {
padding: 4px;
}
:deep(.ant-upload.ant-upload-select-picture-card:hover) {
border-color: #1890ff;
}
.image-list {
margin-top: 20px;
display: grid;
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>