add user? new page? new web
This commit is contained in:
BIN
web/assets/favicon.png
Normal file
BIN
web/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 851 B |
BIN
web/assets/icons/IMG_4533.png
Normal file
BIN
web/assets/icons/IMG_4533.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
3
web/assets/icons/README
Normal file
3
web/assets/icons/README
Normal file
@@ -0,0 +1,3 @@
|
||||
# Положи сюда своё изображение с именем loading-animation.png
|
||||
# Это заменит мигающий логотип Flet при загрузке страницы.
|
||||
# Рекомендуемый размер: PNG, минимум 200x200 px.
|
||||
BIN
web/assets/icons/loading-animation.png
Normal file
BIN
web/assets/icons/loading-animation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -9,10 +9,31 @@ class clolors:
|
||||
|
||||
|
||||
|
||||
class TextField_input(ft.TextField):
|
||||
def __init__(self):
|
||||
class CastomTextField_input(ft.TextField):
|
||||
def __init__(self, on_change, on_submit, label="Ваше имя", hint_text="Введите текст...", prefix_icon=None, suffix_icon=None):
|
||||
super().__init__(
|
||||
|
||||
label=label,
|
||||
hint_text=hint_text,
|
||||
filled=True, # заливка фона
|
||||
bgcolor=clolors.ferst, # цвет заливки
|
||||
border_color=clolors.laer2, # цвет границы
|
||||
border_radius=12, # скругление углов
|
||||
label_style=ft.TextStyle(
|
||||
color=clolors.laer3,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
),
|
||||
hint_style=ft.TextStyle(
|
||||
color=clolors.laer1,
|
||||
italic=True,
|
||||
),
|
||||
text_style=ft.TextStyle(
|
||||
color=clolors.laer3,
|
||||
size=16,
|
||||
),
|
||||
prefix_icon=prefix_icon,
|
||||
suffix_icon=suffix_icon,
|
||||
on_change=on_change,
|
||||
on_submit=on_submit,
|
||||
)
|
||||
|
||||
class CastomButton(ft.Button):
|
||||
|
||||
50
web/info.py
50
web/info.py
@@ -1,7 +1,49 @@
|
||||
import os
|
||||
import httpx
|
||||
import flet as ft
|
||||
|
||||
class user():
|
||||
def __init__(self, page: ft.Page):
|
||||
pass
|
||||
API_URL = os.getenv("API_URL", "http://api:8000")
|
||||
_TOKEN_KEY = "auth_token"
|
||||
|
||||
#page.client_storage.set("token", "your_jwt_token_here")
|
||||
|
||||
class AuthService:
|
||||
"""Сервис авторизации: логин, логаут, проверка токена."""
|
||||
|
||||
def __init__(self, page: ft.Page):
|
||||
self._page = page
|
||||
|
||||
async def login(self, username: str, password: str) -> str | None:
|
||||
"""
|
||||
Отправляет запрос на POST /auth/login.
|
||||
Возвращает None при успехе, строку с ошибкой при неудаче.
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(base_url=API_URL, timeout=10) as client:
|
||||
resp = await client.post(
|
||||
"/auth/login",
|
||||
data={"username": username, "password": password},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
token = resp.json()["access_token"]
|
||||
self._page.session.set(_TOKEN_KEY, token)
|
||||
return None
|
||||
elif resp.status_code == 401:
|
||||
return "Неверный логин или пароль"
|
||||
else:
|
||||
return f"Ошибка сервера: {resp.status_code}"
|
||||
except httpx.ConnectError:
|
||||
return "Не удалось подключиться к серверу"
|
||||
except Exception as e:
|
||||
return f"Ошибка: {e}"
|
||||
|
||||
async def logout(self):
|
||||
"""Удаляет токен — пользователь разлогинен."""
|
||||
self._page.session.remove(_TOKEN_KEY)
|
||||
|
||||
async def get_token(self) -> str | None:
|
||||
"""Возвращает сохранённый токен или None если не авторизован."""
|
||||
return self._page.session.get(_TOKEN_KEY)
|
||||
|
||||
async def is_authenticated(self) -> bool:
|
||||
token = await self.get_token()
|
||||
return token is not None and token != ""
|
||||
|
||||
29
web/main.py
29
web/main.py
@@ -1,19 +1,36 @@
|
||||
import flet as ft
|
||||
from view import ViewsHendler
|
||||
import designer as ds
|
||||
from info import AuthService
|
||||
|
||||
async def main(page: ft.Page):
|
||||
page.title = "COPP"
|
||||
page.bgcolor = ds.clolors.background
|
||||
|
||||
auth = AuthService(page)
|
||||
_PUBLIC_ROUTES = {"/login"}
|
||||
|
||||
def route_change(e: ft.RouteChangeEvent):
|
||||
print("Route changed to:", page.route)
|
||||
page.run_task(_guarded_route_change, page.route)
|
||||
|
||||
async def _guarded_route_change(route: str):
|
||||
if route not in _PUBLIC_ROUTES and not await auth.is_authenticated():
|
||||
await page.push_route("/login")
|
||||
return
|
||||
page.views.clear()
|
||||
|
||||
view = ViewsHendler(page).get(page.route)
|
||||
if view: # проверяем, что view получен
|
||||
view = ViewsHendler(page).get(route)
|
||||
if view:
|
||||
page.views.append(view)
|
||||
else:
|
||||
await page.push_route("/main")
|
||||
page.update()
|
||||
|
||||
page.on_route_change = route_change
|
||||
|
||||
|
||||
await page.push_route("/main")
|
||||
|
||||
ft.run(main, port=80345, assets_dir="assets")
|
||||
|
||||
|
||||
# export_asgi_app=True — Flet отдаёт ASGI-объект вместо запуска встроенного сервера.
|
||||
# Uvicorn из CMD в Dockerfile подхватывает этот объект через «main:app».
|
||||
app = ft.run(main, export_asgi_app=True, assets_dir="assets")
|
||||
@@ -1,9 +1,10 @@
|
||||
import flet as ft
|
||||
from views import main_view
|
||||
from views import main_view, login_view
|
||||
|
||||
def ViewsHendler(page: ft.Page):
|
||||
return {
|
||||
"/main": main_view.MainView(),
|
||||
"/login": login_view.LoginView(page),
|
||||
"/about": ft.View(route="/about", controls=[ft.Text("О нас",color="#008000", size=20)]),
|
||||
"/contact": ft.View(route="/contact", controls=[ft.Text("Контакты",color="#FF0000", size=20)])
|
||||
}
|
||||
96
web/views/login_view.py
Normal file
96
web/views/login_view.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import flet as ft
|
||||
import designer as ds
|
||||
from info import AuthService
|
||||
|
||||
|
||||
class LoginView(ft.View):
|
||||
def __init__(self, page: ft.Page):
|
||||
self._auth = AuthService(page)
|
||||
|
||||
self._username = ft.TextField(
|
||||
label="Логин",
|
||||
hint_text="Введите логин...",
|
||||
filled=True,
|
||||
bgcolor=ds.clolors.ferst,
|
||||
border_color=ds.clolors.laer2,
|
||||
border_radius=12,
|
||||
prefix_icon=ft.Icons.PERSON,
|
||||
label_style=ft.TextStyle(color=ds.clolors.laer3, weight=ft.FontWeight.BOLD),
|
||||
text_style=ft.TextStyle(color=ds.clolors.laer3, size=16),
|
||||
)
|
||||
|
||||
self._password = ft.TextField(
|
||||
label="Пароль",
|
||||
hint_text="Введите пароль...",
|
||||
filled=True,
|
||||
bgcolor=ds.clolors.ferst,
|
||||
border_color=ds.clolors.laer2,
|
||||
border_radius=12,
|
||||
prefix_icon=ft.Icons.LOCK,
|
||||
password=True,
|
||||
can_reveal_password=True,
|
||||
label_style=ft.TextStyle(color=ds.clolors.laer3, weight=ft.FontWeight.BOLD),
|
||||
text_style=ft.TextStyle(color=ds.clolors.laer3, size=16),
|
||||
on_submit=lambda e: page.run_task(self._do_login),
|
||||
)
|
||||
|
||||
self._error = ft.Text("", color=ft.Colors.RED_400, size=14, visible=False)
|
||||
|
||||
self._btn = ds.CastomButton(
|
||||
text="Войти",
|
||||
on_click=lambda e: page.run_task(self._do_login),
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
route="/login",
|
||||
bgcolor=ds.clolors.background,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
vertical_alignment=ft.MainAxisAlignment.CENTER,
|
||||
)
|
||||
self.controls = [
|
||||
ft.Text("COPP", color=ds.clolors.ferst, size=30, weight=ft.FontWeight.BOLD),
|
||||
ft.Container(height=8),
|
||||
ft.Text("Вход в систему", color=ds.clolors.ferst, size=16),
|
||||
ft.Container(height=24),
|
||||
ft.Container(
|
||||
width=360,
|
||||
padding=ft.padding.all(24),
|
||||
bgcolor=ds.clolors.laer3,
|
||||
border_radius=16,
|
||||
content=ft.Column(
|
||||
spacing=16,
|
||||
controls=[
|
||||
self._username,
|
||||
self._password,
|
||||
self._error,
|
||||
self._btn,
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
async def _do_login(self):
|
||||
username = self._username.value.strip()
|
||||
password = self._password.value
|
||||
|
||||
if not username or not password:
|
||||
self._show_error("Заполните логин и пароль")
|
||||
return
|
||||
|
||||
self._btn.disabled = True
|
||||
self._error.visible = False
|
||||
self.update()
|
||||
|
||||
error = await self._auth.login(username, password)
|
||||
|
||||
if error:
|
||||
self._btn.disabled = False
|
||||
self._show_error(error)
|
||||
else:
|
||||
# Успешный вход — переходим на главную
|
||||
await self.page.push_route("/main")
|
||||
|
||||
def _show_error(self, message: str):
|
||||
self._error.value = message
|
||||
self._error.visible = True
|
||||
self.update()
|
||||
Reference in New Issue
Block a user