224 lines
6.9 KiB
Python
224 lines
6.9 KiB
Python
"""
|
||
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, width: int | None = 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,
|
||
width=width or s(220),
|
||
)
|
||
|
||
@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
|
||
try:
|
||
if self.page:
|
||
self.update()
|
||
except RuntimeError:
|
||
pass
|
||
|
||
@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,
|
||
)
|