217 lines
8.7 KiB
Python
217 lines
8.7 KiB
Python
import flet as ft
|
||
import designer as d
|
||
import api_client as api
|
||
from views.dashboard import _admin_appbar, _sidebar
|
||
|
||
|
||
class ArticlesListView:
|
||
def __init__(self, page: ft.Page):
|
||
self.page = page
|
||
self._table_col = ft.Column(spacing=0, expand=True)
|
||
self._filter = "all"
|
||
|
||
def build_page_view(self) -> ft.View:
|
||
filter_tabs = ft.Tabs(
|
||
selected_index=0,
|
||
on_change=self._on_filter,
|
||
tabs=[
|
||
ft.Tab(text="Все"),
|
||
ft.Tab(text="Опубликовано"),
|
||
ft.Tab(text="Черновики"),
|
||
],
|
||
)
|
||
|
||
return ft.View(
|
||
route="/admin/articles",
|
||
bgcolor=d.BACKGROUND,
|
||
appbar=_admin_appbar(self.page, "Статьи"),
|
||
controls=[
|
||
ft.Row(
|
||
[
|
||
_sidebar(self.page, active="/admin/articles"),
|
||
ft.Container(
|
||
content=ft.Column(
|
||
[
|
||
ft.Row(
|
||
[
|
||
d.headline("Статьи", size=22),
|
||
ft.Row(
|
||
[
|
||
d.btn_icon(ft.icons.REFRESH, "Обновить список",
|
||
on_click=lambda e: self.page.run_task(self._load_articles)),
|
||
d.btn_primary(
|
||
"Новая статья",
|
||
icon=ft.icons.ADD,
|
||
on_click=lambda e: self._open_editor("new"),
|
||
),
|
||
],
|
||
spacing=8,
|
||
),
|
||
],
|
||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||
),
|
||
filter_tabs,
|
||
self._table_col,
|
||
],
|
||
expand=True,
|
||
scroll=ft.ScrollMode.AUTO,
|
||
spacing=16,
|
||
),
|
||
expand=True,
|
||
padding=24,
|
||
),
|
||
],
|
||
expand=True,
|
||
spacing=0,
|
||
)
|
||
],
|
||
)
|
||
|
||
def _open_editor(self, article_id: str):
|
||
token = self.page.session.get("token") or ""
|
||
url = f"/editor/{article_id}?t={token}"
|
||
self.page.launch_url(url, web_window_name="_blank")
|
||
|
||
async def load(self):
|
||
self.page.window.on_focus = lambda e: self.page.run_task(self._load_articles)
|
||
await self._load_articles()
|
||
|
||
async def _on_filter(self, e):
|
||
idx = e.control.selected_index
|
||
self._filter = ["all", "published", "draft"][idx]
|
||
await self._load_articles()
|
||
|
||
async def _load_articles(self):
|
||
token = self.page.session.get("token")
|
||
status = None if self._filter == "all" else self._filter
|
||
data = await api.admin_list_articles(token, limit=200, status=status)
|
||
items = data.get("items", []) if data else []
|
||
self._render(items)
|
||
|
||
def _render(self, items: list):
|
||
if not items:
|
||
self._table_col.controls = [
|
||
ft.Container(
|
||
content=d.empty_state(ft.icons.ARTICLE_OUTLINED, "Статей пока нет"),
|
||
alignment=ft.alignment.center,
|
||
padding=40,
|
||
)
|
||
]
|
||
self.page.update()
|
||
return
|
||
|
||
header = ft.Container(
|
||
content=ft.Row(
|
||
[
|
||
ft.Text("Заголовок", size=12, color=d.TEXT_SECONDARY, expand=3),
|
||
ft.Text("Категория", size=12, color=d.TEXT_SECONDARY, expand=1),
|
||
ft.Text("Статус", size=12, color=d.TEXT_SECONDARY, width=110),
|
||
ft.Text("Дата", size=12, color=d.TEXT_SECONDARY, width=110),
|
||
ft.Text("Действия", size=12, color=d.TEXT_SECONDARY, width=130),
|
||
],
|
||
spacing=8,
|
||
),
|
||
bgcolor=d.BACKGROUND,
|
||
padding=ft.padding.symmetric(horizontal=16, vertical=10),
|
||
border_radius=ft.BorderRadius(8, 8, 0, 0),
|
||
border=ft.Border(bottom=ft.BorderSide(1, d.DIVIDER)),
|
||
)
|
||
|
||
rows = [header] + [self._build_row(a) for a in items]
|
||
self._table_col.controls = [
|
||
ft.Container(
|
||
content=ft.Column(rows, spacing=0),
|
||
bgcolor=d.SURFACE,
|
||
border_radius=8,
|
||
border=ft.border.all(1, d.DIVIDER),
|
||
)
|
||
]
|
||
self.page.update()
|
||
|
||
def _build_row(self, article: dict) -> ft.Container:
|
||
pub_date = ""
|
||
if article.get("published_at"):
|
||
pub_date = article["published_at"][:10]
|
||
elif article.get("created_at"):
|
||
pub_date = article["created_at"][:10]
|
||
|
||
cat_name = article.get("category", {})
|
||
cat_name = (cat_name or {}).get("name", "—")
|
||
|
||
return ft.Container(
|
||
content=ft.Row(
|
||
[
|
||
ft.Text(
|
||
article["title"],
|
||
size=14,
|
||
expand=3,
|
||
max_lines=2,
|
||
overflow=ft.TextOverflow.ELLIPSIS,
|
||
),
|
||
ft.Text(cat_name, size=13, color=d.TEXT_SECONDARY, expand=1),
|
||
ft.Container(content=d.status_badge(article.get("status", "draft")), width=110),
|
||
ft.Text(pub_date, size=12, color=d.TEXT_SECONDARY, width=110),
|
||
ft.Row(
|
||
[
|
||
d.btn_icon(
|
||
ft.icons.EDIT_OUTLINED,
|
||
"Редактировать",
|
||
on_click=lambda e, aid=article["id"]: self._open_editor(aid),
|
||
color=d.PRIMARY,
|
||
),
|
||
*([
|
||
d.btn_icon(
|
||
ft.icons.PUBLISH,
|
||
"Опубликовать",
|
||
on_click=lambda e, aid=article["id"]: self.page.run_task(self._publish, aid),
|
||
color=d.SUCCESS,
|
||
)
|
||
] if article.get("status") == "draft" else []),
|
||
d.btn_icon(
|
||
ft.icons.DELETE_OUTLINE,
|
||
"Удалить",
|
||
on_click=lambda e, a=article: self._confirm_delete(a),
|
||
color=d.ERROR,
|
||
),
|
||
],
|
||
spacing=0,
|
||
width=130,
|
||
),
|
||
],
|
||
spacing=8,
|
||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||
),
|
||
padding=ft.padding.symmetric(horizontal=16, vertical=12),
|
||
border=ft.Border(bottom=ft.BorderSide(1, d.DIVIDER)),
|
||
ink=True,
|
||
)
|
||
|
||
def _confirm_delete(self, article: dict):
|
||
dlg = d.confirm_dialog(
|
||
self.page,
|
||
"Удалить статью?",
|
||
f'"{article["title"]}" будет удалена навсегда.',
|
||
lambda: self.page.run_task(self._delete, article["id"]),
|
||
)
|
||
self.page.dialog = dlg
|
||
dlg.open = True
|
||
self.page.update()
|
||
|
||
async def _delete(self, article_id: str):
|
||
token = self.page.session.get("token")
|
||
ok = await api.admin_delete_article(token, article_id)
|
||
if ok:
|
||
d.snack(self.page, "Статья удалена")
|
||
await self._load_articles()
|
||
else:
|
||
d.snack(self.page, "Ошибка удаления", error=True)
|
||
|
||
async def _publish(self, article_id: str):
|
||
token = self.page.session.get("token")
|
||
ok = await api.admin_publish_article(token, article_id)
|
||
if ok:
|
||
d.snack(self.page, "Статья опубликована")
|
||
await self._load_articles()
|
||
else:
|
||
d.snack(self.page, "Ошибка публикации", error=True)
|