first commit

This commit is contained in:
brusnitsyn
2026-03-23 00:51:38 +09:00
commit 07854e0a9d
110 changed files with 19528 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
<template>
<AppLayout>
<template #header>
<h2 class="text-xl font-bold">
{{ schedule?.id ? 'Редактирование' : 'Создание' }} расписания миграции
</h2>
</template>
<Card>
<template #content>
<form @submit.prevent="submit" class="flex flex-col gap-4">
<div>
<label for="name" class="block text-sm font-medium mb-1">Название *</label>
<InputText id="name" v-model="form.name" class="w-full" :class="{ 'p-invalid': form.errors.name }" />
<small class="p-error">{{ form.errors.name }}</small>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="source_database_id" class="block text-sm font-medium mb-1">Исходная БД *</label>
<Dropdown id="source_database_id" v-model="form.source_database_id"
:options="sourceDatabases" optionLabel="name" optionValue="id"
class="w-full" :class="{ 'p-invalid': form.errors.source_database_id }"
placeholder="Выберите исходную БД" />
<small class="p-error">{{ form.errors.source_database_id }}</small>
</div>
<div>
<label for="target_database_id" class="block text-sm font-medium mb-1">Целевая БД *</label>
<Dropdown id="target_database_id" v-model="form.target_database_id"
:options="targetDatabases" optionLabel="name" optionValue="id"
class="w-full" :class="{ 'p-invalid': form.errors.target_database_id }"
placeholder="Выберите целевую БД" />
<small class="p-error">{{ form.errors.target_database_id }}</small>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Таблицы *</label>
<Listbox v-model="form.tables" :options="availableTables" optionLabel="table_name" optionValue="id"
multiple class="w-full h-64" :class="{ 'p-invalid': form.errors.tables }" />
<small class="p-error">{{ form.errors.tables }}</small>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="cron_expression" class="block text-sm font-medium mb-1">Cron выражение *</label>
<InputText id="cron_expression" v-model="form.cron_expression" class="w-full"
placeholder="0 * * * *" :class="{ 'p-invalid': form.errors.cron_expression }" />
<small class="text-muted-color">Например: "0 * * * *" - каждый час, "0 0 * * *" - ежедневно</small>
<small class="p-error">{{ form.errors.cron_expression }}</small>
</div>
<div>
<label for="timezone" class="block text-sm font-medium mb-1">Часовой пояс</label>
<InputText id="timezone" v-model="form.timezone" class="w-full" placeholder="UTC" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="batch_size" class="block text-sm font-medium mb-1">Размер пакета</label>
<InputNumber id="batch_size" v-model="form.batch_size" class="w-full" />
</div>
<div>
<label for="python_script_path" class="block text-sm font-medium mb-1">Путь к скрипту Python</label>
<InputText id="python_script_path" v-model="form.python_script_path" class="w-full"
placeholder="/path/to/migration_script.py" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex items-center gap-2">
<Checkbox id="is_active" v-model="form.is_active" :binary="true" />
<label for="is_active" class="text-sm">Активно</label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="is_incremental" v-model="form.is_incremental" :binary="true" />
<label for="is_incremental" class="text-sm">Инкрементальная миграция</label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="run_in_parallel" v-model="form.run_in_parallel" :binary="true" />
<label for="run_in_parallel" class="text-sm">Параллельное выполнение</label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="truncate_before_migration" v-model="form.truncate_before_migration" :binary="true" />
<label for="truncate_before_migration" class="text-sm">Очищать таблицы перед миграцией</label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="create_indexes_after" v-model="form.create_indexes_after" :binary="true" />
<label for="create_indexes_after" class="text-sm">Создавать индексы после миграции</label>
</div>
</div>
<div v-if="form.is_incremental" class="p-4 bg-surface-50 dark:bg-surface-900 rounded-lg">
<label for="incremental_column" class="block text-sm font-medium mb-1">Колонка для инкрементальной миграции</label>
<InputText id="incremental_column" v-model="form.incremental_column" class="w-full"
placeholder="updated_at" />
<small class="text-muted-color">Данные будут мигрированы только с момента последнего запуска (по этой колонке)</small>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex items-center gap-2">
<Checkbox id="use_life_table" v-model="form.use_life_table" :binary="true" />
<label for="use_life_table" class="text-sm">Использовать Life-таблицы (MSSQL)</label>
</div>
</div>
<div v-if="form.use_life_table" class="p-4 bg-surface-50 dark:bg-surface-900 rounded-lg space-y-4">
<h4 class="font-bold text-sm">Настройки Life-таблиц</h4>
<div>
<label for="life_table_name" class="block text-sm font-medium mb-1">Life-таблица</label>
<InputText id="life_table_name" v-model="form.life_table_name" class="w-full"
placeholder="Life_oms_kl_VisitResult" />
<small class="text-muted-color">Например: Life_oms_kl_VisitResult</small>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="life_id_column" class="block text-sm font-medium mb-1">LifeID колонка</label>
<InputText id="life_id_column" v-model="form.life_id_column" class="w-full"
placeholder="kl_VisitResultLifeID" />
</div>
<div>
<label for="base_id_column" class="block text-sm font-medium mb-1">Base ID колонка</label>
<InputText id="base_id_column" v-model="form.base_id_column" class="w-full"
placeholder="kl_VisitResultID" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="operation_column" class="block text-sm font-medium mb-1">Колонка операции</label>
<InputText id="operation_column" v-model="form.operation_column" class="w-full"
placeholder="x_Operation" />
<small class="text-muted-color">x_Operation (i/u/d)</small>
</div>
<div>
<label for="datetime_column" class="block text-sm font-medium mb-1">Колонка времени</label>
<InputText id="datetime_column" v-model="form.datetime_column" class="w-full"
placeholder="x_DateTime" />
<small class="text-muted-color">x_DateTime</small>
</div>
</div>
</div>
<div>
<label for="description" class="block text-sm font-medium mb-1">Описание</label>
<Textarea id="description" v-model="form.description" class="w-full" rows="3" />
</div>
<div class="flex gap-2 pt-4">
<Button type="submit" label="Сохранить" icon="pi pi-check" :loading="form.processing" />
<Link :href="route('migrations.index')">
<Button type="button" label="Отмена" severity="secondary" />
</Link>
</div>
</form>
</template>
</Card>
</AppLayout>
</template>
<script setup>
import { computed, watch } from 'vue';
import { useForm, Link } from '@inertiajs/vue3';
import AppLayout from '@/Layouts/AppLayout.vue';
import Card from 'primevue/card';
import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import Button from 'primevue/button';
import Dropdown from 'primevue/dropdown';
import Listbox from 'primevue/listbox';
import Checkbox from 'primevue/checkbox';
import InputNumber from 'primevue/inputnumber';
const props = defineProps({
schedule: Object,
sourceDatabases: Array,
targetDatabases: Array,
tables: Object,
});
const availableTables = computed(() => {
if (!props.tables) return [];
// Если tables это объект с ключами source_database_id
// if (form.source_database_id && props.tables[form.source_database_id]) {
// const tables = props.tables[form.source_database_id];
// console.log(props.tables)
// return Array.isArray(tables) ? tables : [];
// }
// Если tables это плоский массив
if (Array.isArray(props.tables)) {
return props.tables;
}
// Если tables это объект, преобразуем в массив
if (typeof props.tables === 'object') {
return Object.values(props.tables).flat();
}
return [];
});
const form = useForm({
name: props.schedule?.name || '',
source_database_id: props.schedule?.source_database_id || null,
target_database_id: props.schedule?.target_database_id || null,
tables: props.schedule?.scheduled_tables?.map(t => t.id) || [],
cron_expression: props.schedule?.cron_expression || '',
timezone: props.schedule?.timezone || 'UTC',
is_active: props.schedule?.is_active ?? true,
is_incremental: props.schedule?.is_incremental ?? false,
incremental_column: props.schedule?.incremental_column || 'updated_at',
use_life_table: props.schedule?.use_life_table ?? false,
life_table_name: props.schedule?.life_table_name || '',
life_id_column: props.schedule?.life_id_column || '',
base_id_column: props.schedule?.base_id_column || '',
operation_column: props.schedule?.operation_column || 'x_Operation',
datetime_column: props.schedule?.datetime_column || 'x_DateTime',
run_in_parallel: props.schedule?.run_in_parallel ?? false,
batch_size: props.schedule?.batch_size || 1000,
truncate_before_migration: props.schedule?.truncate_before_migration ?? false,
create_indexes_after: props.schedule?.create_indexes_after ?? true,
python_script_path: props.schedule?.python_script_path || '',
description: props.schedule?.description || '',
});
watch(() => form.source_database_id, (newVal) => {
if (newVal) {
form.tables = [];
}
});
const submit = () => {
if (props.schedule?.id) {
form.put(route('migrations.update', props.schedule.id));
} else {
form.post(route('migrations.store'));
}
};
</script>