新增商家页面基础完成
This commit is contained in:
parent
d5f40e0dfd
commit
9b6e38edce
@ -1,15 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>蜂快到家后台管理系统</title>
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<script>
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: '6c7f5a402a13bf2dee1a4bbe0d8023c7'
|
||||
}
|
||||
</script>
|
||||
<script src="https://webapi.amap.com/maps?v=2.0&key=fd47f3d4f54b675693c7d59dcd2a6c5f&plugin=AMap.Geocoder,AMap.AutoComplete,AMap.PlaceSearch"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but 蜂快到家后台管理系统 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
|
||||
39
src/components/CommunitySelect/index.vue
Normal file
39
src/components/CommunitySelect/index.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<a-select
|
||||
v-bind="$attrs"
|
||||
:style="selectStyle"
|
||||
>
|
||||
<slot></slot>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CommunitySelect',
|
||||
setup() {
|
||||
const selectStyle = {
|
||||
height: '32px',
|
||||
width: '100%'
|
||||
}
|
||||
|
||||
return {
|
||||
selectStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-select-selector) {
|
||||
height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
}
|
||||
|
||||
:deep(.ant-select-selection-search-input) {
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
:deep(.ant-select-selection-item) {
|
||||
line-height: 30px !important;
|
||||
}
|
||||
</style>
|
||||
232
src/components/MapPicker/index.vue
Normal file
232
src/components/MapPicker/index.vue
Normal file
@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="map-picker">
|
||||
<a-form-item :label="label">
|
||||
<a-auto-complete
|
||||
v-model:value="searchAddress"
|
||||
:options="searchOptions"
|
||||
placeholder="输入地址搜索"
|
||||
@change="handleSearch"
|
||||
@select="handleSelect"
|
||||
:loading="searchLoading"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="地图选点" required>
|
||||
<div class="map-container">
|
||||
<div :id="mapContainerId" style="height: 300px;"></div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="详细地址" required>
|
||||
<a-input v-model:value="address" placeholder="请输入详细地址" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="经纬度" required>
|
||||
<a-input-group compact>
|
||||
<a-input-number
|
||||
v-model:value="longitude"
|
||||
:min="-180"
|
||||
:max="180"
|
||||
style="width: 50%"
|
||||
placeholder="经度"
|
||||
disabled
|
||||
/>
|
||||
<a-input-number
|
||||
v-model:value="latitude"
|
||||
:min="-90"
|
||||
:max="90"
|
||||
style="width: 50%"
|
||||
placeholder="纬度"
|
||||
disabled
|
||||
/>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { loadAMap, createMap, createAutoComplete, createGeocoder } from '@/utils/amap.js'
|
||||
|
||||
export default {
|
||||
name: 'MapPicker',
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '地址搜索'
|
||||
},
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
address: '',
|
||||
longitude: null,
|
||||
latitude: null
|
||||
})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const mapContainerId = `map-container-${Date.now()}`
|
||||
const map = ref(null)
|
||||
const marker = ref(null)
|
||||
const searchAddress = ref('')
|
||||
const searchOptions = ref([])
|
||||
const searchLoading = ref(false)
|
||||
|
||||
// 双向绑定的值
|
||||
const address = ref(props.modelValue.address)
|
||||
const longitude = ref(props.modelValue.longitude)
|
||||
const latitude = ref(props.modelValue.latitude)
|
||||
|
||||
// 监听值的变化并触发更新
|
||||
const updateValue = () => {
|
||||
emit('update:modelValue', {
|
||||
address: address.value,
|
||||
longitude: longitude.value,
|
||||
latitude: latitude.value
|
||||
})
|
||||
}
|
||||
|
||||
// 更新标记位置
|
||||
const updateMarkerPosition = (lng, lat) => {
|
||||
longitude.value = lng
|
||||
latitude.value = lat
|
||||
|
||||
if (marker.value) {
|
||||
marker.value.setPosition([lng, lat])
|
||||
} else {
|
||||
marker.value = new window.AMap.Marker({
|
||||
position: [lng, lat],
|
||||
map: map.value
|
||||
})
|
||||
}
|
||||
updateValue()
|
||||
}
|
||||
|
||||
// 初始化地图
|
||||
const initMap = async () => {
|
||||
try {
|
||||
await loadAMap()
|
||||
|
||||
if (!map.value) {
|
||||
map.value = createMap(mapContainerId, {
|
||||
zoom: 13,
|
||||
viewMode: '2D'
|
||||
})
|
||||
|
||||
// 添加点击事件
|
||||
map.value.on('click', async (e) => {
|
||||
const { lng, lat } = e.lnglat
|
||||
updateMarkerPosition(lng, lat)
|
||||
|
||||
// 逆地理编码获取地址
|
||||
const geocoder = createGeocoder()
|
||||
geocoder.getAddress([lng, lat], (status, result) => {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
address.value = result.regeocode.formattedAddress
|
||||
updateValue()
|
||||
} else {
|
||||
message.error('获取地址信息失败')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 如果有初始值,设置标记
|
||||
if (props.modelValue.longitude && props.modelValue.latitude) {
|
||||
updateMarkerPosition(props.modelValue.longitude, props.modelValue.latitude)
|
||||
map.value.setCenter([props.modelValue.longitude, props.modelValue.latitude])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化地图失败:', error)
|
||||
message.error('初始化地图失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 地址搜索
|
||||
const handleSearch = async (value) => {
|
||||
if (!value || value.length < 2) {
|
||||
searchOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
searchLoading.value = true
|
||||
await loadAMap()
|
||||
|
||||
const autoComplete = createAutoComplete()
|
||||
await new Promise((resolve, reject) => {
|
||||
autoComplete.search(value, (status, result) => {
|
||||
if (status === 'complete') {
|
||||
searchOptions.value = result.tips.map(tip => ({
|
||||
value: tip.name,
|
||||
label: `${tip.name} (${tip.district})`,
|
||||
location: tip.location
|
||||
}))
|
||||
resolve(result)
|
||||
} else {
|
||||
reject(new Error(status))
|
||||
}
|
||||
searchLoading.value = false
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('搜索地址失败:', error)
|
||||
searchLoading.value = false
|
||||
message.error('搜索地址失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 选择地址
|
||||
const handleSelect = (value, option) => {
|
||||
const selected = searchOptions.value.find(opt => opt.value === value)
|
||||
if (selected && selected.location) {
|
||||
const { lng, lat } = selected.location
|
||||
updateMarkerPosition(lng, lat)
|
||||
map.value.setCenter([lng, lat])
|
||||
|
||||
// 使用逆地理编码获取完整地址
|
||||
const geocoder = createGeocoder()
|
||||
geocoder.getAddress([lng, lat], (status, result) => {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
address.value = result.regeocode.formattedAddress
|
||||
updateValue()
|
||||
} else {
|
||||
address.value = value
|
||||
updateValue()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initMap()
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
mapContainerId,
|
||||
searchAddress,
|
||||
searchOptions,
|
||||
searchLoading,
|
||||
address,
|
||||
longitude,
|
||||
latitude,
|
||||
handleSearch,
|
||||
handleSelect
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-container {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
4
src/config/map.js
Normal file
4
src/config/map.js
Normal file
@ -0,0 +1,4 @@
|
||||
export const MAP_CONFIG = {
|
||||
key: 'fd47f3d4f54b675693c7d59dcd2a6c5f',
|
||||
securityJsCode: '6c7f5a402a13bf2dee1a4bbe0d8023c7'
|
||||
}
|
||||
@ -1,6 +1,20 @@
|
||||
export function initAMap() {
|
||||
// 高德地图配置
|
||||
const MAP_CONFIG = {
|
||||
key: 'fd47f3d4f54b675693c7d59dcd2a6c5f',
|
||||
securityJsCode: '93527b49270ba2142f47f0407da7c0d6'
|
||||
}
|
||||
|
||||
// 设置安全密钥
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: MAP_CONFIG.securityJsCode
|
||||
}
|
||||
|
||||
let isMapLoaded = false
|
||||
|
||||
// 加载高德地图及其插件
|
||||
export const loadAMap = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.AMap) {
|
||||
if (isMapLoaded && window.AMap) {
|
||||
resolve(window.AMap)
|
||||
return
|
||||
}
|
||||
@ -8,26 +22,41 @@ export function initAMap() {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.async = true
|
||||
script.src = `https://webapi.amap.com/maps?v=2.0&key=fd47f3d4f54b675693c7d59dcd2a6c5f&plugin=AMap.PlaceSearch,AMap.AutoComplete,AMap.Geocoder&callback=initAMapCallback`
|
||||
|
||||
window.initAMapCallback = () => {
|
||||
script.src = `https://webapi.amap.com/maps?v=2.0&key=${MAP_CONFIG.key}&plugin=AMap.Geocoder,AMap.AutoComplete,AMap.PlaceSearch`
|
||||
|
||||
script.onload = () => {
|
||||
isMapLoaded = true
|
||||
resolve(window.AMap)
|
||||
}
|
||||
|
||||
script.onerror = () => {
|
||||
reject(new Error('加载高德地图失败'))
|
||||
}
|
||||
|
||||
script.onerror = reject
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
// 创建地图实例
|
||||
export function createMap(container, options = {}) {
|
||||
export const createMap = (container, options = {}) => {
|
||||
const defaultOptions = {
|
||||
zoom: 15,
|
||||
viewMode: '2D'
|
||||
viewMode: '2D',
|
||||
center: [116.397428, 39.90923] // 默认中心点
|
||||
}
|
||||
|
||||
return new window.AMap.Map(container, {
|
||||
...defaultOptions,
|
||||
return new window.AMap.Map(container, { ...defaultOptions, ...options })
|
||||
}
|
||||
|
||||
// 创建地址搜索服务
|
||||
export const createAutoComplete = (options = {}) => {
|
||||
return new window.AMap.AutoComplete({
|
||||
city: '全国',
|
||||
citylimit: true,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// 创建地理编码服务
|
||||
export const createGeocoder = () => {
|
||||
return new window.AMap.Geocoder()
|
||||
}
|
||||
@ -73,58 +73,14 @@
|
||||
:wrapper-col="{ span: 16 }"
|
||||
class="community-form"
|
||||
>
|
||||
<a-form-item label="地址搜索">
|
||||
<a-auto-complete
|
||||
v-model:value="searchAddress"
|
||||
:options="searchOptions"
|
||||
placeholder="输入地址搜索"
|
||||
@change="handleSearch"
|
||||
@select="handleSelect"
|
||||
:loading="searchLoading"
|
||||
allow-clear
|
||||
>
|
||||
<template #option="{ value: val, label }">
|
||||
<div>{{ label }}</div>
|
||||
</template>
|
||||
</a-auto-complete>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label=" " :colon="false">
|
||||
<div class="map-container">
|
||||
<div id="add-map-container" style="height: 240px;"></div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="小区名称" name="name" required>
|
||||
<a-input v-model:value="formState.name" placeholder="请输入小区名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="详细地址" name="address" required>
|
||||
<a-input v-model:value="formState.address" placeholder="请输入详细地址" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="经纬度" required>
|
||||
<a-input-group compact>
|
||||
<a-input-number
|
||||
v-model:value="formState.longitude"
|
||||
:min="-180"
|
||||
:max="180"
|
||||
:controls="false"
|
||||
disabled
|
||||
style="width: 50%"
|
||||
placeholder="经度"
|
||||
/>
|
||||
<a-input-number
|
||||
v-model:value="formState.latitude"
|
||||
:min="-90"
|
||||
:max="90"
|
||||
:controls="false"
|
||||
disabled
|
||||
style="width: 50%"
|
||||
placeholder="纬度"
|
||||
/>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<map-picker
|
||||
v-model="formState.location"
|
||||
label="地址搜索"
|
||||
/>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
@ -133,18 +89,23 @@
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, onMounted, nextTick } from 'vue'
|
||||
import { message, Tag } from 'ant-design-vue'
|
||||
import { message, Tag, Menu, Dropdown } from 'ant-design-vue'
|
||||
import { getCommunityList, createCommunity, updateCommunityStatus } from '@/api/community'
|
||||
import { initAMap, createMap } from '@/utils/amap'
|
||||
import { loadAMap, createMap } from '@/utils/amap.js'
|
||||
import dayjs from 'dayjs'
|
||||
import PageContainer from '@/components/PageContainer.vue'
|
||||
import { DownOutlined } from '@ant-design/icons-vue'
|
||||
import MapPicker from '@/components/MapPicker/index.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
PageContainer,
|
||||
[Tag.name]: Tag,
|
||||
DownOutlined
|
||||
ATag: Tag,
|
||||
ADropdown: Dropdown,
|
||||
AMenu: Menu,
|
||||
AMenuItem: Menu.Item,
|
||||
DownOutlined,
|
||||
MapPicker
|
||||
},
|
||||
setup() {
|
||||
const loading = ref(false)
|
||||
@ -255,31 +216,26 @@ export default defineComponent({
|
||||
const showMap = async (record) => {
|
||||
mapVisible.value = true
|
||||
|
||||
// 等待 DOM 更新
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
// 设置中心点
|
||||
const position = [record.longitude, record.latitude]
|
||||
currentMap.value.setCenter(position)
|
||||
|
||||
// 清除旧标记
|
||||
if (currentMarker.value) {
|
||||
currentMap.value.remove(currentMarker.value)
|
||||
}
|
||||
|
||||
// 添加新标记
|
||||
currentMarker.value = new AMap.Marker({
|
||||
currentMarker.value = new window.AMap.Marker({
|
||||
position,
|
||||
title: record.name
|
||||
})
|
||||
@ -308,191 +264,38 @@ export default defineComponent({
|
||||
|
||||
const formState = ref({
|
||||
name: '',
|
||||
address: '',
|
||||
longitude: null,
|
||||
latitude: null
|
||||
location: {
|
||||
address: '',
|
||||
longitude: null,
|
||||
latitude: null
|
||||
}
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入小区名称' }],
|
||||
address: [{ required: true, message: '请输入详细地址' }],
|
||||
longitude: [{ required: true, message: '请选择位置获取经度' }],
|
||||
latitude: [{ required: true, message: '请选择位置获取纬度' }],
|
||||
'location.address': [{ required: true, message: '请选择地址' }],
|
||||
'location.longitude': [{ required: true, message: '请在地图上选择位置' }],
|
||||
'location.latitude': [{ required: true, message: '请在地图上选择位置' }]
|
||||
}
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = async () => {
|
||||
const showAddModal = () => {
|
||||
addModalVisible.value = true
|
||||
await nextTick()
|
||||
initAddMap()
|
||||
}
|
||||
|
||||
// 更新标记位置和表单信息
|
||||
const updateMarkerPosition = (lng, lat, name = '') => {
|
||||
if (!addMap.value) return
|
||||
|
||||
formState.value.longitude = lng
|
||||
formState.value.latitude = lat
|
||||
|
||||
// 如果提供了名称,更新小区名称
|
||||
if (name && !formState.value.name) {
|
||||
formState.value.name = name
|
||||
}
|
||||
|
||||
if (addMarker.value) {
|
||||
addMarker.value.setPosition([lng, lat])
|
||||
} else {
|
||||
const AMap = window.AMap
|
||||
addMarker.value = new AMap.Marker({
|
||||
position: [lng, lat],
|
||||
map: addMap.value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 地图点击事件处理
|
||||
const initAddMap = async () => {
|
||||
try {
|
||||
const AMap = await initAMap();
|
||||
|
||||
if (!addMap.value) {
|
||||
addMap.value = createMap('add-map-container', {
|
||||
center: [116.397428, 39.90923]
|
||||
});
|
||||
|
||||
// 添加点击事件
|
||||
addMap.value.on('click', (e) => {
|
||||
const { lng, lat } = e.lnglat;
|
||||
// 点击地图时,使用逆地理编码获取位置信息
|
||||
const geocoder = new AMap.Geocoder({
|
||||
radius: 100, // 搜索半径
|
||||
extensions: 'all' // 返回完整信息
|
||||
});
|
||||
|
||||
geocoder.getAddress([lng, lat], (status, result) => {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
const address = result.regeocode;
|
||||
formState.value.address = address.formattedAddress;
|
||||
|
||||
// 优先使用最近的 POI 名称作为小区名称
|
||||
if (address.pois && address.pois.length > 0) {
|
||||
// 筛选类型包含"住宅区"、"小区"、"住宅小区"的 POI
|
||||
const residentialPoi = address.pois.find(poi =>
|
||||
poi.type.includes('住宅区') ||
|
||||
poi.type.includes('小区') ||
|
||||
poi.type.includes('住宅小区')
|
||||
);
|
||||
|
||||
if (residentialPoi) {
|
||||
formState.value.name = residentialPoi.name;
|
||||
} else {
|
||||
// 如果没有找到住宅类 POI,使用第一个 POI 的名称
|
||||
formState.value.name = address.pois[0].name;
|
||||
}
|
||||
} else if (address.aois && address.aois.length > 0) {
|
||||
// 如果没有合适的 POI,尝试使用 AOI 信息
|
||||
formState.value.name = address.aois[0].name;
|
||||
} else {
|
||||
// 如果没有 POI 和 AOI,使用地址信息组合
|
||||
const township = address.addressComponent.township || '';
|
||||
const street = address.addressComponent.street || '';
|
||||
formState.value.name = `${township}${street}`;
|
||||
}
|
||||
|
||||
// 更新标记位置
|
||||
updateMarkerPosition(lng, lat);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化地图失败:', error);
|
||||
message.error('初始化地图失败,请刷新重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索地址
|
||||
const handleSearch = async (value) => {
|
||||
if (!value || value.length < 2) {
|
||||
searchOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
searchLoading.value = true;
|
||||
await initAMap(); // 确保地图已加载
|
||||
|
||||
const autoComplete = new window.AMap.AutoComplete({
|
||||
city: '全国'
|
||||
});
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
autoComplete.search(value, (status, result) => {
|
||||
if (status === 'complete') {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(new Error('搜索失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
searchOptions.value = result.tips.map(tip => ({
|
||||
value: tip.name,
|
||||
label: `${tip.name} (${tip.district})`,
|
||||
location: tip.location,
|
||||
address: tip.address || tip.district
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('搜索地址失败:', error);
|
||||
searchOptions.value = [];
|
||||
} finally {
|
||||
searchLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 选择地址
|
||||
const handleSelect = (value, option) => {
|
||||
const selectedOption = searchOptions.value.find(opt => opt.value === value);
|
||||
if (selectedOption) {
|
||||
// 更新地址和小区名称
|
||||
formState.value.address = selectedOption.address;
|
||||
formState.value.name = selectedOption.value; // 使用选中的地点名称作为小区名称
|
||||
|
||||
if (selectedOption.location) {
|
||||
updateMarkerPosition(
|
||||
selectedOption.location.lng,
|
||||
selectedOption.location.lat,
|
||||
selectedOption.value
|
||||
);
|
||||
addMap.value.setCenter([selectedOption.location.lng, selectedOption.location.lat]);
|
||||
} else {
|
||||
// 如果没有直接的位置信息,使用 PlaceSearch 获取详细信息
|
||||
const placeSearch = new window.AMap.PlaceSearch({
|
||||
city: '全国'
|
||||
});
|
||||
|
||||
placeSearch.search(value, (status, result) => {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
const poi = result.poiList.pois[0];
|
||||
formState.value.address = poi.address;
|
||||
updateMarkerPosition(
|
||||
poi.location.lng,
|
||||
poi.location.lat,
|
||||
poi.name
|
||||
);
|
||||
addMap.value.setCenter([poi.location.lng, poi.location.lat]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleAdd = () => {
|
||||
formRef.value.validate().then(async () => {
|
||||
try {
|
||||
confirmLoading.value = true
|
||||
const res = await createCommunity(formState.value)
|
||||
const params = {
|
||||
name: formState.value.name,
|
||||
address: formState.value.location.address,
|
||||
longitude: formState.value.location.longitude,
|
||||
latitude: formState.value.location.latitude
|
||||
}
|
||||
|
||||
const res = await createCommunity(params)
|
||||
if (res.code === 200) {
|
||||
message.success('添加成功')
|
||||
addModalVisible.value = false
|
||||
@ -513,11 +316,6 @@ export default defineComponent({
|
||||
const handleCancel = () => {
|
||||
formRef.value?.resetFields()
|
||||
addModalVisible.value = false
|
||||
searchAddress.value = ''
|
||||
if (addMarker.value) {
|
||||
addMarker.value.setMap(null)
|
||||
addMarker.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理状态变更
|
||||
@ -558,17 +356,12 @@ export default defineComponent({
|
||||
formatDateTime,
|
||||
addModalVisible,
|
||||
confirmLoading,
|
||||
searchLoading,
|
||||
searchAddress,
|
||||
formState,
|
||||
formRef,
|
||||
rules,
|
||||
showAddModal,
|
||||
handleAdd,
|
||||
handleCancel,
|
||||
handleSearch,
|
||||
searchOptions,
|
||||
handleSelect,
|
||||
handleStatusChange
|
||||
}
|
||||
}
|
||||
@ -862,4 +655,11 @@ export default defineComponent({
|
||||
:deep(.ant-select-selection-placeholder) {
|
||||
line-height: 30px !important;
|
||||
}
|
||||
|
||||
/* 调整地图容器样式 */
|
||||
:deep(.map-container) {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@ -3,6 +3,7 @@
|
||||
<div class="merchant-list">
|
||||
<div class="table-header">
|
||||
<h1>商家列表</h1>
|
||||
<a-button type="primary" @click="showAddModal">添加商家</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
@ -46,6 +47,112 @@
|
||||
>
|
||||
<!-- 这里添加图片管理的具体实现 -->
|
||||
</a-modal>
|
||||
|
||||
<!-- 添加商家模态框 -->
|
||||
<a-modal
|
||||
v-model:visible="addModalVisible"
|
||||
title="添加商家"
|
||||
:confirmLoading="confirmLoading"
|
||||
width="800px"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" :loading="confirmLoading" @click="handleAdd">
|
||||
保存
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="商家名称" name="name" required>
|
||||
<a-input v-model:value="formState.name" placeholder="请输入商家名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="营业时间" name="business_hours" required>
|
||||
<a-input v-model:value="formState.business_hours" placeholder="例如:10:30~20:30" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="联系电话" name="phone" required>
|
||||
<a-input v-model:value="formState.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="地址搜索">
|
||||
<a-auto-complete
|
||||
v-model:value="searchAddress"
|
||||
:options="searchOptions"
|
||||
placeholder="输入地址搜索"
|
||||
@change="handleSearch"
|
||||
@select="handleSelect"
|
||||
:loading="searchLoading"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="地图选点" required>
|
||||
<div class="map-container">
|
||||
<div id="add-map-container" style="height: 300px;"></div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="详细地址" name="address" required>
|
||||
<a-input v-model:value="formState.address" placeholder="请输入详细地址" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="经纬度" required>
|
||||
<a-input-group compact>
|
||||
<a-input-number
|
||||
v-model:value="formState.longitude"
|
||||
:min="-180"
|
||||
:max="180"
|
||||
style="width: 50%"
|
||||
placeholder="经度"
|
||||
disabled
|
||||
/>
|
||||
<a-input-number
|
||||
v-model:value="formState.latitude"
|
||||
:min="-90"
|
||||
:max="90"
|
||||
style="width: 50%"
|
||||
placeholder="纬度"
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- 图片预览模态框 -->
|
||||
<a-modal
|
||||
v-model:visible="previewVisible"
|
||||
:title="previewTitle"
|
||||
:footer="null"
|
||||
>
|
||||
<img :src="previewImage" style="width: 100%" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</page-container>
|
||||
</template>
|
||||
@ -53,13 +160,15 @@
|
||||
<script>
|
||||
import { defineComponent, ref, onMounted, nextTick } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import PageContainer from '@/components/PageContainer.vue'
|
||||
import { initAMap } from '@/utils/amap'
|
||||
import { loadAMap, createMap, createAutoComplete, createGeocoder } from '@/utils/amap.js'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
PageContainer
|
||||
PageContainer,
|
||||
PlusOutlined
|
||||
},
|
||||
setup() {
|
||||
const loading = ref(false)
|
||||
@ -214,6 +323,239 @@ export default defineComponent({
|
||||
// 这里添加图片管理的具体实现
|
||||
}
|
||||
|
||||
// 添加商家相关
|
||||
const addModalVisible = ref(false)
|
||||
const confirmLoading = ref(false)
|
||||
const formRef = ref(null)
|
||||
const searchAddress = ref('')
|
||||
const searchOptions = ref([])
|
||||
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: '',
|
||||
business_hours: '',
|
||||
address: '',
|
||||
longitude: null,
|
||||
latitude: null,
|
||||
phone: '',
|
||||
images: []
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入商家名称' }],
|
||||
business_hours: [{ required: true, message: '请输入营业时间' }],
|
||||
address: [{ required: true, message: '请输入详细地址' }],
|
||||
phone: [{ required: true, message: '请输入联系电话' }],
|
||||
}
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = async () => {
|
||||
addModalVisible.value = true
|
||||
await nextTick()
|
||||
initAddMap()
|
||||
}
|
||||
|
||||
// 初始化地图
|
||||
const initAddMap = async () => {
|
||||
try {
|
||||
await loadAMap()
|
||||
|
||||
if (!addMap.value) {
|
||||
addMap.value = createMap('add-map-container', {
|
||||
zoom: 13,
|
||||
viewMode: '2D'
|
||||
})
|
||||
|
||||
// 添加点击事件
|
||||
addMap.value.on('click', async (e) => {
|
||||
const { lng, lat } = e.lnglat
|
||||
updateMarkerPosition(lng, lat)
|
||||
|
||||
// 逆地理编码获取地址
|
||||
const geocoder = createGeocoder()
|
||||
geocoder.getAddress([lng, lat], (status, result) => {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
const address = result.regeocode
|
||||
// 更新表单数据
|
||||
formState.value.address = address.formattedAddress
|
||||
formState.value.longitude = lng
|
||||
formState.value.latitude = lat
|
||||
} else {
|
||||
message.error('获取地址信息失败')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化地图失败:', error)
|
||||
message.error('初始化地图失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新标记位置
|
||||
const updateMarkerPosition = (lng, lat) => {
|
||||
if (addMarker.value) {
|
||||
addMarker.value.setPosition([lng, lat])
|
||||
} else {
|
||||
const AMap = window.AMap
|
||||
addMarker.value = new AMap.Marker({
|
||||
position: [lng, lat],
|
||||
map: addMap.value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 地址搜索
|
||||
const handleSearch = async (value) => {
|
||||
if (!value || value.length < 2) {
|
||||
searchOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
searchLoading.value = true
|
||||
await loadAMap()
|
||||
|
||||
const autoComplete = createAutoComplete()
|
||||
await new Promise((resolve, reject) => {
|
||||
autoComplete.search(value, (status, result) => {
|
||||
if (status === 'complete') {
|
||||
searchOptions.value = result.tips.map(tip => ({
|
||||
value: tip.name,
|
||||
label: `${tip.name} (${tip.district})`,
|
||||
location: tip.location
|
||||
}))
|
||||
resolve(result)
|
||||
} else {
|
||||
reject(new Error(status))
|
||||
}
|
||||
searchLoading.value = false
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('搜索地址失败:', error)
|
||||
searchLoading.value = false
|
||||
message.error('搜索地址失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 选择地址
|
||||
const handleSelect = (value, option) => {
|
||||
const selected = searchOptions.value.find(opt => opt.value === value)
|
||||
if (selected && selected.location) {
|
||||
const { lng, lat } = selected.location
|
||||
updateMarkerPosition(lng, lat)
|
||||
addMap.value.setCenter([lng, lat])
|
||||
|
||||
// 使用逆地理编码获取完整地址
|
||||
const geocoder = createGeocoder()
|
||||
geocoder.getAddress([lng, lat], (status, result) => {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
const address = result.regeocode
|
||||
// 更新表单数据
|
||||
formState.value.address = address.formattedAddress
|
||||
formState.value.longitude = lng
|
||||
formState.value.latitude = lat
|
||||
} else {
|
||||
// 如果获取详细地址失败,至少使用搜索选中的地址
|
||||
formState.value.address = value
|
||||
formState.value.longitude = lng
|
||||
formState.value.latitude = lat
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图片上传
|
||||
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 || '添加失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加商家失败:', error)
|
||||
message.error('添加失败')
|
||||
} finally {
|
||||
confirmLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 取消添加
|
||||
const handleCancel = () => {
|
||||
formRef.value?.resetFields()
|
||||
fileList.value = []
|
||||
formState.value.images = []
|
||||
addModalVisible.value = false
|
||||
if (addMarker.value) {
|
||||
addMarker.value.setMap(null)
|
||||
addMarker.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
@ -229,7 +571,27 @@ export default defineComponent({
|
||||
showMap,
|
||||
closeMap,
|
||||
formatDateTime,
|
||||
handleManageImages
|
||||
handleManageImages,
|
||||
addModalVisible,
|
||||
confirmLoading,
|
||||
formRef,
|
||||
formState,
|
||||
rules,
|
||||
searchAddress,
|
||||
searchOptions,
|
||||
searchLoading,
|
||||
fileList,
|
||||
previewVisible,
|
||||
previewImage,
|
||||
previewTitle,
|
||||
showAddModal,
|
||||
handleSearch,
|
||||
handleSelect,
|
||||
handleUpload,
|
||||
handlePreview,
|
||||
handleRemove,
|
||||
handleAdd,
|
||||
handleCancel
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -254,4 +616,22 @@ export default defineComponent({
|
||||
:deep(.ant-table-content) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user