Files
qhmes/jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data.ts

1034 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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() }));
}
/** 确保每行有唯一 idJVxeTable 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=APHR 写入第 1 混合段STEP=QPHR 写入当前混合段数的最后一列。
* 返回需写回表格的 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;
}
/**
* 混合段单元格编辑后:合计不超过 PHRSTEP=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' },
};