|
@@ -0,0 +1,357 @@
|
|
|
|
+<template>
|
|
|
|
+ <div class="spatiotemporal-distribution">
|
|
|
|
+ <!-- 筛选条件 -->
|
|
|
|
+ <el-card class="filter-card">
|
|
|
|
+ <el-form :inline="true" :model="filters" size="small">
|
|
|
|
+ <el-form-item label="疾病名称">
|
|
|
|
+ <el-select
|
|
|
|
+ v-model="filters.disease"
|
|
|
|
+ filterable
|
|
|
|
+ remote
|
|
|
|
+ reserve-keyword
|
|
|
|
+ placeholder="请输入疾病名称"
|
|
|
|
+ :remote-method="searchDisease"
|
|
|
|
+ :loading="diseaseLoading"
|
|
|
|
+ style="width: 180px"
|
|
|
|
+ >
|
|
|
|
+ <el-option
|
|
|
|
+ v-for="item in diseaseOptions"
|
|
|
|
+ :key="item"
|
|
|
|
+ :label="item"
|
|
|
|
+ :value="item"
|
|
|
|
+ />
|
|
|
|
+ </el-select>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item label="亚型">
|
|
|
|
+ <el-select v-model="filters.subtype" placeholder="请选择亚型" style="width: 120px">
|
|
|
|
+ <el-option v-for="item in subtypeOptions" :key="item" :label="item" :value="item" />
|
|
|
|
+ </el-select>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item label="分支">
|
|
|
|
+ <el-select v-model="filters.branch" placeholder="请选择分支" style="width: 120px">
|
|
|
|
+ <el-option v-for="item in branchOptions" :key="item" :label="item" :value="item" />
|
|
|
|
+ </el-select>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item label="时间">
|
|
|
|
+ <el-date-picker
|
|
|
|
+ v-model="filters.dateRange"
|
|
|
|
+ type="daterange"
|
|
|
|
+ range-separator="至"
|
|
|
|
+ start-placeholder="开始日期"
|
|
|
|
+ end-placeholder="结束日期"
|
|
|
|
+ style="width: 240px"
|
|
|
|
+ value-format="yyyy-MM-dd"
|
|
|
|
+ />
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item label="地区">
|
|
|
|
+ <el-cascader
|
|
|
|
+ v-model="filters.region"
|
|
|
|
+ :options="regionOptions"
|
|
|
|
+ :props="{ checkStrictly: true, emitPath: false }"
|
|
|
|
+ placeholder="请选择地区"
|
|
|
|
+ style="width: 200px"
|
|
|
|
+ @change="onRegionChange"
|
|
|
|
+ />
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item>
|
|
|
|
+ <el-button type="primary" @click="fetchAllData">筛选</el-button>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ </el-form>
|
|
|
|
+ </el-card>
|
|
|
|
+
|
|
|
|
+ <div class="charts-container">
|
|
|
|
+ <!-- 子图一:增长率曲线 -->
|
|
|
|
+ <el-card class="chart-card">
|
|
|
|
+ <div slot="header">增长率曲线</div>
|
|
|
|
+ <div ref="growthChart" style="height: 320px;"></div>
|
|
|
|
+ </el-card>
|
|
|
|
+ <!-- 子图二:分区条形图 -->
|
|
|
|
+ <el-card class="chart-card">
|
|
|
|
+ <div slot="header">地区分布条形图</div>
|
|
|
|
+ <div ref="barChart" style="height: 320px;"></div>
|
|
|
|
+ </el-card>
|
|
|
|
+ <!-- 子图三:分支饼图 -->
|
|
|
|
+ <el-card class="chart-card">
|
|
|
|
+ <div slot="header">病原体分支分布饼图</div>
|
|
|
|
+ <div ref="pieChart" style="height: 320px;"></div>
|
|
|
|
+ </el-card>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- 地图 -->
|
|
|
|
+ <el-card class="map-card">
|
|
|
|
+ <div slot="header">全球/中国地图</div>
|
|
|
|
+ <!-- 确保 ref 名称正确 -->
|
|
|
|
+ <div ref="mapContainer" style="width: 100%; height: 600px;"></div>
|
|
|
|
+ </el-card>
|
|
|
|
+
|
|
|
|
+ </div>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script>
|
|
|
|
+import * as echarts from 'echarts';
|
|
|
|
+import AMapLoader from '@amap/amap-jsapi-loader';
|
|
|
|
+
|
|
|
|
+// 引入中国地图
|
|
|
|
+// // 按需引入中国地图和世界地图
|
|
|
|
+// import chinaJson from 'echarts/map/json/china.json';
|
|
|
|
+// import worldJson from 'echarts/map/json/world.json';
|
|
|
|
+
|
|
|
|
+// // 注册地图
|
|
|
|
+// echarts.registerMap('china', chinaJson);
|
|
|
|
+// echarts.registerMap('world', worldJson);
|
|
|
|
+export default {
|
|
|
|
+ name: 'SpatiotemporalDistribution',
|
|
|
|
+ data() {
|
|
|
|
+ return {
|
|
|
|
+ filters: {
|
|
|
|
+ disease: '',
|
|
|
|
+ subtype: '',
|
|
|
|
+ branch: '',
|
|
|
|
+ dateRange: [],
|
|
|
|
+ region: []
|
|
|
|
+ },
|
|
|
|
+ diseaseOptions: [],
|
|
|
|
+ diseaseLoading: false,
|
|
|
|
+ subtypeOptions: [],
|
|
|
|
+ branchOptions: [],
|
|
|
|
+ regionOptions: [],
|
|
|
|
+ growthData: [],
|
|
|
|
+ barData: [],
|
|
|
|
+ pieData: [],
|
|
|
|
+ mapData: [],
|
|
|
|
+ description: '',
|
|
|
|
+ isChinaMap: false
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ mounted() {
|
|
|
|
+ this.initRegionOptions()
|
|
|
|
+ this.initCharts()
|
|
|
|
+ this.initMap()
|
|
|
|
+ },
|
|
|
|
+ methods: {
|
|
|
|
+ async searchDisease(query) {
|
|
|
|
+ if (!query) return
|
|
|
|
+ this.diseaseLoading = true
|
|
|
|
+ // 假数据
|
|
|
|
+ this.diseaseOptions = ['疾病 A', '疾病 B', '疾病 C'].filter(item => item.includes(query))
|
|
|
|
+ this.diseaseLoading = false
|
|
|
|
+ },
|
|
|
|
+ async initRegionOptions() {
|
|
|
|
+ // 假数据
|
|
|
|
+ this.regionOptions = [
|
|
|
|
+ {
|
|
|
|
+ value: '全球',
|
|
|
|
+ label: '全球',
|
|
|
|
+ children: [
|
|
|
|
+ {
|
|
|
|
+ value: '中国',
|
|
|
|
+ label: '中国',
|
|
|
|
+ children: [
|
|
|
|
+ { value: '北京市', label: '北京市' },
|
|
|
|
+ { value: '上海市', label: '上海市' }
|
|
|
|
+ ]
|
|
|
|
+ },
|
|
|
|
+ { value: '美国', label: '美国' }
|
|
|
|
+ ]
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ },
|
|
|
|
+ async onRegionChange(val) {
|
|
|
|
+ // 切换地图模式
|
|
|
|
+ if (val && val.length && val[val.length - 1] === '中国') {
|
|
|
|
+ this.isChinaMap = true
|
|
|
|
+ } else {
|
|
|
|
+ this.isChinaMap = false
|
|
|
|
+ }
|
|
|
|
+ // 可根据地区动态加载下一级选项
|
|
|
|
+ },
|
|
|
|
+ async fetchAllData() {
|
|
|
|
+ // 联动下拉框
|
|
|
|
+ if (this.filters.disease) {
|
|
|
|
+ // 假数据
|
|
|
|
+ this.subtypeOptions = ['亚型 1', '亚型 2', '亚型 3']
|
|
|
|
+ }
|
|
|
|
+ if (this.filters.subtype) {
|
|
|
|
+ // 假数据
|
|
|
|
+ this.branchOptions = ['分支 A', '分支 B', '分支 C']
|
|
|
|
+ }
|
|
|
|
+ // 获取假数据
|
|
|
|
+ this.growthData = [
|
|
|
|
+ { date: '2025-06-01', value: 10, ratio: 10, count: 5 },
|
|
|
|
+ { date: '2025-06-02', value: 20, ratio: 20, count: 8 }
|
|
|
|
+ ]
|
|
|
|
+ this.barData = [
|
|
|
|
+ { region: '中国', count: 100, percent: 60 },
|
|
|
|
+ { region: '美国', count: 80, percent: 40 }
|
|
|
|
+ ]
|
|
|
|
+ this.pieData = [
|
|
|
|
+ { value: 335, name: '分支 A' },
|
|
|
|
+ { value: 310, name: '分支 B' },
|
|
|
|
+ { value: 234, name: '分支 C' }
|
|
|
|
+ ]
|
|
|
|
+ this.mapData = [
|
|
|
|
+ { name: '中国', value: 100 },
|
|
|
|
+ { name: '美国', value: 80 }
|
|
|
|
+ ]
|
|
|
|
+ this.description = '这是一段假的数据分析文本。'
|
|
|
|
+ this.renderCharts()
|
|
|
|
+ },
|
|
|
|
+ initCharts() {
|
|
|
|
+ this.growthChart = echarts.init(this.$refs.growthChart)
|
|
|
|
+ this.barChart = echarts.init(this.$refs.barChart)
|
|
|
|
+ this.pieChart = echarts.init(this.$refs.pieChart)
|
|
|
|
+ // this.mapChart = echarts.init(this.$refs.mapChart)
|
|
|
|
+ window.addEventListener('resize', () => {
|
|
|
|
+ this.growthChart.resize()
|
|
|
|
+ this.barChart.resize()
|
|
|
|
+ this.pieChart.resize()
|
|
|
|
+ // this.mapChart.resize()
|
|
|
|
+ })
|
|
|
|
+ },
|
|
|
|
+ renderCharts() {
|
|
|
|
+ // 子图一:增长率曲线
|
|
|
|
+ this.growthChart.setOption({
|
|
|
|
+ tooltip: {
|
|
|
|
+ trigger: 'axis',
|
|
|
|
+ formatter: params => {
|
|
|
|
+ const p = params[0]
|
|
|
|
+ return `时间: ${p.name}<br/>累计占比: ${p.data.ratio}%<br/>分支数: ${p.data.count}`
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ xAxis: { type: 'category', data: this.growthData.map(i => i.date) },
|
|
|
|
+ yAxis: { type: 'value' },
|
|
|
|
+ series: [{
|
|
|
|
+ data: this.growthData.map(i => ({ value: i.value, ratio: i.ratio, count: i.count })),
|
|
|
|
+ type: 'line',
|
|
|
|
+ smooth: true
|
|
|
|
+ }]
|
|
|
|
+ })
|
|
|
|
+ // 子图二:分区条形图
|
|
|
|
+ this.barChart.setOption({
|
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
|
+ legend: { data: ['数量', '百分比'] },
|
|
|
|
+ xAxis: { type: 'category', data: this.barData.map(i => i.region) },
|
|
|
|
+ yAxis: [{ type: 'value', name: '数量' }, { type: 'value', name: '百分比', max: 100 }],
|
|
|
|
+ series: [
|
|
|
|
+ {
|
|
|
|
+ name: '数量',
|
|
|
|
+ type: 'bar',
|
|
|
|
+ data: this.barData.map(i => i.count)
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ name: '百分比',
|
|
|
|
+ type: 'bar',
|
|
|
|
+ yAxisIndex: 1,
|
|
|
|
+ data: this.barData.map(i => i.percent)
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ })
|
|
|
|
+ // 子图三:分支饼图
|
|
|
|
+ this.pieChart.setOption({
|
|
|
|
+ tooltip: {
|
|
|
|
+ trigger: 'item',
|
|
|
|
+ formatter: '{b}: {c} ({d}%)'
|
|
|
|
+ },
|
|
|
|
+ series: [{
|
|
|
|
+ type: 'pie',
|
|
|
|
+ radius: '60%',
|
|
|
|
+ data: this.pieData
|
|
|
|
+ }]
|
|
|
|
+ })
|
|
|
|
+ // // 地图
|
|
|
|
+ // this.mapChart.setOption({
|
|
|
|
+ // tooltip: {
|
|
|
|
+ // trigger: 'item',
|
|
|
|
+ // formatter: p => `${p.name}<br/>分支数: ${p.value || 0}`
|
|
|
|
+ // },
|
|
|
|
+ // series: [{
|
|
|
|
+ // type: 'map',
|
|
|
|
+ // map: this.isChinaMap ? 'china' : 'world',
|
|
|
|
+ // roam: true,
|
|
|
|
+ // data: this.mapData
|
|
|
|
+ // }]
|
|
|
|
+ // })
|
|
|
|
+ // // 地图点击切换
|
|
|
|
+ // this.mapChart.off('click')
|
|
|
|
+ // this.mapChart.on('click', params => {
|
|
|
|
+ // if (!this.isChinaMap && params.name === '中国') {
|
|
|
|
+ // this.isChinaMap = true
|
|
|
|
+ // this.fetchAllData()
|
|
|
|
+ // }
|
|
|
|
+ // })
|
|
|
|
+ },
|
|
|
|
+ async initMap() {
|
|
|
|
+ try {
|
|
|
|
+ // 检查 ref 是否正确获取到 DOM 元素
|
|
|
|
+ if (!this.$refs.mapContainer) {
|
|
|
|
+ console.error('未找到地图容器 DOM 元素');
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ const AMap = await AMapLoader.load({
|
|
|
|
+ key: 'ddcbab53e51ba9adfdcf281a32d513f0', // 替换为你的高德 Key
|
|
|
|
+ version: '2.0',
|
|
|
|
+ plugins: []
|
|
|
|
+ });
|
|
|
|
+ this.map = new AMap.Map(this.$refs.mapContainer, {
|
|
|
|
+ zoom: 4, // 初始缩放级别
|
|
|
|
+ center: [105.403119, 38.028658] // 初始地图中心点
|
|
|
|
+ });
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('地图初始化失败:', error);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ // async fetchAllData() {
|
|
|
|
+ // // ... 已有代码 ...
|
|
|
|
+ // this.updateMap();
|
|
|
|
+ // },
|
|
|
|
+ updateMap() {
|
|
|
|
+ // 清空现有覆盖物
|
|
|
|
+ this.map.clearMap();
|
|
|
|
+ this.mapData.forEach(item => {
|
|
|
|
+ const marker = new AMap.Marker({
|
|
|
|
+ position: this.getCoordinates(item.name), // 根据地区名称获取经纬度
|
|
|
|
+ title: `${item.name}: ${item.value} 分支数`
|
|
|
|
+ });
|
|
|
|
+ marker.setMap(this.map);
|
|
|
|
+ });
|
|
|
|
+ },
|
|
|
|
+ getCoordinates(regionName) {
|
|
|
|
+ // 简单示例,实际需要根据地区名称获取准确经纬度
|
|
|
|
+ const coordinatesMap = {
|
|
|
|
+ '中国': [105.403119, 38.028658],
|
|
|
|
+ '美国': [-95.712891, 37.09024]
|
|
|
|
+ };
|
|
|
|
+ return coordinatesMap[regionName] || [0, 0];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+};
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style scoped>
|
|
|
|
+.spatiotemporal-distribution {
|
|
|
|
+ padding: 16px;
|
|
|
|
+}
|
|
|
|
+.filter-card {
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+}
|
|
|
|
+.charts-container {
|
|
|
|
+ display: flex;
|
|
|
|
+ gap: 16px;
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+}
|
|
|
|
+.chart-card {
|
|
|
|
+ flex: 1;
|
|
|
|
+ min-width: 0;
|
|
|
|
+}
|
|
|
|
+.map-card {
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+}
|
|
|
|
+.desc-card {
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+}
|
|
|
|
+.desc-text {
|
|
|
|
+ font-size: 15px;
|
|
|
|
+ line-height: 1.8;
|
|
|
|
+ color: #333;
|
|
|
|
+}
|
|
|
|
+</style>
|