代码修改

This commit is contained in:
qiuyuan 2025-10-11 18:21:53 +08:00
parent b3377a317a
commit a0188776e9
6 changed files with 1034 additions and 99 deletions

11
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "HaHa-Admin", "name": "HaHa-Admin",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/colors": "^7.0.0", "@ant-design/colors": "^7.0.0",
"@ant-design/icons-vue": "^6.1.0", "@ant-design/icons-vue": "^6.1.0",
"@icon-park/vue-next": "^1.4.2", "@icon-park/vue-next": "^1.4.2",
@ -240,6 +241,11 @@
"@algolia/requester-common": "4.19.1" "@algolia/requester-common": "4.19.1"
} }
}, },
"node_modules/@amap/amap-jsapi-loader": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz",
"integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw=="
},
"node_modules/@ant-design/colors": { "node_modules/@ant-design/colors": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.0.0.tgz", "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.0.0.tgz",
@ -5179,6 +5185,11 @@
"@algolia/requester-common": "4.19.1" "@algolia/requester-common": "4.19.1"
} }
}, },
"@amap/amap-jsapi-loader": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz",
"integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw=="
},
"@ant-design/colors": { "@ant-design/colors": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.0.0.tgz", "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.0.0.tgz",

View File

@ -17,6 +17,7 @@
"src/**/*.{js,vue}": "eslint --ext .js,.vue .eslintignore --no-cache --fix" "src/**/*.{js,vue}": "eslint --ext .js,.vue .eslintignore --no-cache --fix"
}, },
"dependencies": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/colors": "^7.0.0", "@ant-design/colors": "^7.0.0",
"@ant-design/icons-vue": "^6.1.0", "@ant-design/icons-vue": "^6.1.0",
"@icon-park/vue-next": "^1.4.2", "@icon-park/vue-next": "^1.4.2",

View File

