混炼示方优化

This commit is contained in:
geht
2026-06-12 19:38:33 +08:00
parent 75bc744fc8
commit ece8e590e4
22 changed files with 1425 additions and 23 deletions

View File

@@ -80,3 +80,23 @@ export async function loadRecordWithTrace(
}
return record;
}
/** 业务页签章区展示(起草取主表,校对/审核/批准取痕迹表) */
export interface BizSignDisplay {
draftBy: string;
proofreadBy: string;
auditBy: string;
approveBy: string;
}
export function buildBizSignDisplay(
row: Recordable = {},
resolveDraftBy: (record: Recordable) => string,
): BizSignDisplay {
return {
draftBy: resolveDraftBy(row) || '',
proofreadBy: row.traceProofreadBy || '',
auditBy: row.traceAuditBy || '',
approveBy: row.traceApproveBy || '',
};
}

View File

@@ -12,6 +12,7 @@ enum Api {
queryById = '/xslmes/mesXslMixingSpec/queryById',
queryIssueNumberOptions = '/xslmes/mesXslMixingSpec/queryIssueNumberOptions',
queryPurposeOptions = '/xslmes/mesXslMixingSpec/queryPurposeOptions',
updateSmallWeighRange = '/xslmes/mesXslMixingSpec/updateSmallWeighRange',
}
export const getExportUrl = Api.exportXls;
@@ -24,6 +25,7 @@ export const saveOrUpdate = (params, isUpdate) =>
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A45】混炼示方主子表保存延长超时避免误报失败-----------
export const queryIssueNumberOptions = (params) => defHttp.get({ url: Api.queryIssueNumberOptions, params });
export const queryPurposeOptions = (params) => defHttp.get({ url: Api.queryPurposeOptions, params });
export const updateSmallWeighRange = (params) => defHttp.post({ url: Api.updateSmallWeighRange, params });
export const deleteOne = (params, handleSuccess) =>
defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess());

View File

