فهرست منبع

报表完善和新增样本选择患者信息功能

cynthia-qin 6 روز پیش
والد
کامیت
5ed822640d

+ 2 - 0
package.json

@@ -37,6 +37,8 @@
     "leaflet": "^1.9.4",
     "nprogress": "0.2.0",
     "phylotree": "^2.1.0",
+    "plotly.js": "^3.0.1",
+    "plotly.js-dist-min": "^3.0.1",
     "quill": "2.0.2",
     "screenfull": "5.0.2",
     "sortablejs": "1.10.2",

+ 7 - 0
src/api/sample/sampleInfo.js

@@ -65,3 +65,10 @@ export function getSampleHospitalSelect() {
   })
 }
 
+// 根据身份证号模糊查询患者信息
+export function getPatientByIdCard(idCard) {
+  return request({
+    url: '/sample/samplePatient/loadDataByIdCard/' + idCard,
+    method: 'get',
+  })
+}

+ 18 - 2
src/api/statistics/report.js

@@ -17,9 +17,9 @@ export async function getyblxtjInfo(id) {
 }
 
 // 获取样本统计信息
-export async function getybtjInfo(id) {
+export async function getybtjInfo(type,assemblyAccession) {
     return request({
-    url: '/data/report/ybtj/' + id,
+    url: `/data/report/ybtj/${type}/${assemblyAccession}`,
     method: 'get',
   })
 }
@@ -73,3 +73,19 @@ export function generatezgbytfzzbqkInfo(data) {
     data: data
   })
 }
+
+// 获取诊断统计设置编号列表
+export async function getConfigList() {
+  return request({
+    url: '/system/systemConfig/getAll',
+    method: 'get',
+  })
+}
+
+// 获取诊断统计设置编号对应图表数据
+export async function getConfigData(type, assemblyAccession, configId) {
+  return request({
+    url: `/data/report/yblxtj/${type}/${assemblyAccession}/${configId}`,
+    method: 'get',
+  })
+}

+ 134 - 57
src/views/sample/sampleInfo/index.vue

@@ -37,7 +37,6 @@
         </el-select>
       </el-form-item>
       <el-form-item label="送检医院" prop="sampleHospitalId">
-
         <el-select
           v-model="queryParams.sampleHospitalId"
           placeholder="请选择送检医院"
@@ -50,7 +49,6 @@
           >
           </el-option>
         </el-select>
-
       </el-form-item>
       <el-form-item label="送检科室" prop="sampleDeptId">
         <el-select
@@ -65,7 +63,6 @@
           >
           </el-option>
         </el-select>
-
       </el-form-item>
       <el-form-item label="医生名字" prop="doctorName">
         <el-input
@@ -123,11 +120,7 @@
         >
       </el-col>
       <el-col :span="1.5">
-        <el-button
-          plain
-          size="mini"
-          @click="handleBack"
-        >返回</el-button>
+        <el-button plain size="mini" @click="handleBack">返回</el-button>
       </el-col>
       <right-toolbar
         :showSearch.sync="showSearch"
@@ -141,13 +134,17 @@
       @selection-change="handleSelectionChange"
     >
       <el-table-column type="selection" width="55" align="center" />
-      
+
       <el-table-column label="样本编码" align="center" prop="sampleCode" />
       <el-table-column label="患者名称" align="center" prop="patientName" />
       <el-table-column label="患者电话" align="center" prop="patientPhone" />
 
       <el-table-column label="样本类型" align="center" prop="sampleTypeName" />
-      <el-table-column label="送检医院" align="center" prop="sampleHospitalName" />
+      <el-table-column
+        label="送检医院"
+        align="center"
+        prop="sampleHospitalName"
+      />
       <el-table-column label="送检科室" align="center" prop="sampleDeptName" />
 
       <el-table-column label="医生名字" align="center" prop="doctorName" />
@@ -158,23 +155,14 @@
         class-name="small-padding fixed-width"
       >
         <template slot-scope="scope">
-          <el-button
-            size="mini"
-            type="text"
-            @click="handleUpdate(scope.row)"
+          <el-button size="mini" type="text" @click="handleUpdate(scope.row)"
             >修改</el-button
           >
-          <el-button
-            size="mini"
-            type="text"
-            @click="handleDelete(scope.row)"
+          <el-button size="mini" type="text" @click="handleDelete(scope.row)"
             >删除</el-button
           >
 
-          <el-button
-            size="mini"
-            type="text"
-            @click="gojdzx(scope.row)"
+          <el-button size="mini" type="text" @click="gojdzx(scope.row)"
             >实验解读</el-button
           >
         </template>
@@ -199,7 +187,7 @@
           <el-select
             v-model="form.sampleTypeId"
             placeholder="请选择样本类型"
-            style="width: 100%;"
+            style="width: 100%"
           >
             <el-option
               v-for="item in options"
@@ -212,33 +200,59 @@
         </el-form-item>
         <el-form-item label="送检医院" prop="sampleHospitalId">
           <el-select
-          v-model="form.sampleHospitalId"
-          placeholder="请选择送检医院"
-        >
-          <el-option
-            v-for="item in options3"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
+            v-model="form.sampleHospitalId"
+            placeholder="请选择送检医院"
           >
-          </el-option>
+            <el-option
+              v-for="item in options3"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            >
+            </el-option>
           </el-select>
-
         </el-form-item>
         <el-form-item label="送检科室" prop="sampleDeptId">
-         <el-select
-          v-model="form.sampleDeptId"
-          placeholder="请选择送检"
-        >
-          <el-option
-            v-for="item in options2"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
-          >
-          </el-option>
+          <el-select v-model="form.sampleDeptId" placeholder="请选择送检">
+            <el-option
+              v-for="item in options2"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            >
+            </el-option>
           </el-select>
         </el-form-item>
+        <el-form-item label="患者身份证号" prop="patientIdCard">
+          <el-row type="flex" justify="space-between">
+            <el-col :span="20">
+              <el-select
+                v-model="form.patientIdCard"
+                filterable
+                remote
+                reserve-keyword
+                placeholder="请输入患者身份证号"
+                :remote-method="queryPatientInfo"
+                :loading="patientInfoLoading"
+                @change="handlePatientSelect"
+                style="width: calc(100% - 20px); "
+              >
+                <el-option
+                  v-for="item in patientInfoOptions"
+                  :key="item.id"
+                  :label="item.label"
+                  :value="item.value"
+                >
+                </el-option>
+              </el-select>
+            </el-col>
+            <el-col :span="4" style="text-align: right">
+              <el-button type="primary" size="small" @click="handleAddPatient"
+                >新增患者</el-button
+              >
+            </el-col>
+          </el-row>
+        </el-form-item>
         <el-form-item label="医生名字" prop="doctorName">
           <el-input v-model="form.doctorName" placeholder="请输入医生名字" />
         </el-form-item>
