1034 lines
32 KiB
Vue
1034 lines
32 KiB
Vue
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||
import { JVxeColumn, JVxeTypes } from '/@/components/jeecg/JVxeTable/types';
|
||
import { buildUUID } from '/@/utils/uuid';
|
||
import { createLocalStorage } from '/@/utils/cache';
|
||
import { loadCategoryData } from '/@/api/common/api';
|
||
import { loadTreeData as loadCategoryTreeRoot } from '/@/views/system/category/category.api';
|
||
import { MATERIAL_CATEGORY_ROOT_CODE } from '/@/views/mes/material/MesMixerMaterialSysCategory.api';
|
||
|
||
const formulaLineColumnStorage = createLocalStorage();
|
||
|
||
/** 明细列隐藏偏好 localStorage 键 */
|
||
export const FORMULA_LINE_COLUMN_CACHE_KEY = 'mes_xsl_formula_spec_line_hidden_columns';
|
||
|
||
/** 不允许隐藏的明细列 */
|
||
export const FORMULA_LINE_LOCKED_COLUMN_KEYS = ['phr', 'mixerMaterialId'];
|
||
|
||
export interface FormulaLineColumnSettingItem {
|
||
key: string;
|
||
title: string;
|
||
locked?: boolean;
|
||
}
|
||
|
||
const colHalf = { span: 12 };
|
||
const colThird = { span: 8 };
|
||
const colQuarter = { span: 6 };
|
||
|
||
/** 汇总区隐藏字段(明细表 footer 行录入,表单仅承载数据) */
|
||
export const summaryFooterHiddenFields: FormSchema[] = [
|
||
{ field: 'aRubberTotalPhr', label: '', component: 'InputNumber', show: false },
|
||
{ field: 'totalPhr', label: '', component: 'InputNumber', show: false },
|
||
...Array.from({ length: 7 }, (_, index) => ({
|
||
field: `stage${index + 1}Total`,
|
||
label: '',
|
||
component: 'InputNumber',
|
||
show: false,
|
||
})),
|
||
];
|
||
|
||
export const SUMMARY_FOOTER_FIELD_KEYS = [
|
||
'aRubberTotalPhr',
|
||
'totalPhr',
|
||
'stage1Total',
|
||
'stage2Total',
|
||
'stage3Total',
|
||
'stage4Total',
|
||
'stage5Total',
|
||
'stage6Total',
|
||
'stage7Total',
|
||
] as const;
|
||
|
||
export const SUMMARY_METRICS_FIELD_KEYS = [
|
||
'naturalRubber',
|
||
'syntheticRubber',
|
||
'totalAmount',
|
||
'weightUnitPrice',
|
||
'volumeUnitPrice',
|
||
'qRubberSg',
|
||
'aRubberSg',
|
||
] as const;
|
||
|
||
/** 汇总指标隐藏字段(自定义表格录入,表单仅承载数据) */
|
||
export const summaryMetricsHiddenFields: FormSchema[] = SUMMARY_METRICS_FIELD_KEYS.map((field) => ({
|
||
field,
|
||
label: '',
|
||
component: 'InputNumber',
|
||
show: false,
|
||
}));
|
||
|
||
export const stageTotalNumberProps = {
|
||
min: 0,
|
||
precision: 4,
|
||
style: { width: '100%' },
|
||
bordered: false,
|
||
controls: false,
|
||
};
|
||
|
||
export const summaryMetricNumberProps = {
|
||
min: 0,
|
||
precision: 4,
|
||
style: { width: '100%' },
|
||
bordered: false,
|
||
controls: false,
|
||
};
|
||
|
||
export const summarySgNumberProps = {
|
||
min: 0,
|
||
precision: 6,
|
||
style: { width: '100%' },
|
||
bordered: false,
|
||
controls: false,
|
||
};
|
||
|
||
/** 明细行 PHR 合计(作为重量%计算的分母) */
|
||
export function calcTotalPhrFromLines(lines: Recordable[]): number | null {
|
||
const sum = (lines || []).reduce((acc, row) => {
|
||
const value = Number(row?.phr);
|
||
return acc + (Number.isFinite(value) ? value : 0);
|
||
}, 0);
|
||
return sum > 0 ? Number(sum.toFixed(4)) : null;
|
||
}
|
||
|
||
/** 按 单条PHR / 总PHR × 100 回填各行重量% */
|
||
export function applyWeightPercentToLines(lines: Recordable[]): void {
|
||
const totalPhr = calcTotalPhrFromLines(lines);
|
||
(lines || []).forEach((row) => {
|
||
if (!row) {
|
||
return;
|
||
}
|
||
const phr = Number(row.phr);
|
||
if (!Number.isFinite(phr) || totalPhr == null || totalPhr <= 0) {
|
||
row.weightPercent = null;
|
||
return;
|
||
}
|
||
row.weightPercent = Number(((phr / totalPhr) * 100).toFixed(1));
|
||
});
|
||
}
|
||
|
||
/** STEP=A 的 PHR 合计(A胶 TOTAL PHR) */
|
||
export function calcARubberTotalPhrFromLines(lines: Recordable[]): number | null {
|
||
const sum = (lines || []).reduce((acc, row) => {
|
||
if (row?.step !== 'A') {
|
||
return acc;
|
||
}
|
||
const value = Number(row?.phr);
|
||
return acc + (Number.isFinite(value) ? value : 0);
|
||
}, 0);
|
||
return sum > 0 ? Number(sum.toFixed(4)) : null;
|
||
}
|
||
|
||
/**
|
||
* 混合段累计合计(仅统计可编辑段数):
|
||
* stage1Total=列1总和,stage2Total=列1+列2,…;超出混合段数的框置空。
|
||
*/
|
||
export function calcStageTotalsFromLines(lines: Recordable[], mixingStages?: number | string | null): Recordable {
|
||
const totals: Recordable = {};
|
||
const stageCount = getActiveStageCount(mixingStages);
|
||
|
||
for (let i = 1; i <= 7; i++) {
|
||
totals[`stage${i}Total`] = null;
|
||
}
|
||
if (stageCount <= 0) {
|
||
return totals;
|
||
}
|
||
|
||
const colSums: number[] = [];
|
||
for (let i = 1; i <= stageCount; i++) {
|
||
const key = `stage${i}`;
|
||
const sum = (lines || []).reduce((acc, row) => {
|
||
const value = Number(row?.[key]);
|
||
return acc + (Number.isFinite(value) ? value : 0);
|
||
}, 0);
|
||
colSums.push(sum);
|
||
}
|
||
|
||
let cumulative = 0;
|
||
for (let i = 1; i <= stageCount; i++) {
|
||
cumulative += colSums[i - 1];
|
||
totals[`stage${i}Total`] = cumulative > 0 ? Number(cumulative.toFixed(4)) : null;
|
||
}
|
||
return totals;
|
||
}
|
||
|
||
/** 底部汇总区:A胶 TOTAL PHR、TOTAL PHR、混合段 1..N 累计合计(N=混合段数) */
|
||
export function calcFooterSummaryFromLines(lines: Recordable[], mixingStages?: number | string | null): Recordable {
|
||
return {
|
||
aRubberTotalPhr: calcARubberTotalPhrFromLines(lines),
|
||
totalPhr: calcTotalPhrFromLines(lines),
|
||
...calcStageTotalsFromLines(lines, mixingStages),
|
||
};
|
||
}
|
||
|
||
/** 明细行有效体积(优先使用已计算的 volume) */
|
||
function getLineVolumeValue(row: Recordable): number | null {
|
||
const volume = Number(row?.volume);
|
||
return Number.isFinite(volume) && volume > 0 ? volume : null;
|
||
}
|
||
|
||
/**
|
||
* A胶比重 = STEP=A 各行 PHR 合计 ÷ STEP=A 各行体积合计
|
||
* 单行体积 = PHR ÷ 物料比重
|
||
*/
|
||
export function calcARubberSgFromLines(lines: Recordable[]): number | null {
|
||
let totalPhr = 0;
|
||
let totalVolume = 0;
|
||
(lines || []).forEach((row) => {
|
||
if (row?.step !== 'A') {
|
||
return;
|
||
}
|
||
const phr = Number(row?.phr);
|
||
const volume = getLineVolumeValue(row);
|
||
if (!Number.isFinite(phr) || phr <= 0 || volume == null) {
|
||
return;
|
||
}
|
||
totalPhr += phr;
|
||
totalVolume += volume;
|
||
});
|
||
if (totalPhr <= 0 || totalVolume <= 0) {
|
||
return null;
|
||
}
|
||
return Number((totalPhr / totalVolume).toFixed(6));
|
||
}
|
||
|
||
/**
|
||
* Q胶比重 = STEP=Q 各行 PHR 合计 ÷ STEP=Q 各行体积合计
|
||
* 单行体积 = PHR ÷ 物料比重
|
||
*/
|
||
export function calcQRubberSgFromLines(lines: Recordable[]): number | null {
|
||
let totalPhr = 0;
|
||
let totalVolume = 0;
|
||
(lines || []).forEach((row) => {
|
||
if (row?.step !== 'Q') {
|
||
return;
|
||
}
|
||
const phr = Number(row?.phr);
|
||
const volume = getLineVolumeValue(row);
|
||
if (!Number.isFinite(phr) || phr <= 0 || volume == null) {
|
||
return;
|
||
}
|
||
totalPhr += phr;
|
||
totalVolume += volume;
|
||
});
|
||
if (totalPhr <= 0 || totalVolume <= 0) {
|
||
return null;
|
||
}
|
||
return Number((totalPhr / totalVolume).toFixed(6));
|
||
}
|
||
|
||
/** 默认物料小类编码(仅作初始配置参考) */
|
||
export const MIXER_MINOR_CATEGORY_CODE = {
|
||
NATURAL_RUBBER: 'XSLMES_MATERIAL_RAW_AUX_TRJ',
|
||
SYNTHETIC_RUBBER: 'XSLMES_MATERIAL_RAW_AUX_HCJ',
|
||
} as const;
|
||
|
||
export type RubberContentSetting = {
|
||
naturalMinorCategoryIds: string[];
|
||
syntheticMinorCategoryIds: string[];
|
||
};
|
||
|
||
export const EMPTY_RUBBER_CONTENT_SETTING: RubberContentSetting = {
|
||
naturalMinorCategoryIds: [],
|
||
syntheticMinorCategoryIds: [],
|
||
};
|
||
|
||
/** 将 MES物料分类 树递归展开为下拉选项(含大类、小类等全部节点) */
|
||
function flattenMaterialCategoryTreeToOptions(
|
||
nodes: Recordable[],
|
||
parentLabel = '',
|
||
): Array<{ label: string; value: string }> {
|
||
const options: Array<{ label: string; value: string }> = [];
|
||
(nodes || []).forEach((node) => {
|
||
const id = String(node?.key ?? node?.id ?? node?.value ?? '');
|
||
const name = String(node?.title ?? node?.name ?? node?.text ?? '');
|
||
if (!id || !name) {
|
||
return;
|
||
}
|
||
const label = parentLabel ? `${parentLabel} / ${name}` : name;
|
||
options.push({ label, value: id });
|
||
const children = (node?.children as Recordable[]) || [];
|
||
if (children.length) {
|
||
options.push(...flattenMaterialCategoryTreeToOptions(children, label));
|
||
}
|
||
});
|
||
return options;
|
||
}
|
||
|
||
/** 一次性加载 MES物料分类 全部节点,供含胶率设置手动多选 */
|
||
export async function fetchMaterialCategoryOptions(): Promise<Array<{ label: string; value: string }>> {
|
||
try {
|
||
const list = await loadCategoryData({ code: MATERIAL_CATEGORY_ROOT_CODE });
|
||
const rows = Array.isArray(list) ? list : [];
|
||
if (rows.length) {
|
||
return rows
|
||
.map((item: Recordable) => ({
|
||
label: String(item?.text ?? item?.label ?? item?.title ?? item?.name ?? ''),
|
||
value: String(item?.value ?? item?.id ?? item?.key ?? ''),
|
||
}))
|
||
.filter((item) => item.value && item.label);
|
||
}
|
||
} catch {
|
||
// 回退到与密炼物料页一致的分类树加载
|
||
}
|
||
const treeRaw = await loadCategoryTreeRoot({ async: false, pcode: MATERIAL_CATEGORY_ROOT_CODE });
|
||
const tree = Array.isArray(treeRaw) ? treeRaw : [];
|
||
return flattenMaterialCategoryTreeToOptions(tree);
|
||
}
|
||
|
||
/** @deprecated 请使用 fetchMaterialCategoryOptions */
|
||
export const fetchMixerMinorCategoryOptions = fetchMaterialCategoryOptions;
|
||
|
||
/** 汇总区比重指标:Q胶比重、A胶比重 */
|
||
export function calcRubberSgMetricsFromLines(lines: Recordable[]): Recordable {
|
||
return {
|
||
qRubberSg: calcQRubberSgFromLines(lines),
|
||
aRubberSg: calcARubberSgFromLines(lines),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 含胶率:天然橡胶 / 合成橡胶 = 对应小类密炼物料的重量%之和,合计为两者之和
|
||
*/
|
||
export function calcRubberContentMetricsFromLines(
|
||
lines: Recordable[],
|
||
naturalMinorCategoryIds?: string[] | null,
|
||
syntheticMinorCategoryIds?: string[] | null,
|
||
): Recordable {
|
||
const naturalIdSet = new Set((naturalMinorCategoryIds || []).filter(Boolean));
|
||
const syntheticIdSet = new Set((syntheticMinorCategoryIds || []).filter(Boolean));
|
||
let naturalRubber = 0;
|
||
let syntheticRubber = 0;
|
||
let hasNatural = false;
|
||
let hasSynthetic = false;
|
||
(lines || []).forEach((row) => {
|
||
if (!row?.mixerMinorCategoryId) {
|
||
return;
|
||
}
|
||
const weightPercent = Number(row?.weightPercent);
|
||
if (!Number.isFinite(weightPercent)) {
|
||
return;
|
||
}
|
||
if (naturalIdSet.has(row.mixerMinorCategoryId)) {
|
||
naturalRubber += weightPercent;
|
||
hasNatural = true;
|
||
} else if (syntheticIdSet.has(row.mixerMinorCategoryId)) {
|
||
syntheticRubber += weightPercent;
|
||
hasSynthetic = true;
|
||
}
|
||
});
|
||
const naturalValue = hasNatural ? Number(naturalRubber.toFixed(4)) : null;
|
||
const syntheticValue = hasSynthetic ? Number(syntheticRubber.toFixed(4)) : null;
|
||
const totalAmount =
|
||
naturalValue != null || syntheticValue != null
|
||
? Number(((naturalValue ?? 0) + (syntheticValue ?? 0)).toFixed(4))
|
||
: null;
|
||
return {
|
||
naturalRubber: naturalValue,
|
||
syntheticRubber: syntheticValue,
|
||
totalAmount,
|
||
};
|
||
}
|
||
|
||
/** 明细行仅前端展示字段(不落库) */
|
||
export const FORMULA_LINE_DISPLAY_FIELDS = ['mixerMajorCategoryText', 'mixerMinorCategoryText', 'mixerMinorCategoryId'] as const;
|
||
|
||
/** 写入密炼物料关联的大类/小类展示文本 */
|
||
export function applyMixerCategoryDisplay(row: Recordable, material?: Recordable | null) {
|
||
if (!row) {
|
||
return;
|
||
}
|
||
if (!material) {
|
||
row.mixerMajorCategoryText = '';
|
||
row.mixerMinorCategoryText = '';
|
||
row.mixerMinorCategoryId = null;
|
||
return;
|
||
}
|
||
row.mixerMajorCategoryText = material.majorCategoryId_dictText || material.majorCategoryText || '';
|
||
row.mixerMinorCategoryText = material.minorCategoryId_dictText || material.minorCategoryText || '';
|
||
row.mixerMinorCategoryId = material.minorCategoryId || null;
|
||
}
|
||
|
||
let materialCategoryNameCache: Map<string, string> | null = null;
|
||
|
||
async function getMaterialCategoryNameCache(): Promise<Map<string, string>> {
|
||
if (materialCategoryNameCache) {
|
||
return materialCategoryNameCache;
|
||
}
|
||
try {
|
||
const list = await loadCategoryData({ code: MATERIAL_CATEGORY_ROOT_CODE });
|
||
const rows = Array.isArray(list) ? list : [];
|
||
materialCategoryNameCache = new Map(
|
||
rows
|
||
.map((item: Recordable) => [String(item?.value ?? item?.id ?? ''), String(item?.text ?? item?.label ?? '')] as const)
|
||
.filter(([id, name]) => id && name),
|
||
);
|
||
} catch {
|
||
materialCategoryNameCache = new Map();
|
||
}
|
||
return materialCategoryNameCache;
|
||
}
|
||
|
||
/** 关联查询并补全大类/小类展示(queryById 无 dictText 时回退分类字典) */
|
||
export async function hydrateMixerCategoryDisplay(row: Recordable, material?: Recordable | null) {
|
||
applyMixerMaterialMeta(row, material);
|
||
if (!row || !material) {
|
||
return;
|
||
}
|
||
const needMajor = !row.mixerMajorCategoryText && material.majorCategoryId;
|
||
const needMinor = !row.mixerMinorCategoryText && material.minorCategoryId;
|
||
if (!needMajor && !needMinor) {
|
||
return;
|
||
}
|
||
const cache = await getMaterialCategoryNameCache();
|
||
if (needMajor) {
|
||
row.mixerMajorCategoryText = cache.get(String(material.majorCategoryId)) || '';
|
||
}
|
||
if (needMinor) {
|
||
row.mixerMinorCategoryText = cache.get(String(material.minorCategoryId)) || '';
|
||
}
|
||
}
|
||
|
||
/** 选择配合剂后回填物料关联信息(含大类/小类展示) */
|
||
export function applyMixerMaterialMeta(row: Recordable, material?: Recordable | null) {
|
||
if (!row) {
|
||
return;
|
||
}
|
||
if (!material) {
|
||
row.mixerMaterialName = '';
|
||
row.mixerMaterialCode = '';
|
||
applyMixerCategoryDisplay(row, null);
|
||
return;
|
||
}
|
||
row.mixerMaterialName = material.materialName || row.mixerMaterialName || '';
|
||
row.mixerMaterialCode = material.materialCode || row.mixerMaterialCode || '';
|
||
applyMixerCategoryDisplay(row, material);
|
||
if (!row.materialDesc) {
|
||
row.materialDesc = material.materialDesc || material.materialName || '';
|
||
}
|
||
}
|
||
|
||
/** 提交前剔除明细行展示字段 */
|
||
export function stripFormulaLineDisplayFields(line: Recordable) {
|
||
const { mixerMajorCategoryText, mixerMinorCategoryText, mixerMinorCategoryId, ...rest } = line;
|
||
return rest;
|
||
}
|
||
|
||
/** 明细表默认空行数(参照原系统约 20 行) */
|
||
export const DEFAULT_LINE_ROW_COUNT = 20;
|
||
|
||
export function createEmptyLineRows(count = DEFAULT_LINE_ROW_COUNT): Recordable[] {
|
||
return Array.from({ length: count }, () => ({ id: buildUUID() }));
|
||
}
|
||
|
||
/** 确保每行有唯一 id(JVxeTable rowKey 依赖 id,否则单元格无法渲染) */
|
||
export function normalizeLineRows(rows: Recordable[]): Recordable[] {
|
||
return (rows || []).map((r) => ({ ...r, id: r?.id || buildUUID() }));
|
||
}
|
||
|
||
function sectionTitle(label: string, field: string): FormSchema {
|
||
return {
|
||
field,
|
||
label,
|
||
component: 'Divider',
|
||
componentProps: { orientation: 'left', plain: false },
|
||
colProps: { span: 24 },
|
||
};
|
||
}
|
||
|
||
const hasWorkflowInfo = ({ values }) =>
|
||
!!(values.proofreadBy || values.proofreadTime || values.auditBy || values.auditTime || values.approveBy || values.approveTime);
|
||
|
||
export const columns: BasicColumn[] = [
|
||
{ title: '示方编号', align: 'center', dataIndex: 'specCode', width: 150, fixed: 'left' },
|
||
{ title: '发行编号', align: 'center', dataIndex: 'issueNumber', width: 140 },
|
||
{ title: '胶料代号', align: 'center', dataIndex: 'rubberCode', width: 140 },
|
||
{ title: '分类', align: 'center', dataIndex: 'category_dictText', width: 80 },
|
||
{ title: '用途', align: 'center', dataIndex: 'purpose', width: 120, ellipsis: true },
|
||
{
|
||
title: '人工配料',
|
||
align: 'center',
|
||
dataIndex: 'hasManualBatch',
|
||
width: 90,
|
||
customRender: ({ text }) => (text === 1 ? '是' : '否'),
|
||
},
|
||
{ title: '发行日期', align: 'center', dataIndex: 'issueDate', width: 110 },
|
||
{
|
||
title: '制定人',
|
||
align: 'center',
|
||
dataIndex: 'createBy',
|
||
width: 100,
|
||
customRender: ({ record }) => record?.createBy_dictText || record?.createBy || '',
|
||
},
|
||
{ title: '审核人', align: 'center', dataIndex: 'auditBy', width: 100, defaultHidden: true },
|
||
{ title: '批准人', align: 'center', dataIndex: 'approveBy', width: 100, defaultHidden: true },
|
||
{ title: '状态', align: 'center', dataIndex: 'status_dictText', width: 120 },
|
||
{ title: '混合段数', align: 'center', dataIndex: 'mixingStages', width: 90, defaultHidden: true },
|
||
{ title: 'TOTAL PHR', align: 'center', dataIndex: 'totalPhr', width: 100, defaultHidden: true },
|
||
{ title: '修改时间', align: 'center', dataIndex: 'updateTime', width: 165 },
|
||
];
|
||
|
||
export const searchFormSchema: FormSchema[] = [
|
||
{ label: '关键字', field: 'keyword', component: 'Input', colProps: { span: 6 }, componentProps: { placeholder: '示方编号/胶料代号/发行编号/用途' } },
|
||
{
|
||
label: '分类',
|
||
field: 'category',
|
||
component: 'JDictSelectTag',
|
||
componentProps: { dictCode: 'xslmes_formula_spec_category', placeholder: '请选择分类' },
|
||
colProps: { span: 6 },
|
||
},
|
||
{
|
||
label: '状态',
|
||
field: 'status',
|
||
component: 'JDictSelectTag',
|
||
componentProps: { dictCode: 'xslmes_formula_spec_status', placeholder: '请选择状态' },
|
||
colProps: { span: 6 },
|
||
},
|
||
{
|
||
label: '发行日期起',
|
||
field: 'issueDate_begin',
|
||
component: 'DatePicker',
|
||
componentProps: { valueFormat: 'YYYY-MM-DD', placeholder: '开始日期' },
|
||
colProps: { span: 6 },
|
||
},
|
||
{
|
||
label: '发行日期止',
|
||
field: 'issueDate_end',
|
||
component: 'DatePicker',
|
||
componentProps: { valueFormat: 'YYYY-MM-DD', placeholder: '结束日期' },
|
||
colProps: { span: 6 },
|
||
},
|
||
];
|
||
|
||
/** 基本信息(顶部) */
|
||
export const basicFormSchema: FormSchema[] = [
|
||
{ label: '', field: 'id', component: 'Input', show: false },
|
||
{
|
||
label: '分类',
|
||
field: 'category',
|
||
component: 'JDictSelectTag',
|
||
defaultValue: 'S',
|
||
show: false,
|
||
componentProps: { dictCode: 'xslmes_formula_spec_category', type: 'radioButton' },
|
||
colProps: { span: 24 },
|
||
},
|
||
{
|
||
label: '示方编号',
|
||
field: 'specCode',
|
||
component: 'Input',
|
||
show: false,
|
||
dynamicRules: () => [{ required: true, message: '请选择胶料并生成示方编号' }],
|
||
},
|
||
{
|
||
label: '胶料代号',
|
||
field: 'rubberCode',
|
||
component: 'Input',
|
||
show: false,
|
||
dynamicRules: () => [{ required: true, message: '请选择胶料并生成胶料代号' }],
|
||
},
|
||
{
|
||
label: '',
|
||
field: 'rubberMaterialId',
|
||
component: 'Input',
|
||
show: false,
|
||
},
|
||
{
|
||
label: '基本配合',
|
||
field: 'basicFormula',
|
||
component: 'Input',
|
||
show: false,
|
||
},
|
||
{
|
||
label: '发行日期',
|
||
field: 'issueDate',
|
||
component: 'Input',
|
||
show: false,
|
||
},
|
||
{
|
||
label: '发行编号',
|
||
field: 'issueNumber',
|
||
component: 'Input',
|
||
show: false,
|
||
},
|
||
{
|
||
label: '用途',
|
||
field: 'purpose',
|
||
component: 'Input',
|
||
show: false,
|
||
},
|
||
{
|
||
label: '混合段数',
|
||
field: 'mixingStages',
|
||
component: 'InputNumber',
|
||
show: false,
|
||
},
|
||
{
|
||
label: '混合机器',
|
||
field: 'mixingMachine',
|
||
component: 'Input',
|
||
show: false,
|
||
},
|
||
{
|
||
label: '发行部门',
|
||
field: 'issueDeptName',
|
||
component: 'Input',
|
||
show: false,
|
||
},
|
||
{
|
||
label: '',
|
||
field: 'issueDeptId',
|
||
component: 'Input',
|
||
show: false,
|
||
},
|
||
{
|
||
label: '状态',
|
||
field: 'status',
|
||
component: 'JDictSelectTag',
|
||
defaultValue: 'compile',
|
||
show: false,
|
||
componentProps: { dictCode: 'xslmes_formula_spec_status', disabled: true },
|
||
colProps: colThird,
|
||
},
|
||
];
|
||
|
||
/** 汇总信息(底部,参照原配合施工表布局) */
|
||
export const summaryFormSchema: FormSchema[] = [
|
||
...summaryFooterHiddenFields,
|
||
...summaryMetricsHiddenFields,
|
||
{
|
||
label: '发行理由',
|
||
field: 'issueReason',
|
||
component: 'InputTextArea',
|
||
colProps: { span: 24 },
|
||
componentProps: { rows: 2, placeholder: '请输入发行理由', maxlength: 1000, showCount: true, bordered: false },
|
||
},
|
||
];
|
||
|
||
/** 审批记录(详情时展示) */
|
||
export const workflowFormSchema: FormSchema[] = [
|
||
sectionTitle('审批记录', 'dividerWorkflow'),
|
||
{
|
||
label: '校对人',
|
||
field: 'proofreadBy',
|
||
component: 'Input',
|
||
componentProps: { disabled: true, bordered: false },
|
||
colProps: colHalf,
|
||
ifShow: ({ values }) => !!values.proofreadBy,
|
||
},
|
||
{
|
||
label: '校对时间',
|
||
field: 'proofreadTime',
|
||
component: 'Input',
|
||
componentProps: { disabled: true, bordered: false },
|
||
colProps: colHalf,
|
||
ifShow: ({ values }) => !!values.proofreadTime,
|
||
},
|
||
{
|
||
label: '审核人',
|
||
field: 'auditBy',
|
||
component: 'Input',
|
||
componentProps: { disabled: true, bordered: false },
|
||
colProps: colHalf,
|
||
ifShow: ({ values }) => !!values.auditBy,
|
||
},
|
||
{
|
||
label: '审核时间',
|
||
field: 'auditTime',
|
||
component: 'Input',
|
||
componentProps: { disabled: true, bordered: false },
|
||
colProps: colHalf,
|
||
ifShow: ({ values }) => !!values.auditTime,
|
||
},
|
||
{
|
||
label: '批准人',
|
||
field: 'approveBy',
|
||
component: 'Input',
|
||
componentProps: { disabled: true, bordered: false },
|
||
colProps: colHalf,
|
||
ifShow: ({ values }) => !!values.approveBy,
|
||
},
|
||
{
|
||
label: '批准时间',
|
||
field: 'approveTime',
|
||
component: 'Input',
|
||
componentProps: { disabled: true, bordered: false },
|
||
colProps: colHalf,
|
||
ifShow: ({ values }) => !!values.approveTime,
|
||
},
|
||
];
|
||
|
||
/** 有效混合段数(1-7),未配置时返回 0 */
|
||
export function getActiveStageCount(mixingStages?: number | string | null): number {
|
||
if (mixingStages == null || mixingStages === '') {
|
||
return 0;
|
||
}
|
||
const n = Number(mixingStages);
|
||
if (Number.isNaN(n) || n <= 0) {
|
||
return 0;
|
||
}
|
||
return Math.min(Math.max(Math.floor(n), 1), 7);
|
||
}
|
||
|
||
/**
|
||
* STEP=A:PHR 写入第 1 混合段;STEP=Q:PHR 写入当前混合段数的最后一列。
|
||
* 返回需写回表格的 stage1-stage7 补丁;非 A/Q 或无可编辑段时返回 null。
|
||
*/
|
||
export function buildStepStageValues(row: Recordable, mixingStages?: number | null): Recordable | null {
|
||
const step = row?.step;
|
||
if (step !== 'A' && step !== 'Q') {
|
||
return null;
|
||
}
|
||
const stageCount = getActiveStageCount(mixingStages);
|
||
if (stageCount <= 0) {
|
||
return null;
|
||
}
|
||
const phrRaw = row?.phr;
|
||
const phr =
|
||
phrRaw != null && phrRaw !== '' && Number.isFinite(Number(phrRaw)) ? Number(phrRaw) : null;
|
||
const targetStage = step === 'A' ? 1 : stageCount;
|
||
const patch: Recordable = {};
|
||
for (let i = 1; i <= 7; i++) {
|
||
patch[`stage${i}`] = i === targetStage ? phr : null;
|
||
}
|
||
return patch;
|
||
}
|
||
|
||
/** 将 STEP 规则应用到行对象(就地修改) */
|
||
export function applyStepPhrToLineRow(row: Recordable, mixingStages?: number | null): Recordable | null {
|
||
const patch = buildStepStageValues(row, mixingStages);
|
||
if (!patch) {
|
||
return null;
|
||
}
|
||
Object.assign(row, patch);
|
||
return patch;
|
||
}
|
||
|
||
/** 解析行 PHR(无效时返回 null) */
|
||
export function getLinePhrValue(row: Recordable): number | null {
|
||
const phr = Number(row?.phr);
|
||
return Number.isFinite(phr) && phr > 0 ? phr : null;
|
||
}
|
||
|
||
/** 可编辑混合段列合计(仅统计 1..stageCount) */
|
||
export function calcLineActiveStageSum(row: Recordable, stageCount: number): number {
|
||
let sum = 0;
|
||
for (let i = 1; i <= stageCount; i++) {
|
||
const v = Number(row[`stage${i}`]);
|
||
if (Number.isFinite(v)) {
|
||
sum += v;
|
||
}
|
||
}
|
||
return sum;
|
||
}
|
||
|
||
/** 混合段合计超过 PHR 时,扣减当前编辑列 */
|
||
export function clampStageToPhr(row: Recordable, stageCount: number, editedKey: string): number | null {
|
||
const phr = getLinePhrValue(row);
|
||
if (phr == null || stageCount <= 0) {
|
||
return null;
|
||
}
|
||
const total = calcLineActiveStageSum(row, stageCount);
|
||
if (total <= phr) {
|
||
return null;
|
||
}
|
||
const current = Number(row[editedKey]);
|
||
if (!Number.isFinite(current)) {
|
||
return null;
|
||
}
|
||
const clamped = Math.max(0, Number((current - (total - phr)).toFixed(4)));
|
||
row[editedKey] = clamped;
|
||
return clamped;
|
||
}
|
||
|
||
/** STEP=A:手动编辑 2..N 列时,第 1 列 = PHR − 其余可编辑段之和 */
|
||
export function balanceStepAStages(row: Recordable, stageCount: number, editedKey?: string): Recordable {
|
||
const patch: Recordable = {};
|
||
if (editedKey === 'stage1') {
|
||
return patch;
|
||
}
|
||
const phr = getLinePhrValue(row);
|
||
if (phr == null || stageCount <= 0) {
|
||
return patch;
|
||
}
|
||
let otherSum = 0;
|
||
for (let i = 2; i <= stageCount; i++) {
|
||
const v = Number(row[`stage${i}`]);
|
||
if (Number.isFinite(v)) {
|
||
otherSum += v;
|
||
}
|
||
}
|
||
const stage1 = Math.max(0, Number((phr - otherSum).toFixed(4)));
|
||
const next = otherSum > 0 ? stage1 : phr;
|
||
if (Number(row.stage1) !== next) {
|
||
row.stage1 = next;
|
||
patch.stage1 = next;
|
||
}
|
||
return patch;
|
||
}
|
||
|
||
/** STEP=Q:手动编辑非末列时,末列 = PHR − 其余可编辑段之和 */
|
||
export function balanceStepQStages(row: Recordable, stageCount: number, editedKey?: string): Recordable {
|
||
const patch: Recordable = {};
|
||
const phr = getLinePhrValue(row);
|
||
if (phr == null || stageCount <= 0) {
|
||
return patch;
|
||
}
|
||
const lastKey = `stage${stageCount}`;
|
||
if (editedKey === lastKey) {
|
||
return patch;
|
||
}
|
||
let otherSum = 0;
|
||
for (let i = 1; i < stageCount; i++) {
|
||
const v = Number(row[`stage${i}`]);
|
||
if (Number.isFinite(v)) {
|
||
otherSum += v;
|
||
}
|
||
}
|
||
const lastVal = Math.max(0, Number((phr - otherSum).toFixed(4)));
|
||
const next = otherSum > 0 ? lastVal : phr;
|
||
if (Number(row[lastKey]) !== next) {
|
||
row[lastKey] = next;
|
||
patch[lastKey] = next;
|
||
}
|
||
return patch;
|
||
}
|
||
|
||
/**
|
||
* 混合段单元格编辑后:合计不超过 PHR;STEP=A/Q 自动平衡锚定列。
|
||
* STEP=A 编辑 2..N 列、STEP=Q 编辑非末列时:先扣减锚定列,再判断是否需截断当前列。
|
||
*/
|
||
export function processStageCellEdit(
|
||
row: Recordable,
|
||
editedKey: string,
|
||
mixingStages?: number | string | null
|
||
): { patch: Recordable; exceeded: boolean; needPhr: boolean } {
|
||
const patch: Recordable = {};
|
||
const stageCount = getActiveStageCount(mixingStages);
|
||
if (stageCount <= 0 || !/^stage\d+$/.test(editedKey)) {
|
||
return { patch, exceeded: false, needPhr: false };
|
||
}
|
||
const phr = getLinePhrValue(row);
|
||
if (phr == null) {
|
||
return { patch, exceeded: false, needPhr: true };
|
||
}
|
||
|
||
const lastKey = `stage${stageCount}`;
|
||
let exceeded = false;
|
||
|
||
const mergePatch = (p: Recordable) => {
|
||
Object.keys(p).forEach((k) => {
|
||
patch[k] = p[k];
|
||
});
|
||
};
|
||
|
||
// STEP=A:编辑第 2..N 列时先从第 1 列扣减,避免误把当前列截断为 0
|
||
if (row.step === 'A' && editedKey !== 'stage1') {
|
||
mergePatch(balanceStepAStages(row, stageCount, editedKey));
|
||
const clamped = clampStageToPhr(row, stageCount, editedKey);
|
||
if (clamped != null) {
|
||
patch[editedKey] = clamped;
|
||
exceeded = true;
|
||
mergePatch(balanceStepAStages(row, stageCount, editedKey));
|
||
}
|
||
return { patch, exceeded, needPhr: false };
|
||
}
|
||
|
||
// STEP=Q:编辑非末列时先从末列扣减
|
||
if (row.step === 'Q' && editedKey !== lastKey) {
|
||
mergePatch(balanceStepQStages(row, stageCount, editedKey));
|
||
const clamped = clampStageToPhr(row, stageCount, editedKey);
|
||
if (clamped != null) {
|
||
patch[editedKey] = clamped;
|
||
exceeded = true;
|
||
mergePatch(balanceStepQStages(row, stageCount, editedKey));
|
||
}
|
||
return { patch, exceeded, needPhr: false };
|
||
}
|
||
|
||
// 编辑锚定列或无 STEP:仅截断当前列
|
||
const clamped = clampStageToPhr(row, stageCount, editedKey);
|
||
if (clamped != null) {
|
||
patch[editedKey] = clamped;
|
||
exceeded = true;
|
||
}
|
||
|
||
return { patch, exceeded, needPhr: false };
|
||
}
|
||
|
||
/** 根据混合段数动态生成明细列(1-7 段列按混合段数控制可编辑;未填混合段数时全部禁用) */
|
||
export function buildLineJVxeColumns(mixingStages?: number | null, tableDisabled = false): JVxeColumn[] {
|
||
const stageCount = getActiveStageCount(mixingStages);
|
||
const hasStages = stageCount > 0;
|
||
|
||
const baseCols: JVxeColumn[] = [
|
||
{
|
||
title: 'PHR',
|
||
key: 'phr',
|
||
type: JVxeTypes.inputNumber,
|
||
minWidth: 90,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '配合剂',
|
||
key: 'mixerMaterialId',
|
||
type: JVxeTypes.slot,
|
||
slotName: 'mixerMaterialSlot',
|
||
minWidth: 200,
|
||
},
|
||
{
|
||
title: '物料大类',
|
||
key: 'mixerMajorCategoryText',
|
||
type: JVxeTypes.normal,
|
||
minWidth: 110,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '物料小类',
|
||
key: 'mixerMinorCategoryText',
|
||
type: JVxeTypes.normal,
|
||
minWidth: 110,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '物料描述',
|
||
key: 'materialDesc',
|
||
type: JVxeTypes.input,
|
||
minWidth: 160,
|
||
},
|
||
{
|
||
title: 'STEP',
|
||
key: 'step',
|
||
type: JVxeTypes.select,
|
||
minWidth: 80,
|
||
align: 'center',
|
||
dictCode: 'xslmes_formula_spec_step',
|
||
},
|
||
{
|
||
title: '自动/人工',
|
||
key: 'weighMode',
|
||
type: JVxeTypes.select,
|
||
minWidth: 110,
|
||
align: 'center',
|
||
dictCode: 'xslmes_formula_spec_weigh_mode',
|
||
},
|
||
{
|
||
title: '重量%',
|
||
key: 'weightPercent',
|
||
type: JVxeTypes.inputNumber,
|
||
minWidth: 90,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '体积',
|
||
key: 'volume',
|
||
type: JVxeTypes.inputNumber,
|
||
minWidth: 90,
|
||
align: 'center',
|
||
placeholder: '自动计算',
|
||
},
|
||
{
|
||
title: '备注',
|
||
key: 'remark',
|
||
type: JVxeTypes.input,
|
||
minWidth: 80,
|
||
},
|
||
];
|
||
|
||
for (let i = 1; i <= 7; i++) {
|
||
const stageNo = i;
|
||
const stageDisabled = tableDisabled || !hasStages || stageNo > stageCount;
|
||
baseCols.push({
|
||
title: String(i),
|
||
key: `stage${i}`,
|
||
type: JVxeTypes.inputNumber,
|
||
minWidth: 56,
|
||
align: 'center',
|
||
disabled: stageDisabled,
|
||
className: stageDisabled ? 'formula-stage-cell-disabled' : '',
|
||
headerClassName: stageDisabled ? 'formula-stage-header-disabled' : '',
|
||
props: {
|
||
isDisabledCell: () => stageDisabled,
|
||
},
|
||
});
|
||
}
|
||
return baseCols;
|
||
}
|
||
|
||
/** 明细列设置项(1-7 段列固定展示在设置列表中) */
|
||
export function getFormulaLineColumnSettingItems(): FormulaLineColumnSettingItem[] {
|
||
return buildLineJVxeColumns(null, false).map((col) => ({
|
||
key: String(col.key),
|
||
title: String(col.title),
|
||
locked: FORMULA_LINE_LOCKED_COLUMN_KEYS.includes(String(col.key)),
|
||
}));
|
||
}
|
||
|
||
/** 读取已隐藏的明细列 key */
|
||
export function loadFormulaLineHiddenColumnKeys(): string[] {
|
||
const saved = formulaLineColumnStorage.get(FORMULA_LINE_COLUMN_CACHE_KEY);
|
||
if (!Array.isArray(saved)) {
|
||
return [];
|
||
}
|
||
const validKeys = new Set(getFormulaLineColumnSettingItems().map((item) => item.key));
|
||
return saved.filter(
|
||
(key) => typeof key === 'string' && !FORMULA_LINE_LOCKED_COLUMN_KEYS.includes(key) && validKeys.has(key),
|
||
);
|
||
}
|
||
|
||
/** 保存已隐藏的明细列 key */
|
||
export function saveFormulaLineHiddenColumnKeys(hiddenKeys: string[]) {
|
||
const validKeys = hiddenKeys.filter((key) => !FORMULA_LINE_LOCKED_COLUMN_KEYS.includes(key));
|
||
if (validKeys.length) {
|
||
formulaLineColumnStorage.set(FORMULA_LINE_COLUMN_CACHE_KEY, validKeys);
|
||
return;
|
||
}
|
||
formulaLineColumnStorage.remove(FORMULA_LINE_COLUMN_CACHE_KEY);
|
||
}
|
||
|
||
/** 按隐藏配置过滤明细列 */
|
||
export function applyFormulaLineColumnVisibility(columns: JVxeColumn[], hiddenKeys: string[]): JVxeColumn[] {
|
||
if (!hiddenKeys?.length) {
|
||
return columns;
|
||
}
|
||
const hiddenSet = new Set(hiddenKeys);
|
||
return columns.filter((col) => !col.key || !hiddenSet.has(String(col.key)));
|
||
}
|
||
|
||
/** 审批进度操作人展示:优先字典翻译姓名,其次当前登录用户匹配用户名时取 realname */
|
||
export function resolveFormulaSpecUserDisplayName(
|
||
username?: string | null,
|
||
dictText?: string | null,
|
||
fallbackUser?: { username?: string; realname?: string },
|
||
) {
|
||
if (dictText != null && dictText !== '') {
|
||
return String(dictText);
|
||
}
|
||
const user = fallbackUser || {};
|
||
if (username != null && username !== '') {
|
||
const name = String(username);
|
||
if (user.username && name === user.username && user.realname) {
|
||
return user.realname;
|
||
}
|
||
return name;
|
||
}
|
||
return user.realname || user.username || '';
|
||
}
|
||
|
||
export const superQuerySchema = {
|
||
specCode: { title: '示方编号', order: 0, view: 'text' },
|
||
rubberCode: { title: '胶料代号', order: 1, view: 'text' },
|
||
issueNumber: { title: '发行编号', order: 2, view: 'text' },
|
||
category: { title: '分类', order: 3, view: 'list', dictCode: 'xslmes_formula_spec_category' },
|
||
purpose: { title: '用途', order: 4, view: 'text' },
|
||
status: { title: '状态', order: 5, view: 'list', dictCode: 'xslmes_formula_spec_status' },
|
||
issueDate: { title: '发行日期', order: 6, view: 'date' },
|
||
};
|