ваше сообщение коммита
This commit is contained in:
773
frontend/src/views/smartcontracts/ModulesView.vue
Normal file
773
frontend/src/views/smartcontracts/ModulesView.vue
Normal file
@@ -0,0 +1,773 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="modules-management">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Модули DLE</h1>
|
||||
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}</p>
|
||||
<p v-else-if="isLoadingDle">Загрузка...</p>
|
||||
<p v-else>DLE не выбран</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Информация о модулях -->
|
||||
<div class="modules-info">
|
||||
<div class="info-card">
|
||||
<h3>📊 Информация о модулях</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<strong>Всего модулей:</strong> {{ modulesCount }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>Активных модулей:</strong> {{ activeModulesCount }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>Неактивных модулей:</strong> {{ inactiveModulesCount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма добавления модуля -->
|
||||
<div class="add-module-form">
|
||||
<div class="form-header">
|
||||
<h3>➕ Добавить модуль</h3>
|
||||
<p>Создать предложение для добавления нового модуля</p>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="moduleId">ID модуля:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="moduleId"
|
||||
v-model="newModule.moduleId"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
<small class="form-help">Уникальный идентификатор модуля (bytes32)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="moduleAddress">Адрес модуля:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="moduleAddress"
|
||||
v-model="newModule.moduleAddress"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
<small class="form-help">Адрес контракта модуля</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="moduleDescription">Описание предложения:</label>
|
||||
<textarea
|
||||
id="moduleDescription"
|
||||
v-model="newModule.description"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Описание предложения для добавления модуля..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="moduleDuration">Продолжительность голосования (сек):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="moduleDuration"
|
||||
v-model="newModule.duration"
|
||||
class="form-control"
|
||||
placeholder="86400"
|
||||
>
|
||||
<small class="form-help">Время голосования в секундах (86400 = 1 день)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="moduleChainId">ID сети:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="moduleChainId"
|
||||
v-model="newModule.chainId"
|
||||
class="form-control"
|
||||
placeholder="11155111"
|
||||
>
|
||||
<small class="form-help">ID сети (11155111 = Sepolia)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="handleCreateAddModuleProposal"
|
||||
:disabled="!isFormValid || isCreating"
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{{ isCreating ? 'Создание предложения...' : 'Создать предложение' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Список модулей -->
|
||||
<div class="modules-list">
|
||||
<div class="list-header">
|
||||
<h3>📋 Модули DLE</h3>
|
||||
<button class="btn btn-sm btn-outline-secondary" @click="loadModules" :disabled="isLoadingModules">
|
||||
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingModules }"></i> Обновить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingModules" class="loading-modules">
|
||||
<p>Загрузка модулей...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="modules.length === 0" class="no-modules">
|
||||
<p>Модулей пока нет</p>
|
||||
<p>Используйте форму выше для добавления первого модуля</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="modules-grid">
|
||||
<div
|
||||
v-for="module in modules"
|
||||
:key="module.moduleId"
|
||||
class="module-card"
|
||||
:class="{ 'active': module.isActive, 'inactive': !module.isActive }"
|
||||
>
|
||||
<div class="module-header">
|
||||
<h5>{{ module.moduleId }}</h5>
|
||||
<span class="module-status" :class="{ 'active': module.isActive, 'inactive': !module.isActive }">
|
||||
{{ module.isActive ? 'Активен' : 'Неактивен' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="module-details">
|
||||
<div class="detail-item">
|
||||
<strong>Адрес:</strong>
|
||||
<a
|
||||
:href="`https://sepolia.etherscan.io/address/${module.moduleAddress}`"
|
||||
target="_blank"
|
||||
class="address-link"
|
||||
>
|
||||
{{ shortenAddress(module.moduleAddress) }}
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-actions">
|
||||
<button
|
||||
v-if="module.isActive"
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="handleCreateRemoveModuleProposal(module.moduleId)"
|
||||
:disabled="isRemoving === module.moduleId"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
{{ isRemoving === module.moduleId ? 'Создание предложения...' : 'Удалить' }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-sm btn-success"
|
||||
@click="activateModule(module.moduleId)"
|
||||
:disabled="isActivating === module.moduleId"
|
||||
>
|
||||
<i class="fas fa-check"></i>
|
||||
{{ isActivating === module.moduleId ? 'Активация...' : 'Активировать' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref, onMounted, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import {
|
||||
createAddModuleProposal,
|
||||
createRemoveModuleProposal,
|
||||
isModuleActive,
|
||||
getModuleAddress,
|
||||
getAllModules
|
||||
} from '../../services/modulesService.js';
|
||||
import api from '../../api/axios';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: { type: Boolean, default: false },
|
||||
identities: { type: Array, default: () => [] },
|
||||
tokenBalances: { type: Object, default: () => ({}) },
|
||||
isLoadingTokens: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Состояние
|
||||
const selectedDle = ref(null);
|
||||
const isLoadingDle = ref(false);
|
||||
const modules = ref([]);
|
||||
const isLoadingModules = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const isRemoving = ref(null);
|
||||
const isActivating = ref(null);
|
||||
|
||||
// Форма нового модуля
|
||||
const newModule = ref({
|
||||
moduleId: '',
|
||||
moduleAddress: '',
|
||||
description: '',
|
||||
duration: 86400,
|
||||
chainId: 11155111
|
||||
});
|
||||
|
||||
// Вычисляемые свойства
|
||||
const isFormValid = computed(() => {
|
||||
return newModule.value.moduleId &&
|
||||
newModule.value.moduleAddress &&
|
||||
newModule.value.description &&
|
||||
newModule.value.duration > 0 &&
|
||||
newModule.value.chainId > 0;
|
||||
});
|
||||
|
||||
const modulesCount = computed(() => modules.value.length);
|
||||
const activeModulesCount = computed(() => modules.value.filter(m => m.isActive).length);
|
||||
const inactiveModulesCount = computed(() => modules.value.filter(m => !m.isActive).length);
|
||||
|
||||
// Загрузка данных DLE
|
||||
async function loadDleData() {
|
||||
try {
|
||||
isLoadingDle.value = true;
|
||||
const dleAddress = route.query.address;
|
||||
|
||||
if (!dleAddress) {
|
||||
console.error('Адрес DLE не указан');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ModulesView] Загрузка данных DLE:', dleAddress);
|
||||
|
||||
// Читаем данные из блокчейна
|
||||
const response = await api.post('/blockchain/read-dle-info', {
|
||||
dleAddress: dleAddress
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
selectedDle.value = response.data.data;
|
||||
console.log('[ModulesView] Данные DLE загружены:', selectedDle.value);
|
||||
} else {
|
||||
console.error('[ModulesView] Ошибка загрузки DLE:', response.data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ModulesView] Ошибка загрузки DLE:', error);
|
||||
} finally {
|
||||
isLoadingDle.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка модулей
|
||||
async function loadModules() {
|
||||
try {
|
||||
isLoadingModules.value = true;
|
||||
const dleAddress = route.query.address;
|
||||
|
||||
if (!dleAddress) {
|
||||
console.error('Адрес DLE не указан');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ModulesView] Загрузка модулей для DLE:', dleAddress);
|
||||
|
||||
// Загружаем модули через modulesService
|
||||
const modulesResponse = await getAllModules(dleAddress);
|
||||
|
||||
if (modulesResponse.success) {
|
||||
modules.value = modulesResponse.data.modules || [];
|
||||
console.log('[ModulesView] Модули загружены:', modules.value);
|
||||
} else {
|
||||
console.error('[ModulesView] Ошибка загрузки модулей:', modulesResponse.error);
|
||||
modules.value = [];
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ModulesView] Ошибка загрузки модулей:', error);
|
||||
modules.value = [];
|
||||
} finally {
|
||||
isLoadingModules.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Создание предложения добавления модуля
|
||||
async function handleCreateAddModuleProposal() {
|
||||
try {
|
||||
isCreating.value = true;
|
||||
const dleAddress = route.query.address;
|
||||
|
||||
if (!dleAddress) {
|
||||
alert('Адрес DLE не указан');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ModulesView] Создание предложения добавления модуля:', newModule.value);
|
||||
|
||||
// Создаем предложение через modulesService
|
||||
const result = await createAddModuleProposal(dleAddress, {
|
||||
description: newModule.value.description,
|
||||
duration: newModule.value.duration,
|
||||
moduleId: newModule.value.moduleId,
|
||||
moduleAddress: newModule.value.moduleAddress,
|
||||
chainId: newModule.value.chainId
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('[ModulesView] Предложение создано:', result);
|
||||
alert('✅ Предложение для добавления модуля создано!');
|
||||
|
||||
// Очищаем форму
|
||||
newModule.value = {
|
||||
moduleId: '',
|
||||
moduleAddress: '',
|
||||
description: '',
|
||||
duration: 86400,
|
||||
chainId: 11155111
|
||||
};
|
||||
|
||||
// Перезагружаем модули
|
||||
await loadModules();
|
||||
} else {
|
||||
alert('❌ Ошибка создания предложения: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ModulesView] Ошибка создания предложения:', error);
|
||||
alert('❌ Ошибка создания предложения: ' + error.message);
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Создание предложения удаления модуля
|
||||
async function handleCreateRemoveModuleProposal(moduleId) {
|
||||
try {
|
||||
isRemoving.value = moduleId;
|
||||
const dleAddress = route.query.address;
|
||||
|
||||
if (!dleAddress) {
|
||||
alert('Адрес DLE не указан');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ModulesView] Создание предложения удаления модуля:', moduleId);
|
||||
|
||||
// Создаем предложение через modulesService
|
||||
const result = await createRemoveModuleProposal(dleAddress, {
|
||||
description: `Удаление модуля ${moduleId}`,
|
||||
duration: 86400, // 1 день
|
||||
moduleId: moduleId,
|
||||
chainId: 11155111 // Sepolia
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('[ModulesView] Предложение удаления создано:', result);
|
||||
alert('✅ Предложение для удаления модуля создано!');
|
||||
|
||||
// Перезагружаем модули
|
||||
await loadModules();
|
||||
} else {
|
||||
alert('❌ Ошибка создания предложения: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ModulesView] Ошибка создания предложения удаления:', error);
|
||||
alert('❌ Ошибка создания предложения: ' + error.message);
|
||||
} finally {
|
||||
isRemoving.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Активация модуля (заглушка)
|
||||
async function activateModule(moduleId) {
|
||||
try {
|
||||
isActivating.value = moduleId;
|
||||
console.log('[ModulesView] Активация модуля:', moduleId);
|
||||
|
||||
// Здесь нужно будет реализовать активацию модуля
|
||||
alert('Функция активации модуля будет реализована позже');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ModulesView] Ошибка активации модуля:', error);
|
||||
alert('❌ Ошибка активации модуля: ' + error.message);
|
||||
} finally {
|
||||
isActivating.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Утилиты
|
||||
function shortenAddress(address) {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
onMounted(() => {
|
||||
loadDleData();
|
||||
loadModules();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modules-management {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Информация о модулях */
|
||||
.modules-info {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Форма добавления модуля */
|
||||
.add-module-form {
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-header h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
margin: 0 0 20px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Список модулей */
|
||||
.modules-list {
|
||||
background: white;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.list-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.loading-modules,
|
||||
.no-modules {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.modules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.module-card {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 15px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.module-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.module-card.active {
|
||||
border-color: #28a745;
|
||||
background: #f8fff9;
|
||||
}
|
||||
|
||||
.module-card.inactive {
|
||||
border-color: #dc3545;
|
||||
background: #fff8f8;
|
||||
}
|
||||
|
||||
.module-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.module-header h5 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.module-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.module-status.active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.module-status.inactive {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.module-details {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-item strong {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.address-link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.address-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.module-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
background: transparent;
|
||||
color: #6c757d;
|
||||
border: 1px solid #6c757d;
|
||||
}
|
||||
|
||||
.btn-outline-secondary:hover:not(:disabled) {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modules-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user