完善MECMAS21参数配置与分组交互
This commit is contained in:
1 parent
32f21f08ff
commit
be54070855
28 files changed
+3418
-202
No files matched your search
+361
-72
@@ -95,6 +95,7 @@ import {
|
||||
loadParameterColumnLayout,
|
||||
saveParameterColumnLayout,
|
||||
type ParameterColumnLayout,
|
||||
type ParameterTableGroup,
|
||||
type ParameterTableRow,
|
||||
} from "./ParameterTable";
|
||||
import {
|
||||
@@ -174,8 +175,9 @@ type ParameterDefinition = {
|
||||
min?: number;
|
||||
max?: number;
|
||||
minExclusive?: boolean;
|
||||
editor?: "amesimGasReference" | "amesimGasPropertyModel";
|
||||
editor?: "choice" | "amesimGasReference" | "amesimGasPropertyModel";
|
||||
options?: ParameterOption[];
|
||||
visibleWhen?: ParameterVisibilityCondition[];
|
||||
};
|
||||
|
||||
type ParameterOption = {
|
||||
@@ -183,6 +185,19 @@ type ParameterOption = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type ParameterVisibilityCondition = {
|
||||
parameter: string;
|
||||
values: number[];
|
||||
};
|
||||
|
||||
type ParameterGroupDefinition = {
|
||||
id: string;
|
||||
label: string;
|
||||
order: number;
|
||||
parameters: string[];
|
||||
defaultExpanded: boolean;
|
||||
};
|
||||
|
||||
type ComponentDefinition = {
|
||||
type: string;
|
||||
label: string;
|
||||
@@ -198,6 +213,7 @@ type ComponentDefinition = {
|
||||
role?: "amesimGasMediumDefinition";
|
||||
ports: PortDefinition[];
|
||||
parameters: Record<string, ParameterDefinition>;
|
||||
parameterGroups?: ParameterGroupDefinition[];
|
||||
};
|
||||
|
||||
type ComponentLibraryDefinition = {
|
||||
@@ -220,8 +236,9 @@ type ComponentCatalogParameterPayload = {
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minimumExclusive: boolean;
|
||||
editor?: "amesimGasReference" | "amesimGasPropertyModel";
|
||||
editor?: "choice" | "amesimGasReference" | "amesimGasPropertyModel";
|
||||
options?: ParameterOption[];
|
||||
visibleWhen?: ParameterVisibilityCondition[];
|
||||
};
|
||||
|
||||
type ComponentCatalogComponentPayload = {
|
||||
@@ -239,6 +256,13 @@ type ComponentCatalogComponentPayload = {
|
||||
role?: "amesimGasMediumDefinition";
|
||||
ports: Array<PortDefinition & { order: number }>;
|
||||
parameters: ComponentCatalogParameterPayload[];
|
||||
parameterGroups?: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
order: number;
|
||||
parameters: string[];
|
||||
defaultExpanded?: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
type ComponentCatalogResponse = {
|
||||
@@ -313,7 +337,8 @@ type ProjectEdgePayload = {
|
||||
|
||||
type ReactFlowProjectPayload = {
|
||||
name: string;
|
||||
mediumReferenceVersion?: 1;
|
||||
mediumReferenceVersion?: 1 | null;
|
||||
amesimParameterEncodingVersion?: 1 | null;
|
||||
nodes: ProjectNodePayload[];
|
||||
edges: ProjectEdgePayload[];
|
||||
simulation: SimulationConfig;
|
||||
@@ -1055,6 +1080,58 @@ function normalizeComponentCatalog(payload: unknown): ComponentLibraryDefinition
|
||||
}
|
||||
componentTypes.add(component.type);
|
||||
|
||||
const parameters = Object.fromEntries(
|
||||
component.parameters.map((parameter) => {
|
||||
const visibleWhen = normalizeParameterVisibilityConditions(
|
||||
parameter.visibleWhen,
|
||||
);
|
||||
return [
|
||||
parameter.name,
|
||||
{
|
||||
label: parameter.label || parameter.name,
|
||||
...(typeof parameter.description === "string" &&
|
||||
parameter.description.trim()
|
||||
? { description: parameter.description.trim() }
|
||||
: {}),
|
||||
...(parameter.unit ? { unit: parameter.unit } : {}),
|
||||
...(parameter.quantity && parameter.quantity !== "dimensionless"
|
||||
? { quantity: parameter.quantity }
|
||||
: {}),
|
||||
default: parameter.default,
|
||||
...(parameter.minimum !== undefined
|
||||
? { min: parameter.minimum }
|
||||
: {}),
|
||||
...(parameter.maximum !== undefined
|
||||
? { max: parameter.maximum }
|
||||
: {}),
|
||||
...(parameter.minimumExclusive
|
||||
? { minExclusive: true }
|
||||
: {}),
|
||||
...(parameter.editor === "choice" ||
|
||||
parameter.editor === "amesimGasReference" ||
|
||||
parameter.editor === "amesimGasPropertyModel"
|
||||
? { editor: parameter.editor }
|
||||
: {}),
|
||||
...(Array.isArray(parameter.options)
|
||||
? {
|
||||
options: parameter.options.flatMap((option) =>
|
||||
Number.isFinite(Number(option?.value)) &&
|
||||
typeof option?.label === "string"
|
||||
? [{ value: Number(option.value), label: option.label }]
|
||||
: [],
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(visibleWhen ? { visibleWhen } : {}),
|
||||
},
|
||||
] as const;
|
||||
}),
|
||||
);
|
||||
const parameterGroups = normalizeParameterGroups(
|
||||
component.parameterGroups,
|
||||
new Set(Object.keys(parameters)),
|
||||
);
|
||||
|
||||
return {
|
||||
type: component.type,
|
||||
label: component.label,
|
||||
@@ -1080,46 +1157,8 @@ function normalizeComponentCatalog(payload: unknown): ComponentLibraryDefinition
|
||||
: {}),
|
||||
side: port.side,
|
||||
})),
|
||||
parameters: Object.fromEntries(
|
||||
component.parameters.map((parameter) => [
|
||||
parameter.name,
|
||||
{
|
||||
label: parameter.label || parameter.name,
|
||||
...(typeof parameter.description === "string" &&
|
||||
parameter.description.trim()
|
||||
? { description: parameter.description.trim() }
|
||||
: {}),
|
||||
...(parameter.unit ? { unit: parameter.unit } : {}),
|
||||
...(parameter.quantity && parameter.quantity !== "dimensionless"
|
||||
? { quantity: parameter.quantity }
|
||||
: {}),
|
||||
default: parameter.default,
|
||||
...(parameter.minimum !== undefined
|
||||
? { min: parameter.minimum }
|
||||
: {}),
|
||||
...(parameter.maximum !== undefined
|
||||
? { max: parameter.maximum }
|
||||
: {}),
|
||||
...(parameter.minimumExclusive
|
||||
? { minExclusive: true }
|
||||
: {}),
|
||||
...(parameter.editor === "amesimGasReference" ||
|
||||
parameter.editor === "amesimGasPropertyModel"
|
||||
? { editor: parameter.editor }
|
||||
: {}),
|
||||
...(Array.isArray(parameter.options)
|
||||
? {
|
||||
options: parameter.options.flatMap((option) =>
|
||||
Number.isFinite(Number(option?.value)) &&
|
||||
typeof option?.label === "string"
|
||||
? [{ value: Number(option.value), label: option.label }]
|
||||
: [],
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
]),
|
||||
),
|
||||
parameters,
|
||||
...(parameterGroups.length > 0 ? { parameterGroups } : {}),
|
||||
} satisfies ComponentDefinition;
|
||||
});
|
||||
|
||||
@@ -1193,11 +1232,44 @@ function defaultParameterUnits(definition: ComponentDefinition) {
|
||||
);
|
||||
}
|
||||
|
||||
function orderedParameterEntries(definition: ComponentDefinition) {
|
||||
return Object.entries(definition.parameters).sort(
|
||||
([firstKey], [secondKey]) =>
|
||||
Number(secondKey === "gi") - Number(firstKey === "gi"),
|
||||
function ungroupedParameterPriority(
|
||||
key: string,
|
||||
definition: ParameterDefinition,
|
||||
) {
|
||||
if (key === "gi") {
|
||||
return 0;
|
||||
}
|
||||
return definition.editor ? 1 : 2;
|
||||
}
|
||||
|
||||
function orderedParameterEntries(
|
||||
definition: ComponentDefinition,
|
||||
): Array<[string, ParameterDefinition]> {
|
||||
const entries = Object.entries(definition.parameters);
|
||||
const entryByName = new Map(entries);
|
||||
const groupedNames = new Set(
|
||||
definition.parameterGroups?.flatMap((group) => group.parameters) ?? [],
|
||||
);
|
||||
const ungroupedEntries = entries
|
||||
.map((entry, index) => ({ entry, index }))
|
||||
.filter(({ entry: [key] }) => !groupedNames.has(key))
|
||||
.sort(
|
||||
(first, second) =>
|
||||
ungroupedParameterPriority(first.entry[0], first.entry[1]) -
|
||||
ungroupedParameterPriority(second.entry[0], second.entry[1]) ||
|
||||
first.index - second.index,
|
||||
)
|
||||
.map(({ entry }) => entry);
|
||||
const groupedEntries =
|
||||
definition.parameterGroups?.flatMap((group) =>
|
||||
group.parameters.flatMap((name) => {
|
||||
const parameter = entryByName.get(name);
|
||||
return parameter
|
||||
? ([[name, parameter]] as Array<[string, ParameterDefinition]>)
|
||||
: [];
|
||||
}),
|
||||
) ?? [];
|
||||
return [...ungroupedEntries, ...groupedEntries];
|
||||
}
|
||||
|
||||
function buildNodeData(definition: ComponentDefinition): SimulationNodeData {
|
||||
@@ -1392,9 +1464,9 @@ function repairAmesimGasReferences(
|
||||
function migrateLegacyDefaultAmesimGasReference(
|
||||
nodes: SimulationNode[],
|
||||
componentDefinitions: ComponentDefinition[],
|
||||
mediumReferenceVersion: 1 | undefined,
|
||||
mediumReferenceVersion: 1 | null | undefined,
|
||||
): AmesimGasReferenceRepair {
|
||||
const hasVersionMarker = mediumReferenceVersion !== undefined;
|
||||
const hasVersionMarker = mediumReferenceVersion === 1;
|
||||
const hasMediumDefinition = nodes.some((node) =>
|
||||
isAmesimGasMediumDefinition(
|
||||
componentDefinitionForNode(node, componentDefinitions),
|
||||
@@ -4260,7 +4332,11 @@ function FlowWorkbench() {
|
||||
recordHistory();
|
||||
}
|
||||
const normalizedNodes = project.nodes.map((node) =>
|
||||
normalizeLoadedNode(node, componentDefinitions),
|
||||
normalizeLoadedNode(
|
||||
node,
|
||||
componentDefinitions,
|
||||
project.amesimParameterEncodingVersion,
|
||||
),
|
||||
);
|
||||
const indexedMedia = assignMissingAmesimGasDefinitionIndices(
|
||||
normalizedNodes,
|
||||
@@ -4993,8 +5069,11 @@ function FlowWorkbench() {
|
||||
readOnly: true,
|
||||
description: "系统为画布节点分配的稳定标识,用于工程数据和连接关系。",
|
||||
},
|
||||
...orderedParameterEntries(selectedDefinition).map(
|
||||
([key, definition]): ParameterTableRow => {
|
||||
...orderedParameterEntries(selectedDefinition)
|
||||
.filter(([, definition]) =>
|
||||
parameterIsVisible(definition, selectedNode.data.parameters),
|
||||
)
|
||||
.map(([key, definition]): ParameterTableRow => {
|
||||
const selectedUnit =
|
||||
selectedNode.data.parameterUnits[key] ?? definition.unit ?? "";
|
||||
const units = parameterUnitOptions(definition);
|
||||
@@ -5012,6 +5091,7 @@ function FlowWorkbench() {
|
||||
definition.editor === "amesimGasReference";
|
||||
const gasPropertyModel =
|
||||
definition.editor === "amesimGasPropertyModel";
|
||||
const genericChoice = definition.editor === "choice";
|
||||
const parameterLabel = mediumDefinitionIndex
|
||||
? "介质定义索引"
|
||||
: gasReference
|
||||
@@ -5025,21 +5105,20 @@ function FlowWorkbench() {
|
||||
amesimGasMediumOptions.some(
|
||||
(option) => option.value === gasReferenceIndex,
|
||||
);
|
||||
const propertyModelValue = Number(baseValue);
|
||||
const propertyModelAvailable =
|
||||
gasPropertyModel &&
|
||||
Number.isFinite(propertyModelValue) &&
|
||||
const optionValue = numericChoiceValue(baseValue);
|
||||
const optionAvailable =
|
||||
(gasPropertyModel || genericChoice) &&
|
||||
optionValue !== null &&
|
||||
(definition.options ?? []).some(
|
||||
(option) => option.value === propertyModelValue,
|
||||
(option) => option.value === optionValue,
|
||||
);
|
||||
const error =
|
||||
gasReference && !gasReferenceAvailable
|
||||
? "引用的介质定义不存在"
|
||||
: parameterValidationMessage(
|
||||
definition,
|
||||
baseValue,
|
||||
selectedUnit,
|
||||
);
|
||||
componentParameterValidationMessage(
|
||||
definition,
|
||||
baseValue,
|
||||
selectedUnit,
|
||||
gasReference ? amesimGasMediumOptions : undefined,
|
||||
);
|
||||
let valueControl: ReactNode;
|
||||
if (mediumDefinitionIndex) {
|
||||
valueControl = (
|
||||
@@ -5076,7 +5155,7 @@ function FlowWorkbench() {
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
} else if (gasPropertyModel) {
|
||||
} else if (gasPropertyModel || genericChoice) {
|
||||
valueControl = (
|
||||
<select
|
||||
aria-invalid={Boolean(error)}
|
||||
@@ -5087,12 +5166,14 @@ function FlowWorkbench() {
|
||||
}
|
||||
onFocus={recordHistory}
|
||||
value={
|
||||
propertyModelAvailable ? String(propertyModelValue) : ""
|
||||
optionAvailable ? String(optionValue) : ""
|
||||
}
|
||||
>
|
||||
{!propertyModelAvailable ? (
|
||||
{!optionAvailable ? (
|
||||
<option disabled value="">
|
||||
当前模型无效,请重新选择
|
||||
{gasPropertyModel
|
||||
? "当前模型无效,请重新选择"
|
||||
: "当前选项无效,请重新选择"}
|
||||
</option>
|
||||
) : null}
|
||||
{(definition.options ?? []).map((option) => (
|
||||
@@ -5160,10 +5241,61 @@ function FlowWorkbench() {
|
||||
selectedUnit,
|
||||
),
|
||||
};
|
||||
},
|
||||
),
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
const componentParameterGroups: ParameterTableGroup[] =
|
||||
selectedNode && selectedDefinition
|
||||
? (selectedDefinition.parameterGroups ?? []).flatMap((group) => {
|
||||
const rowIds = group.parameters.flatMap((name) => {
|
||||
const definition = selectedDefinition.parameters[name];
|
||||
return definition &&
|
||||
parameterIsVisible(definition, selectedNode.data.parameters)
|
||||
? [`component-parameter-${name}`]
|
||||
: [];
|
||||
});
|
||||
const invalidParameters = group.parameters.filter((name) => {
|
||||
const definition = selectedDefinition.parameters[name];
|
||||
if (!definition) {
|
||||
return false;
|
||||
}
|
||||
const selectedUnit =
|
||||
selectedNode.data.parameterUnits[name] ?? definition.unit ?? "";
|
||||
const mediumDefinitionIndex =
|
||||
isAmesimGasMediumDefinition(selectedDefinition) && name === "gi";
|
||||
const gasReference =
|
||||
!mediumDefinitionIndex &&
|
||||
definition.editor === "amesimGasReference";
|
||||
return Boolean(
|
||||
componentParameterValidationMessage(
|
||||
definition,
|
||||
selectedNode.data.parameters[name] ?? "",
|
||||
selectedUnit,
|
||||
gasReference ? amesimGasMediumOptions : undefined,
|
||||
),
|
||||
);
|
||||
});
|
||||
const hiddenErrorCount = invalidParameters.filter((name) => {
|
||||
const definition = selectedDefinition.parameters[name];
|
||||
return (
|
||||
definition &&
|
||||
!parameterIsVisible(definition, selectedNode.data.parameters)
|
||||
);
|
||||
}).length;
|
||||
return rowIds.length > 0 || invalidParameters.length > 0
|
||||
? [
|
||||
{
|
||||
id: `${selectedNode.id}:${group.id}`,
|
||||
label: group.label,
|
||||
rowIds,
|
||||
defaultExpanded: group.defaultExpanded,
|
||||
errorCount: invalidParameters.length,
|
||||
hiddenErrorCount,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
})
|
||||
: [];
|
||||
|
||||
const simulationSettingErrors = simulationConfigValidationMessages(
|
||||
simulationConfig,
|
||||
@@ -5825,6 +5957,7 @@ function FlowWorkbench() {
|
||||
ariaLabel="组件参数"
|
||||
availableWidth={parameterTableAvailableWidth}
|
||||
columnLayout={parameterColumnLayout}
|
||||
groups={componentParameterGroups}
|
||||
onColumnLayoutChange={setParameterColumnLayout}
|
||||
rows={componentParameterRows}
|
||||
/>
|
||||
@@ -5929,6 +6062,7 @@ function buildProjectPayload(
|
||||
return {
|
||||
name: name.trim() || "untitled",
|
||||
mediumReferenceVersion: 1,
|
||||
amesimParameterEncodingVersion: 1,
|
||||
nodes: nodes.map((node) => ({
|
||||
id: node.id,
|
||||
type: "simulationComponent",
|
||||
@@ -5961,6 +6095,110 @@ function buildProjectPayload(
|
||||
};
|
||||
}
|
||||
|
||||
function numericChoiceValue(value: ParameterValue | undefined) {
|
||||
if (typeof value === "string" && value.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
const numericValue = Number(value);
|
||||
return Number.isFinite(numericValue) ? numericValue : null;
|
||||
}
|
||||
|
||||
function parameterIsVisible(
|
||||
definition: ParameterDefinition,
|
||||
parameters: Record<string, ParameterValue>,
|
||||
) {
|
||||
return (
|
||||
!definition.visibleWhen?.length ||
|
||||
definition.visibleWhen.every((condition) => {
|
||||
const currentValue = numericChoiceValue(parameters[condition.parameter]);
|
||||
return currentValue !== null && condition.values.includes(currentValue);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeParameterGroups(
|
||||
value: unknown,
|
||||
parameterNames: ReadonlySet<string>,
|
||||
): ParameterGroupDefinition[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const groupIds = new Set<string>();
|
||||
const groupedParameters = new Set<string>();
|
||||
const groups = value.flatMap((entry) => {
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
return [];
|
||||
}
|
||||
const candidate = entry as Record<string, unknown>;
|
||||
const id = typeof candidate.id === "string" ? candidate.id.trim() : "";
|
||||
const label =
|
||||
typeof candidate.label === "string" ? candidate.label.trim() : "";
|
||||
if (!id || !label || groupIds.has(id) || !Array.isArray(candidate.parameters)) {
|
||||
return [];
|
||||
}
|
||||
const parameters = candidate.parameters.flatMap((parameter) => {
|
||||
if (
|
||||
typeof parameter !== "string" ||
|
||||
!parameterNames.has(parameter) ||
|
||||
groupedParameters.has(parameter)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
groupedParameters.add(parameter);
|
||||
return [parameter];
|
||||
});
|
||||
if (parameters.length === 0) {
|
||||
return [];
|
||||
}
|
||||
groupIds.add(id);
|
||||
const numericOrder = Number(candidate.order);
|
||||
return [
|
||||
{
|
||||
id,
|
||||
label,
|
||||
order: Number.isInteger(numericOrder) ? numericOrder : 0,
|
||||
parameters,
|
||||
defaultExpanded: candidate.defaultExpanded === true,
|
||||
},
|
||||
];
|
||||
});
|
||||
return groups.sort(
|
||||
(first, second) => first.order - second.order || first.id.localeCompare(second.id),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeParameterVisibilityConditions(
|
||||
value: unknown,
|
||||
): ParameterVisibilityCondition[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const conditions = value.flatMap((entry) => {
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
return [];
|
||||
}
|
||||
const candidate = entry as Record<string, unknown>;
|
||||
if (typeof candidate.parameter !== "string" || !candidate.parameter.trim()) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(candidate.values)) {
|
||||
return [];
|
||||
}
|
||||
const values = [
|
||||
...new Set(
|
||||
candidate.values.flatMap((item) => {
|
||||
const numericValue = Number(item);
|
||||
return Number.isFinite(numericValue) ? [numericValue] : [];
|
||||
}),
|
||||
),
|
||||
];
|
||||
return values.length > 0
|
||||
? [{ parameter: candidate.parameter.trim(), values }]
|
||||
: [];
|
||||
});
|
||||
return conditions.length > 0 ? conditions : undefined;
|
||||
}
|
||||
|
||||
function normalizeParameterScientificNotation(
|
||||
value: unknown,
|
||||
): Record<string, ParameterScientificNotation> {
|
||||
@@ -5991,14 +6229,22 @@ function normalizeParameterScientificNotation(
|
||||
function normalizeLoadedNode(
|
||||
node: ProjectNodePayload,
|
||||
componentDefinitions: ComponentDefinition[],
|
||||
amesimParameterEncodingVersion?: 1 | null,
|
||||
): SimulationNode {
|
||||
const definition =
|
||||
componentDefinitions.find((item) => item.type === node.data.componentType) ??
|
||||
componentDefinitions.find((item) => item.modelType === node.data.modelType);
|
||||
const effectiveModelType =
|
||||
definition?.modelType || node.data.modelType || node.data.componentType;
|
||||
const rawParameters = node.data.parameters ?? {};
|
||||
const parameters = definition
|
||||
const mergedParameters = definition
|
||||
? { ...defaultParameters(definition), ...rawParameters }
|
||||
: { ...rawParameters };
|
||||
const parameters = migrateLegacyAmesimParameterEncoding(
|
||||
effectiveModelType,
|
||||
mergedParameters,
|
||||
amesimParameterEncodingVersion,
|
||||
);
|
||||
if (
|
||||
isAmesimGasMediumDefinition(definition) &&
|
||||
!Object.prototype.hasOwnProperty.call(rawParameters, "gi")
|
||||
@@ -6288,6 +6534,26 @@ function displayParameterValue(
|
||||
return formatEditableNumber(displayValue);
|
||||
}
|
||||
|
||||
function migrateLegacyAmesimParameterEncoding(
|
||||
modelType: string,
|
||||
parameters: Record<string, ParameterValue>,
|
||||
version?: 1 | null,
|
||||
) {
|
||||
if (version === 1 || modelType !== "amesim_mecmas21") {
|
||||
return { ...parameters };
|
||||
}
|
||||
const migrated = { ...parameters };
|
||||
(["useFriction", "strib"] as const).forEach((name) => {
|
||||
const value = numericChoiceValue(migrated[name]);
|
||||
if (value === 0) {
|
||||
migrated[name] = 1;
|
||||
} else if (value === 1) {
|
||||
migrated[name] = 2;
|
||||
}
|
||||
});
|
||||
return migrated;
|
||||
}
|
||||
|
||||
function formatEditableNumber(value: number) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return "";
|
||||
@@ -6496,6 +6762,24 @@ function parameterValidationMessage(
|
||||
return "";
|
||||
}
|
||||
|
||||
function componentParameterValidationMessage(
|
||||
definition: ParameterDefinition,
|
||||
value: ParameterValue,
|
||||
selectedUnit: string,
|
||||
gasMediumOptions?: readonly AmesimGasMediumOption[],
|
||||
) {
|
||||
if (gasMediumOptions !== undefined) {
|
||||
const gasReferenceIndex = normalizeAmesimGasIndex(value);
|
||||
if (
|
||||
gasReferenceIndex === null ||
|
||||
!gasMediumOptions.some((option) => option.value === gasReferenceIndex)
|
||||
) {
|
||||
return "引用的介质定义不存在";
|
||||
}
|
||||
}
|
||||
return parameterValidationMessage(definition, value, selectedUnit);
|
||||
}
|
||||
|
||||
function simulationCalculatedValueMetadata(
|
||||
value: ParameterValue,
|
||||
unit: string,
|
||||
@@ -6951,7 +7235,11 @@ function parseProjectPayload(raw: string | null): ReactFlowProjectPayload | null
|
||||
!project.simulation ||
|
||||
typeof project.simulation !== "object" ||
|
||||
(project.mediumReferenceVersion !== undefined &&
|
||||
project.mediumReferenceVersion !== 1)
|
||||
project.mediumReferenceVersion !== null &&
|
||||
project.mediumReferenceVersion !== 1) ||
|
||||
(project.amesimParameterEncodingVersion !== undefined &&
|
||||
project.amesimParameterEncodingVersion !== null &&
|
||||
project.amesimParameterEncodingVersion !== 1)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -6987,6 +7275,7 @@ function buildSystemXml(
|
||||
system.setAttribute("name", project.name);
|
||||
system.setAttribute("schemaVersion", "2");
|
||||
system.setAttribute("mediumReferenceVersion", "1");
|
||||
system.setAttribute("amesimParameterEncodingVersion", "1");
|
||||
system.setAttribute("unitSystem", "SI");
|
||||
|
||||
const simulation = xmlDocument.createElement("Simulation");
|
||||
|
||||
+191
-58
@@ -2,13 +2,13 @@ import {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import type {
|
||||
CSSProperties,
|
||||
FocusEvent as ReactFocusEvent,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
PointerEvent as ReactPointerEvent,
|
||||
@@ -36,9 +36,19 @@ export type ParameterTableRow = {
|
||||
calculatedValue?: string;
|
||||
};
|
||||
|
||||
export type ParameterTableGroup = {
|
||||
id: string;
|
||||
label: string;
|
||||
rowIds: readonly string[];
|
||||
defaultExpanded?: boolean;
|
||||
errorCount?: number;
|
||||
hiddenErrorCount?: number;
|
||||
};
|
||||
|
||||
type ParameterTableProps = {
|
||||
ariaLabel: string;
|
||||
rows: readonly ParameterTableRow[];
|
||||
groups?: readonly ParameterTableGroup[];
|
||||
columnLayout: ParameterColumnLayout;
|
||||
availableWidth: number;
|
||||
onColumnLayoutChange: (layout: ParameterColumnLayout) => void;
|
||||
@@ -170,10 +180,12 @@ function helpPreviewPosition(clientX: number, clientY: number) {
|
||||
export function ParameterTable({
|
||||
ariaLabel,
|
||||
rows,
|
||||
groups = [],
|
||||
columnLayout,
|
||||
availableWidth,
|
||||
onColumnLayoutChange,
|
||||
}: ParameterTableProps) {
|
||||
const groupDescriptionIdPrefix = useId().replace(/:/g, "");
|
||||
const tableWrapRef = useRef<HTMLDivElement>(null);
|
||||
const [measuredAvailableWidth, setMeasuredAvailableWidth] = useState<
|
||||
number | null
|
||||
@@ -188,6 +200,9 @@ export function ParameterTable({
|
||||
const helpPreviewAnchorRef = useRef<HelpPreviewAnchor | null>(null);
|
||||
const [activeColumnResize, setActiveColumnResize] =
|
||||
useState<ColumnName | null>(null);
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
const [helpPreview, setHelpPreview] = useState<HelpPreviewState | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -219,6 +234,17 @@ export function ParameterTable({
|
||||
setHelpPreview(null);
|
||||
}, []);
|
||||
|
||||
const hasActiveParameterSelect = useCallback(() => {
|
||||
if (typeof document === "undefined") {
|
||||
return false;
|
||||
}
|
||||
const activeElement = document.activeElement;
|
||||
return (
|
||||
activeElement instanceof HTMLSelectElement &&
|
||||
activeElement.closest(".parameter-table") !== null
|
||||
);
|
||||
}, []);
|
||||
|
||||
const scheduleHelpPreview = useCallback(
|
||||
(
|
||||
target: HTMLElement,
|
||||
@@ -226,7 +252,7 @@ export function ParameterTable({
|
||||
pointer?: { clientX: number; clientY: number },
|
||||
) => {
|
||||
dismissHelpPreview();
|
||||
if (!rowHasHelpPreview(row)) {
|
||||
if (hasActiveParameterSelect() || !rowHasHelpPreview(row)) {
|
||||
return;
|
||||
}
|
||||
const bounds = target.getBoundingClientRect();
|
||||
@@ -250,11 +276,19 @@ export function ParameterTable({
|
||||
});
|
||||
}, HELP_PREVIEW_DELAY_MS);
|
||||
},
|
||||
[dismissHelpPreview],
|
||||
[dismissHelpPreview, hasActiveParameterSelect],
|
||||
);
|
||||
|
||||
const dismissHelpOnSelectPointerDown = (
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
) => {
|
||||
if (event.target instanceof HTMLSelectElement) {
|
||||
dismissHelpPreview();
|
||||
}
|
||||
};
|
||||
|
||||
const trackHelpPointer = (
|
||||
event: ReactMouseEvent<HTMLTableRowElement>,
|
||||
event: ReactMouseEvent<HTMLTableCellElement>,
|
||||
row: ParameterTableRow,
|
||||
) => {
|
||||
helpPreviewAnchorRef.current = {
|
||||
@@ -278,6 +312,15 @@ export function ParameterTable({
|
||||
};
|
||||
}, [dismissHelpPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
helpPreview &&
|
||||
!rows.some((row) => row.id === helpPreview.rowId)
|
||||
) {
|
||||
dismissHelpPreview();
|
||||
}
|
||||
}, [dismissHelpPreview, helpPreview, rows]);
|
||||
|
||||
const changeColumnLayout = (
|
||||
column: ColumnName,
|
||||
boundaryDelta: number,
|
||||
@@ -363,6 +406,25 @@ export function ParameterTable({
|
||||
: null;
|
||||
const previewDescription = previewRow?.description ||
|
||||
(typeof previewRow?.help === "string" ? previewRow.help : "");
|
||||
const rowsById = new Map(rows.map((row) => [row.id, row]));
|
||||
const groupedRowIds = new Set<string>();
|
||||
const resolvedGroups = groups.flatMap((group) => {
|
||||
const groupRows = group.rowIds.flatMap((rowId) => {
|
||||
if (groupedRowIds.has(rowId)) {
|
||||
return [];
|
||||
}
|
||||
const row = rowsById.get(rowId);
|
||||
if (!row) {
|
||||
return [];
|
||||
}
|
||||
groupedRowIds.add(rowId);
|
||||
return [row];
|
||||
});
|
||||
return groupRows.length > 0 || (group.errorCount ?? 0) > 0
|
||||
? [{ ...group, rows: groupRows }]
|
||||
: [];
|
||||
});
|
||||
const ungroupedRows = rows.filter((row) => !groupedRowIds.has(row.id));
|
||||
|
||||
const renderColumnResizer = (column: ColumnName) => {
|
||||
const currentWidth =
|
||||
@@ -396,16 +458,56 @@ export function ParameterTable({
|
||||
);
|
||||
};
|
||||
|
||||
const rowBlurred = (event: ReactFocusEvent<HTMLTableRowElement>) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget as globalThis.Node | null)) {
|
||||
dismissHelpPreview();
|
||||
}
|
||||
};
|
||||
const renderRows = (parameterRows: readonly ParameterTableRow[]) =>
|
||||
parameterRows.map((row) => {
|
||||
const hasUnit = row.unit !== undefined && row.unit !== null;
|
||||
return (
|
||||
<Fragment key={row.id}>
|
||||
<tr
|
||||
className={`parameter-table-row ${row.error ? "invalid" : ""} ${hasUnit ? "" : "unitless"} ${row.readOnly ? "read-only" : ""}`}
|
||||
>
|
||||
<th
|
||||
aria-describedby={
|
||||
helpPreview?.rowId === row.id
|
||||
? "parameter-help-preview"
|
||||
: undefined
|
||||
}
|
||||
className="parameter-table-cell parameter-table-name-cell"
|
||||
onMouseEnter={(event) =>
|
||||
scheduleHelpPreview(event.currentTarget, row, {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
})
|
||||
}
|
||||
onMouseLeave={dismissHelpPreview}
|
||||
onMouseMove={(event) => trackHelpPointer(event, row)}
|
||||
scope="row"
|
||||
>
|
||||
{row.name}
|
||||
</th>
|
||||
<td className="parameter-table-cell parameter-table-value-cell">
|
||||
{row.value}
|
||||
</td>
|
||||
<td className="parameter-table-cell parameter-table-unit-cell">
|
||||
{hasUnit ? row.unit : <span aria-label="无单位">—</span>}
|
||||
</td>
|
||||
</tr>
|
||||
{row.error ? (
|
||||
<tr className="parameter-table-message-row">
|
||||
<td className="parameter-table-cell" colSpan={3}>
|
||||
<small className="field-error">{row.error}</small>
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`parameter-table-wrap ${activeColumnResize ? `resizing-${activeColumnResize}` : ""}`}
|
||||
data-resizing-column={activeColumnResize ?? undefined}
|
||||
onPointerDownCapture={dismissHelpOnSelectPointerDown}
|
||||
ref={tableWrapRef}
|
||||
>
|
||||
<table aria-label={ariaLabel} className="parameter-table" style={tableStyle}>
|
||||
@@ -429,57 +531,88 @@ export function ParameterTable({
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => {
|
||||
const hasPreview = rowHasHelpPreview(row);
|
||||
const hasUnit = row.unit !== undefined && row.unit !== null;
|
||||
return (
|
||||
<Fragment key={row.id}>
|
||||
<tr
|
||||
aria-describedby={
|
||||
helpPreview?.rowId === row.id
|
||||
? "parameter-help-preview"
|
||||
: undefined
|
||||
}
|
||||
className={`parameter-table-row ${row.error ? "invalid" : ""} ${hasUnit ? "" : "unitless"} ${row.readOnly ? "read-only" : ""}`}
|
||||
onBlurCapture={rowBlurred}
|
||||
onFocusCapture={(event) =>
|
||||
scheduleHelpPreview(event.currentTarget, row)
|
||||
}
|
||||
onMouseEnter={(event) =>
|
||||
scheduleHelpPreview(event.currentTarget, row, {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
})
|
||||
}
|
||||
onMouseLeave={dismissHelpPreview}
|
||||
onMouseMove={(event) => trackHelpPointer(event, row)}
|
||||
tabIndex={row.readOnly && hasPreview ? 0 : undefined}
|
||||
<tbody>{renderRows(ungroupedRows)}</tbody>
|
||||
{resolvedGroups.map((group, groupIndex) => {
|
||||
const expanded =
|
||||
expandedGroups[group.id] ?? Boolean(group.defaultExpanded);
|
||||
const errorCount =
|
||||
group.errorCount ?? group.rows.filter((row) => Boolean(row.error)).length;
|
||||
const hiddenErrorCount = Math.min(
|
||||
errorCount,
|
||||
Math.max(0, group.hiddenErrorCount ?? 0),
|
||||
);
|
||||
const errorDescriptionId = `${groupDescriptionIdPrefix}-group-${groupIndex}-error-description`;
|
||||
const errorDescription = hiddenErrorCount > 0
|
||||
? `${group.label}分组有 ${errorCount} 个参数错误,其中 ${hiddenErrorCount} 个参数当前被配置选项隐藏。`
|
||||
: `${group.label}分组有 ${errorCount} 个参数错误。`;
|
||||
return (
|
||||
<tbody
|
||||
className={`parameter-table-group${errorCount > 0 ? " invalid" : ""}`}
|
||||
key={group.id}
|
||||
>
|
||||
<tr className="parameter-table-group-row">
|
||||
<th
|
||||
className="parameter-table-cell parameter-table-group-cell"
|
||||
colSpan={3}
|
||||
scope="rowgroup"
|
||||
>
|
||||
<th
|
||||
className="parameter-table-cell parameter-table-name-cell"
|
||||
scope="row"
|
||||
<button
|
||||
aria-describedby={errorCount > 0 ? errorDescriptionId : undefined}
|
||||
aria-expanded={expanded}
|
||||
className="parameter-table-group-toggle"
|
||||
onClick={() => {
|
||||
dismissHelpPreview();
|
||||
setExpandedGroups((current) => ({
|
||||
...current,
|
||||
[group.id]: !expanded,
|
||||
}));
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{row.name}
|
||||
</th>
|
||||
<td className="parameter-table-cell parameter-table-value-cell">
|
||||
{row.value}
|
||||
</td>
|
||||
<td className="parameter-table-cell parameter-table-unit-cell">
|
||||
{hasUnit ? row.unit : <span aria-label="无单位">—</span>}
|
||||
</td>
|
||||
</tr>
|
||||
{row.error ? (
|
||||
<tr className="parameter-table-message-row">
|
||||
<td className="parameter-table-cell" colSpan={3}>
|
||||
<small className="field-error">{row.error}</small>
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="parameter-table-group-chevron"
|
||||
>
|
||||
{expanded ? "▾" : "▸"}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="parameter-table-group-folder"
|
||||
/>
|
||||
<span className="parameter-table-group-label">
|
||||
{group.label}
|
||||
</span>
|
||||
{errorCount > 0 ? (
|
||||
<span aria-hidden="true" className="parameter-table-group-error">
|
||||
{errorCount} 个错误
|
||||
{hiddenErrorCount > 0
|
||||
? `(${hiddenErrorCount} 个隐藏)`
|
||||
: ""}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
{errorCount > 0 ? (
|
||||
<span className="visually-hidden" id={errorDescriptionId}>
|
||||
{errorDescription}
|
||||
</span>
|
||||
) : null}
|
||||
</th>
|
||||
</tr>
|
||||
{expanded ? (
|
||||
<>
|
||||
{renderRows(group.rows)}
|
||||
{hiddenErrorCount > 0 ? (
|
||||
<tr className="parameter-table-group-hidden-error-row">
|
||||
<td className="parameter-table-cell" colSpan={3}>
|
||||
其中 {hiddenErrorCount} 个错误参数被当前配置选项隐藏,请调整上方配置后修改。
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</tbody>
|
||||
);
|
||||
})}
|
||||
</table>
|
||||
{helpPreview && previewRow && typeof document !== "undefined"
|
||||
? createPortal(
|
||||
|
||||
+108
-1
@@ -528,6 +528,113 @@ textarea {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.parameter-table-group-row {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.parameter-table .parameter-table-group-cell {
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
border-right: 0;
|
||||
background: #eceeef;
|
||||
}
|
||||
|
||||
.parameter-table-group-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
color: #2f3c48;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.parameter-table-group-toggle:hover {
|
||||
background: #e0e7ec;
|
||||
}
|
||||
|
||||
.parameter-table-group-toggle:focus-visible {
|
||||
outline: 2px solid rgba(29, 111, 184, 0.52);
|
||||
outline-offset: -2px;
|
||||
background: #deedf5;
|
||||
}
|
||||
|
||||
.parameter-table-group-chevron {
|
||||
flex: 0 0 10px;
|
||||
width: 10px;
|
||||
color: #364451;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.parameter-table-group-folder {
|
||||
position: relative;
|
||||
flex: 0 0 14px;
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #65717b;
|
||||
background: #f7f8f9;
|
||||
}
|
||||
|
||||
.parameter-table-group-folder::before {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -1px;
|
||||
width: 6px;
|
||||
height: 4px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #65717b;
|
||||
border-bottom: 0;
|
||||
background: #f7f8f9;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.parameter-table-group-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.parameter-table-group-error {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
color: #b42318;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.parameter-table-group.invalid .parameter-table-group-cell {
|
||||
background: #fff1ef;
|
||||
}
|
||||
|
||||
.parameter-table-group.invalid .parameter-table-group-toggle:hover {
|
||||
background: #ffe5e1;
|
||||
}
|
||||
|
||||
.parameter-table-group-hidden-error-row > .parameter-table-cell {
|
||||
height: auto;
|
||||
min-height: 28px;
|
||||
padding: 6px 8px;
|
||||
overflow: visible;
|
||||
color: #9f2018;
|
||||
background: #fff8f7;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.parameter-table-cell {
|
||||
min-width: 0;
|
||||
height: 32px;
|
||||
@@ -723,7 +830,7 @@ textarea {
|
||||
background: #fff8f7;
|
||||
}
|
||||
|
||||
.parameter-table > tbody > tr:last-child > .parameter-table-cell {
|
||||
.parameter-table > tbody:last-of-type > tr:last-child > .parameter-table-cell {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,239 @@ export const componentCatalog = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "choice_visibility_test",
|
||||
label: "离散参数测试元件",
|
||||
modelType: "choice_visibility_test",
|
||||
modelVersion: "1.0.0",
|
||||
symbol: "symbol-not-registered",
|
||||
order: 5,
|
||||
category: {
|
||||
id: "generic",
|
||||
label: "通用组件",
|
||||
order: 1,
|
||||
},
|
||||
ports: [],
|
||||
parameters: [
|
||||
{
|
||||
name: "mode",
|
||||
label: "工作模式",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 1,
|
||||
minimum: 1,
|
||||
maximum: 3,
|
||||
minimumExclusive: false,
|
||||
editor: "choice",
|
||||
options: [
|
||||
{ value: 1, label: "基础模式" },
|
||||
{ value: 2, label: "高级模式" },
|
||||
{ value: 3, label: "专家模式" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "variant",
|
||||
label: "高级变体",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 10,
|
||||
minimum: 10,
|
||||
maximum: 20,
|
||||
minimumExclusive: false,
|
||||
editor: "choice",
|
||||
options: [
|
||||
{ value: 10, label: "变体 A" },
|
||||
{ value: 20, label: "变体 B" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "basic_value",
|
||||
label: "基础参数",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 1,
|
||||
minimumExclusive: false,
|
||||
visibleWhen: [{ parameter: "mode", values: [1] }],
|
||||
},
|
||||
{
|
||||
name: "advanced_value",
|
||||
label: "高级参数",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 1,
|
||||
minimumExclusive: false,
|
||||
visibleWhen: [{ parameter: "mode", values: [2, 3] }],
|
||||
},
|
||||
{
|
||||
name: "combined_value",
|
||||
label: "组合参数",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 3,
|
||||
minimumExclusive: false,
|
||||
visibleWhen: [
|
||||
{ parameter: "mode", values: [2, 3] },
|
||||
{ parameter: "variant", values: [20] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "amesim_mecmas21",
|
||||
label: "MECMAS21 编码测试元件",
|
||||
modelType: "amesim_mecmas21",
|
||||
modelVersion: "1.0.0",
|
||||
symbol: "symbol-not-registered",
|
||||
order: 6,
|
||||
category: {
|
||||
id: "generic",
|
||||
label: "通用组件",
|
||||
order: 1,
|
||||
},
|
||||
ports: [],
|
||||
parameterGroups: [
|
||||
{
|
||||
id: "friction",
|
||||
label: "摩擦",
|
||||
order: 10,
|
||||
parameters: ["frictionType", "strib", "rvisc"],
|
||||
defaultExpanded: false,
|
||||
},
|
||||
{
|
||||
id: "endstops",
|
||||
label: "限位",
|
||||
order: 20,
|
||||
parameters: ["discContactOption", "xmin"],
|
||||
defaultExpanded: false,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: "mass",
|
||||
label: "质量",
|
||||
quantity: "mass",
|
||||
unit: "kg",
|
||||
default: 1,
|
||||
minimum: 0,
|
||||
minimumExclusive: true,
|
||||
},
|
||||
{
|
||||
name: "rvisc",
|
||||
label: "黏性摩擦系数",
|
||||
quantity: "translational_damping",
|
||||
unit: "N/(m/s)",
|
||||
default: 0,
|
||||
minimum: 0,
|
||||
minimumExclusive: false,
|
||||
description: "用于验证折叠摩擦参数仍支持详情提示和值保留。",
|
||||
visibleWhen: [{ parameter: "useFriction", values: [2] }],
|
||||
},
|
||||
{
|
||||
name: "xmin",
|
||||
label: "下位移限位",
|
||||
quantity: "length",
|
||||
unit: "m",
|
||||
default: -1,
|
||||
minimumExclusive: false,
|
||||
visibleWhen: [{ parameter: "stoptype", values: [1, 2, 3] }],
|
||||
},
|
||||
{
|
||||
name: "useFriction",
|
||||
label: "启用摩擦",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 1,
|
||||
minimum: 1,
|
||||
maximum: 2,
|
||||
minimumExclusive: false,
|
||||
editor: "choice",
|
||||
options: [
|
||||
{ value: 1, label: "禁用" },
|
||||
{ value: 2, label: "启用" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "stoptype",
|
||||
label: "限位类型",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 4,
|
||||
minimum: 1,
|
||||
maximum: 4,
|
||||
minimumExclusive: false,
|
||||
editor: "choice",
|
||||
options: [
|
||||
{ value: 1, label: "理想限位" },
|
||||
{ value: 2, label: "弹性限位" },
|
||||
{ value: 3, label: "恢复碰撞" },
|
||||
{ value: 4, label: "无限位" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "discContactOption",
|
||||
label: "允许负接触力",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 1,
|
||||
minimum: 1,
|
||||
maximum: 2,
|
||||
minimumExclusive: false,
|
||||
editor: "choice",
|
||||
options: [
|
||||
{ value: 1, label: "允许负接触力" },
|
||||
{ value: 2, label: "不允许负接触力" },
|
||||
],
|
||||
visibleWhen: [{ parameter: "stoptype", values: [2] }],
|
||||
},
|
||||
{
|
||||
name: "strib",
|
||||
label: "Stribeck 选项",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 1,
|
||||
minimum: 1,
|
||||
maximum: 2,
|
||||
minimumExclusive: false,
|
||||
editor: "choice",
|
||||
options: [
|
||||
{ value: 1, label: "禁用" },
|
||||
{ value: 2, label: "启用" },
|
||||
],
|
||||
visibleWhen: [
|
||||
{ parameter: "useFriction", values: [2] },
|
||||
{ parameter: "stoptype", values: [1, 2, 4] },
|
||||
{ parameter: "frictionType", values: [2] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "frictionType",
|
||||
label: "摩擦类型",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 1,
|
||||
minimum: 1,
|
||||
maximum: 2,
|
||||
minimumExclusive: false,
|
||||
editor: "choice",
|
||||
options: [
|
||||
{ value: 1, label: "简单" },
|
||||
{ value: 2, label: "高级" },
|
||||
],
|
||||
visibleWhen: [
|
||||
{ parameter: "useFriction", values: [2] },
|
||||
{ parameter: "stoptype", values: [1, 2, 4] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "unrelatedCode",
|
||||
label: "无关编码",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 7,
|
||||
minimumExclusive: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -7,7 +7,11 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
|
||||
function parameterRow(table: Locator, name: string) {
|
||||
return table.getByRole("rowheader", { name, exact: true }).locator("..");
|
||||
return parameterNameCell(table, name).locator("..");
|
||||
}
|
||||
|
||||
function parameterNameCell(table: Locator, name: string) {
|
||||
return table.getByRole("rowheader", { name, exact: true });
|
||||
}
|
||||
|
||||
async function addPneumaticTestComponent(page: Page) {
|
||||
@@ -22,6 +26,30 @@ async function addPneumaticTestComponent(page: Page) {
|
||||
await node.click();
|
||||
}
|
||||
|
||||
async function addChoiceVisibilityTestComponent(page: Page) {
|
||||
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
||||
await page
|
||||
.getByRole("button", { name: "离散参数测试元件", exact: true })
|
||||
.dragTo(canvas, { targetPosition: { x: 620, y: 330 } });
|
||||
const node = page.locator(
|
||||
'.flow-canvas .react-flow__node[data-id="choice_visibility_test_1"]',
|
||||
);
|
||||
await expect(node).toBeVisible();
|
||||
await node.click();
|
||||
}
|
||||
|
||||
async function addMecmasEncodingTestComponent(page: Page) {
|
||||
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
||||
await page
|
||||
.getByRole("button", { name: "MECMAS21 编码测试元件", exact: true })
|
||||
.dragTo(canvas, { targetPosition: { x: 620, y: 330 } });
|
||||
const node = page.locator(
|
||||
'.flow-canvas .react-flow__node[data-id="amesim_mecmas21_1"]',
|
||||
);
|
||||
await expect(node).toBeVisible();
|
||||
await node.click();
|
||||
}
|
||||
|
||||
test("组件参数和仿真设置使用共享的三列表格", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await addPneumaticTestComponent(page);
|
||||
@@ -170,6 +198,456 @@ test("组件参数和仿真设置使用共享的三列表格", async ({ page })
|
||||
await expect(solver).toHaveValue("Radau");
|
||||
});
|
||||
|
||||
test("通用离散选项按多条件控制参数行且保留隐藏值和未知旧值", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await addChoiceVisibilityTestComponent(page);
|
||||
|
||||
const componentTable = page.getByRole("table", { name: "组件参数" });
|
||||
const mode = page.getByLabel("工作模式", { exact: true });
|
||||
const variant = page.getByLabel("高级变体", { exact: true });
|
||||
await expect(mode).toHaveValue("1");
|
||||
await expect(mode.locator("option:checked")).toHaveText("基础模式");
|
||||
await expect(variant).toHaveValue("10");
|
||||
await expect(parameterRow(componentTable, "基础参数")).toBeVisible();
|
||||
await expect(parameterRow(componentTable, "高级参数")).toHaveCount(0);
|
||||
await expect(parameterRow(componentTable, "组合参数")).toHaveCount(0);
|
||||
|
||||
await mode.selectOption("2");
|
||||
await expect(parameterRow(componentTable, "基础参数")).toHaveCount(0);
|
||||
await expect(parameterRow(componentTable, "高级参数")).toBeVisible();
|
||||
await expect(parameterRow(componentTable, "组合参数")).toHaveCount(0);
|
||||
const advancedValue = page.getByLabel("高级参数", { exact: true });
|
||||
await advancedValue.fill("42");
|
||||
await advancedValue.press("Enter");
|
||||
|
||||
await variant.selectOption("20");
|
||||
await expect(parameterRow(componentTable, "组合参数")).toBeVisible();
|
||||
await mode.selectOption("3");
|
||||
await expect(parameterRow(componentTable, "高级参数")).toBeVisible();
|
||||
await expect(parameterRow(componentTable, "组合参数")).toBeVisible();
|
||||
await mode.selectOption("1");
|
||||
await expect(parameterRow(componentTable, "高级参数")).toHaveCount(0);
|
||||
await expect(parameterRow(componentTable, "组合参数")).toHaveCount(0);
|
||||
await mode.selectOption("2");
|
||||
await expect(page.getByLabel("高级参数", { exact: true })).toHaveValue("42");
|
||||
|
||||
await page.getByRole("button", { name: "保存工程", exact: true }).click();
|
||||
const changedProjectCount = await page.evaluate(() => {
|
||||
let changed = 0;
|
||||
Object.keys(window.localStorage)
|
||||
.filter((key) =>
|
||||
[
|
||||
"system-simulation-flow:autosave",
|
||||
"system-simulation-flow:project:",
|
||||
].some((prefix) => key.startsWith(prefix)),
|
||||
)
|
||||
.forEach((key) => {
|
||||
const raw = window.localStorage.getItem(key);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const project = JSON.parse(raw) as {
|
||||
nodes?: Array<{
|
||||
id?: string;
|
||||
data?: { parameters?: Record<string, number | string> };
|
||||
}>;
|
||||
};
|
||||
const node = project.nodes?.find(
|
||||
(candidate) => candidate.id === "choice_visibility_test_1",
|
||||
);
|
||||
if (!node?.data?.parameters) {
|
||||
return;
|
||||
}
|
||||
node.data.parameters.mode = 99;
|
||||
window.localStorage.setItem(key, JSON.stringify(project));
|
||||
changed += 1;
|
||||
});
|
||||
return changed;
|
||||
});
|
||||
expect(changedProjectCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||
await page
|
||||
.locator('.flow-canvas .react-flow__node[data-id="choice_visibility_test_1"]')
|
||||
.click();
|
||||
await expect(mode).toHaveValue("");
|
||||
await expect(mode).toHaveAttribute("aria-invalid", "true");
|
||||
await expect(mode.locator("option:checked")).toHaveText(
|
||||
"当前选项无效,请重新选择",
|
||||
);
|
||||
await expect(parameterRow(componentTable, "基础参数")).toHaveCount(0);
|
||||
await expect(parameterRow(componentTable, "高级参数")).toHaveCount(0);
|
||||
await expect(parameterRow(componentTable, "组合参数")).toHaveCount(0);
|
||||
|
||||
await page.getByRole("button", { name: "保存工程", exact: true }).click();
|
||||
const persistedModes = await page.evaluate(() =>
|
||||
Object.keys(window.localStorage)
|
||||
.filter((key) => key.startsWith("system-simulation-flow:project:"))
|
||||
.flatMap((key) => {
|
||||
const raw = window.localStorage.getItem(key);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
const project = JSON.parse(raw) as {
|
||||
nodes?: Array<{
|
||||
id?: string;
|
||||
data?: { parameters?: Record<string, number | string> };
|
||||
}>;
|
||||
};
|
||||
const node = project.nodes?.find(
|
||||
(candidate) => candidate.id === "choice_visibility_test_1",
|
||||
);
|
||||
return node?.data?.parameters ? [node.data.parameters.mode] : [];
|
||||
}),
|
||||
);
|
||||
expect(persistedModes).toContain(99);
|
||||
|
||||
await mode.selectOption("2");
|
||||
await expect(mode).toHaveValue("2");
|
||||
await expect(mode).not.toHaveAttribute("aria-invalid", "true");
|
||||
await expect(page.getByLabel("高级参数", { exact: true })).toHaveValue("42");
|
||||
});
|
||||
|
||||
test("MECMAS21 可配置参数前置并按条件显示可折叠参数组", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await addMecmasEncodingTestComponent(page);
|
||||
|
||||
const componentTable = page.getByRole("table", { name: "组件参数" });
|
||||
const useFriction = page.getByLabel("启用摩擦", { exact: true });
|
||||
const stoptype = page.getByLabel("限位类型", { exact: true });
|
||||
const frictionGroup = componentTable.getByRole("button", { name: /摩擦/ });
|
||||
const endstopsGroup = componentTable.getByRole("button", { name: /限位/ });
|
||||
const parameterOrder = (
|
||||
await componentTable.getByRole("rowheader").allTextContents()
|
||||
).map((text) => text.trim());
|
||||
|
||||
expect(parameterOrder.indexOf("启用摩擦")).toBeGreaterThanOrEqual(0);
|
||||
expect(parameterOrder.indexOf("限位类型")).toBeGreaterThanOrEqual(0);
|
||||
expect(parameterOrder.indexOf("质量")).toBeGreaterThanOrEqual(0);
|
||||
expect(parameterOrder.indexOf("启用摩擦")).toBeLessThan(
|
||||
parameterOrder.indexOf("质量"),
|
||||
);
|
||||
expect(parameterOrder.indexOf("限位类型")).toBeLessThan(
|
||||
parameterOrder.indexOf("质量"),
|
||||
);
|
||||
await expect(useFriction).toHaveValue("1");
|
||||
await expect(stoptype).toHaveValue("4");
|
||||
await expect(frictionGroup).toHaveCount(0);
|
||||
await expect(endstopsGroup).toHaveCount(0);
|
||||
|
||||
await useFriction.selectOption("2");
|
||||
await expect(frictionGroup).toBeVisible();
|
||||
await expect(frictionGroup).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(frictionGroup.locator("xpath=ancestor::th")).toHaveAttribute(
|
||||
"colspan",
|
||||
"3",
|
||||
);
|
||||
await expect(page.getByLabel("摩擦类型", { exact: true })).toBeHidden();
|
||||
const frictionGroupBox = await frictionGroup.boundingBox();
|
||||
const unrelatedRowBox = await parameterRow(
|
||||
componentTable,
|
||||
"无关编码",
|
||||
).boundingBox();
|
||||
expect(frictionGroupBox).not.toBeNull();
|
||||
expect(unrelatedRowBox).not.toBeNull();
|
||||
expect(frictionGroupBox!.y).toBeGreaterThan(unrelatedRowBox!.y);
|
||||
|
||||
await frictionGroup.focus();
|
||||
await expect(frictionGroup).toBeFocused();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(frictionGroup).toHaveAttribute("aria-expanded", "true");
|
||||
const frictionType = page.getByLabel("摩擦类型", { exact: true });
|
||||
const strib = page.getByLabel("Stribeck 选项", { exact: true });
|
||||
const viscousFriction = page.getByLabel("黏性摩擦系数", { exact: true });
|
||||
await expect(frictionType).toBeVisible();
|
||||
await expect(viscousFriction).toBeVisible();
|
||||
await expect(strib).toHaveCount(0);
|
||||
|
||||
await frictionType.selectOption("2");
|
||||
await expect(strib).toBeVisible();
|
||||
const expandedOrder = (
|
||||
await componentTable.getByRole("rowheader").allTextContents()
|
||||
).map((text) => text.trim());
|
||||
expect(expandedOrder.indexOf("摩擦类型")).toBeLessThan(
|
||||
expandedOrder.indexOf("Stribeck 选项"),
|
||||
);
|
||||
expect(expandedOrder.indexOf("Stribeck 选项")).toBeLessThan(
|
||||
expandedOrder.indexOf("黏性摩擦系数"),
|
||||
);
|
||||
await frictionGroup.focus();
|
||||
await expect(frictionGroup).toBeFocused();
|
||||
await page.keyboard.press("Space");
|
||||
await expect(frictionGroup).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(frictionType).toBeHidden();
|
||||
await expect(strib).toBeHidden();
|
||||
await expect(viscousFriction).toBeHidden();
|
||||
|
||||
await page.keyboard.press("Space");
|
||||
await expect(frictionGroup).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(frictionType).toHaveValue("2");
|
||||
|
||||
await stoptype.selectOption("2");
|
||||
await expect(endstopsGroup).toBeVisible();
|
||||
await expect(endstopsGroup).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(endstopsGroup.locator("xpath=ancestor::th")).toHaveAttribute(
|
||||
"colspan",
|
||||
"3",
|
||||
);
|
||||
await endstopsGroup.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(endstopsGroup).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(page.getByLabel("允许负接触力", { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel("下位移限位", { exact: true })).toBeVisible();
|
||||
const endstopOrder = (
|
||||
await componentTable.getByRole("rowheader").allTextContents()
|
||||
).map((text) => text.trim());
|
||||
expect(endstopOrder.indexOf("允许负接触力")).toBeLessThan(
|
||||
endstopOrder.indexOf("下位移限位"),
|
||||
);
|
||||
const endstopsGroupBox = await endstopsGroup.boundingBox();
|
||||
expect(endstopsGroupBox).not.toBeNull();
|
||||
expect(frictionGroupBox!.y).toBeLessThan(endstopsGroupBox!.y);
|
||||
|
||||
await stoptype.selectOption("3");
|
||||
await expect(frictionGroup).toBeVisible();
|
||||
await expect(endstopsGroup).toBeVisible();
|
||||
await expect(frictionType).toHaveCount(0);
|
||||
await expect(strib).toHaveCount(0);
|
||||
await expect(viscousFriction).toBeVisible();
|
||||
await expect(page.getByLabel("允许负接触力", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByLabel("下位移限位", { exact: true })).toBeVisible();
|
||||
|
||||
await stoptype.selectOption("4");
|
||||
await expect(endstopsGroup).toHaveCount(0);
|
||||
await expect(frictionType).toBeVisible();
|
||||
});
|
||||
|
||||
test("MECMAS21 参数组折叠和条件切换保留值并清理详情浮层", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await addMecmasEncodingTestComponent(page);
|
||||
|
||||
const componentTable = page.getByRole("table", { name: "组件参数" });
|
||||
const useFriction = page.getByLabel("启用摩擦", { exact: true });
|
||||
const frictionGroup = componentTable.getByRole("button", { name: /摩擦/ });
|
||||
await useFriction.selectOption("2");
|
||||
await frictionGroup.click();
|
||||
|
||||
const frictionType = page.getByLabel("摩擦类型", { exact: true });
|
||||
await frictionType.selectOption("2");
|
||||
const strib = page.getByLabel("Stribeck 选项", { exact: true });
|
||||
const viscousFriction = page.getByLabel("黏性摩擦系数", { exact: true });
|
||||
await strib.selectOption("2");
|
||||
await viscousFriction.fill("12.5");
|
||||
await viscousFriction.press("Enter");
|
||||
await expect(viscousFriction).toHaveValue("12.5");
|
||||
|
||||
await frictionGroup.focus();
|
||||
await parameterNameCell(componentTable, "黏性摩擦系数").hover();
|
||||
await page.waitForTimeout(600);
|
||||
const preview = page.locator("body > #parameter-help-preview");
|
||||
await expect(preview.locator(".parameter-help-preview-title")).toHaveText(
|
||||
"黏性摩擦系数",
|
||||
);
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(frictionGroup).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(preview).toHaveCount(0);
|
||||
|
||||
await frictionGroup.click();
|
||||
await expect(frictionType).toHaveValue("2");
|
||||
await expect(strib).toHaveValue("2");
|
||||
await expect(viscousFriction).toHaveValue("12.5");
|
||||
|
||||
await useFriction.selectOption("1");
|
||||
await expect(frictionGroup).toHaveCount(0);
|
||||
await useFriction.selectOption("2");
|
||||
await expect(frictionGroup).toBeVisible();
|
||||
if ((await frictionGroup.getAttribute("aria-expanded")) !== "true") {
|
||||
await frictionGroup.click();
|
||||
}
|
||||
await expect(frictionType).toHaveValue("2");
|
||||
await expect(strib).toHaveValue("2");
|
||||
await expect(viscousFriction).toHaveValue("12.5");
|
||||
|
||||
await viscousFriction.fill("-1");
|
||||
await viscousFriction.press("Enter");
|
||||
await expect(componentTable.getByText("不能小于 0", { exact: true })).toBeVisible();
|
||||
await frictionGroup.click();
|
||||
await expect(frictionGroup).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(frictionGroup).toContainText("1 个错误");
|
||||
|
||||
await useFriction.selectOption("1");
|
||||
await expect(frictionGroup).toBeVisible();
|
||||
await expect(frictionGroup).toContainText("1 个错误");
|
||||
await expect(frictionGroup).toHaveAttribute("aria-describedby");
|
||||
await frictionGroup.click();
|
||||
await expect(componentTable).toContainText(
|
||||
"1 个错误参数被当前配置选项隐藏,请调整上方配置后修改",
|
||||
);
|
||||
});
|
||||
|
||||
test("MECMAS21 旧参数编码只迁移一次且新工程和 XML 写入版本", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await addMecmasEncodingTestComponent(page);
|
||||
|
||||
const useFriction = page.getByLabel("启用摩擦", { exact: true });
|
||||
const frictionGroup = page
|
||||
.getByRole("table", { name: "组件参数" })
|
||||
.getByRole("button", { name: /摩擦/ });
|
||||
await expect(useFriction).toHaveValue("1");
|
||||
await expect(frictionGroup).toHaveCount(0);
|
||||
|
||||
await page.getByRole("button", { name: "保存工程", exact: true }).click();
|
||||
const savedEncodingVersions = await page.evaluate(() =>
|
||||
Object.keys(window.localStorage)
|
||||
.filter((key) => key.startsWith("system-simulation-flow:project:"))
|
||||
.flatMap((key) => {
|
||||
const raw = window.localStorage.getItem(key);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
const project = JSON.parse(raw) as {
|
||||
amesimParameterEncodingVersion?: number | null;
|
||||
nodes?: Array<{ id?: string }>;
|
||||
};
|
||||
return project.nodes?.some(
|
||||
(node) => node.id === "amesim_mecmas21_1",
|
||||
)
|
||||
? [project.amesimParameterEncodingVersion]
|
||||
: [];
|
||||
}),
|
||||
);
|
||||
expect(savedEncodingVersions).toContain(1);
|
||||
|
||||
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
|
||||
await expect(page.getByLabel("生成的系统 XML")).toContainText(
|
||||
'amesimParameterEncodingVersion="1"',
|
||||
);
|
||||
|
||||
const migratedProjectCount = await page.evaluate(() => {
|
||||
let changed = 0;
|
||||
Object.keys(window.localStorage)
|
||||
.filter((key) =>
|
||||
[
|
||||
"system-simulation-flow:autosave",
|
||||
"system-simulation-flow:project:",
|
||||
].some((prefix) => key.startsWith(prefix)),
|
||||
)
|
||||
.forEach((key) => {
|
||||
const raw = window.localStorage.getItem(key);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const project = JSON.parse(raw) as {
|
||||
mediumReferenceVersion?: number | null;
|
||||
amesimParameterEncodingVersion?: number | null;
|
||||
nodes?: Array<{
|
||||
id?: string;
|
||||
data?: {
|
||||
modelType?: string;
|
||||
parameters?: Record<string, number | string>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
const node = project.nodes?.find(
|
||||
(candidate) => candidate.id === "amesim_mecmas21_1",
|
||||
);
|
||||
if (!node?.data?.parameters) {
|
||||
return;
|
||||
}
|
||||
project.amesimParameterEncodingVersion = null;
|
||||
project.mediumReferenceVersion = null;
|
||||
delete node.data.modelType;
|
||||
node.data.parameters.useFriction = 0;
|
||||
delete node.data.parameters.strib;
|
||||
node.data.parameters.unrelatedCode = 9;
|
||||
window.localStorage.setItem(key, JSON.stringify(project));
|
||||
changed += 1;
|
||||
});
|
||||
return changed;
|
||||
});
|
||||
expect(migratedProjectCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||
await page
|
||||
.locator('.flow-canvas .react-flow__node[data-id="amesim_mecmas21_1"]')
|
||||
.click();
|
||||
await expect(useFriction).toHaveValue("1");
|
||||
await expect(useFriction.locator("option:checked")).toHaveText("禁用");
|
||||
await useFriction.selectOption("2");
|
||||
await expect(frictionGroup).toHaveAttribute("aria-expanded", "false");
|
||||
await frictionGroup.click();
|
||||
const frictionType = page.getByLabel("摩擦类型", { exact: true });
|
||||
await frictionType.selectOption("2");
|
||||
const strib = page.getByLabel("Stribeck 选项", { exact: true });
|
||||
await expect(strib).toHaveValue("2");
|
||||
await expect(strib.locator("option:checked")).toHaveText("启用");
|
||||
await expect(page.getByLabel("无关编码", { exact: true })).toHaveValue("9");
|
||||
await useFriction.selectOption("1");
|
||||
await expect(frictionGroup).toHaveCount(0);
|
||||
|
||||
await page.getByRole("button", { name: "保存工程", exact: true }).click();
|
||||
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||
await page
|
||||
.locator('.flow-canvas .react-flow__node[data-id="amesim_mecmas21_1"]')
|
||||
.click();
|
||||
await expect(useFriction).toHaveValue("1");
|
||||
await expect(page.getByLabel("无关编码", { exact: true })).toHaveValue("9");
|
||||
await useFriction.selectOption("2");
|
||||
await expect(frictionGroup).toBeVisible();
|
||||
if ((await frictionGroup.getAttribute("aria-expanded")) !== "true") {
|
||||
await frictionGroup.click();
|
||||
}
|
||||
await expect(frictionType).toHaveValue("2");
|
||||
await expect(strib).toHaveValue("2");
|
||||
});
|
||||
|
||||
test("详情浮层只在参数列悬停且下拉选择期间不触发", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await addMecmasEncodingTestComponent(page);
|
||||
|
||||
const componentTable = page.getByRole("table", { name: "组件参数" });
|
||||
const choice = page.getByLabel("启用摩擦", { exact: true });
|
||||
const otherNameCell = parameterNameCell(componentTable, "无关编码");
|
||||
const preview = page.locator("body > #parameter-help-preview");
|
||||
|
||||
await otherNameCell.hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview).toBeVisible();
|
||||
await expect(preview.locator(".parameter-help-preview-title")).toHaveText(
|
||||
"无关编码",
|
||||
);
|
||||
|
||||
await choice.hover();
|
||||
await expect(preview).toHaveCount(0);
|
||||
await choice.focus();
|
||||
await expect(choice).toBeFocused();
|
||||
await otherNameCell.hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview).toHaveCount(0);
|
||||
|
||||
await choice.selectOption("2");
|
||||
await choice.focus();
|
||||
await otherNameCell.hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview).toHaveCount(0);
|
||||
|
||||
await choice.evaluate((element) => element.blur());
|
||||
await page.locator(".panel-title").filter({ hasText: "组件参数" }).hover();
|
||||
await otherNameCell.hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview.locator(".parameter-help-preview-title")).toHaveText(
|
||||
"无关编码",
|
||||
);
|
||||
});
|
||||
|
||||
test("最窄参数面板滚动时表头固定且值列保持可用宽度", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await addPneumaticTestComponent(page);
|
||||
@@ -234,19 +712,31 @@ test("最窄参数面板滚动时表头固定且值列保持可用宽度", async
|
||||
expect(narrowLayout.horizontalOverflow).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("参数说明延迟显示,单位切换不改变 XML 基础值", async ({ page }) => {
|
||||
test("参数说明只在参数列悬停显示,单位切换不改变 XML 基础值", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await addPneumaticTestComponent(page);
|
||||
|
||||
const componentTable = page.getByRole("table", { name: "组件参数" });
|
||||
const pressureRow = parameterRow(componentTable, "参考压力");
|
||||
const pressureNameCell = parameterNameCell(componentTable, "参考压力");
|
||||
const pressureValueCell = pressureRow.locator(".parameter-table-value-cell");
|
||||
const pressureUnitCell = pressureRow.locator(".parameter-table-unit-cell");
|
||||
const preview = page.locator("body > #parameter-help-preview");
|
||||
|
||||
const pressureRowBox = await pressureRow.boundingBox();
|
||||
expect(pressureRowBox).not.toBeNull();
|
||||
await pressureValueCell.hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview).toHaveCount(0);
|
||||
await pressureUnitCell.hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview).toHaveCount(0);
|
||||
|
||||
const pressureNameCellBox = await pressureNameCell.boundingBox();
|
||||
expect(pressureNameCellBox).not.toBeNull();
|
||||
const pointer = {
|
||||
x: pressureRowBox!.x + pressureRowBox!.width / 2,
|
||||
y: pressureRowBox!.y + pressureRowBox!.height / 2,
|
||||
x: pressureNameCellBox!.x + pressureNameCellBox!.width / 2,
|
||||
y: pressureNameCellBox!.y + pressureNameCellBox!.height / 2,
|
||||
};
|
||||
await page.mouse.move(pointer.x, pointer.y);
|
||||
await page.waitForTimeout(600);
|
||||
@@ -283,13 +773,15 @@ test("参数说明延迟显示,单位切换不改变 XML 基础值", async ({
|
||||
expect(Math.abs(stationaryPreviewBox!.x - previewBox!.x)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(stationaryPreviewBox!.y - previewBox!.y)).toBeLessThanOrEqual(1);
|
||||
|
||||
await pressureValueCell.hover();
|
||||
await expect(preview).toHaveCount(0);
|
||||
await page.locator(".panel-title").filter({ hasText: "组件参数" }).hover();
|
||||
await expect(preview).toHaveCount(0);
|
||||
|
||||
await page.getByLabel("参考压力单位").selectOption("kPa");
|
||||
await expect(page.getByLabel("参考压力", { exact: true })).toHaveValue("100");
|
||||
|
||||
await pressureRow.hover();
|
||||
await pressureNameCell.hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview.locator(".parameter-help-preview-meta")).toContainText(
|
||||
"默认值:100",
|
||||
@@ -527,7 +1019,7 @@ test("组件参数公式保留原式,并在说明中显示计算值且 XML 只
|
||||
await expect(pressureInput).toHaveValue(expression);
|
||||
await expect(pressureRow).not.toContainText("78.5");
|
||||
|
||||
await pressureRow.hover();
|
||||
await parameterNameCell(componentTable, "参考压力").hover();
|
||||
await page.waitForTimeout(600);
|
||||
const preview = page.locator("body > #parameter-help-preview");
|
||||
await expect(preview).toBeVisible();
|
||||
@@ -602,7 +1094,7 @@ test("仿真数值设置同样保留公式,并在执行协议中使用计算
|
||||
|
||||
await expect(stopInput).toHaveValue(expression);
|
||||
await expect(stopRow).not.toContainText("7");
|
||||
await stopRow.hover();
|
||||
await parameterNameCell(simulationTable, "结束时间").hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(
|
||||
page.getByTestId("parameter-calculated-value"),
|
||||
|
||||
Reference in new issue
Block a user