<script lang="ts">
|
/* eslint-disable import/first -- the parser combines this export with the script-setup imports */
|
const DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
export function parseDecimalListInput(value: string): string[] {
|
const values = value
|
.split(/\r?\n/)
|
.map((item) => item.trim())
|
.filter(Boolean);
|
for (const [index, item] of values.entries()) {
|
if (!DECIMAL_PATTERN.test(item)) throw new Error(`第 ${index + 1} 行不是有效十进制数`);
|
}
|
return values;
|
}
|
</script>
|
|
<script lang="ts" setup>
|
import type { FormulaPreview, FormulaRule } from '#/api/x/eln/formulas';
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
import { previewFormula } from '#/api/x/eln/formulas';
|
|
defineOptions({ name: 'ElnFormulaTrialPanel' });
|
|
const props = defineProps<{ rule: FormulaRule }>();
|
|
const running = ref(false);
|
const requestError = ref('');
|
const fieldErrors = reactive<Record<string, string>>({});
|
const result = ref<FormulaPreview>();
|
const values = reactive<Record<string, boolean | number | string | undefined>>({});
|
const lastRunSignature = ref('');
|
const diagnosticsText = computed(() => result.value?.diagnostics.map((diagnostic) => diagnostic.message || diagnostic.code || '未说明诊断').join(';') || '无');
|
const ruleSignature = computed(() =>
|
JSON.stringify({
|
biaodashi: props.rule.biaodashi,
|
jieguoLeixing: props.rule.jieguoLeixing,
|
parameters: props.rule.parameters.map(({ canshuBianma, paixu, shifouBitian, shujuLeixing }) => ({ canshuBianma, paixu, shifouBitian, shujuLeixing })),
|
xiuyueMoshi: props.rule.xiuyueMoshi,
|
xiuyueXiaoshuWei: props.rule.xiuyueXiaoshuWei,
|
}),
|
);
|
const trialSignature = computed(() =>
|
JSON.stringify({ rule: ruleSignature.value, values: props.rule.parameters.map((slot) => [slot.canshuBianma, values[slot.canshuBianma]]) }),
|
);
|
const stale = computed(() => Boolean(result.value && lastRunSignature.value !== trialSignature.value));
|
|
watch(
|
() => props.rule.parameters.map((slot) => slot.canshuBianma),
|
(codes) => {
|
const activeCodes = new Set(codes);
|
for (const code of Object.keys(values)) {
|
if (!activeCodes.has(code)) delete values[code];
|
}
|
for (const code of Object.keys(fieldErrors)) {
|
if (!activeCodes.has(code)) delete fieldErrors[code];
|
}
|
},
|
{ immediate: true },
|
);
|
|
function statusLabel(status: FormulaPreview['status']) {
|
return { ERROR: '试算失败', MISSING_INPUT: '缺少参数', SUCCESS: '试算成功' }[status];
|
}
|
|
function buildInputs() {
|
const inputs: Record<string, Array<null | number | string> | boolean | null | number | string> = {};
|
for (const slot of props.rule.parameters) {
|
const value = values[slot.canshuBianma];
|
delete fieldErrors[slot.canshuBianma];
|
try {
|
if (slot.shujuLeixing === 'DECIMAL_LIST') inputs[slot.canshuBianma] = parseDecimalListInput(String(value || ''));
|
else if (slot.shujuLeixing === 'BOOLEAN') inputs[slot.canshuBianma] = value === undefined ? null : value;
|
else inputs[slot.canshuBianma] = value === undefined || value === '' ? null : value;
|
} catch (error) {
|
fieldErrors[slot.canshuBianma] = error instanceof Error ? error.message : '数值列表格式错误';
|
throw error;
|
}
|
}
|
return inputs;
|
}
|
|
function clearInputs() {
|
for (const code of Object.keys(values)) delete values[code];
|
for (const code of Object.keys(fieldErrors)) delete fieldErrors[code];
|
requestError.value = '';
|
}
|
|
async function runTrial() {
|
if (running.value) return;
|
requestError.value = '';
|
let inputs: ReturnType<typeof buildInputs>;
|
try {
|
inputs = buildInputs();
|
} catch {
|
return;
|
}
|
const requestSignature = trialSignature.value;
|
running.value = true;
|
try {
|
const response = await previewFormula({ ...props.rule, inputs });
|
result.value = response.data;
|
lastRunSignature.value = requestSignature;
|
} catch {
|
requestError.value = '试算请求失败';
|
} finally {
|
running.value = false;
|
}
|
}
|
</script>
|
|
<template>
|
<div class="trial-panel">
|
<div class="trial-panel__parameters">
|
<div v-for="slot in rule.parameters" :key="slot.canshuBianma" class="trial-parameter">
|
<div class="trial-parameter__meta">
|
<span class="trial-parameter__name"><span v-if="slot.shifouBitian === 1" class="trial-parameter__required">*</span>{{ slot.canshuMingcheng }}</span>
|
<span class="trial-parameter__code">{{ slot.canshuBianma }}</span>
|
<span class="trial-parameter__type">{{ slot.shujuLeixing }}</span>
|
<span v-if="slot.danwei" class="trial-parameter__unit">{{ slot.danwei }}</span>
|
</div>
|
<a-input-number v-if="slot.shujuLeixing === 'DECIMAL'" v-model:value="values[slot.canshuBianma]" class="trial-number" placeholder="请输入数值" />
|
<a-textarea
|
v-else-if="slot.shujuLeixing === 'DECIMAL_LIST'"
|
v-model:value="values[slot.canshuBianma]"
|
class="trial-list"
|
:rows="3"
|
placeholder="每行一个数值" />
|
<a-input v-else-if="slot.shujuLeixing === 'TEXT'" v-model:value="values[slot.canshuBianma]" class="trial-text" placeholder="请输入文本" />
|
<a-select
|
v-else-if="slot.shujuLeixing === 'BOOLEAN'"
|
v-model:value="values[slot.canshuBianma]"
|
allow-clear
|
class="trial-boolean"
|
:field-names="{ label: 'fullName', value: 'id' }"
|
:options="[
|
{ fullName: '是', id: true },
|
{ fullName: '否', id: false },
|
]"
|
placeholder="请选择" />
|
<div v-if="fieldErrors[slot.canshuBianma]" class="trial-parameter__error">{{ fieldErrors[slot.canshuBianma] }}</div>
|
</div>
|
<div v-if="!rule.parameters.length" class="trial-panel__empty">当前公式没有试算参数</div>
|
</div>
|
<div class="trial-panel__actions">
|
<a-button data-test="clear-trial-inputs" @click="clearInputs">清空参数</a-button>
|
<a-button data-test="run-trial" :disabled="running" :loading="running" type="primary" @click="runTrial">开始试算</a-button>
|
</div>
|
<a-alert v-if="requestError" :message="requestError" show-icon type="error" />
|
<div v-if="result" class="trial-panel__result">
|
<a-alert v-if="stale" message="结果已过期,请重新试算" show-icon type="warning" />
|
<a-descriptions :column="1" size="small">
|
<a-descriptions-item label="试算状态">{{ result.status }}({{ statusLabel(result.status) }})</a-descriptions-item>
|
<a-descriptions-item label="诊断信息">{{ diagnosticsText }}</a-descriptions-item>
|
</a-descriptions>
|
<template v-if="result.status === 'SUCCESS'">
|
<div>原始结果:{{ result.rawResult }}</div>
|
<div>修约结果:{{ result.roundedResult }}</div>
|
</template>
|
</div>
|
</div>
|
</template>
|
|
<style lang="scss" scoped>
|
.trial-panel {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
min-height: 0;
|
}
|
|
.trial-panel__parameters {
|
flex: 1;
|
min-height: 120px;
|
overflow: auto;
|
}
|
|
.trial-parameter {
|
padding: 10px 0;
|
}
|
|
.trial-parameter__meta {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
min-width: 0;
|
margin-bottom: 6px;
|
}
|
|
.trial-parameter__name {
|
min-width: 0;
|
overflow: hidden;
|
font-size: 13px;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.trial-parameter__required,
|
.trial-parameter__error {
|
color: var(--error-color);
|
}
|
|
.trial-parameter__code,
|
.trial-parameter__type,
|
.trial-parameter__unit {
|
flex: none;
|
color: var(--text-color-secondary);
|
font-size: 11px;
|
}
|
|
.trial-parameter__error {
|
padding-top: 4px;
|
font-size: 12px;
|
}
|
|
.trial-number,
|
.trial-list,
|
.trial-text,
|
.trial-boolean {
|
width: 100%;
|
}
|
|
.trial-panel__actions {
|
display: flex;
|
flex: none;
|
justify-content: center;
|
gap: 8px;
|
padding: 12px 0;
|
background: var(--app-content-background);
|
}
|
|
.trial-panel__result {
|
flex: none;
|
max-height: 180px;
|
padding-top: 8px;
|
overflow: auto;
|
}
|
|
.trial-panel__empty {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
min-height: 120px;
|
color: var(--text-color-secondary);
|
}
|
</style>
|