<script lang="ts" setup>
|
import type { ScrollActionType } from '@jnpf/ui';
|
|
import { computed, nextTick, reactive, ref, toRefs, unref, watch } from 'vue';
|
|
import { useGlobSetting } from '@jnpf/hooks';
|
import { ScrollContainer } from '@jnpf/ui';
|
import { ModalClose } from '@jnpf/ui/modal';
|
|
import { $t } from '@vben/locales';
|
|
import { globalShareState } from '@vben-core/shared/global-state';
|
|
import AMapLoader from '@amap/amap-jsapi-loader';
|
import { CloseCircleFilled } from '@ant-design/icons-vue';
|
import { Modal as AModal, message } from 'ant-design-vue';
|
|
defineOptions({ inheritAttrs: false, name: 'JnpfLocation' });
|
const props = defineProps({
|
adjustmentScope: { default: 500, type: Number },
|
allowClear: { default: true, type: Boolean },
|
autoLocation: { default: false, type: Boolean },
|
detailed: { default: false, type: Boolean },
|
disabled: { default: false, type: Boolean },
|
enableDesktopLocation: { default: false, type: Boolean },
|
enableLocationScope: { default: false, type: Boolean },
|
locationScope: { default: () => [], type: Array },
|
useAutoLocation: { default: true, type: Boolean },
|
value: { default: '', type: String },
|
});
|
|
const emit = defineEmits(['update:value', 'change']);
|
|
const defaultValue = {
|
fullAddress: '',
|
lat: '',
|
lng: '',
|
name: '',
|
};
|
|
interface State {
|
AMap: any;
|
currentLocation: any;
|
dragLoading: boolean;
|
finish: boolean;
|
innerValue: any;
|
keyword: string;
|
list: any[];
|
listQuery: {
|
currentPage: number;
|
pageSize: number;
|
};
|
loading: boolean;
|
location: any;
|
map: any;
|
selectValue: number;
|
setCenterLoading: boolean;
|
visible: boolean;
|
}
|
|
const state = reactive<State>({
|
AMap: null,
|
currentLocation: {},
|
dragLoading: false,
|
finish: false,
|
innerValue: defaultValue,
|
keyword: '',
|
loading: false,
|
location: {
|
lat: 39.915,
|
lng: 116.404,
|
},
|
map: null,
|
selectValue: -1,
|
setCenterLoading: false,
|
visible: false,
|
list: [],
|
listQuery: {
|
currentPage: 1,
|
pageSize: 50,
|
},
|
});
|
const { innerValue, keyword, loading, selectValue, visible, list, listQuery } = toRefs(state);
|
const infiniteBody = ref<Nullable<ScrollActionType>>(null);
|
const globSetting = useGlobSetting();
|
const { getMapAroundList, getMapTextList } = globalShareState.getApi();
|
|
(window as any)._AMapSecurityConfig = {
|
securityJsCode: import.meta.env.VITE_A_MAP_SECURITY_JS_CODE,
|
};
|
|
watch(
|
() => unref(props.value),
|
(val) => {
|
state.innerValue = val ? JSON.parse(val) : defaultValue;
|
},
|
{ immediate: true },
|
);
|
|
const staticMapUrl = computed(() => {
|
if (!props.enableLocationScope) return ' ';
|
const location = `${state.innerValue.lng},${state.innerValue.lat}`;
|
const url = `${globSetting.apiURL}/api/system/Location/staticmap?location=${location}&zoom=19&size=80*80&key=${import.meta.env.VITE_A_MAP_WEB_KEY}`;
|
return url;
|
});
|
|
function handleLocation() {
|
if (props.disabled) return;
|
state.visible = true;
|
state.selectValue = -1;
|
state.list = [];
|
state.keyword = '';
|
nextTick(() => {
|
initMap();
|
});
|
}
|
async function initMap() {
|
AMapLoader.load({
|
key: import.meta.env.VITE_A_MAP_JS_KEY,
|
plugins: ['AMap.PlaceSearch', 'AMap.Geolocation'],
|
version: '2.0',
|
}).then((AMap) => {
|
state.AMap = AMap;
|
const query: any = {
|
viewMode: '3D',
|
zoom: 16,
|
};
|
if (state.innerValue && state.innerValue.lng && state.innerValue.lat) {
|
state.location = { lat: state.innerValue.lat, lng: state.innerValue.lng };
|
query.center = [state.location.lng, state.location.lat];
|
}
|
state.map = new AMap.Map('container', query);
|
// 获取中心位置
|
handleGetCenter();
|
// 查询附近地点
|
getList();
|
bindScroll();
|
// 添加可选区域圆形
|
handleCircle();
|
// 添加微调区域圆形
|
handleScopeCircle();
|
// 地图事件
|
handleListener();
|
// 添加定位按钮
|
state.map.addControl(new AMap.Geolocation());
|
});
|
}
|
function handleCircle() {
|
if (!props.enableDesktopLocation || props.locationScope.length === 0) return;
|
for (let i = 0; i < props.locationScope.length; i++) {
|
const o: any = props.locationScope[i];
|
if (!o.lng || !o.lat || !o.radius) continue;
|
addCircle({ ...o, fillColor: '#7ad98f' });
|
}
|
}
|
function handleScopeCircle() {
|
if (!props.enableLocationScope) return;
|
state.currentLocation = state.location;
|
addCircle({ ...state.location, fillColor: '#1791fc', radius: props.adjustmentScope || 500 });
|
}
|
function addCircle(o) {
|
const circle = new state.AMap.Circle({
|
bubble: false,
|
center: [o.lng, o.lat],
|
fillColor: o.fillColor,
|
fillOpacity: 0.4,
|
radius: o.radius,
|
strokeOpacity: 0.2,
|
});
|
circle.on('click', handleCenter);
|
state.map.add(circle);
|
}
|
function bindScroll() {
|
const bodyRef = infiniteBody.value;
|
const vBody = bodyRef?.getScrollWrap();
|
vBody?.addEventListener('scroll', () => {
|
if (vBody.scrollHeight - vBody.clientHeight - vBody.scrollTop <= 50 && !state.loading && !state.finish) {
|
state.listQuery.currentPage += 1;
|
getList();
|
}
|
});
|
}
|
function getList() {
|
state.loading = true;
|
const query = {
|
key: import.meta.env.VITE_A_MAP_WEB_KEY,
|
location: `${state.location.lng},${state.location.lat}`,
|
offset: state.listQuery.pageSize,
|
page: state.listQuery.currentPage,
|
radius: -1,
|
};
|
getMapAroundList(query).then((res) => {
|
handleResult(res.data);
|
});
|
}
|
function handleGetCenter() {
|
const { lat, lng } = state.map.getCenter();
|
state.location = { lat, lng };
|
}
|
function handleListener() {
|
state.map.on('moveend', handleMapChange);
|
state.map.on('movestart', handleSetLoading);
|
state.map.on('click', handleCenter);
|
}
|
function handleOffListener() {
|
state.map.off('moveend', handleMapChange);
|
state.map.off('movestart', handleSetLoading);
|
state.map.off('click', handleCenter);
|
}
|
function handleCenter(e) {
|
state.map.setCenter(e.lnglat);
|
}
|
function handleMapChange() {
|
if (state.dragLoading) return (state.dragLoading = false);
|
handleGetCenter();
|
state.selectValue = -1;
|
state.listQuery.currentPage = 1;
|
state.list = [];
|
if (props.enableLocationScope) {
|
const discount = getDiscount(state.currentLocation.lat, state.currentLocation.lng, state.location.lat, state.location.lng) || 0;
|
if (discount > (props.adjustmentScope || 500)) return message.warning('超出微调范围');
|
}
|
getList();
|
}
|
function handleSetLoading() {
|
if (state.setCenterLoading) state.dragLoading = true;
|
}
|
function onSelectValueChange() {
|
state.setCenterLoading = true;
|
nextTick(() => {
|
const location = state.list[state.selectValue]?.location || '';
|
const [lng, lat] = location.split(',');
|
state.location = { lat, lng };
|
state.map.setCenter([lng, lat]);
|
setTimeout(() => {
|
state.setCenterLoading = false;
|
}, 500);
|
});
|
}
|
function handleSearch() {
|
state.loading = true;
|
const query = {
|
key: import.meta.env.VITE_A_MAP_WEB_KEY,
|
keywords: state.keyword,
|
offset: state.listQuery.pageSize,
|
page: state.listQuery.currentPage,
|
radius: props.enableLocationScope ? props.adjustmentScope || 500 : -1,
|
};
|
getMapTextList(query).then((res) => {
|
handleResult(res.data);
|
});
|
}
|
function onSearch() {
|
state.selectValue = -1;
|
state.listQuery.currentPage = 1;
|
state.list = [];
|
state.keyword ? handleSearch() : getList();
|
}
|
function handleResult(res) {
|
state.loading = false;
|
if (res.status == '1') {
|
if (res.pois.length < state.listQuery.pageSize) state.finish = true;
|
state.list = [...state.list, ...res.pois];
|
} else {
|
message.error(res.info || res.infocode);
|
}
|
}
|
function handleSubmit() {
|
if ((!state.selectValue && state.selectValue != 0) || state.selectValue == -1) return message.error('请选择地址');
|
const data = state.list[state.selectValue];
|
const [lng, lat] = data.location.split(',');
|
// 判断微调范围
|
if (props.enableLocationScope) {
|
const discount = getDiscount(state.currentLocation.lat, state.currentLocation.lng, lat, lng) || 0;
|
if (discount > (props.adjustmentScope || 500)) return message.warning('超出微调范围');
|
}
|
// 判断可选范围
|
if (props.enableDesktopLocation && props.locationScope.length > 0) {
|
const list: any[] = [];
|
for (let i = 0; i < props.locationScope.length; i++) {
|
const o: any = props.locationScope[i];
|
const discount = getDiscount(o.lat, o.lng, lat, lng) || 0;
|
list.push(discount > o.radius);
|
}
|
if (list.every((o) => o === true)) return message.warning('超出规定范围');
|
}
|
const address = data.address && data.address.length > 0 ? data.address : '';
|
// 台湾、北京、上海、重庆、深圳地址特殊处理
|
let fullAddress = data.pname + data.cityname + data.adname + address + data.name;
|
if (data.pname == data.cityname) fullAddress = data.pname + data.adname + address + data.name;
|
if (data.pname == data.cityname && data.pname == data.adname) fullAddress = data.pname + address + data.name;
|
state.innerValue = {
|
address,
|
adName: data.adname,
|
cName: data.cityname,
|
fullAddress,
|
lat,
|
lng,
|
name: data.name,
|
pName: data.pname,
|
};
|
emit('update:value', JSON.stringify(state.innerValue));
|
emit('change', JSON.stringify(state.innerValue));
|
handleCancel();
|
}
|
function handleCancel() {
|
state.visible = false;
|
state.map && state.map.destroy();
|
handleOffListener();
|
}
|
function handleClear() {
|
state.innerValue = defaultValue;
|
emit('update:value', '');
|
emit('change', '');
|
}
|
function getDiscount(lat1, lng1, lat2, lng2) {
|
const p1 = new state.AMap.LngLat(lng1, lat1);
|
const p2 = new state.AMap.LngLat(lng2, lat2);
|
return p1.distance(p2) || 0;
|
}
|
function handleClick() {
|
if (props.detailed) return openAMap();
|
if (props.disabled) return;
|
if (props.enableLocationScope) return handleLocation();
|
}
|
function openAMap() {
|
const position = `${state.innerValue.lng},${state.innerValue.lat}`;
|
const url = `http://uri.amap.com/marker?position=${position}&name=${state.innerValue.name}&coordinate=gaode&callnative=0`;
|
window.open(url, '_blank');
|
}
|
/**
|
* 自动定位
|
*/
|
// function handleAutoLocation() {
|
// if (!props.autoLocation || state.innerValue.fullAddress || props.detailed || !props.useAutoLocation) return;
|
// AMapLoader.load({
|
// key: import.meta.env.VITE_A_MAP_JS_KEY,
|
// version: '2.0',
|
// plugins: ['AMap.Geolocation', 'AMap.Geocoder'],
|
// }).then(AMap => {
|
// state.AMap = AMap;
|
// var geolocation = new AMap.Geolocation({
|
// enableHighAccuracy: true,
|
// timeout: 1500,
|
// });
|
// const getAddress = position => {
|
// var geocoder = new AMap.Geocoder();
|
// geocoder.getAddress(position, function (status, res) {
|
// if (status === 'complete' && res.info === 'OK') {
|
// const data = res.regeocode.addressComponent;
|
// state.innerValue = {
|
// pName: data.province,
|
// cName: data.city,
|
// adName: data.district,
|
// address: data.street + data.streetNumber,
|
// name: '',
|
// lng: position[0],
|
// lat: position[1],
|
// fullAddress: res.regeocode.formattedAddress,
|
// };
|
// emit('update:value', JSON.stringify(state.innerValue));
|
// emit('change', JSON.stringify(state.innerValue));
|
// }
|
// });
|
// };
|
// geolocation.getCityInfo((_status, res) => {
|
// if (res.status == 0) {
|
// if (props.enableDesktopLocation && props.locationScope.length) {
|
// let list: any[] = [];
|
// for (let i = 0; i < props.locationScope.length; i++) {
|
// const o: any = props.locationScope[i];
|
// const discount = getDiscount(o.lat, o.lng, res.position[1], res.position[0]) || 0;
|
// list.push(discount > o.radius);
|
// }
|
// if (list.every(o => o === true)) return;
|
// }
|
// getAddress(res.position);
|
// } else {
|
// console.error('定位失败');
|
// }
|
// });
|
// });
|
// }
|
|
// onMounted(() => {
|
// handleAutoLocation();
|
// });
|
</script>
|
|
<template>
|
<div class="jnpf-location">
|
<a-button v-if="!detailed" :disabled="disabled" pre-icon="icon-ym icon-ym-daka" @click="handleLocation()">
|
{{ innerValue.fullAddress ? $t('component.jnpf.location.relocation') : $t('component.jnpf.location.location') }}
|
</a-button>
|
<div v-if="innerValue.fullAddress" class="location-card" @click="handleClick()">
|
<div class="location-card-info">
|
<img v-if="enableLocationScope" :src="staticMapUrl" class="location-card-static-map" />
|
<div class="location-card-address">{{ innerValue.fullAddress }}</div>
|
</div>
|
<CloseCircleFilled v-if="!detailed && !disabled && allowClear" class="location-card-actions" @click.stop="handleClear()" />
|
</div>
|
</div>
|
<AModal
|
v-model:open="visible"
|
:mask-closable="false"
|
:title="$t('component.jnpf.location.modalTitle')"
|
:width="600"
|
class="location-container-modal"
|
destroy-on-close
|
@cancel="handleCancel"
|
@ok="handleSubmit">
|
<template #closeIcon>
|
<ModalClose :can-fullscreen="false" @cancel="handleCancel" />
|
</template>
|
<div class="location-contain">
|
<a-input-search v-model:value="keyword" :placeholder="$t('component.jnpf.location.searchPlaceholder')" allow-clear @search="onSearch" />
|
<div class="map-contain">
|
<div id="container" class="map"></div>
|
<img class="map-marker" draggable="false" src="./mark.png" />
|
</div>
|
<ScrollContainer ref="infiniteBody" class="around-contain" v-loading="loading && listQuery.currentPage == 1">
|
<a-form-item-rest>
|
<a-radio-group v-model:value="selectValue" @change="onSelectValueChange">
|
<a-radio v-for="(item, index) in list" :key="index" :value="index" class="around-contain-item">
|
<div :title="item.name" class="around-item-title">{{ item.name }}</div>
|
<div :title="item.address.length > 0 ? item.address : item.name" class="around-item-sub-title">
|
{{ item.address.length > 0 ? item.address : item.name }}
|
</div>
|
</a-radio>
|
</a-radio-group>
|
</a-form-item-rest>
|
<!-- 请选择允许微调范围内的地点 -->
|
<jnpf-empty v-if="list.length === 0 && !loading" class="!mt-[100px]" />
|
</ScrollContainer>
|
</div>
|
</AModal>
|
</template>
|
<style lang="scss">
|
.location-container-modal {
|
.ant-modal-body {
|
height: 62vh;
|
overflow: hidden;
|
|
.location-contain {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
padding: 20px 20px 0;
|
|
.map-contain {
|
position: relative;
|
|
.map {
|
width: 100%;
|
height: 230px;
|
margin: 10px 0;
|
}
|
|
.map-marker {
|
position: absolute;
|
top: 50%;
|
left: 50%;
|
width: 19px;
|
height: 32px;
|
transform: translate(-50%, calc(-50% - 9.5px));
|
}
|
}
|
|
.around-contain {
|
.ant-radio-group {
|
width: 100%;
|
}
|
|
.around-contain-item {
|
display: flex;
|
align-items: center;
|
width: 100%;
|
height: 60px;
|
padding: 8px;
|
line-height: 22px;
|
border-bottom: 1px solid var(--border-color-base1);
|
|
.around-item-title {
|
width: 512px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.around-item-sub-title {
|
width: 512px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
color: #b9babb;
|
white-space: nowrap;
|
}
|
}
|
}
|
}
|
}
|
}
|
|
.jnpf-location {
|
.location-card {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 12px;
|
margin-top: 8px;
|
cursor: pointer;
|
background: var(--app-main-background);
|
border-radius: 8px;
|
|
.location-card-info {
|
display: flex;
|
flex: 1;
|
align-items: center;
|
|
.location-card-static-map {
|
flex-shrink: 0;
|
width: 48px;
|
height: 48px;
|
margin-right: 4px;
|
}
|
|
.location-card-address {
|
padding: 0 4px;
|
word-break: normal;
|
white-space: normal;
|
}
|
}
|
|
.location-card-actions {
|
flex-shrink: 0;
|
color: rgb(135 143 149);
|
cursor: pointer;
|
}
|
}
|
}
|
</style>
|