+ Приведены все функции приложения в рабочий вид

+ Наведен порядок в файлах проекта
+ Наведен порядок в документации
+ Настроены скрипты установки, развертки и так далее, расширен MakeFile
This commit is contained in:
2025-11-02 06:09:55 +09:00
parent 367e1c932e
commit 2e535513b5
6103 changed files with 7040 additions and 1027861 deletions

View File

@@ -0,0 +1,672 @@
'use client'
import React, { useState, useEffect } from 'react'
interface DesignSettings {
id?: number
theme_color: string
background_image?: string
background_image_url?: string
dashboard_layout: 'sidebar' | 'grid' | 'list' | 'cards' | 'compact' | 'masonry' | 'timeline' | 'magazine'
groups_default_expanded: boolean
show_group_icons: boolean
show_link_icons: boolean
dashboard_background_color: string
font_family: string
custom_css: string
group_text_color?: string
link_text_color?: string
header_text_color?: string
cover_overlay_enabled?: boolean
cover_overlay_color?: string
cover_overlay_opacity?: number
}
interface CustomizationPanelProps {
isOpen: boolean
onClose: () => void
onSettingsUpdate: (settings: DesignSettings) => void
}
export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: CustomizationPanelProps) {
const [settings, setSettings] = useState<DesignSettings>({
theme_color: '#ffffff',
dashboard_layout: 'list',
groups_default_expanded: true,
show_group_icons: true,
show_link_icons: true,
dashboard_background_color: '#f8f9fa',
font_family: 'sans-serif',
custom_css: '',
group_text_color: '#333333',
link_text_color: '#666666',
header_text_color: '#000000'
})
const [loading, setLoading] = useState(false)
const [activeTab, setActiveTab] = useState<'layout' | 'colors' | 'groups' | 'advanced'>('layout')
const [backgroundImageFile, setBackgroundImageFile] = useState<File | null>(null)
useEffect(() => {
if (isOpen) {
loadSettings()
}
}, [isOpen])
const loadSettings = async () => {
try {
const token = localStorage.getItem('token')
const API = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
const response = await fetch(`${API}/api/customization/settings/`, {
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
const data = await response.json()
setSettings(data)
}
} catch (error) {
console.error('Error loading settings:', error)
}
}
const saveSettings = async () => {
setLoading(true)
try {
const token = localStorage.getItem('token')
const API = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
// Если есть новый файл фоновой картинки, отправляем через FormData
if (backgroundImageFile) {
const formData = new FormData()
// Добавляем все настройки
formData.append('theme_color', settings.theme_color)
formData.append('dashboard_layout', settings.dashboard_layout)
formData.append('groups_default_expanded', settings.groups_default_expanded.toString())
formData.append('show_group_icons', settings.show_group_icons.toString())
formData.append('show_link_icons', settings.show_link_icons.toString())
formData.append('dashboard_background_color', settings.dashboard_background_color)
formData.append('font_family', settings.font_family)
formData.append('custom_css', settings.custom_css)
formData.append('header_text_color', settings.header_text_color || '#000000')
formData.append('group_text_color', settings.group_text_color || '#333333')
formData.append('link_text_color', settings.link_text_color || '#666666')
formData.append('cover_overlay_enabled', (settings.cover_overlay_enabled || false).toString())
formData.append('cover_overlay_color', settings.cover_overlay_color || '#000000')
formData.append('cover_overlay_opacity', (settings.cover_overlay_opacity || 0.3).toString())
formData.append('background_image', backgroundImageFile)
const response = await fetch(`${API}/api/customization/settings/`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
})
if (response.ok) {
const updatedSettings = await response.json()
onSettingsUpdate(updatedSettings)
setBackgroundImageFile(null) // Сбрасываем выбранный файл
onClose()
} else {
const errorData = await response.json()
console.error('Server error:', errorData)
}
} else {
// Если файл не выбран, отправляем только JSON настройки (картинка остается прежней)
const editableSettings = {
theme_color: settings.theme_color,
dashboard_layout: settings.dashboard_layout,
groups_default_expanded: settings.groups_default_expanded,
show_group_icons: settings.show_group_icons,
show_link_icons: settings.show_link_icons,
dashboard_background_color: settings.dashboard_background_color,
font_family: settings.font_family,
custom_css: settings.custom_css,
header_text_color: settings.header_text_color || '#000000',
header_text_color: settings.header_text_color || '#000000',
group_text_color: settings.group_text_color || '#333333',
link_text_color: settings.link_text_color || '#666666',
cover_overlay_enabled: settings.cover_overlay_enabled || false,
cover_overlay_color: settings.cover_overlay_color || '#000000',
cover_overlay_opacity: settings.cover_overlay_opacity || 0.3
}
const response = await fetch(`${API}/api/customization/settings/`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(editableSettings)
})
if (response.ok) {
const updatedSettings = await response.json()
onSettingsUpdate(updatedSettings)
onClose()
} else {
const errorData = await response.json()
console.error('Server error:', errorData)
}
}
} catch (error) {
console.error('Error saving settings:', error)
} finally {
setLoading(false)
}
}
const handleChange = (field: keyof DesignSettings, value: any) => {
setSettings(prev => ({
...prev,
[field]: value
}))
}
if (!isOpen) return null
return (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog modal-lg">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">
<i className="bi bi-palette me-2"></i>
Настройки дашборда
</h5>
<button
type="button"
className="btn-close"
onClick={onClose}
></button>
</div>
<div className="modal-body">
{/* Вкладки */}
<ul className="nav nav-tabs mb-3">
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'layout' ? 'active' : ''}`}
onClick={() => setActiveTab('layout')}
>
<i className="bi bi-layout-sidebar me-1"></i>
Макет
</button>
</li>
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'colors' ? 'active' : ''}`}
onClick={() => setActiveTab('colors')}
>
<i className="bi bi-palette-fill me-1"></i>
Цвета
</button>
</li>
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'groups' ? 'active' : ''}`}
onClick={() => setActiveTab('groups')}
>
<i className="bi bi-collection me-1"></i>
Группы
</button>
</li>
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'advanced' ? 'active' : ''}`}
onClick={() => setActiveTab('advanced')}
>
<i className="bi bi-gear me-1"></i>
Дополнительно
</button>
</li>
</ul>
{/* Содержимое вкладок */}
<div className="tab-content">
{/* Вкладка: Макет */}
{activeTab === 'layout' && (
<div className="tab-pane fade show active">
<div className="row">
<div className="col-12 mb-4">
<label className="form-label fs-5 mb-3">
<i className="bi bi-layout-text-window-reverse me-2"></i>
Стиль отображения групп и ссылок
</label>
<div className="row g-3">
{[
{
value: 'list',
label: 'Список',
icon: 'bi-list-ul',
description: 'Классический вертикальный список'
},
{
value: 'grid',
label: 'Сетка',
icon: 'bi-grid-3x3',
description: 'Равномерная сетка карточек'
},
{
value: 'cards',
label: 'Карточки',
icon: 'bi-card-heading',
description: 'Большие информативные карточки'
},
{
value: 'compact',
label: 'Компактный',
icon: 'bi-layout-text-sidebar',
description: 'Компактное отображение без отступов'
},
{
value: 'sidebar',
label: 'Боковая панель',
icon: 'bi-layout-sidebar',
description: 'Навигация в боковой панели'
},
{
value: 'masonry',
label: 'Кладка',
icon: 'bi-bricks',
description: 'Динамическая сетка разной высоты'
},
{
value: 'timeline',
label: 'Лента времени',
icon: 'bi-clock-history',
description: 'Хронологическое отображение'
},
{
value: 'magazine',
label: 'Журнальный',
icon: 'bi-newspaper',
description: 'Стиль журнала с крупными изображениями'
}
].map((layout) => (
<div key={layout.value} className="col-md-6 col-lg-4">
<div
className={`card text-center h-100 layout-option ${settings.dashboard_layout === layout.value ? 'border-primary bg-primary bg-opacity-10 selected' : 'border-secondary'}`}
style={{ cursor: 'pointer', transition: 'all 0.2s ease' }}
onClick={() => handleChange('dashboard_layout', layout.value)}
>
<div className="card-body d-flex flex-column">
<i className={`${layout.icon} fs-1 mb-3 text-primary`}></i>
<h6 className="card-title mb-2">{layout.label}</h6>
<p className="card-text small text-muted flex-grow-1">{layout.description}</p>
{settings.dashboard_layout === layout.value && (
<div className="mt-2">
<span className="badge bg-primary">
<i className="bi bi-check-lg me-1"></i>
Выбрано
</span>
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
<div className="col-12 mb-3">
<div className="alert alert-info">
<i className="bi bi-info-circle me-2"></i>
<strong>Совет:</strong> Попробуйте разные макеты, чтобы найти наиболее подходящий для вашего контента.
Каждый стиль имеет свои преимущества в зависимости от количества ссылок и их типа.
</div>
</div>
</div>
</div>
)}
{/* Вкладка: Цвета */}
{activeTab === 'colors' && (
<div className="tab-pane fade show active">
<div className="row">
<div className="col-md-6 mb-3">
<label className="form-label">Основной цвет темы</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.theme_color}
onChange={(e) => handleChange('theme_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.theme_color}
onChange={(e) => handleChange('theme_color', e.target.value)}
/>
</div>
</div>
<div className="col-md-6 mb-3">
<label className="form-label">Цвет фона дашборда</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.dashboard_background_color}
onChange={(e) => handleChange('dashboard_background_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.dashboard_background_color}
onChange={(e) => handleChange('dashboard_background_color', e.target.value)}
/>
</div>
</div>
<div className="col-12 mb-3">
<label className="form-label">Фоновое изображение</label>
<div className="mb-2">
<input
type="file"
className="form-control"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) {
setBackgroundImageFile(file)
}
}}
/>
<div className="form-text">
Выберите изображение для фона (JPG, PNG, GIF). Если не выбрано - текущее изображение останется без изменений.
</div>
</div>
{settings.background_image_url && (
<div className="mb-2">
<label className="form-label small">Текущее изображение:</label>
<div>
<img
src={settings.background_image_url}
alt="Текущий фон"
className="img-thumbnail"
style={{ maxWidth: '200px', maxHeight: '100px', objectFit: 'cover' }}
/>
</div>
</div>
)}
{backgroundImageFile && (
<div className="mb-2">
<label className="form-label small">Новое изображение (будет применено после сохранения):</label>
<div className="text-success">
<i className="bi bi-file-earmark-image me-1"></i>
{backgroundImageFile.name}
</div>
</div>
)}
</div>
<div className="col-md-4 mb-3">
<label className="form-label">Цвет заголовков</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.header_text_color || '#000000'}
onChange={(e) => handleChange('header_text_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.header_text_color || '#000000'}
onChange={(e) => handleChange('header_text_color', e.target.value)}
/>
</div>
</div>
<div className="col-md-4 mb-3">
<label className="form-label">Цвет названий групп</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.group_text_color || '#333333'}
onChange={(e) => handleChange('group_text_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.group_text_color || '#333333'}
onChange={(e) => handleChange('group_text_color', e.target.value)}
/>
</div>
</div>
<div className="col-md-4 mb-3">
<label className="form-label">Цвет названий ссылок</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.link_text_color || '#666666'}
onChange={(e) => handleChange('link_text_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.link_text_color || '#666666'}
onChange={(e) => handleChange('link_text_color', e.target.value)}
/>
</div>
</div>
</div>
</div>
)}
{/* Вкладка: Группы */}
{activeTab === 'groups' && (
<div className="tab-pane fade show active">
<div className="row">
<div className="col-12 mb-3">
<h6 className="text-muted">Настройки отображения групп</h6>
</div>
<div className="col-12 mb-3">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
checked={settings.groups_default_expanded}
onChange={(e) => handleChange('groups_default_expanded', e.target.checked)}
/>
<label className="form-check-label">
Развернуть группы по умолчанию
</label>
</div>
</div>
<div className="col-12 mb-3">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
checked={settings.show_group_icons}
onChange={(e) => handleChange('show_group_icons', e.target.checked)}
/>
<label className="form-check-label">
Показывать иконки групп
</label>
</div>
</div>
<div className="col-12 mb-3">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
checked={settings.show_link_icons}
onChange={(e) => handleChange('show_link_icons', e.target.checked)}
/>
<label className="form-check-label">
Показывать иконки ссылок
</label>
</div>
</div>
<div className="col-12">
<div className="alert alert-info">
<i className="bi bi-info-circle me-2"></i>
<strong>Настройки отдельных групп</strong><br/>
Чтобы настроить конкретную группу (публичность, избранное, разворачивание), используйте кнопку редактирования рядом с названием группы в основном списке.
</div>
</div>
</div>
</div>
)}
{/* Вкладка: Дополнительно */}
{activeTab === 'advanced' && (
<div className="tab-pane fade show active">
<div className="row">
<div className="col-12 mb-3">
<label className="form-label">Шрифт</label>
<select
className="form-select"
value={settings.font_family}
onChange={(e) => handleChange('font_family', e.target.value)}
>
<option value="sans-serif">Sans Serif</option>
<option value="serif">Serif</option>
<option value="monospace">Monospace</option>
<option value="Inter, sans-serif">Inter</option>
<option value="Roboto, sans-serif">Roboto</option>
<option value="Open Sans, sans-serif">Open Sans</option>
</select>
</div>
<div className="col-12 mb-3">
<label className="form-label">Дополнительный CSS</label>
<textarea
className="form-control font-monospace"
rows={6}
value={settings.custom_css}
onChange={(e) => handleChange('custom_css', e.target.value)}
placeholder="/* Ваш дополнительный CSS */&#10;.my-custom-class {&#10; color: #333;&#10;}"
/>
</div>
{/* Cover Overlay Section */}
<div className="col-12 mb-3">
<div className="card">
<div className="card-header">
<h6 className="mb-0">Перекрытие обложки</h6>
</div>
<div className="card-body">
<div className="form-check mb-3">
<input
className="form-check-input"
type="checkbox"
id="coverOverlayEnabled"
checked={settings.cover_overlay_enabled || false}
onChange={(e) => handleChange('cover_overlay_enabled', e.target.checked)}
/>
<label className="form-check-label" htmlFor="coverOverlayEnabled">
Включить цветовое перекрытие обложки
</label>
</div>
{settings.cover_overlay_enabled && (
<>
<div className="row">
<div className="col-6 mb-3">
<label className="form-label">Цвет перекрытия</label>
<div className="d-flex gap-2">
<input
type="color"
className="form-control form-control-color"
value={settings.cover_overlay_color || '#000000'}
onChange={(e) => handleChange('cover_overlay_color', e.target.value)}
title="Выберите цвет перекрытия"
/>
<input
type="text"
className="form-control"
value={settings.cover_overlay_color || '#000000'}
onChange={(e) => handleChange('cover_overlay_color', e.target.value)}
placeholder="#000000"
title="Hex код цвета"
/>
</div>
</div>
<div className="col-6 mb-3">
<label className="form-label">
Прозрачность ({Math.round((settings.cover_overlay_opacity || 0.3) * 100)}%)
</label>
<input
type="range"
className="form-range"
min="0"
max="1"
step="0.1"
value={settings.cover_overlay_opacity || 0.3}
onChange={(e) => handleChange('cover_overlay_opacity', parseFloat(e.target.value))}
title="Настройка прозрачности перекрытия"
/>
</div>
</div>
{/* Preview */}
<div className="mb-3">
<label className="form-label">Предварительный просмотр</label>
<div className="position-relative" style={{ height: '100px', border: '1px solid #dee2e6', borderRadius: '0.375rem', overflow: 'hidden' }}>
<div
className="w-100 h-100"
style={{
background: 'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)',
backgroundSize: '20px 20px',
backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px'
}}
></div>
<div
className="position-absolute top-0 start-0 w-100 h-100"
style={{
backgroundColor: settings.cover_overlay_color || '#000000',
opacity: settings.cover_overlay_opacity || 0.3
}}
></div>
</div>
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
>
Отмена
</button>
<button
type="button"
className="btn btn-primary"
onClick={saveSettings}
disabled={loading}
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
Сохранение...
</>
) : (
<>
<i className="bi bi-check-lg me-2"></i>
Сохранить
</>
)}
</button>
</div>
</div>
</div>
</div>
)
}