import flet as ft import designer as d import api_client as api class DashboardView: def __init__(self, page: ft.Page): self.page = page self._stats_row = ft.Row(wrap=True, spacing=16, run_spacing=16) self._recent = ft.Column(spacing=0) def build_page_view(self) -> ft.View: return ft.View( route="/admin", bgcolor=d.BACKGROUND, appbar=_admin_appbar(self.page, "Главная"), controls=[ ft.Row( [ _sidebar(self.page, active="/admin"), ft.Container( content=ft.Column( [ d.headline("Обзор", size=22), ft.Divider(height=8, color="transparent"), self._stats_row, ft.Divider(height=16, color="transparent"), d.headline("Последние статьи", size=18), ft.Divider(height=8, color="transparent"), self._recent, ], scroll=ft.ScrollMode.AUTO, ), expand=True, padding=24, ), ], expand=True, spacing=0, ) ], ) async def load(self): token = self.page.session.get("token") data = await api.admin_list_articles(token, limit=100) if not data: return items = data.get("items", []) total = data.get("total", 0) published = sum(1 for a in items if a.get("status") == "published") drafts = total - published self._stats_row.controls = [ d.stat_card("Всего статей", str(total), ft.icons.ARTICLE, d.PRIMARY), d.stat_card("Опубликовано", str(published), ft.icons.CHECK_CIRCLE, d.SUCCESS), d.stat_card("Черновики", str(drafts), ft.icons.EDIT_NOTE, d.WARNING), ] self._recent.controls = [_recent_item(a, self.page) for a in items[:10]] self.page.update() def _recent_item(article: dict, page: ft.Page) -> ft.Container: return ft.Container( content=ft.Row( [ d.status_badge(article.get("status", "draft")), ft.Text(article["title"], size=14, expand=True, max_lines=1, overflow=ft.TextOverflow.ELLIPSIS), ft.TextButton( "Редактировать", on_click=lambda e, aid=article["id"]: page.go(f"/admin/articles/{aid}"), ), ], spacing=12, vertical_alignment=ft.CrossAxisAlignment.CENTER, ), padding=ft.padding.symmetric(vertical=10, horizontal=4), border=ft.Border(bottom=ft.BorderSide(1, d.DIVIDER)), ) # ─── Общие admin-компоненты ───────────────────────────────────────────────── def _admin_appbar(page: ft.Page, title: str) -> ft.AppBar: return ft.AppBar( title=ft.Text(title, size=18, weight=ft.FontWeight.W_600, color=ft.colors.WHITE), bgcolor=d.ADMIN_SIDEBAR, color=ft.colors.WHITE, actions=[ ft.TextButton( "На сайт", icon=ft.icons.OPEN_IN_NEW, style=ft.ButtonStyle(color=ft.colors.WHITE70), on_click=lambda e: page.launch_url("/"), ), ft.IconButton( ft.icons.LOGOUT, tooltip="Выйти", icon_color=ft.colors.WHITE70, on_click=lambda e: _logout(page), ), ], ) def _logout(page: ft.Page): page.session.remove("token") page.session.remove("username") page.go("/admin/login") def _sidebar(page: ft.Page, active: str = "") -> ft.Container: items = [ (ft.icons.DASHBOARD, "Главная", "/admin"), (ft.icons.ARTICLE, "Статьи", "/admin/articles"), (ft.icons.CATEGORY, "Категории", "/admin/categories"), (ft.icons.PERM_MEDIA, "Медиа", "/admin/media"), ] def nav_item(icon, label, route): is_active = active == route return ft.Container( content=ft.Row( [ ft.Icon(icon, color=d.ADMIN_SIDEBAR_TEXT if not is_active else d.ACCENT, size=20), ft.Text(label, color=d.ADMIN_SIDEBAR_TEXT, size=14, weight=ft.FontWeight.W_500 if is_active else ft.FontWeight.NORMAL), ], spacing=12, ), bgcolor=d.ADMIN_SIDEBAR_ACTIVE if is_active else None, border_radius=8, padding=ft.padding.symmetric(horizontal=16, vertical=12), on_click=lambda e, r=route: page.go(r), ink=True, ) return ft.Container( content=ft.Column( [ft.Container(height=16)] + [nav_item(i, l, r) for i, l, r in items], spacing=2, ), bgcolor=d.ADMIN_SIDEBAR, width=220, padding=ft.padding.symmetric(horizontal=8, vertical=8), )