<script lang="ts" setup>
|
import type { EChartsOption } from 'echarts';
|
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
|
import type { TrendTableColumn, TrendTableOption } from './helpers/trendOptionPopup';
|
import type { WaterTrendChartType } from './helpers/waterTrend';
|
|
import type { SxtTrendData, SxtTrendInspectionItem, SxtTrendOptions } from '#/api/x/lims/shiyanshi/sxt';
|
|
import { computed, defineComponent, h, onMounted, reactive, ref, watch } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
import { Spin as ASpin } from 'ant-design-vue';
|
import dayjs from 'dayjs';
|
|
import { getSxtTrendData, getSxtTrendOptions } from '#/api/x/lims/shiyanshi/sxt';
|
|
import TrendOptionPopup from './components/TrendOptionPopup.vue';
|
import TrendTablePopup from './components/TrendTablePopup.vue';
|
import { createInspectionItemTableOptions, inspectionItemColumns } from './helpers/inspectionItemTable';
|
import { buildWaterTrendModels, createWaterTrendOption } from './helpers/waterTrend';
|
|
defineOptions({ name: 'XlimsShiyanshiSxtQushiFen' });
|
|
interface SelectOption {
|
label: string;
|
value: string;
|
}
|
|
interface TrendChart {
|
key: string;
|
option: EChartsOption;
|
}
|
|
const LineChartPanel = defineComponent({
|
name: 'LineChartPanel',
|
props: {
|
option: {
|
default: () => ({}),
|
type: Object,
|
},
|
},
|
setup(props) {
|
const chartRef = ref<EchartsUIType>();
|
const { renderEcharts } = useEcharts(chartRef);
|
|
watch(
|
() => props.option,
|
(option) => {
|
renderEcharts(option as EChartsOption);
|
},
|
{ deep: true, immediate: true },
|
);
|
|
return () => h(EchartsUI, { ref: chartRef, height: '520px', width: '100%' });
|
},
|
});
|
|
function createEmptyOptions(): SxtTrendOptions {
|
return {
|
chejian_list: [],
|
jianyan_xiangmu_list: [],
|
quyangdian_list: [],
|
yangpin_leixing_list: [],
|
};
|
}
|
|
function createEmptyData(): SxtTrendData {
|
return { records: [], standards: [] };
|
}
|
|
function toSelectOptions(options: Array<{ id: string; name: string }>): SelectOption[] {
|
return options.map(({ id, name }) => ({ label: name, value: id }));
|
}
|
|
const { createMessage } = useMessage();
|
const optionsLoading = ref(false);
|
const dataLoading = ref(false);
|
const loading = computed(() => optionsLoading.value || dataLoading.value);
|
const queried = ref(false);
|
const errorMessage = ref('');
|
const dateRange = ref<string[]>([]);
|
const trendOptions = ref<SxtTrendOptions>(createEmptyOptions());
|
const trendData = ref<SxtTrendData>(createEmptyData());
|
let optionsRequestId = 0;
|
let dataRequestId = 0;
|
const searchForm = reactive({
|
chart_type: 'line' as WaterTrendChartType,
|
chejian_id: undefined as string | undefined,
|
jianyan_xiangmu_ids: [] as string[],
|
quyangdian_ids: [] as string[],
|
yangpin_leixing_id: undefined as string | undefined,
|
});
|
|
watch(
|
() => [dateRange.value, searchForm.quyangdian_ids, searchForm.jianyan_xiangmu_ids],
|
() => {
|
dataRequestId += 1;
|
dataLoading.value = false;
|
trendData.value = createEmptyData();
|
queried.value = false;
|
errorMessage.value = '';
|
},
|
{ deep: true },
|
);
|
|
const sampleTypeOptions = computed(() => toSelectOptions(trendOptions.value.yangpin_leixing_list));
|
const workshopOptions = computed(() => toSelectOptions(trendOptions.value.chejian_list));
|
const chartTypeOptions: Array<{ label: string; value: WaterTrendChartType }> = [
|
{ label: '折线图', value: 'line' },
|
{ label: '直方图', value: 'bar' },
|
];
|
const pointColumns: TrendTableColumn[] = [
|
{ dataIndex: 'quyangdian_bianhao', key: 'quyangdian_bianhao', title: '取样点编号', width: 220 },
|
{ dataIndex: 'quyang_didian', key: 'quyang_didian', title: '取样地点' },
|
];
|
const pointOptions = computed<TrendTableOption[]>(() =>
|
trendOptions.value.quyangdian_list
|
.filter(
|
(point) =>
|
(!searchForm.chejian_id || point.chejian_id === searchForm.chejian_id) &&
|
(!searchForm.yangpin_leixing_id || point.yangpin_leixing_id === searchForm.yangpin_leixing_id),
|
)
|
.map((point) => ({
|
label: point.name,
|
quyang_didian: point.quyang_didian || '-',
|
quyangdian_bianhao: point.quyangdian_bianhao || point.name,
|
value: point.id,
|
})),
|
);
|
const trendItems = computed<SxtTrendInspectionItem[]>(() => trendOptions.value.jianyan_xiangmu_list);
|
const inspectionItemOptions = computed<TrendTableOption[]>(() => createInspectionItemTableOptions(trendItems.value));
|
const selectedItems = computed(() => trendItems.value.filter((item) => searchForm.jianyan_xiangmu_ids.includes(item.id)));
|
const chartList = computed<TrendChart[]>(() =>
|
buildWaterTrendModels({
|
items: selectedItems.value,
|
records: trendData.value.records,
|
standards: trendData.value.standards,
|
}).map((model) => ({
|
key: model.key,
|
option: createWaterTrendOption(model, getChartTitle(model.item), searchForm.chart_type),
|
})),
|
);
|
|
function getDateRange() {
|
return {
|
end: dateRange.value[1] ? dayjs(dateRange.value[1]).format('YYYY-MM-DD') : '',
|
start: dateRange.value[0] ? dayjs(dateRange.value[0]).format('YYYY-MM-DD') : '',
|
};
|
}
|
|
function getChartTitle(item: SxtTrendInspectionItem) {
|
const { end, start } = getDateRange();
|
const sampleType = sampleTypeOptions.value.find((option) => option.value === searchForm.yangpin_leixing_id)?.label;
|
const workshop = workshopOptions.value.find((option) => option.value === searchForm.chejian_id)?.label;
|
return `${start}~${end} ${[sampleType, workshop, item.name].filter(Boolean).join(' ')} 趋势分析图`;
|
}
|
|
function validateSearch() {
|
const { end, start } = getDateRange();
|
if (!start || !end) return '请选择日期范围';
|
if (!searchForm.yangpin_leixing_id) return '请选择样品类型';
|
if (!searchForm.chejian_id) return '请选择车间';
|
if (!searchForm.quyangdian_ids.length) return '请选择取样点';
|
if (!searchForm.jianyan_xiangmu_ids.length) return '请选择检验项目';
|
return '';
|
}
|
|
async function refreshOptions(data?: { chejian_id?: string; quyangdian_ids?: string[]; yangpin_leixing_id?: string }) {
|
const requestId = ++optionsRequestId;
|
optionsLoading.value = true;
|
errorMessage.value = '';
|
try {
|
const response = await getSxtTrendOptions(data);
|
if (requestId !== optionsRequestId) return;
|
trendOptions.value = response?.data || createEmptyOptions();
|
} catch {
|
if (requestId !== optionsRequestId) return;
|
trendOptions.value = createEmptyOptions();
|
errorMessage.value = '趋势筛选条件加载失败';
|
createMessage.error(errorMessage.value);
|
} finally {
|
if (requestId === optionsRequestId) optionsLoading.value = false;
|
}
|
}
|
|
async function handleSampleTypeChange() {
|
searchForm.chejian_id = undefined;
|
searchForm.quyangdian_ids = [];
|
searchForm.jianyan_xiangmu_ids = [];
|
trendOptions.value = {
|
...createEmptyOptions(),
|
yangpin_leixing_list: trendOptions.value.yangpin_leixing_list,
|
};
|
trendData.value = createEmptyData();
|
queried.value = false;
|
await refreshOptions(searchForm.yangpin_leixing_id ? { yangpin_leixing_id: searchForm.yangpin_leixing_id } : undefined);
|
}
|
|
async function handleWorkshopChange() {
|
searchForm.quyangdian_ids = [];
|
searchForm.jianyan_xiangmu_ids = [];
|
trendOptions.value = {
|
...createEmptyOptions(),
|
chejian_list: trendOptions.value.chejian_list,
|
yangpin_leixing_list: trendOptions.value.yangpin_leixing_list,
|
};
|
trendData.value = createEmptyData();
|
queried.value = false;
|
if (!searchForm.yangpin_leixing_id) return;
|
await refreshOptions({
|
chejian_id: searchForm.chejian_id,
|
yangpin_leixing_id: searchForm.yangpin_leixing_id,
|
});
|
}
|
|
async function handleSamplingPointChange() {
|
searchForm.jianyan_xiangmu_ids = [];
|
trendOptions.value = {
|
...trendOptions.value,
|
jianyan_xiangmu_list: [],
|
};
|
trendData.value = createEmptyData();
|
queried.value = false;
|
if (!searchForm.yangpin_leixing_id || !searchForm.chejian_id || !searchForm.quyangdian_ids.length) {
|
optionsRequestId += 1;
|
optionsLoading.value = false;
|
return;
|
}
|
await refreshOptions({
|
chejian_id: searchForm.chejian_id,
|
quyangdian_ids: searchForm.quyangdian_ids,
|
yangpin_leixing_id: searchForm.yangpin_leixing_id,
|
});
|
}
|
|
async function handleSearch() {
|
const validationMessage = validateSearch();
|
if (validationMessage) {
|
createMessage.error(validationMessage);
|
return;
|
}
|
|
const requestId = ++dataRequestId;
|
const { end, start } = getDateRange();
|
dataLoading.value = true;
|
queried.value = true;
|
errorMessage.value = '';
|
try {
|
const response = await getSxtTrendData({
|
chejian_id: searchForm.chejian_id,
|
end_date: end,
|
jianyan_xiangmu_ids: searchForm.jianyan_xiangmu_ids,
|
quyangdian_ids: searchForm.quyangdian_ids.length ? searchForm.quyangdian_ids : undefined,
|
start_date: start,
|
yangpin_leixing_id: searchForm.yangpin_leixing_id!,
|
});
|
if (requestId !== dataRequestId) return;
|
trendData.value = response?.data || createEmptyData();
|
} catch {
|
if (requestId !== dataRequestId) return;
|
trendData.value = createEmptyData();
|
errorMessage.value = '趋势数据加载失败';
|
createMessage.error(errorMessage.value);
|
} finally {
|
if (requestId === dataRequestId) dataLoading.value = false;
|
}
|
}
|
|
async function handleReset() {
|
dateRange.value = [];
|
searchForm.chart_type = 'line';
|
searchForm.chejian_id = undefined;
|
searchForm.jianyan_xiangmu_ids = [];
|
searchForm.quyangdian_ids = [];
|
searchForm.yangpin_leixing_id = undefined;
|
trendData.value = createEmptyData();
|
queried.value = false;
|
await refreshOptions();
|
}
|
|
onMounted(() => refreshOptions());
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper sxt-trend">
|
<div class="jnpf-content-wrapper-center">
|
<div class="qushi-title">水系统趋势分析</div>
|
|
<div class="jnpf-content-wrapper-search-box qushi-search">
|
<div class="search-row primary-row">
|
<div class="filter-field date-field">
|
<span class="search-label">日期范围:</span>
|
<jnpf-date-range v-model:value="dateRange" allow-clear class="date-range" format="YYYY-MM-DD" />
|
</div>
|
<div class="filter-field">
|
<span class="search-label">样品类型:</span>
|
<TrendOptionPopup
|
v-model:value="searchForm.yangpin_leixing_id"
|
:options="sampleTypeOptions"
|
placeholder="请选择样品类型"
|
popup-title="选择样品类型"
|
@change="handleSampleTypeChange" />
|
</div>
|
<div class="filter-field">
|
<span class="search-label">车间:</span>
|
<TrendOptionPopup
|
v-model:value="searchForm.chejian_id"
|
:disabled="!searchForm.yangpin_leixing_id"
|
:options="workshopOptions"
|
placeholder="请选择车间"
|
popup-title="选择车间"
|
@change="handleWorkshopChange" />
|
</div>
|
</div>
|
<div class="search-row secondary-row">
|
<div class="filter-field">
|
<span class="search-label">取样点:</span>
|
<TrendTablePopup
|
v-model:value="searchForm.quyangdian_ids"
|
:columns="pointColumns"
|
:disabled="!searchForm.chejian_id"
|
:options="pointOptions"
|
placeholder="全部取样点"
|
popup-title="选择取样点"
|
:width="760"
|
@change="handleSamplingPointChange" />
|
</div>
|
<div class="filter-field">
|
<span class="search-label">检验项目:</span>
|
<TrendTablePopup
|
v-model:value="searchForm.jianyan_xiangmu_ids"
|
:columns="inspectionItemColumns"
|
:disabled="!searchForm.quyangdian_ids.length"
|
:options="inspectionItemOptions"
|
placeholder="请选择检验项目"
|
popup-title="选择检验项目"
|
:width="1040" />
|
</div>
|
<div class="filter-field chart-field">
|
<span class="search-label">图形:</span>
|
<a-select v-model:value="searchForm.chart_type" :options="chartTypeOptions" class="chart-select" />
|
</div>
|
<div class="search-actions">
|
<a-button :loading="loading" type="primary" @click="handleSearch">搜索</a-button>
|
<a-button @click="handleReset">重置</a-button>
|
</div>
|
</div>
|
</div>
|
|
<div class="jnpf-content-wrapper-content qushi-content">
|
<ASpin :spinning="loading">
|
<div v-if="chartList.length" class="chart-list">
|
<div v-for="chart in chartList" :key="chart.key" class="chart-block">
|
<LineChartPanel class="chart-canvas" :option="chart.option" />
|
</div>
|
</div>
|
<div v-else class="empty-panel">
|
{{ errorMessage || (queried ? '暂无趋势数据' : '请选择筛选条件后搜索') }}
|
</div>
|
</ASpin>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style lang="scss" scoped>
|
.sxt-trend {
|
box-sizing: border-box;
|
min-height: 100%;
|
padding: 10px 12px 12px;
|
background: #fff;
|
|
.jnpf-content-wrapper-center {
|
overflow: hidden;
|
background: #fff;
|
border-radius: 6px;
|
}
|
}
|
|
.qushi-title {
|
height: 66px;
|
color: #111;
|
font-size: 30px;
|
font-weight: 700;
|
line-height: 66px;
|
text-align: center;
|
text-shadow: 0 4px 10px rgb(0 0 0 / 24%);
|
}
|
|
.sxt-trend .qushi-search {
|
display: flex;
|
flex-direction: column;
|
box-sizing: border-box;
|
min-height: 112px;
|
padding: 14px;
|
border-top: 1px solid #d9d9d9;
|
border-right: 0;
|
border-bottom: 1px solid #d9d9d9;
|
border-left: 0;
|
border-radius: 0;
|
gap: 12px;
|
}
|
|
.search-row {
|
display: grid;
|
align-items: center;
|
justify-content: start;
|
min-width: 0;
|
gap: 12px 18px;
|
}
|
|
.primary-row {
|
grid-template-columns: minmax(420px, 460px) minmax(240px, 300px) minmax(240px, 300px);
|
}
|
|
.secondary-row {
|
grid-template-columns: minmax(280px, 320px) minmax(320px, 360px) max-content max-content;
|
}
|
|
.filter-field {
|
display: grid;
|
grid-template-columns: max-content minmax(0, 1fr);
|
align-items: center;
|
min-width: 0;
|
gap: 8px;
|
}
|
|
.search-label {
|
color: #444;
|
font-size: 14px;
|
font-weight: 600;
|
white-space: nowrap;
|
}
|
|
.date-range {
|
width: 100%;
|
}
|
|
.chart-field {
|
grid-template-columns: max-content 112px;
|
width: max-content;
|
}
|
|
.chart-select {
|
width: 112px;
|
}
|
|
.search-actions {
|
display: flex;
|
align-items: center;
|
justify-content: flex-start;
|
gap: 8px;
|
}
|
|
.sxt-trend .qushi-content {
|
flex: 1;
|
min-height: 0;
|
height: auto;
|
overflow-x: hidden;
|
overflow-y: auto;
|
padding: 12px 14px 28px;
|
background: #fff;
|
}
|
|
.chart-list {
|
display: flex;
|
flex-direction: column;
|
gap: 24px;
|
padding-bottom: 16px;
|
}
|
|
.chart-block,
|
.chart-canvas {
|
width: 100%;
|
}
|
|
.chart-block {
|
background: #fff;
|
}
|
|
.chart-canvas {
|
height: 520px;
|
}
|
|
.empty-panel {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
height: 360px;
|
color: #999;
|
font-size: 14px;
|
}
|
|
@media (max-width: 1180px) {
|
.primary-row,
|
.secondary-row {
|
grid-template-columns: repeat(2, minmax(280px, 1fr));
|
}
|
}
|
|
@media (max-width: 820px) {
|
.primary-row,
|
.secondary-row {
|
grid-template-columns: minmax(0, 1fr);
|
}
|
}
|
</style>
|