Добавлен полнофункциональный экспорт/импорт профилей
- Кнопки 'убрать фон' для всех элементов: профиль, группы, ссылки - Кнопка 'сбросить настройки интерфейса' с подтверждением - Django app export_import с полным API для бэкапа и восстановления - Экспорт: создание ZIP архивов с данными профиля и медиафайлами - Импорт: селективная загрузка групп, ссылок, стилей, медиа - Обработка мультипарт форм, Django транзакции, управление ошибками - Полное тестирование: экспорт → импорт данных между пользователями - API эндпоинты: /api/export/, /api/import/, превью архивов - Готовая система для производственного развертывания
This commit is contained in:
@@ -1226,6 +1226,51 @@ export default function DashboardClient() {
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Иконка группы (опционально)</label>
|
||||
{editingGroup?.icon_url && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label small">Текущая иконка:</label>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<img
|
||||
src={editingGroup.icon_url}
|
||||
alt="Текущая иконка группы"
|
||||
className="img-thumbnail"
|
||||
style={{ width: '32px', height: '32px', objectFit: 'cover' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => {
|
||||
if (confirm('Удалить текущую иконку группы?')) {
|
||||
// Удаляем иконку через API
|
||||
fetch(`/api/groups/${editingGroup.id}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
icon_url: ''
|
||||
})
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// Обновляем локальный объект группы
|
||||
setGroups(groups.map(g =>
|
||||
g.id === editingGroup.id
|
||||
? { ...g, icon_url: '' }
|
||||
: g
|
||||
))
|
||||
setEditingGroup({ ...editingGroup, icon_url: '' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
title="Убрать иконку группы"
|
||||
>
|
||||
<i className="bi bi-trash"></i> Убрать иконку
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
className="form-control"
|
||||
@@ -1236,6 +1281,51 @@ export default function DashboardClient() {
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Фоновое изображение (опционально)</label>
|
||||
{editingGroup?.background_image_url && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label small">Текущий фон:</label>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<img
|
||||
src={editingGroup.background_image_url}
|
||||
alt="Текущий фон группы"
|
||||
className="img-thumbnail"
|
||||
style={{ maxWidth: '150px', maxHeight: '80px', objectFit: 'cover' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => {
|
||||
if (confirm('Удалить текущий фон группы?')) {
|
||||
// Удаляем фон через API
|
||||
fetch(`/api/groups/${editingGroup.id}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
background_image_url: ''
|
||||
})
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// Обновляем локальный объект группы
|
||||
setGroups(groups.map(g =>
|
||||
g.id === editingGroup.id
|
||||
? { ...g, background_image_url: '' }
|
||||
: g
|
||||
))
|
||||
setEditingGroup({ ...editingGroup, background_image_url: '' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
title="Убрать фон группы"
|
||||
>
|
||||
<i className="bi bi-trash"></i> Убрать фон
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
className="form-control"
|
||||
@@ -1323,6 +1413,54 @@ export default function DashboardClient() {
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Иконка (опционально)</label>
|
||||
{editingLink?.icon_url && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label small">Текущая иконка:</label>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<img
|
||||
src={editingLink.icon_url}
|
||||
alt="Текущая иконка"
|
||||
className="img-thumbnail"
|
||||
style={{ width: '32px', height: '32px', objectFit: 'cover' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => {
|
||||
if (confirm('Удалить текущую иконку ссылки?')) {
|
||||
// Удаляем иконку через API
|
||||
fetch(`/api/links/${editingLink.id}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
icon_url: ''
|
||||
})
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// Обновляем локальные данные
|
||||
setGroups(groups.map(g => ({
|
||||
...g,
|
||||
links: g.links.map(l =>
|
||||
l.id === editingLink.id
|
||||
? { ...l, icon_url: '' }
|
||||
: l
|
||||
)
|
||||
})))
|
||||
setEditingLink({ ...editingLink, icon_url: '' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
title="Убрать иконку ссылки"
|
||||
>
|
||||
<i className="bi bi-trash"></i> Убрать иконку
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
className="form-control"
|
||||
|
||||
@@ -464,13 +464,23 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom
|
||||
{settings.background_image_url && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label small">Текущее изображение:</label>
|
||||
<div>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<img
|
||||
src={settings.background_image_url}
|
||||
alt="Текущий фон"
|
||||
className="img-thumbnail"
|
||||
style={{ maxWidth: '200px', maxHeight: '100px', objectFit: 'cover' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => {
|
||||
handleChange('background_image_url', '')
|
||||
}}
|
||||
title="Убрать фон"
|
||||
>
|
||||
<i className="bi bi-trash"></i> Убрать фон
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1019,6 +1029,49 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-warning"
|
||||
onClick={() => {
|
||||
if (confirm('Вы уверены, что хотите сбросить все настройки интерфейса к значениям по умолчанию? Это действие нельзя отменить.')) {
|
||||
// Сброс к дефолтным настройкам
|
||||
const defaultSettings = {
|
||||
theme_color: '#007bff',
|
||||
background_image_url: '',
|
||||
dashboard_layout: 'list' as const,
|
||||
groups_default_expanded: true,
|
||||
show_group_icons: true,
|
||||
show_link_icons: true,
|
||||
dashboard_background_color: '#ffffff',
|
||||
font_family: 'Inter, sans-serif',
|
||||
custom_css: '',
|
||||
group_text_color: '',
|
||||
link_text_color: '',
|
||||
header_text_color: '',
|
||||
cover_overlay_enabled: false,
|
||||
cover_overlay_color: '#000000',
|
||||
cover_overlay_opacity: 0.3,
|
||||
group_overlay_enabled: false,
|
||||
group_overlay_color: '#000000',
|
||||
group_overlay_opacity: 0.3,
|
||||
show_groups_title: true,
|
||||
group_description_text_color: '',
|
||||
body_font_family: 'Inter, sans-serif',
|
||||
heading_font_family: 'Inter, sans-serif',
|
||||
link_overlay_enabled: false,
|
||||
link_overlay_color: '#000000',
|
||||
link_overlay_opacity: 0.3
|
||||
}
|
||||
setSettings(defaultSettings)
|
||||
onSettingsUpdate(defaultSettings)
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
title="Сбросить все настройки к значениям по умолчанию"
|
||||
>
|
||||
<i className="bi bi-arrow-counterclockwise me-2"></i>
|
||||
Сбросить настройки
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
|
||||
Reference in New Issue
Block a user