@@ -265,7 +279,10 @@ import {
   delSampleInfo,
   addSampleInfo,
   updateSampleInfo,
-  getSampleTypeSelect,getSampleDeptSelect,getSampleHospitalSelect
+  getSampleTypeSelect,
+  getSampleDeptSelect,
+  getSampleHospitalSelect,
+  getPatientByIdCard,
 } from "@/api/sample/sampleInfo";
 
 export default {
@@ -302,19 +319,26 @@ export default {
         sampleDeptId: null,
         doctorName: null,
       },
-      patientId:null,
+      patientId: null,
       // 表单参数
-      form: {},
+      form: {
+        patientIdCard: "",
+      },
       // 表单校验
       rules: {},
       options: [],
-      options2:[],
-      options3:[]
+      options2: [],
+      options3: [],
+      // 患者信息下拉框选项
+      patientInfoOptions: [],
+      // 患者信息加载状态
+      patientInfoLoading: false,
+      // 防抖定时器
+      patientInfoTimer: null,
     };
   },
   created() {
     this.patientId = this.$route.params && this.$route.params.id;
-    
 
     this.getList();
     this.getSampleTypeOptions();
@@ -322,17 +346,60 @@ export default {
     this.getSampleHospitalOptions();
   },
   methods: {
-    handleBack(){
-      const obj = { path: "/sample/samplePatient"}
+    /** 防抖查询患者信息 */
+    queryPatientInfo(queryString) {
+      if (this.patientInfoTimer) {
+        clearTimeout(this.patientInfoTimer);
+      }
+      this.patientInfoTimer = setTimeout(() => {
+        if (queryString.length > 0) {
+          this.patientInfoLoading = true;
+          getPatientByIdCard(queryString)
+            .then((response) => {
+              if (response.data && response.data.length > 0) {
+                this.patientInfoOptions = response.data.map((item) => ({
+                  value: item.idCard,
+                  label: `${item.idCard}/${item.name}/${item.bahCode}/${item.createTime}`,
+                  id: item.id,
+                }));
+              } else {
+                this.patientInfoOptions = [];
+              }
+            })
+            .catch((error) => {
+              console.error("查询患者信息失败:", error);
+              this.patientInfoOptions = [];
+            })
+            .finally(() => {
+              this.patientInfoLoading = false;
+            });
+        } else {
+          this.patientInfoOptions = [];
+        }
+      }, 500);
+    },
+    /** 选择患者信息 */
+    handlePatientSelect(value) {
+      const selectedItem = this.patientInfoOptions.find(
+        (item) => item.value === value
+      );
+      if (selectedItem && selectedItem.value !== "no-data") {
+        this.form.patientId = selectedItem.id;
+      }
+    },
+    handleBack() {
+      const obj = { path: "/sample/samplePatient" };
       this.$tab.closeOpenPage(obj);
     },
-    gojdzx(row){
-      this.$router.push("/read/sampleExperiment?baseid="+this.patientId+"&id=" + row.id)
+    gojdzx(row) {
+      this.$router.push(
+        "/read/sampleExperiment?baseid=" + this.patientId + "&id=" + row.id
+      );
     },
     /** 查询样本管理列表 */
     getList() {
       this.loading = true;
-      this.queryParams.patientId = this.patientId
+      this.queryParams.patientId = this.patientId;
       listSampleInfo(this.queryParams).then((response) => {
         this.sampleInfoList = response.rows;
         this.total = response.total;
@@ -348,7 +415,7 @@ export default {
         }));
       });
     },
-     // 获取样本类型选项
+    // 获取样本类型选项
     getSampleDeptOptions() {
       getSampleDeptSelect().then((response) => {
         this.options2 = response.data.map((item) => ({
@@ -357,7 +424,7 @@ export default {
         }));
       });
     },
-     // 获取样本类型选项
+    // 获取样本类型选项
     getSampleHospitalOptions() {
       getSampleHospitalSelect().then((response) => {
         this.options3 = response.data.map((item) => ({
@@ -468,6 +535,16 @@ export default {
         `sampleInfo_${new Date().getTime()}.xlsx`
       );
     },
+    /** 新增患者操作 */
+    handleAddPatient() {
+      this.open = false; // 关闭当前对话框
+      this.$router.push( {
+        path:"/sample/samplePatient",
+        query: {
+          openAdd: "1", // 传递一个标识,表示从样本信息页面跳转
+        },
+      });
+    },
   },
 };
 </script>

+ 11 - 0
src/views/sample/samplePatient/index.vue

@@ -556,6 +556,17 @@ export default {
         ...this.queryParams
       }, `samplePatient_${new Date().getTime()}.xlsx`)
     }
+  },
+  watch:{
+    $route:{
+      handler(newVal, oldVal) {
+        // 如果路由变化,重置查询参数
+        if (newVal.query.openAdd === '1') {
+          this.handleAdd()
+        }
+      }
+    }
   }
 }
+
 </script>

+ 187 - 267
src/views/statistics/Spatiotemporal-distribution/index.vue

@@ -11,22 +11,6 @@
             @keyup.enter="fetchAllData"
           />
         </el-form-item>
-        <el-form-item label="亚型">
-          <el-input
-            v-model="form.batch"
-            placeholder="请输入亚型"
-            style="width: 180px"
-            @keyup.enter="fetchAllData"
-          />
-        </el-form-item>
-        <el-form-item label="分支">
-          <el-input
-            v-model="form.model"
-            placeholder="请输入分支"
-            style="width: 180px"
-            @keyup.enter="fetchAllData"
-          />
-        </el-form-item>
         <el-form-item label="时间">
           <el-date-picker
             v-model="form.dateRange"
@@ -38,38 +22,6 @@
             value-format="yyyy-MM-dd"
           />
         </el-form-item>
-        <el-form-item label="地区">
-          <el-input
-            v-model="form.country"
-            placeholder="请输入地区"
-            style="width: 180px"
-            @keyup.enter="fetchAllData"
-          />
-          <!-- <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 label="省份">
-          <el-input
-            v-model="form.province"
-            placeholder="请输入省份"
-            style="width: 180px"
-            @keyup.enter="fetchAllData"
-          />
-          <!-- <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>
@@ -77,30 +29,28 @@
     </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>
-
+    <!-- 地图容器 -->
+    <el-row :gutter="20">
+      <el-col :span="12">
+        <el-card class="map-card">
+          <div slot="header">世界地图</div>
+          <div ref="worldMapContainer" style="width: 100%; height: 600px;"></div>
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card class="map-card">
+          <div slot="header">中国地图</div>
+          <div ref="chinaMapContainer" style="width: 100%; height: 600px;"></div>
+        </el-card>
+      </el-col>
+    </el-row>
   </div>
 </template>
 
@@ -114,42 +64,22 @@ import {getbytzbInfo, getbytzzfbnfo, getsjbytztqkInfo,getzgbytztqkInfo} from '@/
 echarts.registerMap('china', chinaJson);
 echarts.registerMap('world', worldJson);
 
-// 假数据
-const worldPieData = {
-  '中国': [
-    { value: 335, name: '分支 A' },
-    { value: 310, name: '分支 B' },
-    { value: 234, name: '分支 C' }
-  ],
-  '美国': [
-    { value: 200, name: '分支 X' },
-    { value: 150, name: '分支 Y' },
-    { value: 100, name: '分支 Z' }
-  ]
-};
-
-const chinaPieData = {
-  '北京市': [
-    { value: 150, name: '分支 A1' },
-    { value: 120, name: '分支 B1' },
-    { value: 80, name: '分支 C1' }
-  ],
-  '上海市': [
-    { value: 180, name: '分支 A2' },
-    { value: 130, name: '分支 B2' },
-    { value: 90, name: '分支 C2' }
-  ]
-};
-
-// 定义中国省份和世界国家的经纬度
-const chinaGeoCoordMap = {
-  '北京市': [116.46, 39.92],
-  '上海市': [121.48, 31.22]
+// 中国地区名称映射表
+const chinaRegionMap = {
+  '内蒙古自治区': '内蒙古',
+  '新疆维吾尔自治区': '新疆',
+  '西藏自治区': '西藏',
+  '宁夏回族自治区': '宁夏',
+  '广西壮族自治区': '广西',
+  // 可根据实际情况添加更多映射
 };
 
-const worldGeoCoordMap = {
-  '中国': [104.1954, 35.8617],
-  '美国': [-95.7129, 37.0902]
+// 世界地区名称映射表
+const worldRegionMap = {
+  '中国': 'China',
+  '美国': 'United States',
+  '俄罗斯': 'Russia',
+  // 可根据实际情况添加更多映射
 };
 
 export default {
@@ -171,100 +101,44 @@ export default {
       growthData: [],
       barData: [],
       pieData: [],
-      mapData: [],
+      worldMapData: [],
+      chinaMapData: [],
       description: '',
-      isChinaMap: false,
-      growthChart: null,
       barChart: null,
-      pieChart: null,
-      mapChart: null,
-      chinaPieData: [],
-      worldPieData: [],
-      form:{
-  batch: "",
-  bytName: "",
-  country: "",
-  endDate: "",
-  model: "",
-  province: "",
-  startDate: "",
-  dateRange: []
+      worldMapChart: null,
+      chinaMapChart: null,
+      form: {
+        bytName: "",
+        endDate: "",
+        province: "",
+        startDate: "",
+        dateRange: []
       }
     };
   },
   mounted() {
-    // this.initRegionOptions();
-    this.fetchAllData()
-    this.initCharts();
-      // 地图点击切换
-  this.$nextTick(() => {
-    this.mapChart.on('click', params => {
-      console.log('地图点击:', params);
-      if (params && params.name === 'China' && !this.isChinaMap) {
-        this.isChinaMap = true;
-        this.fetchAllData();
-      } else if (params && params.name !== 'China' && this.isChinaMap) {
-        this.isChinaMap = false;
-        this.fetchAllData();
-      }
+    this.fetchAllData();
+    // 延迟初始化,确保容器尺寸正确
+    this.$nextTick(() => {
+      this.initCharts();
     });
-  });
   },
   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;
-      }
-      this.fetchAllData();
-    },
     async fetchAllData() {
       if (this.form.dateRange.length  > 0) {
-      const [startDate, endDate] = this.form.dateRange;
-      this.form.startDate = startDate;
-      this.form.endDate = endDate;
+        const [startDate, endDate] = this.form.dateRange;
+        this.form.startDate = startDate;
+        this.form.endDate = endDate;
       }
 
       // 获取数据
       try {
-        const response = await getbytzbInfo(this.form);
         const response2 = await getbytzzfbnfo(this.form);
         const response3 = await getsjbytztqkInfo(this.form);
         const response4 = await getzgbytztqkInfo(this.form);
-        this.growthData = response.data;
         this.barData = response2.data;
-        this.worldPieData = response3.data;
-        this.chinaPieData = response4.data;
-        this.mapData = this.isChinaMap ? this.chinaPieData : this.worldPieData;
+        this.worldMapData = response3.data;
+        this.chinaMapData = response4.data;
       } catch (error) {
         console.error('获取数据失败:', error);
         this.$message.error('获取数据失败,请稍后重试');
@@ -273,57 +147,32 @@ export default {
       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.mapContainer);
+      this.worldMapChart = echarts.init(this.$refs.worldMapContainer);
+      this.chinaMapChart = echarts.init(this.$refs.chinaMapContainer);
       window.addEventListener('resize', () => {
-        this.growthChart.resize();
         this.barChart.resize();
-        // this.pieChart.resize();
-        this.mapChart.resize();
+        this.worldMapChart.resize();
+        this.chinaMapChart.resize();
       });
     },
     renderCharts() {
-      // 子图一:增长率曲线
-      this.growthChart.setOption({
-        tooltip: {
-          trigger: 'axis',
-          formatter: params => {
-            const p = params[0];
-            return `时间: ${p.name}<br/>数量: ${p.value}`;
-          }
-        },
-        xAxis: {
-          type: 'category',
-          data: this.growthData.map(item => item.day)
-        },
-        yAxis: {
-          type: 'value',
-          name: '增长率'
-        },
-        series: [{
-          data: this.growthData.map(item => ({ value: item.per })),
-          type: 'line',
-          smooth: true
-        }]
-      });
       // 子图二:分区条形图
-      // 提取所有地区名称
       const regions = this.barData.length > 0
         ? Object.keys(this.barData[0]).filter(key => key!== 'day')
         : [];
-      // 提取所有日期
       const dates = this.barData.map(item => item.day);
 
-      // 生成每个地区对应的系列数据
       const seriesData = regions.map(region => ({
         name: region,
         type: 'bar',
-        data: this.barData.map(item => item[region])
+        data: this.barData.map(item => item[region]),
+
       }));
 
       this.barChart.setOption({
+        // 添加动画效果
+        animationDuration: 1000,
         tooltip: {
           trigger: 'axis',
           axisPointer: {
@@ -338,79 +187,150 @@ export default {
         },
         xAxis: {
           type: 'category',
-          // 使用动态日期数据
-          data: dates
+          data: dates,
+           // 优化 x 轴线样式
+          axisLine: {
+            lineStyle: {
+              color: '#999'
+            }
+          },
+          // 优化 x 轴刻度线样式
+          axisTick: {
+            show: false
+          }
         },
         yAxis: {
           type: 'value',
           name: '数量'
         },
-        // 使用动态生成的系列数据
         series: seriesData
       });
-      // // 地图
-      // 处理 mapData 数据,转换为符合 ECharts 要求的格式
-      const processedMapData = this.mapData.map(item => {
-        const [lat, lng] = item.latlng.split(',').map(Number);
+
+      // 处理世界地图数据
+      const processedWorldMapData = this.worldMapData.map(item => {
+        // const name = worldRegionMap[item.area] || item.area;
+        const value = item.num || 0;
+        return {
+          name: item.eng,
+          value,
+          cnName:item.area
+        };
+      });
+      // 处理中国地图数据
+      const processedChinaMapData = this.chinaMapData.map(item => {
+        const name = chinaRegionMap[item.area] || item.area;
+        const value = item.num || 0;
         return {
-          name: item.area,
-          value: [lng, lat, item.num],
-          num: item.num
+          name,
+          value,
         };
       });
-   this.mapChart.setOption({
-  title: { text: '病毒预测分布', left: 'center' },
-  tooltip: {
-    trigger: 'item',
-    formatter: p => `${p.name}<br/>数量: ${p.data ? p.data.num : '-'}`
-  },
-  visualMap: {
-    min: 0,
-    max: 150,
-    left: 'left',
-    top: 'bottom',
-    text: ['高', '低'],
-    calculable: true,
-    inRange: {
-      color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'] // 颜色深浅
-    }
-  },
-  geo: {
-    map: this.isChinaMap ? 'china' : 'world',
-    roam: true,
-    label: { show: false },
-    itemStyle: {
-      areaColor: '#f0f0f0',
-      borderColor: '#999'
-    },
-    emphasis: {
-      itemStyle: {
-        areaColor: '#e0e0e0'
+
+      // 检查数据是否为空
+      if (processedWorldMapData.every(item => item.value === 0)) {
+        console.warn('世界地图数据值全部为 0');
       }
-    }
-  },
-  series: [
-    {
-      type: 'scatter',
-      coordinateSystem: 'geo',
-      data: processedMapData,
-      symbolSize: val => Math.max(8, Math.sqrt(val[2]) * 2), // 数量越大点越大
-      encode: { value: 2 },
-      label: {
-        show: false
-      },
-      itemStyle: {
-        color: '#4575b4'
-      },
-      emphasis: {
-        itemStyle: {
-          color: '#d73027'
-        }
+      if (processedChinaMapData.every(item => item.value === 0)) {
+        console.warn('中国地图数据值全部为 0');
       }
-    }
-  ]
-});
 
+      // 世界地图配置
+      this.worldMapChart.setOption({
+        title: { text: '全球病毒分布', left: 'center' },
+        tooltip: {
+          trigger: 'item',
+          formatter: p => `${p.data?.cnName || p.data?.name}<br/>数量: ${p.value || '-'}`
+        },
+        visualMap: {
+          min: Math.min(...processedWorldMapData.map(item => item.value)),
+          max: Math.max(...processedWorldMapData.map(item => item.value)),
+          left: 'left',
+          top: 'bottom',
+          text: ['高', '低'],
+          calculable: true,
+          inRange: {
+            color: ['#ffeeee', '#ffcccc', '#ff9999', '#ff6666', '#ff3333']
+          }
+        },
+        // geo: {
+        //   map: 'world',
+        //   roam: true,
+        //   label: { show: false },
+        //   itemStyle: {
+        //     areaColor: '#f0f0f0',
+        //     borderColor: '#999'
+        //   },
+        //   emphasis: {
+        //     itemStyle: {
+        //       areaColor: '#e0e0e0'
+        //     }
+        //   }
+        // },
+        series: [
+          {
+            type: 'map',
+            map: 'world',
+            data: processedWorldMapData,
+            label: {
+              show: false
+            },
+            emphasis: {
+              itemStyle: {
+                areaColor: '#e0e0e0'
+              }
+            }
+          }
+        ]
+      });
+
+      // 中国地图配置
+      this.chinaMapChart.setOption({
+        title: { text: '中国病毒分布', left: 'center' },
+        tooltip: {
+          trigger: 'item',
+          formatter: p => `${p.name}<br/>数量: ${p.value || '-'}`
+        },
+        visualMap: {
+          min: Math.min(...processedChinaMapData.map(item => item.value)),
+          max: Math.max(...processedChinaMapData.map(item => item.value)),
+          left: 'left',
+          top: 'bottom',
+          text: ['高', '低'],
+          calculable: true,
+          inRange: {
+            color:['#ffeeee', '#ffcccc', '#ff9999', '#ff6666', '#ff3333']
+          }
+        },
+        // geo: {
+        //   map: 'china',
+        //   roam: true,
+        //   label: { show: false },
+        //   itemStyle: {
+        //     areaColor: '#f0f0f0',
+        //     borderColor: '#999'
+        //   },
+        //   emphasis: {
+        //     itemStyle: {
+        //       areaColor: '#e0e0e0'
+        //     }
+        //   }
+        // },
+        series: [
+          {
+            type: 'map',
+            map: 'china',
+            data: processedChinaMapData,
+            label: {
+              show: false
+            },
+            emphasis: {
+              itemStyle: {
+                areaColor: '#e0e0e0'
+              }
+            }
+          }
+        ]
+      });
     }
   }
 };

+ 157 - 130
src/views/statistics/popularity-prediction/index.vue

@@ -1,176 +1,196 @@
 <template>
   <div class="popularity-prediction">
     <div class="tree-map-container">
-      <div class="phylo-tree">
-        <!-- <h3>病毒进化树</h3> -->
+          <!-- 筛选条件 -->
+    <el-card class="filter-card">
+      <el-form :inline="true" :model="form" size="small">
+        <el-form-item label="病原体名称">
+          <el-input
+            v-model="form.bytName"
+            placeholder="请输入病原体名称"
+            style="width: 180px"
+            @keyup.enter="initData"
+          />
+        </el-form-item>
+        <el-form-item label="时间">
+          <el-date-picker
+            v-model="form.dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            style="width: 240px"
+            value-format="yyyy-MM-dd"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="initData">查询</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+      <!-- <div class="phylo-tree">
         <div id="phyloTree" ref="treeContainer" style="width: 100%; height: 500px;"></div>
-      </div>
-      <div class="world-map">
-        <!-- <h3>世界地图病毒预测图</h3> -->
-        <div id="virusMap" style="width: 100%; height: 500px;"></div>
-      </div>
+      </div> -->
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <div class="world-map">
+            <h3>世界地图病毒预测图</h3>
+            <div id="worldVirusMap" style="width: 100%; height: 500px;"></div>
+          </div>
+        </el-col>
+        <el-col :span="12">
+          <div class="china-map">
+            <h3>中国地图病毒预测图</h3>
+            <div id="chinaVirusMap" style="width: 100%; height: 500px;"></div>
+          </div>
+        </el-col>
+      </el-row>
     </div>
   </div>
 </template>
 
 <script>
 import * as echarts from 'echarts';
-// 引入世界地图数据
 import worldJson from 'echarts/map/json/world.json';
-// 引入中国地图数据
 import chinaJson from 'echarts/map/json/china.json';
 import { generatesjbytfzzbqkInfo, generatezgbytfzzbqkInfo } from '@/api/statistics/report';
 
-// 注册世界地图
 echarts.registerMap('world', worldJson);
-// 注册中国地图
 echarts.registerMap('china', chinaJson);
 
 export default {
   name: 'PopularityPrediction',
   data() {
     return {
-      form: {
-        batch: '',
-        bytName: '',
-        country: '',
-        endDate: '',
-        model: '',
-        province: '',
-        startDate: '',
+        form: {
+        bytName: "",
+        endDate: "",
+        startDate: "",
+        dateRange: []
       },
-      mapData: [],
-      isChina: false, // 是否显示中国地图
       worldData: [],
       chinaData: [],
+      worldMapChart: null,
+      chinaMapChart: null
     };
   },
   methods: {
-    renderVirusMap() {
-      const chart = echarts.init(document.getElementById('virusMap'));
-
-      // 定义病原体颜色映射
-      const pathogenColorMap = {};
-      const colorList = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF', '#800080', '#FFA500', '#808080', '#008000'];
-      let colorIndex = 0;
-
-      // 处理数据,生成散点数据
-      const scatterData = [];
-      const currentData = this.isChina ? this.chinaData : this.worldData;
-      currentData.forEach(item => {
-        const [lat, lng] = item.latlng.split(',').map(str => {
-          const num = parseFloat(str.trim());
-          return isNaN(num) ? 0 : num;
-        });
-        const pathogenData = [];
-        Object.keys(item).filter(key => key!== 'area' && key!== 'latlng').forEach(pathogen => {
-          if (!pathogenColorMap[pathogen]) {
-            pathogenColorMap[pathogen] = colorList[colorIndex % colorList.length];
-            colorIndex++;
+    renderWorldVirusMap() {
+      if (!this.worldMapChart) {
+        this.worldMapChart = echarts.init(document.getElementById('worldVirusMap'));
+      }
+      this.renderMap(this.worldMapChart, this.worldData, 'world');
+    },
+    renderChinaVirusMap() {
+      if (!this.chinaMapChart) {
+        this.chinaMapChart = echarts.init(document.getElementById('chinaVirusMap'));
+      }
+      this.renderMap(this.chinaMapChart, this.chinaData, 'china');
+    },
+    renderMap(chart, data, mapType) {
+      // 处理数据,生成区域数据,保留每个病原体的数量信息
+      const regionData = data.map(item => {
+        const pathogenData = {};
+        let totalValue = 0;
+        Object.keys(item).forEach(key => {
+          if (key!== 'area' && key!== 'eng') {
+            const value = item[key];
+            if (!isNaN(value) && value!== null && value!== undefined) {
+              pathogenData[key] = value;
+              totalValue += value;
+            }
           }
-          pathogenData.push({
-            name: pathogen,
-            value: item[pathogen],
-            color: pathogenColorMap[pathogen]
-          });
         });
 
-        scatterData.push({
-          name: item.area,
-          value: [lng, lat, pathogenData.reduce((sum, data) => sum + data.value, 0)],
+        return {
+          name: item.eng || item.area || '未知区域',
+          value: isNaN(totalValue)? 0 : totalValue,
+          cn: item.area || '未知区域',
           pathogenData: pathogenData
-        });
+        };
       });
 
+      // 校验数据,避免 min 或 max 为 NaN
+      let visualMapMin = 0;
+      let visualMapMax = 0;
+      if (regionData.length > 0) {
+        const values = regionData.map(item => item.value).filter(value =>!isNaN(value));
+        if (values.length > 0) {
+          visualMapMin = Math.min(...values);
+          visualMapMax = Math.max(...values);
+        }
+      }
+
+      // 清空旧的配置
+      chart.clear();
       chart.setOption({
-        title: { text: '病毒预测分布', left: 'center' },
+        title: { text: mapType === 'world' ? '全球病毒预测分布' : '中国病毒预测分布', left: 'center' },
         tooltip: {
           trigger: 'item',
           formatter: (params) => {
-            let tooltipStr = `<div>${params.data.name}</div>`;
-            params.data.pathogenData.forEach(data => {
-              tooltipStr += `<div style="color: ${data.color}">● ${data.name}: ${data.value}</div>`;
+            const name = params.data && params.data.cn? params.data.cn : '未知区域';
+            const value = params.data &&!isNaN(params.data.value)? params.data.value : 0;
+            const pathogenData = params.data && params.data.pathogenData? params.data.pathogenData : {};
+
+            let tooltipStr = `<div>${name}</div><div>总数: ${value}</div>`;
+            Object.keys(pathogenData).forEach(pathogen => {
+              tooltipStr += `<div>${pathogen}: ${pathogenData[pathogen]}</div>`;
             });
             return tooltipStr;
           }
         },
-        legend: {
-          data: Object.keys(pathogenColorMap).map(pathogen => ({
-            name: pathogen,
-            icon: 'circle',
-            textStyle: {
-              color: pathogenColorMap[pathogen]
-            }
-          })),
-          type: 'scroll',
-          orient: 'vertical',
-          right: 10,
-          top: 20,
-          bottom: 20
-        },
-        geo: {
-          map: this.isChina ? 'china' : 'world',
-          roam: true,
-          label: {
-            emphasis: {
-              show: false
-            }
-          },
-          itemStyle: {
-            normal: {
-              areaColor: '#f0f0f0',
-              borderColor: '#999'
-            },
-            emphasis: {
-              areaColor: '#e0e0e0'
-            }
-          },
-          // 设置初始中心和缩放级别
-          center: this.isChina ? [104.1954, 35.8617] : [10, 10],
-          zoom: this.isChina ? 2 : 1.2
-        },
+        // geo: {
+        //   map: mapType,
+        //   roam: true,
+        //   label: {
+        //     emphasis: {
+        //       show: false
+        //     }
+        //   },
+        //   itemStyle: {
+        //     normal: {
+        //       areaColor: '#f0f0f0',
+        //       borderColor: '#999'
+        //     },
+        //     emphasis: {
+        //       areaColor: '#e0e0e0'
+        //     }
+        //   },
+        //   center: mapType === 'china' ? [104.1954, 35.8617] : [10, 10],
+        //   zoom: mapType === 'china' ? 2 : 1.2
+        // },
         series: [
           {
-            type: 'scatter',
-            coordinateSystem: 'geo',
-            data: scatterData,
-            // 根据地图类型调整散点大小
-            symbolSize: val => {
-              const baseSize = Math.sqrt(val[2]) * 2;
-              return this.isChina ? baseSize : baseSize * 0.6; // 世界地图散点缩小为原来的 60%
-            },
+            type: 'map',
+            map: mapType,
+            // coordinateSystem: 'geo',
+            data: regionData,
             label: {
               show: false
             },
             itemStyle: {
-              color: (params) => {
-                // 随机选择一个病原体颜色作为散点颜色
-                const randomIndex = Math.floor(Math.random() * params.data.pathogenData.length);
-                return params.data.pathogenData[randomIndex].color;
-              }
-            },
-            emphasis: {
-              itemStyle: {
+              normal: {
+                areaColor: '#f0f0f0',
+                borderColor: '#999'
+              },
+              emphasis: {
+                areaColor: '#e0e0e0',
                 borderWidth: 2,
                 borderColor: '#fff'
               }
             }
           }
-        ]
-      });
-
-      // 添加地图点击事件监听
-      chart.on('click', params => {
-        if (params.componentType === 'geo') {
-          if (params.name === 'China' && !this.isChina) {
-            this.isChina = true;
-            this.mapData = this.chinaData;
-            this.renderVirusMap();
-          } else if (params.name!== 'China' && this.isChina) {
-            this.isChina = false;
-            this.mapData = this.worldData;
-            this.renderVirusMap();
-          }
+        ],
+        visualMap: {
+          min: visualMapMin,
+          max: visualMapMax,
+          left: 'left',
+          top: 'bottom',
+          text: ['高', '低'],
+          calculable: true,
+          // inRange: {
+          //   color: ['#ffeeee', '#ffcccc', '#ff9999', '#ff6666', '#ff3333']
+          // }
         }
       });
     },
@@ -366,21 +386,28 @@ export default {
       });
     },
     async initData() {
-      const res = await  generatesjbytfzzbqkInfo(this.form)
-      const res2 = await generatezgbytfzzbqkInfo(this.form)
-      this.worldData = res.data
-      this.chinaData = res2.data
-      this.mapData = this.isChina ? this.chinaData : this.worldData
+       if (this.form.dateRange.length  > 0) {
+        const [startDate, endDate] = this.form.dateRange;
+        this.form.startDate = startDate;
+        this.form.endDate = endDate;
+      }
+
+      const res = await generatesjbytfzzbqkInfo(this.form);
+      const res2 = await generatezgbytfzzbqkInfo(this.form);
+      this.worldData = res.data;
+      this.chinaData = res2.data;
+      this.renderWorldVirusMap();
+      this.renderChinaVirusMap();
     }
   },
   created() {
-    this.initData(); // 初始化数据
+    this.initData();
   },
   mounted() {
-    // 延迟 500 毫秒渲染,确保容器尺寸正确
     setTimeout(() => {
-      this.renderVirusMap();
-      this.renderPhyloTree(); // 新增
+      this.renderWorldVirusMap();
+      this.renderChinaVirusMap();
+      // this.renderPhyloTree();
     }, 500);
   }
 };

+ 501 - 127
src/views/statistics/sample-statistics/index.vue

@@ -1,14 +1,14 @@
 <template>
   <div class="sample-statistics">
     <!-- Top: Statistics & Time Filter -->
-    <el-row :gutter="20" class="top-bar">
-      <el-col :span="12">
-        <div class="stat-box">
-          <div class="stat-title">样本统计</div>
-          <!-- <div class="stat-value">{{ totalSamples }}</div> -->
-        </div>
+    <el-row  :gutter="20" class="top-bar">
+      <el-col :span="10">
+       <div style="display: flex;align-items: center;">
+       <span style="display: block;"> 病原体注册号:</span>
+        <el-input style="flex: 1;" v-model="assemblyAccession" placeholder="请输入病原体注册号"></el-input>
+       </div>
       </el-col>
-      <el-col :span="12" class="filter-col">
+      <el-col :span="3" class="filter-col">
         <el-select v-model="selectedRange" placeholder="选择时间范围" @change="onRangeChange" size="small">
           <el-option label="近24小时" value="1"></el-option>
           <el-option label="近3天(72小时)" value="2"></el-option>
@@ -16,6 +16,9 @@
           <el-option label="近30天" value="4"></el-option>
         </el-select>
       </el-col>
+      <el-col :span="6">
+        <el-button type="primary" @click="fetchData">查询</el-button>
+      </el-col>
     </el-row>
 
     <!-- Middle: Line Chart -->
@@ -23,28 +26,37 @@
       <div ref="lineChart" class="line-chart"></div>
     </div>
 
-    <!-- Bottom: Pie Charts -->
-    <el-row :gutter="20" class="pie-row">
-      <el-col :span="12">
-        <div class="pie-title">样本类型占比</div>
-        <div ref="typePieChart" class="pie-chart"></div>
-      </el-col>
-      <el-col :span="12">
-        <div class="pie-title">检出物占比</div>
-        <div ref="detectedPieChart" class="pie-chart"></div>
+    <!-- 动态图表 -->
+     <el-row>
+      <el-col :span="12" class="pie-row">
+        <div style="display: flex;align-items: center;">
+          <div>诊断统计设置编号:</div>
+           <el-select style="flex: 1;" v-model="value" multiple placeholder="请选择">
+    <el-option
+      v-for="item in options"
+      :key="item.value"
+      :label="item.label"
+      :value="item.value">
+    </el-option>
+  </el-select>
+        </div>
       </el-col>
-    </el-row>
+     </el-row>
+   <div class="chart-section dynamic-charts">
+    </div>
   </div>
 </template>
 
 <script>
 import * as echarts from 'echarts';
-import {getjcwzbInfo, getyblxtjInfo, getybtjInfo} from '@/api/statistics/report';
+import * as Plotly from 'plotly.js-dist-min';
+import {getjcwzbInfo, getyblxtjInfo, getybtjInfo,getConfigList,getConfigData} from '@/api/statistics/report';
 export default {
   name: 'SampleStatistics',
   data() {
     return {
       selectedRange: '1',
+      assemblyAccession: '',
       totalSamples: 1234,
       lineChart: null,
       typePieChart: null,
@@ -52,144 +64,500 @@ export default {
       lineData: [],
       typePieData: [],
       detectedPieData: [],
+      value: [],
+      options: [],
+      dynamicChartContainers: [],
+      isGettingConfigList:false // 新增:标记是否正在获取配置列表
     }
   },
   mounted() {
-    this.initCharts()
-    this.fetchData()
+    this.lineChart = echarts.init(this.$refs.lineChart);
+    // this.fetchData()
+  },
+  // 新增监听 value 变化
+  watch: {
+    value: {
+      handler(newValue, oldValue) {
+        // 判断 newValue 和 oldValue 是否相同
+        if (this.isArrayEqual(newValue, oldValue)) {
+          return;
+        }
+        if (newValue.length > 0 && this.assemblyAccession && !this.isGettingConfigList) {
+            this.getChartsByValue();
+        }
+      },
+      // deep: true
+    }
   },
   methods: {
-    onRangeChange() {
-      this.fetchData()
-    },
-   async fetchData() {
-    const response =  await getjcwzbInfo(this.selectedRange)
-     if (response.data ) {
-          let arr = []
-          for(let k in response.data) {
-            arr.push({
-              name: k,
-              value: response.data[k]
-            })
-          }
-          this.detectedPieData = [...arr];
+    // 新增:判断两个数组是否相等的方法
+    isArrayEqual(arr1, arr2) {
+      if (arr1.length !== arr2.length) {
+        return false;
+      }
+      // 对数组进行排序后再比较
+      const sortedArr1 = [...arr1].sort();
+      const sortedArr2 = [...arr2].sort();
+      for (let i = 0; i < sortedArr1.length; i++) {
+        if (sortedArr1[i] !== sortedArr2[i]) {
+          return false;
         }
-   const res = await getyblxtjInfo(this.selectedRange)
-       if (res.data ) {
-          let arr = []
-          for(let k in res.data) {
-            arr.push({
-              name: k,
-              value: res.data[k]
-            })
+      }
+      return true;
+    },
+    async getAllConfigList() {
+      this.isGettingConfigList = true; // 设置标记为正在获取
+      const res = await getConfigList();
+      this.options = res.data.map(item => ({
+        label: item.keyword,
+        value: item.id,
+        ...item
+      }));
+      this.value = res.data
+        .map(item => (item.defaultShow === 'Y' ? item.id : ''))
+        .filter(Boolean);
+      this.getChartsByValue();
+
+    },
+    async getChartsByValue() {
+         this.clearDynamicCharts();
+         const requests = this.value.map(async item => {
+        const optionItem = this.options.find(opt => opt.value === item);
+        if (optionItem) {
+          const res = await getConfigData(
+            this.selectedRange,
+            this.assemblyAccession,
+            item
+          );
+          if (optionItem.isNum === 'Y') {
+            this.renderViolinChart(res.data, optionItem.label);
+          } else {
+            this.renderLineChart(res.data, optionItem.label);
           }
-          this.typePieData = [...arr];
         }
-     const result = await getybtjInfo(this.selectedRange)
-      const analysisData = []
-      const adoptionData = []
-      // 生成日期
-      const xData = []
-      if (result.data ) {
-        result.data.forEach(item => {
-          analysisData.push(item.fxl)
-          adoptionData.push(item.ybl)
-          xData.push(item.hour)
-        })
-      }
-      // 模拟数据,根据selectedRange更新
-      // const now = new Date()
-      // let days = 1
-      // if (this.selectedRange === '2') days = 3
-      // if (this.selectedRange === '3') days = 7
-      // if (this.selectedRange === '4') days = 30
-
-
-
-      // for (let i = days - 1; i >= 0; i--) {
-      //   const d = new Date(now)
-      //   d.setDate(now.getDate() - i)
-      //   xData.push(`${d.getMonth() + 1}-${d.getDate()}`)
-      //   analysisData.push(Math.floor(Math.random() * 100 + 100))
-      //   adoptionData.push(Math.floor(Math.random() * 80 + 50))
-      // }
-      this.lineData = { xData, analysisData, adoptionData }
-
-      // // 饼图数据
-      // this.typePieData = [
-      //   { value: 335, name: '血液' },
-      //   { value: 310, name: '尿液' },
-      //   { value: 234, name: '唾液' },
-      //   { value: 135, name: '其他' }
-      // ]
-      // this.detectedPieData = [
-      //   { value: 400, name: '物质A' },
-      //   { value: 335, name: '物质B' },
-      //   { value: 310, name: '物质C' },
-      //   { value: 234, name: '其他' }
-      // ]
-      // this.totalSamples = analysisData.reduce((a, b) => a + b, 0)
-      this.updateCharts()
+      });
+
+      await Promise.all(requests);
+
+      this.isGettingConfigList = false; // 重置标记为获取完成
     },
-    initCharts() {
-      this.lineChart = echarts.init(this.$refs.lineChart)
-      this.typePieChart = echarts.init(this.$refs.typePieChart)
-      this.detectedPieChart = echarts.init(this.$refs.detectedPieChart)
+    renderViolinChart(data, label) {
+      const chartContainer = document.createElement('div');
+      chartContainer.style.width = '100%';
+      chartContainer.style.height = '600px';
+      chartContainer.id = `violinChart_${label}`;
+      chartContainer.classList.add('dynamic-chart');
+      document.querySelector('.dynamic-charts').appendChild(chartContainer);
+
+      // 新增:将容器添加到数组中
+      this.dynamicChartContainers.push(chartContainer);
+
+      const traces = data.map((item, index) => {
+        let name;
+        if (typeof item.hour === 'number') {
+          name = `${item.hour}小时`;
+        } else {
+          name = item.hour;
+        }
+        return {
+          type: 'violin',
+          y: item.y,
+          name,
+          box: {
+            visible: true,
+            line: {
+              color: '#333',
+              width: 1.5
+            }
+          },
+          meanline: {
+            visible: true,
+            color: '#ff4d4f'
+          },
+          line: {
+            color: `hsl(${index * 360 / data.length}, 70%, 50%)`
+          },
+          fillcolor: `hsla(${index * 360 / data.length}, 70%, 50%, 0.2)`,
+          opacity: 0.8
+        };
+      });
+
+      const layout = {
+        title: {
+          text: `提琴图 - ${label}`,
+          font: {
+            size: 20,
+            color: '#333'
+          },
+          y: 0.95
+        },
+        yaxis: {
+          zeroline: false,
+          title: {
+            text: '数值',
+            font: {
+              size: 16,
+              color: '#666'
+            }
+          },
+          tickfont: {
+            size: 14,
+            color: '#666'
+          }
+        },
+        xaxis: {
+          title: {
+            text: typeof data[0]?.hour === 'number' ? '小时' : '日期',
+            font: {
+              size: 16,
+              color: '#666'
+            }
+          },
+          tickfont: {
+            size: 14,
+            color: '#666'
+          },
+          tickangle: -45
+        },
+        violinmode: 'group',
+        margin: {
+          l: 80,
+          r: 80,
+          b: 120,
+          t: 120,
+          pad: 10
+        },
+        legend: {
+          font: {
+            size: 14,
+            color: '#666'
+          },
+          orientation: 'h',
+          yanchor: 'top',
+          y: -0.2,
+          xanchor: 'center',
+          x: 0.5
+        },
+        plot_bgcolor: '#fff',
+        paper_bgcolor: '#fff'
+      };
+
+      Plotly.newPlot(chartContainer, traces, layout);
     },
-    updateCharts() {
-      // 折线图
-      this.lineChart.setOption({
-        tooltip: { trigger: 'axis' },
-        legend: { data: ['数据分析量', '收样量'] },
-        xAxis: { type: 'category', data: this.lineData.xData },
-        yAxis: { type: 'value', name: '数量' },
+
+    renderLineChart(data, label) {
+      const chartContainer = document.createElement('div');
+      chartContainer.style.width = '100%';
+      chartContainer.style.height = '600px';
+      chartContainer.id = `lineChart_${label}`;
+      chartContainer.classList.add('dynamic-chart');
+      document.querySelector('.dynamic-charts').appendChild(chartContainer);
+
+      // 新增:将容器添加到数组中
+      this.dynamicChartContainers.push(chartContainer);
+
+      const chart = echarts.init(chartContainer);
+      const xData = data.map(item => {
+        if (typeof item.hour === 'number') {
+          return `${item.hour}小时`;
+        }
+        return item.hour;
+      });
+      const xdsData = data.map(item => item.xds);
+      const xdsyxData = data.map(item => item.xdsyx);
+
+      const option = {
+        title: {
+          text: `折线图 - ${label}`,
+          left: 'center',
+          top: '3%',
+          textStyle: {
+            color: '#333',
+            fontSize: 20
+          }
+        },
+        tooltip: {
+          trigger: 'axis',
+          backgroundColor: 'rgba(0, 0, 0, 0.8)',
+          textStyle: {
+            color: '#fff'
+          },
+          axisPointer: {
+            type: 'cross',
+            crossStyle: {
+              color: '#999'
+            }
+          }
+        },
+        legend: {
+          top: '10%',
+          textStyle: {
+            color: '#666',
+            fontSize: 14
+          }
+        },
+        xAxis: {
+          type: 'category',
+          data: xData,
+          axisLabel: {
+            color: '#666',
+            fontSize: 14,
+            rotate: 45
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#666'
+            }
+          },
+          axisTick: {
+            show: false
+          },
+          name: typeof data[0]?.hour === 'number' ? '小时' : '日期'
+        },
+        yAxis: {
+          type: 'value',
+          name: '数值',
+          nameTextStyle: {
+            color: '#666',
+            fontSize: 16
+          },
+          axisLabel: {
+            color: '#666',
+            fontSize: 14
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#666'
+            }
+          },
+          axisTick: {
+            show: false
+          },
+          splitLine: {
+            lineStyle: {
+              color: '#eee'
+            }
+          }
+        },
         series: [
           {
-            name: '数据分析量',
+            name: 'xds',
             type: 'line',
+            data: xdsData,
             smooth: true,
-            data: this.lineData.analysisData
+            lineStyle: {
+              color: '#409EFF',
+              width: 2
+            },
+            symbol: 'circle',
+            symbolSize: 8,
+            itemStyle: {
+              color: '#409EFF'
+            },
+            emphasis: {
+              symbolSize: 12
+            }
           },
           {
-            name: '收样量',
+            name: 'xdsyx',
             type: 'line',
+            data: xdsyxData,
             smooth: true,
-            data: this.lineData.adoptionData
+            lineStyle: {
+              color: '#E6A23C',
+              width: 2
+            },
+            symbol: 'circle',
+            symbolSize: 8,
+            itemStyle: {
+              color: '#E6A23C'
+            },
+            emphasis: {
+              symbolSize: 12
+            }
           }
         ]
-      })
-      // 样本类型饼图
-      this.typePieChart.setOption({
-        tooltip: { trigger: 'item' },
-        legend: { bottom: 0 },
-        series: [
+      };
+
+      chart.setOption(option);
+    },
+
+    onRangeChange() {
+      this.fetchData()
+    },
+    async fetchData() {
+      if (!this.assemblyAccession) {
+        this.$message.error('请输入病原体注册号');
+        return;
+      }
+      this.getAllConfigList();
+      const result = await getybtjInfo(this.selectedRange, this.assemblyAccession);
+      this.processData(result.data);
+      this.updateCharts();
+    },
+    clearDynamicCharts() {
+      this.dynamicChartContainers.forEach(container => {
+        // 移除图表容器
+        container.remove();
+      });
+      // 清空数组
+      this.dynamicChartContainers = [];
+    },
+    processData(data) {
+      const groupedData = {};
+      data.forEach(item => {
+        if (!groupedData[item.hour]) {
+          groupedData[item.hour] = { yxs: 0, yxl: 0 };
+        }
+        groupedData[item.hour].yxs += item.yxs;
+        groupedData[item.hour].yxl += item.yxl;
+      });
+      this.lineData = Object.keys(groupedData).map(hour => ({
+        hour, // 直接使用原始的日期字符串
+        yxs: groupedData[hour].yxs,
+        yxl: groupedData[hour].yxl
+      })).sort((a, b) => new Date(a.hour) - new Date(b.hour)); // 按日期排序
+    },
+    updateCharts() {
+      const xData = this.lineData.map(item => item.hour);
+      const yxsData = this.lineData.map(item => item.yxs);
+      const yxlData = this.lineData.map(item => item.yxl);
+
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'cross',
+            crossStyle: {
+              color: '#999'
+            }
+          },
+          // 格式化提示框内容
+          formatter: function (params) {
+            // let result = `${params[0].name}<br/>`;
+            let result = '';
+            params.forEach(param => {
+              result += `${param.seriesName}: ${param.value}<br/>`;
+            });
+            return result;
+          }
+        },
+        legend: {
+          data: ['阳性占比', '阳性率'],
+          show: true,
+          // 调整图例位置
+          top: '5%'
+        },
+        xAxis: {
+          type: 'category', // 依然使用 category 类型
+          data: xData,
+          axisLabel: {
+            // 格式化日期显示
+            formatter: function (value) {
+              return value;
+            }
+          },
+          // 美化 X 轴轴线
+          axisLine: {
+            lineStyle: {
+              color: '#666'
+            }
+          },
+          // 美化 X 轴刻度线
+          axisTick: {
+            show: false
+          }
+        },
+        yAxis: [
           {
-            name: '样本类型',
-            type: 'pie',
-            radius: '60%',
-            data: this.typePieData,
-            emphasis: {
-              itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' }
+            type: 'value',
+            name: '阳性占比',
+            position: 'left',
+            axisLabel: {
+              formatter: '{value}'
+            },
+            // 美化 Y 轴轴线
+            axisLine: {
+              lineStyle: {
+                color: '#666'
+              }
+            },
+            // 美化 Y 轴刻度线
+            axisTick: {
+              show: false
+            },
+            // 美化 Y 轴网格线
+            splitLine: {
+              lineStyle: {
+                color: '#eee'
+              }
+            }
+          },
+          {
+            type: 'value',
+            name: '阳性率',
+            position: 'right',
+            axisLabel: {
+              formatter: '{value}'
+            },
+            // 美化 Y 轴轴线
+            axisLine: {
+              lineStyle: {
+                color: '#666'
+              }
+            },
+            // 美化 Y 轴刻度线
+            axisTick: {
+              show: false
+            },
+            // 美化 Y 轴网格线
+            splitLine: {
+              lineStyle: {
+                color: '#eee'
+              }
             }
           }
-        ]
-      })
-      // 检出物饼图
-      this.detectedPieChart.setOption({
-        tooltip: { trigger: 'item' },
-        legend: { bottom: 0 },
+        ],
         series: [
           {
-            name: '检出物',
-            type: 'pie',
-            radius: '60%',
-            data: this.detectedPieData,
+            name: '阳性占比',
+            type: 'bar',
+            data: yxsData,
+            // 更换柱状图颜色
+            itemStyle: {
+              color: '#91cc75'
+            },
+            // 调整柱状图宽度
+            barWidth: '40%'
+          },
+          {
+            name: '阳性率',
+            type: 'line',
+            yAxisIndex: 1,
+            data: yxlData,
+            // 让折线更圆滑
+            smooth: true,
+            // 更换折线颜色
+            lineStyle: {
+              color: '#fac858'
+            },
+            // 显示折线数据点
+            symbol: 'circle',
+            // 数据点大小
+            symbolSize: 8,
+            // 数据点颜色
+            itemStyle: {
+              color: '#fac858'
+            },
+            // 鼠标悬停时数据点变大
             emphasis: {
-              itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' }
+              symbolSize: 12
             }
           }
         ]
-      })
+      };
+
+      this.lineChart.setOption(option);
     }
   }
 }
@@ -244,4 +612,10 @@ export default {
   margin-bottom: 8px;
   color: #333;
 }
+.dynamic-chart {
+  margin-bottom: 24px;
+  border: 1px solid #eee;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
 </style>

+ 2 - 2
vue.config.js

@@ -10,8 +10,8 @@ const CompressionPlugin = require('compression-webpack-plugin')
 const name = process.env.VUE_APP_TITLE || '传染病溯源预测系统' // 网页标题
 
 // const baseUrl = '/' // 后端接口
-const baseUrl = 'http://127.0.0.1:8081/' // 后端接口
-// const baseUrl = 'http://173.18.12.205:8081/' // 后端接口
+// const baseUrl = 'http://127.0.0.1:8081/' // 后端接口
+const baseUrl = 'http://173.18.12.205:8081/' // 后端接口
 
 const port = process.env.port || process.env.npm_config_port || 80 // 端口