@@ -69,7 +69,15 @@ export const mainSchema: FormSchema[] = [
{ label: '用途', field: 'purpose', component: 'Input', required: true, colProps: { span: 8 } },
{ label: '机台', field: 'machineName', component: 'Input', colProps: { span: 8 } },
{ label: '制作日期', field: 'makeDate', component: 'DatePicker', colProps: { span: 8 }, componentProps: { valueFormat: 'YYYY-MM-DD' } },
{ label: '发行编号', field: 'issueNumber', component: 'Input', required: true, colProps: { span: 8 } },
{
label: '发行编号',
field: 'issueNumber',
component: 'Input',
required: true,
colProps: { span: 8 },
componentProps: { placeholder: '请点击选择密炼PS' },
rules: [{ required: true, message: '请选择发行编号' }],
},
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A33】混炼示方基本信息字段优化-----------
{ label: '换算系数', field: 'convertFactor', component: 'InputNumber', colProps: { span: 8 }, componentProps: { precision: 2, style: { width: '100%' } } },
{ label: '填充体积', field: 'fillVolume', component: 'InputNumber', colProps: { span: 8 }, componentProps: { precision: 6, style: { width: '100%' } } },
@@ -1387,7 +1395,7 @@ export function resolveMixingSpecFormulaStatus(record: Recordable = {}): string
return '编制中';
}
/** 混炼示方是否允许编辑/删除(与配合示方一致:仅编制状态可改 */
/** 混炼示方是否允许删除(仅编制状态可删;编辑不受状态限制 */
export function isMixingSpecEditable(record: Recordable = {}): boolean {
//update-begin---author:cursor ---date:20260608 for【XSLMES-20260608-A01】混炼示方编辑权限按状态字段判断-----------
return !record?.status || record.status === 'compile';
@@ -1471,3 +1479,180 @@ export function cloneMixingSpecPageForReferenceAdd(source: Recordable = {}): Rec
};
}
//update-end---author:cursor ---date:20260526 for【XSLMES-20260526-A59】规格点击选择混炼示方及参照新增-----------
//update-begin---author:cursor ---date:20260612 for【XSLMES-20260612-A01】混炼示方列表查看人工/自动小料明细-----------
/** 小料计量默认容差KG与旧系统施工表一致 */
export const MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE = 0.01;
/** 小料称重容差 */
export interface MixingSmallWeighTolerance {
lower: number;
upper: number;
}
/** 解析示方主表小料称重容差(未配置时默认 0.01 */
export function resolveMixingSmallWeighTolerance(
main: Recordable | null | undefined,
type: MixingSmallMaterialViewType,
): MixingSmallWeighTolerance {
const lowerKey = type === 'manual' ? 'manualSmallWeighLowerTol' : 'autoSmallWeighLowerTol';
const upperKey = type === 'manual' ? 'manualSmallWeighUpperTol' : 'autoSmallWeighUpperTol';
const lower = toMixingMaterialNumber(main?.[lowerKey]);
const upper = toMixingMaterialNumber(main?.[upperKey]);
return {
lower: lower != null && lower >= 0 ? lower : MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE,
upper: upper != null && upper >= 0 ? upper : MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE,
};
}
/** 构建小料称重范围表单默认值 */
export function buildMixingSmallWeighRangeForm(main: Recordable = {}): Recordable {
const manual = resolveMixingSmallWeighTolerance(main, 'manual');
const auto = resolveMixingSmallWeighTolerance(main, 'auto');
return {
id: main.id || '',
specName: main.specName || '',
manualSmallWeighLowerTol: manual.lower,
manualSmallWeighUpperTol: manual.upper,
autoSmallWeighLowerTol: auto.lower,
autoSmallWeighUpperTol: auto.upper,
};
}
/** 小料查看类型 */
export type MixingSmallMaterialViewType = 'manual' | 'auto';
export interface MixingSmallMaterialWeighRow {
materialCode: string;
standardWeight: number | null;
lowerLimit: number | null;
upperLimit: number | null;
accumWeight: number | null;
}
export interface MixingSmallMaterialAutoGroup {
groupKey: string;
groupLabel: string;
rows: MixingSmallMaterialWeighRow[];
}
/** 判断明细行是否属于人工小料(按种类/小类) */
export function isMixingManualSmallMaterial(row: Recordable): boolean {
if (!isMixingMaterialDataRow(row)) {
return false;
}
const kind = String(row.materialKind || '').trim();
const minor = String(row.materialMinor || '').trim();
if (kind.includes('人工') || minor.includes('人工')) {
return true;
}
return false;
}
/** 判断明细行是否属于自动小料(按种类/小类) */
export function isMixingAutoSmallMaterial(row: Recordable): boolean {
if (!isMixingMaterialDataRow(row)) {
return false;
}
const kind = String(row.materialKind || '').trim();
const minor = String(row.materialMinor || '').trim();
if (kind.includes('自动') || minor.includes('自动')) {
return true;
}
return false;
}
/** 解析自动小料分组标签:顺序未填默认 自动1-1 */
export function resolveMixingAutoSmallGroupLabel(seqNo: unknown): string {
const seq = toMixingMaterialNumber(seqNo);
const suffix = seq != null && seq > 0 ? Math.round(seq) : 1;
return `自动1-${suffix}`;
}
/** 按顺序字段分组自动小料明细 */
export function groupMixingAutoSmallMaterials(
rows: Recordable[] = [],
main?: Recordable,
): MixingSmallMaterialAutoGroup[] {
const filtered = (rows || []).filter(isMixingAutoSmallMaterial);
if (!filtered.length) {
return [];
}
const groupMap = new Map<string, Recordable[]>();
const groupOrder: string[] = [];
for (const row of filtered) {
const label = resolveMixingAutoSmallGroupLabel(row.seqNo);
if (!groupMap.has(label)) {
groupMap.set(label, []);
groupOrder.push(label);
}
groupMap.get(label)!.push(row);
}
groupOrder.sort((a, b) => resolveAutoGroupSortNo(a) - resolveAutoGroupSortNo(b));
return groupOrder.map((label) => ({
groupKey: label,
groupLabel: label,
rows: buildMixingSmallMaterialWeighRows(
groupMap.get(label) || [],
resolveMixingSmallWeighTolerance(main, 'auto'),
),
}));
}
function resolveAutoGroupSortNo(groupLabel: string): number {
const matched = groupLabel.match(/-(\d+)$/);
if (!matched) {
return 1;
}
const num = Number(matched[1]);
return Number.isFinite(num) ? num : 1;
}
/** 构建人工小料计量行 */
export function buildMixingManualSmallMaterialRows(
rows: Recordable[] = [],
main?: Recordable,
): MixingSmallMaterialWeighRow[] {
return buildMixingSmallMaterialWeighRows(
(rows || []).filter(isMixingManualSmallMaterial),
resolveMixingSmallWeighTolerance(main, 'manual'),
);
}
/** 将明细行转为药品计量展示行(含累计标准重量) */
export function buildMixingSmallMaterialWeighRows(
rows: Recordable[] = [],
tolerance: MixingSmallWeighTolerance = {
lower: MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE,
upper: MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE,
},
): MixingSmallMaterialWeighRow[] {
let running = 0;
let hasRunning = false;
return (rows || []).map((row) => {
const standardWeight = toMixingMaterialNumber(row.unitWeight);
const lowerLimit =
standardWeight != null ? roundMixingMaterialNumber(standardWeight - tolerance.lower) : null;
const upperLimit =
standardWeight != null ? roundMixingMaterialNumber(standardWeight + tolerance.upper) : null;
if (standardWeight != null) {
running += standardWeight;
hasRunning = true;
}
const materialCode =
String(row.mixerMaterialName || row.mixerMaterialDesc || row.materialKind || '').trim();
return {
materialCode,
standardWeight,
lowerLimit,
upperLimit,
accumWeight: standardWeight != null && hasRunning ? roundMixingMaterialNumber(running) : null,
};
});
}
/** 小料查看弹窗标题 */
export function resolveMixingSmallMaterialViewTitle(viewType: MixingSmallMaterialViewType): string {
return viewType === 'manual' ? '混炼母胶药品计量(人工)' : '混炼母胶药品计量(自动)';
}
//update-end---author:cursor ---date:20260612 for【XSLMES-20260612-A01】混炼示方列表查看人工/自动小料明细-----------

