Add comprehensive group customization features
- Add group overlay color and opacity settings - Add font customization (body and heading fonts) - Add group description text color control - Add option to hide 'Groups' title - Update frontend DesignSettings interface - Update CustomizationPanel with new UI controls - Update Django model with new fields - Create migration for new customization options - Update DRF serializer with validation
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-11-09 01:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('customization', '0006_designsettings_cover_overlay_color_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='designsettings',
|
||||||
|
name='body_font_family',
|
||||||
|
field=models.CharField(blank=True, default='', help_text='Шрифт для основного текста', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='designsettings',
|
||||||
|
name='group_description_text_color',
|
||||||
|
field=models.CharField(default='#666666', help_text='Цвет текста описаний групп (hex)', max_length=7),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='designsettings',
|
||||||
|
name='group_overlay_color',
|
||||||
|
field=models.CharField(default='#000000', help_text='Цвет перекрытия групп (hex)', max_length=7),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='designsettings',
|
||||||
|
name='group_overlay_enabled',
|
||||||
|
field=models.BooleanField(default=False, help_text='Включить цветовое перекрытие групп'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='designsettings',
|
||||||
|
name='group_overlay_opacity',
|
||||||
|
field=models.FloatField(default=0.3, help_text='Прозрачность перекрытия групп (0.0 - 1.0)'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='designsettings',
|
||||||
|
name='heading_font_family',
|
||||||
|
field=models.CharField(blank=True, default='', help_text='Шрифт для заголовков', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='designsettings',
|
||||||
|
name='show_groups_title',
|
||||||
|
field=models.BooleanField(default=True, help_text='Показывать заголовок "Группы ссылок"'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -102,6 +102,44 @@ class DesignSettings(models.Model):
|
|||||||
help_text='Прозрачность перекрытия (0.0 - 1.0)'
|
help_text='Прозрачность перекрытия (0.0 - 1.0)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Новые поля для кастомизации групп
|
||||||
|
group_overlay_enabled = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='Включить цветовое перекрытие групп'
|
||||||
|
)
|
||||||
|
group_overlay_color = models.CharField(
|
||||||
|
max_length=7,
|
||||||
|
default='#000000',
|
||||||
|
help_text='Цвет перекрытия групп (hex)'
|
||||||
|
)
|
||||||
|
group_overlay_opacity = models.FloatField(
|
||||||
|
default=0.3,
|
||||||
|
help_text='Прозрачность перекрытия групп (0.0 - 1.0)'
|
||||||
|
)
|
||||||
|
show_groups_title = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text='Показывать заголовок "Группы ссылок"'
|
||||||
|
)
|
||||||
|
group_description_text_color = models.CharField(
|
||||||
|
max_length=7,
|
||||||
|
default='#666666',
|
||||||
|
help_text='Цвет текста описаний групп (hex)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Новые поля для шрифтов
|
||||||
|
body_font_family = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
default='',
|
||||||
|
blank=True,
|
||||||
|
help_text='Шрифт для основного текста'
|
||||||
|
)
|
||||||
|
heading_font_family = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
default='',
|
||||||
|
blank=True,
|
||||||
|
help_text='Шрифт для заголовков'
|
||||||
|
)
|
||||||
|
|
||||||
updated_at = models.DateTimeField(
|
updated_at = models.DateTimeField(
|
||||||
auto_now=True,
|
auto_now=True,
|
||||||
help_text='Дата и время последнего изменения'
|
help_text='Дата и время последнего изменения'
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ class DesignSettingsSerializer(serializers.ModelSerializer):
|
|||||||
'cover_overlay_enabled',
|
'cover_overlay_enabled',
|
||||||
'cover_overlay_color',
|
'cover_overlay_color',
|
||||||
'cover_overlay_opacity',
|
'cover_overlay_opacity',
|
||||||
|
'group_overlay_enabled',
|
||||||
|
'group_overlay_color',
|
||||||
|
'group_overlay_opacity',
|
||||||
|
'show_groups_title',
|
||||||
|
'group_description_text_color',
|
||||||
|
'body_font_family',
|
||||||
|
'heading_font_family',
|
||||||
'updated_at'
|
'updated_at'
|
||||||
]
|
]
|
||||||
read_only_fields = ['id', 'updated_at', 'background_image_url']
|
read_only_fields = ['id', 'updated_at', 'background_image_url']
|
||||||
@@ -197,6 +204,54 @@ class DesignSettingsSerializer(serializers.ModelSerializer):
|
|||||||
raise serializers.ValidationError('Прозрачность должна быть между 0.0 и 1.0')
|
raise serializers.ValidationError('Прозрачность должна быть между 0.0 и 1.0')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def validate_group_overlay_color(self, value):
|
||||||
|
"""
|
||||||
|
Валидация цвета перекрытия групп
|
||||||
|
"""
|
||||||
|
if not value.startswith('#') or len(value) != 7:
|
||||||
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
||||||
|
try:
|
||||||
|
int(value[1:], 16)
|
||||||
|
except ValueError:
|
||||||
|
raise serializers.ValidationError('Некорректный hex цвет')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_group_overlay_opacity(self, value):
|
||||||
|
"""
|
||||||
|
Валидация прозрачности перекрытия групп
|
||||||
|
"""
|
||||||
|
if not 0.0 <= value <= 1.0:
|
||||||
|
raise serializers.ValidationError('Прозрачность должна быть между 0.0 и 1.0')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_group_description_text_color(self, value):
|
||||||
|
"""
|
||||||
|
Валидация цвета описаний групп
|
||||||
|
"""
|
||||||
|
if not value.startswith('#') or len(value) != 7:
|
||||||
|
raise serializers.ValidationError('Цвет должен быть в формате #RRGGBB')
|
||||||
|
try:
|
||||||
|
int(value[1:], 16)
|
||||||
|
except ValueError:
|
||||||
|
raise serializers.ValidationError('Некорректный hex цвет')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_body_font_family(self, value):
|
||||||
|
"""
|
||||||
|
Валидация шрифта основного текста
|
||||||
|
"""
|
||||||
|
if value and len(value) > 100:
|
||||||
|
raise serializers.ValidationError('Название шрифта слишком длинное')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_heading_font_family(self, value):
|
||||||
|
"""
|
||||||
|
Валидация шрифта заголовков
|
||||||
|
"""
|
||||||
|
if value and len(value) > 100:
|
||||||
|
raise serializers.ValidationError('Название шрифта слишком длинное')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class PublicDesignSettingsSerializer(serializers.ModelSerializer):
|
class PublicDesignSettingsSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ interface DesignSettings {
|
|||||||
cover_overlay_enabled?: boolean
|
cover_overlay_enabled?: boolean
|
||||||
cover_overlay_color?: string
|
cover_overlay_color?: string
|
||||||
cover_overlay_opacity?: number
|
cover_overlay_opacity?: number
|
||||||
|
// Новые опции кастомизации
|
||||||
|
group_overlay_enabled?: boolean
|
||||||
|
group_overlay_color?: string
|
||||||
|
group_overlay_opacity?: number
|
||||||
|
show_groups_title?: boolean
|
||||||
|
group_description_text_color?: string
|
||||||
|
body_font_family?: string
|
||||||
|
heading_font_family?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardClient() {
|
export default function DashboardClient() {
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ interface DesignSettings {
|
|||||||
cover_overlay_enabled?: boolean
|
cover_overlay_enabled?: boolean
|
||||||
cover_overlay_color?: string
|
cover_overlay_color?: string
|
||||||
cover_overlay_opacity?: number
|
cover_overlay_opacity?: number
|
||||||
|
// Новые опции кастомизации
|
||||||
|
group_overlay_enabled?: boolean
|
||||||
|
group_overlay_color?: string
|
||||||
|
group_overlay_opacity?: number
|
||||||
|
show_groups_title?: boolean
|
||||||
|
group_description_text_color?: string
|
||||||
|
body_font_family?: string
|
||||||
|
heading_font_family?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CustomizationPanelProps {
|
interface CustomizationPanelProps {
|
||||||
@@ -96,6 +104,13 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom
|
|||||||
formData.append('cover_overlay_enabled', (settings.cover_overlay_enabled || false).toString())
|
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_color', settings.cover_overlay_color || '#000000')
|
||||||
formData.append('cover_overlay_opacity', (settings.cover_overlay_opacity || 0.3).toString())
|
formData.append('cover_overlay_opacity', (settings.cover_overlay_opacity || 0.3).toString())
|
||||||
|
formData.append('group_overlay_enabled', (settings.group_overlay_enabled || false).toString())
|
||||||
|
formData.append('group_overlay_color', settings.group_overlay_color || '#000000')
|
||||||
|
formData.append('group_overlay_opacity', (settings.group_overlay_opacity || 0.3).toString())
|
||||||
|
formData.append('show_groups_title', (settings.show_groups_title !== false).toString())
|
||||||
|
formData.append('group_description_text_color', settings.group_description_text_color || '#666666')
|
||||||
|
formData.append('body_font_family', settings.body_font_family || 'sans-serif')
|
||||||
|
formData.append('heading_font_family', settings.heading_font_family || 'sans-serif')
|
||||||
formData.append('background_image', backgroundImageFile)
|
formData.append('background_image', backgroundImageFile)
|
||||||
|
|
||||||
const response = await fetch(`${API}/api/customization/settings/`, {
|
const response = await fetch(`${API}/api/customization/settings/`, {
|
||||||
@@ -132,7 +147,14 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom
|
|||||||
link_text_color: settings.link_text_color || '#666666',
|
link_text_color: settings.link_text_color || '#666666',
|
||||||
cover_overlay_enabled: settings.cover_overlay_enabled || false,
|
cover_overlay_enabled: settings.cover_overlay_enabled || false,
|
||||||
cover_overlay_color: settings.cover_overlay_color || '#000000',
|
cover_overlay_color: settings.cover_overlay_color || '#000000',
|
||||||
cover_overlay_opacity: settings.cover_overlay_opacity || 0.3
|
cover_overlay_opacity: settings.cover_overlay_opacity || 0.3,
|
||||||
|
group_overlay_enabled: settings.group_overlay_enabled || false,
|
||||||
|
group_overlay_color: settings.group_overlay_color || '#000000',
|
||||||
|
group_overlay_opacity: settings.group_overlay_opacity || 0.3,
|
||||||
|
show_groups_title: settings.show_groups_title !== false,
|
||||||
|
group_description_text_color: settings.group_description_text_color || '#666666',
|
||||||
|
body_font_family: settings.body_font_family || 'sans-serif',
|
||||||
|
heading_font_family: settings.heading_font_family || 'sans-serif'
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API}/api/customization/settings/`, {
|
const response = await fetch(`${API}/api/customization/settings/`, {
|
||||||
@@ -505,6 +527,127 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Новые настройки */}
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<div className="form-check form-switch">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.show_groups_title !== false}
|
||||||
|
onChange={(e) => handleChange('show_groups_title', e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label">
|
||||||
|
Показывать заголовок "Группы ссылок"
|
||||||
|
</label>
|
||||||
|
</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.group_description_text_color || '#666666'}
|
||||||
|
onChange={(e) => handleChange('group_description_text_color', e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={settings.group_description_text_color || '#666666'}
|
||||||
|
onChange={(e) => handleChange('group_description_text_color', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Перекрытие групп цветом */}
|
||||||
|
<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="groupOverlayEnabled"
|
||||||
|
checked={settings.group_overlay_enabled || false}
|
||||||
|
onChange={(e) => handleChange('group_overlay_enabled', e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor="groupOverlayEnabled">
|
||||||
|
Включить цветовое перекрытие групп
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{settings.group_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.group_overlay_color || '#000000'}
|
||||||
|
onChange={(e) => handleChange('group_overlay_color', e.target.value)}
|
||||||
|
title="Выберите цвет перекрытия"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={settings.group_overlay_color || '#000000'}
|
||||||
|
onChange={(e) => handleChange('group_overlay_color', e.target.value)}
|
||||||
|
placeholder="#000000"
|
||||||
|
title="Hex код цвета"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 mb-3">
|
||||||
|
<label className="form-label">
|
||||||
|
Прозрачность ({Math.round((settings.group_overlay_opacity || 0.3) * 100)}%)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
className="form-range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
value={settings.group_overlay_opacity || 0.3}
|
||||||
|
onChange={(e) => handleChange('group_overlay_opacity', parseFloat(e.target.value))}
|
||||||
|
title="Настройка прозрачности перекрытия"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label">Предварительный просмотр</label>
|
||||||
|
<div className="position-relative rounded" style={{ height: '80px', border: '1px solid #dee2e6', overflow: 'hidden' }}>
|
||||||
|
<div
|
||||||
|
className="w-100 h-100 d-flex align-items-center justify-content-center text-white fw-bold"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Пример группы
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="position-absolute top-0 start-0 w-100 h-100"
|
||||||
|
style={{
|
||||||
|
backgroundColor: settings.group_overlay_color || '#000000',
|
||||||
|
opacity: settings.group_overlay_opacity || 0.3
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="alert alert-info">
|
<div className="alert alert-info">
|
||||||
<i className="bi bi-info-circle me-2"></i>
|
<i className="bi bi-info-circle me-2"></i>
|
||||||
@@ -520,8 +663,11 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom
|
|||||||
{activeTab === 'advanced' && (
|
{activeTab === 'advanced' && (
|
||||||
<div className="tab-pane fade show active">
|
<div className="tab-pane fade show active">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 mb-3">
|
<div className="col-12 mb-4">
|
||||||
<label className="form-label">Шрифт</label>
|
<h6 className="text-muted">Настройки шрифтов</h6>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<label className="form-label">Основной шрифт</label>
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={settings.font_family}
|
value={settings.font_family}
|
||||||
@@ -533,8 +679,61 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom
|
|||||||
<option value="Inter, sans-serif">Inter</option>
|
<option value="Inter, sans-serif">Inter</option>
|
||||||
<option value="Roboto, sans-serif">Roboto</option>
|
<option value="Roboto, sans-serif">Roboto</option>
|
||||||
<option value="Open Sans, sans-serif">Open Sans</option>
|
<option value="Open Sans, sans-serif">Open Sans</option>
|
||||||
|
<option value="Source Sans Pro, sans-serif">Source Sans Pro</option>
|
||||||
|
<option value="Lato, sans-serif">Lato</option>
|
||||||
|
<option value="Nunito, sans-serif">Nunito</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<label className="form-label">Шрифт заголовков</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={settings.heading_font_family || settings.font_family}
|
||||||
|
onChange={(e) => handleChange('heading_font_family', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Как основной</option>
|
||||||
|
<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>
|
||||||
|
<option value="Source Sans Pro, sans-serif">Source Sans Pro</option>
|
||||||
|
<option value="Lato, sans-serif">Lato</option>
|
||||||
|
<option value="Nunito, sans-serif">Nunito</option>
|
||||||
|
<option value="Playfair Display, serif">Playfair Display</option>
|
||||||
|
<option value="Merriweather, serif">Merriweather</option>
|
||||||
|
<option value="Oswald, sans-serif">Oswald</option>
|
||||||
|
<option value="Montserrat, sans-serif">Montserrat</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<label className="form-label">Шрифт основного текста</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={settings.body_font_family || settings.font_family}
|
||||||
|
onChange={(e) => handleChange('body_font_family', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Как основной</option>
|
||||||
|
<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>
|
||||||
|
<option value="Source Sans Pro, sans-serif">Source Sans Pro</option>
|
||||||
|
<option value="Lato, sans-serif">Lato</option>
|
||||||
|
<option value="Nunito, sans-serif">Nunito</option>
|
||||||
|
<option value="Georgia, serif">Georgia</option>
|
||||||
|
<option value="Times New Roman, serif">Times New Roman</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 mb-4">
|
||||||
|
<hr />
|
||||||
|
<h6 className="text-muted">Дополнительные настройки</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="col-12 mb-3">
|
<div className="col-12 mb-3">
|
||||||
<label className="form-label">Дополнительный CSS</label>
|
<label className="form-label">Дополнительный CSS</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
Reference in New Issue
Block a user