@ -0,0 +1,926 @@
<template>
<a-modal
:open="visible"
:title="title"
:width="width"
:maskClosable="false"
@ok="handleOk"
@cancel="handleCancel"
>
<!-- 错误提示区域 -->
<a-alert
v-if="errorMessage"
:message="errorMessage"
:description="errorDetails"
type="error"
show-icon
style="margin-bottom: 12px"
closable
@close="clearError"
/>
<!-- 搜索区域 -->
<div class="search-section">
<a-form layout="vertical" :model="searchForm">
<a-form-item label="输入地址">
<a-input-search
v-model:value="searchForm.keyword"
placeholder="输入需要查找的地址"
enter-button
@search="handleSearch"
:disabled="!mapLoaded || isSearching"
allow-clear
/>
</a-form-item>
<a-form-item label="搜索结果" v-if="searchList.length > 0">
<a-select
v-model:value="selectedPlaceId"
style="width: 100%"
placeholder="请选择搜索结果"
@change="handlePlaceSelect"
>
<a-select-option
v-for="item in searchList"
:key="item.id"
:value="item.id"
>
{{ item.name }} - {{ item.address }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</div>
<!-- 地图容器带加载状态 -->
<div class="map-container" :style="{ height: mapHeight }">
<div v-if="!mapLoaded" class="map-loading">
<a-spin size="large" />
<div class="loading-text">{{ loadingText }}</div>
</div>
<!-- 调试信息 (仅开发环境显示) -->
<!-- <div v-if="debugMode && mapLoaded" class="debug-info">
<div class="debug-header">调试信息</div>
<pre>{{ debugInfo }}</pre>
</div> -->
<div id="amap-container" class="amap-instance" ref="mapContainer"></div>
</div>
<a-form :model="formState" layout="vertical" style="margin-top: 16px">
<a-form-item label="经纬度">
<a-input
v-model:value="formState.lnglat"
readonly
placeholder="点击地图或搜索地点选择位置"
>
<template #suffix>
<a-spin v-if="isGeocoding" size="small" />
</template>
</a-input>
</a-form-item>
<a-form-item label="详细地址">
<a-input
v-model:value="formState.address"
readonly
placeholder="地址信息将自动显示"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick, reactive, computed } from 'vue'
import { message } from 'ant-design-vue'
import AMapLoader from '@amap/amap-jsapi-loader'
import { debounce } from 'lodash-es'
//
interface FormState {
lnglat: string
address: string
location: { lng: number; lat: number } | null
}
interface SearchPlace {
id: string
name: string
address: string
location: { lng: number; lat: number }
}
interface LocationData {
lng: number
lat: number
address: string
name?: string
}
//
const props = defineProps({
open: {
type: Boolean,
default: false
},
title: {
type: String,
default: '选择位置'
},
width: {
type: String,
default: '800px'
},
searchPlaceholder: {
type: String,
default: '请输入地点名称,如:北京西站、上海外滩'
},
defaultCenter: {
type: Array as () => [number, number],
default: () => [120.894522, 31.981269] //
},
defaultZoom: {
type: Number,
default: 12
},
searchCity: {
type: String,
default: '全国'
},
mapHeight: {
type: String,
default: '400px'
},
debugMode: {
type: Boolean,
default: process.env.NODE_ENV === 'development'
}
})
//
const emit = defineEmits(['update:open', 'select', 'error', 'handleGetLng'])
//
const visible = ref(false)
const mapLoaded = ref(false)
const isGeocoding = ref(false)
const isSearching = ref(false)
const loadingText = ref('地图加载中...')
const errorMessage = ref('')
const errorDetails = ref('')
const debugInfo = ref('')
const selectedPlaceId = ref('')
const formState: FormState = reactive({
lnglat: '',
address: '',
location: null
})
const searchForm = reactive({
keyword: ''
})
const searchList = ref<SearchPlace[]>([])
//
const mapContainer = ref<HTMLElement>()
let map: AMap.Map | null = null
let marker: AMap.Marker | null = null
let placeSearch: AMap.PlaceSearch | null = null
let geocoder: AMap.Geocoder | null = null
let infoWindow: AMap.InfoWindow | null = null
let markers: AMap.Marker[] = []
// (300ms)
const debouncedSearch = debounce((keyword: string) => {
performSearch(keyword)
}, 300)
//
const logDebug = (message: string) => {
if (props.debugMode) {
const timestamp = new Date().toLocaleTimeString()
debugInfo.value = `[${timestamp}] ${message}\n${debugInfo.value}`
if (debugInfo.value.length > 5000) {
debugInfo.value = debugInfo.value.substring(0, 5000)
}
}
}
//
const clearError = () => {
errorMessage.value = ''
errorDetails.value = ''
emit('error', null)
}
//
const setError = (message: string, details?: string) => {
errorMessage.value = message
errorDetails.value = details || ''
emit('error', { message, details })
logDebug(`错误: ${message} - ${details}`)
}
// XSS
const escapeHtml = (str: string): string => {
if (!str) return ''
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
//
const cleanupMap = () => {
logDebug('开始清理地图资源')
debouncedSearch.cancel()
isSearching.value = false
clearMarkers()
if (placeSearch) {
try {
placeSearch.clear()
placeSearch.destroy()
placeSearch = null
logDebug('搜索服务已销毁')
} catch (e) {
logDebug(`清理搜索服务时出错: ${e instanceof Error ? e.message : String(e)}`)
}
}
if (infoWindow) {
infoWindow.close()
infoWindow = null
logDebug('信息窗口已关闭')
}
clearMarker()
if (map) {
try {
map.destroy()
map = null
logDebug('地图实例已销毁')
} catch (e) {
logDebug(`清理地图时出错: ${e instanceof Error ? e.message : String(e)}`)
}
}
mapLoaded.value = false
isGeocoding.value = false
clearError()
Object.assign(formState, {
lnglat: '',
address: '',
location: null
})
searchForm.keyword = ''
searchList.value = []
selectedPlaceId.value = ''
}
//
const clearMarker = () => {
if (marker) {
try {
map?.remove(marker)
marker = null
logDebug('主标记已移除')
} catch (e) {
logDebug(`清理主标记时出错: ${e instanceof Error ? e.message : String(e)}`)
}
}
}
//
const clearMarkers = () => {
markers.forEach(marker => {
map?.remove(marker)
})
markers = []
logDebug(`已清除 ${markers.length} 个搜索结果标记`)
}
//
const createMarkerContent = (name: string, index: number) => {
const escapedName = escapeHtml(name)
return `
<div style="
background: #1890ff;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
white-space: nowrap;
border: 2px solid white;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
">
${index}. ${escapedName}
</div>
`
}
//
const showLocationInfo = (place: SearchPlace, isSearchResult: boolean = false) => {
if (infoWindow) {
infoWindow.close()
}
const escapedName = escapeHtml(place.name)
const escapedAddress = escapeHtml(place.address || '地址不详')
const content = `
<div style="padding: 8px; max-width: 200px;">
<div style="font-weight: bold; margin-bottom: 4px;">${escapedName}</div>
<div style="color: #666; font-size: 12px;">${escapedAddress}</div>
<div style="color: #999; font-size: 12px; margin-top: 4px;">
${isSearchResult ? '搜索结果' : '选中位置'}
</div>
</div>
`
infoWindow = new AMap.InfoWindow({
content: content,
offset: new AMap.Pixel(0, -30),
position: [place.location.lng, place.location.lat]
})
infoWindow.open(map, [place.location.lng, place.location.lat])
logDebug(`显示位置信息: ${place.name}`)
}
//
const handleSearchComplete = (result: AMap.PlaceSearchResult) => {
isSearching.value = false
logDebug(`搜索完成: ${result.info}, 结果数: ${result.poiList?.pois?.length || 0}`)
if (result.info === 'OK' && result.poiList?.pois?.length > 0) {
const pois = result.poiList.pois
//
searchList.value = pois.map((poi: any) => ({
id: poi.id,
name: poi.name,
address: poi.address,
location: {
lng: poi.location.lng,
lat: poi.location.lat
}
}))
//
clearMarkers()
//
pois.forEach((poi: any, index: number) => {
const position = [poi.location.lng, poi.location.lat]
const poiMarker = new AMap.Marker({
position: position,
map: map,
title: poi.name,
content: createMarkerContent(poi.name, index + 1),
offset: new AMap.Pixel(-15, -42)
})
//
poiMarker.on('click', () => {
const place: SearchPlace = {
id: poi.id,
name: poi.name,
address: poi.address,
location: { lng: poi.location.lng, lat: poi.location.lat }
}
updateSelectedLocation([poi.location.lng, poi.location.lat], place)
showLocationInfo(place, true)
selectedPlaceId.value = poi.id
})
markers.push(poiMarker)
})
//
if (pois.length > 0) {
setTimeout(() => {
const firstPoi = pois[0]
const place: SearchPlace = {
id: firstPoi.id,
name: firstPoi.name,
address: firstPoi.address,
location: { lng: firstPoi.location.lng, lat: firstPoi.location.lat }
}
selectedPlaceId.value = firstPoi.id
updateSelectedLocation([firstPoi.location.lng, firstPoi.location.lat], place)
map?.setCenter([firstPoi.location.lng, firstPoi.location.lat])
map?.setZoom(16)
}, 100)
}
map?.setFitView()
} else {
message.info('未找到相关地点')
searchList.value = []
logDebug('未找到相关地点')
}
}
//
const performSearch = (keyword: string) => {
if (!placeSearch) {
setError('搜索服务未初始化', '请等待地图加载完成后再试')
return
}
if (!mapLoaded.value) {
setError('地图未加载完成', '请等待地图加载完成后再试')
return
}
isSearching.value = true
searchList.value = []
selectedPlaceId.value = ''
logDebug(`执行搜索: ${keyword}`)
try {
placeSearch.clear()
clearMarkers()
placeSearch.search(keyword)
} catch (e) {
isSearching.value = false
const errorMsg = e instanceof Error ? e.message : '搜索执行失败'
setError('搜索执行失败', errorMsg)
message.error('搜索过程中发生错误')
}
}
//
const createPlaceSearch = () => {
if (!map) return
logDebug(`创建PlaceSearch实例城市限制: ${props.searchCity}`)
try {
placeSearch = new AMap.PlaceSearch({
map: map,
pageSize: 10,
city: props.searchCity,
citylimit: props.searchCity !== '全国',
autoFitView: true
})
placeSearch.on('complete', handleSearchComplete)
placeSearch.on('error', (error: any) => {
isSearching.value = false
logDebug(`搜索错误: 代码=${error.errorCode}, 信息=${error.errorMsg}, 状态=${error.status}`)
let userMsg = '搜索服务出错,请重试'
let details = `错误代码: ${error.errorCode}, 信息: ${error.errorMsg}`
switch(error.errorCode) {
case 10001:
userMsg = 'API Key错误或已过期'
details += '\n解决方案: 检查并更新高德地图API Key'
break
case 10002:
userMsg = '没有使用该服务的权限'
details += '\n解决方案: 在高德开放平台为当前Key开启POI搜索权限'
break
case 10003:
userMsg = '请求来源未被授权'
details += '\n解决方案: 将当前域名添加到Key的Referer白名单中'
break
case 10004:
userMsg = '服务调用配额不足'
details += '\n解决方案: 检查服务调用配额或升级服务'
break
case 10005:
userMsg = '服务暂不可用'
details += '\n解决方案: 稍后重试或联系高德技术支持'
break
}
setError(userMsg, details)
message.error(userMsg)
})
} catch (e) {
const errorMsg = e instanceof Error ? e.message : '创建搜索服务失败'
setError('搜索服务初始化失败', errorMsg)
logDebug(`创建搜索服务失败: ${errorMsg}`)
}
}
//
const createGeocoder = () => {
try {
geocoder = new AMap.Geocoder({
radius: 1000,
extensions: 'all',
city: '全国'
})
logDebug('地理编码服务已创建')
} catch (e) {
const errorMsg = e instanceof Error ? e.message : '创建地理编码服务失败'
setError('地理编码服务初始化失败', errorMsg)
logDebug(`创建地理编码服务失败: ${errorMsg}`)
}
}
//
const initPlugins = async (): Promise<void> => {
logDebug('初始化地图插件')
return new Promise((resolve, reject) => {
AMap.plugin(['AMap.PlaceSearch', 'AMap.Geocoder', 'AMap.ToolBar', 'AMap.Scale'], () => {
logDebug('插件加载回调触发')
try {
createPlaceSearch()
createGeocoder()
//
map?.addControl(new AMap.ToolBar())
map?.addControl(new AMap.Scale())
logDebug('插件加载成功')
resolve()
} catch (e) {
const errorMsg = e instanceof Error ? e.message : '插件初始化失败'
logDebug(`插件初始化失败: ${errorMsg}`)
reject(new Error(errorMsg))
}
})
})
}
//
const updateSelectedLocation = async (position: [number, number], place?: SearchPlace) => {
const [lng, lat] = position
formState.lnglat = `${lng.toFixed(6)}, ${lat.toFixed(6)}`
formState.location = { lng, lat }
logDebug(`位置更新: ${formState.lnglat}`)
//
updateMarker(position)
//
if (geocoder) {
isGeocoding.value = true
geocoder.getAddress(position, (status, result) => {
isGeocoding.value = false
if (status === 'complete' && result.regeocode) {
formState.address = result.regeocode.formattedAddress
logDebug(`地址反查成功: ${formState.address}`)
// handleGetLng
const locationData: LocationData = {
lng,
lat,
address: formState.address,
name: place?.name
}
emit('handleGetLng', locationData)
} else if (place?.address) {
formState.address = place.address
logDebug(`使用POI地址: ${formState.address}`)
const locationData: LocationData = {
lng,
lat,
address: place.address,
name: place.name
}
emit('handleGetLng', locationData)
} else {
formState.address = '地址不详'
logDebug('地址反查失败')
const locationData: LocationData = {
lng,
lat,
address: '地址不详'
}
emit('handleGetLng', locationData)
}
})
} else if (place?.address) {
formState.address = place.address
logDebug(`使用POI地址: ${formState.address}`)
const locationData: LocationData = {
lng,
lat,
address: place.address,
name: place.name
}
emit('handleGetLng', locationData)
}
}
//
const updateMarker = (position: [number, number]) => {
if (!map) return
clearMarker()
try {
marker = new AMap.Marker({
position: position,
map: map,
draggable: true,
icon: new AMap.Icon({
size: new AMap.Size(36, 36),
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png',
imageSize: new AMap.Size(36, 36)
})
})
marker.on('dragend', (event) => {
const newPosition = [event.lnglat.lng, event.lnglat.lat] as [number, number]
updateSelectedLocation(newPosition)
})
logDebug(`标记已更新到: ${position.join(', ')}`)
} catch (e) {
const errorMsg = e instanceof Error ? e.message : '创建标记失败'
logDebug(`更新标记失败: ${errorMsg}`)
message.error('更新位置标记失败')
}
}
//
const handleMapClick = (e: any) => {
const lng = e.lnglat.getLng()
const lat = e.lnglat.getLat()
//
clearMarkers()
searchList.value = []
selectedPlaceId.value = ''
updateSelectedLocation([lng, lat])
// 使
if (geocoder) {
geocoder.getAddress([lng, lat], (status, result) => {
if (status === 'complete' && result.regeocode) {
const address = result.regeocode.formattedAddress
//
if (infoWindow) {
infoWindow.close()
}
infoWindow = new AMap.InfoWindow({
content: `
<div style="padding:10px;min-width:200px;">
<div style="font-weight:bold;margin-bottom:5px;">点击位置</div>
<div>${address}</div>
<div style="margin-top:8px;color:#666;">
<div>经度: ${lng.toFixed(6)}</div>
<div>纬度: ${lat.toFixed(6)}</div>
</div>
</div>
`,
offset: new AMap.Pixel(0, -35)
})
infoWindow.open(map, [lng, lat])
}
})
}
}
//
const initMap = async () => {
logDebug('开始初始化地图')
clearError()
if (!mapContainer.value) {
const errorMsg = '地图容器元素未找到'
setError('初始化失败', errorMsg)
console.error(errorMsg)
return
}
try {
loadingText.value = '正在初始化地图...'
//
window._AMapSecurityConfig = {
securityJsCode: 'df197447a4adc77f0cb376a44462272c'
}
// SDK
const AMap = await AMapLoader.load({
key: "38b334d84b1f89aa39d4efae76f0b341",
version: "2.0",
plugins: ['AMap.ToolBar', 'AMap.Scale', 'AMap.Geocoder', 'AMap.PlaceSearch']
})
//
map = new AMap.Map('amap-container', {
zoom: props.defaultZoom,
center: props.defaultCenter,
viewMode: "3D",
resizeEnable: true
})
map.on('complete', () => {
logDebug('地图加载完成')
mapLoaded.value = true
loadingText.value = '地图加载中...'
initPlugins().catch(error => {
console.error('插件初始化失败:', error)
setError('功能初始化失败', error.message)
message.error('地图功能初始化失败: ' + error.message)
})
})
map.on('click', handleMapClick)
//
map.on('error', (error) => {
const errorMsg = error.error || '地图加载错误'
logDebug(`地图错误: ${errorMsg}`)
setError('地图加载错误', errorMsg)
})
} catch (error) {
const errorMsg = error instanceof Error ? error.message : '地图初始化失败'
logDebug(`地图初始化失败: ${errorMsg}`)
setError('地图初始化失败', errorMsg)
message.error('地图初始化失败,请重试')
}
}
//
const handleSearch = (keyword: string) => {
clearError()
if (!keyword?.trim()) {
message.warning('请输入搜索关键词')
debouncedSearch.cancel()
searchList.value = []
selectedPlaceId.value = ''
clearMarkers()
return
}
if (!placeSearch) {
message.warning('搜索服务初始化中,请稍后重试')
logDebug('搜索服务尚未初始化,等待初始化后重试')
initPlugins().then(() => {
handleSearch(keyword)
}).catch(() => {
message.error('搜索功能初始化失败,无法执行搜索')
})
return
}
if (!mapLoaded.value) {
message.warning('地图尚未加载完成,请稍后重试')
return
}
debouncedSearch(keyword.trim())
}
//
const handlePlaceSelect = (placeId: string) => {
const selectedPlace = searchList.value.find(item => item.id === placeId)
if (selectedPlace) {
updateSelectedLocation([selectedPlace.location.lng, selectedPlace.location.lat], selectedPlace)
map?.setCenter([selectedPlace.location.lng, selectedPlace.location.lat])
map?.setZoom(16)
showLocationInfo(selectedPlace, true)
}
}
//
const handleOk = () => {
if (!formState.location) {
message.warning('请先在地图上选择一个位置')
return
}
emit('select', {
lnglat: formState.lnglat,
address: formState.address,
location: formState.location
})
handleCancel()
}
//
const handleCancel = () => {
visible.value = false
emit('update:open', false)
cleanupMap()
}
//
watch(
() => props.open,
(val) => {
logDebug(`弹窗状态变化: ${val}`)
visible.value = val
if (val) {
nextTick(() => {
initMap()
})
} else {
cleanupMap()
}
},
{ immediate: true }
)
watch(visible, (val) => {
if (!val) {
emit('update:open', false)
}
})
//
onUnmounted(() => {
logDebug('组件卸载,清理资源')
cleanupMap()
})
</script>
<style scoped>
:deep(.ant-input-search) {
width: 100%;
}
.search-section {
margin-bottom: 16px;
}
.map-container {
position: relative;
width: 100%;
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
}
.amap-instance {
width: 100%;
height: 100%;
}
.map-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-text {
margin-top: 16px;
color: #666;
font-size: 14px;
}
.debug-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 12px;
padding: 8px;
max-height: 150px;
overflow-y: auto;
z-index: 100;
}
.debug-header {
font-weight: bold;
margin-bottom: 4px;
padding-bottom: 4px;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
:deep(.ant-alert) {
margin-bottom: 12px;
}
</style>

View File

@ -5,7 +5,7 @@
<a-row :gutter="gutter"> <a-row :gutter="gutter">
<a-col v-bind="colSpan"> <a-col v-bind="colSpan">
<a-form-item :label="'所在区域'" name="area"> <a-form-item :label="'所在区域'" name="area">
<a-input :placeholder="'请选择区域'" v-model:value="searchFormData.area"></a-input> <AreaCascader v-model:value="searchFormData.currentNode" @change="onAreaChange" />
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -24,26 +24,30 @@
<a-col v-bind="colSpan"> <a-col v-bind="colSpan">
<a-form-item :label="'等级'" name="level"> <a-form-item :label="'等级'" name="level">
<a-select v-model:value="searchFormData.level" @change="handleChange"> <a-select v-model:value="searchFormData.level" @change="handleChange">
<a-select-option value="jack">已结单</a-select-option> <a-select-option v-for="item in dicsStore.dictOptions.level" :key="item.dval"
<a-select-option value="lucy">已作废</a-select-option> :value="item.dval">
{{ item.introduction }}
</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col v-bind="colSpan"> <!-- <a-col v-bind="colSpan">
<a-form-item :label="'可用钱包'" name="wallet"> <a-form-item :label="'可用钱包'" name="wallet">
<a-select v-model:value="searchFormData.wallet" @change="handleChange"> <a-select v-model:value="searchFormData.wallet" @change="handleChange">
<a-select-option value="jack">已结单</a-select-option> <a-select-option value="jack">已结单</a-select-option>
<a-select-option value="lucy">已作废</a-select-option> <a-select-option value="lucy">已作废</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col> -->
<a-col v-bind="colSpan"> <a-col v-bind="colSpan">
<a-form-item :label="'否中标'" name="bidStatus"> <a-form-item :label="'否中标'" name="bidStatus">
<a-select v-model:value="searchFormData.bidStatus" @change="handleChange"> <a-select v-model:value="searchFormData.bidStatus" @change="handleChange">
<a-select-option value="jack">已结单</a-select-option> <a-select-option v-for="item in dicsStore.dictOptions.WinTheBidding" :key="item.dval"
<a-select-option value="lucy">已作废</a-select-option> :value="item.dval">
{{ item.introduction }}
</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -135,6 +139,8 @@ import EditDialog from './components/EditDialog.vue'
import DeviceManagementModal from './components/AddEquipments.vue' import DeviceManagementModal from './components/AddEquipments.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import AreaCascader from '@/components/AreaCascader/index.vue'
import { useDicsStore } from '@/store'
defineOptions({ defineOptions({
name: 'menu', name: 'menu',
@ -169,6 +175,7 @@ const columns = ref([
const { listData, loading, showLoading, hideLoading, searchFormData, paginationState, resetPagination } = const { listData, loading, showLoading, hideLoading, searchFormData, paginationState, resetPagination } =
usePagination() usePagination()
const { resetForm } = useForm() const { resetForm } = useForm()
const dicsStore = useDicsStore()
getMenuList() getMenuList()

View File

@ -1,15 +1,6 @@
<template> <template>
<a-modal <a-modal :width="800" :open="modal.open" :title="modal.title" :confirm-loading="modal.confirmLoading"
:width="800" :after-close="onAfterClose" :cancel-text="cancelText" :ok-text="okText" @ok="handleOk" @cancel="handleCancel">
:open="modal.open"
:title="modal.title"
:confirm-loading="modal.confirmLoading"
:after-close="onAfterClose"
:cancel-text="cancelText"
:ok-text="okText"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form layout="vertical" ref="formRef" :model="formData" :rules="formRules"> <a-form layout="vertical" ref="formRef" :model="formData" :rules="formRules">
<a-card class="mb-4"> <a-card class="mb-4">
<a-row :gutter="16"> <a-row :gutter="16">
@ -43,12 +34,12 @@
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item :label="'负责人姓名'" name="manager" :required="true"> <a-form-item :label="'负责人姓名'" name="manager" :required="true">
<a-input v-model:value="formData.manager"></a-input> <a-input v-model:value="formData.manager"></a-input>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item :label="'联系电话'" name="phone" :required="true"> <a-form-item :label="'联系电话'" name="phone" :required="true">
<a-input v-model:value="formData.phone" placeholder="请输入联系电话"></a-input> <a-input v-model:value="formData.phone" placeholder="请输入联系电话"></a-input>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
@ -64,7 +55,8 @@
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item :label="'人员编制(个)'" name="staffCount"> <a-form-item :label="'人员编制(个)'" name="staffCount">
<a-input-number v-model:value="formData.staffCount" min="0" style="width: 100%"></a-input-number> <a-input-number v-model:value="formData.staffCount" min="0"
style="width: 100%"></a-input-number>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -75,12 +67,8 @@
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="24"> <a-col :span="24">
<a-form-item :label="'服务中心地址'" name="address"> <a-form-item :label="'服务中心地址'" name="address">
<a-cascader <a-cascader v-model:value="formData.address" :options="addressOptions"
v-model:value="formData.address" placeholder="请选择省/市/区" style="width: 100%"></a-cascader>
:options="addressOptions"
placeholder="请选择省/市/区"
style="width: 100%"
></a-cascader>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="24"> <a-col :span="24">
@ -92,11 +80,7 @@
<a-form-item :label="'地图定位地址'" name="location"> <a-form-item :label="'地图定位地址'" name="location">
<a-row :gutter="8"> <a-row :gutter="8">
<a-col :span="18"> <a-col :span="18">
<a-input <a-input v-model:value="formData.location" placeholder="请选择地图位置" readonly></a-input>
v-model:value="formData.location"
placeholder="请选择地图位置"
readonly
></a-input>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-button type="primary" block @click="openMapSelector">选择地图位置</a-button> <a-button type="primary" block @click="openMapSelector">选择地图位置</a-button>
@ -112,30 +96,20 @@
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item :label="'建成时间'" name="buildTime"> <a-form-item :label="'建成时间'" name="buildTime">
<a-date-picker <a-date-picker v-model:value="formData.buildTime" format="YYYY-MM-DD"
v-model:value="formData.buildTime" placeholder="请选择建成时间"></a-date-picker>
format="YYYY-MM-DD"
placeholder="请选择建成时间"
></a-date-picker>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item :label="'建筑面积(平方米)'" name="area"> <a-form-item :label="'建筑面积(平方米)'" name="area">
<a-input-number <a-input-number v-model:value="formData.area" min="0" style="width: 100%"
v-model:value="formData.area" placeholder="请输入建筑面积"></a-input-number>
min="0"
style="width: 100%"
placeholder="请输入建筑面积"
></a-input-number>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="24"> <a-col :span="24">
<a-form-item :label="'服务介绍'" name="description"> <a-form-item :label="'服务介绍'" name="description">
<a-textarea <a-textarea v-model:value="formData.description" rows="4"
v-model:value="formData.description" placeholder="请输入服务介绍"></a-textarea>
rows="4"
placeholder="请输入服务介绍"
></a-textarea>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -146,16 +120,10 @@
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item :label="'开始营业时间'" name="openTime"> <a-form-item :label="'开始营业时间'" name="openTime">
<a-time-picker <a-time-picker v-model:value="formData.openTime" format="HH:mm"
v-model:value="formData.openTime" placeholder="请选择开始营业时间"></a-time-picker>
format="HH:mm" <a-time-picker v-model:value="formData.closeTime" format="HH:mm"
placeholder="请选择开始营业时间" placeholder="请选择结束营业时间"></a-time-picker>
></a-time-picker>
<a-time-picker
v-model:value="formData.closeTime"
format="HH:mm"
placeholder="请选择结束营业时间"
></a-time-picker>
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -183,14 +151,11 @@
</span> </span>
</template> </template>
<div class="service-tags"> <div class="service-tags">
<a-tag <a-tag v-for="service in allServices" :key="service.value"
v-for="service in allServices"
:key="service.value"
:checked="formData.services?.includes(service.value)" :checked="formData.services?.includes(service.value)"
@click="handleServiceClick(service.value)" @click="handleServiceClick(service.value)"
:class="{'ant-tag-checkable-checked': formData.services?.includes(service.value)}" :class="{ 'ant-tag-checkable-checked': formData.services?.includes(service.value) }"
class="service-tag" class="service-tag">
>
{{ service.label }} {{ service.label }}
</a-tag> </a-tag>
</div> </div>
@ -228,7 +193,8 @@
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item :label="'床位数'" name="bedCount" :required="true"> <a-form-item :label="'床位数'" name="bedCount" :required="true">
<a-input-number v-model:value="formData.bedCount" placeholder="请输入床位数" min="0" style="width: 100%"></a-input-number> <a-input-number v-model:value="formData.bedCount" placeholder="请输入床位数" min="0"
style="width: 100%"></a-input-number>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
@ -244,11 +210,8 @@
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item :label="'资质附件'" name="qualificationFiles"> <a-form-item :label="'资质附件'" name="qualificationFiles">
<a-upload <a-upload list-type="picture-card" v-model:file-list="formData.qualificationFiles"
list-type="picture-card" :before-upload="beforeUpload">
v-model:file-list="formData.qualificationFiles"
:before-upload="beforeUpload"
>
<div v-if="formData.qualificationFiles.length < 5"> <div v-if="formData.qualificationFiles.length < 5">
<plus-outlined /> <plus-outlined />
<div class="ant-upload-text">上传</div> <div class="ant-upload-text">上传</div>
@ -258,11 +221,8 @@
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item :label="'站点图片'" name="siteImages"> <a-form-item :label="'站点图片'" name="siteImages">
<a-upload <a-upload list-type="picture-card" v-model:file-list="formData.siteImages"
list-type="picture-card" :before-upload="beforeUpload">
v-model:file-list="formData.siteImages"
:before-upload="beforeUpload"
>
<div v-if="formData.siteImages.length < 5"> <div v-if="formData.siteImages.length < 5">
<plus-outlined /> <plus-outlined />
<div class="ant-upload-text">上传</div> <div class="ant-upload-text">上传</div>
@ -274,6 +234,17 @@
</a-card> </a-card>
</a-form> </a-form>
<MapPickerModal
:open="showMapPicker"
@update:open="mapVisible = $event"
@handleGetLng="handleLocationChange"
@select="handleLocationSelect" />
<div v-if="selectedLocation">
<p>经纬度: {{ selectedLocation.lnglat }}</p>
<p>地址: {{ selectedLocation.address }}</p>
<p>名称: {{ selectedLocation.placeName }}</p>
</div>
</a-modal> </a-modal>
</template> </template>
@ -288,6 +259,7 @@ import { config } from '@/config'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import MapPickerModal from '@/components/Map/index.vue'
const emit = defineEmits(['ok']) const emit = defineEmits(['ok'])
const { t } = useI18n() // t const { t } = useI18n() // t
const { modal, showModal, hideModal, showLoading, hideLoading } = useModal() const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
@ -404,6 +376,20 @@ const reqType = ['GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'DELETE']
const cancelText = ref(t('button.cancel')) const cancelText = ref(t('button.cancel'))
const okText = ref(t('button.confirm')) const okText = ref(t('button.confirm'))
const platform = ref('') const platform = ref('')
const showMapPicker = ref(false)
const location = ref('')
const handleLocationSelect = (lngLat) => {
console.log('确认选择:', lngLat)
formData.value.location = lngLat.address
showMapPicker.value = false
}
//
const handleLocationChange = (location) => {
console.log('位置变化:', location)
// UI
}
// - // -
const addressOptions = ref([ const addressOptions = ref([
@ -615,9 +601,7 @@ function handleServiceClick(value) {
* 打开地图选择器 * 打开地图选择器
*/ */
function openMapSelector() { function openMapSelector() {
// showMapPicker.value = true
// 使
formData.value.location = '北京市海淀区中关村大街1号 (经纬度: 39.9847, 116.3055)'
} }
/** /**

View File

@ -11,16 +11,18 @@
<a-col v-bind="colSpan"> <a-col v-bind="colSpan">
<a-form-item :label="'站点类型'" name="type"> <a-form-item :label="'站点类型'" name="type">
<a-select v-model:value="searchFormData.type" @change="handleChange"> <a-select v-model:value="searchFormData.type" placeholder="请选择站点类型" allow-clear>
<a-select-option value="jack">已结单</a-select-option> <a-select-option v-for="item in dicsStore.dictOptions.Station_Type" :key="item.dval"
<a-select-option value="lucy">已作废</a-select-option> :value="item.dval">
{{ item.introduction }}
</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col v-bind="colSpan"> <a-col v-bind="colSpan">
<a-form-item :label="'所在区域'" name="area"> <a-form-item :label="'所在区域'" name="area">
<a-input :placeholder="'请选择区域'" v-model:value="searchFormData.area"></a-input> <AreaCascader v-model:value="searchFormData.area" @change="onAreaChange" />
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -187,6 +189,8 @@ import ImportRecordsModal from '@/components/ImportRecord/index.vue'
import ExportRecordsModal from '@/components/ExportRecord/index.vue' import ExportRecordsModal from '@/components/ExportRecord/index.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import AreaCascader from '@/components/AreaCascader/index.vue'
defineOptions({ defineOptions({
name: 'menu', name: 'menu',
@ -220,6 +224,8 @@ const { listData, loading, showLoading, hideLoading, searchFormData, paginationS
usePagination() usePagination()
const { resetForm } = useForm() const { resetForm } = useForm()
const editDialogRef = ref() const editDialogRef = ref()
import { useDicsStore } from '@/store'
const dicsStore = useDicsStore()
getMenuList() getMenuList()