645 lines
16 KiB
Vue
645 lines
16 KiB
Vue
<template>
|
||
<div class="community-list">
|
||
<div class="table-header">
|
||
<h1>小区列表</h1>
|
||
<a-button type="primary" @click="showAddModal">添加小区</a-button>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="columns"
|
||
:data-source="tableData"
|
||
:pagination="pagination"
|
||
:loading="loading"
|
||
@change="handleTableChange"
|
||
row-key="id"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'status'">
|
||
<a-tag :color="getStatusColor(record.status)">
|
||
{{ getStatusText(record.status) }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-if="column.key === 'location'">
|
||
<a @click="showMap(record)">查看位置</a>
|
||
</template>
|
||
<template v-if="column.key === 'create_time'">
|
||
{{ formatDateTime(record.create_time) }}
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
|
||
<a-modal
|
||
v-model:visible="mapVisible"
|
||
title="小区位置"
|
||
:footer="null"
|
||
width="800px"
|
||
@cancel="closeMap"
|
||
>
|
||
<div id="map-container" style="height: 500px;"></div>
|
||
</a-modal>
|
||
|
||
<a-modal
|
||
v-model:visible="addModalVisible"
|
||
title="添加小区"
|
||
@ok="handleAdd"
|
||
@cancel="handleCancel"
|
||
:confirmLoading="confirmLoading"
|
||
width="680px"
|
||
>
|
||
<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="地址搜索">
|
||
<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>
|
||
</a-form>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { defineComponent, ref, onMounted, nextTick } from 'vue'
|
||
import { message, Tag } from 'ant-design-vue'
|
||
import { getCommunityList, createCommunity } from '@/api/community'
|
||
import { initAMap, createMap } from '@/utils/amap'
|
||
import dayjs from 'dayjs'
|
||
|
||
export default defineComponent({
|
||
components: {
|
||
[Tag.name]: Tag
|
||
},
|
||
setup() {
|
||
const loading = ref(false)
|
||
const tableData = ref([])
|
||
const mapVisible = ref(false)
|
||
const currentMap = ref(null)
|
||
const currentMarker = ref(null)
|
||
|
||
const pagination = ref({
|
||
current: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
showSizeChanger: true,
|
||
showTotal: (total) => `共 ${total} 条记录`
|
||
})
|
||
|
||
// 获取状态显示文本
|
||
const getStatusText = (status) => {
|
||
const statusMap = {
|
||
'OPENING': '运营中',
|
||
'UNOPEN': '未启用'
|
||
}
|
||
return statusMap[status] || status
|
||
}
|
||
|
||
// 获取状态标签颜色
|
||
const getStatusColor = (status) => {
|
||
const colorMap = {
|
||
'OPENING': 'green',
|
||
'UNOPEN': 'orange'
|
||
}
|
||
return colorMap[status] || 'default'
|
||
}
|
||
|
||
// 格式化日期时间
|
||
const formatDateTime = (value) => {
|
||
if (!value) return ''
|
||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
|
||
}
|
||
|
||
const columns = [
|
||
{
|
||
title: 'ID',
|
||
dataIndex: 'id',
|
||
key: 'id',
|
||
width: 80,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '小区名称',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
width: 150,
|
||
},
|
||
{
|
||
title: '地址',
|
||
dataIndex: 'address',
|
||
key: 'address',
|
||
width: 200,
|
||
},
|
||
{
|
||
title: '位置',
|
||
key: 'location',
|
||
width: 100,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
width: 100,
|
||
align: 'center',
|
||
}
|
||
]
|
||
|
||
// 获取小区列表数据
|
||
const fetchData = async () => {
|
||
try {
|
||
loading.value = true
|
||
const params = {
|
||
skip: (pagination.value.current - 1) * pagination.value.pageSize,
|
||
limit: pagination.value.pageSize
|
||
}
|
||
|
||
const res = await getCommunityList(params)
|
||
if (res.code === 200) {
|
||
tableData.value = res.data.items
|
||
pagination.value.total = res.data.total
|
||
} else {
|
||
message.error(res.message || '获取数据失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取小区列表失败:', error)
|
||
message.error('获取数据失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 表格变化处理
|
||
const handleTableChange = (pag) => {
|
||
pagination.value.current = pag.current
|
||
pagination.value.pageSize = pag.pageSize
|
||
fetchData()
|
||
}
|
||
|
||
// 显示地图
|
||
const showMap = async (record) => {
|
||
mapVisible.value = true
|
||
|
||
// 等待 DOM 更新
|
||
await nextTick()
|
||
|
||
try {
|
||
const AMap = await initAMap()
|
||
|
||
// 创建地图实例
|
||
if (!currentMap.value) {
|
||
currentMap.value = new AMap.Map('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({
|
||
position,
|
||
title: record.name
|
||
})
|
||
currentMap.value.add(currentMarker.value)
|
||
|
||
} catch (error) {
|
||
console.error('加载地图失败:', error)
|
||
message.error('加载地图失败')
|
||
}
|
||
}
|
||
|
||
// 关闭地图
|
||
const closeMap = () => {
|
||
mapVisible.value = false
|
||
}
|
||
|
||
// 添加小区相关的响应式变量
|
||
const addModalVisible = ref(false)
|
||
const confirmLoading = ref(false)
|
||
const searchLoading = ref(false)
|
||
const searchAddress = ref('')
|
||
const searchOptions = ref([])
|
||
const addMap = ref(null)
|
||
const addMarker = ref(null)
|
||
const formRef = ref(null)
|
||
|
||
const formState = ref({
|
||
name: '',
|
||
address: '',
|
||
longitude: null,
|
||
latitude: null
|
||
})
|
||
|
||
const rules = {
|
||
name: [{ required: true, message: '请输入小区名称' }],
|
||
address: [{ required: true, message: '请输入详细地址' }],
|
||
longitude: [{ required: true, message: '请选择位置获取经度' }],
|
||
latitude: [{ required: true, message: '请选择位置获取纬度' }],
|
||
}
|
||
|
||
// 显示添加模态框
|
||
const showAddModal = async () => {
|
||
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 {
|
||
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 window.AMap.Geocoder();
|
||
geocoder.getAddress([lng, lat], (status, result) => {
|
||
if (status === 'complete' && result.info === 'OK') {
|
||
const address = result.regeocode;
|
||
formState.value.address = address.formattedAddress;
|
||
// 如果还没有设置小区名称,使用POI信息或地址信息作为默认名称
|
||
if (!formState.value.name && address.pois && address.pois.length > 0) {
|
||
formState.value.name = address.pois[0].name;
|
||
}
|
||
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)
|
||
if (res.code === 200) {
|
||
message.success('添加成功')
|
||
addModalVisible.value = false
|
||
fetchData() // 刷新列表
|
||
} else {
|
||
message.error(res.message || '添加失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('添加小区失败:', error)
|
||
message.error('添加失败')
|
||
} finally {
|
||
confirmLoading.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
// 取消添加
|
||
const handleCancel = () => {
|
||
formRef.value?.resetFields()
|
||
addModalVisible.value = false
|
||
searchAddress.value = ''
|
||
if (addMarker.value) {
|
||
addMarker.value.setMap(null)
|
||
addMarker.value = null
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchData()
|
||
})
|
||
|
||
return {
|
||
loading,
|
||
columns,
|
||
tableData,
|
||
pagination,
|
||
mapVisible,
|
||
handleTableChange,
|
||
showMap,
|
||
closeMap,
|
||
getStatusText,
|
||
getStatusColor,
|
||
formatDateTime,
|
||
addModalVisible,
|
||
confirmLoading,
|
||
searchLoading,
|
||
searchAddress,
|
||
formState,
|
||
formRef,
|
||
rules,
|
||
showAddModal,
|
||
handleAdd,
|
||
handleCancel,
|
||
handleSearch,
|
||
searchOptions,
|
||
handleSelect
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.table-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.table-header h1 {
|
||
margin: 0;
|
||
}
|
||
|
||
:deep(.ant-table-content) {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
:deep(.ant-modal-body) {
|
||
padding: 24px;
|
||
max-width: 100%;
|
||
}
|
||
|
||
:deep(.ant-form) {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
:deep(.ant-form-item) {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:deep(.ant-input),
|
||
:deep(.ant-input-number),
|
||
:deep(.ant-auto-complete) {
|
||
width: 100% !important;
|
||
}
|
||
|
||
:deep(.ant-input-number-input) {
|
||
height: 32px;
|
||
}
|
||
|
||
:deep(.ant-form-item-control-input) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.ant-col) {
|
||
padding-right: 12px;
|
||
padding-left: 12px;
|
||
}
|
||
|
||
:deep(.ant-select-item-option-content) {
|
||
white-space: normal;
|
||
word-break: break-all;
|
||
}
|
||
|
||
:deep(.ant-row) {
|
||
margin-bottom: 0 !important;
|
||
}
|
||
|
||
:deep(.ant-form-item) {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:deep(.ant-col) {
|
||
padding-right: 8px;
|
||
padding-left: 8px;
|
||
}
|
||
|
||
/* 地图区域样式 */
|
||
.map-section {
|
||
margin-bottom: 24px;
|
||
padding: 16px;
|
||
background: #fafafa;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.search-item {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.map-container {
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.ant-form-item-label) {
|
||
text-align: right;
|
||
}
|
||
|
||
:deep(.ant-form-item) {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
:deep(.ant-input),
|
||
:deep(.ant-input-number),
|
||
:deep(.ant-auto-complete) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.ant-input-group) {
|
||
display: flex;
|
||
}
|
||
|
||
:deep(.ant-input-group .ant-input-number) {
|
||
border-radius: 0;
|
||
|
||
&:first-child {
|
||
border-top-left-radius: 2px;
|
||
border-bottom-left-radius: 2px;
|
||
}
|
||
|
||
&:last-child {
|
||
border-top-right-radius: 2px;
|
||
border-bottom-right-radius: 2px;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-form) {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
:deep(.ant-modal-body) {
|
||
padding: 24px;
|
||
}
|
||
|
||
:deep(.ant-modal-footer) {
|
||
text-align: right;
|
||
padding: 16px 24px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
</style> |