View File

@@ -0,0 +1,10 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
listByMixingSpecId = '/xslmes/mesXslMixingSpecHistory/listByMixingSpecId',
queryById = '/xslmes/mesXslMixingSpecHistory/queryById',
}
export const listHistoryByMixingSpecId = (params) => defHttp.get({ url: Api.listByMixingSpecId, params });
export const queryHistoryById = (params) => defHttp.get({ url: Api.queryById, params });

View File

@@ -27,6 +27,9 @@
</template>
</BasicTable>
<MesXslMixingSpecModal @register="registerModal" @success="handleSuccess" />
<MesXslMixingSpecHistoryModal @register="registerHistoryModal" @viewDetail="handleViewHistoryDetail" />
<MesXslMixingSmallMaterialViewModal @register="registerSmallMaterialModal" />
<MesXslMixingSmallWeighRangeModal @register="registerWeighRangeModal" @success="handleSuccess" />
</div>
</template>
@@ -37,6 +40,9 @@
import { useListPage } from '/@/hooks/system/useListPage';
import Icon from '/@/components/Icon';
import MesXslMixingSpecModal from './components/MesXslMixingSpecModal.vue';
import MesXslMixingSpecHistoryModal from './components/MesXslMixingSpecHistoryModal.vue';
import MesXslMixingSmallMaterialViewModal from './components/MesXslMixingSmallMaterialViewModal.vue';
import MesXslMixingSmallWeighRangeModal from './components/MesXslMixingSmallWeighRangeModal.vue';
import { columns, searchFormSchema, isMixingSpecEditable } from './MesXslMixingSpec.data';
import { list, deleteOne, batchDelete, getExportUrl, getImportUrl } from './MesXslMixingSpec.api';
import { useMessage } from '/@/hooks/web/useMessage';
@@ -45,6 +51,9 @@
const queryParam = reactive<any>({});
const [registerModal, { openModal }] = useModal();
const [registerHistoryModal, { openModal: openHistoryModal }] = useModal();
const [registerSmallMaterialModal, { openModal: openSmallMaterialModal }] = useModal();
const [registerWeighRangeModal, { openModal: openWeighRangeModal }] = useModal();
const { tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
@@ -60,7 +69,7 @@
actionColumn: {
title: '操作',
dataIndex: 'action',
width: 160,
width: 260,
fixed: 'right',
slots: { customRender: 'action' },
},
@@ -84,10 +93,6 @@
}
function handleEdit(record: Recordable) {
if (!isMixingSpecEditable(record)) {
createMessage.warning('已进入审批流程的混炼示方不允许编辑');
return;
}
openModal(true, { record, isUpdate: true, showFooter: true });
}
@@ -95,6 +100,32 @@
openModal(true, { record, isUpdate: true, showFooter: false });
}
function handleViewManualSmallMaterial(record: Recordable) {
openSmallMaterialModal(true, { record, viewType: 'manual' });
}
function handleViewAutoSmallMaterial(record: Recordable) {
openSmallMaterialModal(true, { record, viewType: 'auto' });
}
function handleSmallWeighRange(record: Recordable) {
openWeighRangeModal(true, { record });
}
function handleHistory(record: Recordable) {
openHistoryModal(true, { record });
}
function handleViewHistoryDetail(historyRecord: Recordable) {
openModal(true, {
historyId: historyRecord?.id,
historyVersion: historyRecord?.versionNo,
isUpdate: true,
showFooter: false,
isHistoryView: true,
});
}
function handleDelete(record: Recordable) {
if (!isMixingSpecEditable(record)) {
createMessage.warning('已进入审批流程的混炼示方不允许删除');
@@ -116,26 +147,43 @@
const editable = isMixingSpecEditable(record);
return [
{
label: '编辑',
icon: 'ant-design:edit-outlined',
tooltip: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'xslmes:mes_xsl_mixing_spec:edit',
ifShow: editable,
},
{
label: '详情',
icon: 'ant-design:eye-outlined',
tooltip: '详情',
onClick: handleDetail.bind(null, record),
ifShow: !editable,
},
{
label: '删除',
icon: 'ant-design:delete-outlined',
tooltip: '删除',
auth: 'xslmes:mes_xsl_mixing_spec:delete',
ifShow: editable,
popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record) },
},
{
label: '详情',
onClick: handleDetail.bind(null, record),
ifShow: editable,
icon: 'ant-design:user-outlined',
tooltip: '查看人工小料',
onClick: handleViewManualSmallMaterial.bind(null, record),
},
{
icon: 'ant-design:robot-outlined',
tooltip: '查看自动小料',
onClick: handleViewAutoSmallMaterial.bind(null, record),
},
{
icon: 'ant-design:column-width-outlined',
tooltip: '小料称重范围',
onClick: handleSmallWeighRange.bind(null, record),
auth: 'xslmes:mes_xsl_mixing_spec:edit',
},
{
icon: 'ant-design:history-outlined',
tooltip: '历史记录',
onClick: handleHistory.bind(null, record),
},
];
}

