Добавлен полнофункциональный экспорт/импорт профилей

- Кнопки 'убрать фон' для всех элементов: профиль, группы, ссылки
- Кнопка 'сбросить настройки интерфейса' с подтверждением
- Django app export_import с полным API для бэкапа и восстановления
- Экспорт: создание ZIP архивов с данными профиля и медиафайлами
- Импорт: селективная загрузка групп, ссылок, стилей, медиа
- Обработка мультипарт форм, Django транзакции, управление ошибками
- Полное тестирование: экспорт → импорт данных между пользователями
- API эндпоинты: /api/export/, /api/import/, превью архивов
- Готовая система для производственного развертывания
This commit is contained in:
2025-11-09 14:28:45 +09:00
parent ae54fb7ed1
commit d78c296e5a
16 changed files with 1110 additions and 1 deletions

View File

@@ -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"

View File

@@ -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"