add dashboard
This commit is contained in:
220
stats/designer.py
Normal file
220
stats/designer.py
Normal file
@@ -0,0 +1,220 @@
|
||||
"""
|
||||
designer.py для stats-приложения.
|
||||
Та же палитра и компоненты что в web/designer.py, адаптированные для дашборда.
|
||||
"""
|
||||
import flet as ft
|
||||
|
||||
SCALE: float = 1.0
|
||||
|
||||
|
||||
def s(v: int | float) -> int:
|
||||
return round(v * SCALE)
|
||||
|
||||
|
||||
class colors:
|
||||
primary = "#F85A40"
|
||||
primary_dark = "#D94530"
|
||||
primary_light = "#FFEAB7"
|
||||
background = "#F4F6F8"
|
||||
surface = "#FFFFFF"
|
||||
surface_alt = "#F0F2F5"
|
||||
accent = "#FFC845"
|
||||
info = "#007FBD"
|
||||
success = "#85C446"
|
||||
text_primary = "#1E2A3A"
|
||||
text_secondary = "#5A6779"
|
||||
text_on_primary = "#FFFFFF"
|
||||
border = "#DDE3EA"
|
||||
|
||||
|
||||
class SearchableDropdown(ft.Container):
|
||||
"""Выпадающий список с поисковой строкой."""
|
||||
|
||||
def __init__(self, label: str = "", hint_text: str = "", options=None, on_select=None):
|
||||
self._label_str = label
|
||||
self._hint_str = hint_text
|
||||
self._options: list = options or []
|
||||
self.on_select = on_select
|
||||
self._value = None
|
||||
|
||||
self._value_text = ft.Text(
|
||||
hint_text,
|
||||
color=colors.border,
|
||||
size=s(14),
|
||||
italic=True,
|
||||
expand=True,
|
||||
no_wrap=True,
|
||||
overflow=ft.TextOverflow.ELLIPSIS,
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
content=ft.Column(
|
||||
spacing=s(2),
|
||||
tight=True,
|
||||
controls=[
|
||||
ft.Text(label, color=colors.text_secondary, size=s(12), weight=ft.FontWeight.W_500),
|
||||
ft.Row(
|
||||
controls=[
|
||||
self._value_text,
|
||||
ft.Icon(ft.Icons.ARROW_DROP_DOWN, color=colors.text_secondary, size=s(20)),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
),
|
||||
],
|
||||
),
|
||||
bgcolor=colors.surface,
|
||||
border=ft.border.all(1, colors.border),
|
||||
border_radius=s(10),
|
||||
padding=ft.padding.symmetric(horizontal=s(12), vertical=s(10)),
|
||||
on_click=self._open_dialog,
|
||||
ink=True,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, val):
|
||||
self._value = val
|
||||
if val is None:
|
||||
self._value_text.value = self._hint_str
|
||||
self._value_text.color = colors.border
|
||||
self._value_text.italic = True
|
||||
else:
|
||||
for opt in self._options:
|
||||
if opt.key == val:
|
||||
self._value_text.value = opt.text
|
||||
self._value_text.color = colors.text_primary
|
||||
self._value_text.italic = False
|
||||
break
|
||||
if self.page:
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return self._options
|
||||
|
||||
@options.setter
|
||||
def options(self, opts):
|
||||
self._options = opts or []
|
||||
|
||||
@property
|
||||
def hint_text(self):
|
||||
return self._hint_str
|
||||
|
||||
@hint_text.setter
|
||||
def hint_text(self, val):
|
||||
self._hint_str = val
|
||||
if self._value is None:
|
||||
self._value_text.value = val
|
||||
if self.page:
|
||||
self.update()
|
||||
|
||||
def _open_dialog(self, e):
|
||||
search_field = ft.TextField(
|
||||
label="Поиск",
|
||||
hint_text="Введите для поиска...",
|
||||
prefix_icon=ft.Icons.SEARCH,
|
||||
autofocus=True,
|
||||
filled=True,
|
||||
bgcolor=colors.surface,
|
||||
border_color=colors.border,
|
||||
focused_border_color=colors.primary,
|
||||
border_radius=s(8),
|
||||
text_style=ft.TextStyle(color=colors.text_primary, size=s(14)),
|
||||
)
|
||||
items_list = ft.ListView(spacing=0, expand=True)
|
||||
|
||||
def _build_items(query: str = ""):
|
||||
q = query.lower()
|
||||
items_list.controls = [
|
||||
ft.ListTile(
|
||||
title=ft.Text(opt.text, color=colors.text_primary, size=s(13)),
|
||||
hover_color=colors.primary_light,
|
||||
on_click=lambda ev, o=opt: _select(o),
|
||||
)
|
||||
for opt in self._options
|
||||
if not q or q in opt.text.lower()
|
||||
]
|
||||
|
||||
def _on_search(ev):
|
||||
_build_items(search_field.value or "")
|
||||
items_list.update()
|
||||
|
||||
def _select(option):
|
||||
self._value = option.key
|
||||
self._value_text.value = option.text
|
||||
self._value_text.color = colors.text_primary
|
||||
self._value_text.italic = False
|
||||
dlg.open = False
|
||||
self.page.update()
|
||||
self.update()
|
||||
if self.on_select:
|
||||
self.on_select(type("_E", (), {"control": self})())
|
||||
|
||||
search_field.on_change = _on_search
|
||||
_build_items()
|
||||
|
||||
dlg = ft.AlertDialog(
|
||||
title=ft.Text(self._label_str, color=colors.text_primary, size=s(15), weight=ft.FontWeight.W_600),
|
||||
content=ft.Container(
|
||||
content=ft.Column(
|
||||
controls=[search_field, ft.Divider(color=colors.border), items_list],
|
||||
spacing=s(8),
|
||||
tight=True,
|
||||
),
|
||||
height=s(320),
|
||||
width=s(380),
|
||||
),
|
||||
bgcolor=colors.surface,
|
||||
shape=ft.RoundedRectangleBorder(radius=s(12)),
|
||||
)
|
||||
self.page.overlay.append(dlg)
|
||||
dlg.open = True
|
||||
self.page.update()
|
||||
|
||||
|
||||
def stat_card(title: str, value: str, icon: str, color: str) -> ft.Container:
|
||||
"""Карточка с одной цифрой для summary-панели."""
|
||||
return ft.Container(
|
||||
content=ft.Column(
|
||||
spacing=s(6),
|
||||
tight=True,
|
||||
controls=[
|
||||
ft.Row(
|
||||
spacing=s(8),
|
||||
controls=[
|
||||
ft.Icon(icon, color=color, size=s(20)),
|
||||
ft.Text(title, color=colors.text_secondary, size=s(12)),
|
||||
],
|
||||
),
|
||||
ft.Text(value, color=colors.text_primary, size=s(28), weight=ft.FontWeight.BOLD),
|
||||
],
|
||||
),
|
||||
bgcolor=colors.surface,
|
||||
border_radius=s(12),
|
||||
padding=ft.padding.all(s(16)),
|
||||
border=ft.border.all(1, colors.border),
|
||||
expand=True,
|
||||
)
|
||||
|
||||
|
||||
def chart_card(title: str, body: ft.Control) -> ft.Container:
|
||||
"""Обёртка-карточка для графика."""
|
||||
return ft.Container(
|
||||
content=ft.Column(
|
||||
spacing=s(12),
|
||||
controls=[
|
||||
ft.Text(title, color=colors.text_primary, size=s(15), weight=ft.FontWeight.W_600),
|
||||
body,
|
||||
],
|
||||
),
|
||||
bgcolor=colors.surface,
|
||||
border_radius=s(12),
|
||||
padding=ft.padding.all(s(16)),
|
||||
border=ft.border.all(1, colors.border),
|
||||
expand=True,
|
||||
)
|
||||
Reference in New Issue
Block a user