fix
This commit is contained in:
+43
-1
@@ -1,16 +1,30 @@
|
||||
import { createModelApi, apiClient } from "@/api_client.ts";
|
||||
import type { Params, ControlCase, Analize } from "@/models.ts";
|
||||
import type { Params, ControlCase, Analize, InitialCondition } from "@/models.ts";
|
||||
|
||||
export const paramsApi = createModelApi<Params>("params");
|
||||
|
||||
export const controlCaseApi = createModelApi<ControlCase>("control_case");
|
||||
export const analizeApi = createModelApi<Analize>("analize");
|
||||
export const initialConditionApi = createModelApi<InitialCondition>("initial_condition");
|
||||
|
||||
export interface CsvChartResponse {
|
||||
columns: string[];
|
||||
rows: Array<Array<string | number>>;
|
||||
}
|
||||
|
||||
export interface PsiSpectrumPoint {
|
||||
frequency: number;
|
||||
amplitude: number;
|
||||
}
|
||||
|
||||
export interface PsiSpectrumResponse {
|
||||
time_step: number;
|
||||
points: {
|
||||
psi_m: PsiSpectrumPoint[];
|
||||
psi_l: PsiSpectrumPoint[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface FieldMapResponse {
|
||||
requested_t: number;
|
||||
stage_t: number;
|
||||
@@ -19,6 +33,10 @@ export interface FieldMapResponse {
|
||||
fields: Record<string, number[][]>;
|
||||
}
|
||||
|
||||
export interface InitialConditionResponse {
|
||||
initial_condition: InitialCondition;
|
||||
}
|
||||
|
||||
export async function getControlCaseChartData(id: number) {
|
||||
const response = await apiClient.get<CsvChartResponse>(
|
||||
`/control_case/${id}/chart-data`,
|
||||
@@ -26,6 +44,13 @@ export async function getControlCaseChartData(id: number) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getControlCasePsiSpectrum(id: number) {
|
||||
const response = await apiClient.get<PsiSpectrumResponse>(
|
||||
`/control_case/${id}/psi-spectrum`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getControlCaseFieldMap(id: number, time: number) {
|
||||
const response = await apiClient.get<FieldMapResponse>(
|
||||
`/control_case/${id}/field-map`,
|
||||
@@ -36,6 +61,13 @@ export async function getControlCaseFieldMap(id: number, time: number) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createControlCaseInitialCondition(id: number) {
|
||||
const response = await apiClient.post<InitialConditionResponse>(
|
||||
`/control_case/${id}/initial-condition`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export interface LaunchControlCasePayload {
|
||||
params_id: number;
|
||||
name?: string;
|
||||
@@ -69,16 +101,26 @@ export interface AnalizeSeriesPoint {
|
||||
psi_max: number;
|
||||
}
|
||||
|
||||
export interface AnalizeSeriesGroup {
|
||||
group_value: number;
|
||||
group_label: string;
|
||||
points: AnalizeSeriesPoint[];
|
||||
}
|
||||
|
||||
export interface AnalizeSeriesResponse {
|
||||
parameter: string;
|
||||
label: string;
|
||||
group_parameter?: string;
|
||||
group_label?: string;
|
||||
points: AnalizeSeriesPoint[];
|
||||
groups?: AnalizeSeriesGroup[];
|
||||
}
|
||||
|
||||
export interface AnalizeSeriesFilters {
|
||||
filter_parameter?: string;
|
||||
filter_min?: number;
|
||||
filter_max?: number;
|
||||
group_parameter?: string;
|
||||
}
|
||||
|
||||
export async function getAnalizeSeries(parameter: string, filters?: AnalizeSeriesFilters) {
|
||||
|
||||
@@ -74,6 +74,10 @@ export function formatApiError(
|
||||
error: unknown,
|
||||
fallback = "Не удалось выполнить запрос.",
|
||||
): string {
|
||||
if (error instanceof Error && !axios.isAxiosError(error)) {
|
||||
return error.message || fallback;
|
||||
}
|
||||
|
||||
if (!axios.isAxiosError(error)) return fallback;
|
||||
|
||||
const payload = error.response?.data;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import type { InitialCondition, Params } from '@/models.ts'
|
||||
|
||||
defineProps<{
|
||||
item: Params
|
||||
form: Params
|
||||
initialConditions: InitialCondition[]
|
||||
saving: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
submit: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<div class="text-muted small">Редактирование</div>
|
||||
<div class="fw-semibold">Params #{{ item.id }}</div>
|
||||
</div>
|
||||
<div class="text-muted small">Created: {{ new Date(item.created_at).toLocaleString() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form class="row g-3" @submit.prevent="$emit('submit')">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">rel</label>
|
||||
<input v-model="form.rel" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">rel_c</label>
|
||||
<input v-model="form.rel_c" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">le</label>
|
||||
<input v-model="form.le" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">sc</label>
|
||||
<input v-model="form.sc" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">pe</label>
|
||||
<input v-model="form.pe" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">ma</label>
|
||||
<input v-model="form.ma" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">initial_condition</label>
|
||||
<select v-model="form.initial_condition_id" class="form-select">
|
||||
<option :value="null">Не задано</option>
|
||||
<option v-for="condition in initialConditions" :key="condition.ID" :value="condition.ID">
|
||||
{{ condition.name || `#${condition.ID}` }} — {{ condition.file_path }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">folder_path</label>
|
||||
<input v-model="form.folder_path" type="text" class="form-control" />
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-primary" type="submit" :disabled="saving">
|
||||
{{ saving ? 'Сохранение...' : 'Сохранить' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import type { InitialCondition } from '@/models.ts'
|
||||
|
||||
interface BatchForm {
|
||||
fixed: {
|
||||
rel: number
|
||||
rel_c: number
|
||||
le: number
|
||||
sc: number
|
||||
pe: number
|
||||
ma: number
|
||||
initial_condition_id: number | null
|
||||
time: number
|
||||
}
|
||||
variable: string
|
||||
start: number
|
||||
end: number
|
||||
step: number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
batchForm: BatchForm
|
||||
initialConditions: InitialCondition[]
|
||||
variableOptions: string[]
|
||||
running: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
submit: []
|
||||
reset: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white fw-semibold">Быстрый расчет</div>
|
||||
<div class="card-body">
|
||||
<form @submit.prevent="$emit('submit')">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">Переменный параметр</th>
|
||||
<td>
|
||||
<select v-model="batchForm.variable" class="form-select">
|
||||
<option v-for="item in variableOptions" :key="item" :value="item">{{ item }}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">Start</th>
|
||||
<td>
|
||||
<input v-model.number="batchForm.start" type="number" step="any" class="form-control" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">End</th>
|
||||
<td>
|
||||
<input v-model.number="batchForm.end" type="number" step="any" class="form-control" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">Step</th>
|
||||
<td>
|
||||
<input v-model.number="batchForm.step" type="number" step="any" class="form-control" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" colspan="2" class="bg-body-tertiary">Фиксированные параметры</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">rel</th>
|
||||
<td><input v-model.number="batchForm.fixed.rel" type="number" step="any" class="form-control" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">rel_c</th>
|
||||
<td><input v-model.number="batchForm.fixed.rel_c" type="number" step="any" class="form-control" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">le</th>
|
||||
<td><input v-model.number="batchForm.fixed.le" type="number" step="any" class="form-control" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">sc</th>
|
||||
<td><input v-model.number="batchForm.fixed.sc" type="number" step="any" class="form-control" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">pe</th>
|
||||
<td><input v-model.number="batchForm.fixed.pe" type="number" step="any" class="form-control" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">ma</th>
|
||||
<td><input v-model.number="batchForm.fixed.ma" type="number" step="any" class="form-control" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">initial_condition</th>
|
||||
<td>
|
||||
<select v-model="batchForm.fixed.initial_condition_id" class="form-select">
|
||||
<option :value="null">Не задано</option>
|
||||
<option v-for="condition in initialConditions" :key="condition.ID" :value="condition.ID">
|
||||
{{ condition.name || `#${condition.ID}` }} — {{ condition.file_path }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">time</th>
|
||||
<td><input v-model.number="batchForm.fixed.time" type="number" step="any" class="form-control" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="$emit('reset')" :disabled="running">
|
||||
Сбросить
|
||||
</button>
|
||||
<button class="btn btn-primary" type="submit" :disabled="running">
|
||||
{{ running ? 'Создание...' : 'Создать серию' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import type { InitialCondition, Params } from '@/models.ts'
|
||||
|
||||
interface CreateForm {
|
||||
rel: number
|
||||
rel_c: number
|
||||
le: number
|
||||
sc: number
|
||||
pe: number
|
||||
ma: number
|
||||
initial_condition_id: number | null
|
||||
time: number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
items: Params[]
|
||||
loading: boolean
|
||||
selectedId: number | null
|
||||
createForm: CreateForm
|
||||
initialConditions: InitialCondition[]
|
||||
saving: boolean
|
||||
launchingId: number | null
|
||||
page: number
|
||||
totalPages: number
|
||||
totalCount: number
|
||||
pageStart: number
|
||||
pageEnd: number
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
create: []
|
||||
select: [item: Params]
|
||||
launch: [item: Params]
|
||||
goToPage: [page: number]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div v-if="loading" class="p-4 text-center text-muted">Загрузка...</div>
|
||||
<div v-else class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>id</th>
|
||||
<th>rel</th>
|
||||
<th>rel_c</th>
|
||||
<th>le</th>
|
||||
<th>sc</th>
|
||||
<th>pe</th>
|
||||
<th>ma</th>
|
||||
<th>Initial</th>
|
||||
<th>time</th>
|
||||
<th>folder_path</th>
|
||||
<th class="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="table-success">
|
||||
<td class="fw-semibold text-success">+</td>
|
||||
<td>
|
||||
<input v-model.number="createForm.rel" type="number" step="any" class="form-control form-control-sm" placeholder="Rel" />
|
||||
</td>
|
||||
<td>
|
||||
<input v-model.number="createForm.rel_c" type="number" step="any" class="form-control form-control-sm" placeholder="RelC" />
|
||||
</td>
|
||||
<td>
|
||||
<input v-model.number="createForm.le" type="number" step="any" class="form-control form-control-sm" placeholder="Le" />
|
||||
</td>
|
||||
<td>
|
||||
<input v-model.number="createForm.sc" type="number" step="any" class="form-control form-control-sm" placeholder="Sc" />
|
||||
</td>
|
||||
<td>
|
||||
<input v-model.number="createForm.pe" type="number" step="any" class="form-control form-control-sm" placeholder="Pe" />
|
||||
</td>
|
||||
<td>
|
||||
<input v-model.number="createForm.ma" type="number" step="any" class="form-control form-control-sm" placeholder="Ma" />
|
||||
</td>
|
||||
<td>
|
||||
<select v-model="createForm.initial_condition_id" class="form-select form-select-sm">
|
||||
<option :value="null">—</option>
|
||||
<option v-for="condition in initialConditions" :key="condition.ID" :value="condition.ID">
|
||||
{{ condition.name || `#${condition.ID}` }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input v-model.number="createForm.time" type="number" step="any" class="form-control form-control-sm" placeholder="Time" />
|
||||
</td>
|
||||
<td class="text-muted small">new</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-success" type="button" @click="$emit('create')" :disabled="saving">
|
||||
{{ saving ? 'Сохранение...' : 'Добавить' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="items.length === 0">
|
||||
<td colspan="11" class="text-center text-muted py-4">Записи не найдены</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:class="{ 'table-primary': item.id === selectedId }"
|
||||
>
|
||||
<td class="fw-semibold">{{ item.id }}</td>
|
||||
<td>{{ item.rel }}</td>
|
||||
<td>{{ item.rel_c }}</td>
|
||||
<td>{{ item.le }}</td>
|
||||
<td>{{ item.sc }}</td>
|
||||
<td>{{ item.pe }}</td>
|
||||
<td>{{ item.ma }}</td>
|
||||
<td class="text-truncate" style="max-width: 180px">
|
||||
{{ item.initial_condition?.name || item.initial_condition?.file_path || '—' }}
|
||||
</td>
|
||||
<td>{{ item.time }}</td>
|
||||
<td class="text-truncate" style="max-width: 220px">{{ item.folder_path || '—' }}</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-primary" type="button" @click="$emit('select', item)">Редактировать</button>
|
||||
<button class="btn btn-success" type="button" @click="$emit('launch', item)" :disabled="launchingId === item.id">
|
||||
{{ launchingId === item.id ? 'Запуск...' : 'Запустить' }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 p-3 border-top">
|
||||
<div class="text-muted small">Показано {{ pageStart }}-{{ pageEnd }} из {{ totalCount }}</div>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Pagination">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="$emit('goToPage', page - 1)" :disabled="page <= 1">Назад</button>
|
||||
<button class="btn btn-outline-secondary" type="button" disabled>{{ page }} / {{ totalPages }}</button>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="$emit('goToPage', page + 1)" :disabled="page >= totalPages">Вперёд</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,23 +11,33 @@ export interface Params {
|
||||
rel: number;
|
||||
rel_c: number;
|
||||
le: number;
|
||||
pr: number;
|
||||
sc: number;
|
||||
pe: number;
|
||||
ma: number;
|
||||
initial_condition: string;
|
||||
initial_condition_id: number | null;
|
||||
initial_condition: InitialCondition | null;
|
||||
time: number;
|
||||
folder_path: string;
|
||||
}
|
||||
|
||||
export interface InitialCondition {
|
||||
ID: number;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
DeletedAt: GormDeletedAt | null;
|
||||
name: string;
|
||||
file_path: string;
|
||||
}
|
||||
|
||||
export interface ControlCase {
|
||||
ID: number;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
DeletedAt: GormDeletedAt | null;
|
||||
Name: string;
|
||||
ParamsID: number;
|
||||
Params: Params;
|
||||
Status: string;
|
||||
name: string;
|
||||
paramsID: number;
|
||||
params: Params;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface Analize {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const controlCaseStatuses: Record<string, { label: string; className: string }> = {
|
||||
N: { label: 'В очереди', className: 'text-bg-secondary' },
|
||||
R: { label: 'Ожидает анализа', className: 'text-bg-warning' },
|
||||
D: { label: 'Готово', className: 'text-bg-success' },
|
||||
}
|
||||
|
||||
export function controlCaseStatusMeta(status: string | undefined) {
|
||||
if (!status) {
|
||||
return { code: '', label: '—', className: 'text-bg-secondary' }
|
||||
}
|
||||
|
||||
const normalized = status.toUpperCase()
|
||||
const meta = controlCaseStatuses[normalized]
|
||||
if (!meta) {
|
||||
return { code: normalized, label: normalized, className: 'text-bg-secondary' }
|
||||
}
|
||||
|
||||
return { code: normalized, ...meta }
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { controlCaseApi, getAnalizeSeries, getControlCaseChartData, getControlCaseFieldMap } from '@/api.ts'
|
||||
import { controlCaseApi, getAnalizeSeries, getControlCaseChartData, getControlCaseFieldMap, getControlCasePsiSpectrum } from '@/api.ts'
|
||||
import { formatApiError } from '@/api_client.ts'
|
||||
import type { ControlCase } from '@/models.ts'
|
||||
import { controlCaseStatusMeta } from '@/statuses.ts'
|
||||
|
||||
const fieldOptions = [
|
||||
{ value: 'psi', label: 'psi' },
|
||||
@@ -16,13 +17,15 @@ const parameterOptions = [
|
||||
{ value: 'Rel', label: 'Rel' },
|
||||
{ value: 'RelC', label: 'RelC' },
|
||||
{ value: 'Le', label: 'Le' },
|
||||
{ value: 'Pr', label: 'Pr' },
|
||||
{ value: 'Sc', label: 'Sc' },
|
||||
{ value: 'Pe', label: 'Pe' },
|
||||
{ value: 'Ma', label: 'Ma' },
|
||||
{ value: 'Time', label: 'Time' },
|
||||
] as const
|
||||
|
||||
const selectedParameter = ref<(typeof parameterOptions)[number]['value']>('Rel')
|
||||
const groupParameter = ref('')
|
||||
const showTrend = ref(false)
|
||||
const filterParameter = ref('')
|
||||
const filterMin = ref('')
|
||||
const filterMax = ref('')
|
||||
@@ -32,11 +35,24 @@ const containerRef = ref<HTMLDivElement | null>(null)
|
||||
const caseChartRef = ref<HTMLDivElement | null>(null)
|
||||
const chartTitle = ref('')
|
||||
const seriesData = ref<{ case_id: number; x: number; omega: number; psi_max: number; case_name: string }[]>([])
|
||||
const groupedSeriesData = ref<{
|
||||
group_value: number
|
||||
group_label: string
|
||||
points: { case_id: number; case_name: string; x: number; omega: number; psi_max: number }[]
|
||||
}[]>([])
|
||||
const selectedCaseId = ref<number | null>(null)
|
||||
const selectedCase = ref<ControlCase | null>(null)
|
||||
const selectedCaseLoading = ref(false)
|
||||
const selectedCaseError = ref('')
|
||||
const selectedCasePoints = ref<{ t: number; psi_m: number }[]>([])
|
||||
const selectedCasePsiLPoints = ref<{ t: number; psi_l: number }[]>([])
|
||||
const selectedCaseSpectrum = ref<{
|
||||
time_step: number
|
||||
points: {
|
||||
psi_m: { frequency: number; amplitude: number }[]
|
||||
psi_l: { frequency: number; amplitude: number }[]
|
||||
}
|
||||
} | null>(null)
|
||||
const selectedFieldMap = ref<{
|
||||
requested_t: number
|
||||
stage_t: number
|
||||
@@ -47,6 +63,7 @@ const selectedFieldMap = ref<{
|
||||
const selectedFieldMapLoading = ref(false)
|
||||
const selectedFieldMapError = ref('')
|
||||
const selectedField = ref<(typeof fieldOptions)[number]['value']>('psi')
|
||||
const psiViewMode = ref<'time' | 'fft'>('time')
|
||||
const fieldMapRef = ref<HTMLDivElement | null>(null)
|
||||
let chart: echarts.ECharts | null = null
|
||||
let caseChart: echarts.ECharts | null = null
|
||||
@@ -56,15 +73,160 @@ const selectedLabel = computed(
|
||||
() => parameterOptions.find((item) => item.value === selectedParameter.value)?.label ?? selectedParameter.value,
|
||||
)
|
||||
|
||||
function buildOption() {
|
||||
const groupLabel = computed(
|
||||
() => parameterOptions.find((item) => item.value === groupParameter.value)?.label ?? groupParameter.value,
|
||||
)
|
||||
|
||||
const groupOptions = computed(() => [
|
||||
{ value: '', label: 'Без группировки' },
|
||||
...parameterOptions.filter((item) => item.value !== selectedParameter.value),
|
||||
])
|
||||
|
||||
type AnalizePoint = { case_id: number; case_name: string; x: number; omega: number; psi_max: number }
|
||||
type ChartPoint = {
|
||||
value: [number, number]
|
||||
case_id: number
|
||||
case_name: string
|
||||
group_label?: string
|
||||
x: number
|
||||
omega: number | null
|
||||
psi_max: number | null
|
||||
psi_max2: number | null
|
||||
}
|
||||
|
||||
function psiMaxSquared(point: AnalizePoint) {
|
||||
return point.psi_max * point.psi_max
|
||||
}
|
||||
|
||||
function chartPoint(point: AnalizePoint, y: number, groupLabel?: string): ChartPoint {
|
||||
return {
|
||||
title: { text: `Omega / PsiMax vs ${chartTitle.value || selectedLabel.value}` },
|
||||
value: [point.x, y],
|
||||
case_id: point.case_id,
|
||||
case_name: point.case_name,
|
||||
group_label: groupLabel,
|
||||
x: point.x,
|
||||
omega: point.omega,
|
||||
psi_max: point.psi_max,
|
||||
psi_max2: psiMaxSquared(point),
|
||||
}
|
||||
}
|
||||
|
||||
function buildPsiMaxSquaredTrend(points: AnalizePoint[], groupLabel?: string) {
|
||||
const source = points
|
||||
.map((point) => ({ ...point, psi_max2: psiMaxSquared(point) }))
|
||||
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.psi_max2) && point.psi_max2 > 0.1)
|
||||
|
||||
if (source.length < 2) return null
|
||||
|
||||
const xMean = source.reduce((sum, point) => sum + point.x, 0) / source.length
|
||||
const yMean = source.reduce((sum, point) => sum + point.psi_max2, 0) / source.length
|
||||
const denominator = source.reduce((sum, point) => sum + (point.x - xMean) ** 2, 0)
|
||||
if (denominator === 0) return null
|
||||
|
||||
const slope = source.reduce((sum, point) => sum + (point.x - xMean) * (point.psi_max2 - yMean), 0) / denominator
|
||||
const intercept = yMean - slope * xMean
|
||||
const sorted = [...source].sort((a, b) => a.x - b.x)
|
||||
const first = sorted[0]
|
||||
const last = sorted[sorted.length - 1]
|
||||
if (!first || !last) return null
|
||||
const zeroX = slope === 0 ? first.x : -intercept / slope
|
||||
const startX = Math.min(first.x, last.x, zeroX)
|
||||
const endX = Math.max(first.x, last.x, zeroX)
|
||||
|
||||
return [startX, endX].map((x) => ({
|
||||
value: [x, slope * x + intercept],
|
||||
case_id: 0,
|
||||
case_name: 'Линейный тренд',
|
||||
group_label: groupLabel,
|
||||
x,
|
||||
omega: null,
|
||||
psi_max: null,
|
||||
psi_max2: slope * x + intercept,
|
||||
}))
|
||||
}
|
||||
|
||||
function buildOption() {
|
||||
if (groupParameter.value && groupedSeriesData.value.length > 0) {
|
||||
const titleGroup = groupLabel.value || groupParameter.value
|
||||
return {
|
||||
title: { text: `Omega / PsiMax^2 vs ${chartTitle.value || selectedLabel.value} grouped by ${titleGroup}` },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params: any) => {
|
||||
const p = params.data
|
||||
const isOmega = params.seriesName.includes('Omega')
|
||||
const valueLabel = isOmega ? 'Omega' : 'PsiMax^2'
|
||||
const value = isOmega ? p.omega : p.psi_max2
|
||||
return [
|
||||
`<strong>${p.case_name}</strong>`,
|
||||
`${selectedLabel.value}: ${p.x}`,
|
||||
`${titleGroup}: ${p.group_label ?? '—'}`,
|
||||
`${valueLabel}: ${value}`,
|
||||
].join('<br/>')
|
||||
},
|
||||
},
|
||||
legend: { top: 0, type: 'scroll' },
|
||||
grid: { left: 56, right: 32, top: 80, bottom: 56, containLabel: true },
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: 0 },
|
||||
{ type: 'inside', yAxisIndex: [0, 1] },
|
||||
{ type: 'slider', xAxisIndex: 0, height: 18, bottom: 8 },
|
||||
],
|
||||
xAxis: { type: 'value', name: selectedLabel.value },
|
||||
yAxis: [
|
||||
{ type: 'value', name: 'Omega', position: 'left' },
|
||||
{ type: 'value', name: 'PsiMax^2', position: 'right' },
|
||||
],
|
||||
series: groupedSeriesData.value.flatMap((group) => {
|
||||
const trend = showTrend.value ? buildPsiMaxSquaredTrend(group.points, group.group_label) : null
|
||||
return [
|
||||
{
|
||||
name: `${group.group_label} · Omega`,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
connectNulls: true,
|
||||
showSymbol: true,
|
||||
yAxisIndex: 0,
|
||||
encode: { x: 0, y: 1 },
|
||||
data: group.points.map((point) => chartPoint(point, point.omega, group.group_label)),
|
||||
},
|
||||
{
|
||||
name: `${group.group_label} · PsiMax^2`,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
connectNulls: true,
|
||||
showSymbol: true,
|
||||
yAxisIndex: 1,
|
||||
encode: { x: 0, y: 1 },
|
||||
data: group.points.map((point) => chartPoint(point, psiMaxSquared(point), group.group_label)),
|
||||
},
|
||||
...(trend
|
||||
? [{
|
||||
name: `${group.group_label} · trend PsiMax^2`,
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
yAxisIndex: 1,
|
||||
encode: { x: 0, y: 1 },
|
||||
lineStyle: { type: 'dashed', width: 2 },
|
||||
data: trend,
|
||||
}]
|
||||
: []),
|
||||
]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const trend = showTrend.value ? buildPsiMaxSquaredTrend(seriesData.value) : null
|
||||
|
||||
return {
|
||||
title: { text: `Omega / PsiMax^2 vs ${chartTitle.value || selectedLabel.value}` },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params: any) => {
|
||||
const p = params.data
|
||||
const valueLabel = params.seriesName === 'Omega' ? 'Omega' : 'PsiMax'
|
||||
const value = params.seriesName === 'Omega' ? p.omega : p.psi_max
|
||||
const isOmega = params.seriesName === 'Omega'
|
||||
const valueLabel = isOmega ? 'Omega' : 'PsiMax^2'
|
||||
const value = isOmega ? p.omega : p.psi_max2
|
||||
return [
|
||||
`<strong>${p.case_name}</strong>`,
|
||||
`${selectedLabel.value}: ${p.x}`,
|
||||
@@ -74,10 +236,15 @@ function buildOption() {
|
||||
},
|
||||
legend: { top: 28 },
|
||||
grid: { left: 56, right: 32, top: 80, bottom: 56, containLabel: true },
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: 0 },
|
||||
{ type: 'inside', yAxisIndex: [0, 1] },
|
||||
{ type: 'slider', xAxisIndex: 0, height: 18, bottom: 8 },
|
||||
],
|
||||
xAxis: { type: 'value', name: selectedLabel.value },
|
||||
yAxis: [
|
||||
{ type: 'value', name: 'Omega', position: 'left' },
|
||||
{ type: 'value', name: 'PsiMax', position: 'right' },
|
||||
{ type: 'value', name: 'PsiMax^2', position: 'right' },
|
||||
],
|
||||
series: [
|
||||
{
|
||||
@@ -85,31 +252,28 @@ function buildOption() {
|
||||
type: 'scatter',
|
||||
symbolSize: 10,
|
||||
yAxisIndex: 0,
|
||||
data: seriesData.value.map((point) => ({
|
||||
case_id: point.case_id,
|
||||
name: point.case_name,
|
||||
value: [point.x, point.omega],
|
||||
case_name: point.case_name,
|
||||
x: point.x,
|
||||
omega: point.omega,
|
||||
psi_max: point.psi_max,
|
||||
})),
|
||||
encode: { x: 0, y: 1 },
|
||||
data: seriesData.value.map((point) => chartPoint(point, point.omega)),
|
||||
},
|
||||
{
|
||||
name: 'PsiMax',
|
||||
name: 'PsiMax^2',
|
||||
type: 'scatter',
|
||||
symbolSize: 10,
|
||||
yAxisIndex: 1,
|
||||
data: seriesData.value.map((point) => ({
|
||||
case_id: point.case_id,
|
||||
name: point.case_name,
|
||||
value: [point.x, point.psi_max],
|
||||
case_name: point.case_name,
|
||||
x: point.x,
|
||||
omega: point.omega,
|
||||
psi_max: point.psi_max,
|
||||
})),
|
||||
encode: { x: 0, y: 1 },
|
||||
data: seriesData.value.map((point) => chartPoint(point, psiMaxSquared(point))),
|
||||
},
|
||||
...(trend
|
||||
? [{
|
||||
name: 'trend PsiMax^2',
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
yAxisIndex: 1,
|
||||
encode: { x: 0, y: 1 },
|
||||
lineStyle: { type: 'dashed', width: 2 },
|
||||
data: trend,
|
||||
}]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -117,22 +281,67 @@ function buildOption() {
|
||||
function renderChart() {
|
||||
if (!chart) return
|
||||
chart.setOption(buildOption(), true)
|
||||
requestAnimationFrame(() => chart?.resize())
|
||||
}
|
||||
|
||||
function buildCaseOption() {
|
||||
if (psiViewMode.value === 'fft') {
|
||||
return {
|
||||
title: { text: 'FFT spectrum of psi' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 0 },
|
||||
grid: { left: 56, right: 24, top: 48, bottom: 56, containLabel: true },
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: 0 },
|
||||
{ type: 'slider', xAxisIndex: 0, height: 18, bottom: 8 },
|
||||
],
|
||||
xAxis: { type: 'value', name: 'f' },
|
||||
yAxis: { type: 'value', name: 'Amplitude' },
|
||||
series: [
|
||||
{
|
||||
name: 'psi_m',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: selectedCaseSpectrum.value?.points.psi_m.map((point) => [point.frequency, point.amplitude]) ?? [],
|
||||
},
|
||||
{
|
||||
name: 'psi_l',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: selectedCaseSpectrum.value?.points.psi_l.map((point) => [point.frequency, point.amplitude]) ?? [],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: { text: 'psi_m / time' },
|
||||
title: { text: 'psi_m / psi_l / time' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 0 },
|
||||
grid: { left: 56, right: 24, top: 48, bottom: 48, containLabel: true },
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: 0 },
|
||||
{ type: 'slider', xAxisIndex: 0, height: 18, bottom: 8 },
|
||||
],
|
||||
xAxis: { type: 'value', name: 't' },
|
||||
yAxis: { type: 'value', name: 'psi_m' },
|
||||
yAxis: { type: 'value', name: 'psi' },
|
||||
series: [
|
||||
{
|
||||
name: 'psi_m',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: selectedCasePoints.value.map((point) => [point.t, point.psi_m]),
|
||||
},
|
||||
{
|
||||
name: 'psi_l',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: selectedCasePsiLPoints.value.map((point) => [point.t, point.psi_l]),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -181,6 +390,11 @@ function buildFieldMapOption() {
|
||||
},
|
||||
},
|
||||
grid: { left: 56, right: 32, top: 64, bottom: 48, containLabel: true },
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: 0 },
|
||||
{ type: 'inside', yAxisIndex: 0 },
|
||||
{ type: 'slider', xAxisIndex: 0, height: 18, bottom: 8 },
|
||||
],
|
||||
xAxis: { type: 'category', name: 'x', data: Array.from({ length: cols }, (_, index) => index) },
|
||||
yAxis: { type: 'category', name: 'y', data: Array.from({ length: rows }, (_, index) => index), inverse: true },
|
||||
visualMap: {
|
||||
@@ -233,12 +447,25 @@ async function loadData() {
|
||||
|
||||
try {
|
||||
const response = await getAnalizeSeries(selectedParameter.value, {
|
||||
group_parameter: groupParameter.value || undefined,
|
||||
filter_parameter: filterParameter.value || undefined,
|
||||
filter_min: filterMin.value === '' ? undefined : Number(filterMin.value),
|
||||
filter_max: filterMax.value === '' ? undefined : Number(filterMax.value),
|
||||
})
|
||||
chartTitle.value = response.label
|
||||
seriesData.value = response.points.map((point) => ({
|
||||
groupedSeriesData.value = (response.groups ?? []).map((group) => ({
|
||||
group_value: group.group_value,
|
||||
group_label: group.group_label,
|
||||
points: group.points.map((point) => ({
|
||||
case_id: point.case_id,
|
||||
case_name: point.case_name,
|
||||
x: point.x,
|
||||
omega: point.omega,
|
||||
psi_max: point.psi_max,
|
||||
})),
|
||||
}))
|
||||
const responsePoints = response.points ?? groupedSeriesData.value.flatMap((group) => group.points)
|
||||
seriesData.value = responsePoints.map((point) => ({
|
||||
case_id: point.case_id,
|
||||
x: point.x,
|
||||
omega: point.omega,
|
||||
@@ -249,6 +476,7 @@ async function loadData() {
|
||||
} catch (err) {
|
||||
error.value = formatApiError(err, 'Не удалось загрузить данные графика.')
|
||||
seriesData.value = []
|
||||
groupedSeriesData.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -260,24 +488,28 @@ async function loadSelectedCase(caseId: number) {
|
||||
selectedCaseError.value = ''
|
||||
selectedCase.value = null
|
||||
selectedCasePoints.value = []
|
||||
selectedCasePsiLPoints.value = []
|
||||
selectedCaseSpectrum.value = null
|
||||
selectedFieldMap.value = null
|
||||
selectedFieldMapError.value = ''
|
||||
selectedFieldMapLoading.value = false
|
||||
fieldMapChart?.clear()
|
||||
|
||||
try {
|
||||
const [caseData, csvData] = await Promise.all([
|
||||
const [caseData, csvData, spectrumData] = await Promise.all([
|
||||
controlCaseApi.retrieve(caseId),
|
||||
getControlCaseChartData(caseId),
|
||||
getControlCasePsiSpectrum(caseId),
|
||||
])
|
||||
|
||||
selectedCase.value = caseData
|
||||
|
||||
const tIndex = csvData.columns.findIndex((column) => column.toLowerCase() === 't')
|
||||
const psiMIndex = csvData.columns.findIndex((column) => column.toLowerCase() === 'psi_m')
|
||||
const psiLIndex = csvData.columns.findIndex((column) => column.toLowerCase() === 'psi_l')
|
||||
|
||||
if (tIndex < 0 || psiMIndex < 0) {
|
||||
throw new Error('CSV does not contain t/psi_m columns')
|
||||
if (tIndex < 0 || psiMIndex < 0 || psiLIndex < 0) {
|
||||
throw new Error('CSV does not contain t/psi_m/psi_l columns')
|
||||
}
|
||||
|
||||
selectedCasePoints.value = csvData.rows
|
||||
@@ -287,6 +519,15 @@ async function loadSelectedCase(caseId: number) {
|
||||
}))
|
||||
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.psi_m))
|
||||
|
||||
selectedCasePsiLPoints.value = csvData.rows
|
||||
.map((row) => ({
|
||||
t: Number(row[tIndex]),
|
||||
psi_l: Number(row[psiLIndex]),
|
||||
}))
|
||||
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.psi_l))
|
||||
|
||||
selectedCaseSpectrum.value = spectrumData
|
||||
|
||||
renderCaseChart()
|
||||
} catch (err) {
|
||||
selectedCaseError.value = formatApiError(err, 'Не удалось загрузить расчетный случай.')
|
||||
@@ -353,8 +594,11 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
watch(selectedParameter, loadData)
|
||||
watch(groupParameter, loadData)
|
||||
watch(showTrend, renderChart)
|
||||
watch(selectedField, renderFieldMap)
|
||||
watch(selectedFieldMap, renderFieldMap)
|
||||
watch(psiViewMode, renderCaseChart)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
@@ -370,16 +614,25 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<section class="d-flex flex-column gap-3">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Analize graph</h1>
|
||||
<p class="text-muted mb-0">Omega и PsiMax по выбранному параметру</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Analize graph</h1>
|
||||
<p class="text-muted mb-0">Omega, PsiMax² и линейный тренд по выбранному параметру</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<select v-model="selectedParameter" class="form-select">
|
||||
<option v-for="option in parameterOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="groupParameter" class="form-select">
|
||||
<option v-for="option in groupOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<label class="form-check form-switch mb-0 text-nowrap">
|
||||
<input v-model="showTrend" class="form-check-input" type="checkbox" />
|
||||
<span class="form-check-label">Тренд</span>
|
||||
</label>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="loadData" :disabled="loading">
|
||||
Обновить
|
||||
</button>
|
||||
@@ -448,15 +701,17 @@ onBeforeUnmount(() => {
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="text-muted small">Название</div>
|
||||
<div class="fw-semibold">{{ selectedCase.Name }}</div>
|
||||
<div class="fw-semibold">{{ selectedCase.name }}</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="text-muted small">Статус</div>
|
||||
<div class="fw-semibold">{{ selectedCase.Status }}</div>
|
||||
<span class="badge" :class="controlCaseStatusMeta(selectedCase.status).className">
|
||||
{{ controlCaseStatusMeta(selectedCase.status).label }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="text-muted small">Params ID</div>
|
||||
<div class="fw-semibold">{{ selectedCase.ParamsID }}</div>
|
||||
<div class="fw-semibold">{{ selectedCase.paramsID }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -487,13 +742,34 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
:class="psiViewMode === 'time' ? 'btn-primary' : 'btn-outline-primary'"
|
||||
type="button"
|
||||
@click="psiViewMode = 'time'"
|
||||
:disabled="!selectedCase"
|
||||
>
|
||||
Time
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
:class="psiViewMode === 'fft' ? 'btn-primary' : 'btn-outline-primary'"
|
||||
type="button"
|
||||
@click="psiViewMode = 'fft'"
|
||||
:disabled="!selectedCaseSpectrum"
|
||||
>
|
||||
FFT
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="position-relative" style="min-height: 360px;">
|
||||
<div ref="caseChartRef" style="height: 360px; width: 100%;"></div>
|
||||
<div
|
||||
v-if="!selectedCase && !selectedCaseLoading && !selectedCaseError"
|
||||
class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center text-muted"
|
||||
>
|
||||
Нажмите на точку на верхнем графике, чтобы увидеть расчетный случай.
|
||||
Нажмите на точку на верхнем графике, чтобы увидеть `psi`.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { analizeApi, recalculateAnalize } from '@/api.ts'
|
||||
import { formatApiError } from '@/api_client.ts'
|
||||
import { formatApiError, isPaginatedResponse } from '@/api_client.ts'
|
||||
import type { Analize } from '@/models.ts'
|
||||
|
||||
const items = ref<Analize[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const recalculatingId = ref<number | null>(null)
|
||||
const page = ref(1)
|
||||
const totalCount = ref(0)
|
||||
const serverPageSize = ref(0)
|
||||
|
||||
const total = computed(() => totalCount.value)
|
||||
const totalPages = computed(() => {
|
||||
const size = serverPageSize.value || items.value.length || 1
|
||||
return Math.max(1, Math.ceil(totalCount.value / size))
|
||||
})
|
||||
const pageStart = computed(() => {
|
||||
const size = serverPageSize.value || items.value.length
|
||||
return totalCount.value === 0 || size === 0 ? 0 : (page.value - 1) * size + 1
|
||||
})
|
||||
const pageEnd = computed(() => {
|
||||
const size = serverPageSize.value || items.value.length
|
||||
return size === 0 ? 0 : Math.min(page.value * size, totalCount.value)
|
||||
})
|
||||
|
||||
function normalizeAnalize(item: Analize | Record<string, unknown>): Analize {
|
||||
const record = item as Record<string, unknown>
|
||||
@@ -32,13 +49,30 @@ function formatDate(value: string | undefined) {
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString()
|
||||
}
|
||||
|
||||
async function goToPage(nextPage: number) {
|
||||
page.value = Math.min(Math.max(1, nextPage), totalPages.value)
|
||||
await loadItems()
|
||||
}
|
||||
|
||||
async function loadItems() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const response = await analizeApi.listAll()
|
||||
items.value = response.map((item) => normalizeAnalize(item))
|
||||
const response = await analizeApi.list({ page: page.value })
|
||||
if (isPaginatedResponse(response)) {
|
||||
items.value = response.results.map((item) => normalizeAnalize(item))
|
||||
totalCount.value = response.count
|
||||
if (serverPageSize.value === 0 && response.results.length > 0) {
|
||||
serverPageSize.value = response.results.length
|
||||
}
|
||||
} else {
|
||||
items.value = response.map((item) => normalizeAnalize(item))
|
||||
totalCount.value = response.length
|
||||
if (serverPageSize.value === 0 && response.length > 0) {
|
||||
serverPageSize.value = response.length
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = formatApiError(err, 'Не удалось загрузить analize.')
|
||||
} finally {
|
||||
@@ -69,7 +103,7 @@ onMounted(loadItems)
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Analize</h1>
|
||||
<p class="text-muted mb-0">Результаты обработки расчетных случаев</p>
|
||||
<p class="text-muted mb-0">Результаты обработки расчетных случаев. Всего записей: {{ total }}</p>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="loadItems" :disabled="loading">
|
||||
Обновить
|
||||
@@ -132,6 +166,16 @@ onMounted(loadItems)
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 p-3 border-top">
|
||||
<div class="text-muted small">
|
||||
Показано {{ pageStart }}-{{ pageEnd }} из {{ totalCount }}
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Pagination">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="goToPage(page - 1)" :disabled="page <= 1">Назад</button>
|
||||
<button class="btn btn-outline-secondary" type="button" disabled>{{ page }} / {{ totalPages }}</button>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="goToPage(page + 1)" :disabled="page >= totalPages">Вперёд</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -31,6 +31,10 @@ function buildOption(payload: CsvChartResponse): echarts.EChartsOption {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 0 },
|
||||
grid: { left: 48, right: 24, top: 48, bottom: 48, containLabel: true },
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: 0 },
|
||||
{ type: 'slider', xAxisIndex: 0, height: 18, bottom: 10 },
|
||||
],
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
name: xKey ?? 'x',
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { controlCaseApi, recalculateControlCaseAnalysis } from '@/api.ts'
|
||||
import { controlCaseApi, recalculateControlCaseAnalysis, createControlCaseInitialCondition } from '@/api.ts'
|
||||
import { formatApiError } from '@/api_client.ts'
|
||||
import type { ControlCase } from '@/models.ts'
|
||||
import { controlCaseStatusMeta } from '@/statuses.ts'
|
||||
|
||||
const route = useRoute()
|
||||
const item = ref<ControlCase | null>(null)
|
||||
const loading = ref(false)
|
||||
const recalculating = ref(false)
|
||||
const creatingInitialCondition = ref(false)
|
||||
const error = ref('')
|
||||
const statusMessage = ref('')
|
||||
|
||||
@@ -73,6 +75,23 @@ async function recalculateAnalysis() {
|
||||
}
|
||||
}
|
||||
|
||||
async function createInitialCondition() {
|
||||
if (!Number.isFinite(id.value)) return
|
||||
|
||||
creatingInitialCondition.value = true
|
||||
error.value = ''
|
||||
statusMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await createControlCaseInitialCondition(id.value)
|
||||
statusMessage.value = `Начальные условия созданы: ${response.initial_condition.file_path}`
|
||||
} catch (err) {
|
||||
error.value = formatApiError(err, 'Не удалось создать начальные условия.')
|
||||
} finally {
|
||||
creatingInitialCondition.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadItem)
|
||||
watch(id, loadItem)
|
||||
</script>
|
||||
@@ -102,6 +121,15 @@ watch(id, loadItem)
|
||||
>
|
||||
{{ recalculating ? 'Пересчет...' : 'Пересчитать анализ' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="item"
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
@click="createInitialCondition"
|
||||
:disabled="creatingInitialCondition"
|
||||
>
|
||||
{{ creatingInitialCondition ? 'Создание...' : 'Взять за начальные условия' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="alert alert-danger mb-0" role="alert">{{ error }}</div>
|
||||
@@ -116,9 +144,11 @@ watch(id, loadItem)
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<div class="text-muted small">Название</div>
|
||||
<div class="fw-semibold">{{ item.Name || '—' }}</div>
|
||||
<div class="fw-semibold">{{ item.name || '—' }}</div>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary fs-6">{{ item.Status || '—' }}</span>
|
||||
<span class="badge fs-6" :class="controlCaseStatusMeta(item.status).className">
|
||||
{{ controlCaseStatusMeta(item.status).label }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
@@ -126,7 +156,7 @@ watch(id, loadItem)
|
||||
<dd class="col-sm-9">{{ item.ID }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Params ID</dt>
|
||||
<dd class="col-sm-9">{{ item.ParamsID }}</dd>
|
||||
<dd class="col-sm-9">{{ item.paramsID }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Создан</dt>
|
||||
<dd class="col-sm-9">{{ formatDate(item.CreatedAt) }}</dd>
|
||||
@@ -146,7 +176,7 @@ watch(id, loadItem)
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0 align-middle">
|
||||
<tbody>
|
||||
<tr v-for="(value, key) in item.Params" :key="key">
|
||||
<tr v-for="(value, key) in item.params" :key="key">
|
||||
<th class="table-light" style="width: 240px">{{ key }}</th>
|
||||
<td>{{ formatValue(value) }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,85 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { controlCaseApi } from '@/api.ts'
|
||||
import { formatApiError, isPaginatedResponse } from '@/api_client.ts'
|
||||
import type { ControlCase } from '@/models.ts'
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { controlCaseApi } from "@/api.ts";
|
||||
import { formatApiError, isPaginatedResponse } from "@/api_client.ts";
|
||||
import type { ControlCase } from "@/models.ts";
|
||||
import { controlCaseStatusMeta } from "@/statuses.ts";
|
||||
|
||||
const items = ref<ControlCase[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const page = ref(1)
|
||||
const totalCount = ref(0)
|
||||
const serverPageSize = ref(0)
|
||||
const items = ref<ControlCase[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const page = ref(1);
|
||||
const totalCount = ref(0);
|
||||
const serverPageSize = ref(0);
|
||||
|
||||
const total = computed(() => totalCount.value)
|
||||
const total = computed(() => totalCount.value);
|
||||
const totalPages = computed(() => {
|
||||
const size = serverPageSize.value || items.value.length || 1
|
||||
return Math.max(1, Math.ceil(totalCount.value / size))
|
||||
})
|
||||
const size = serverPageSize.value || items.value.length || 1;
|
||||
return Math.max(1, Math.ceil(totalCount.value / size));
|
||||
});
|
||||
const pageStart = computed(() => {
|
||||
const size = serverPageSize.value || items.value.length
|
||||
return totalCount.value === 0 || size === 0 ? 0 : (page.value - 1) * size + 1
|
||||
})
|
||||
const size = serverPageSize.value || items.value.length;
|
||||
return totalCount.value === 0 || size === 0 ? 0 : (page.value - 1) * size + 1;
|
||||
});
|
||||
const pageEnd = computed(() => {
|
||||
const size = serverPageSize.value || items.value.length
|
||||
return size === 0 ? 0 : Math.min(page.value * size, totalCount.value)
|
||||
})
|
||||
const size = serverPageSize.value || items.value.length;
|
||||
return size === 0 ? 0 : Math.min(page.value * size, totalCount.value);
|
||||
});
|
||||
|
||||
async function goToPage(nextPage: number) {
|
||||
page.value = Math.min(Math.max(1, nextPage), totalPages.value)
|
||||
await loadItems()
|
||||
page.value = Math.min(Math.max(1, nextPage), totalPages.value);
|
||||
await loadItems();
|
||||
}
|
||||
|
||||
function formatDate(value: string | undefined) {
|
||||
if (!value) return '—'
|
||||
if (!value) return "—";
|
||||
|
||||
const date = new Date(value)
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString()
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
||||
}
|
||||
|
||||
async function loadItems() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
const response = await controlCaseApi.list({ page: page.value })
|
||||
const response = await controlCaseApi.list({ page: page.value });
|
||||
if (isPaginatedResponse(response)) {
|
||||
items.value = response.results
|
||||
totalCount.value = response.count
|
||||
items.value = response.results;
|
||||
totalCount.value = response.count;
|
||||
if (serverPageSize.value === 0 && response.results.length > 0) {
|
||||
serverPageSize.value = response.results.length
|
||||
serverPageSize.value = response.results.length;
|
||||
}
|
||||
} else {
|
||||
items.value = response
|
||||
totalCount.value = response.length
|
||||
items.value = response;
|
||||
totalCount.value = response.length;
|
||||
if (serverPageSize.value === 0 && response.length > 0) {
|
||||
serverPageSize.value = response.length
|
||||
serverPageSize.value = response.length;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = formatApiError(err, 'Не удалось загрузить control_case.')
|
||||
error.value = formatApiError(err, "Не удалось загрузить control_case.");
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadItems)
|
||||
onMounted(loadItems);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="d-flex flex-column gap-3">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div
|
||||
class="d-flex flex-wrap justify-content-between align-items-center gap-2"
|
||||
>
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Control cases</h1>
|
||||
<p class="text-muted mb-0">Всего записей: {{ total }}</p>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="loadItems" :disabled="loading">
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
@click="loadItems"
|
||||
:disabled="loading"
|
||||
>
|
||||
Обновить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="alert alert-danger mb-0" role="alert">{{ error }}</div>
|
||||
<div v-if="error" class="alert alert-danger mb-0" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
@@ -98,16 +108,31 @@ onMounted(loadItems)
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="items.length === 0">
|
||||
<td colspan="6" class="text-center text-muted py-4">Записи не найдены</td>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
Записи не найдены
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="item in items" :key="item.ID">
|
||||
<td class="fw-semibold">{{ item.ID }}</td>
|
||||
<td>{{ item.Name || '—' }}</td>
|
||||
<td><span class="badge text-bg-secondary">{{ item.Status || '—' }}</span></td>
|
||||
<td>{{ item.ParamsID }}</td>
|
||||
<td>{{ item.name || "—" }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
:class="controlCaseStatusMeta(item.status).className"
|
||||
>
|
||||
{{ controlCaseStatusMeta(item.status).label }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ item.paramsID }}</td>
|
||||
<td>{{ formatDate(item.CreatedAt) }}</td>
|
||||
<td class="text-end">
|
||||
<RouterLink class="btn btn-sm btn-primary" :to="{ name: 'control-case-detail', params: { id: item.ID } }">
|
||||
<RouterLink
|
||||
class="btn btn-sm btn-primary"
|
||||
:to="{
|
||||
name: 'control-case-detail',
|
||||
params: { id: item.ID },
|
||||
}"
|
||||
>
|
||||
Открыть
|
||||
</RouterLink>
|
||||
</td>
|
||||
@@ -115,14 +140,36 @@ onMounted(loadItems)
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 p-3 border-top">
|
||||
<div
|
||||
class="d-flex flex-wrap justify-content-between align-items-center gap-2 p-3 border-top"
|
||||
>
|
||||
<div class="text-muted small">
|
||||
Показано {{ pageStart }}-{{ pageEnd }} из {{ totalCount }}
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Pagination">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="goToPage(page - 1)" :disabled="page <= 1">Назад</button>
|
||||
<button class="btn btn-outline-secondary" type="button" disabled>{{ page }} / {{ totalPages }}</button>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="goToPage(page + 1)" :disabled="page >= totalPages">Вперёд</button>
|
||||
<div
|
||||
class="btn-group btn-group-sm"
|
||||
role="group"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
@click="goToPage(page - 1)"
|
||||
:disabled="page <= 1"
|
||||
>
|
||||
Назад
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" type="button" disabled>
|
||||
{{ page }} / {{ totalPages }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
@click="goToPage(page + 1)"
|
||||
:disabled="page >= totalPages"
|
||||
>
|
||||
Вперёд
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { paramsApi } from "@/api.ts";
|
||||
import { initialConditionApi, launchControlCase, paramsApi } from "@/api.ts";
|
||||
import { formatApiError, isPaginatedResponse } from "@/api_client.ts";
|
||||
import { launchControlCase } from "@/api.ts";
|
||||
import type { Params } from "@/models.ts";
|
||||
import ParamsEditCard from "@/components/params/ParamsEditCard.vue";
|
||||
import ParamsQuickBatchCard from "@/components/params/ParamsQuickBatchCard.vue";
|
||||
import ParamsTableCard from "@/components/params/ParamsTableCard.vue";
|
||||
import type { InitialCondition, Params } from "@/models.ts";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const items = ref<Params[]>([]);
|
||||
const initialConditions = ref<InitialCondition[]>([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const launchingId = ref<number | null>(null);
|
||||
@@ -16,6 +20,9 @@ const selectedId = ref<number | null>(null);
|
||||
const page = ref(1);
|
||||
const totalCount = ref(0);
|
||||
const serverPageSize = ref(0);
|
||||
const batchRunning = ref(false);
|
||||
const batchMessage = ref("");
|
||||
const formsPrefilled = ref(false);
|
||||
|
||||
const form = reactive<Params>({
|
||||
id: 0,
|
||||
@@ -25,10 +32,11 @@ const form = reactive<Params>({
|
||||
rel: 0,
|
||||
rel_c: 0,
|
||||
le: 0,
|
||||
pr: 0,
|
||||
sc: 0,
|
||||
pe: 0,
|
||||
ma: 0,
|
||||
initial_condition: "",
|
||||
initial_condition_id: null,
|
||||
initial_condition: null,
|
||||
time: 0,
|
||||
folder_path: "",
|
||||
});
|
||||
@@ -37,10 +45,10 @@ const createForm = reactive({
|
||||
rel: 0,
|
||||
rel_c: 0,
|
||||
le: 0,
|
||||
pr: 0,
|
||||
sc: 0,
|
||||
pe: 0,
|
||||
ma: 0,
|
||||
initial_condition: "",
|
||||
initial_condition_id: null as number | null,
|
||||
time: 0,
|
||||
});
|
||||
|
||||
@@ -49,10 +57,10 @@ const batchForm = reactive({
|
||||
rel: 0,
|
||||
rel_c: 0,
|
||||
le: 0,
|
||||
pr: 0,
|
||||
sc: 0,
|
||||
pe: 0,
|
||||
ma: 0,
|
||||
initial_condition: "",
|
||||
initial_condition_id: null as number | null,
|
||||
time: 0,
|
||||
},
|
||||
variable: "rel",
|
||||
@@ -61,8 +69,7 @@ const batchForm = reactive({
|
||||
step: 1,
|
||||
});
|
||||
|
||||
const batchRunning = ref(false);
|
||||
const batchMessage = ref("");
|
||||
const variableOptions = ["rel", "rel_c", "le", "sc", "pe", "ma", "time"];
|
||||
|
||||
const selectedItem = computed(
|
||||
() => items.value.find((item) => item.id === selectedId.value) ?? null,
|
||||
@@ -83,25 +90,38 @@ const pageEnd = computed(() => {
|
||||
return size === 0 ? 0 : Math.min(page.value * size, totalCount.value);
|
||||
});
|
||||
|
||||
const variableOptions = ["rel", "rel_c", "le", "pr", "pe", "ma", "time"];
|
||||
|
||||
function toNumber(value: string) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function formatDate(value: string | undefined) {
|
||||
if (!value) return "—";
|
||||
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
||||
}
|
||||
|
||||
function selectItem(item: Params) {
|
||||
selectedId.value = item.id;
|
||||
Object.assign(form, structuredClone(item));
|
||||
}
|
||||
|
||||
function fillCreateFormFromParams(item: Params) {
|
||||
createForm.rel = item.rel;
|
||||
createForm.rel_c = item.rel_c;
|
||||
createForm.le = item.le;
|
||||
createForm.sc = item.sc;
|
||||
createForm.pe = item.pe;
|
||||
createForm.ma = item.ma;
|
||||
createForm.initial_condition_id = item.initial_condition_id;
|
||||
createForm.time = item.time;
|
||||
}
|
||||
|
||||
function fillBatchFormFromParams(item: Params) {
|
||||
batchForm.fixed.rel = item.rel;
|
||||
batchForm.fixed.rel_c = item.rel_c;
|
||||
batchForm.fixed.le = item.le;
|
||||
batchForm.fixed.sc = item.sc;
|
||||
batchForm.fixed.pe = item.pe;
|
||||
batchForm.fixed.ma = item.ma;
|
||||
batchForm.fixed.initial_condition_id = item.initial_condition_id;
|
||||
batchForm.fixed.time = item.time;
|
||||
}
|
||||
|
||||
function fillFormsFromParams(item: Params) {
|
||||
fillCreateFormFromParams(item);
|
||||
fillBatchFormFromParams(item);
|
||||
}
|
||||
|
||||
async function goToPage(nextPage: number) {
|
||||
page.value = Math.min(Math.max(1, nextPage), totalPages.value);
|
||||
await loadItems();
|
||||
@@ -111,10 +131,10 @@ function resetCreateForm() {
|
||||
createForm.rel = 0;
|
||||
createForm.rel_c = 0;
|
||||
createForm.le = 0;
|
||||
createForm.pr = 0;
|
||||
createForm.sc = 0;
|
||||
createForm.pe = 0;
|
||||
createForm.ma = 0;
|
||||
createForm.initial_condition = "";
|
||||
createForm.initial_condition_id = null;
|
||||
createForm.time = 0;
|
||||
}
|
||||
|
||||
@@ -122,10 +142,10 @@ function resetBatchForm() {
|
||||
batchForm.fixed.rel = 0;
|
||||
batchForm.fixed.rel_c = 0;
|
||||
batchForm.fixed.le = 0;
|
||||
batchForm.fixed.pr = 0;
|
||||
batchForm.fixed.sc = 0;
|
||||
batchForm.fixed.pe = 0;
|
||||
batchForm.fixed.ma = 0;
|
||||
batchForm.fixed.initial_condition = "";
|
||||
batchForm.fixed.initial_condition_id = null;
|
||||
batchForm.fixed.time = 0;
|
||||
batchForm.variable = "rel";
|
||||
batchForm.start = 0;
|
||||
@@ -170,10 +190,10 @@ async function loadItems() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedId.value) {
|
||||
const first = items.value[0];
|
||||
if (first) selectItem(first);
|
||||
}
|
||||
if (!selectedId.value) {
|
||||
const first = items.value[0];
|
||||
if (first) selectItem(first);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = formatApiError(err, "Не удалось загрузить params.");
|
||||
} finally {
|
||||
@@ -181,6 +201,29 @@ async function loadItems() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInitialConditions() {
|
||||
try {
|
||||
initialConditions.value = await initialConditionApi.listAll();
|
||||
} catch (err) {
|
||||
error.value = formatApiError(err, "Не удалось загрузить начальные условия.");
|
||||
}
|
||||
}
|
||||
|
||||
async function prefillFormsFromLatestParams() {
|
||||
if (formsPrefilled.value) return;
|
||||
|
||||
try {
|
||||
const allParams = await paramsApi.listAll();
|
||||
if (allParams.length === 0) return;
|
||||
|
||||
const latest = allParams.reduce((current, item) => (item.id > current.id ? item : current));
|
||||
fillFormsFromParams(latest);
|
||||
formsPrefilled.value = true;
|
||||
} catch {
|
||||
// Leave default values if the bootstrap fetch fails.
|
||||
}
|
||||
}
|
||||
|
||||
async function saveItem() {
|
||||
if (!selectedId.value) return;
|
||||
|
||||
@@ -189,13 +232,15 @@ async function saveItem() {
|
||||
|
||||
try {
|
||||
const updated = await paramsApi.update(selectedId.value, {
|
||||
...form,
|
||||
rel: Number(form.rel),
|
||||
rel_c: Number(form.rel_c),
|
||||
le: Number(form.le),
|
||||
pr: Number(form.pr),
|
||||
sc: Number(form.sc),
|
||||
pe: Number(form.pe),
|
||||
ma: Number(form.ma),
|
||||
initial_condition_id: form.initial_condition_id,
|
||||
time: Number(form.time),
|
||||
folder_path: form.folder_path,
|
||||
});
|
||||
|
||||
await loadItems();
|
||||
@@ -216,10 +261,10 @@ async function createItem() {
|
||||
rel: Number(createForm.rel),
|
||||
rel_c: Number(createForm.rel_c),
|
||||
le: Number(createForm.le),
|
||||
pr: Number(createForm.pr),
|
||||
sc: Number(createForm.sc),
|
||||
pe: Number(createForm.pe),
|
||||
ma: Number(createForm.ma),
|
||||
initial_condition: createForm.initial_condition,
|
||||
initial_condition_id: createForm.initial_condition_id,
|
||||
time: Number(createForm.time),
|
||||
});
|
||||
|
||||
@@ -249,12 +294,12 @@ async function createBatch() {
|
||||
rel: Number(batchForm.fixed.rel),
|
||||
rel_c: Number(batchForm.fixed.rel_c),
|
||||
le: Number(batchForm.fixed.le),
|
||||
pr: Number(batchForm.fixed.pr),
|
||||
sc: Number(batchForm.fixed.sc),
|
||||
pe: Number(batchForm.fixed.pe),
|
||||
ma: Number(batchForm.fixed.ma),
|
||||
initial_condition: batchForm.fixed.initial_condition,
|
||||
initial_condition_id: batchForm.fixed.initial_condition_id,
|
||||
time: Number(batchForm.fixed.time),
|
||||
} as Record<string, string | number>;
|
||||
} as Record<string, string | number | null>;
|
||||
|
||||
payload[batchForm.variable] = value;
|
||||
|
||||
@@ -291,24 +336,19 @@ async function launchParams(item: Params) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadItems);
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadItems(), loadInitialConditions(), prefillFormsFromLatestParams()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="d-flex flex-column gap-3">
|
||||
<div
|
||||
class="d-flex flex-wrap justify-content-between align-items-center gap-2"
|
||||
>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Params</h1>
|
||||
<p class="text-muted mb-0">Таблица параметров с редактированием</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
@click="loadItems"
|
||||
:disabled="loading"
|
||||
>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="loadItems" :disabled="loading">
|
||||
Обновить
|
||||
</button>
|
||||
</div>
|
||||
@@ -321,326 +361,41 @@ onMounted(loadItems);
|
||||
{{ batchMessage }}
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white fw-semibold">Быстрый расчет</div>
|
||||
<div class="card-body">
|
||||
<form class="row g-3" @submit.prevent="createBatch">
|
||||
<div class="col-12 col-lg-4">
|
||||
<label class="form-label">Переменный параметр</label>
|
||||
<select v-model="batchForm.variable" class="form-select">
|
||||
<option v-for="item in variableOptions" :key="item" :value="item">{{ item }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4 col-lg-2">
|
||||
<label class="form-label">Start</label>
|
||||
<input v-model.number="batchForm.start" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-4 col-lg-2">
|
||||
<label class="form-label">End</label>
|
||||
<input v-model.number="batchForm.end" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-4 col-lg-2">
|
||||
<label class="form-label">Step</label>
|
||||
<input v-model.number="batchForm.step" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<ParamsQuickBatchCard
|
||||
:batch-form="batchForm"
|
||||
:initial-conditions="initialConditions"
|
||||
:variable-options="variableOptions"
|
||||
:running="batchRunning"
|
||||
@submit="createBatch"
|
||||
@reset="resetBatchForm"
|
||||
/>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="text-muted small mb-2">Фиксированные параметры</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label class="form-label">rel</label>
|
||||
<input v-model.number="batchForm.fixed.rel" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label class="form-label">rel_c</label>
|
||||
<input v-model.number="batchForm.fixed.rel_c" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label class="form-label">le</label>
|
||||
<input v-model.number="batchForm.fixed.le" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label class="form-label">pr</label>
|
||||
<input v-model.number="batchForm.fixed.pr" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label class="form-label">pe</label>
|
||||
<input v-model.number="batchForm.fixed.pe" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label class="form-label">ma</label>
|
||||
<input v-model.number="batchForm.fixed.ma" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<label class="form-label">initial_condition</label>
|
||||
<input v-model="batchForm.fixed.initial_condition" type="text" class="form-control" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<label class="form-label">time</label>
|
||||
<input v-model.number="batchForm.fixed.time" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<ParamsTableCard
|
||||
:items="items"
|
||||
:loading="loading"
|
||||
:selected-id="selectedId"
|
||||
:create-form="createForm"
|
||||
:initial-conditions="initialConditions"
|
||||
:saving="saving"
|
||||
:launching-id="launchingId"
|
||||
:page="page"
|
||||
:total-pages="totalPages"
|
||||
:total-count="totalCount"
|
||||
:page-start="pageStart"
|
||||
:page-end="pageEnd"
|
||||
@create="createItem"
|
||||
@select="selectItem"
|
||||
@launch="launchParams"
|
||||
@go-to-page="goToPage"
|
||||
/>
|
||||
|
||||
<div class="col-12 d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="resetBatchForm" :disabled="batchRunning">
|
||||
Сбросить
|
||||
</button>
|
||||
<button class="btn btn-primary" type="submit" :disabled="batchRunning">
|
||||
{{ batchRunning ? "Создание..." : "Создать серию" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div v-if="loading" class="p-4 text-center text-muted">Загрузка...</div>
|
||||
<div v-else class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>id</th>
|
||||
<th>rel</th>
|
||||
<th>rel_c</th>
|
||||
<th>le</th>
|
||||
<th>pr</th>
|
||||
<th>pe</th>
|
||||
<th>ma</th>
|
||||
<th>Initial</th>
|
||||
<th>time</th>
|
||||
<th>folder_path</th>
|
||||
<th class="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="table-success">
|
||||
<td class="fw-semibold text-success">+</td>
|
||||
<td>
|
||||
<input
|
||||
v-model.number="createForm.rel"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Rel"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model.number="createForm.rel_c"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="RelC"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model.number="createForm.le"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Le"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model.number="createForm.pr"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Pr"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model.number="createForm.pe"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Pe"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model.number="createForm.ma"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Ma"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model="createForm.initial_condition"
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Initial"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model.number="createForm.time"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Time"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-muted small">new</td>
|
||||
<td class="text-end">
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
type="button"
|
||||
@click="createItem"
|
||||
:disabled="saving"
|
||||
>
|
||||
{{ saving ? "Сохранение..." : "Добавить" }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="items.length === 0">
|
||||
<td colspan="11" class="text-center text-muted py-4">
|
||||
Записи не найдены
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:class="{ 'table-primary': item.id === selectedId }"
|
||||
>
|
||||
<td class="fw-semibold">{{ item.id }}</td>
|
||||
<td>{{ item.rel }}</td>
|
||||
<td>{{ item.rel_c }}</td>
|
||||
<td>{{ item.le }}</td>
|
||||
<td>{{ item.pr }}</td>
|
||||
<td>{{ item.pe }}</td>
|
||||
<td>{{ item.ma }}</td>
|
||||
<td class="text-truncate" style="max-width: 180px">
|
||||
{{ item.initial_condition || "—" }}
|
||||
</td>
|
||||
<td>{{ item.time }}</td>
|
||||
<td class="text-truncate" style="max-width: 220px">
|
||||
{{ item.folder_path || "—" }}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
class="btn btn-outline-primary"
|
||||
type="button"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
Редактировать
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-success"
|
||||
type="button"
|
||||
@click="launchParams(item)"
|
||||
:disabled="launchingId === item.id"
|
||||
>
|
||||
{{ launchingId === item.id ? 'Запуск...' : 'Запустить' }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 p-3 border-top">
|
||||
<div class="text-muted small">
|
||||
Показано {{ pageStart }}-{{ pageEnd }} из {{ totalCount }}
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Pagination">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="goToPage(page - 1)" :disabled="page <= 1">
|
||||
Назад
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" type="button" disabled>
|
||||
{{ page }} / {{ totalPages }}
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="goToPage(page + 1)" :disabled="page >= totalPages">
|
||||
Вперёд
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedItem" class="card shadow-sm">
|
||||
<div
|
||||
class="card-header bg-white d-flex justify-content-between align-items-center flex-wrap gap-2"
|
||||
>
|
||||
<div>
|
||||
<div class="text-muted small">Редактирование</div>
|
||||
<div class="fw-semibold">Params #{{ selectedItem.id }}</div>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
Created: {{ formatDate(selectedItem.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form class="row g-3" @submit.prevent="saveItem">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">rel</label>
|
||||
<input v-model="form.rel" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">rel_c</label>
|
||||
<input v-model="form.rel_c" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">le</label>
|
||||
<input v-model="form.le" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">pr</label>
|
||||
<input v-model="form.pr" type="number" step="any" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">pe</label>
|
||||
<input
|
||||
v-model="form.pe"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">ma</label>
|
||||
<input
|
||||
v-model="form.ma"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">time</label>
|
||||
<input
|
||||
v-model="form.time"
|
||||
type="number"
|
||||
step="any"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">initial_condition</label>
|
||||
<input
|
||||
v-model="form.initial_condition"
|
||||
type="text"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">folder_path</label>
|
||||
<input v-model="form.folder_path" type="text" class="form-control" />
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-primary" type="submit" :disabled="saving">
|
||||
{{ saving ? "Сохранение..." : "Сохранить" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<ParamsEditCard
|
||||
v-if="selectedItem"
|
||||
:item="selectedItem"
|
||||
:form="form"
|
||||
:initial-conditions="initialConditions"
|
||||
:saving="saving"
|
||||
@submit="saveItem"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user