View File

@@ -0,0 +1,408 @@
<template>
<BasicModal
v-bind="$attrs"
destroyOnClose
:width="'88%'"
:title="modalTitle"
:showOkBtn="false"
cancelText="关闭"
wrapClassName="mixing-small-material-view-modal"
@register="registerModal"
>
<a-spin :spinning="loading">
<div class="mixing-small-sheet">
<table class="mixing-small-form-table">
<colgroup>
<col style="width: 22%" />
<col style="width: 42%" />
<col style="width: 9%" />
<col style="width: 9%" />
<col style="width: 9%" />
<col style="width: 9%" />
</colgroup>
<tbody>
<tr>
<th class="formTitle sheet-title-cell">{{ sheetTitle }}</th>
<td class="formValue info-cell"></td>
<th class="formTitle">起草</th>
<th class="formTitle">校对</th>
<th class="formTitle">审核</th>
<th class="formTitle">批准</th>
</tr>
<tr>
<th class="formTitle sheet-subtitle-cell">施工表</th>
<td class="formValue info-cell">发行处{{ displayText(mainForm.issueDept) }}</td>
<td class="formValue sign-cell">{{ displaySign(signDisplay.draftBy) }}</td>
<td class="formValue sign-cell">{{ displaySign(signDisplay.proofreadBy) }}</td>
<td class="formValue sign-cell">{{ displaySign(signDisplay.auditBy) }}</td>
<td class="formValue sign-cell">{{ displaySign(signDisplay.approveBy) }}</td>
</tr>
<tr>
<td class="formValue blank-cell"></td>
<td class="formValue info-cell">发行编号{{ displayText(mainForm.issueNumber) }}</td>
<td class="formValue sign-cell"></td>
<td class="formValue sign-cell"></td>
<td class="formValue sign-cell"></td>
<td class="formValue sign-cell"></td>
</tr>
<tr>
<td class="formValue blank-cell"></td>
<td class="formValue info-cell">作成日{{ displayText(mainForm.makeDate) }}</td>
<td class="formValue sign-cell"></td>
<td class="formValue sign-cell"></td>
<td class="formValue sign-cell"></td>
<td class="formValue sign-cell"></td>
</tr>
<tr>
<td class="formValue blank-cell"></td>
<td class="formValue info-cell">适用工厂{{ displayText(mainForm.applyFactory) }}</td>
<td class="formValue sign-cell"></td>
<td class="formValue sign-cell"></td>
<td class="formValue sign-cell"></td>
<td class="formValue sign-cell"></td>
</tr>
<tr>
<th class="formTitle key-label-cell">&emsp;</th>
<td class="formValue key-value-cell" colspan="5">{{ displayText(mainForm.specName) }}</td>
</tr>
<tr>
<th class="formTitle key-label-cell">&emsp;</th>
<td class="formValue key-value-cell" colspan="5">{{ displayText(mainForm.machineName) }}</td>
</tr>
</tbody>
</table>
<div class="unit-label">单位KG</div>
<template v-if="viewType === 'manual'">
<table class="mixing-small-data-table">
<thead>
<tr>
<th>药品CODE</th>
<th>标准重量</th>
<th>计量下限</th>
<th>计量上限</th>
<th>累计标准重量</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in manualRows" :key="`manual-${index}`">
<td>{{ row.materialCode }}</td>
<td>{{ formatWeight(row.standardWeight) }}</td>
<td>{{ formatWeight(row.lowerLimit) }}</td>
<td>{{ formatWeight(row.upperLimit) }}</td>
<td>{{ formatWeight(row.accumWeight) }}</td>
</tr>
<tr v-if="!manualRows.length">
<td colspan="5" class="empty-cell">暂无人工小料明细</td>
</tr>
</tbody>
</table>
</template>
<template v-else>
<div v-for="group in autoGroups" :key="group.groupKey" class="auto-group-block">
<div class="auto-group-title">{{ group.groupLabel }}</div>
<table class="mixing-small-data-table">
<thead>
<tr>
<th>药品CODE</th>
<th>标准重量</th>
<th>计量下限</th>
<th>计量上限</th>
<th>累计标准重量</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in group.rows" :key="`${group.groupKey}-${index}`">
<td>{{ row.materialCode }}</td>
<td>{{ formatWeight(row.standardWeight) }}</td>
<td>{{ formatWeight(row.lowerLimit) }}</td>
<td>{{ formatWeight(row.upperLimit) }}</td>
<td>{{ formatWeight(row.accumWeight) }}</td>
</tr>
<tr v-if="!group.rows.length">
<td colspan="5" class="empty-cell">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<div v-if="!autoGroups.length" class="empty-block">暂无自动小料明细</div>
</template>
<table class="mixing-small-remark-table">
<tbody>
<tr>
<th class="formTitle">备注</th>
<td class="formValue remark-cell"></td>
</tr>
</tbody>
</table>
</div>
</a-spin>
</BasicModal>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { queryById } from '../MesXslMixingSpec.api';
import {
MixingSmallMaterialAutoGroup,
MixingSmallMaterialViewType,
MixingSmallMaterialWeighRow,
buildMixingManualSmallMaterialRows,
groupMixingAutoSmallMaterials,
resolveMixingSmallMaterialViewTitle,
} from '../MesXslMixingSpec.data';
import { resolveFormulaSpecUserDisplayName } from '/@/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data';
import {
MIXING_SPEC_BIZ_TABLE,
buildBizSignDisplay,
loadRecordWithTrace,
} from '/@/views/xslmes/approval/integration/traceRecordHelper';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
const { createMessage } = useMessage();
const userStore = useUserStore();
const loading = ref(false);
const viewType = ref<MixingSmallMaterialViewType>('manual');
const mainForm = ref<Recordable>({});
const manualRows = ref<MixingSmallMaterialWeighRow[]>([]);
const autoGroups = ref<MixingSmallMaterialAutoGroup[]>([]);
const signDisplay = reactive({
draftBy: '',
proofreadBy: '',
auditBy: '',
approveBy: '',
});
const sheetTitle = computed(() => resolveMixingSmallMaterialViewTitle(viewType.value));
const modalTitle = computed(() => sheetTitle.value);
const [registerModal, { setModalProps }] = useModalInner(async (data) => {
viewType.value = data?.viewType === 'auto' ? 'auto' : 'manual';
mainForm.value = {};
manualRows.value = [];
autoGroups.value = [];
signDisplay.draftBy = '';
signDisplay.proofreadBy = '';
signDisplay.auditBy = '';
signDisplay.approveBy = '';
setModalProps({ loading: true });
loading.value = true;
try {
const record = data?.record || {};
if (!record?.id) {
createMessage.warning('未找到混炼示方记录');
return;
}
const detail = await loadRecordWithTrace(record.id, MIXING_SPEC_BIZ_TABLE, queryById, record);
mainForm.value = {
...detail,
issueDept: detail?.issueDept || detail?.applyFactory || '',
};
const sign = buildBizSignDisplay(detail, (row) =>
resolveFormulaSpecUserDisplayName(
row.draftBy || row.createBy,
row.draftBy_dictText || row.createBy_dictText,
userStore.getUserInfo || {},
),
);
signDisplay.draftBy = sign.draftBy;
signDisplay.proofreadBy = sign.proofreadBy;
signDisplay.auditBy = sign.auditBy;
signDisplay.approveBy = sign.approveBy;
const materialList = detail?.materialList || [];
if (viewType.value === 'manual') {
manualRows.value = buildMixingManualSmallMaterialRows(materialList, detail);
} else {
autoGroups.value = groupMixingAutoSmallMaterials(materialList, detail);
}
} catch (error) {
console.error(error);
createMessage.error('加载小料明细失败');
} finally {
loading.value = false;
setModalProps({ loading: false });
}
});
function displayText(value: unknown) {
if (value == null || value === '') {
return '';
}
return String(value);
}
function displaySign(value: unknown) {
const text = displayText(value);
return text || '—';
}
function formatWeight(value: number | null | undefined) {
if (value == null) {
return '';
}
const text = String(value);
if (text.includes('.')) {
return text.replace(/0+$/, '').replace(/\.$/, '');
}
return text;
}
</script>
<style lang="less" scoped>
@form-border: #ccc;
@form-label-bg: #f5f5f5;
.mixing-small-sheet {
border: 1px solid @form-border;
background: #fcfdfd;
overflow-x: auto;
}
.mixing-small-form-table,
.mixing-small-data-table,
.mixing-small-remark-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.mixing-small-form-table {
th.formTitle,
td.formValue {
border: 1px solid @form-border;
height: 30px;
padding: 0 8px;
font-size: 12px;
text-align: center;
vertical-align: middle;
}
th.formTitle {
background: @form-label-bg;
font-weight: 600;
color: #333;
}
td.formValue {
background: #fff;
color: #333;
}
th.sheet-title-cell {
font-size: 15px;
font-weight: 700;
letter-spacing: 1px;
}
th.sheet-subtitle-cell {
font-size: 14px;
font-weight: 700;
letter-spacing: 6px;
}
td.info-cell {
text-align: left;
}
td.blank-cell {
background: @form-label-bg;
}
th.key-label-cell {
font-size: 14px;
font-weight: 700;
}
td.key-value-cell {
height: 34px;
font-size: 15px;
font-weight: 700;
letter-spacing: 1px;
}
}
.unit-label {
padding: 6px 8px 0;
text-align: right;
font-size: 12px;
color: #333;
}
.mixing-small-data-table {
margin-top: 4px;
th,
td {
border: 1px solid @form-border;
height: 30px;
padding: 0 6px;
font-size: 12px;
text-align: center;
vertical-align: middle;
}
th {
background: @form-label-bg;
font-weight: 600;
color: #333;
}
td {
background: #fff;
color: #333;
}
.empty-cell {
color: #999;
height: 48px;
}
}
.auto-group-block {
margin-top: 10px;
}
.auto-group-title {
padding: 5px 8px;
font-size: 13px;
font-weight: 700;
color: #333;
text-align: center;
background: #e8e8e8;
border: 1px solid @form-border;
border-bottom: none;
}
.empty-block {
margin: 16px 0;
text-align: center;
color: #999;
font-size: 13px;
}
.mixing-small-remark-table {
margin-top: 10px;
th.formTitle {
width: 80px;
border: 1px solid @form-border;
background: @form-label-bg;
font-size: 12px;
font-weight: 600;
text-align: center;
vertical-align: middle;
}
td.remark-cell {
border: 1px solid @form-border;
height: 72px;
background: #fff;
}
}
</style>

