Files
2023-08-01 15:50:13 +08:00

528 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import AMapLoader from "@amap/amap-jsapi-loader";
import { computed, onMounted, ref, shallowRef, toRefs, watch } from "vue";
import { init, registerMap } from "echarts";
import {
countAchievementByArea,
countAchievementByCity,
countAchievementByProvince,
countDemandByArea,
countDemandByCity,
countDemandByProvince,
countEnterpriseByArea,
countEnterpriseByCity,
countEnterpriseByProvince,
countExpertByArea,
countExpertByCity,
countExpertByProvince,
} from "@/api/website/home";
import backBtnPng from "@/assets/images/map_back.png";
import RegionPagine from "@/views/website/home/comp/RegionPagine.vue";
import html2canvas from "html2canvas";
import anime from "animejs";
import { useI18n } from "vue-i18n";
import geoJSONRU from "@/assets/custom.geo.json";
const { t, locale } = useI18n();
const leftBoxPageNum = ref(1);
const rightBoxPageNum = ref(1);
const mapRef = ref(null);
const districtSearch = shallowRef(null);
const myEcharts = shallowRef(null);
const map = shallowRef(null);
const mapData = ref([]);
const props = defineProps({
mapIndex: {
type: Number,
default: 0,
},
});
const { mapIndex } = toRefs(props);
const emit = defineEmits(["changeMapIndex"]);
const methods = [
{
name: "expert",
title: computed(() => t("map.expertMap")),
byProvince: countExpertByProvince,
byCity: countExpertByCity,
byArea: countExpertByArea,
},
{
name: "technology",
// title: "技术分布地图",
title: computed(() => t("map.technologyMap")),
byProvince: countAchievementByProvince,
byCity: countAchievementByCity,
byArea: countAchievementByArea,
},
{
name: "demand",
// title: "需求分布地图",
title: computed(() => t("map.demandMap")),
byProvince: countDemandByProvince,
byCity: countDemandByCity,
byArea: countDemandByArea,
},
{
name: "enterprise",
// title: "企业分布地图",
title: computed(() => t("map.enterpriseMap")),
byProvince: countEnterpriseByProvince,
byCity: countEnterpriseByCity,
byArea: countEnterpriseByArea,
},
];
const areaCount = ref([]); // 按行政区划统计
const industryCount = ref([]); // 按领域统计
const areaCountPaged = computed(() =>
areaCount.value.slice(
(leftBoxPageNum.value - 1) * 5,
leftBoxPageNum.value * 5
)
);
const industryCountPaged = computed(() =>
industryCount.value.slice(
(rightBoxPageNum.value - 1) * 5,
rightBoxPageNum.value * 5
)
);
const options = {
// nameProperty: 'adcode',
visualMap: {
type: "piecewise",
left: "center", //组件离容器左侧的距离,'left', 'center', 'right','20%'
bottom: "30",
orient: "horizontal", //图例排列方向
padding: 5,
pieces: [
{ gte: 0, lte: 99, label: "99", color: "#CAE9FD" },
{ gte: 100, lte: 299, label: "100-299", color: "#7ED2F7" },
{ gte: 300, lte: 499, label: "299-499", color: "#039DDD" },
{ gte: 500, label: "500", color: "#0D4884" },
],
textStyle: {
color: "#fff",
},
visibility: "off",
},
tooltip: {
//提示框信息
trigger: "item",
formatter: "{b}\n{c}人",
},
series: [
{
type: "map",
name: "中国",
map: "map",
data: [],
},
],
};
const loadAMap = async () => {
const AMap = await AMapLoader.load({
key: "377d7c36dd385e2a722f29d4c6e1ffbf", // 申请好的Web端开发者Key首次调用 load 时必填
version: "2.0", // 指定要加载的 JS API 的版本,缺省时默认为 1.4.15
plugins: [
/*"AMap.DistrictSearch", "AMap.DistrictExplorer"*/
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
AMapUI: {
// 是否加载 AMapUI缺省不加载
version: "1.1", // AMapUI 缺省 1.1
plugins: [], // 需要加载的 AMapUI ui插件
},
});
map.value = new AMap.Map("container", {
zoom: 4, //级别
center: [108.946609, 34.262324], //中心点坐标
// viewMode: '3D' //使用3D视图
});
map.value.on("click", mapClick);
// districtSearch.value = new AMap.DistrictSearch({
// level: 'province',
// extensions: 'all',
// subdistrict: 1,
// showbiz: false,
// })
};
const loading = ref(false);
const loadChinaDistrict = (adcode) => {
loading.value = true;
AMapUI.loadUI(["geo/DistrictExplorer"], (DistrictExplorer) => {
const districtExplorer = new DistrictExplorer({
map: map, //关联的地图实例
});
districtExplorer.loadAreaNode(adcode, async (error, areaNode) => {
if (error) {
console.error(error);
loading.value = false;
return;
}
const parentAdcode = areaNode.getAdcode();
// 绘制载入的区划节点
const geoJSON = {
type: "FeatureCollection",
features: areaNode.getSubFeatures(),
};
const level = getLevel(adcode);
let result;
try {
if (level === 0) {
result = await methods[mapIndex.value]["byProvince"]();
} else if (level === 1) {
result = await methods[mapIndex.value]["byCity"](adcode);
} else if (level === 2) {
result = await methods[mapIndex.value]["byArea"](adcode);
}
} catch (e) {
loading.value = false;
return;
}
if (result.industry)
industryCount.value = Object.keys(result.industry).map((key) => ({
name: key,
count: result.industry[key],
}));
areaCount.value = result.count ?? [];
mapData.value = geoJSON.features.map((el) => {
const areaProp = el.properties;
return {
adcode: areaProp.adcode,
name: areaProp.name,
value:
result.count.find((el) => el.code == areaProp.adcode)?.count ?? 0,
};
});
registerMap("map", { geoJSON, specialAreas: {} });
options.series[0].data = mapData.value;
myEcharts.value.setOption(options);
loading.value = false;
});
});
};
const loadRussiaDistrict = () => {
registerMap("map", { geoJSON: geoJSONRU, specialAreas: {} });
myEcharts.value.setOption(options);
// TODO:get count
};
// 返回地图上一级
const backMap = () => {
// loadChinaDistrict(parentAdcode)
loadChinaDistrict("100000");
};
onMounted(async () => {
await loadAMap();
myEcharts.value = init(mapRef.value);
myEcharts.value.on("click", mapClick);
if (locale.value === "zh") {
loadChinaDistrict("100000");
} else {
loadRussiaDistrict();
}
});
/**
* 根据adcode判断省市区级别
* @param adcode
* @return {number} 国家: 0 省: 1 市: 2 区: 3
*/
const getLevel = (adcode) => {
adcode = adcode.toString();
if (adcode === "100000") {
return 0;
}
const splitPattern = /(\d{2})(\d{2})(\d{2})/;
const resultArray = adcode.match(splitPattern).slice(1);
// 国家: 0 省: 1 市: 2 区: 3
let result = resultArray.indexOf("00");
if (result === -1) {
result = 3;
}
return result;
};
const mapClick = (ev) => {
const level = getLevel(ev.data.adcode);
if (level === 3) {
return;
}
loadChinaDistrict(ev.data.adcode);
};
const playScrollAnimation = (direction, start) => {
const pageWrap = document.querySelector(".page-wrap");
const rootWrap = document.querySelector(".root-container");
html2canvas(pageWrap).then((canvas) => {
canvas.addEventListener("wheel", (ev) => {
ev.stopPropagation();
ev.preventDefault();
});
if (direction === "down") {
rootWrap.insertBefore(canvas, pageWrap);
} else if (direction === "up") {
rootWrap.appendChild(canvas);
rootWrap.style.transform = "translateY(-100%)";
}
anime({
targets: rootWrap,
translateY: direction === "down" ? "-100%" : "0",
duration: 300,
easing: "linear",
begin: start,
complete: () => {
rootWrap.removeChild(canvas);
rootWrap.style.transform = "translateY(0%)";
isScrolling.value = false;
},
});
});
};
const isScrolling = ref(false);
const handleScroll = async (ev) => {
let direction = ev.deltaY > 0 ? "down" : "up";
// 判断滚轮滚动方向
if (
(direction === "down" && mapIndex.value >= methods.length - 1) ||
(direction === "up" && mapIndex.value <= 0)
) {
return;
}
ev.preventDefault();
ev.stopPropagation();
if (isScrolling.value) {
return;
}
isScrolling.value = true;
playScrollAnimation(direction, () => {});
if (direction === "down" && mapIndex.value < methods.length - 1) {
emit("changeMapIndex", mapIndex.value + 1);
} else if (direction === "up" && mapIndex.value > 0) {
emit("changeMapIndex", mapIndex.value - 1);
}
};
watch(mapIndex, (newVal, oldVal) => {
console.log(`mapIndex change from ${oldVal} to ${newVal}`);
if (newVal === oldVal) {
return;
}
isScrolling.value = true;
const direction = newVal > oldVal ? "down" : "up";
playScrollAnimation(direction, () => {});
if (locale.value === "zh") {
loadChinaDistrict("100000");
} else {
loadRussiaDistrict();
}
});
watch(locale, (newVal) => {
console.log(newVal);
if (newVal === "zh") {
loadChinaDistrict("100000");
} else if (newVal === "ru") {
loadRussiaDistrict();
}
});
</script>
<template>
<div class="root-wrap">
<div class="root-container">
<div class="page-wrap" @wheel="handleScroll">
<div class="title">{{ methods[mapIndex].title.value }}</div>
<div v-if="loading" class="loading-modal"></div>
<!-- 返回上一级按钮 -->
<div class="back-btn" @click="backMap">
<img :src="backBtnPng" alt="back" />
</div>
<!-- 人数表格 -->
<div class="count-table area">
<div class="table">
<div class="head">
<div class="th">
<div class="title">地区</div>
<div class="count">人数</div>
</div>
</div>
<div class="body">
<div v-for="item in areaCountPaged" :key="item.adcode" class="tr">
<div class="title">{{ item.name }}</div>
<div class="count">{{ item.count }}</div>
</div>
</div>
</div>
<RegionPagine
v-model:page="leftBoxPageNum"
:total="areaCount.length"
/>
</div>
<!-- 领域 -->
<div class="count-table industry">
<div class="table">
<div class="head">
<div class="th">
<div class="title">领域</div>
<div class="count">人数</div>
</div>
</div>
<div class="body">
<div
v-for="item in industryCountPaged"
:key="item.adcode"
class="tr"
>
<div class="title">{{ item.name }}</div>
<div class="count">{{ item.count }}</div>
</div>
</div>
</div>
<RegionPagine
v-model:page="rightBoxPageNum"
:total="industryCount.length"
/>
</div>
<div id="map-container" ref="mapRef"></div>
<div
id="container"
style="
width: 100%;
height: 100%;
background-color: rgb(0, 0, 0);
display: none;
"
></div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.scroll-to-top {
animation: 1s linear infinite page-scroll;
}
.root-wrap {
width: 100%;
height: 100%;
overflow: hidden;
.root-container {
width: 100%;
height: 100%;
> canvas {
transition: all 0.3s linear;
}
.page-wrap {
width: 100%;
height: 100%;
//top: 0;
//left: 0;
position: relative;
//transition: all 0.3s linear;
//animation: 1s linear forwards page-scroll;
.loading-modal {
position: absolute;
left: 0;
top: 0;
background: #f0f2f5;
}
#map-container {
width: 100%;
height: 100%;
background: linear-gradient(0deg, #010101, #041744);
}
> .title {
position: absolute;
top: 75px;
width: 100%;
text-align: center;
font-size: 36px;
font-family: Source Han Sans CN, sans-serif;
font-weight: 300;
color: #ffffff;
z-index: 101;
}
.back-btn {
position: absolute;
left: 120px;
top: 120px;
z-index: 99;
cursor: pointer;
}
.count-table {
position: absolute;
z-index: 101;
&.area {
top: 200px;
left: 120px;
}
&.industry {
top: 200px;
right: 120px;
}
.table {
border: 1px solid #0054ff;
.head,
.body {
.tr,
.th {
display: flex;
color: white;
.title,
.count {
width: 100px;
padding: 8px 12px;
text-align: center;
}
.title {
border-right: 1px solid #0054ff;
}
}
.th {
border-bottom: 1px solid #0054ff;
}
.tr {
color: rgb(161, 192, 255);
}
}
}
.pagination {
display: flex;
}
}
}
}
}
//@keyframes page-scroll {
// 0% {
// transform: translateY(0);
// }
// 100% {
// transform: translateY(-100%);
// }
//}
</style>