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

1034 lines
32 KiB
Vue
Raw Normal View History

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' },
};