<script lang="ts" setup>
|
import type { TableColumnType } from 'ant-design-vue';
|
|
import type { StabilityAnalysisData, StabilityAnalysisOptions, StabilityAnalysisQuery, StabilityAnalysisRow } from '#/api/x/lims/wendingxing';
|
|
import { computed, onMounted, reactive, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
|
import { Search } from '@vben/icons';
|
|
import { DownloadOutlined } from '@ant-design/icons-vue';
|
import { Spin as ASpin } from 'ant-design-vue';
|
|
import { getStabilityAnalysisData, getStabilityAnalysisOptions } from '#/api/x/lims/wendingxing';
|
|
import AnalysisOptionPopup from './components/AnalysisOptionPopup.vue';
|
import StabilityTrendChart from './components/StabilityTrendChart.vue';
|
import { buildStabilityChartModels, createStabilityChartOption, formatDisplayNumber, formatPeriodUnit } from './helpers/stabilityAnalysis';
|
import { downloadStabilityExport } from './helpers/stabilityExport';
|
|
defineOptions({ name: 'XlimsWendingxingShujuFenxi' });
|
|
interface PopupColumn {
|
customRender?: ({ index }: { index: number }) => number;
|
dataIndex: string;
|
key: string;
|
title: string;
|
width?: number;
|
}
|
|
const emptyOptions = (): StabilityAnalysisOptions => ({
|
fenlei_list: [],
|
jihua_list: [],
|
pihao_list: [],
|
xiangmu_list: [],
|
yangpin_list: [],
|
});
|
const emptyData = (): StabilityAnalysisData => ({
|
completeness: { audited_count: 0, expected_count: 0, missing_count: 0, pending_count: 0 },
|
expected_periods: [],
|
rows: [],
|
});
|
|
const categoryColumns: PopupColumn[] = [
|
{ dataIndex: 'code', key: 'code', title: '分类编码', width: 220 },
|
{ dataIndex: 'name', key: 'name', title: '分类名称' },
|
];
|
const productColumns: PopupColumn[] = [
|
{ dataIndex: 'code', key: 'code', title: '样品编码', width: 200 },
|
{ dataIndex: 'name', key: 'name', title: '品名', width: 260 },
|
{ dataIndex: 'guige', key: 'guige', title: '规格' },
|
];
|
const itemColumns: PopupColumn[] = [
|
{ customRender: ({ index }) => index + 1, dataIndex: 'sequence', key: 'sequence', title: '序号', width: 70 },
|
{ dataIndex: 'code', key: 'code', title: '项目编号', width: 180 },
|
{ dataIndex: 'name', key: 'name', title: '项目名称', width: 240 },
|
{ dataIndex: 'shuju_leixing', key: 'shuju_leixing', title: '数据类型', width: 140 },
|
];
|
const planColumns: PopupColumn[] = [
|
{ customRender: ({ index }) => index + 1, dataIndex: 'sequence', key: 'sequence', title: '序号', width: 70 },
|
{ dataIndex: 'name', key: 'name', title: '稳定性考察编号' },
|
];
|
const batchColumns: PopupColumn[] = [
|
{ customRender: ({ index }) => index + 1, dataIndex: 'sequence', key: 'sequence', title: '序号', width: 70 },
|
{ dataIndex: 'jihua_bianhao', key: 'jihua_bianhao', title: '稳定性考察编号', width: 230 },
|
{ dataIndex: 'fangan_mingcheng', key: 'fangan_mingcheng', title: '方案名称', width: 320 },
|
{ dataIndex: 'pihao', key: 'pihao', title: '批号', width: 180 },
|
];
|
const resultColumns: TableColumnType<StabilityAnalysisRow>[] = [
|
{ customRender: ({ index }) => index, title: '序号', width: '4%' },
|
{ dataIndex: 'yangpin_mingcheng', ellipsis: true, title: '品名', width: '11%' },
|
{ dataIndex: 'jihua_bianhao', ellipsis: true, title: '稳定性考察编号', width: '13%' },
|
{ dataIndex: 'pihao', ellipsis: true, title: '批号', width: '12%' },
|
{ dataIndex: 'jieguo_luru_riqi', title: '录入日期', width: '9%' },
|
{ customRender: ({ text }) => formatDisplayNumber(String(text ?? '')), dataIndex: 'zhouqi', title: '考察周期', width: '7%' },
|
{ customRender: ({ text }) => formatPeriodUnit(String(text ?? '')), dataIndex: 'zhouqi_danwei', title: '周期单位', width: '7%' },
|
{ dataIndex: 'xiangmu_mingcheng', ellipsis: true, title: '项目名称', width: '10%' },
|
{
|
customRender: ({ record }) => record.raw_result || '-',
|
dataIndex: 'raw_result',
|
title: '数据',
|
width: '9%',
|
},
|
{ dataIndex: 'shangxian', ellipsis: true, title: '上限', width: '6%' },
|
{ dataIndex: 'xiaxian', ellipsis: true, title: '下限', width: '6%' },
|
{ dataIndex: 'unit', ellipsis: true, title: '单位', width: '6%' },
|
];
|
|
const { createMessage } = useMessage();
|
const options = ref<StabilityAnalysisOptions>(emptyOptions());
|
const analysisData = ref<StabilityAnalysisData>(emptyData());
|
const loadingOptions = ref(false);
|
const loadingData = ref(false);
|
const exporting = ref(false);
|
const queried = ref(false);
|
const selectedBatchKeys = ref<string[]>([]);
|
const resultContext = reactive({ categoryName: '', productName: '' });
|
let optionsRequestId = 0;
|
let dataRequestId = 0;
|
|
const searchForm = reactive({
|
fenlei_bianma: undefined as string | undefined,
|
jihua_ids: [] as string[],
|
xiangmu_ids: [] as string[],
|
yangpin_id: undefined as string | undefined,
|
});
|
|
const selectedCategoryName = computed(() => options.value.fenlei_list.find((item) => item.key === searchForm.fenlei_bianma)?.name || '');
|
const selectedProductName = computed(() => options.value.yangpin_list.find((item) => item.key === searchForm.yangpin_id)?.name || '');
|
const numericCharts = computed(() =>
|
buildStabilityChartModels(analysisData.value.rows)
|
.filter((model) => !model.text_only)
|
.map((model) => ({
|
key: model.key,
|
option: createStabilityChartOption(model, getChartTitle(model)),
|
})),
|
);
|
const textModels = computed(() => buildStabilityChartModels(analysisData.value.rows).filter((model) => model.text_only));
|
|
function getChartTitle(model: ReturnType<typeof buildStabilityChartModels>[number]) {
|
const details = [
|
model.unit ? `单位-${model.unit}` : '',
|
model.lower_limit || model.upper_limit ? `限值-${model.lower_limit || '-'}~${model.upper_limit || '-'}` : '',
|
]
|
.filter(Boolean)
|
.join(',');
|
return `分类-${resultContext.categoryName},品名-${resultContext.productName},项目-${model.item_name}${details ? `,${details}` : ''}`;
|
}
|
|
function markFiltersChanged() {
|
dataRequestId += 1;
|
loadingData.value = false;
|
}
|
|
function buildPartialQuery(): Partial<StabilityAnalysisQuery> {
|
return {
|
fenlei_bianma: searchForm.fenlei_bianma,
|
jihua_ids: searchForm.jihua_ids,
|
pihao_keys: resolveBatchKeys(),
|
xiangmu_ids: searchForm.xiangmu_ids,
|
yangpin_id: searchForm.yangpin_id,
|
};
|
}
|
|
function resolveBatchKeys() {
|
const map = new Map(options.value.pihao_list.map((batch) => [batch.key, batch]));
|
return selectedBatchKeys.value.flatMap((key) => {
|
const batch = map.get(key);
|
return batch ? [{ jihua_id: batch.jihua_id, pihao: batch.pihao }] : [];
|
});
|
}
|
|
function buildQuery(): StabilityAnalysisQuery {
|
return {
|
fenlei_bianma: searchForm.fenlei_bianma!,
|
jihua_ids: [...searchForm.jihua_ids],
|
pihao_keys: resolveBatchKeys(),
|
xiangmu_ids: [...searchForm.xiangmu_ids],
|
yangpin_id: searchForm.yangpin_id!,
|
};
|
}
|
|
function getRowKey(row: StabilityAnalysisRow, index?: number) {
|
return [row.batch_key, row.xiangmu_id, row.zhouqi, row.zhouqi_danwei, row.jieguo_luru_riqi, index ?? 0].join('::');
|
}
|
|
async function refreshOptions() {
|
const requestId = ++optionsRequestId;
|
loadingOptions.value = true;
|
try {
|
const response = await getStabilityAnalysisOptions(buildPartialQuery());
|
if (requestId === optionsRequestId) options.value = response?.data || emptyOptions();
|
} catch {
|
if (requestId !== optionsRequestId) return;
|
options.value = emptyOptions();
|
createMessage.error('筛选条件加载失败');
|
} finally {
|
if (requestId === optionsRequestId) loadingOptions.value = false;
|
}
|
}
|
|
async function handleCategoryChange() {
|
searchForm.yangpin_id = undefined;
|
searchForm.xiangmu_ids = [];
|
searchForm.jihua_ids = [];
|
selectedBatchKeys.value = [];
|
markFiltersChanged();
|
await refreshOptions();
|
}
|
|
async function handleProductChange() {
|
searchForm.xiangmu_ids = [];
|
searchForm.jihua_ids = [];
|
selectedBatchKeys.value = [];
|
markFiltersChanged();
|
await refreshOptions();
|
}
|
|
async function handleItemChange() {
|
searchForm.jihua_ids = [];
|
selectedBatchKeys.value = [];
|
markFiltersChanged();
|
await refreshOptions();
|
}
|
|
async function handlePlanChange() {
|
selectedBatchKeys.value = [];
|
markFiltersChanged();
|
await refreshOptions();
|
}
|
|
function validateQuery() {
|
if (!searchForm.fenlei_bianma) return '请选择分类';
|
if (!searchForm.yangpin_id) return '请选择品名';
|
if (!searchForm.xiangmu_ids.length) return '请选择检验项目';
|
return '';
|
}
|
|
async function handleSearch() {
|
const message = validateQuery();
|
if (message) return createMessage.error(message);
|
const requestId = ++dataRequestId;
|
loadingData.value = true;
|
queried.value = true;
|
const context = { categoryName: selectedCategoryName.value, productName: selectedProductName.value };
|
try {
|
const response = await getStabilityAnalysisData(buildQuery());
|
if (requestId === dataRequestId) {
|
analysisData.value = response?.data || emptyData();
|
resultContext.categoryName = context.categoryName;
|
resultContext.productName = context.productName;
|
}
|
} catch {
|
if (requestId !== dataRequestId) return;
|
createMessage.error('稳定性分析数据加载失败');
|
} finally {
|
if (requestId === dataRequestId) loadingData.value = false;
|
}
|
}
|
|
async function handleExport() {
|
if (!analysisData.value.rows.length) return createMessage.warning('当前没有可导出的已审核数据');
|
exporting.value = true;
|
try {
|
downloadStabilityExport(analysisData.value.rows);
|
} catch {
|
createMessage.error('稳定性分析表导出失败');
|
} finally {
|
exporting.value = false;
|
}
|
}
|
|
onMounted(() => refreshOptions());
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper stability-analysis">
|
<div class="jnpf-content-wrapper-center">
|
<div class="jnpf-content-wrapper-search-box search-band">
|
<div class="filter-toolbar">
|
<label class="filter-field">
|
<span>分类:</span>
|
<AnalysisOptionPopup
|
v-model:value="searchForm.fenlei_bianma"
|
:columns="categoryColumns"
|
mode="single"
|
:options="options.fenlei_list"
|
popup-title="选择分类"
|
@change="handleCategoryChange" />
|
</label>
|
<label class="filter-field">
|
<span>品名:</span>
|
<AnalysisOptionPopup
|
v-model:value="searchForm.yangpin_id"
|
:columns="productColumns"
|
:disabled="!searchForm.fenlei_bianma"
|
mode="single"
|
:options="options.yangpin_list"
|
popup-title="选择品名"
|
@change="handleProductChange" />
|
</label>
|
<label class="filter-field">
|
<span>项目:</span>
|
<AnalysisOptionPopup
|
v-model:value="searchForm.xiangmu_ids"
|
:columns="itemColumns"
|
:disabled="!searchForm.yangpin_id"
|
mode="multiple"
|
:options="options.xiangmu_list"
|
popup-title="选择检验项目"
|
:width="1000"
|
@change="handleItemChange" />
|
</label>
|
<label class="filter-field">
|
<span>考察编号:</span>
|
<AnalysisOptionPopup
|
v-model:value="searchForm.jihua_ids"
|
:columns="planColumns"
|
:disabled="!searchForm.xiangmu_ids.length"
|
mode="multiple"
|
:options="options.jihua_list"
|
popup-title="选择稳定性考察编号"
|
@change="handlePlanChange" />
|
</label>
|
<label class="filter-field">
|
<span>批号:</span>
|
<AnalysisOptionPopup
|
v-model:value="selectedBatchKeys"
|
:columns="batchColumns"
|
:disabled="!searchForm.xiangmu_ids.length"
|
mode="multiple"
|
:options="options.pihao_list"
|
popup-title="选择批号"
|
:width="1080"
|
@change="markFiltersChanged" />
|
</label>
|
<div class="filter-actions">
|
<a-button data-action="search" :loading="loadingData || loadingOptions" type="primary" @click="handleSearch"> <Search :size="16" />查询 </a-button>
|
<a-button data-action="export" :disabled="!queried || !analysisData.rows.length" :loading="exporting" @click="handleExport">
|
<DownloadOutlined />导出
|
</a-button>
|
</div>
|
</div>
|
</div>
|
|
<div class="jnpf-content-wrapper-content result-content" data-scroll-container="true">
|
<ASpin :spinning="loadingData">
|
<section class="table-section">
|
<h2>稳定性数据分析表</h2>
|
<a-table
|
bordered
|
class="analysis-table"
|
:columns="resultColumns"
|
:data-source="analysisData.rows"
|
:pagination="false"
|
:row-key="getRowKey"
|
size="small"
|
table-layout="fixed" />
|
</section>
|
|
<div v-for="model in textModels" :key="model.key" class="text-only-note">
|
{{ model.item_name }}({{ model.item_code }})为文本型项目,仅在上方表格展示。
|
</div>
|
|
<section v-for="chart in numericCharts" :key="chart.key" class="chart-block">
|
<StabilityTrendChart :option="chart.option" />
|
</section>
|
</ASpin>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style lang="scss" scoped>
|
.stability-analysis {
|
min-width: 0;
|
overflow: hidden;
|
background: #fff;
|
}
|
|
.search-band {
|
padding: 9px 12px !important;
|
margin-bottom: 0 !important;
|
background: #fff;
|
border-bottom: 1px solid #dfe3e8;
|
border-radius: 0 !important;
|
}
|
|
.filter-toolbar {
|
display: grid;
|
grid-template-columns: repeat(5, minmax(0, 1fr)) 188px;
|
gap: 12px;
|
align-items: center;
|
width: 100%;
|
min-width: 0;
|
}
|
|
.filter-field {
|
display: flex;
|
gap: 8px;
|
align-items: center;
|
min-width: 0;
|
color: #30343b;
|
|
> span {
|
flex-shrink: 0;
|
font-size: 13px;
|
font-weight: 500;
|
line-height: 32px;
|
}
|
|
> :deep(.analysis-option-popup) {
|
flex: 1 1 auto;
|
width: 0;
|
min-width: 0;
|
}
|
}
|
|
.filter-actions {
|
display: flex;
|
gap: 8px;
|
align-items: center;
|
justify-content: flex-end;
|
width: 188px;
|
white-space: nowrap;
|
|
:deep(.ant-btn) {
|
display: inline-flex;
|
gap: 6px;
|
align-items: center;
|
justify-content: center;
|
}
|
|
:deep([data-action='search']) {
|
width: 96px;
|
}
|
|
:deep([data-action='export']) {
|
width: 84px;
|
}
|
}
|
|
.result-content {
|
min-height: 0;
|
padding: 0 16px 32px;
|
overflow: hidden auto !important;
|
background: #fff;
|
border-radius: 0 !important;
|
|
> :deep(.ant-spin-nested-loading),
|
> :deep(.ant-spin-nested-loading > .ant-spin-container) {
|
min-height: 100%;
|
}
|
}
|
|
.table-section {
|
h2 {
|
margin: 24px 0 12px;
|
font-size: 24px;
|
font-weight: 600;
|
line-height: 32px;
|
text-align: center;
|
letter-spacing: 0;
|
}
|
}
|
|
.analysis-table {
|
width: 100%;
|
overflow: visible !important;
|
border-radius: 0 !important;
|
|
:deep(.ant-table),
|
:deep(.ant-table-container),
|
:deep(.ant-table-thead > tr:first-child > th:first-child),
|
:deep(.ant-table-thead > tr:first-child > th:last-child),
|
:deep(.ant-table-tbody > tr:last-child > td:first-child),
|
:deep(.ant-table-tbody > tr:last-child > td:last-child) {
|
border-radius: 0 !important;
|
}
|
|
:deep(.ant-table-bordered .ant-table-container),
|
:deep(.ant-table-bordered .ant-table-cell) {
|
border-color: #b9c2ce !important;
|
}
|
|
:deep(.ant-table-bordered > .ant-table-container) {
|
border-top: 1px solid #b9c2ce !important;
|
border-inline-start: 1px solid #b9c2ce !important;
|
}
|
|
:deep(.ant-table-container),
|
:deep(.ant-table-content) {
|
overflow-x: visible !important;
|
}
|
|
:deep(.ant-table-thead > tr > th) {
|
font-size: 14px;
|
font-weight: 600;
|
color: #fff;
|
text-align: center;
|
white-space: nowrap;
|
background: #169fd5;
|
}
|
|
:deep(.ant-table-cell) {
|
min-width: 0;
|
padding: 9px 8px !important;
|
overflow: hidden;
|
font-size: 13px;
|
line-height: 20px;
|
text-align: center;
|
text-overflow: ellipsis;
|
vertical-align: middle;
|
white-space: nowrap;
|
}
|
|
:deep(.ant-table-tbody > tr > td) {
|
max-width: 0;
|
white-space: nowrap;
|
}
|
|
:deep(.ant-table-tbody > .ant-table-placeholder > td) {
|
border-bottom: 1px solid #b9c2ce !important;
|
}
|
}
|
|
.chart-block {
|
min-height: 520px;
|
padding: 0;
|
margin: 30px 0 0;
|
}
|
|
.text-only-note {
|
padding: 10px 12px;
|
margin-top: 16px;
|
color: #475569;
|
background: #f8fafc;
|
border-top: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
}
|
</style>
|