View File

@@ -0,0 +1,171 @@
<template>
<BasicModal
v-bind="$attrs"
destroyOnClose
title="小料称重范围"
:width="560"
@register="registerModal"
@ok="handleSubmit"
>
<a-spin :spinning="loading">
<div class="weigh-range-tip">
计量下限 = 标准重量 下限容差计量上限 = 标准重量 + 上限容差单位KG默认容差 0.01
</div>
<div v-if="formState.specName" class="weigh-range-spec">规格{{ formState.specName }}</div>
<a-divider orientation="left">人工小料</a-divider>
<a-form layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="下限容差" required>
<a-input-number
v-model:value="formState.manualSmallWeighLowerTol"
:min="0"
:precision="6"
style="width: 100%"
placeholder="默认 0.01"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="上限容差" required>
<a-input-number
v-model:value="formState.manualSmallWeighUpperTol"
:min="0"
:precision="6"
style="width: 100%"
placeholder="默认 0.01"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<a-divider orientation="left">自动小料</a-divider>
<a-form layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="下限容差" required>
<a-input-number
v-model:value="formState.autoSmallWeighLowerTol"
:min="0"
:precision="6"
style="width: 100%"
placeholder="默认 0.01"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="上限容差" required>
<a-input-number
v-model:value="formState.autoSmallWeighUpperTol"
:min="0"
:precision="6"
style="width: 100%"
placeholder="默认 0.01"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-spin>
</BasicModal>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { queryById, updateSmallWeighRange } from '../MesXslMixingSpec.api';
import { MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE, buildMixingSmallWeighRangeForm } from '../MesXslMixingSpec.data';
import { useMessage } from '/@/hooks/web/useMessage';
const emit = defineEmits(['register', 'success']);
const { createMessage } = useMessage();
const loading = ref(false);
const formState = reactive({
id: '',
specName: '',
manualSmallWeighLowerTol: MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE,
manualSmallWeighUpperTol: MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE,
autoSmallWeighLowerTol: MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE,
autoSmallWeighUpperTol: MIXING_SMALL_MATERIAL_WEIGH_TOLERANCE,
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
Object.assign(formState, buildMixingSmallWeighRangeForm({}));
const record = data?.record || {};
if (!record?.id) {
createMessage.warning('未找到混炼示方记录');
return;
}
loading.value = true;
setModalProps({ confirmLoading: false });
try {
const detail = await queryById({ id: record.id });
Object.assign(formState, buildMixingSmallWeighRangeForm(detail || record));
} catch (error) {
console.error(error);
Object.assign(formState, buildMixingSmallWeighRangeForm(record));
createMessage.error('加载小料称重范围失败');
} finally {
loading.value = false;
}
});
function normalizeTol(value: unknown) {
const num = Number(value);
if (Number.isNaN(num) || num < 0) {
return null;
}
return num;
}
async function handleSubmit() {
if (!formState.id) {
createMessage.warning('未找到混炼示方记录');
return;
}
const payload = {
id: formState.id,
manualSmallWeighLowerTol: normalizeTol(formState.manualSmallWeighLowerTol),
manualSmallWeighUpperTol: normalizeTol(formState.manualSmallWeighUpperTol),
autoSmallWeighLowerTol: normalizeTol(formState.autoSmallWeighLowerTol),
autoSmallWeighUpperTol: normalizeTol(formState.autoSmallWeighUpperTol),
};
if (
payload.manualSmallWeighLowerTol == null ||
payload.manualSmallWeighUpperTol == null ||
payload.autoSmallWeighLowerTol == null ||
payload.autoSmallWeighUpperTol == null
) {
createMessage.warning('请填写有效的小料称重容差(不能为负数)');
return;
}
setModalProps({ confirmLoading: true });
try {
await updateSmallWeighRange(payload);
createMessage.success('保存成功');
closeModal();
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
.weigh-range-tip {
margin-bottom: 8px;
font-size: 12px;
color: #666;
line-height: 1.6;
}
.weigh-range-spec {
margin-bottom: 4px;
font-size: 13px;
font-weight: 600;
color: #333;
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
title="混炼示方历史记录"
:width="960"
destroyOnClose
:showOkBtn="false"
cancelText="关闭"
>
<a-spin :spinning="loading">
<a-descriptions bordered :column="3" size="small" class="mb-3">
<a-descriptions-item label="规格名">{{ specTitle || '-' }}</a-descriptions-item>
<a-descriptions-item label="发行编号">{{ issueNumber || '-' }}</a-descriptions-item>
<a-descriptions-item label="历史条数">{{ historyList.length }}</a-descriptions-item>
</a-descriptions>
<BasicTable
:columns="historyColumns"
:dataSource="historyList"
:pagination="false"
:canResize="false"
size="small"
bordered
rowKey="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button type="link" size="small" @click="handleViewDetail(record)">查看详情</a-button>
</template>
</template>
</BasicTable>
<a-empty v-if="!loading && historyList.length === 0" description="暂无历史记录" />
</a-spin>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicTable, BasicColumn } from '/@/components/Table';
import { listHistoryByMixingSpecId } from '../MesXslMixingSpecHistory.api';
const emit = defineEmits(['register', 'viewDetail']);
const loading = ref(false);
const specTitle = ref('');
const issueNumber = ref('');
const historyList = ref<Recordable[]>([]);
const historyColumns: BasicColumn[] = [
{ title: '版本号', dataIndex: 'versionNo', width: 90 },
{ title: '操作类型', dataIndex: 'actionType_dictText', width: 90 },
{ title: '操作人', dataIndex: 'operateByName', width: 120 },
{ title: '操作时间', dataIndex: 'operateTime', width: 170 },
{ title: '操作', dataIndex: 'action', width: 100, fixed: 'right' },
];
const [registerModal, { setModalProps }] = useModalInner(async (data) => {
historyList.value = [];
specTitle.value = data?.record?.specName || '';
issueNumber.value = data?.record?.issueNumber || '';
setModalProps({ confirmLoading: false });
const mixingSpecId = data?.record?.id;
if (!mixingSpecId) {
return;
}
loading.value = true;
try {
historyList.value = (await listHistoryByMixingSpecId({ mixingSpecId })) || [];
} finally {
loading.value = false;
}
});
function handleViewDetail(record: Recordable) {
emit('viewDetail', record);
}
</script>

View File

@@ -536,6 +536,7 @@ import {
MIXING_STEP_MIN_COLUMN_WIDTH,
} from '../MesXslMixingSpec.data';
import { saveOrUpdate, queryById } from '../MesXslMixingSpec.api';
import { queryHistoryById } from '../MesXslMixingSpecHistory.api';
import { resolveFormulaSpecUserDisplayName } from '/@/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data';
import { MIXING_SPEC_BIZ_TABLE, loadRecordWithTrace } from '/@/views/xslmes/approval/integration/traceRecordHelper';
import MesXslMixingMaterialColumnSetting from './MesXslMixingMaterialColumnSetting.vue';
@@ -556,6 +557,8 @@ const userStore = useUserStore();
const isUpdate = ref(false);
const showFooter = ref(true);
const isHistoryView = ref(false);
const historyVersion = ref('');
const mixerPsCompilePickerId = ref('');
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A34】混合步骤动作/组合下拉选项-----------
const mixerActionOptions = ref<{ title: string; value: string }[]>([]);
@@ -1245,11 +1248,21 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
downStepData.value = [];
tcuData.value = ensureTcuDefaultRows([]);
await loadMixerStepOptions('');
isHistoryView.value = !!data?.isHistoryView;
historyVersion.value = data?.historyVersion || '';
isUpdate.value = !!data?.isUpdate;
showFooter.value = !!data?.showFooter;
showFooter.value = !!data?.showFooter && !isHistoryView.value;
await setProps({ disabled: !showFooter.value });
setModalProps({ showOkBtn: showFooter.value, showCancelBtn: showFooter.value, confirmLoading: false });
if (isUpdate.value && data?.record?.id) {
if (isHistoryView.value && data?.historyId) {
const row = await queryHistoryById({ id: data.historyId });
if (!row?.id) {
createMessage.warning('未找到历史记录数据');
return;
}
historyVersion.value = historyVersion.value || data?.historyVersion || '';
await applyMixingSpecPageData(row, 'edit');
} else if (isUpdate.value && data?.record?.id) {
const row = await loadRecordWithTrace(data.record.id, MIXING_SPEC_BIZ_TABLE, queryById, data.record);
await applyMixingSpecPageData(row, 'edit');
} else {
@@ -1271,11 +1284,46 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
}
});
const title = computed(() => (!showFooter.value && unref(isUpdate) ? '混炼示方详情' : !unref(isUpdate) ? '新增混炼示方' : '编辑混炼示方'));
const title = computed(() => {
if (unref(isHistoryView)) {
const version = unref(historyVersion);
return version ? `混炼示方历史详情(${version}` : '混炼示方历史详情';
}
return !showFooter.value && unref(isUpdate) ? '混炼示方详情' : !unref(isUpdate) ? '新增混炼示方' : '编辑混炼示方';
});
function resolveValidateErrorMessage(error: unknown) {
const err = error as { errorFields?: { name?: string[]; errors?: string[] }[] };
const messages = (err?.errorFields || [])
.flatMap((item) => item?.errors || [])
.filter((msg) => !!msg);
if (messages.length) {
return messages.join('');
}
const fieldLabelMap: Recordable<string> = {
specName: '规格',
purpose: '用途',
issueNumber: '发行编号',
};
const fields = (err?.errorFields || [])
.map((item) => item?.name?.[0])
.filter(Boolean)
.map((name) => fieldLabelMap[name as string] || name);
if (fields.length) {
return `请完善必填项:${fields.join('、')}`;
}
return '请完善表单必填项';
}
async function handleSubmit() {
await syncSheetToForm();
const formValues = await validate();
let formValues: Recordable;
try {
formValues = await validate();
} catch (error) {
createMessage.warning(resolveValidateErrorMessage(error));
return;
}
const materialList = resolveMaterialTableRawRows();
applyMaterialAccumWeight(materialList);
const stepList = stepRef.value?.getTableData?.() || stepData.value;