ваше сообщение коммита
This commit is contained in:
525
frontend/src/components/AIQueueMonitor.vue
Normal file
525
frontend/src/components/AIQueueMonitor.vue
Normal file
@@ -0,0 +1,525 @@
|
||||
<template>
|
||||
<div class="ai-queue-monitor">
|
||||
<div class="monitor-header">
|
||||
<h3>Мониторинг AI Очереди</h3>
|
||||
<div class="refresh-controls">
|
||||
<button @click="refreshStats" :disabled="loading" class="btn-refresh">
|
||||
<i class="fas fa-sync-alt" :class="{ 'fa-spin': loading }"></i>
|
||||
Обновить
|
||||
</button>
|
||||
<label class="auto-refresh">
|
||||
<input type="checkbox" v-model="autoRefresh" />
|
||||
Автообновление
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<!-- Основная статистика -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Статус очереди</div>
|
||||
<div class="stat-value" :class="queueStatusClass">
|
||||
{{ queueStatus }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Задач в очереди</div>
|
||||
<div class="stat-value">{{ stats.currentQueueSize }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Выполняется</div>
|
||||
<div class="stat-value">{{ stats.runningTasks }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Успешность</div>
|
||||
<div class="stat-value" :class="successRateClass">
|
||||
{{ successRate }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Среднее время</div>
|
||||
<div class="stat-value">{{ averageTime }}с</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Всего обработано</div>
|
||||
<div class="stat-value">{{ stats.totalProcessed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Детальная информация -->
|
||||
<div class="detailed-stats">
|
||||
<h4>Детальная статистика</h4>
|
||||
<div class="stats-table">
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Всего задач:</span>
|
||||
<span class="stat-value">{{ stats.totalProcessed }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Успешных:</span>
|
||||
<span class="stat-value success">{{ stats.totalProcessed - stats.totalFailed }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Ошибок:</span>
|
||||
<span class="stat-value error">{{ stats.totalFailed }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Среднее время обработки:</span>
|
||||
<span class="stat-value">{{ Math.round(stats.averageProcessingTime) }}мс</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Последняя обработка:</span>
|
||||
<span class="stat-value">{{ lastProcessedTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Управление очередью (только для админов) -->
|
||||
<div v-if="isAdmin" class="queue-controls">
|
||||
<h4>Управление очередью</h4>
|
||||
<div class="control-buttons">
|
||||
<button @click="controlQueue('pause')" class="btn-control btn-pause">
|
||||
<i class="fas fa-pause"></i>
|
||||
Пауза
|
||||
</button>
|
||||
<button @click="controlQueue('resume')" class="btn-control btn-resume">
|
||||
<i class="fas fa-play"></i>
|
||||
Возобновить
|
||||
</button>
|
||||
<button @click="controlQueue('clear')" class="btn-control btn-clear">
|
||||
<i class="fas fa-trash"></i>
|
||||
Очистить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- График производительности -->
|
||||
<div class="performance-chart">
|
||||
<h4>Производительность</h4>
|
||||
<div class="chart-container">
|
||||
<canvas ref="performanceChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Chart from 'chart.js/auto'
|
||||
|
||||
export default {
|
||||
name: 'AIQueueMonitor',
|
||||
props: {
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const stats = ref({
|
||||
totalProcessed: 0,
|
||||
totalFailed: 0,
|
||||
averageProcessingTime: 0,
|
||||
currentQueueSize: 0,
|
||||
runningTasks: 0,
|
||||
lastProcessedAt: null,
|
||||
isInitialized: false
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const autoRefresh = ref(true)
|
||||
const refreshInterval = ref(null)
|
||||
const performanceChart = ref(null)
|
||||
const chartInstance = ref(null)
|
||||
|
||||
// Вычисляемые свойства
|
||||
const queueStatus = computed(() => {
|
||||
if (!stats.value.isInitialized) return 'Не инициализирована'
|
||||
if (stats.value.currentQueueSize === 0 && stats.value.runningTasks === 0) return 'Пуста'
|
||||
if (stats.value.runningTasks > 0) return 'Работает'
|
||||
return 'Ожидает'
|
||||
})
|
||||
|
||||
const queueStatusClass = computed(() => {
|
||||
if (!stats.value.isInitialized) return 'status-error'
|
||||
if (stats.value.currentQueueSize === 0 && stats.value.runningTasks === 0) return 'status-idle'
|
||||
if (stats.value.runningTasks > 0) return 'status-active'
|
||||
return 'status-waiting'
|
||||
})
|
||||
|
||||
const successRate = computed(() => {
|
||||
if (stats.value.totalProcessed === 0) return 0
|
||||
return Math.round(((stats.value.totalProcessed - stats.value.totalFailed) / stats.value.totalProcessed) * 100)
|
||||
})
|
||||
|
||||
const successRateClass = computed(() => {
|
||||
if (successRate.value >= 95) return 'success'
|
||||
if (successRate.value >= 80) return 'warning'
|
||||
return 'error'
|
||||
})
|
||||
|
||||
const averageTime = computed(() => {
|
||||
return Math.round(stats.value.averageProcessingTime / 1000)
|
||||
})
|
||||
|
||||
const lastProcessedTime = computed(() => {
|
||||
if (!stats.value.lastProcessedAt) return 'Нет данных'
|
||||
return new Date(stats.value.lastProcessedAt).toLocaleString('ru-RU')
|
||||
})
|
||||
|
||||
// Методы
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await axios.get('/api/ai-queue/stats')
|
||||
if (response.data.success) {
|
||||
stats.value = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching queue stats:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshStats = () => {
|
||||
fetchStats()
|
||||
}
|
||||
|
||||
const controlQueue = async (action) => {
|
||||
try {
|
||||
const response = await axios.post('/api/ai-queue/control', { action })
|
||||
if (response.data.success) {
|
||||
await fetchStats()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error controlling queue (${action}):`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const initChart = () => {
|
||||
const ctx = performanceChart.value.getContext('2d')
|
||||
chartInstance.value = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Время обработки (мс)',
|
||||
data: [],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateChart = () => {
|
||||
if (chartInstance.value) {
|
||||
const now = new Date().toLocaleTimeString('ru-RU')
|
||||
chartInstance.value.data.labels.push(now)
|
||||
chartInstance.value.data.datasets[0].data.push(stats.value.averageProcessingTime)
|
||||
|
||||
// Ограничиваем количество точек на графике
|
||||
if (chartInstance.value.data.labels.length > 20) {
|
||||
chartInstance.value.data.labels.shift()
|
||||
chartInstance.value.data.datasets[0].data.shift()
|
||||
}
|
||||
|
||||
chartInstance.value.update()
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдатели
|
||||
watch(autoRefresh, (newValue) => {
|
||||
if (newValue) {
|
||||
refreshInterval.value = setInterval(fetchStats, 5000)
|
||||
} else {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value)
|
||||
refreshInterval.value = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Жизненный цикл
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
initChart()
|
||||
|
||||
if (autoRefresh.value) {
|
||||
refreshInterval.value = setInterval(fetchStats, 5000)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value)
|
||||
}
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
stats,
|
||||
loading,
|
||||
autoRefresh,
|
||||
performanceChart,
|
||||
queueStatus,
|
||||
queueStatusClass,
|
||||
successRate,
|
||||
successRateClass,
|
||||
averageTime,
|
||||
lastProcessedTime,
|
||||
refreshStats,
|
||||
controlQueue
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-queue-monitor {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.monitor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.monitor-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.refresh-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn-refresh {
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-refresh:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-refresh:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auto-refresh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-waiting {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.status-idle {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.detailed-stats {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.detailed-stats h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stats-table {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.stat-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.queue-controls {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.queue-controls h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-control {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-pause {
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-pause:hover {
|
||||
background: #e0a800;
|
||||
}
|
||||
|
||||
.btn-resume {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-resume:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-clear:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.performance-chart {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.performance-chart h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.monitor-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
903
frontend/src/components/EmbeddedDLEInterface.vue
Normal file
903
frontend/src/components/EmbeddedDLEInterface.vue
Normal file
@@ -0,0 +1,903 @@
|
||||
<!--
|
||||
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>
|
||||
<div class="embedded-dle-interface">
|
||||
<!-- Заголовок встраиваемого интерфейса -->
|
||||
<div class="embedded-header">
|
||||
<div class="embedded-title">
|
||||
<h3>{{ dleInfo.name }}</h3>
|
||||
<span class="embedded-subtitle">Встраиваемый интерфейс управления</span>
|
||||
</div>
|
||||
<div class="embedded-status">
|
||||
<span class="status-indicator" :class="connectionStatus">
|
||||
{{ getStatusText() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информация о DLE -->
|
||||
<div class="embedded-info">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Адрес DLE:</label>
|
||||
<span class="address">{{ formatAddress(dleAddress) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Ваш баланс токенов:</label>
|
||||
<span class="balance">{{ userTokenBalance }} {{ dleInfo.symbol }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Общий запас:</label>
|
||||
<span>{{ totalSupply }} {{ dleInfo.symbol }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Кворум:</label>
|
||||
<span>{{ quorumPercentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Быстрые действия -->
|
||||
<div class="quick-actions">
|
||||
<h4>Быстрые действия</h4>
|
||||
<div class="actions-grid">
|
||||
<button
|
||||
@click="createProposal"
|
||||
class="action-btn action-primary"
|
||||
:disabled="!canCreateProposal"
|
||||
>
|
||||
<i class="icon">📝</i>
|
||||
<span>Создать предложение</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="viewProposals"
|
||||
class="action-btn action-secondary"
|
||||
>
|
||||
<i class="icon">📋</i>
|
||||
<span>Просмотр предложений</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="viewTokenHolders"
|
||||
class="action-btn action-secondary"
|
||||
>
|
||||
<i class="icon">👥</i>
|
||||
<span>Держатели токенов</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="viewTreasury"
|
||||
class="action-btn action-secondary"
|
||||
>
|
||||
<i class="icon">💰</i>
|
||||
<span>Казначейство</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Активные предложения -->
|
||||
<div class="active-proposals">
|
||||
<h4>Активные предложения</h4>
|
||||
<div v-if="proposals.length === 0" class="empty-state">
|
||||
<p>Нет активных предложений</p>
|
||||
</div>
|
||||
<div v-else class="proposals-list">
|
||||
<div
|
||||
v-for="proposal in proposals.slice(0, 3)"
|
||||
:key="proposal.id"
|
||||
class="proposal-item"
|
||||
:class="{ 'proposal-signed': proposal.hasSigned }"
|
||||
>
|
||||
<div class="proposal-content">
|
||||
<div class="proposal-title">
|
||||
<span class="proposal-id">#{{ proposal.id }}</span>
|
||||
<span class="proposal-status" :class="getProposalStatusClass(proposal)">
|
||||
{{ getProposalStatusText(proposal) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="proposal-description">{{ proposal.description }}</p>
|
||||
<div class="proposal-meta">
|
||||
<span>Подписи: {{ proposal.signaturesCount }}/{{ proposal.quorumRequired }}</span>
|
||||
<span>До: {{ formatTimeLeft(proposal.timelock) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proposal-actions">
|
||||
<button
|
||||
v-if="!proposal.hasSigned && !proposal.executed"
|
||||
@click="signProposal(proposal.id)"
|
||||
class="btn-sign"
|
||||
:disabled="isSigning"
|
||||
>
|
||||
Подписать
|
||||
</button>
|
||||
<button
|
||||
v-if="canExecuteProposal(proposal)"
|
||||
@click="executeProposal(proposal.id)"
|
||||
class="btn-execute"
|
||||
:disabled="isExecuting"
|
||||
>
|
||||
Выполнить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="proposals.length > 3" class="view-more">
|
||||
<button @click="viewAllProposals" class="btn-view-more">
|
||||
Показать все предложения ({{ proposals.length }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статистика -->
|
||||
<div class="embedded-stats">
|
||||
<h4>Статистика DLE</h4>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ proposals.length }}</div>
|
||||
<div class="stat-label">Всего предложений</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ executedProposals }}</div>
|
||||
<div class="stat-label">Выполнено</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ tokenHolders.length }}</div>
|
||||
<div class="stat-label">Держателей токенов</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ installedModules.length }}</div>
|
||||
<div class="stat-label">Установленных модулей</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Уведомления -->
|
||||
<div v-if="notifications.length > 0" class="notifications">
|
||||
<h4>Уведомления</h4>
|
||||
<div class="notifications-list">
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
class="notification-item"
|
||||
:class="notification.type"
|
||||
>
|
||||
<div class="notification-content">
|
||||
<span class="notification-title">{{ notification.title }}</span>
|
||||
<p class="notification-message">{{ notification.message }}</p>
|
||||
<span class="notification-time">{{ formatTime(notification.timestamp) }}</span>
|
||||
</div>
|
||||
<button @click="dismissNotification(notification.id)" class="btn-dismiss">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, defineProps, defineEmits } from 'vue';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
dleAddress: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
dleInfo: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: 'Неизвестное DLE',
|
||||
symbol: 'TOKEN',
|
||||
location: 'Не указано'
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
'proposal-created',
|
||||
'proposal-signed',
|
||||
'proposal-executed',
|
||||
'interface-action'
|
||||
]);
|
||||
|
||||
// Состояние
|
||||
const connectionStatus = ref('connecting');
|
||||
const userTokenBalance = ref(0);
|
||||
const totalSupply = ref(0);
|
||||
const quorumPercentage = ref(51);
|
||||
const proposals = ref([]);
|
||||
const tokenHolders = ref([]);
|
||||
const installedModules = ref([]);
|
||||
const notifications = ref([]);
|
||||
|
||||
// Состояние загрузки
|
||||
const isSigning = ref(false);
|
||||
const isExecuting = ref(false);
|
||||
|
||||
// Вычисляемые свойства
|
||||
const executedProposals = computed(() => {
|
||||
return proposals.value.filter(p => p.executed).length;
|
||||
});
|
||||
|
||||
const canCreateProposal = computed(() => {
|
||||
return userTokenBalance.value > 0 && connectionStatus.value === 'connected';
|
||||
});
|
||||
|
||||
// Методы
|
||||
const connectToDLE = async () => {
|
||||
try {
|
||||
connectionStatus.value = 'connecting';
|
||||
|
||||
// Здесь будет подключение к DLE через Web3
|
||||
console.log('Подключение к DLE:', props.dleAddress);
|
||||
|
||||
// Имитация подключения
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Загрузка данных
|
||||
await loadDLEData();
|
||||
|
||||
connectionStatus.value = 'connected';
|
||||
} catch (error) {
|
||||
console.error('Ошибка подключения к DLE:', error);
|
||||
connectionStatus.value = 'error';
|
||||
}
|
||||
};
|
||||
|
||||
const loadDLEData = async () => {
|
||||
try {
|
||||
// Загрузка баланса токенов пользователя
|
||||
userTokenBalance.value = 500; // Временные данные
|
||||
|
||||
// Загрузка общего запаса
|
||||
totalSupply.value = 10000;
|
||||
|
||||
// Загрузка предложений
|
||||
proposals.value = [
|
||||
{
|
||||
id: 1,
|
||||
description: 'Перевод 100 токенов партнеру',
|
||||
timelock: Math.floor(Date.now() / 1000) + 3600,
|
||||
signaturesCount: 3000,
|
||||
quorumRequired: 5100,
|
||||
executed: false,
|
||||
hasSigned: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
description: 'Установка нового модуля казначейства',
|
||||
timelock: Math.floor(Date.now() / 1000) + 7200,
|
||||
signaturesCount: 5100,
|
||||
quorumRequired: 5100,
|
||||
executed: false,
|
||||
hasSigned: true
|
||||
}
|
||||
];
|
||||
|
||||
// Загрузка держателей токенов
|
||||
tokenHolders.value = [
|
||||
{ address: '0x1234...5678', balance: 2000 },
|
||||
{ address: '0x8765...4321', balance: 1500 },
|
||||
{ address: '0xabcd...efgh', balance: 1000 }
|
||||
];
|
||||
|
||||
// Загрузка модулей
|
||||
installedModules.value = [
|
||||
{ name: 'Казначейство', address: '0x1111...2222' },
|
||||
{ name: 'Коммуникации', address: '0x3333...4444' }
|
||||
];
|
||||
|
||||
// Загрузка уведомлений
|
||||
notifications.value = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'info',
|
||||
title: 'Новое предложение',
|
||||
message: 'Создано предложение #3 для установки модуля',
|
||||
timestamp: Date.now() - 3600000
|
||||
}
|
||||
];
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных DLE:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const createProposal = () => {
|
||||
emit('interface-action', {
|
||||
action: 'create-proposal',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const viewProposals = () => {
|
||||
emit('interface-action', {
|
||||
action: 'view-proposals',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const viewTokenHolders = () => {
|
||||
emit('interface-action', {
|
||||
action: 'view-token-holders',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const viewTreasury = () => {
|
||||
emit('interface-action', {
|
||||
action: 'view-treasury',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const signProposal = async (proposalId) => {
|
||||
if (isSigning.value) return;
|
||||
|
||||
try {
|
||||
isSigning.value = true;
|
||||
|
||||
// Здесь будет подписание предложения
|
||||
console.log('Подписание предложения:', proposalId);
|
||||
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (proposal) {
|
||||
proposal.signaturesCount += userTokenBalance.value;
|
||||
proposal.hasSigned = true;
|
||||
}
|
||||
|
||||
emit('proposal-signed', { proposalId, dleAddress: props.dleAddress });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка подписания предложения:', error);
|
||||
} finally {
|
||||
isSigning.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const executeProposal = async (proposalId) => {
|
||||
if (isExecuting.value) return;
|
||||
|
||||
try {
|
||||
isExecuting.value = true;
|
||||
|
||||
// Здесь будет выполнение предложения
|
||||
console.log('Выполнение предложения:', proposalId);
|
||||
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (proposal) {
|
||||
proposal.executed = true;
|
||||
}
|
||||
|
||||
emit('proposal-executed', { proposalId, dleAddress: props.dleAddress });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка выполнения предложения:', error);
|
||||
} finally {
|
||||
isExecuting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const viewAllProposals = () => {
|
||||
emit('interface-action', {
|
||||
action: 'view-all-proposals',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const dismissNotification = (notificationId) => {
|
||||
notifications.value = notifications.value.filter(n => n.id !== notificationId);
|
||||
};
|
||||
|
||||
// Утилиты
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
const formatTimeLeft = (timestamp) => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = timestamp - now;
|
||||
|
||||
if (diff <= 0) return 'Истекло';
|
||||
|
||||
const hours = Math.floor(diff / 3600);
|
||||
const minutes = Math.floor((diff % 3600) / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}ч ${minutes}м`;
|
||||
}
|
||||
return `${minutes}м`;
|
||||
};
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleString('ru-RU');
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (connectionStatus.value) {
|
||||
case 'connecting': return 'Подключение...';
|
||||
case 'connected': return 'Подключено';
|
||||
case 'error': return 'Ошибка подключения';
|
||||
default: return 'Неизвестно';
|
||||
}
|
||||
};
|
||||
|
||||
const canExecuteProposal = (proposal) => {
|
||||
return !proposal.executed &&
|
||||
proposal.signaturesCount >= proposal.quorumRequired &&
|
||||
Date.now() >= proposal.timelock * 1000;
|
||||
};
|
||||
|
||||
const getProposalStatusClass = (proposal) => {
|
||||
if (proposal.executed) return 'status-executed';
|
||||
if (proposal.signaturesCount >= proposal.quorumRequired) return 'status-ready';
|
||||
return 'status-pending';
|
||||
};
|
||||
|
||||
const getProposalStatusText = (proposal) => {
|
||||
if (proposal.executed) return 'Выполнено';
|
||||
if (proposal.signaturesCount >= proposal.quorumRequired) return 'Готово';
|
||||
return 'Ожидает';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
connectToDLE();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.embedded-dle-interface {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Заголовок */
|
||||
.embedded-header {
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.embedded-title h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.embedded-subtitle {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-indicator.connecting {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-indicator.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-indicator.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Информация */
|
||||
.embedded-info {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-item span {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.address {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.balance {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Быстрые действия */
|
||||
.quick-actions {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.quick-actions h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: var(--radius-md);
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.action-btn:hover:not(:disabled) {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.action-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.action-btn span {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Предложения */
|
||||
.active-proposals {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.active-proposals h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.proposals-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.proposal-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 15px;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.proposal-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.proposal-item.proposal-signed {
|
||||
border-left: 4px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.proposal-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.proposal-id {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.proposal-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-ready {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.status-executed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.proposal-description {
|
||||
margin: 8px 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.proposal-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.proposal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-sign, .btn-execute {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-sign {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sign:hover:not(:disabled) {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
.btn-execute {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-execute:hover:not(:disabled) {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-sign:disabled, .btn-execute:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.view-more {
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-view-more {
|
||||
background: none;
|
||||
border: 1px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-view-more:hover {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Статистика */
|
||||
.embedded-stats {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.embedded-stats h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
/* Уведомления */
|
||||
.notifications {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.notifications h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.notifications-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.notification-item.info {
|
||||
background: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.notification-item.warning {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
}
|
||||
|
||||
.notification-item.error {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
font-size: 0.8rem;
|
||||
margin: 4px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.btn-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-grey-dark);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-dismiss:hover {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Состояния */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.embedded-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.proposal-meta {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.proposal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
435
frontend/src/components/OllamaModelManager.vue
Normal file
435
frontend/src/components/OllamaModelManager.vue
Normal file
@@ -0,0 +1,435 @@
|
||||
<!--
|
||||
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>
|
||||
<div class="ollama-model-manager">
|
||||
<h3>Управление локальными моделями</h3>
|
||||
|
||||
<!-- Статус подключения -->
|
||||
<div class="connection-status">
|
||||
<div class="status-indicator" :class="{ connected: isConnected }">
|
||||
{{ isConnected ? '🟢 Подключено' : '🔴 Не подключено' }}
|
||||
</div>
|
||||
<button @click="checkConnection" :disabled="checking">
|
||||
{{ checking ? 'Проверка...' : 'Проверить подключение' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Установленные модели -->
|
||||
<div class="installed-models" v-if="isConnected">
|
||||
<h4>Установленные модели</h4>
|
||||
<div v-if="installedModels.length === 0" class="no-models">
|
||||
Нет установленных моделей
|
||||
</div>
|
||||
<div v-else class="models-list">
|
||||
<div v-for="model in installedModels" :key="model.name" class="model-item">
|
||||
<div class="model-info">
|
||||
<div class="model-name">{{ model.name }}</div>
|
||||
<div class="model-size">{{ formatSize(model.size) }}</div>
|
||||
<div class="model-modified">{{ formatDate(model.modified) }}</div>
|
||||
</div>
|
||||
<div class="model-actions">
|
||||
<button @click="removeModel(model.name)" :disabled="removing === model.name" class="remove-btn">
|
||||
{{ removing === model.name ? 'Удаление...' : 'Удалить' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поиск и установка моделей -->
|
||||
<div class="model-search" v-if="isConnected">
|
||||
<h4>Установить новую модель</h4>
|
||||
<div class="search-form">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="Введите название модели (например: qwen2.5:7b, llama2:7b)"
|
||||
@keyup.enter="searchModels"
|
||||
/>
|
||||
<button @click="searchModels" :disabled="searching || !searchQuery">
|
||||
{{ searching ? 'Поиск...' : 'Найти' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Популярные модели -->
|
||||
<div class="popular-models">
|
||||
<h5>Популярные модели:</h5>
|
||||
<div class="popular-list">
|
||||
<button
|
||||
v-for="model in popularModels"
|
||||
:key="model"
|
||||
@click="installModel(model)"
|
||||
:disabled="installing === model"
|
||||
class="popular-model-btn"
|
||||
>
|
||||
{{ installing === model ? 'Установка...' : model }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Инструкции -->
|
||||
<div class="instructions" v-if="!isConnected">
|
||||
<h4>Как установить Ollama</h4>
|
||||
<div class="instruction-steps">
|
||||
<div class="step">
|
||||
<strong>1. Установите Ollama:</strong>
|
||||
<a href="https://ollama.ai/download" target="_blank" class="download-link">
|
||||
Скачать с официального сайта
|
||||
</a>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>2. Запустите Ollama:</strong>
|
||||
<code>ollama serve</code>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>3. Установите модель:</strong>
|
||||
<code>ollama pull qwen2.5:7b</code>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>4. Проверьте подключение:</strong>
|
||||
Нажмите кнопку "Проверить подключение" выше
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const isConnected = ref(false);
|
||||
const checking = ref(false);
|
||||
const installedModels = ref([]);
|
||||
const searchQuery = ref('');
|
||||
const searching = ref(false);
|
||||
const installing = ref('');
|
||||
const removing = ref('');
|
||||
|
||||
const popularModels = [
|
||||
'qwen2.5:7b',
|
||||
'llama2:7b',
|
||||
'mistral:7b',
|
||||
'codellama:7b',
|
||||
'llama2:13b',
|
||||
'qwen2.5:14b'
|
||||
];
|
||||
|
||||
// Проверка подключения к Ollama
|
||||
async function checkConnection() {
|
||||
checking.value = true;
|
||||
try {
|
||||
const response = await axios.get('/ollama/status');
|
||||
isConnected.value = response.data.connected;
|
||||
if (isConnected.value) {
|
||||
await loadInstalledModels();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки подключения:', error);
|
||||
isConnected.value = false;
|
||||
} finally {
|
||||
checking.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка установленных моделей
|
||||
async function loadInstalledModels() {
|
||||
try {
|
||||
const response = await axios.get('/ollama/models');
|
||||
installedModels.value = response.data.models || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки моделей:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Поиск моделей
|
||||
async function searchModels() {
|
||||
if (!searchQuery.value.trim()) return;
|
||||
|
||||
searching.value = true;
|
||||
try {
|
||||
// Здесь можно добавить поиск моделей в реестре Ollama
|
||||
// Пока просто устанавливаем модель напрямую
|
||||
await installModel(searchQuery.value.trim());
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска моделей:', error);
|
||||
} finally {
|
||||
searching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Установка модели
|
||||
async function installModel(modelName) {
|
||||
installing.value = modelName;
|
||||
try {
|
||||
await axios.post('/ollama/install', { model: modelName });
|
||||
await loadInstalledModels();
|
||||
searchQuery.value = '';
|
||||
} catch (error) {
|
||||
console.error('Ошибка установки модели:', error);
|
||||
} finally {
|
||||
installing.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление модели
|
||||
async function removeModel(modelName) {
|
||||
removing.value = modelName;
|
||||
try {
|
||||
await axios.delete(`/ollama/models/${encodeURIComponent(modelName)}`);
|
||||
await loadInstalledModels();
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления модели:', error);
|
||||
} finally {
|
||||
removing.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Форматирование размера
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 Б';
|
||||
const k = 1024;
|
||||
const sizes = ['Б', 'КБ', 'МБ', 'ГБ'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Форматирование даты
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkConnection();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ollama-model-manager {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-indicator.connected {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-indicator:not(.connected) {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.installed-models {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.no-models {
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.models-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.model-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.model-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.model-size, .model-modified {
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.remove-btn:hover:not(:disabled) {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.remove-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.model-search {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-form button {
|
||||
padding: 10px 20px;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-form button:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.search-form button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.popular-models h5 {
|
||||
margin-bottom: 10px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.popular-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.popular-model-btn {
|
||||
padding: 8px 16px;
|
||||
background: #e9ecef;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.popular-model-btn:hover:not(:disabled) {
|
||||
background: #dee2e6;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.popular-model-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.instruction-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.step code {
|
||||
background: #e9ecef;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.download-link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.download-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h3, h4, h5 {
|
||||
margin-bottom: 15px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user