123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689 |
- <template>
- <div class="sample-statistics">
- <!-- Top: Statistics & Time Filter -->
- <el-row :gutter="20" class="top-bar">
- <el-col :span="10">
- <div style="display: flex;align-items: center;">
- <span style="display: block;">NCBI基因组序列编号:</span>
- <!-- <el-input style="flex: 1;" v-model="assemblyAccession" placeholder="请输入病原体注册号"></el-input> -->
- <el-select
- v-model="assemblyAccession"
- filterable
- remote
- reserve-keyword
- placeholder="请输入NCBI基因组序列编号"
- :remote-method="queryInfo"
- :loading="loading"
- @change="handleSelect"
- style="flex:1"
- >
- <el-option
- v-for="item in Assoptions"
- :key="item.id"
- :label="item.assemblyAccession"
- :value="item.assemblyAccession"
- >
- </el-option>
- </el-select>
- </div>
- </el-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>
- <el-option label="近7天" value="3"></el-option>
- <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 -->
- <div class="chart-section">
- <div ref="lineChart" class="line-chart"></div>
- </div>
- <!-- 动态图表 -->
- <el-row>
- <el-col :span="24" class="pie-row">
- <div style="display: flex;align-items: center;">
- <div>诊断统计设置:</div>
- <el-select style="flex: 1;width:100%" 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>
- <div class="chart-section dynamic-charts">
- </div>
- </div>
- </template>
- <script>
- import * as echarts from 'echarts';
- import * as Plotly from 'plotly.js-dist-min';
- import {getjcwzbInfo, getyblxtjInfo, getybtjInfo,getConfigList,getConfigData,getAssemblyAccessionList} from '@/api/statistics/report';
- export default {
- name: 'SampleStatistics',
- data() {
- return {
- selectedRange: '1',
- assemblyAccession: '',
- totalSamples: 1234,
- lineChart: null,
- typePieChart: null,
- detectedPieChart: null,
- lineData: [],
- typePieData: [],
- detectedPieData: [],
- value: [],
- options: [],
- dynamicChartContainers: [],
- isGettingConfigList:false, // 新增:标记是否正在获取配置列表
- loading:false,
- Assoptions:[],
- ASSTimer:null,
- }
- },
- mounted() {
- 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: {
- // 搜索
- queryInfo(queryString){
- if (this.ASSTimer) {
- clearTimeout(this.ASSTimer);
- }
- this.ASSTimer = setTimeout(() => {
- if (queryString.length > 0){
- this.loading = true
- getAssemblyAccessionList(queryString).then(res => {
- console.log('查询基因组序列编号',res)
- if (res.data && res.data.length > 0) {
- this.Assoptions = res.data
- }
- this.loading = false
- }).finally(() => {
- this.loading = false
- })
- }
- }, 500);
- },
- handleSelect(){
- if (this.assemblyAccession) {
- this.fetchData();
- }
- },
- // 新增:判断两个数组是否相等的方法
- 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;
- }
- }
- return true;
- },
- async getAllConfigList() {
- this.isGettingConfigList = true; // 设置标记为正在获取
- const res = await getConfigList();
- this.options = res.data.map(item => ({
- label: item.title,
- 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
- );
- console.log(optionItem,'optionItem.label')
- if (optionItem.isNum === 'Y') {
- this.renderViolinChart(res.data, optionItem.title);
- } else {
- this.renderLineChart(res.data, optionItem.title);
- }
- }
- });
- await Promise.all(requests);
- this.isGettingConfigList = false; // 重置标记为获取完成
- },
- renderViolinChart(data, label) {
- const chartContainer = document.createElement('div');
- chartContainer.style.width = '100%';
- chartContainer.style.height = '800px';
- 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) => {
- console.log('item', item);
- const xValue = item.hour; // 直接使用 item.hour
- return {
- type: 'violin',
- x: [xValue],
- y: item.y,
- name: xValue,
- 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
- };
- });
- // 提取所有 x 轴标签
- const xLabels = data.map((item) => item.hour);
- 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: 12,
- color: '#666'
- },
- type: 'linear',
- automargin: true
- },
- xaxis: {
- title: {
- text: '日期',
- font: {
- size: 16,
- color: '#666'
- }
- },
- tickmode: 'array',
- tickvals: xLabels,
- ticktext: xLabels,
- tickfont: {
- size: 12,
- color: '#666'
- },
- tickangle: -45,
- automargin: true
- },
- violinmode: 'group',
- margin: {
- l: 80,
- r: 80,
- b: 80,
- t: 120,
- pad: 10
- },
- legend: {
- font: {
- size: 12,
- color: '#666'
- },
- orientation: 'h',
- yanchor: 'top',
- y: -0.3,
- xanchor: 'center',
- x: 0.5
- },
- plot_bgcolor: '#fff',
- paper_bgcolor: '#fff',
- dragmode: false,
- };
- // 直接隐藏工具栏
- const config = {
- displayModeBar: false
- };
- Plotly.newPlot(chartContainer, traces, layout,config);
- },
- 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 = {
- // 添加 grid 配置项,设置底部边距
- grid: {
- left: '3%',
- right: '4%',
- bottom: '15%', // 增大底部边距,为 X 轴标签留出空间
- containLabel: true
- },
- 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: 'xds',
- type: 'line',
- data: xdsData,
- smooth: true,
- lineStyle: {
- color: '#409EFF',
- width: 2
- },
- symbol: 'circle',
- symbolSize: 8,
- itemStyle: {
- color: '#409EFF'
- },
- emphasis: {
- symbolSize: 12
- }
- },
- {
- name: 'xdsyx',
- type: 'line',
- data: xdsyxData,
- smooth: true,
- lineStyle: {
- color: '#E6A23C',
- width: 2
- },
- symbol: 'circle',
- symbolSize: 8,
- itemStyle: {
- color: '#E6A23C'
- },
- emphasis: {
- symbolSize: 12
- }
- }
- ]
- };
- 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
- })) // 按日期排序
- },
- 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: [
- {
- 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'
- }
- }
- }
- ],
- series: [
- {
- 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: {
- symbolSize: 12
- }
- }
- ]
- };
- this.lineChart.setOption(option);
- }
- }
- }
- </script>
- <style scoped>
- .sample-statistics {
- padding: 24px;
- background: #fff;
- }
- .top-bar {
- margin-bottom: 24px;
- align-items: center;
- }
- .stat-box {
- display: flex;
- flex-direction: column;
- justify-content: center;
- height: 60px;
- }
- .stat-title {
- font-size: 16px;
- color: #888;
- }
- .stat-value {
- font-size: 28px;
- font-weight: bold;
- color: #409EFF;
- }
- .filter-col {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- }
- .chart-section {
- margin-bottom: 24px;
- }
- .line-chart {
- width: 100%;
- height: 320px;
- }
- .pie-row {
- margin-top: 12px;
- }
- .pie-chart {
- width: 100%;
- height: 260px;
- }
- .pie-title {
- text-align: center;
- font-size: 16px;
- 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>
|