From 32f21f08ffe1b0e3cd5b19b89c7530892e71a6c1 Mon Sep 17 00:00:00 2001 From: ljz <425868052@qq.com> Date: Mon, 3 Aug 2026 17:07:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8F=82=E6=95=B0=E8=A1=A8?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=B8=8E=E7=A7=91=E5=AD=A6=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E6=B3=95=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 171 +++++++++++--- frontend/src/ParameterTable.tsx | 46 +++- frontend/src/parameterExpression.ts | 8 + frontend/src/styles.css | 126 ++++++++--- frontend/tests/e2e/fixtures.ts | 10 + frontend/tests/e2e/parameter-table.spec.ts | 250 +++++++++++++++++++++ 6 files changed, 547 insertions(+), 64 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e2d8b0b..7cf6c79 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -100,6 +100,7 @@ import { import { evaluateParameterExpression, isParameterExpressionValue, + isScientificNotationValue, } from "./parameterExpression"; import { WorkspaceViewTabs, @@ -108,6 +109,10 @@ import { type ParameterValue = number | string; type UnitQuantity = string; +type ParameterScientificNotation = { + text: string; + unit: string; +}; type PortKind = "physical" | "signal"; type PortNominalRole = "inlet" | "outlet" | "bidirectional" | "input" | "output"; type PortSide = "left" | "right"; @@ -255,6 +260,7 @@ type SimulationNodeData = { ports: PortDefinition[]; parameters: Record; parameterUnits: Record; + parameterScientificNotation: Record; rotation: NodeRotation; mirrored: boolean; portConnectionStates?: Record; @@ -288,8 +294,12 @@ type ProjectNodePayload = { x: number; y: number; }; - data: Omit & { + data: Omit< + SimulationNodeData, + "parameterUnits" | "parameterScientificNotation" + > & { parameterUnits?: Record; + parameterScientificNotation?: Record; }; }; @@ -1199,6 +1209,7 @@ function buildNodeData(definition: ComponentDefinition): SimulationNodeData { ports: definition.ports.map((port) => ({ ...port })), parameters: defaultParameters(definition), parameterUnits: defaultParameterUnits(definition), + parameterScientificNotation: {}, rotation: 0, mirrored: false, }; @@ -2709,6 +2720,7 @@ function DeferredNumberInput({ { editingRef.current = false; @@ -3971,34 +3983,62 @@ function FlowWorkbench() { const unit = findUnitOption(definition, selectedUnit); const normalizedDisplayValue = displayValue.trim(); const numericValue = Number(normalizedDisplayValue); - const baseValue = - normalizedDisplayValue === "" || !Number.isFinite(numericValue) - ? normalizedDisplayValue - : unit.toBase(numericValue); + const hasFiniteNumericValue = + normalizedDisplayValue !== "" && Number.isFinite(numericValue); + const convertedBaseValue = hasFiniteNumericValue + ? unit.toBase(numericValue) + : numericValue; + const baseValue = hasFiniteNumericValue + ? convertedBaseValue + : normalizedDisplayValue; + const scientificNotation = + hasFiniteNumericValue && + Number.isFinite(convertedBaseValue) && + isScientificNotationValue(normalizedDisplayValue) + ? { text: normalizedDisplayValue, unit: selectedUnit } + : null; - updateSelectedNode((node) => ({ - ...node, - data: { - ...node.data, - parameters: { - ...node.data.parameters, - [key]: baseValue, + updateSelectedNode((node) => { + const parameterScientificNotation = { + ...node.data.parameterScientificNotation, + }; + if (scientificNotation) { + parameterScientificNotation[key] = scientificNotation; + } else { + delete parameterScientificNotation[key]; + } + return { + ...node, + data: { + ...node.data, + parameters: { + ...node.data.parameters, + [key]: baseValue, + }, + parameterScientificNotation, }, - }, - })); + }; + }); }; const updateSelectedChoice = (key: string, value: number) => { - updateSelectedNode((node) => ({ - ...node, - data: { - ...node.data, - parameters: { - ...node.data.parameters, - [key]: value, + updateSelectedNode((node) => { + const parameterScientificNotation = { + ...node.data.parameterScientificNotation, + }; + delete parameterScientificNotation[key]; + return { + ...node, + data: { + ...node.data, + parameters: { + ...node.data.parameters, + [key]: value, + }, + parameterScientificNotation, }, - }, - })); + }; + }); }; const updateSelectedParameterUnit = (key: string, unit: string) => { @@ -4181,7 +4221,9 @@ function FlowWorkbench() { key === "method" ? value : normalizedValue !== "" && Number.isFinite(numericValue) - ? numericValue + ? isScientificNotationValue(normalizedValue) + ? normalizedValue + : numericValue : normalizedValue, })); }; @@ -4961,6 +5003,7 @@ function FlowWorkbench() { baseValue, definition, selectedUnit, + selectedNode.data.parameterScientificNotation[key], ); const mediumDefinitionIndex = isAmesimGasMediumDefinition(selectedDefinition) && key === "gi"; @@ -5249,7 +5292,7 @@ function FlowWorkbench() { ]; const parameterTableAvailableWidth = Math.max( 0, - modelingPaneLayout.propertiesWidth - 24, + modelingPaneLayout.propertiesWidth, ); return ( @@ -5900,6 +5943,9 @@ function buildProjectPayload( ports: node.data.ports.map((port) => ({ ...port })), parameters: { ...node.data.parameters }, parameterUnits: { ...node.data.parameterUnits }, + parameterScientificNotation: { + ...node.data.parameterScientificNotation, + }, rotation: normalizeNodeRotation(node.data.rotation), mirrored: Boolean(node.data.mirrored), }, @@ -5915,6 +5961,33 @@ function buildProjectPayload( }; } +function normalizeParameterScientificNotation( + value: unknown, +): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return {}; + } + const normalized: Record = {}; + Object.entries(value as Record).forEach(([key, entry]) => { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + return; + } + const candidate = entry as Record; + if ( + typeof candidate.text !== "string" || + typeof candidate.unit !== "string" || + !isScientificNotationValue(candidate.text) + ) { + return; + } + normalized[key] = { + text: candidate.text.trim(), + unit: candidate.unit, + }; + }); + return normalized; +} + function normalizeLoadedNode( node: ProjectNodePayload, componentDefinitions: ComponentDefinition[], @@ -5935,6 +6008,9 @@ function normalizeLoadedNode( const parameterUnits = definition ? { ...defaultParameterUnits(definition), ...(node.data.parameterUnits ?? {}) } : { ...(node.data.parameterUnits ?? {}) }; + const parameterScientificNotation = normalizeParameterScientificNotation( + node.data.parameterScientificNotation, + ); return { id: node.id, @@ -5953,6 +6029,7 @@ function normalizeLoadedNode( ports: normalizeLoadedPorts(node.data.ports, definition), parameters, parameterUnits, + parameterScientificNotation, rotation: normalizeNodeRotation(node.data.rotation), mirrored: Boolean(node.data.mirrored), }, @@ -6190,6 +6267,7 @@ function displayParameterValue( baseValue: ParameterValue, definition: ParameterDefinition, selectedUnit: string, + scientificNotation?: ParameterScientificNotation, ) { if (typeof baseValue === "string" && baseValue.trim() === "") { return ""; @@ -6198,7 +6276,16 @@ function displayParameterValue( if (!Number.isFinite(numericValue)) { return String(baseValue); } - return formatEditableNumber(findUnitOption(definition, selectedUnit).fromBase(numericValue)); + const displayValue = findUnitOption(definition, selectedUnit).fromBase(numericValue); + if ( + scientificNotation && + isScientificNotationValue(scientificNotation.text) + ) { + return scientificNotation.unit === selectedUnit + ? scientificNotation.text.trim() + : formatScientificEditableNumber(displayValue, scientificNotation.text); + } + return formatEditableNumber(displayValue); } function formatEditableNumber(value: number) { @@ -6208,6 +6295,38 @@ function formatEditableNumber(value: number) { return Number(value.toPrecision(12)).toString(); } +const SCIENTIFIC_NOTATION_STYLE_PATTERN = + /^[+-]?(?:\d+(?:\.(\d*))?|\.(\d+))([eE])([+-]?)(\d+)$/; + +function formatScientificEditableNumber(value: number, notationHint: string) { + if (!Number.isFinite(value)) { + return ""; + } + const style = SCIENTIFIC_NOTATION_STYLE_PATTERN.exec(notationHint.trim()); + if (!style) { + return Number(value.toPrecision(12)).toExponential(); + } + + const minimumFractionDigits = (style[1] ?? style[2] ?? "").length; + const exponentMarker = style[3]; + const showPositiveExponentSign = style[4] === "+"; + const roundedDisplayValue = Number(value.toPrecision(15)); + const [rawMantissa, rawExponent] = roundedDisplayValue.toExponential().split("e"); + const [wholePart, rawFraction = ""] = rawMantissa.split("."); + let fraction = rawFraction; + while (fraction.endsWith("0") && fraction.length > minimumFractionDigits) { + fraction = fraction.slice(0, -1); + } + if (fraction.length < minimumFractionDigits) { + fraction = fraction.padEnd(minimumFractionDigits, "0"); + } + + const exponent = Number(rawExponent); + const exponentSign = exponent < 0 ? "-" : showPositiveExponentSign ? "+" : ""; + const exponentDigits = String(Math.abs(exponent)); + return `${wholePart}${fraction ? `.${fraction}` : ""}${exponentMarker}${exponentSign}${exponentDigits}`; +} + function parameterMetadataValue( value: ParameterValue, definition: ParameterDefinition, diff --git a/frontend/src/ParameterTable.tsx b/frontend/src/ParameterTable.tsx index df842b0..8a2be7b 100644 --- a/frontend/src/ParameterTable.tsx +++ b/frontend/src/ParameterTable.tsx @@ -2,6 +2,7 @@ import { Fragment, useCallback, useEffect, + useLayoutEffect, useRef, useState, } from "react"; @@ -173,7 +174,15 @@ export function ParameterTable({ availableWidth, onColumnLayoutChange, }: ParameterTableProps) { - const effectiveLayout = effectiveColumnLayout(columnLayout, availableWidth); + const tableWrapRef = useRef(null); + const [measuredAvailableWidth, setMeasuredAvailableWidth] = useState< + number | null + >(null); + const resolvedAvailableWidth = measuredAvailableWidth ?? availableWidth; + const effectiveLayout = effectiveColumnLayout( + columnLayout, + resolvedAvailableWidth, + ); const columnDragRef = useRef(null); const helpPreviewTimerRef = useRef(null); const helpPreviewAnchorRef = useRef(null); @@ -181,6 +190,26 @@ export function ParameterTable({ useState(null); const [helpPreview, setHelpPreview] = useState(null); + useLayoutEffect(() => { + const wrap = tableWrapRef.current; + if (!wrap) { + return; + } + const measureAvailableWidth = () => { + const nextWidth = wrap.clientWidth; + if (nextWidth <= 0) { + return; + } + setMeasuredAvailableWidth((currentWidth) => + currentWidth === nextWidth ? currentWidth : nextWidth, + ); + }; + measureAvailableWidth(); + const observer = new ResizeObserver(measureAvailableWidth); + observer.observe(wrap); + return () => observer.disconnect(); + }, []); + const dismissHelpPreview = useCallback(() => { if (helpPreviewTimerRef.current !== null) { window.clearTimeout(helpPreviewTimerRef.current); @@ -258,7 +287,9 @@ export function ParameterTable({ column === "name" ? { ...startLayout, nameWidth: startLayout.nameWidth + boundaryDelta } : { ...startLayout, unitWidth: startLayout.unitWidth - boundaryDelta }; - onColumnLayoutChange(effectiveColumnLayout(proposed, availableWidth)); + onColumnLayoutChange( + effectiveColumnLayout(proposed, resolvedAvailableWidth), + ); }; const startColumnResize = ( @@ -311,11 +342,17 @@ export function ParameterTable({ const nameColumnMaximum = Math.max( MIN_NAME_WIDTH, - Math.min(MAX_NAME_WIDTH, availableWidth - MIN_VALUE_WIDTH - effectiveLayout.unitWidth), + Math.min( + MAX_NAME_WIDTH, + resolvedAvailableWidth - MIN_VALUE_WIDTH - effectiveLayout.unitWidth, + ), ); const unitColumnMaximum = Math.max( MIN_UNIT_WIDTH, - Math.min(MAX_UNIT_WIDTH, availableWidth - MIN_VALUE_WIDTH - effectiveLayout.nameWidth), + Math.min( + MAX_UNIT_WIDTH, + resolvedAvailableWidth - MIN_VALUE_WIDTH - effectiveLayout.nameWidth, + ), ); const tableStyle = { "--parameter-name-width": `${effectiveLayout.nameWidth}px`, @@ -369,6 +406,7 @@ export function ParameterTable({
diff --git a/frontend/src/parameterExpression.ts b/frontend/src/parameterExpression.ts index 37d8a49..af6547f 100644 --- a/frontend/src/parameterExpression.ts +++ b/frontend/src/parameterExpression.ts @@ -13,6 +13,14 @@ const NUMERIC_LITERAL_PATTERN = const UNSIGNED_NUMBER_PREFIX = /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/; +export function isScientificNotationValue(value: number | string): value is string { + if (typeof value !== "string") { + return false; + } + const trimmed = value.trim(); + return NUMERIC_LITERAL_PATTERN.test(trimmed) && /[eE]/.test(trimmed); +} + type TokenKind = | "number" | "identifier" diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 51118d5..5420590 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -447,6 +447,17 @@ textarea { font-weight: 700; } +.properties .panel-title { + display: flex; + align-items: center; + min-height: 32px; + padding: 0 10px; + border-bottom-color: #c7cdd4; + color: #293642; + background: #f2f4f6; + font-size: 12px; +} + .palette-list { display: block; padding: 12px; @@ -461,13 +472,13 @@ textarea { .parameter-table-wrap { width: 100%; min-width: 0; - padding: 0 12px 12px; - overflow-x: hidden; + padding: 0; + overflow-x: clip; } .parameter-table-wrap.parameter-form { display: block; - padding: 0 12px 12px; + padding: 0; } .parameter-table { @@ -475,27 +486,30 @@ textarea { --parameter-unit-width: 72px; width: 100%; min-width: 0; - border: 1px solid #cbd5df; + border: 0; border-spacing: 0; - border-radius: 4px; + border-radius: 0; color: #263442; background: #ffffff; table-layout: fixed; - font-size: 11px; + font-size: 12px; } .parameter-table-head .parameter-table-cell { - position: relative; + position: sticky; z-index: 1; - height: 30px; - padding: 0 7px; + top: 0; + height: 28px; + padding: 0 8px; overflow: visible; - border-bottom-color: #bfcbd7; - color: #475569; - background: #edf1f5; - font-size: 10px; + border-right-color: #c4cad1; + border-bottom-color: #b9c1c9; + color: #35414c; + background: #e3e6e9; + box-shadow: 0 1px 0 rgba(51, 65, 85, 0.06); + font-size: 11px; font-weight: 700; - line-height: 30px; + line-height: 27px; } .parameter-table-head .parameter-table-name-cell { @@ -507,16 +521,21 @@ textarea { } .parameter-table-row { - height: 36px; + height: 32px; +} + +.parameter-table-head-row { + height: 28px; } .parameter-table-cell { min-width: 0; - height: 36px; - padding: 2px 6px; + height: 32px; + box-sizing: border-box; + padding: 1px 7px; overflow: hidden; - border-right: 1px solid #d9e0e7; - border-bottom: 1px solid #d9e0e7; + border-right: 1px solid #d5d9de; + border-bottom: 1px solid #e1e4e7; vertical-align: middle; } @@ -524,8 +543,22 @@ textarea { border-right: 0; } +.parameter-table + > tbody + > .parameter-table-row:nth-child(even) + > .parameter-table-cell { + background: #f3f4f5; +} + .parameter-table > tbody > .parameter-table-row:hover > .parameter-table-cell { - background: #f8fbfe; + background: #eaf3f8; +} + +.parameter-table + > tbody + > .parameter-table-row:focus-within + > .parameter-table-cell { + background: #deedf5; } .parameter-table-row:focus-visible { @@ -537,16 +570,21 @@ textarea { .parameter-table-name-cell { overflow: hidden; color: #344256; + text-align: left; text-overflow: ellipsis; white-space: nowrap; } +.parameter-table > tbody .parameter-table-name-cell { + font-weight: 400; +} + .parameter-value-cell, .parameter-table-value-cell, .parameter-unit-cell, .parameter-table-unit-cell { min-width: 0; - padding: 2px; + padding: 1px 3px; } .parameter-unit-cell, @@ -561,30 +599,44 @@ textarea { width: 100%; min-width: 0; max-width: 100%; - height: 30px; + height: 28px; + box-sizing: border-box; margin: 0; padding: 0 6px; - border: 1px solid #d5dde6; - border-radius: 2px; + border: 1px solid transparent; + border-radius: 1px; outline: 0; color: #1f2933; - background: #ffffff; - font-size: 11px; + background: transparent; + font-size: 12px; + font-variant-numeric: tabular-nums; } .parameter-table-cell select { padding-right: 4px; } +.parameter-table > tbody .parameter-table-value-cell, +.parameter-table > tbody .parameter-table-value-cell input, +.parameter-table > tbody .parameter-table-value-cell select { + text-align: right; +} + +.parameter-table > tbody .parameter-table-value-cell select { + text-align-last: right; +} + .parameter-table-cell input:hover:not(:disabled), .parameter-table-cell select:hover:not(:disabled) { - border-color: #aebdcb; + border-color: #aebbc7; + background: rgba(255, 255, 255, 0.72); } .parameter-table-cell input:focus, .parameter-table-cell select:focus { border-color: #1d6fb8; outline: 0; + background: #ffffff; box-shadow: inset 0 0 0 1px #1d6fb8; } @@ -597,24 +649,27 @@ textarea { .parameter-table-row.read-only .parameter-value-cell, .parameter-table-row.read-only .parameter-table-value-cell, .parameter-table-cell input:read-only { - background: #f4f6f8; + background: transparent; } .parameter-read-only-value, .parameter-table-read-only-value { display: flex; align-items: center; + justify-content: flex-end; width: 100%; min-width: 0; - height: 30px; - padding: 0 7px; + height: 28px; + box-sizing: border-box; + padding: 0 6px; overflow: hidden; - border: 1px solid #d5dde6; - border-radius: 2px; + border: 0; + border-radius: 0; color: #526172; + text-align: right; text-overflow: ellipsis; white-space: nowrap; - background: #f4f6f8; + background: transparent; font-variant-numeric: tabular-nums; } @@ -628,7 +683,10 @@ textarea { content: "—"; } -.parameter-table-row.invalid > .parameter-table-cell { +.parameter-table + > tbody + > .parameter-table-row.invalid + > .parameter-table-cell { background: #fff8f7; } diff --git a/frontend/tests/e2e/fixtures.ts b/frontend/tests/e2e/fixtures.ts index 52e11a8..2eb5d2e 100644 --- a/frontend/tests/e2e/fixtures.ts +++ b/frontend/tests/e2e/fixtures.ts @@ -146,6 +146,16 @@ export const componentCatalog = { minimumExclusive: true, description: "用于验证参数表单位换算和 XML 基础单位输出的参考压力。", }, + { + name: "reference_temperature", + label: "参考温度", + quantity: "temperature", + unit: "K", + default: 300, + minimum: 0, + minimumExclusive: false, + description: "用于验证带偏移量单位的数值换算和显示精度。", + }, ], }, { diff --git a/frontend/tests/e2e/parameter-table.spec.ts b/frontend/tests/e2e/parameter-table.spec.ts index a9af781..519667a 100644 --- a/frontend/tests/e2e/parameter-table.spec.ts +++ b/frontend/tests/e2e/parameter-table.spec.ts @@ -41,6 +41,7 @@ test("组件参数和仿真设置使用共享的三列表格", async ({ page }) "节点 ID", "介质物性索引", "参考压力", + "参考温度", ]); await expect(simulationTable.getByRole("rowheader")).toHaveText([ "起始时间", @@ -64,6 +65,173 @@ test("组件参数和仿真设置使用共享的三列表格", async ({ page }) .locator("thead .parameter-table-name-cell") .evaluate((element) => element.getBoundingClientRect().width); expect(Math.abs(componentNameWidth - simulationNameWidth)).toBeLessThanOrEqual(1); + + const panelTitleHeights = await page + .locator(".properties .panel-title") + .evaluateAll((elements) => + elements.map((element) => element.getBoundingClientRect().height), + ); + expect(panelTitleHeights).toHaveLength(2); + panelTitleHeights.forEach((height) => expect(height).toBeLessThanOrEqual(32)); + + const compactTableMetrics = await page + .locator(".properties .parameter-table") + .evaluateAll((tables) => + tables.map((table) => { + const content = table.closest(".properties-subpane-content"); + const headerCell = table.querySelector("thead th"); + const bodyRows = Array.from( + table.querySelectorAll("tbody .parameter-table-row"), + ); + const control = table.querySelector("input, select"); + if (!content || !headerCell || !control) { + throw new Error("参数表缺少布局测量所需的元素"); + } + const tableBounds = table.getBoundingClientRect(); + const contentBounds = content.getBoundingClientRect(); + const tableStyle = getComputedStyle(table); + const contentLeft = contentBounds.left + content.clientLeft; + const contentRight = contentLeft + content.clientWidth; + const controlBounds = control.getBoundingClientRect(); + const controlCell = control.closest("td"); + if (!controlCell) { + throw new Error("参数控件不在数据单元格中"); + } + const controlCellBounds = controlCell.getBoundingClientRect(); + return { + leftInset: tableBounds.left - contentLeft, + rightInset: contentRight - tableBounds.right, + borderRadius: tableStyle.borderRadius, + borderTopWidth: tableStyle.borderTopWidth, + headerHeight: headerCell.getBoundingClientRect().height, + headerPosition: getComputedStyle(headerCell).position, + maximumRowHeight: Math.max( + ...bodyRows.map((row) => row.getBoundingClientRect().height), + ), + controlHeight: controlBounds.height, + controlContained: + controlBounds.left >= controlCellBounds.left - 0.5 && + controlBounds.right <= controlCellBounds.right + 0.5 && + controlBounds.top >= controlCellBounds.top - 0.5 && + controlBounds.bottom <= controlCellBounds.bottom + 0.5, + }; + }), + ); + expect(compactTableMetrics).toHaveLength(2); + compactTableMetrics.forEach((metrics) => { + expect(Math.abs(metrics.leftInset)).toBeLessThanOrEqual(1); + expect(Math.abs(metrics.rightInset)).toBeLessThanOrEqual(1); + expect(metrics.borderRadius).toBe("0px"); + expect(metrics.borderTopWidth).toBe("0px"); + expect(metrics.headerHeight).toBeLessThanOrEqual(28); + expect(metrics.headerPosition).toBe("sticky"); + expect(metrics.maximumRowHeight).toBeLessThanOrEqual(32); + expect(metrics.controlHeight).toBeGreaterThanOrEqual(22); + expect(metrics.controlHeight).toBeLessThanOrEqual(28); + expect(metrics.controlContained).toBe(true); + }); + await expect( + componentTable.getByRole("rowheader", { + name: "组件名称", + exact: true, + }), + ).toHaveCSS("text-align", "left"); + await expect(page.getByLabel("参考压力", { exact: true })).toHaveCSS( + "text-align", + "right", + ); + const valueColumnAlignment = await page + .locator(".properties .parameter-table tbody .parameter-table-value-cell") + .evaluateAll((cells) => + cells.every((cell) => { + const controls = Array.from( + cell.querySelectorAll("input, select"), + ); + return ( + getComputedStyle(cell).textAlign === "right" && + controls.every( + (control) => getComputedStyle(control).textAlign === "right", + ) + ); + }), + ); + expect(valueColumnAlignment).toBe(true); + await expect( + componentTable.locator(".parameter-table-read-only-value").first(), + ).toHaveCSS("justify-content", "flex-end"); + await expect(page.getByLabel("介质物性索引", { exact: true })).toHaveCSS( + "text-align-last", + "right", + ); + const solver = page.getByLabel("求解器", { exact: true }); + await expect(solver).toHaveCSS("text-align", "right"); + await expect(solver).toHaveCSS("text-align-last", "right"); + await solver.selectOption("Radau"); + await expect(solver).toHaveValue("Radau"); +}); + +test("最窄参数面板滚动时表头固定且值列保持可用宽度", async ({ page }) => { + await page.goto("/"); + await addPneumaticTestComponent(page); + + const widthSeparator = page.getByRole("separator", { + name: "调整参数面板宽度", + exact: true, + }); + await widthSeparator.focus(); + await page.keyboard.press("ArrowRight"); + await page.keyboard.press("ArrowRight"); + await page.keyboard.press("ArrowRight"); + await expect(widthSeparator).toHaveAttribute("aria-valuenow", "280"); + + const heightSeparator = page.getByRole("separator", { + name: "调整组件参数和仿真设置区域高度", + exact: true, + }); + await heightSeparator.focus(); + for (let step = 0; step < 11; step += 1) { + await page.keyboard.press("ArrowUp"); + } + await expect(heightSeparator).toHaveAttribute("aria-valuenow", "120"); + + const componentContent = page + .getByRole("region", { name: "组件参数子栏", exact: true }) + .locator(".properties-subpane-content"); + await expect + .poll(() => + componentContent.evaluate( + (element) => element.scrollHeight - element.clientHeight, + ), + ) + .toBeGreaterThan(0); + await componentContent.evaluate((element) => { + element.scrollTop = 48; + }); + await expect + .poll(() => componentContent.evaluate((element) => element.scrollTop)) + .toBeGreaterThan(0); + + const narrowLayout = await componentContent.evaluate((content) => { + const header = content.querySelector( + "thead .parameter-table-name-cell", + ); + const valueHeader = content.querySelector( + "thead .parameter-table-value-cell", + ); + const wrap = content.querySelector(".parameter-table-wrap"); + if (!header || !valueHeader || !wrap) { + throw new Error("无法测量最窄参数表布局"); + } + return { + stickyTopDelta: + header.getBoundingClientRect().top - content.getBoundingClientRect().top, + valueWidth: valueHeader.getBoundingClientRect().width, + horizontalOverflow: wrap.scrollWidth - wrap.clientWidth, + }; + }); + expect(Math.abs(narrowLayout.stickyTopDelta)).toBeLessThanOrEqual(1); + expect(narrowLayout.valueWidth).toBeGreaterThanOrEqual(95); + expect(narrowLayout.horizontalOverflow).toBeLessThanOrEqual(1); }); test("参数说明延迟显示,单位切换不改变 XML 基础值", async ({ page }) => { @@ -140,6 +308,63 @@ test("参数说明延迟显示,单位切换不改变 XML 基础值", async ({ ); }); +test("科学计数法在提交、单位换算和工程加载后保持", async ({ page }) => { + await page.goto("/"); + await addPneumaticTestComponent(page); + + const pressureInput = page.getByLabel("参考压力", { exact: true }); + const pressureUnit = page.getByLabel("参考压力单位", { exact: true }); + + await pressureInput.fill("1e-5"); + await pressureInput.press("Enter"); + await pressureUnit.selectOption("kPa"); + await expect(pressureInput).toHaveValue("1e-8"); + await pressureUnit.selectOption("Pa"); + await expect(pressureInput).toHaveValue("1e-5"); + + await pressureUnit.selectOption("kPa"); + await pressureInput.fill("1.234567890123E+3"); + await pressureInput.press("Enter"); + await pressureUnit.selectOption("Pa"); + await expect(pressureInput).toHaveValue("1.234567890123E+6"); + await pressureUnit.selectOption("kPa"); + await expect(pressureInput).toHaveValue("1.234567890123E+3"); + + await pressureInput.fill("1.20E+3"); + await pressureInput.press("Enter"); + await pressureUnit.selectOption("Pa"); + await expect(pressureInput).toHaveValue("1.20E+6"); + await pressureUnit.selectOption("kPa"); + await expect(pressureInput).toHaveValue("1.20E+3"); + + const temperatureInput = page.getByLabel("参考温度", { exact: true }); + const temperatureUnit = page.getByLabel("参考温度单位", { exact: true }); + await temperatureUnit.selectOption("degC"); + await temperatureInput.fill("1e-10"); + await temperatureInput.press("Enter"); + await temperatureUnit.selectOption("K"); + await expect(temperatureInput).toHaveValue("2.731500000001e2"); + await temperatureUnit.selectOption("degC"); + await expect(temperatureInput).toHaveValue("1e-10"); + + await page.getByRole("button", { name: "保存工程", exact: true }).click(); + await pressureInput.fill("2"); + await pressureInput.press("Enter"); + await page.getByRole("button", { name: "加载工程", exact: true }).click(); + await page + .locator('.flow-canvas .react-flow__node[data-id="amesim_pneumatic_test_1"]') + .click(); + await expect(pressureUnit).toHaveValue("kPa"); + await expect(pressureInput).toHaveValue("1.20E+3"); + await expect(temperatureUnit).toHaveValue("degC"); + await expect(temperatureInput).toHaveValue("1e-10"); + + await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); + await expect(page.getByLabel("生成的系统 XML")).toContainText( + ' { await page.goto("/"); @@ -388,3 +613,28 @@ test("仿真数值设置同样保留公式,并在执行协议中使用计算 await expect(xmlBlock).toContainText('tStop="7"'); await expect(xmlBlock).not.toContainText(expression); }); + +test("仿真数值设置中的科学计数法在保存加载后保持", async ({ page }) => { + await page.goto("/"); + + const stepInput = page.getByLabel("采样步长", { exact: true }); + const maxStepInput = page.getByLabel("最大积分步长", { exact: true }); + await stepInput.fill("1e-5"); + await stepInput.press("Enter"); + await maxStepInput.fill("1.20E-4"); + await maxStepInput.press("Enter"); + + await page.getByRole("button", { name: "保存工程", exact: true }).click(); + await stepInput.fill("0.1"); + await stepInput.press("Enter"); + await maxStepInput.fill("0.01"); + await maxStepInput.press("Enter"); + await page.getByRole("button", { name: "加载工程", exact: true }).click(); + + await expect(stepInput).toHaveValue("1e-5"); + await expect(maxStepInput).toHaveValue("1.20E-4"); + await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); + const xmlBlock = page.getByLabel("生成的系统 XML"); + await expect(xmlBlock).toContainText('step="0.00001"'); + await expect(xmlBlock).toContainText('maxStep="0.00012"'); +});